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:
Generate and initialize a new project:
mkdir example-decode && cd example-decode
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 network
const getPublicClient = ( chainId : number ) => {
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
:
EtherscanStrategyResolver,
FourByteStrategyResolver,
} from ' @3loop/transaction-decoder '
// Create an in-memory cache for the ABIs
const abiCache = new Map < string , ContractABI > ()
const abiStore : VanillaAbiStore = {
// Define the strategies to use for fetching the ABIs
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)
const value = abiCache . get (event)
const value = abiCache . get (signature)
// 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 )
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-information
const 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)
// 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 ,
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:
const decoded = await decoder . decodeCalldata ( {
data: ' 0x8d80ff0a000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006b2002d8880bcc0618dbcc5d516640015a69e28fdc406000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003046a761202000000000000000000000000e3c408bd53c31c085a1746af401a4042954ff7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000e0d8be1dc2affefab9c066fd73a4b9788d3d3d4500000000000000000000000000000000000000000000000000064e5a6479f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001045f938ab87e466dca3d4953d42efc1fde996dcd7888622c9db2827bde3ecb169067e93c03124f4558edfed7819f454599a24aa75c8ba0aecb707bb6d7e8a265911f0ecd835bf25f1d74e4d89e30acb4176113c07d8a43a42712f4b1c6b658ff87f927e05f54ebf5b8c91ba843aefbb8e7d88ab73aaa66ede612e620d9847dc2260d1bbaa39fdd8469f14ea2342526d20e4458cbf9323a04ff6752acd302021067c84a7b59e4bc389954aa3a7daafe9b4a30fba791aab53266b9a7bbd78c31adba05571c29a581b2fa3ecb44c85cead416f6bfe0038b5d7e60202926e8e409c285a391d719858122d2f9a00e07417ac26b4c5370f0f6459332006edc20c65220acac58a32000000000000000000000000000000000000000000000000000000000002d8880bcc0618dbcc5d516640015a69e28fdc406000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003046a761202000000000000000000000000e3c408bd53c31c085a1746af401a4042954ff7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000008b31d6018d2ee253379e98475c59c934549e62b00000000000000000000000000000000000000000000000000006651728988000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104946f3bb1ed2ba0acc722eec299e4fa02a741f6e5868f0643445f0ead1fe2b2f64cc28e5d2268f1f5f5e0239aaf1e4237be9bc67fb5bc1d8eae2c1d602089dcf720f7a65592538de69348f49784d27e7145ab96ffee198cab2d0c49f77a9cb415424881898c3df33e9318665e73e40ee66de96c5222d2b833edcf115a9b4a16cbd61c31c2a6240ac498822e1c6a61eb5a7271ad305f911985662b9a47e50249d88a7e53e187b542c9cf69dbccecc543fb76d7480a1d29c9db2a6fc81e04ecbfdf609b1bd3f85f39ff0f226493f3f5c8934507c92f24cd5b3365bd5e8bc4a4a2b23e6dcd28654caa13a0f5418f4cc642de81bff61d01b913883acf8c0794e94794a9405320000000000000000000000000000000000000000000000000000000000000000000000000000000000000 ' ,
contractAddress: ' 0x40A2aCCbd92BCA938b02010E17A5b8929b49130D ' ,
console . log ( JSON . stringify (decoded , null , 2 ))
console . error ( JSON . stringify (e , null , 2 ))
Run the script:
Expected output:
"signature" : " multiSend(bytes) " ,
"value" : " 0x002d8880bcc0618dbcc5d516640015a69e28fdc406000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003046a761202000000000000000000000000e3c408bd53c31c085a1746af401a4042954ff7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000044a9059cbb000000000000000000000000e0d8be1dc2affefab9c066fd73a4b9788d3d3d4500000000000000000000000000000000000000000000000000064e5a6479f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001045f938ab87e466dca3d4953d42efc1fde996dcd7888622c9db2827bde3ecb169067e93c03124f4558edfed7819f454599a24aa75c8ba0aecb707bb6d7e8a265911f0ecd835bf25f1d74e4d89e30acb4176113c07d8a43a42712f4b1c6b658ff87f927e05f54ebf5b8c91ba843aefbb8e7d88ab73aaa66ede612e620d9847dc2260d1bbaa39fdd8469f14ea2342526d20e4458cbf9323a04ff6752acd302021067c84a7b59e4bc389954aa3a7daafe9b4a30fba791aab53266b9a7bbd78c31adba05571c29a581b2fa3ecb44c85cead416f6bfe0038b5d7e60202926e8e409c285a391d719858122d2f9a00e07417ac26b4c5370f0f6459332006edc20c65220acac58a32000000000000000000000000000000000000000000000000000000000002d8880bcc0618dbcc5d516640015a69e28fdc406000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003046a761202000000000000000000000000e3c408bd53c31c085a1746af401a4042954ff7400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c00000000000000000000000000000000000000000000000000000000000000044a9059cbb0000000000000000000000008b31d6018d2ee253379e98475c59c934549e62b00000000000000000000000000000000000000000000000000006651728988000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000104946f3bb1ed2ba0acc722eec299e4fa02a741f6e5868f0643445f0ead1fe2b2f64cc28e5d2268f1f5f5e0239aaf1e4237be9bc67fb5bc1d8eae2c1d602089dcf720f7a65592538de69348f49784d27e7145ab96ffee198cab2d0c49f77a9cb415424881898c3df33e9318665e73e40ee66de96c5222d2b833edcf115a9b4a16cbd61c31c2a6240ac498822e1c6a61eb5a7271ad305f911985662b9a47e50249d88a7e53e187b542c9cf69dbccecc543fb76d7480a1d29c9db2a6fc81e04ecbfdf609b1bd3f85f39ff0f226493f3f5c8934507c92f24cd5b3365bd5e8bc4a4a2b23e6dcd28654caa13a0f5418f4cc642de81bff61d01b913883acf8c0794e94794a940532000000000000000000000000000000000000000000000000000000000 " ,
"signature" : " transactions((uint256,address,uint256,uint256,bytes)[]) " ,
"value" : " 0x2D8880BcC0618DBCC5d516640015A69e28fdC406 "