Skip to content

Instantly share code, notes, and snippets.

@killerstorm
Last active March 20, 2025 00:53
Show Gist options
  • Save killerstorm/6f09bcbb4d497a13a6d0c29331c2cc31 to your computer and use it in GitHub Desktop.
Save killerstorm/6f09bcbb4d497a13a6d0c29331c2cc31 to your computer and use it in GitHub Desktop.

Okay, let's break down that line of code and its significance within the useLoadTxHistory.ts file. We'll cover the method itself, the parameters used, what the returned logs represent, and some areas for further investigation.

Detailed Explanation of the Code

  • safeContract.queryFilter(safeContract.filters.ExecutionSuccess(), 0, 'latest')

    • safeContract: This is an instance of an ethers.js Contract object (specifically, a Gnosis_safe contract instance). It represents a connection to a specific Safe smart contract deployed on the blockchain, as defined in eternalsafe/src/utils/safe-versions.ts.
      • How safeContract is obtained: The safeContract is derived by combining the Safe's address, the Safe's version as retrieved from the Safe Info from the Safe Gateway, and the current web3 provider from /src/hooks/wallets/web3.ts. The version will help retrieve the specific ABI from /src/utils/safe-versions.ts. This all happens in eternalsafe/src/hooks/loadables/useLoadTxHistory.ts right before the line in question. getSafeContract() is a helper function in /src/utils/safe-versions.ts that returns the conneced contract. See also /src/services/contracts/safeContracts.ts for other safe contract retrieval.
    • queryFilter(): This is a method provided by ethers.js Contract objects. It's used to query the Ethereum blockchain for specific events emitted by the contract. In this case, it's filtering for ExecutionSuccess events. Critically, this relies on the JSON-RPC provider to access the blockchain data.
      • JSON-RPC Under the Hood: Under the hood, queryFilter translates into one or more JSON-RPC calls to the Ethereum node you're connected to. For example, it might use the eth_getLogs method (or equivalent) to retrieve the matching logs. The provider established in /src/hooks/wallets/web3.ts determines the specific endpoint and authentication method to use.
    • safeContract.filters.ExecutionSuccess(): This generates a filter object that is specific to the ExecutionSuccess event defined in the Safe contract's ABI (Application Binary Interface). The ABI defines all the functions and events of a smart contract. The filters property is automatically created by ethers.js based on the contract's ABI. The ExecutionSuccess event is defined in the Safe's contract interface. You can see where the Gnosis_safeInterface is defined in eternalsafe/src/types/contracts/@safe-global/safe-deployments/dist/assets/v1.3.0/Gnosis_safe.ts.
      • The ExecutionSuccess event signifies that a transaction submitted to the Safe has been successfully executed on the blockchain. It emits data relevant to the transaction, such as the transaction hash and the amount of fees paid.
    • 0: This is the fromBlock parameter, indicating the block number to start the search from. 0 signifies the genesis block (the very first block) of the blockchain. Therefore, it starts looking at all blocks.
    • 'latest': This is the toBlock parameter, indicating the block number to end the search at. 'latest' tells the provider to search up to the most recently mined block.

What the Logs Represent

The logs variable will contain an array of Event objects (from ethers.js). Each Event object represents a single ExecutionSuccess event that occurred on the Safe contract. The structure of each log entry typically includes:

  • blockNumber: The block number in which the event was mined.
  • transactionHash: The hash of the transaction that triggered the event.
  • logIndex: The index of the log within the block.
  • args: An object containing the data emitted by the ExecutionSuccess event, as defined in the Safe's ABI (e.g., txHash which is the internal Safe transaction hash bytes32 and the payment which is the amount of fees paid for execution)
  • A decoded executor parameter from parsing the transaction thanks to safeContract.interface.decodeFunctionData('execTransaction', tx.data).

Processing the Logs

The subsequent code in useLoadTxHistory.ts then iterates through these logs and transforms each Event object into a more user-friendly TxHistoryItem object, populating fields like txId, txHash, safeTxHash, timestamp, executor, and decodedTxData. This involves:

  1. Fetching Block Timestamp: It retrieves the timestamp of the block in which the event occurred using provider.getBlock(log.blockNumber).
  2. Retrieving Transaction: It retrieves the transaction that triggered the event using provider.getTransaction(log.transactionHash). This allows fetching information that's not directly included in the event log.
  3. Parsing Transaction data: It uses the same contract ABI to attempt to decode the transaction data field by calling safeContract.interface.decodeFunctionData('execTransaction', tx.data). This reveals the details of the Safe transaction allowing easy access to the recipients, amounts, and operation types involved.
  4. Constructing TxHistoryItem: It assembles all this information into a TxHistoryItem object.

Potential Issues and Further Investigation

  • Performance: Querying all events from block 0 to 'latest' can be very slow, especially for heavily used Safes or on chains with a lot of history. This could lead to slow loading times or even timeouts.
    • Solutions:
      • Indexed Events: Ensure the ExecutionSuccess event is indexed in the contract. This makes filtering much faster.
      • Pagination: Implement pagination. Fetch logs in batches (e.g., by fetching logs from the last 10,000 blocks at a time). Store the last processed block number and start the next query from there. However, the Gateway service already paginates.
      • Caching: Cache the transaction history data client-side (e.g., localStorage, IndexedDB) with appropriate invalidation strategies. But it makes sense to leverage what's already done by the Gateway.
      • Backend Solution: The best solution would be to rely on a dedicated backend service (like the Safe Transaction Service/Gateway) to provide the transaction history. These services are designed to efficiently index and serve this data. The application does use @safe-global/safe-gateway-typescript-sdk which can be configured to handle tx queues.
  • JSON-RPC Provider Limitations: Public JSON-RPC providers often have rate limits or other restrictions. If the application exceeds those limits, it can lead to errors.
    • Solutions:
      • Use a more robust JSON-RPC provider: Consider using a paid provider like Infura, Alchemy, or QuickNode.
      • Retry Logic: Implement retry logic with exponential backoff when API errors occur.
      • Fallback Providers: Configure multiple JSON-RPC providers and switch to a different provider if one is failing. The application does use useMultiWeb3ReadOnly so this might already be in place.
  • Reorgs (Chain Reorganizations): Ethereum can sometimes experience chain reorganizations, where blocks are removed from the chain and replaced with a different version. This can cause inconsistencies in the transaction history.
    • Solutions:
      • Confirmations: Only consider transactions confirmed after a certain number of blocks to reduce the risk of reorgs.
      • Regularly refresh the transaction history: Re-fetch the history periodically to ensure it's consistent with the current state of the blockchain. This doesn't seem to be implemented and the transactions could become stale.
  • Contract ABI: If the contract ABI used by ethers.js doesn't exactly match the version of the Safe contract deployed on the blockchain, decoding errors can occur.
    • Solutions:
      • Dynamic ABI fetching: The /src/hooks/coreSDK/useInitSafeCoreSDK.ts logic already confirms that chainId and address exist, and throws an error if nothing is set on storage slot 0.

In summary, the queryFilter call is the core mechanism for retrieving transaction history. Optimizing and handling potential issues with this call is critical for the performance and reliability of the application. The current implementation doesn't account for pagination which is done by the Gateway already, so this could lead to performance issues. Relying on the gateway SDK is ideal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment