UNPKG

@wagmi/cli

Version:

Manage and generate code from Ethereum ABIs

355 lines (338 loc) 9.13 kB
import { mkdir, writeFile } from 'node:fs/promises' import { Address as AddressSchema } from 'abitype/zod' import { camelCase } from 'change-case' import { join } from 'pathe' import type { Abi, Address } from 'viem' import { z } from 'zod' import type { ContractConfig } from '../config.js' import { fromZodError } from '../errors.js' import type { Compute } from '../types.js' import { fetch, getCacheDir } from './fetch.js' export type RoutescanConfig<chainId extends number> = { /** * Routescan API key. */ apiKey: string /** * Duration in milliseconds to cache ABIs. * * @default 1_800_000 // 30m in ms */ cacheDuration?: number | undefined /** * Chain ID to use for fetching ABI. * * If `address` is an object, `chainId` is used to select the address. */ chainId: (chainId extends ChainId ? chainId : never) | (ChainId & {}) /** * Contracts to fetch ABIs for. */ contracts: Compute<Omit<ContractConfig<ChainId, chainId>, 'abi'>>[] /** * Whether to try fetching proxy implementation address of the contract * * @default false */ tryFetchProxyImplementation?: boolean | undefined } /** * Fetches contract ABIs from Routescan. */ export function routescan<chainId extends ChainId>( config: RoutescanConfig<chainId>, ) { const { apiKey, cacheDuration = 1_800_000, chainId, tryFetchProxyImplementation = false, } = config const contracts = config.contracts.map((x) => ({ ...x, address: typeof x.address === 'string' ? { [chainId]: x.address } : x.address, })) as Omit<ContractConfig, 'abi'>[] const name = 'Routescan' const getCacheKey: Parameters<typeof fetch>[0]['getCacheKey'] = ({ contract, }) => { if (typeof contract.address === 'string') return `${camelCase(name)}:${contract.address}` return `${camelCase(name)}:${JSON.stringify(contract.address)}` } return fetch({ cacheDuration, contracts, name, getCacheKey, async parse({ response }) { const json = await response.json() const parsed = await GetAbiResponse.safeParseAsync(json) if (!parsed.success) throw fromZodError(parsed.error, { prefix: 'Invalid response' }) if (parsed.data.status === '0') throw new Error(parsed.data.result) return parsed.data.result }, async request(contract) { if (!contract.address) throw new Error('address is required') const resolvedAddress = (() => { if (!contract.address) throw new Error('address is required') if (typeof contract.address === 'string') return contract.address const contractAddress = contract.address[chainId] if (!contractAddress) throw new Error( `No address found for chainId "${chainId}". Make sure chainId "${chainId}" is set as an address.`, ) return contractAddress })() const options = { address: resolvedAddress, apiKey, chainId, } let abi: Abi | undefined const implementationAddress = await (async () => { if (!tryFetchProxyImplementation) return const json = await globalThis .fetch(buildUrl({ ...options, action: 'getsourcecode' })) .then((res) => res.json()) const parsed = await GetSourceCodeResponse.safeParseAsync(json) if (!parsed.success) throw fromZodError(parsed.error, { prefix: 'Invalid response' }) if (parsed.data.status === '0') throw new Error(parsed.data.result) if (!parsed.data.result[0]) return abi = parsed.data.result[0].ABI return parsed.data.result[0].Implementation as Address })() if (abi) { const cacheDir = getCacheDir() await mkdir(cacheDir, { recursive: true }) const cacheKey = getCacheKey({ contract }) const cacheFilePath = join(cacheDir, `${cacheKey}.json`) await writeFile( cacheFilePath, `${JSON.stringify({ abi, timestamp: Date.now() + cacheDuration }, undefined, 2)}\n`, ) } return { url: buildUrl({ ...options, action: 'getabi', address: implementationAddress || resolvedAddress, }), } }, }) } function buildUrl(options: { action: 'getabi' | 'getsourcecode' address: Address apiKey: string chainId: ChainId | undefined }) { const { action, address, apiKey, chainId } = options const baseUrl = `https://api.routescan.io/v2/network/mainnet/evm/${chainId}/etherscan/api` return `${baseUrl}?module=contract&action=${action}&address=${address}${apiKey ? `&apikey=${apiKey}` : ''}` } const GetAbiResponse = z.discriminatedUnion('status', [ z.object({ status: z.literal('1'), message: z.literal('OK'), result: z.string().transform((val) => JSON.parse(val) as Abi), }), z.object({ status: z.literal('0'), message: z.literal('NOTOK'), result: z.string(), }), ]) const GetSourceCodeResponse = z.discriminatedUnion('status', [ z.object({ status: z.literal('1'), message: z.literal('OK'), result: z.array( z.discriminatedUnion('Proxy', [ z.object({ ABI: z.string().transform((val) => JSON.parse(val) as Abi), Implementation: AddressSchema, Proxy: z.literal('1'), }), z.object({ ABI: z.string().transform((val) => JSON.parse(val) as Abi), Implementation: z.string(), Proxy: z.literal('0'), }), ]), ), }), z.object({ status: z.literal('0'), message: z.literal('NOTOK'), result: z.string(), }), ]) // Supported chains type ChainId = | 1 // Ethereum | 10 // OP Mainnet | 14 // Flare Mainnet | 16 // Coston | 19 // Songbird Canary | 56 // BNB Smart Chain | 114 // Coston2 | 130 // Unichain | 151 // Redbelly | 166 // Nomina | 185 // Mint | 252 // Fraxtal | 254 // Swan chain | 288 // Boba Ethereum | 291 // Orderly | 324 // zkSync Era | 335 // DFK | 357 // Pulsar | 369 // Pulse Chain | 919 // Mode | 1301 // Unichain | 1687 // Mint Sepolia | 1946 // Minato | 2037 // Kiwi | 2233 // Chainbase | 2522 // Fraxtal | 3012 // PLAYA3ULL Games | 3636 // Botanix | 3939 // DOS | 4202 // Lisk | 4460 // Orderly | 10888 // GameSwift | 17000 // Holesky | 28882 // Boba Sepolia | 48795 // Space | 49321 // GUNZ | 70800 // Barret | 80008 // Polynomial Sepolia | 80085 // Artio Testnet | 84532 // Base Sepolia | 88882 // Chiliz Spicy | 167008 // Taiko Katla | 167009 // Taiko Hekla | 173750 // Echo | 421614 // Arbitrum Sepolia | 555666 // Eclipse | 763373 // Ink | 779672 // Dispatch | 11155111 // Sepolia | 11155420 // OP Sepolia | 20241133 // Proxima | 21000001 // Corn | 168587773 // Blast Sepolia | 999999999 // Zora Sepolia | 164_4 // Nomina | 153_2 // Redbelly | 70805_2 // Cloud | 378 // Koroshi | 379 // KOROSHI | 987 // Orange | 999 // Hyperliquid EVM | 1088 // Metis | 1135 // Lisk | 1216 // Intersect | 1234 // StepNetwork | 1344 // Blitz | 1853 // HighOctane | 1888 // Memoria | 1923 // Swell | 2038 // Shrapnel | 2044 // Shrapnel | 2786 // Apertum | 2818 // Morph | 3011 // PLAYA3ULL Games | 3084 // XL Network | 3278 // Soshi | 3637 // Botanix | 4227 // Hashfire | 4313 // Artery | 4337 // Beam | 5000 // Mantle | 5039 // Onigiri | 5040 // Onigiri | 5115 // Citrea | 5330 // Superseed | 5566 // StraitsX | 6119 // UPTN | 6533 // Kalichain | 6900 // Nibiru | 6911 // Nibiru Testnet-2 | 7894 // Mintus | 7979 // DOS | 8008 // Polynomial | 8021 // Numine | 8227 // Space | 8453 // Base | 8787 // Animalia | 9745 // Plasma | 10036 // Innovo | 10507 // Numbers | 10849 // Lamina1 | 10850 // Lamina1 Identity | 11227 // Jiritsutes | 12150 // QChain | 13322 // Fifa Blockchain | 13337 // Beam | 13576 // Mythgames | 14174 // Pecorino | 16180 // Plyr | 21024 // Tradex | 21816 // Frqtal | 24010 // Stealthnet | 27827 // Zeroone | 28530 // Blockticity | 33311 // Feature | 34443 // Mode | 42161 // Arbitrum One | 43113 // C-Chain Fuji | 43114 // C-Chain | 43419 // GUNZ | 47208 // Armada | 53188 // DSRV2 | 53302 // Superseed | 53935 // DFK | 54414 // Innovomark | 55197 // Egmtester | 56288 // Boba BNB | 56400 // Zeroone | 57073 // Ink | 59409 // Lifeaiv1 | 59932 // Insomnia | 61587 // Growth | 62521 // Lucid | 62831 // Plyr | 68414 // Henesys | 69696 // Ceden | 76736 // Hiss | 79554 // Lucid | 79685 // Modex | 80069 // Berachain bepolia | 80094 // Berachain | 81457 // Blast | 84358 // Titan | 88888 // Chiliz | 96786 // Delaunch | 97433 // Growth | 124816 // Mitosis | 132008 // BitcoinL1 | 167000 // Taiko | 210815 // Stavax | 432201 // Dexalot | 432204 // Dexalot | 560048 // Hoodi | 710420 // Tiltyard | 723107 // Tixchain | 5278000 // JSC Kaigan | 7777777 // Zora | 21000000 // Corn | 420120000 // Alpha 0 | 420120001 // Alpha 1 | 420420421 // Westend | 9746_5 // Plasma