Decoding Transaction
This guide explains how to decode Ethereum transactions using Loop Decoder. We’ll cover:
- Setting up data loading strategies for ABIs and contract metadata
- Configuring data stores for Contract ABIs and metadata
- Decoding transactions
Learn more about Loop Decoder APIs and the differences between them
Installation
Generate and initialize a new project:
mkdir example-decode && cd example-decodebun init
Install required packages:
bun install @3loop/transaction-decoder viem
Setup Loop Decoder
Loop Decoder requires three components:
- RPC Provider: Fetches raw transaction data
- ABI Data Store: Retrieves and caches contract ABIs
- Contract Metadata Store: Retrieves and caches contract metadata (e.g., token name, symbol, decimals)
1. RPC Provider
Create a getPublicClient
function that accepts a chain ID and returns an object with Viem PublicClient
.
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'), }), }}
For detailed configuration options and trace API settings, see the RPC Provider documentation.
2. ABI Data Store
The ABI Data Store handles:
- Fetching ABIs using predefined strategies (e.g., Etherscan, 4byte). Some strategies like Etherscan require an API key. See the full list of strategies in Data Loaders (ABI Strategies)
- Caching fetched ABIs
To create a custom ABI Data Store, implement the VanillaAbiStore
interface:
export interface VanillaAbiStore { strategies?: readonly ContractAbiResolverStrategy[] get: (key: AbiParams) => Promise<ContractAbiResult> set: (key: AbiParams, val: ContractABI) => Promise<void>}
Example: an ABI data store with Etherscan and 4byte data loaders and in-memory cache
import { EtherscanStrategyResolver, FourByteStrategyResolver, VanillaAbiStore, ContractABI,} from '@3loop/transaction-decoder'
// Create an in-memory cache for the ABIsconst abiCache = new Map<string, ContractABI>()
// ABI store implementation with caching and multiple resolution strategiesconst abiStore: VanillaAbiStore = { strategies: [ // List of stratagies to resolve new ABIs EtherscanV2StrategyResolver({ apikey: process.env.ETHERSCAN_API_KEY || '', }), FourByteStrategyResolver(), ],
// Get ABI from memory by address, event or signature // Can be returned the list of all possible ABIs get: async ({ address, event, signature }) => { const key = address?.toLowerCase() || event || signature if (!key) return []
const cached = abiCache.get(key) return cached ? [ { ...cached, id: key, source: 'etherscan', status: 'success', }, ] : [] },
set: async (_key, abi) => { const key = abi.type === 'address' ? abi.address.toLowerCase() : abi.type === 'event' ? abi.event : abi.type === 'func' ? abi.signature : null
if (key) abiCache.set(key, abi) },}
3. Contract Metadata Store
The Contract Metadata Store handles:
- Fetching contract metadata using predefined strategies (e.g., ERC20, NFT). See the full list of strategies in Data Loaders (Contract Metadata)
- Caching fetched contract metadata
To create a custom Contract Metadata Store, implement the VanillaContractMetaStore
interface:
export interface VanillaContractMetaStore { strategies?: readonly VanillaContractMetaStategy[] get: (key: ContractMetaParams) => Promise<ContractMetaResult> set: (key: ContractMetaParams, val: ContractMetaResult) => Promise<void>}
Example: a Contract Metadata Store with ERC20 data loader and in-memory cache
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>()
// Contract metadata store implementation with in-memory cachingconst contractMetaStore: VanillaContractMetaStore = { strategies: [ERC20RPCStrategyResolver],
get: async ({ address, chainID }) => { const key = `${address}-${chainID}`.toLowerCase() const cached = contractMetaCache.get(key) return cached ? { status: 'success', result: cached } : { status: 'empty', result: null } },
set: async ({ address, chainID }, result) => { if (result.status === 'success') { contractMetaCache.set(`${address}-${chainID}`.toLowerCase(), result.result) } },}
4. Initializing Loop Decoder
Finally, you can create a new instance of the TransactionDecoder class:
import { TransactionDecoder } from '@3loop/transaction-decoder'
const decoder = new TransactionDecoder({ getPublicClient: getPublicClient, abiStore: abiStore, contractMetaStore: contractMetaStore,})
Example: Decoding a Transaction
Once the TransactionDecoder
is set up, you can use it to decode a transaction by calling the decodeTransaction
method:
async function main() { try { const decoded = await decoder.decodeTransaction({ chainID: 1, hash: '0xc0bd04d7e94542e58709f51879f64946ff4a744e1c37f5f920cea3d478e115d7', })
console.log(JSON.stringify(decoded, null, 2)) } catch (e) { console.error(JSON.stringify(e, null, 2)) }}
main()
Check the full expected output in our Playground or see it below:
{ "txHash": "0xc0bd04d7e94542e58709f51879f64946ff4a744e1c37f5f920cea3d478e115d7", "txType": "contract interaction", "fromAddress": "0xf89a3799b90593317e0a1eb74164fbc1755a297a", "toAddress": "0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9", "contractName": null, "contractType": "OTHER", "methodCall": { "name": "repay", "type": "function", "signature": "repay(address,uint256,uint256,address)", "params": [ { "name": "asset", "type": "address", "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7" }, { "name": "amount", "type": "uint256", "value": "1238350000" }, { "name": "rateMode", "type": "uint256", "value": "2" }, { "name": "onBehalfOf", "type": "address", "value": "0xf89a3799b90593317E0a1Eb74164fbc1755A297A" } ] } // ...}
Try it live
Try decoding the above or any other transactions in the our playground here.