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, aGnosis_safe
contract instance). It represents a connection to a specific Safe smart contract deployed on the blockchain, as defined ineternalsafe/src/utils/safe-versions.ts
.- How
safeContract
is obtained: ThesafeContract
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 ineternalsafe/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.
- How
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 forExecutionSuccess
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 theeth_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.
- JSON-RPC Under the Hood: Under the hood,
safeContract.filters.ExecutionSuccess()
: This generates a filter object that is specific to theExecutionSuccess
event defined in the Safe contract's ABI (Application Binary Interface). The ABI defines all the functions and events of a smart contract. Thefilters
property is automatically created by ethers.js based on the contract's ABI. TheExecutionSuccess
event is defined in the Safe's contract interface. You can see where theGnosis_safeInterface
is defined ineternalsafe/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.
- The
0
: This is thefromBlock
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 thetoBlock
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 theExecutionSuccess
event, as defined in the Safe's ABI (e.g.,txHash
which is the internal Safe transaction hash bytes32 and thepayment
which is the amount of fees paid for execution)- A decoded
executor
parameter from parsing the transaction thanks tosafeContract.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:
- Fetching Block Timestamp: It retrieves the timestamp of the block in which the event occurred using
provider.getBlock(log.blockNumber)
. - 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. - Parsing Transaction data: It uses the same contract ABI to attempt to decode the transaction
data
field by callingsafeContract.interface.decodeFunctionData('execTransaction', tx.data)
. This reveals the details of the Safe transaction allowing easy access to the recipients, amounts, and operation types involved. - Constructing
TxHistoryItem
: It assembles all this information into aTxHistoryItem
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.
- Indexed Events: Ensure the
- Solutions:
- 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.
- Solutions:
- 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.
- Solutions:
- 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.
- Dynamic ABI fetching: The
- Solutions:
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.