@wagmi/cli
Version:
Manage and generate code from Ethereum ABIs
118 lines (107 loc) • 3.49 kB
text/typescript
import { homedir } from 'node:os'
import { default as fs } from 'fs-extra'
import { join } from 'pathe'
import type { Abi } from 'abitype'
import type { ContractConfig, Plugin } from '../config.js'
import type { Compute, RequiredBy } from '../types.js'
export type FetchConfig = {
/**
* Duration in milliseconds to cache ABIs from request.
*
* @default 1_800_000 // 30m in ms
*/
cacheDuration?: number | undefined
/**
* Contracts to fetch ABIs for.
*/
contracts: Compute<Omit<ContractConfig, 'abi'>>[]
/**
* Function for creating a cache key for contract.
*/
getCacheKey?:
| ((config: { contract: Compute<Omit<ContractConfig, 'abi'>> }) => string)
| undefined
/**
* Name of source.
*/
name?: ContractConfig['name'] | undefined
/**
* Function for parsing ABI from fetch response.
*
* @default ({ response }) => response.json()
*/
parse?:
| ((config: {
response: Response
}) => ContractConfig['abi'] | Promise<ContractConfig['abi']>)
| undefined
/**
* Function for returning a request to fetch ABI from.
*/
request: (config: {
address?: ContractConfig['address'] | undefined
}) =>
| { url: RequestInfo; init?: RequestInit | undefined }
| Promise<{ url: RequestInfo; init?: RequestInit | undefined }>
/**
* Duration in milliseconds before request times out.
*
* @default 5_000 // 5s in ms
*/
timeoutDuration?: number | undefined
}
type FetchResult = Compute<RequiredBy<Plugin, 'contracts'>>
/** Fetches and parses contract ABIs from network resource with `fetch`. */
export function fetch(config: FetchConfig): FetchResult {
const {
cacheDuration = 1_800_000,
contracts: contractConfigs,
getCacheKey = ({ contract }) => JSON.stringify(contract),
name = 'Fetch',
parse = ({ response }) => response.json(),
request,
timeoutDuration = 5_000,
} = config
return {
async contracts() {
const cacheDir = join(homedir(), '.wagmi-cli/plugins/fetch/cache')
await fs.ensureDir(cacheDir)
const timestamp = Date.now() + cacheDuration
const contracts = []
for (const contract of contractConfigs) {
const cacheKey = getCacheKey({ contract })
const cacheFilePath = join(cacheDir, `${cacheKey}.json`)
const cachedFile = await fs.readJSON(cacheFilePath).catch(() => null)
let abi: Abi | undefined
if (cachedFile?.timestamp > Date.now()) abi = cachedFile.abi
else {
try {
const controller = new globalThis.AbortController()
const timeout = setTimeout(
() => controller.abort(),
timeoutDuration,
)
const { url, init } = await request(contract)
const response = await globalThis.fetch(url, {
...init,
signal: controller.signal,
})
clearTimeout(timeout)
abi = await parse({ response })
await fs.writeJSON(cacheFilePath, { abi, timestamp })
} catch (error) {
try {
// Attempt to read from cache if fetch fails.
abi = (await fs.readJSON(cacheFilePath)).abi
} catch {}
if (!abi) throw error
}
}
if (!abi) throw Error('Failed to fetch ABI for contract.')
contracts.push({ abi, address: contract.address, name: contract.name })
}
return contracts
},
name,
}
}