UNPKG

@0x/0x-parser

Version:

🧾 Designed for 0x integrators: This library parses 0x transactions on EVM blockchains into a format that is both user-friendly and easy to understand.

235 lines (209 loc) • 5.89 kB
import { fromHex, erc20Abi, getAddress, formatUnits, formatEther } from "viem"; import { bsc, base, mode, blast, linea, scroll, mantle, plasma, mainnet, polygon, arbitrum, optimism, unichain, avalanche, berachain, worldchain, monad, abstract, } from "viem/chains"; import { NATIVE_SYMBOL_BY_CHAIN_ID, NATIVE_TOKEN_ADDRESS } from "../constants"; import type { Address } from "viem"; import type { Trace, EnrichedLog, EnrichLogsArgs, SupportedChainId, } from "../types"; export function isChainIdSupported( chainId: number ): chainId is SupportedChainId { const supportedChainIds: number[] = [ bsc.id, base.id, mode.id, monad.id, blast.id, linea.id, scroll.id, mantle.id, plasma.id, polygon.id, mainnet.id, arbitrum.id, optimism.id, unichain.id, avalanche.id, berachain.id, worldchain.id, abstract.id, ]; return supportedChainIds.includes(chainId); } export function calculateNativeTransfer( trace: Trace, options: { recipient: Address; direction?: "to" | "from" } ): string { const { recipient, direction = "to" } = options; let totalTransferred = 0n; const recipientLower = recipient.toLowerCase(); function processCall(call: Trace) { if (!call.value) return; const relevantAddress = direction === "from" ? call.from : call.to; if (relevantAddress.toLowerCase() === recipientLower) { totalTransferred += fromHex(call.value, "bigint"); } } function traverseCalls(calls: Trace[]) { for (const call of calls) { processCall(call); if (call.calls && call.calls.length > 0) { traverseCalls(call.calls); } } } traverseCalls(trace.calls); return formatEther(totalTransferred); } export async function transferLogs({ publicClient, transactionReceipt, }: EnrichLogsArgs): Promise<EnrichedLog[]> { const EVENT_SIGNATURES = { Transfer: `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef`, } as const; const transferLogsAddresses = transactionReceipt.logs .filter((log) => log.topics[0] === EVENT_SIGNATURES.Transfer) .map((log) => ({ ...log, address: getAddress(log.address) })); const contracts = [ ...transferLogsAddresses.map((log) => ({ abi: erc20Abi, address: log.address, functionName: "symbol", })), ...transferLogsAddresses.map((log) => ({ abi: erc20Abi, address: log.address, functionName: "decimals", })), ]; const results = await publicClient.multicall({ contracts }); const midpoint = Math.floor(results.length / 2); const enrichedLogs = transferLogsAddresses .map((log, index) => { const symbol = results[index].result; const decimals = results[midpoint + index].result; // zkSync-style system contracts can emit Transfer-like logs for native accounting, // but those addresses do not implement ERC20 metadata, so skip them here. if (symbol == null || decimals == null || typeof decimals !== "number") { return null; } const amount = log.data === "0x" ? "0" : formatUnits(BigInt(log.data), decimals); const amountRaw = log.data === "0x" ? 0n : BigInt(log.data); const { address, topics } = log; const { 1: fromHex, 2: toHex } = topics; const from = getAddress(convertHexToAddress(fromHex)); const to = getAddress(convertHexToAddress(toHex)); return { to, from, symbol, amount, amountRaw, address, decimals }; }) .filter((log): log is EnrichedLog => log != null && log.amount !== "0") return enrichedLogs; } function convertHexToAddress(hexString: string): string { return `0x${hexString.slice(-40)}`; } export function parseSmartContractWalletTx({ logs, trace, chainId, smartContractWallet, }: { logs: EnrichedLog[]; trace: Trace; chainId: SupportedChainId; smartContractWallet: Address; }) { const smartContractWalletTransferLogs = logs.reduce<{ output?: EnrichedLog; input?: EnrichedLog; }>((acc, curr) => { if (curr.to === smartContractWallet) return { ...acc, output: curr }; if (curr.from === smartContractWallet) return { ...acc, input: curr }; return acc; }, {}); let { input, output } = smartContractWalletTransferLogs; const nativeAmountToTaker = calculateNativeTransfer(trace, { recipient: smartContractWallet, }); const nativeAmountFromTaker = calculateNativeTransfer(trace, { recipient: smartContractWallet, direction: "from", }); if (!output && nativeAmountToTaker !== "0") { if (input) { return { tokenIn: { address: input.address, amount: input.amount, symbol: input.symbol, }, tokenOut: { address: NATIVE_TOKEN_ADDRESS, amount: nativeAmountToTaker, symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId], }, }; } else { return null; } } if (!input && nativeAmountFromTaker !== "0") { const wrappedNativeAsset = chainId === 56 ? "WBNB" : chainId === 137 ? "WPOL" : "WETH"; const inputLog = logs.find((log) => log.symbol === wrappedNativeAsset); if (inputLog && output) { return { tokenIn: { address: NATIVE_TOKEN_ADDRESS, amount: inputLog.amount, symbol: NATIVE_SYMBOL_BY_CHAIN_ID[chainId], }, tokenOut: { address: output.address, amount: output.amount, symbol: output.symbol, }, }; } else { return null; } } if (input && output) { return { tokenIn: { address: input.address, amount: input.amount, symbol: input.symbol, }, tokenOut: { address: output.address, amount: output.amount, symbol: output.symbol, }, }; } return null; }