Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
latest
wevm/abitypeStrict TypeScript types for Ethereum ABIs
This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers
JSR Score
52%
Published
3 months ago (1.0.6)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410import { z } from 'npm:zod@^3.23.8' import type { AbiConstructor as AbiConstructorType, AbiEventParameter as AbiEventParameterType, AbiFallback as AbiFallbackType, AbiFunction as AbiFunctionType, AbiParameter as AbiParameterType, AbiReceive as AbiReceiveType, Address as AddressType, TypedData as TTypedData, } from './abi.ts' import { isSolidityType } from './human-readable/runtime/utils.ts' import { bytesRegex, execTyped, integerRegex } from './regex.ts' const Identifier = z.string().regex(/[a-zA-Z$_][a-zA-Z0-9$_]*/) export const Address = z.string().transform((val, ctx) => { const regex = /^0x[a-fA-F0-9]{40}$/ if (!regex.test(val)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid Address ${val}`, }) } return val as AddressType }) // From https://docs.soliditylang.org/en/latest/abi-spec.html#types export const SolidityAddress = z.literal('address') export const SolidityBool = z.literal('bool') export const SolidityBytes = z.string().regex(bytesRegex) export const SolidityFunction = z.literal('function') export const SolidityString = z.literal('string') export const SolidityTuple = z.literal('tuple') export const SolidityInt = z.string().regex(integerRegex) export const SolidityArrayWithoutTuple = z .string() .regex( /^(address|bool|function|string|bytes([1-9]|1[0-9]|2[0-9]|3[0-2])?|u?int(8|16|24|32|40|48|56|64|72|80|88|96|104|112|120|128|136|144|152|160|168|176|184|192|200|208|216|224|232|240|248|256)?)(\[[0-9]{0,}\])+$/, ) export const SolidityArrayWithTuple = z .string() .regex(/^tuple(\[[0-9]{0,}\])+$/) export const SolidityArray = z.union([ SolidityArrayWithTuple, SolidityArrayWithoutTuple, ]) export const AbiParameter: z.ZodType<AbiParameterType> = z.lazy(() => z.intersection( z.object({ name: z.union([Identifier.optional(), z.literal('')]), /** Representation used by Solidity compiler */ internalType: z.string().optional(), }), z.union([ z.object({ type: z.union([ SolidityAddress, SolidityBool, SolidityBytes, SolidityFunction, SolidityString, SolidityInt, SolidityArrayWithoutTuple, ]), }), z.object({ type: z.union([SolidityTuple, SolidityArrayWithTuple]), components: z.array(AbiParameter).readonly(), }), ]), ), ) export const AbiEventParameter: z.ZodType<AbiEventParameterType> = z.intersection(AbiParameter, z.object({ indexed: z.boolean().optional() })) export const AbiStateMutability = z.union([ z.literal('pure'), z.literal('view'), z.literal('nonpayable'), z.literal('payable'), ]) export const AbiFunction = z.preprocess( (val) => { const abiFunction = val as unknown as AbiFunctionType // Calculate `stateMutability` for deprecated `constant` and `payable` fields if (abiFunction.stateMutability === undefined) { if (abiFunction.constant) abiFunction.stateMutability = 'view' else if (abiFunction.payable) abiFunction.stateMutability = 'payable' else abiFunction.stateMutability = 'nonpayable' } return val }, z.object({ type: z.literal('function'), /** * @deprecated use `pure` or `view` from {@link AbiStateMutability} instead * https://github.com/ethereum/solidity/issues/992 */ constant: z.boolean().optional(), /** * @deprecated Vyper used to provide gas estimates * https://github.com/vyperlang/vyper/issues/2151 */ gas: z.number().optional(), inputs: z.array(AbiParameter).readonly(), name: Identifier, outputs: z.array(AbiParameter).readonly(), /** * @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead * https://github.com/ethereum/solidity/issues/992 */ payable: z.boolean().optional(), stateMutability: AbiStateMutability, }), ) export const AbiConstructor = z.preprocess( (val) => { const abiFunction = val as unknown as AbiConstructorType // Calculate `stateMutability` for deprecated `payable` field if (abiFunction.stateMutability === undefined) { if (abiFunction.payable) abiFunction.stateMutability = 'payable' else abiFunction.stateMutability = 'nonpayable' } return val }, z.object({ type: z.literal('constructor'), /** * @deprecated use `pure` or `view` from {@link AbiStateMutability} instead * https://github.com/ethereum/solidity/issues/992 */ inputs: z.array(AbiParameter).readonly(), /** * @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead * https://github.com/ethereum/solidity/issues/992 */ payable: z.boolean().optional(), stateMutability: z.union([z.literal('nonpayable'), z.literal('payable')]), }), ) export const AbiFallback = z.preprocess( (val) => { const abiFunction = val as unknown as AbiFallbackType // Calculate `stateMutability` for deprecated `payable` field if (abiFunction.stateMutability === undefined) { if (abiFunction.payable) abiFunction.stateMutability = 'payable' else abiFunction.stateMutability = 'nonpayable' } return val }, z.object({ type: z.literal('fallback'), /** * @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead * https://github.com/ethereum/solidity/issues/992 */ payable: z.boolean().optional(), stateMutability: z.union([z.literal('nonpayable'), z.literal('payable')]), }), ) export const AbiReceive = z.object({ type: z.literal('receive'), stateMutability: z.literal('payable'), }) export const AbiEvent = z.object({ type: z.literal('event'), anonymous: z.boolean().optional(), inputs: z.array(AbiEventParameter).readonly(), name: Identifier, }) export const AbiError = z.object({ type: z.literal('error'), inputs: z.array(AbiParameter).readonly(), name: z.string(), }) export const AbiItemType = z.union([ z.literal('constructor'), z.literal('event'), z.literal('error'), z.literal('fallback'), z.literal('function'), z.literal('receive'), ]) /** * Zod Schema for Contract [ABI Specification](https://docs.soliditylang.org/en/latest/abi-spec.html#json) * * @example * const parsedAbi = Abi.parse([…]) */ export const Abi = z .array( z.union([ AbiError, AbiEvent, // TODO: Replace code below to `z.switch` (https://github.com/colinhacks/zod/issues/2106) // Need to redefine `AbiFunction | AbiConstructor | AbiFallback | AbiReceive` since `z.discriminate` doesn't support `z.preprocess` on `options` // https://github.com/colinhacks/zod/issues/1490 z.preprocess( (val) => { const abiItem = val as | AbiConstructorType | AbiFallbackType | AbiFunctionType | AbiReceiveType if (abiItem.type === 'receive') return abiItem // Calculate `stateMutability` for deprecated fields: `constant` and `payable` if ( (val as { stateMutability: AbiFunctionType['stateMutability'] }) .stateMutability === undefined ) { if ( abiItem.type === 'function' && (abiItem as AbiFunctionType).constant ) abiItem.stateMutability = 'view' else if ( ( abiItem as | AbiConstructorType | AbiFallbackType | AbiFunctionType ).payable ) abiItem.stateMutability = 'payable' else abiItem.stateMutability = 'nonpayable' } return val }, z.intersection( z.object({ /** * @deprecated use `pure` or `view` from {@link AbiStateMutability} instead * https://github.com/ethereum/solidity/issues/992 */ constant: z.boolean().optional(), /** * @deprecated Vyper used to provide gas estimates * https://github.com/vyperlang/vyper/issues/2151 */ gas: z.number().optional(), /** * @deprecated use `payable` or `nonpayable` from {@link AbiStateMutability} instead * https://github.com/ethereum/solidity/issues/992 */ payable: z.boolean().optional(), }), z.discriminatedUnion('type', [ z.object({ type: z.literal('function'), inputs: z.array(AbiParameter).readonly(), name: z.string().regex(/[a-zA-Z$_][a-zA-Z0-9$_]*/), outputs: z.array(AbiParameter).readonly(), stateMutability: AbiStateMutability, }), z.object({ type: z.literal('constructor'), inputs: z.array(AbiParameter).readonly(), stateMutability: z.union([ z.literal('payable'), z.literal('nonpayable'), ]), }), z.object({ type: z.literal('fallback'), inputs: z.tuple([]).optional(), stateMutability: z.union([ z.literal('payable'), z.literal('nonpayable'), ]), }), z.object({ type: z.literal('receive'), stateMutability: z.literal('payable'), }), ]), ), ), ]), ) .readonly() //////////////////////////////////////////////////////////////////////////////////////////////////// // Typed Data Types export const TypedDataDomain = z.object({ chainId: z.number().optional(), name: Identifier.optional(), salt: z.string().optional(), verifyingContract: Address.optional(), version: z.string().optional(), }) export const TypedDataType = z.union([ SolidityAddress, SolidityBool, SolidityBytes, SolidityString, SolidityInt, SolidityArray, ]) export const TypedDataParameter = z.object({ name: Identifier, type: z.string(), }) export const TypedData = z .record(Identifier, z.array(TypedDataParameter)) .transform((val, ctx) => validateTypedDataKeys(val, ctx)) // Helper Functions. function validateTypedDataKeys( typedData: Record<string, { type: string; name: string }[]>, zodContext: z.RefinementCtx, ): TTypedData { const keys = Object.keys(typedData) for (let i = 0; i < keys.length; i++) { if (isSolidityType(keys[i]!)) { zodContext.addIssue({ code: 'custom', message: `Invalid key. ${keys[i]} is a solidity type.`, }) return z.NEVER } validateTypedDataParameters(keys[i]!, typedData, zodContext) } return typedData as any } const typeWithoutTupleRegex = /^(?<type>[a-zA-Z$_][a-zA-Z0-9$_]*?)(?<array>(?:\[\d*?\])+?)?$/ function validateTypedDataParameters( key: string, typedData: Record<string, { type: string; name: string }[]>, zodContext: z.RefinementCtx, ancestors = new Set<string>(), ) { const val = typedData[key] as { type: string; name: string }[] const length = val.length for (let i = 0; i < length; i++) { if (val[i]?.type! === key) { zodContext.addIssue({ code: 'custom', message: `Invalid type. ${key} is a self reference.`, }) return z.NEVER } const match = execTyped<{ array?: string; type: string }>( typeWithoutTupleRegex, val[i]?.type!, ) if (!match?.type) { zodContext.addIssue({ code: 'custom', message: `Invalid type. ${key} does not have a type.`, }) return z.NEVER } if (match.type in typedData) { if (ancestors.has(match.type)) { zodContext.addIssue({ code: 'custom', message: `Invalid type. ${match.type} is a circular reference.`, }) return z.NEVER } validateTypedDataParameters( match.type, typedData, zodContext, new Set([...ancestors, match.type]), ) } else if (!isSolidityType(match.type)) { zodContext.addIssue({ code: 'custom', message: `Invalid type. ${match.type} is not a valid EIP-712 type.`, }) } } return }