Last active
March 26, 2025 10:18
-
-
Save diegofigs/4800ed3733c43c371bb4fdcaeaeb4b63 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| ######################## | |
| # Don't trust, verify! # | |
| ######################## | |
| # @license GNU Affero General Public License v3.0 only | |
| # @author pcaversaccio | |
| # Check the Bash version compatibility. | |
| if [[ "$BASH_VERSINFO" -lt 4 ]]; then | |
| echo "Error: This script requires Bash 4.0 or higher." | |
| echo "Current version: $BASH_VERSION" | |
| echo "Please upgrade your Bash installation." | |
| echo "If you've already upgraded via Homebrew, try running:" | |
| echo "/opt/homebrew/bin/bash $0 $@" | |
| exit 1 | |
| fi | |
| # Enable strict error handling: | |
| # -E: Inherit `ERR` traps in functions and subshells. | |
| # -e: Exit immediately if a command exits with a non-zero status. | |
| # -u: Treat unset variables as an error and exit. | |
| # -o pipefail: Return the exit status of the first failed command in a pipeline. | |
| set -Eeuo pipefail | |
| # Enable debug mode if the environment variable `DEBUG` is set to `true`. | |
| if [[ "${DEBUG:-false}" == "true" ]]; then | |
| # Print each command before executing it. | |
| set -x | |
| fi | |
| # Set the terminal formatting constants. | |
| readonly GREEN="\e[32m" | |
| readonly RED="\e[31m" | |
| readonly UNDERLINE="\e[4m" | |
| readonly BOLD="\e[1m" | |
| readonly RESET="\e[0m" | |
| # Set the type hash constants. | |
| # => `keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");` | |
| # See: https://github.com/safe-global/safe-smart-account/blob/a0a1d4292006e26c4dbd52282f4c932e1ffca40f/contracts/Safe.sol#L54-L57. | |
| readonly DOMAIN_SEPARATOR_TYPEHASH="0x47e79534a245952e8b16893a336b85a3d9ea9fa8c573f3d803afb92a79469218" | |
| # => `keccak256("SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)");` | |
| # See: https://github.com/safe-global/safe-smart-account/blob/a0a1d4292006e26c4dbd52282f4c932e1ffca40f/contracts/Safe.sol#L59-L62. | |
| readonly SAFE_TX_TYPEHASH="0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8" | |
| # Define the supported networks from the Safe transaction service. | |
| # See https://docs.safe.global/core-api/transaction-service-supported-networks. | |
| declare -A -r API_URLS=( | |
| ["arbitrum"]="https://safe-transaction-arbitrum.safe.global" | |
| ["aurora"]="https://safe-transaction-aurora.safe.global" | |
| ["avalanche"]="https://safe-transaction-avalanche.safe.global" | |
| ["base"]="https://safe-transaction-base.safe.global" | |
| ["base-sepolia"]="https://safe-transaction-base-sepolia.safe.global" | |
| ["blast"]="https://safe-transaction-blast.safe.global" | |
| ["bsc"]="https://safe-transaction-bsc.safe.global" | |
| ["celo"]="https://safe-transaction-celo.safe.global" | |
| ["ethereum"]="https://safe-transaction-mainnet.safe.global" | |
| ["gnosis"]="https://safe-transaction-gnosis-chain.safe.global" | |
| ["gnosis-chiado"]="https://safe-transaction-chiado.safe.global" | |
| ["linea"]="https://safe-transaction-linea.safe.global" | |
| ["mantle"]="https://safe-transaction-mantle.safe.global" | |
| ["optimism"]="https://safe-transaction-optimism.safe.global" | |
| ["polygon"]="https://safe-transaction-polygon.safe.global" | |
| ["polygon-zkevm"]="https://safe-transaction-zkevm.safe.global" | |
| ["scroll"]="https://safe-transaction-scroll.safe.global" | |
| ["sepolia"]="https://safe-transaction-sepolia.safe.global" | |
| ["worldchain"]="https://safe-transaction-worldchain.safe.global" | |
| ["xlayer"]="https://safe-transaction-xlayer.safe.global" | |
| ["zksync"]="https://safe-transaction-zksync.safe.global" | |
| ) | |
| # Define the chain IDs of the supported networks from the Safe transaction service. | |
| declare -A -r CHAIN_IDS=( | |
| ["arbitrum"]="42161" | |
| ["aurora"]="1313161554" | |
| ["avalanche"]="43114" | |
| ["base"]="8453" | |
| ["base-sepolia"]="84532" | |
| ["blast"]="81457" | |
| ["bsc"]="56" | |
| ["celo"]="42220" | |
| ["ethereum"]="1" | |
| ["gnosis"]="100" | |
| ["gnosis-chiado"]="10200" | |
| ["linea"]="59144" | |
| ["mantle"]="5000" | |
| ["optimism"]="10" | |
| ["polygon"]="137" | |
| ["polygon-zkevm"]="1101" | |
| ["scroll"]="534352" | |
| ["sepolia"]="11155111" | |
| ["worldchain"]="480" | |
| ["xlayer"]="195" | |
| ["zksync"]="324" | |
| ) | |
| # Block Explorer API URLs | |
| declare -A -r EXPLORER_API_URLS=( | |
| ["arbitrum"]="https://api.etherscan.io/v2/api" | |
| ["aurora"]="https://explorer.mainnet.aurora.dev/api" | |
| ["avalanche"]="https://api.etherscan.io/v2/api" | |
| ["base"]="https://api.etherscan.io/v2/api" | |
| ["base-sepolia"]="https://api.etherscan.io/v2/api" | |
| ["blast"]="https://api.etherscan.io/v2/api" | |
| ["bsc"]="https://api.etherscan.io/v2/api" | |
| ["celo"]="https://api.etherscan.io/v2/api" | |
| ["ethereum"]="https://api.etherscan.io/v2/api" | |
| ["gnosis"]="https://api.etherscan.io/v2/api" | |
| ["linea"]="https://api.etherscan.io/v2/api" | |
| ["mantle"]="https://api.etherscan.io/v2/api" | |
| ["optimism"]="https://api.etherscan.io/v2/api" | |
| ["polygon"]="https://api.etherscan.io/v2/api" | |
| ["polygon-zkevm"]="https://api.etherscan.io/v2/api" | |
| ["scroll"]="https://api.etherscan.io/v2/api" | |
| ["sepolia"]="https://api.etherscan.io/v2/api" | |
| ["zksync"]="https://api.etherscan.io/v2/api" | |
| # Add other networks as needed | |
| ) | |
| # Utility function to display the usage information. | |
| usage() { | |
| cat <<EOF | |
| Usage: $0 [--help] [--list-networks] --network <network> --address <address> --nonce <nonce> | |
| Options: | |
| --help Display this help message | |
| --list-networks List all supported networks and their chain IDs | |
| --network <network> Specify the network (required) | |
| --address <address> Specify the Safe multisig address (required) | |
| --nonce <nonce> Specify the transaction nonce (required) | |
| Example: | |
| $0 --network ethereum --address 0x1234...5678 --nonce 42 | |
| EOF | |
| exit 1 | |
| } | |
| # Utility function to list all supported networks. | |
| list_networks() { | |
| echo "Supported Networks:" | |
| for network in "${!CHAIN_IDS[@]}"; do | |
| echo " $network (${CHAIN_IDS[$network]})" | |
| done | |
| exit 0 | |
| } | |
| # Utility function to print a section header. | |
| print_header() { | |
| local header=$1 | |
| if [[ -t 1 ]] && tput sgr0 >/dev/null 2>&1; then | |
| # Terminal supports formatting. | |
| printf "\n${UNDERLINE}%s${RESET}\n" "$header" | |
| else | |
| # Fallback for terminals without formatting support. | |
| printf "\n%s\n" "> $header:" | |
| fi | |
| } | |
| # Utility function to print a labelled value. | |
| print_field() { | |
| local label=$1 | |
| local value=$2 | |
| local empty_line=${3:-false} | |
| if [[ -t 1 ]] && tput sgr0 >/dev/null 2>&1; then | |
| # Terminal supports formatting. | |
| printf "%s: ${GREEN}%s${RESET}\n" "$label" "$value" | |
| else | |
| # Fallback for terminals without formatting support. | |
| printf "%s: %s\n" "$label" "$value" | |
| fi | |
| # Print an empty line if requested. | |
| if [[ "$empty_line" == "true" ]]; then | |
| printf "\n" | |
| fi | |
| } | |
| # Utility function to print the transaction data. | |
| print_transaction_data() { | |
| local address=$1 | |
| local to=$2 | |
| local data=$3 | |
| local message=$4 | |
| print_header "Transaction Data" | |
| print_field "Multisig address" "$address" | |
| print_field "To" "$to" | |
| print_field "Data" "$data" | |
| print_field "Encoded message" "$message" | |
| } | |
| # Utility function to format the hash (keep `0x` lowercase, rest uppercase). | |
| format_hash() { | |
| local hash=$1 | |
| local prefix="${hash:0:2}" | |
| local rest="${hash:2}" | |
| echo "${prefix,,}${rest^^}" | |
| } | |
| # Utility function to print the hash information. | |
| print_hash_info() { | |
| local domain_hash=$1 | |
| local message_hash=$2 | |
| local safe_tx_hash=$3 | |
| print_header "Hashes" | |
| print_field "Domain hash" "$(format_hash "$domain_hash")" | |
| print_field "Message hash" "$(format_hash "$message_hash")" | |
| print_field "Safe transaction hash" "$safe_tx_hash" | |
| } | |
| # Utility function to print the ABI-decoded transaction data. | |
| print_decoded_data() { | |
| local data_decoded=$1 | |
| if [[ "$data_decoded" == "0x" ]]; then | |
| print_field "Method" "0x (ETH Transfer)" | |
| print_field "Parameters" "[]" | |
| else | |
| method=$(echo "$data_decoded" | jq -r ".method") | |
| parameters=$(echo "$data_decoded" | jq -r ".parameters") | |
| print_field "Method" "$method" | |
| print_field "Parameters" "$parameters" | |
| # Check if the called function is sensitive and print a warning in bold. | |
| case "$method" in | |
| addOwnerWithThreshold | removeOwner | swapOwner | changeThreshold) | |
| echo | |
| echo -e "${BOLD}${RED}WARNING: The \"$method\" function modifies the owners or threshold of the Safe. Proceed with caution!${RESET}" | |
| ;; | |
| esac | |
| # Check for sensitive functions in nested transactions. | |
| echo "$parameters" | jq -c '.[] | .valueDecoded[]? | select(.dataDecoded != null)' | while read -r nested_param; do | |
| nested_method=$(echo "$nested_param" | jq -r ".dataDecoded.method") | |
| if [[ "$nested_method" =~ ^(addOwnerWithThreshold|removeOwner|swapOwner|changeThreshold)$ ]]; then | |
| echo | |
| echo -e "${BOLD}${RED}WARNING: The \"$nested_method\" function modifies the owners or threshold of the Safe! Proceed with caution!${RESET}" | |
| fi | |
| done | |
| fi | |
| } | |
| # Utility function to calculate the domain and message hashes. | |
| calculate_hashes() { | |
| local chain_id=$1 | |
| local address=$2 | |
| local to=$3 | |
| local value=$4 | |
| local data=$5 | |
| local operation=$6 | |
| local safe_tx_gas=$7 | |
| local base_gas=$8 | |
| local gas_price=$9 | |
| local gas_token=${10} | |
| local refund_receiver=${11} | |
| local nonce=${12} | |
| local data_decoded=${13} | |
| # Calculate the domain hash. | |
| local domain_hash=$(chisel eval "keccak256(abi.encode($DOMAIN_SEPARATOR_TYPEHASH, $chain_id, $address))" | | |
| awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') | |
| # Calculate the data hash. | |
| # The dynamic value `bytes` is encoded as a `keccak256` hash of its content. | |
| # See: https://eips.ethereum.org/EIPS/eip-712#definition-of-encodedata. | |
| local data_hashed=$(cast keccak "$data") | |
| # Encode the message. | |
| local message=$(cast abi-encode "SafeTxStruct(bytes32,address,uint256,bytes32,uint8,uint256,uint256,uint256,address,address,uint256)" \ | |
| "$SAFE_TX_TYPEHASH" \ | |
| "$to" \ | |
| "$value" \ | |
| "$data_hashed" \ | |
| "$operation" \ | |
| "$safe_tx_gas" \ | |
| "$base_gas" \ | |
| "$gas_price" \ | |
| "$gas_token" \ | |
| "$refund_receiver" \ | |
| "$nonce") | |
| # Calculate the message hash. | |
| local message_hash=$(cast keccak "$message") | |
| # Calculate the Safe transaction hash. | |
| local safe_tx_hash=$(chisel eval "keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), bytes32($domain_hash), bytes32($message_hash)))" | | |
| awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') | |
| # Print the retrieved transaction data. | |
| print_transaction_data "$address" "$to" "$data" "$message" | |
| # Print the ABI-decoded transaction data. | |
| print_decoded_data "$data_decoded" | |
| # Print the results with the same formatting for "Domain hash" and "Message hash" as a Ledger hardware device. | |
| print_hash_info "$domain_hash" "$message_hash" "$safe_tx_hash" | |
| } | |
| # Utility function to fetch contract ABI from Explorer | |
| fetch_contract_abi() { | |
| local endpoint="$1" | |
| local contract_address="$2" | |
| local response | |
| response=$(curl -s "${endpoint}&address=${contract_address}") | |
| # Check if the ABI retrieval was successful | |
| if [[ $(echo "$response" | jq -r ".status") != "1" ]]; then | |
| echo | |
| echo -e "${RED}Error: Failed to fetch ABI for address ${contract_address}.${RESET}" >&2 | |
| exit 1 | |
| fi | |
| echo "$response" | jq -r ".result" | |
| } | |
| match_abi_function() { | |
| local abi="$1" | |
| local method_id="$2" | |
| echo "$abi" | jq -c ".[] | select(.type == \"function\")" | while read -r func; do | |
| local signature | |
| signature=$(echo "$func" | jq -r ".name + \"(\" + (.inputs | map(.type) | join(\",\")) + \")\"") | |
| local computed_id | |
| computed_id=$(echo -n "$signature" | cast keccak | cut -c1-10) | |
| if [[ "$computed_id" == "$method_id" ]]; then | |
| echo "$func" | |
| break | |
| fi | |
| done | |
| } | |
| print_explorer_data() { | |
| local data="$1" | |
| local abi="$2" | |
| local method_id="$3" | |
| local matched_function="$4" | |
| print_header "Explorer Data" | |
| print_field "Method ID" "$method_id" | |
| if [[ -n "$matched_function" ]]; then | |
| local matched_signature=$(echo "$matched_function" | jq -r ".name + \"(\" + (.inputs | map(.type) | join(\",\")) + \")\"") | |
| print_field "Resolved Method" "$matched_signature" | |
| # Extract the encoded parameters | |
| local encoded_params | |
| encoded_params="0x${data:10}" | |
| # Decode the parameters using `cast abi-decode` with the `--input` flag | |
| local decoded_params | |
| decoded_params=$(cast abi-decode --input "$matched_signature" "$encoded_params" 2>/dev/null) | |
| # Log raw decoded_params for debugging | |
| # echo "Raw Decoded Parameters: $decoded_params" >&2 | |
| if [[ -n "$decoded_params" ]]; then | |
| echo "Decoded Parameters:" | |
| # Extract parameter names from the ABI | |
| local param_names | |
| param_names=($(echo "$matched_function" | jq -r ".inputs | map(.name) | @sh" | tr -d "'")) | |
| local i=0 | |
| # Normalize input by removing empty lines and process defensively | |
| echo "$decoded_params" | sed '/^$/d' | while IFS= read -r param; do | |
| # Get the corresponding parameter name | |
| local param_name=${param_names[i]:-"Unnamed"} | |
| # Print the parameter with its name | |
| print_field "[$i] $param_name" "$param" | |
| ((i++)) || true # Ensure the loop doesn't fail on errors | |
| done | |
| else | |
| print_field "Decoded Parameters" "Unable to decode parameters (invalid data or ABI)" | |
| fi | |
| else | |
| print_field "Resolved Method" "Unknown method" | |
| print_field "Decoded Parameters" "Unable to decode parameters (method not in ABI)" | |
| fi | |
| } | |
| # Utility function to validate the network name. | |
| validate_network() { | |
| local network="$1" | |
| if [[ -z "${API_URLS[$network]:-}" || -z "${CHAIN_IDS[$network]:-}" ]]; then | |
| echo -e "${BOLD}${RED}Invalid network name: \"${network}\"${RESET}\n" >&2 | |
| calculate_safe_tx_hashes --list-networks >&2 | |
| exit 1 | |
| fi | |
| } | |
| # Utility function to retrieve the API URL of the selected network. | |
| get_api_url() { | |
| echo "${API_URLS[$1]:-Invalid network}" || exit 1 | |
| } | |
| # Utility function to retrieve the API URL of the selected network. | |
| get_explorer_url() { | |
| echo "${EXPLORER_API_URLS[$1]:-Invalid network}" || exit 1 | |
| } | |
| # Utility function to retrieve the chain ID of the selected network. | |
| get_chain_id() { | |
| echo "${CHAIN_IDS[$1]:-Invalid network}" || exit 1 | |
| } | |
| # Utility function to validate the multisig address. | |
| validate_address() { | |
| local address="$1" | |
| if [[ -z "$address" || ! "$address" =~ ^0x[a-fA-F0-9]{40}$ ]]; then | |
| echo -e "${RED}Invalid Ethereum address format: \"${address}\"${RESET}" >&2 | |
| exit 1 | |
| fi | |
| } | |
| # Utility function to validate the transaction nonce. | |
| validate_nonce() { | |
| local nonce="$1" | |
| if [[ -z "$nonce" || ! "$nonce" =~ ^[0-9]+$ ]]; then | |
| echo -e "${RED}Invalid nonce value: \"${nonce}\". Must be a non-negative integer!${RESET}" >&2 | |
| exit 1 | |
| fi | |
| } | |
| # Safe Transaction Hashes Calculator | |
| # This function orchestrates the entire process of calculating the Safe transaction hashes: | |
| # 1. Parses command-line arguments (`network`, `address`, `nonce`). | |
| # 2. Validates that all required parameters are provided. | |
| # 3. Retrieves the API URL and chain ID for the specified network. | |
| # 4. Constructs the API endpoint URL. | |
| # 5. Fetches the transaction data from the Safe transaction service API. | |
| # 6. Extracts the relevant transaction details from the API response. | |
| # 7. Calls the `calculate_hashes` function to compute and display the results. | |
| calculate_safe_tx_hashes() { | |
| local network="" address="" nonce="" apikey="" | |
| # Parse the command line arguments. | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --help) usage ;; | |
| --network) | |
| network="$2" | |
| shift 2 | |
| ;; | |
| --address) | |
| address="$2" | |
| shift 2 | |
| ;; | |
| --nonce) | |
| nonce="$2" | |
| shift 2 | |
| ;; | |
| --apikey) | |
| apikey="$2" | |
| shift 2 | |
| ;; | |
| --list-networks) list_networks ;; | |
| *) | |
| echo "Unknown option: $1" >&2 | |
| usage | |
| ;; | |
| esac | |
| done | |
| # Validate if the required parameters have the correct format. | |
| ! validate_network "$network" || ! validate_address "$address" || ! validate_nonce "$nonce" | |
| # Check if the required parameters are provided. | |
| [ -z "$network" -o -z "$address" -o -z "$nonce" ] && usage | |
| # Get the API URL and chain ID for the specified network. | |
| local api_url=$(get_api_url "$network") | |
| local chain_id=$(get_chain_id "$network") | |
| local endpoint="${api_url}/api/v1/safes/${address}/multisig-transactions/?nonce=${nonce}" | |
| # Fetch the transaction data from the API. | |
| local response=$(curl -s "$endpoint") | |
| local count=$(echo "$response" | jq '.count') | |
| local idx=0 | |
| # Inform the user that no transactions are available for the specified nonce. | |
| if [[ $count -eq 0 ]]; then | |
| echo "$(tput setaf 3)No transaction is available for this nonce!$(tput setaf 0)" | |
| exit 0 | |
| # Notify the user about multiple transactions with identical nonce values and prompt for user input. | |
| elif [[ $count -gt 1 ]]; then | |
| cat <<EOF | |
| $(tput setaf 3)Several transactions with identical nonce values have been detected. | |
| This occurrence is normal if you are deliberately replacing an existing transaction. | |
| However, if your Safe interface displays only a single transaction, this could indicate | |
| potential irregular activity requiring your attention.$(tput sgr0) | |
| Kindly specify the transaction's array value (available range: 0-$((${count} - 1))). | |
| You can find the array values at the following endpoint: | |
| $(tput setaf 2)$endpoint$(tput sgr0) | |
| Please enter the index of the array: | |
| EOF | |
| while true; do | |
| read -r idx | |
| # Validate if user input is a number. | |
| if ! [[ $idx =~ ^[0-9]+$ ]]; then | |
| echo "$(tput setaf 1)Error: Please enter a valid number!$(tput sgr0)" | |
| continue | |
| fi | |
| array_value=$(echo "$response" | jq ".results[$idx]") | |
| if [[ $array_value == null ]]; then | |
| echo "$(tput setaf 1)Error: No transaction found at index $idx. Please try again.$(tput sgr0)" | |
| continue | |
| fi | |
| printf "\n" | |
| break | |
| done | |
| fi | |
| local to=$(echo "$response" | jq -r ".results[$idx].to // \"0x0000000000000000000000000000000000000000\"") | |
| local value=$(echo "$response" | jq -r ".results[$idx].value // \"0\"") | |
| local data=$(echo "$response" | jq -r ".results[$idx].data // \"0x\"") | |
| local operation=$(echo "$response" | jq -r ".results[$idx].operation // \"0\"") | |
| local safe_tx_gas=$(echo "$response" | jq -r ".results[$idx].safeTxGas // \"0\"") | |
| local base_gas=$(echo "$response" | jq -r ".results[$idx].baseGas // \"0\"") | |
| local gas_price=$(echo "$response" | jq -r ".results[$idx].gasPrice // \"0\"") | |
| local gas_token=$(echo "$response" | jq -r ".results[$idx].gasToken // \"0x0000000000000000000000000000000000000000\"") | |
| local refund_receiver=$(echo "$response" | jq -r ".results[$idx].refundReceiver // \"0x0000000000000000000000000000000000000000\"") | |
| local nonce=$(echo "$response" | jq -r ".results[$idx].nonce // \"0\"") | |
| local data_decoded=$(echo "$response" | jq -r ".results[$idx].dataDecoded // \"0x\"") | |
| # Calculate and display the hashes. | |
| echo "===================================" | |
| echo "= Selected Network Configurations =" | |
| echo -e "===================================\n" | |
| print_field "Network" "$network" | |
| print_field "Chain ID" "$chain_id" true | |
| echo "========================================" | |
| echo "= Transaction Data and Computed Hashes =" | |
| echo "========================================" | |
| calculate_hashes "$chain_id" \ | |
| "$address" \ | |
| "$to" \ | |
| "$value" \ | |
| "$data" \ | |
| "$operation" \ | |
| "$safe_tx_gas" \ | |
| "$base_gas" \ | |
| "$gas_price" \ | |
| "$gas_token" \ | |
| "$refund_receiver" \ | |
| "$nonce" \ | |
| "$data_decoded" | |
| # Get the API URL for the specified network. | |
| local explorer_url=$(get_explorer_url "$network") | |
| local explorer_endpoint="${explorer_url}?chainid=${chain_id}&apikey=${apikey}&module=contract&action=getabi" | |
| # Fetch and parse the ABI for the contract address | |
| local abi=$(fetch_contract_abi "$explorer_endpoint" "$to") | |
| # Extract the method ID (first 4 bytes of the data) | |
| local method_id="${data:0:10}" | |
| # Match the method ID to a function in the ABI | |
| local matched_function=$(match_abi_function "$abi" "$method_id") | |
| # Print ABI information from explorer. | |
| print_explorer_data "$data" "$abi" "$method_id" "$matched_function" | |
| } | |
| calculate_safe_tx_hashes "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment