@wagmi/cli
Version:
Manage and generate code from Ethereum ABIs
129 lines • 5.14 kB
JavaScript
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 Routescan.
*/
export function routescan(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 = 'Routescan';
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 { 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)),
}),
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=routescan.js.map