UNPKG

@wagmi/cli

Version:

Manage and generate code from Ethereum ABIs

129 lines 5.14 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 { z } from 'zod'; import { fromZodError } from '../errors.js'; import { fetch, getCacheDir } from './fetch.js'; /** * Fetches contract ABIs from Etherscan. */ export function etherscan(config) { 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, })); const name = 'Etherscan'; const 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; 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; })(); 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) { const baseUrl = 'https://api.etherscan.io/v2/api'; const { action, address, apiKey, chainId } = options; return `${baseUrl}?${chainId ? `chainId=${chainId}&` : ''}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)), }), 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)), Implementation: AddressSchema, Proxy: z.literal('1'), }), z.object({ ABI: z.string().transform((val) => JSON.parse(val)), Implementation: z.string(), Proxy: z.literal('0'), }), ])), }), z.object({ status: z.literal('0'), message: z.literal('NOTOK'), result: z.string(), }), ]); //# sourceMappingURL=etherscan.js.map