import type { ChainId, FunctionSignature, TxMetadata, } from "../types/crypto.types.js"; /** Well-known ERC-20 / ERC-721 function selectors. */ export const KNOWN_SELECTORS: FunctionSignature[] = [ { selector: "0xa9059cbb", name: "transfer", signature: "transfer(address,uint256)", }, { selector: "0x095ea7b3", name: "approve", signature: "approve(address,uint256)", }, { selector: "0x23b872dd", name: "transferFrom", signature: "transferFrom(address,address,uint256)", }, { selector: "0x70a08231", name: "balanceOf", signature: "balanceOf(address)", }, { selector: "0xdd62ed3e", name: "allowance", signature: "allowance(address,address)", }, { selector: "0x18160ddd", name: "totalSupply", signature: "totalSupply()", }, { selector: "0x42842e0e", name: "safeTransferFrom", signature: "safeTransferFrom(address,address,uint256)", }, { selector: "0xa22cb465", name: "setApprovalForAll", signature: "setApprovalForAll(address,bool)", }, ]; const EVM_TX_HASH = /^0x[0-9a-fA-F]{64}$/; const UTXO_TX_HASH = /^[0-9a-fA-F]{64}$/; const SOLANA_TX_SIG = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/; const CALLDATA_PATTERN = /^0x[0-9a-fA-F]{8,}$/; const EVM_CHAINS: ChainId[] = [ "ethereum", "polygon", "arbitrum", "optimism", "avalanche", "bsc", "base", ]; /** * Decode a transaction hash and extract metadata. Determines the hash type * (`evm` / `utxo` / `solana` / `unknown`) and which chains it could belong to. */ export function decodeTxHash(hash: string): TxMetadata { if (!hash || typeof hash !== "string") { return { hash: "", chains: [], valid: false, hashType: "unknown" }; } const trimmed = hash.trim(); if (EVM_TX_HASH.test(trimmed)) { return { hash: trimmed, chains: [...EVM_CHAINS], valid: true, hashType: "evm", hashHex: trimmed.slice(2).toLowerCase(), }; } if (UTXO_TX_HASH.test(trimmed)) { return { hash: trimmed, chains: ["bitcoin", "litecoin", "dogecoin"], valid: true, hashType: "utxo", hashHex: trimmed.toLowerCase(), }; } if (SOLANA_TX_SIG.test(trimmed)) { return { hash: trimmed, chains: ["solana"], valid: true, hashType: "solana", }; } return { hash: trimmed, chains: [], valid: false, hashType: "unknown" }; } /** * Look up a function selector in the known-signatures table. The lookup is * case-insensitive on the input selector. * * @example * lookupSelector('0xa9059cbb') * // { selector: '0xa9059cbb', name: 'transfer', signature: 'transfer(address,uint256)' } */ export function lookupSelector( selector: string ): FunctionSignature | undefined { const normalized = selector.toLowerCase(); return KNOWN_SELECTORS.find((s) => s.selector === normalized); } /** * Extract the 4-byte function selector from calldata. Returns `undefined` * for empty / non-string / non-hex / too-short input. * * @example * extractSelector('0xa9059cbb000000000000000000000000...') * // '0xa9059cbb' */ export function extractSelector(calldata: string): string | undefined { if (!calldata || typeof calldata !== "string") return undefined; const trimmed = calldata.trim(); if (!CALLDATA_PATTERN.test(trimmed)) return undefined; return trimmed.slice(0, 10).toLowerCase(); } /** * Decode calldata and return the matching function signature if known. * This is a naive matcher — it does not decode the argument bytes, only * the 4-byte selector. ABI-aware decoding lands in a follow-up change. */ export function decodeCalldata( calldata: string ): FunctionSignature | undefined { const selector = extractSelector(calldata); if (!selector) return undefined; return lookupSelector(selector); }