import type { Abi, AbiEvent, AbiFunction, AbiParametersToPrimitiveTypes, Address, ExtractAbiEvent, ExtractAbiEventNames, ExtractAbiFunction, ExtractAbiFunctionNames, FormatAbi, } from 'abitype' import { formatAbi } from 'abitype' import { CreateEventFilterParameters } from 'viem' import { MaybeExtractEventArgsFromAbi } from 'viem/dist/types/types/contract' import { ValueOf } from 'viem/dist/types/types/utils' export type EVMtsContract< TName extends string, TAddresses extends Record, TAbi extends Abi, THumanReadableAbi = FormatAbi, > = { abi: TAbi humanReadableAbi: THumanReadableAbi bytecode: `0x${string}` name: TName addresses: TAddresses events: (options?: { chainId?: TChainId | number | undefined }) => { [TEventName in ExtractAbiEventNames]: (< TStrict extends boolean = false, >( params: Pick< CreateEventFilterParameters< ExtractAbiEvent, TStrict, TAbi, TEventName, MaybeExtractEventArgsFromAbi >, 'fromBlock' | 'toBlock' | 'args' | 'strict' >, ) => CreateEventFilterParameters< ExtractAbiEvent, TStrict, TAbi, TEventName, MaybeExtractEventArgsFromAbi > & { eventName: TEventName; abi: [ExtractAbiEvent] }) & { address: ValueOf eventName: TEventName abi: [ExtractAbiEvent] humanReadableAbi: FormatAbi<[ExtractAbiEvent]> } } read: (options?: { chainId?: TChainId | number | undefined }) => { [TFunctionName in ExtractAbiFunctionNames]: (< TArgs extends AbiParametersToPrimitiveTypes< ExtractAbiFunction['inputs'] > & any[] = AbiParametersToPrimitiveTypes< ExtractAbiFunction['inputs'] > & any[], >( ...args: TArgs ) => { address: ValueOf abi: [ExtractAbiFunction] humanReadableAbi: FormatAbi<[ExtractAbiFunction]> args: TArgs['length'] extends 0 ? undefined : TArgs functionName: TFunctionName }) & { address: ValueOf abi: [ExtractAbiFunction] humanReadableAbi: FormatAbi<[ExtractAbiFunction]> functionName: TFunctionName } } write: (options?: { chainId?: TChainId | number | undefined }) => { [TFunctionName in ExtractAbiFunctionNames]: (< TArgs extends AbiParametersToPrimitiveTypes< ExtractAbiFunction['inputs'] > & any[] = AbiParametersToPrimitiveTypes< ExtractAbiFunction['inputs'] > & any[], >( ...args: TArgs ) => { address: ValueOf abi: [ExtractAbiFunction] humanReadableAbi: FormatAbi<[ExtractAbiFunction]> args: TArgs['length'] extends 0 ? undefined : TArgs functionName: TFunctionName }) & { address: ValueOf abi: [ExtractAbiFunction] humanReadableAbi: FormatAbi<[ExtractAbiFunction]> functionName: TFunctionName } } } export const evmtsContractFactory = < TName extends string, TAddresses extends Record, TAbi extends Abi, >({ abi, name, addresses, bytecode, }: Pick< EVMtsContract, 'name' | 'abi' | 'addresses' | 'bytecode' >): EVMtsContract => { const methods = abi.filter((field) => { return field.type === 'function' }) const events = ({ chainId, }: { chainId?: TChainId | number | undefined } = {}) => Object.fromEntries( abi .filter((field) => { return field.type === 'event' }) .map((eventAbi) => { const creator = (params: any) => { return { eventName: (eventAbi as AbiEvent).name, abi: [eventAbi], humanReadableAbi: formatAbi([eventAbi]), address: chainId ? addresses[chainId as number] : Object.values(addresses)[0], ...params, } } creator.address = chainId ? addresses[chainId as number] : Object.values(addresses)[0] creator.abi = [eventAbi] creator.eventName = (eventAbi as AbiEvent).name creator.humanReadableAbi = formatAbi([eventAbi]) return [(eventAbi as AbiEvent).name, creator] }), ) // we extend keyof TAddresses instead of number to make the types strict and safe // this will force user to often cast the chain id which may be annoying // with feedback we may want to change this const write = ({ chainId, }: { chainId?: TChainId | number | undefined } = {}) => Object.fromEntries( methods.map((method) => { const creator = (...args: any[]) => { // need to handle case where there is an overload // TODO make this more efficient const methodAbi = methods.filter( (m) => (m as AbiFunction).name === (method as AbiFunction)?.name, ) return { abi: methodAbi, humanReadableAbi: formatAbi([method]), functionName: (method as AbiFunction).name, args: args.length > 0 ? args : undefined, // TODO we are currently defaulting to the first address in the case of no chain id // There has to be a better way like providing an explicit default property in the address config address: addresses[chainId as number] ?? (Object.values( addresses, )[0] as unknown as TChainId extends unknown ? ValueOf : TAddresses[TChainId]) ?? undefined, } } creator.address = addresses[chainId as number] ?? undefined creator.abi = [method] creator.humanReadableAbi = formatAbi([method]) return [(method as AbiFunction).name, creator] }), ) // TODO filter for read // TODO ABI type magic const read = ({ chainId, }: { chainId?: TChainId | number | undefined } = {}) => Object.fromEntries( methods.map((method) => { // TODO ABI Type const creator = (...args: any[]) => { // need to handle case where there is an overload // TODO make this more efficient const methodAbi = methods.filter( (m) => (m as AbiFunction).name === (method as AbiFunction)?.name, ) return { abi: methodAbi, humanReadableAbi: formatAbi([method]), functionName: (method as AbiFunction).name, args: args.length > 0 ? args : undefined, // TODO we are currently defaulting to the first address in the case of no chain id // There has to be a better way like providing an explicit default property in the address config address: addresses[chainId as number] ?? (Object.values( addresses, )[0] as unknown as TChainId extends unknown ? ValueOf : TAddresses[TChainId]) ?? undefined, } } creator.address = addresses[chainId as number] ?? undefined creator.abi = [method] creator.humanReadableAbi = formatAbi([method]) return [(method as AbiFunction).name, creator] }), ) return { name, abi, humanReadableAbi: formatAbi(abi), addresses, bytecode, events: events as any, write: write as any, read: read as any, } }