Developing a system for displaying transactions in human-readable format
MetaMask shows: "You are about to call function 0x38ed1739 with arguments [115792089237316195423570985008687907853269984665640564039457584007913129639935, 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D, ...]". The user sees hex and clicks "Confirm" because there's no other choice. This is a fundamental UX problem throughout Web3 — and human-readable transaction systems solve it.
Decoding levels
ABI decoding
First level: from 0x38ed1739 get swapExactTokensForTokens(uint256,uint256,address[],address,uint256). Simple — search for the first 4 bytes of calldata in the ABI or signature database (4byte.directory API, openchain.xyz).
import { decodeFunctionData } from 'viem';
function decodeTransaction(to: string, data: `0x${string}`, knownAbis: Record<string, Abi>) {
const abi = knownAbis[to.toLowerCase()];
if (!abi) return null;
const { functionName, args } = decodeFunctionData({ abi, data });
return { functionName, args };
}
But knowing the function name and arguments isn't human-readable yet. You need a second level.
Semantic interpretation
Rule: swapExactTokensForTokens(amountIn, amountOutMin, path, to, deadline) where path = [USDC, WETH] → "Swap 100 USDC for minimum 0.032 ETH via Uniswap v2".
interface TransactionDescription {
protocol: string;
action: string;
summary: string; // "Swap 100 USDC → ETH"
details: DetailItem[];
riskFlags: RiskFlag[];
}
const uniswapV2Interpreter = {
swapExactTokensForTokens: async (args, context): Promise<TransactionDescription> => {
const [amountIn, amountOutMin, path, to] = args;
const inputToken = await resolveToken(path[0], context.chainId);
const outputToken = await resolveToken(path[path.length - 1], context.chainId);
return {
protocol: 'Uniswap V2',
action: 'Swap',
summary: `Swap ${formatAmount(amountIn, inputToken.decimals)} ${inputToken.symbol} → ${outputToken.symbol}`,
details: [
{ label: 'Minimum receive', value: `${formatAmount(amountOutMin, outputToken.decimals)} ${outputToken.symbol}` },
{ label: 'Recipient', value: to === context.from ? 'You' : shortenAddress(to) },
{ label: 'Route', value: path.map(resolveTokenSymbol).join(' → ') },
],
riskFlags: checkSwapRisks(amountIn, amountOutMin, inputToken, outputToken),
};
},
};
Risk flags and warnings
Human-readable isn't just pretty text. The system should detect potentially dangerous transactions:
High slippage: if amountOutMin / currentPrice < 0.95 — warning "You are accepting slippage > 5%".
Unlimited approve: approve(spender, 2^256-1) — "You are granting unlimited rights to your USDC at address 0x...". Show the contract name and audit status of the spender.
Suspicious contract: to-address is not verified on Etherscan, deployed recently, low transaction count. Don't block, but show a clear warning.
Drain approval: setApprovalForAll(operator, true) for ERC-721/1155 — "You allow 0x... to manage ALL your NFTs from collection XYZ".
Phishing patterns: transaction looks like a transfer but calldata contains something else. Simulate-before-sending is the only reliable way.
Transaction simulation
Tenderly and Alchemy provide simulate API: run a transaction without sending it and get all state changes:
const simulation = await alchemy.transact.simulateExecution({
from: userAddress,
to: contractAddress,
data: calldata,
value: '0x0',
});
// simulation.calls — all internal calls
// simulation.logs — all events that will be emitted
// simulation.changes — balance changes (ERC-20, NFT)
Extract balance changes from simulation: "-100 USDC, +0.034 ETH" — this is the most reliable human-readable result because it shows what will actually happen, not what we think about the function.
Protocol knowledge base
For a scalable system, you need a protocol database:
interface ProtocolRegistry {
[contractAddress: string]: {
name: string;
logoUrl: string;
audited: boolean;
interpreter: TransactionInterpreter;
}
}
Open registries: Etherscan verified contracts API, DeFi Llama protocols list, Coingecko contract database. Supplement with custom entries for protocol-specific ones.
For unknown contracts — fallback to ABI decoding without semantic interpretation, with a clear indication "Unknown contract".
Integration into UI
Transaction preview modal
Before confirming in the wallet — show a preview:
<TransactionPreview
summary="Swap 100 USDC → ETH"
protocol={{ name: 'Uniswap V3', logo: '/logos/uniswap.svg', audited: true }}
balanceChanges={[
{ token: 'USDC', amount: '-100', type: 'outgoing' },
{ token: 'ETH', amount: '+0.034 (min.)', type: 'incoming' },
]}
riskFlags={[]}
gasFee={{ eth: '0.002', usd: '4.50' }}
/>
Transaction history
For each past transaction — human-readable description instead of hash and function. "3 January: Swapped 500 USDC → 1.2 ETH on Uniswap V3 (+$45 profit)". Requires off-chain storage of decoded data — constantly recalculating is expensive.
Timeline estimates
Basic system (ABI decoding + top-10 protocols + simulate): 3 days. Full system with risk flags, protocol database, transaction history: 4-5 days.







