How to decode a Calldata
In this guide, we will go through the process of decoding EVM Calldata using Loop Decoder. As an example we will decode a Calldata for Gnosis Safe Multisend transaction that has deep nested calls.
We recomend to copy all snipepts to a typescript project and run it at the end of this guide, or or you can copy the whole example from this file: Full Example Code. Do not forget to replace the placeholder YourApiKeyToken
with your own free Etherscan API key.
Prerequisites
Create a new project
Optionally, you can create a new project to follow along, or skip to Required packages.
- Install Bun: First, make sure you have Bun installed on your system. If you haven’t installed it yet, you can do so using npm:
npm install -g bun
- Generate and initialize a new project:
mkdir example-decode && cd example-decodebun init
Required packages
For this guide, you will need to have the following packages installed:
bun install @3loop/transaction-decoder viem
Data Sources
Loop Decoder requires some data sources to be able to decode transactions. We will need an RPC provider, a data source to fetch Contracts ABIs and a data source to fetch contract meta-information, such as token name, decimals, symbol, etc.
RPC Provider
We will start by creating a function which will return an object with PublicClient based on the chain ID. For the sake of this example, we will only support mainnet.
import { createPublicClient, http } from 'viem'
// Create a public client for the Ethereum Mainnet networkconst getPublicClient = (chainId: number) => { return { client: createPublicClient({ transport: http('https://rpc.ankr.com/eth'), }), }}
ABI loader
To avoid making unecessary calls to third-party APIs, Loop Decoder uses an API that allows cache. For this example, we will keep it simple and use an in-memory cache. We will also use some strategies to download contract ABIs from Etherscan and 4byte.directory. You can find more information about the strategies in the Strategies reference.
Create a cache for contract ABI and add your free Etherscan API key instead of the placeholder YourApiKeyToken
:
import { EtherscanStrategyResolver, FourByteStrategyResolver, VanillaAbiStore, ContractABI,} from '@3loop/transaction-decoder'
// Create an in-memory cache for the ABIsconst abiCache = new Map<string, ContractABI>()
const abiStore: VanillaAbiStore = { // Define the strategies to use for fetching the ABIs strategies: [ EtherscanStrategyResolver({ apikey: 'YourApiKeyToken', }), FourByteStrategyResolver(), ],
// Get the ABI from the cache // Get it by contract address, event name or signature hash get: async ({ address, event, signature }) => { const value = abiCache.get(address) if (value) { return { status: 'success', result: value, } } else if (event) { const value = abiCache.get(event) if (value) { return { status: 'success', result: value, } } } else if (signature) { const value = abiCache.get(signature) if (value) { return { status: 'success', result: value, } } }
return { status: 'empty', result: null, } },
// Set the ABI in the cache // Store it by contract address, event name or signature hash set: async (_key, value) => { if (value.status === 'success') { if (value.result.type === 'address') { abiCache.set(value.result.address, value.result) } else if (value.result.type === 'event') { abiCache.set(value.result.event, value.result) } else if (value.result.type === 'func') { abiCache.set(value.result.signature, value.result) } } },}
Contract Metadata loader
Create an in-memory cache for contract meta-information. Using ERC20RPCStrategyResolver
we will automatically retrieve token meta information from the contract such as token name, decimals, symbol, etc.
import type { ContractData, VanillaContractMetaStore } from '@3loop/transaction-decoder'import { ERC20RPCStrategyResolver } from '@3loop/transaction-decoder'
// Create an in-memory cache for the contract meta-informationconst contractMetaCache = new Map<string, ContractData>()
const contractMetaStore: VanillaContractMetaStore = { // Define the strategies to use for fetching the contract meta-information strategies: [ERC20RPCStrategyResolver],
// Get the contract meta-information from the cache get: async ({ address, chainID }) => { const key = `${address}-${chainID}`.toLowerCase() const value = contractMetaCache.get(key)
if (value) { return { status: 'success', result: value, } }
return { status: 'empty', result: null, } },
// Set the contract meta-information in the cache set: async ({ address, chainID }, result) => { const key = `${address}-${chainID}`.toLowerCase()
if (result.status === 'success') { contractMetaCache.set(key, result.result) } },}
Finally, you can create a new instance of the LoopDecoder class:
import { TransactionDecoder } from '@3loop/transaction-decoder'
const decoder = new TransactionDecoder({ getPublicClient: getPublicClient, abiStore: abiStore, contractMetaStore: contractMetaStore,})
Decoding a Calldata
Now that we have all the necessary components, we can start decoding a Calldata. For this example, we will use the following Calldata:
async function main() { try { const decoded = await decoder.decodeCalldata({ data: '0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006b2002d8880bcc0618dbcc5d516640015a69e28fdc406000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003046a761202000000000000000000000000e3c408bd53c31c085a1746af401a4042954ff7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000e0d8be1dc2affefab9c066fd73a4b9788d3d3d4500000000000000000000000000000000000000000000000000064e5a6479f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001045f938ab87e466dca3d4953d42efc1fde996dcd7888622c9db2827bde3ecb169067e93c03124f4558edfed7819f454599a24aa75c8ba0aecb707bb6d7e8a265911f0ecd835bf25f1d74e4d89e30acb4176113c07d8a43a42712f4b1c6b658ff87f927e05f54ebf5b8c91ba843aefbb8e7d88ab73aaa66ede612e620d9847dc2260d1bbaa39fdd8469f14ea2342526d20e4458cbf9323a04ff6752acd302021067c84a7b59e4bc389954aa3a7daafe9b4a30fba791aab53266b9a7bbd78c31adba05571c29a581b2fa3ecb44c85cead416f6bfe0038b5d7e60202926e8e409c285a391d719858122d2f9a00e07417ac26b4c5370f0f6459332006edc20c65220acac58a32000000000000000000000000000000000000000000000000000000000002d8880bcc0618dbcc5d516640015a69e28fdc406000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003046a761202000000000000000000000000e3c408bd53c31c085a1746af401a4042954ff7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000008b31d6018d2ee253379e98475c59c934549e62b00000000000000000000000000000000000000000000000000006651728988000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104946f3bb1ed2ba0acc722eec299e4fa02a741f6e5868f0643445f0ead1fe2b2f64cc28e5d2268f1f5f5e0239aaf1e4237be9bc67fb5bc1d8eae2c1d602089dcf720f7a65592538de69348f49784d27e7145ab96ffee198cab2d0c49f77a9cb415424881898c3df33e9318665e73e40ee66de96c5222d2b833edcf115a9b4a16cbd61c31c2a6240ac498822e1c6a61eb5a7271ad305f911985662b9a47e50249d88a7e53e187b542c9cf69dbccecc543fb76d7480a1d29c9db2a6fc81e04ecbfdf609b1bd3f85f39ff0f226493f3f5c8934507c92f24cd5b3365bd5e8bc4a4a2b23e6dcd28654caa13a0f5418f4cc642de81bff61d01b913883acf8c0794e94794a9405320000000000000000000000000000000000000000000000000000000000000000000000000000000000000', chainID: 1, contractAddress: '0x40A2aCCbd92BCA938b02010E17A5b8929b49130D', })
console.log(JSON.stringify(decoded, null, 2)) } catch (e) { console.error(JSON.stringify(e, null, 2)) }}
main()
Run the script:
bun run index.ts
Expected output:
{ "name": "multiSend", "signature": "multiSend(bytes)", "type": "function", "params": [ { "name": "transactions", "type": "bytes", "value": "0x002d8880bcc0618dbcc5d516640015a69e28fdc406000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003046a761202000000000000000000000000e3c408bd53c31c085a1746af401a4042954ff7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000e0d8be1dc2affefab9c066fd73a4b9788d3d3d4500000000000000000000000000000000000000000000000000064e5a6479f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001045f938ab87e466dca3d4953d42efc1fde996dcd7888622c9db2827bde3ecb169067e93c03124f4558edfed7819f454599a24aa75c8ba0aecb707bb6d7e8a265911f0ecd835bf25f1d74e4d89e30acb4176113c07d8a43a42712f4b1c6b658ff87f927e05f54ebf5b8c91ba843aefbb8e7d88ab73aaa66ede612e620d9847dc2260d1bbaa39fdd8469f14ea2342526d20e4458cbf9323a04ff6752acd302021067c84a7b59e4bc389954aa3a7daafe9b4a30fba791aab53266b9a7bbd78c31adba05571c29a581b2fa3ecb44c85cead416f6bfe0038b5d7e60202926e8e409c285a391d719858122d2f9a00e07417ac26b4c5370f0f6459332006edc20c65220acac58a32000000000000000000000000000000000000000000000000000000000002d8880bcc0618dbcc5d516640015a69e28fdc406000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003046a761202000000000000000000000000e3c408bd53c31c085a1746af401a4042954ff7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000008b31d6018d2ee253379e98475c59c934549e62b00000000000000000000000000000000000000000000000000006651728988000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104946f3bb1ed2ba0acc722eec299e4fa02a741f6e5868f0643445f0ead1fe2b2f64cc28e5d2268f1f5f5e0239aaf1e4237be9bc67fb5bc1d8eae2c1d602089dcf720f7a65592538de69348f49784d27e7145ab96ffee198cab2d0c49f77a9cb415424881898c3df33e9318665e73e40ee66de96c5222d2b833edcf115a9b4a16cbd61c31c2a6240ac498822e1c6a61eb5a7271ad305f911985662b9a47e50249d88a7e53e187b542c9cf69dbccecc543fb76d7480a1d29c9db2a6fc81e04ecbfdf609b1bd3f85f39ff0f226493f3f5c8934507c92f24cd5b3365bd5e8bc4a4a2b23e6dcd28654caa13a0f5418f4cc642de81bff61d01b913883acf8c0794e94794a940532000000000000000000000000000000000000000000000000000000000", "valueDecoded": { "name": "transactions", "signature": "transactions((uint256,address,uint256,uint256,bytes)[])", "type": "function", "params": [ { "name": "unknown", "type": "tuple", "components": [ { "name": "unknown", "type": "tuple", "components": [ { "name": "operation", "type": "uint256", "value": "0" }, { "name": "to", "type": "address", "value": "0x2D8880BcC0618DBCC5d516640015A69e28fdC406" }, { "name": "value", "type": "uint256", "value": "0" }, { "name": "dataLength", "type": "uint256", "value": "1544" }, //...