Skip to content

How to decode an Ethereum Transaction

In this guide, we will go through the process of decoding an Ethereum transaction using Loop Decoder. For the simplicity of the example, we assume that that contract ABIs involved in the transaction are verified on Etherscan.

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.

  1. 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:
Terminal window
npm install -g bun
  1. Generate and initialize a new project:
Terminal window
mkdir example-decode && cd example-decode
bun init

Required packages

For this guide, you will need to have the following packages installed:

Terminal window
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.

index.ts
import { createPublicClient, http } from 'viem'
// Create a public client for the Ethereum Mainnet network
const 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:

index.ts
import {
EtherscanStrategyResolver,
FourByteStrategyResolver,
VanillaAbiStore,
ContractABI,
} 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
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.

index.ts
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)
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 Transaction

Now that we have all the necessary components, we can start decoding a transaction. For this example, we will use the following transaction:

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()

Run the script:

Terminal window
bun run index.ts

Expected output:

{
"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"
}
]
}
// ...
}