Custom Scripts

OpenZeppelin Monitor allows you to implement custom scripts for additional filtering of monitor matches and custom notification handling.

Security Risk: Only run scripts that you trust and fully understand. Malicious scripts can harm your system or expose sensitive data. Always review script contents and verify their source before execution.

Custom Filter Scripts

Custom filter scripts allow you to apply additional conditions to matches detected by the monitor. This helps you refine the alerts you receive based on criteria specific to your use case.

Implementation Guide

  1. Create a script in one of the supported languages:

    • Bash

    • Python

    • JavaScript

  2. Your script will receive a JSON object with the following structure:

// Example for EVM MATCH
{
  "args": ["--verbose"],
  "monitor_match": {
    "EVM": {
      "matched_on": {
        "events": [],
        "functions": [
          {
            "expression": null,
            "signature": "transfer(address,uint256)"
          }
        ],
        "transactions": [
          {
            "expression": null,
            "status": "Success"
          }
        ]
      },
      "matched_on_args": {
        "events": null,
        "functions": [
          {
            "args": [
              {
                "indexed": false,
                "kind": "address",
                "name": "to",
                "value": "0x94d953b148d4d7143028f397de3a65a1800f97b3"
              },
              {
                "indexed": false,
                "kind": "uint256",
                "name": "value",
                "value": "434924400"
              }
            ],
            "hex_signature": "a9059cbb",
            "signature": "transfer(address,uint256)"
          }
        ]
      },
      "monitor": {
        "addresses": [
          {
            "abi": null,
            "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
          }
        ],
        "match_conditions": {
          "events": [
            {
              "expression": "value > 10000000000",
              "signature": "Transfer(address,address,uint256)"
            }
          ],
          "functions": [
            {
              "expression": null,
              "signature": "transfer(address,uint256)"
            }
          ],
          "transactions": [
            {
              "expression": null,
              "status": "Success"
            }
          ]
        },
        "name": "Large Transfer of USDC Token",
        "networks": ["ethereum_mainnet"],
        "paused": false,
        "trigger_conditions": [
          {
            "arguments": ["--verbose"],
            "language": "Bash",
            "script_path": "./config/filters/evm_filter_block_number.sh",
            "timeout_ms": 1000
          }
        ],
        "triggers": ["evm_large_transfer_usdc_script"]
      },
      "receipt": {
        "blockHash": "0x...",
        "blockNumber": "0x...",
        "contractAddress": null,
        "cumulativeGasUsed": "0x...",
        "effectiveGasPrice": "0x...",
        "from": "0x...",
        "gasUsed": "0xb068",
        "status": "0x1",
        "to": "0x...",
        "transactionHash": "0x...",
        "transactionIndex": "0x1fc",
        "type": "0x2"
      },
      "transaction": {
        "accessList": [],
        "blockHash": "0x...",
        "blockNumber": "0x1506545",
        "chainId": "0x1",
        "from": "0x...",
        "gas": "0x7a120",
        "gasPrice": "0x...",
        "hash": "0x...",
        "maxFeePerGas": "0x...",
        "maxPriorityFeePerGas": "0x...",
        "nonce": "0x14779f",
        "to": "0x...",
        "transactionIndex": "0x...",
        "type": "0x2",
        "value": "0x0"
      }
    }
  }
}

// Example for STELLAR MATCH
{
  "args": ["--verbose"],
  "monitor_match": {
    "Stellar": {
      "monitor": {
        "name": "Large Swap By Dex",
        "networks": ["stellar_mainnet"],
        "paused": false,
        "addresses": [
          {
            "address": "GCXYK...",
            "abi": null
          }
        ],
        "match_conditions": {
          "functions": [
            {
              "signature": "swap(Address,U32,U32,U128,U128)",
              "expression": "4 > 1000000000"
            }
          ],
          "events": [],
          "transactions": []
        },
        "trigger_conditions": [
          {
            "arguments": ["--verbose"],
            "language": "Bash",
            "script_path": "./config/filters/stellar_filter_block_number.sh",
            "timeout_ms": 1000
          }
        ],
        "triggers": ["stellar_large_transfer_usdc_script"]
      },
      "transaction": {
        "status": "SUCCESS",
        "txHash": "2b5a0c...",
        "applicationOrder": 3,
        "feeBump": false,
        "envelopeXdr": "AAAAAA...",
        "envelopeJson": {
          "type": "ENVELOPE_TYPE_TX",
          "tx": {/* transaction details */}
        },
        "resultXdr": "AAAAAA...",
        "resultJson": {/* result details */},
        "resultMetaXdr": "AAAAAA...",
        "resultMetaJson": {/* metadata details */},
        "diagnosticEventsXdr": ["AAAAAA..."],
        "diagnosticEventsJson": [{/* event details */}],
        "ledger": 123456,
        "createdAt": 1679644800,
        "decoded": {
          "envelope": {/* decoded envelope */},
          "result": {/* decoded result */},
          "meta": {/* decoded metadata */}
        }
      },
      "ledger": {
        "hash": "abc1...",
        "sequence": 123456,
        "ledgerCloseTime": "2024-03-20T10:00:00Z",
        "headerXdr": "AAAAAA...",
        "headerJson": {/* header details */},
        "metadataXdr": "AAAAAA...",
        "metadataJSON": {/* metadata details */}
      },
      "matched_on": {
        "functions": [
          {
            "signature": "swap(Address,U32,U32,U128,U128)",
            "expression": "4 > 1000000000"
          }
        ],
        "events": [],
        "transactions": []
      },
      "matched_on_args": {
        "functions": [],
        "events": null
      }
    }
  }
}

Script Output Requirements

  • Your script should print a boolean value indicating whether the match should be filtered.

  • Print true if the match should be filtered out (not trigger an alert).

  • Print false if the match should be processed (trigger an alert).

  • Only the last printed line will be considered for evaluation.

Example Filter Script (Bash)

#!/bin/bash

main() {
    # Read JSON input from stdin
    input_json=$(cat)

    # Parse arguments from the input JSON and initialize verbose flag
    verbose=false
    args=$(echo "$input_json" | jq -r '.args[]? // empty')
    if [ ! -z "$args" ]; then
        while IFS= read -r arg; do
            if [ "$arg" = "--verbose" ]; then
                verbose=true
                echo "Verbose mode enabled"
            fi
        done <<< "$args"
    fi

    # Extract the monitor match data from the input
    monitor_data=$(echo "$input_json" | jq -r '.monitor_match')

    if [ "$verbose" = true ]; then
        echo "Input JSON received:"
    fi

    # Extract blockNumber from the EVM receipt or transaction
    block_number_hex=$(echo "$monitor_data" | jq -r '.EVM.transaction.blockNumber' || echo "")

    # Validate that block_number_hex is not empty
    if [ -z "$block_number_hex" ]; then
        echo "Invalid JSON or missing blockNumber"
        echo "false"
        exit 1
    fi

    # Remove 0x prefix if present and clean the string
    block_number_hex=$(echo "$block_number_hex" | tr -d '\n' | tr -d ' ')
    block_number_hex=${block_number_hex#0x}

    if [ "$verbose" = true ]; then
        echo "Extracted block number (hex): $block_number_hex"
    fi

    # Convert hex to decimal with error checking
    if ! block_number=$(printf "%d" $((16#${block_number_hex})) 2>/dev/null); then
        echo "Failed to convert hex to decimal"
        echo "false"
        exit 1
    fi

    if [ "$verbose" = true ]; then
        echo "Converted block number (decimal): $block_number"
    fi

    # Check if even or odd using modulo
    is_even=$((block_number % 2))

    if [ $is_even -eq 0 ]; then
        echo "Block number $block_number is even"
        echo "Verbose mode: $verbose"
        echo "true"
        exit 0
    else
        echo "Block number $block_number is odd"
        echo "Verbose mode: $verbose"
        echo "false"
        exit 0
    fi
}

# Call main function
main

This example script filters EVM transactions based on their block number:

  • Returns true (filter out) for transactions in even-numbered blocks

  • Returns false (allow) for transactions in odd-numbered blocks

  • Accepts a --verbose flag for detailed logging

Integration

Integrate your custom filter script with the monitor by following the configuration guidelines.

Trigger conditions are executed sequentially based on their position in the trigger conditions array. Every filter must return false for the match to be included and are only considered if they were executed successfully.

Custom Notification Scripts

Custom notification scripts allow you to define how alerts are delivered when specific conditions are met. This can include sending alerts to different channels or formatting notifications in a particular way.

Implementation Guide

  1. Create a script in one of the supported languages:

    • Bash

    • Python

    • JavaScript

  2. Your script will receive the same JSON input format as filter scripts

Script Output Requirements

  • A non-zero exit code indicates an error occurred

  • Error messages should be written to stderr

  • A zero exit code indicates successful execution

Example Notification Script (Bash)

#!/bin/bash

main() {
    # Read JSON input from stdin
    input_json=$(cat)

    # Parse arguments from the input JSON and initialize verbose flag
    verbose=false
    args=$(echo "$input_json" | jq -r '.args[]? // empty')
    if [ ! -z "$args" ]; then
        while IFS= read -r arg; do
            if [ "$arg" = "--verbose" ]; then
                verbose=true
                echo "Verbose mode enabled"
            fi
        done <<< "$args"
    fi

    # Extract the monitor match data from the input
    monitor_data=$(echo "$input_json" | jq -r '.monitor_match')

    # Validate input
    if [ -z "$input_json" ]; then
        echo "No input JSON provided"
        exit 1
    fi

    # Validate JSON structure
    if ! echo "$input_json" | jq . >/dev/null 2>&1; then
        echo "Invalid JSON input"
        exit 1
    fi

    if [ "$verbose" = true ]; then
        echo "Input JSON received:"
        echo "$input_json" | jq '.'
        echo "Monitor match data:"
        echo "$monitor_data" | jq '.'
    fi

    # Process args if they exist
    args_data=$(echo "$input_json" | jq -r '.args')
    if [ "$args_data" != "null" ]; then
        echo "Args: $args_data"
    fi

    # If we made it here, everything worked
    echo "Verbose mode: $verbose"
    # return a non zero exit code and an error message
    echo "Error: This is a test error" >&2
    exit 1
}

# Call main function
main

This example demonstrates how to:

  • Process the input JSON data

  • Handle verbose mode for debugging

  • Return error messages via stderr

  • Set appropriate exit codes

Integration

Integrate your custom notification script with the triggers by following the configuration guidelines.

Performance Considerations

  • File descriptor limits: Each script execution requires file descriptors for stdin, stdout, and stderr

    • Ensure your system allows at least 2,048 open file descriptors

    • Check your current limit on Unix-based systems with ulimit -n

    • Temporarily increase the limit with ulimit -n 2048

    • For permanent changes, modify /etc/security/limits.conf or equivalent for your system

  • Script timeout: Configure appropriate timeout values in your trigger conditions to prevent long-running scripts from blocking the pipeline

    • The timeout_ms parameter controls how long a script can run before being terminated

  • Resource usage: Complex scripts may consume significant CPU or memory resources

    • Consider optimizing resource-intensive operations in your scripts

    • Monitor system performance during high-volume periods

  • Script reloading: Since scripts are loaded at startup, any modifications to script files require restarting the monitor to take effect