diff --git a/.changeset/fresh-jokes-guess.md b/.changeset/fresh-jokes-guess.md new file mode 100644 index 00000000..c55ae14e --- /dev/null +++ b/.changeset/fresh-jokes-guess.md @@ -0,0 +1,5 @@ +--- +"abitype": minor +--- + +Added experimental named tuple support to `AbiParametersToPrimitiveTypes` diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 4f802115..812dc269 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -50,7 +50,7 @@ jobs: timeout-minutes: 5 strategy: matrix: - version: ['5.0.4', '5.1.6', '5.2.2', '5.3.3', '5.4.5', '5.5.2', '5.6', 'latest'] + version: ['5.0.4', '5.2.2', '5.3.3', '5.4.5', '5.5.4', '5.6.3', '5.7.3', '5.8.3', '5.9.3', 'latest'] steps: - name: Clone repository diff --git a/docs/pages/config.md b/docs/pages/config.md index 5297b012..2dc703e9 100644 --- a/docs/pages/config.md +++ b/docs/pages/config.md @@ -153,6 +153,21 @@ declare module 'abitype' { } ``` +### `experimental_namedTuples` + +Enables named tuple generation in [`AbiParametersToPrimitiveTypes`](/api/utilities#abiparameterstoprimitivetypes) for common ABI parameter names. + +- Type `boolean` +- Default `false` + +```ts twoslash +declare module 'abitype' { + export interface Register { + experimental_namedTuples: false + } +} +``` + ### `strictAbiType` When set, validates `AbiParameter`'s `type` against `AbiType`. diff --git a/packages/abitype/src/abi.ts b/packages/abitype/src/abi.ts index 1eaa84ca..f526bca4 100644 --- a/packages/abitype/src/abi.ts +++ b/packages/abitype/src/abi.ts @@ -245,3 +245,156 @@ export type TypedData = Pretty< [_ in TypedDataType]?: never } > + +//////////////////////////////////////////////////////////////////////////////////////////////////// + +// update lookup with `pnpm node scripts/genAbiParameterNameLookup.ts` +// biome-ignore format: no formatting +export interface AbiParameterTupleNameLookup extends Record { + _data: [_data: type] + a: [a: type] + account: [account: type] + accounts: [accounts: type] + address: [address: type] + addresses: [addresses: type] + admin: [admin: type] + allowFailure: [allowFailure: type] + allowed: [allowed: type] + amount: [amount: type] + approved: [approved: type] + approver: [approver: type] + ask: [ask: type] + asset: [asset: type] + assets: [assets: type] + authority: [authority: type] + available: [available: type] + b: [b: type] + balance: [balance: type] + bid: [bid: type] + buffer: [buffer: type] + c: [c: type] + call: [call: type] + callData: [callData: type] + caller: [caller: type] + calls: [calls: type] + clone: [clone: type] + coinType: [coinType: type] + count: [count: type] + currency: [currency: type] + d: [d: type] + data: [data: type] + deadline: [deadline: type] + decimals: [decimals: type] + dest: [dest: type] + divisor: [divisor: type] + dns: [dns: type] + dst: [dst: type] + e: [e: type] + endTime: [endTime: type] + ens: [ens: type] + errorData: [errorData: type] + f: [f: type] + failures: [failures: type] + from: [from: type] + funder: [funder: type] + g: [g: type] + gateway: [gateway: type] + gateways: [gateways: type] + guy: [guy: type] + h: [h: type] + hash: [hash: type] + hashes: [hashes: type] + i: [i: type] + id: [id: type] + ids: [ids: type] + idsLength: [idsLength: type] + implementation: [implementation: type] + index: [index: type] + interfaceId: [interfaceId: type] + j: [j: type] + k: [k: type] + key: [key: type] + l: [l: type] + label: [label: type] + length: [length: type] + limit: [limit: type] + m: [m: type] + market: [market: type] + memo: [memo: type] + message: [message: type] + n: [n: type] + name: [name: type] + needed: [needed: type] + new: [new: type] + next: [next: type] + nextOwner: [nextOwner: type] + node: [node: type] + nonce: [nonce: type] + nonceKey: [nonceKey: type] + numerator: [numerator: type] + o: [o: type] + offerer: [offerer: type] + old: [old: type] + operator: [operator: type] + order: [order: type] + orders: [orders: type] + owner: [owner: type] + p: [p: type] + policyId: [policyId: type] + policyType: [policyType: type] + previous: [previous: type] + previousOwner: [previousOwner: type] + price: [price: type] + primary: [primary: type] + proposer: [proposer: type] + q: [q: type] + queries: [queries: type] + quoteToken: [quoteToken: type] + r: [r: type] + receiver: [receiver: type] + recipient: [recipient: type] + refund: [refund: type] + required: [required: type] + resolvedName: [resolvedName: type] + resolver: [resolver: type] + responses: [responses: type] + restricted: [restricted: type] + returnData: [returnData: type] + reverseName: [reverseName: type] + reverseResolver: [reverseResolver: type] + s: [s: type] + secs: [secs: type] + selector: [selector: type] + sender: [sender: type] + shares: [shares: type] + signature: [signature: type] + signer: [signer: type] + source: [source: type] + src: [src: type] + startTime: [startTime: type] + status: [status: type] + success: [success: type] + symbol: [symbol: type] + t: [t: type] + target: [target: type] + timestamp: [timestamp: type] + to: [to: type] + token: [token: type] + tokenId: [tokenId: type] + ttl: [ttl: type] + u: [u: type] + updater: [updater: type] + user: [user: type] + v: [v: type] + value: [value: type] + values: [values: type] + valuesLength: [valuesLength: type] + version: [version: type] + w: [w: type] + wad: [wad: type] + weth: [weth: type] + x: [x: type] + y: [y: type] + z: [z: type] + zone: [zone: type] +} diff --git a/packages/abitype/src/register.ts b/packages/abitype/src/register.ts index 4f29120e..f2ed7695 100644 --- a/packages/abitype/src/register.ts +++ b/packages/abitype/src/register.ts @@ -88,6 +88,17 @@ export type ResolvedRegister = { ? type : DefaultRegister['fixedArrayMaxLength'] + /** + * Enables named tuple generation in {@link AbiParametersToPrimitiveTypes} for common ABI parameter names. + * + * @default false + */ + experimental_namedTuples: Register extends { + experimental_namedTuples: infer type extends boolean + } + ? type + : DefaultRegister['experimental_namedTuples'] + /** * When set, validates {@link AbiParameter}'s `type` against {@link AbiType} * @@ -142,6 +153,9 @@ export type DefaultRegister = { /** TypeScript type to use for `int` and `uint` values, where `M <= 48` */ intType: number + /** Enables named tuple generation in {@link AbiParametersToPrimitiveTypes} for common ABI parameter names */ + experimental_namedTuples: false + /** When set, validates {@link AbiParameter}'s `type` against {@link AbiType} */ strictAbiType: false diff --git a/packages/abitype/src/utils.bench-d.ts b/packages/abitype/src/utils.bench-d.ts index 424c8a3f..23fde430 100644 --- a/packages/abitype/src/utils.bench-d.ts +++ b/packages/abitype/src/utils.bench-d.ts @@ -1,9 +1,10 @@ import { attest } from '@arktype/attest' import { describe, test } from 'vitest' - +import type { erc20Abi } from './abis/json.js' import type { AbiParameterToPrimitiveType, AbiParametersToPrimitiveTypes, + ExtractAbiFunction, TypedDataToPrimitiveTypes, } from './utils.js' @@ -225,7 +226,7 @@ test('deeply nested parameters', () => { attest.instantiations([11348, 'instantiations']) attest< [ - { + s: { a: number b: readonly number[] c: readonly { @@ -266,3 +267,53 @@ test('self-referencing', () => { } }>(res) }) + +type transferFrom = ExtractAbiFunction< + typeof erc20Abi, + 'transferFrom' +>['inputs'] +test('basic without named tuple', () => { + const res = {} as AbiParametersToPrimitiveTypes + attest.instantiations([906, 'instantiations']) + attest< + readonly [sender: `0x${string}`, recipient: `0x${string}`, amount: bigint] + >(res) +}) +test('basic with named tuple', () => { + const res = {} as AbiParametersToPrimitiveTypes + attest.instantiations([1131, 'instantiations']) + attest< + readonly [sender: `0x${string}`, recipient: `0x${string}`, amount: bigint] + >(res) +}) + +type parameters = readonly [ + { name: 'account'; type: 'uint8' }, + { name: 'address'; type: 'uint8' }, + { name: 'admin'; type: 'uint8' }, + { name: 'allowed'; type: 'uint8' }, + { name: 'amount'; type: 'uint8' }, + { name: 'authority'; type: 'uint8' }, + { name: 'available'; type: 'uint8' }, + { name: 'count'; type: 'uint8' }, + { name: 'currency'; type: 'uint8' }, + { name: 'deadline'; type: 'uint8' }, + { name: 'from'; type: 'uint8' }, + { name: 'funder'; type: 'uint8' }, + { name: 'hash'; type: 'address' }, + { name: 'id'; type: 'uint8' }, + { name: 'memo'; type: 'uint8' }, + { name: 'nonce'; type: 'uint8' }, + { name: 'nonceKey'; type: 'uint8' }, + { name: 'owner'; type: 'uint8' }, + { name: 'policyId'; type: 'uint8' }, + { name: 'policyType'; type: 'uint8' }, +] +test('without named tuples', () => { + ;({}) as AbiParametersToPrimitiveTypes + attest.instantiations([1276, 'instantiations']) +}) +test('with named tuples', () => { + ;({}) as AbiParametersToPrimitiveTypes + attest.instantiations([1998, 'instantiations']) +}) diff --git a/packages/abitype/src/utils.test-d.ts b/packages/abitype/src/utils.test-d.ts index 8c17e6c6..465a95f4 100644 --- a/packages/abitype/src/utils.test-d.ts +++ b/packages/abitype/src/utils.test-d.ts @@ -1,9 +1,10 @@ import { assertType, describe, expectTypeOf, test } from 'vitest' -import type { Abi } from './abi.js' +import type { Abi, AbiParameterKind } from './abi.js' import type { customSolidityErrorsAbi, ensRegistryWithFallbackAbi, + erc20Abi, nestedTupleArrayAbi, nounsAuctionHouseAbi, wagmiMintExampleAbi, @@ -423,12 +424,14 @@ describe('AbiParameterToPrimitiveType', () => { describe('AbiParametersToPrimitiveTypes', () => { test('no parameters', () => { - type Result = AbiParametersToPrimitiveTypes<[]> - assertType([]) + type r1 = AbiParametersToPrimitiveTypes<[]> + assertType([]) + type r2 = AbiParametersToPrimitiveTypes<[], AbiParameterKind, true> + assertType([]) }) test('single parameter', () => { - type Result = AbiParametersToPrimitiveTypes< + type r1 = AbiParametersToPrimitiveTypes< [ { name: 'tokenId' @@ -436,23 +439,48 @@ describe('AbiParametersToPrimitiveTypes', () => { }, ] > - assertType([[1, 1]]) + assertType([[1, 1]]) // ^? + type r2 = AbiParametersToPrimitiveTypes< + [ + { + name: 'tokenId' + type: 'uint8[2]' + }, + ], + AbiParameterKind, + true + > + expectTypeOf().toEqualTypeOf< + readonly [tokenId: readonly [number, number]] + >() }) test('multiple parameters', () => { - type Result = AbiParametersToPrimitiveTypes< + type r1 = AbiParametersToPrimitiveTypes< [ { name: 'to'; type: 'address' }, { name: 'tokenId'; type: 'uint256' }, { name: 'trait'; type: 'string[]' }, ] > - assertType([zeroAddress, 1n, ['foo']]) + assertType([zeroAddress, 1n, ['foo']]) + type r2 = AbiParametersToPrimitiveTypes< + [ + { name: 'to'; type: 'address' }, + { name: 'tokenId'; type: 'uint256' }, + { name: 'trait'; type: 'string[]' }, + ], + AbiParameterKind, + true + > + expectTypeOf().toEqualTypeOf< + readonly [to: `0x${string}`, tokenId: bigint, readonly string[]] + >() }) test('deeply nested parameters', () => { - type Result = AbiParametersToPrimitiveTypes< + type r1 = AbiParametersToPrimitiveTypes< [ { name: 's' @@ -489,7 +517,55 @@ describe('AbiParametersToPrimitiveTypes', () => { }, ] > - assertType([ + assertType([ + { a: 1, b: [2], c: [{ x: 1, y: 1 }] }, + { x: 1, y: 1 }, + 1, + [ + { x: 1n, y: 1n }, + { x: 1n, y: 1n }, + ], + ]) + type r2 = AbiParametersToPrimitiveTypes< + [ + { + name: 's' + type: 'tuple' + components: [ + { name: 'a'; type: 'uint8' }, + { name: 'b'; type: 'uint8[]' }, + { + name: 'c' + type: 'tuple[]' + components: [ + { name: 'x'; type: 'uint8' }, + { name: 'y'; type: 'uint8' }, + ] + }, + ] + }, + { + name: 't' + type: 'tuple' + components: [ + { name: 'x'; type: 'uint8' }, + { name: 'y'; type: 'uint8' }, + ] + }, + { name: 'a'; type: 'uint8' }, + { + name: 't' + type: 'tuple[2]' + components: [ + { name: 'x'; type: 'uint256' }, + { name: 'y'; type: 'uint256' }, + ] + }, + ], + AbiParameterKind, + true + > + assertType([ { a: 1, b: [2], c: [{ x: 1, y: 1 }] }, { x: 1, y: 1 }, 1, @@ -513,9 +589,24 @@ describe('AbiParametersToPrimitiveTypes', () => { assertType>([ '0xfoo', ]) - assertType>([ - '0xfoo', - ]) + assertType< + AbiParametersToPrimitiveTypes + >(['0xfoo']) + assertType< + AbiParametersToPrimitiveTypes + >(['0xfoo']) + }) + + test('named parameters', () => { + type Result = AbiParametersToPrimitiveTypes< + // ^? + ExtractAbiFunction['inputs'], + 'inputs', + true + > + expectTypeOf().toEqualTypeOf< + readonly [sender: `0x${string}`, recipient: `0x${string}`, amount: bigint] + >() }) }) diff --git a/packages/abitype/src/utils.ts b/packages/abitype/src/utils.ts index 6f0bfca6..bc3e8087 100644 --- a/packages/abitype/src/utils.ts +++ b/packages/abitype/src/utils.ts @@ -2,6 +2,7 @@ import type { Abi, AbiParameter, AbiParameterKind, + AbiParameterTupleNameLookup, AbiStateMutability, AbiType, MBits, @@ -188,16 +189,122 @@ type AbiArrayToPrimitiveType< export type AbiParametersToPrimitiveTypes< abiParameters extends readonly AbiParameter[], abiParameterKind extends AbiParameterKind = AbiParameterKind, + /// + experimental_namedTuples extends + boolean = ResolvedRegister['experimental_namedTuples'], +> = experimental_namedTuples extends true + ? AbiParametersToPrimitiveTypes_named + : AbiParametersToPrimitiveTypes_mapped + +export type AbiParametersToPrimitiveTypes_mapped< + abiParameters extends readonly AbiParameter[], + abiParameterKind extends AbiParameterKind = AbiParameterKind, > = Pretty<{ - // TODO: Convert to labeled tuple so parameter names show up in autocomplete - // e.g. [foo: string, bar: string] - // https://github.com/microsoft/TypeScript/issues/44939 [key in keyof abiParameters]: AbiParameterToPrimitiveType< abiParameters[key], abiParameterKind > }> +export type AbiParametersToPrimitiveTypes_named< + abiParameters extends readonly AbiParameter[], + abiParameterKind extends AbiParameterKind = AbiParameterKind, + /// + acc extends readonly unknown[] = [], + // FIXME: Workaround to create labeled tuple so parameter names show up in autocomplete + // e.g. [foo: string, bar: string] + // Ideally this is a simple mapped type instead of tail recurision, but TypeScript does not support dynamic tuple labels + // https://github.com/microsoft/TypeScript/issues/44939 +> = abiParameters extends readonly [ + // Significantly reduce type instantiations by batch proccessing up to six parameters at a time instead of processing one parameter per recursion + infer head1 extends AbiParameter, + infer head2 extends AbiParameter, + infer head3 extends AbiParameter, + infer head4 extends AbiParameter, + infer head5 extends AbiParameter, + infer head6 extends AbiParameter, + ...infer tail extends readonly AbiParameter[], +] + ? AbiParametersToPrimitiveTypes_named< + tail, + abiParameterKind, + readonly [ + ...acc, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ] + > + : abiParameters extends readonly [ + infer head1 extends AbiParameter, + infer head2 extends AbiParameter, + infer head3 extends AbiParameter, + infer head4 extends AbiParameter, + infer head5 extends AbiParameter, + ] + ? readonly [ + ...acc, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ] + : abiParameters extends readonly [ + infer head1 extends AbiParameter, + infer head2 extends AbiParameter, + infer head3 extends AbiParameter, + infer head4 extends AbiParameter, + ] + ? readonly [ + ...acc, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ] + : abiParameters extends readonly [ + infer head1 extends AbiParameter, + infer head2 extends AbiParameter, + infer head3 extends AbiParameter, + ] + ? readonly [ + ...acc, + ...ToNamedTuple, + ...ToNamedTuple, + ...ToNamedTuple, + ] + : abiParameters extends readonly [ + infer head1 extends AbiParameter, + infer head2 extends AbiParameter, + ] + ? readonly [ + ...acc, + ...ToNamedTuple, + ...ToNamedTuple, + ] + : abiParameters extends readonly [infer head extends AbiParameter] + ? readonly [...acc, ...ToNamedTuple] + : acc extends readonly [] + ? abiParameters extends readonly [] + ? readonly [] + : readonly unknown[] + : acc + +type ToNamedTuple< + abiParameter extends AbiParameter, + abiParameterKind extends AbiParameterKind, +> = unwrapName< + AbiParameterToPrimitiveType, + abiParameter['name'] +> +type unwrapName = name extends string + ? AbiParameterTupleNameLookup[name] + : [type] + /** * Checks if type is {@link Abi}. * diff --git a/playgrounds/functions/src/read.test-d.ts b/playgrounds/functions/src/read.test-d.ts index 6ee334a7..556fe029 100644 --- a/playgrounds/functions/src/read.test-d.ts +++ b/playgrounds/functions/src/read.test-d.ts @@ -220,8 +220,8 @@ const abi = parseAbi([ 'function foo() returns (bool)', 'function foo() returns (uint8)', 'function foo(uint) view returns (address)', - 'function foo(address) view returns (uint)', - 'function foo(uint256, address) view returns (address, uint8)', + 'function foo(address account) view returns (uint)', + 'function foo(uint256 amount, address account) view returns (address, uint8)', 'function bar() pure returns (address)', 'function baz(uint) pure returns (string)', 'function boo(bytes32) pure returns (bytes32)', diff --git a/playgrounds/functions/src/types.ts b/playgrounds/functions/src/types.ts index 18024f5b..71ed6fdb 100644 --- a/playgrounds/functions/src/types.ts +++ b/playgrounds/functions/src/types.ts @@ -36,7 +36,8 @@ type GetArgs< : AbiFunction, primitiveTypes = AbiParametersToPrimitiveTypes< abiFunction['inputs'], - 'inputs' + 'inputs', + true >, args_ = | primitiveTypes // show all values @@ -91,7 +92,8 @@ export type ContractReturnType< outputs extends readonly AbiParameter[] = abiFunction['outputs'], primitiveTypes extends readonly unknown[] = AbiParametersToPrimitiveTypes< outputs, - 'outputs' + 'outputs', + true >, > = [abiFunction] extends [never] ? unknown // `abiFunction` was not inferrable (e.g. `abi` declared as `Abi`) diff --git a/playgrounds/functions/src/watchEvent.ts b/playgrounds/functions/src/watchEvent.ts index 4c85ae76..4c1ca203 100644 --- a/playgrounds/functions/src/watchEvent.ts +++ b/playgrounds/functions/src/watchEvent.ts @@ -21,7 +21,11 @@ export type WatchEventParameters< abiEvent extends AbiEvent = abi extends Abi ? ExtractAbiEvent : AbiEvent, - primitiveTypes = AbiParametersToPrimitiveTypes, + primitiveTypes = AbiParametersToPrimitiveTypes< + abiEvent['inputs'], + 'inputs', + true + >, > = { abi: abi eventName: diff --git a/scripts/genAbiParameterNameLookup.ts b/scripts/genAbiParameterNameLookup.ts new file mode 100644 index 00000000..4c938628 --- /dev/null +++ b/scripts/genAbiParameterNameLookup.ts @@ -0,0 +1,174 @@ +import fs from 'node:fs/promises' + +// Generates ABI parameter name lookup from common names + +console.log('Generating ABI parameter name lookup.') + +let content = + 'export interface AbiParameterTupleNameLookup extends Record {\n' +for (const name of names()) content += ` ${name}: [${name}: type]\n` +content += '}\n' + +const path = './packages/abitype/src/abi.ts' +const text = await fs + .readFile(path, 'utf8') + .then((text) => + text.replace( + /export interface AbiParameterTupleNameLookup[\s\S]*$/, + content, + ), + ) +await fs.writeFile(path, text, 'utf8') + +console.log(`Done. Added ${names().size} names.`) + +function names() { + return new Set([ + '_data', + 'a', + 'account', + 'accounts', + 'address', + 'addresses', + 'admin', + 'allowFailure', + 'allowed', + 'amount', + 'approved', + 'approver', + 'ask', + 'asset', + 'assets', + 'authority', + 'available', + 'b', + 'balance', + 'bid', + 'buffer', + 'c', + 'call', + 'callData', + 'caller', + 'calls', + 'clone', + 'coinType', + 'count', + 'currency', + 'd', + 'data', + 'deadline', + 'decimals', + 'dest', + 'divisor', + 'dns', + 'dst', + 'e', + 'endTime', + 'ens', + 'errorData', + 'f', + 'failures', + 'from', + 'funder', + 'g', + 'gateway', + 'gateways', + 'guy', + 'h', + 'hash', + 'hashes', + 'i', + 'id', + 'ids', + 'idsLength', + 'implementation', + 'index', + 'interfaceId', + 'j', + 'k', + 'key', + 'l', + 'label', + 'length', + 'limit', + 'm', + 'market', + 'memo', + 'message', + 'n', + 'name', + 'needed', + 'new', + 'next', + 'nextOwner', + 'node', + 'nonce', + 'nonceKey', + 'numerator', + 'o', + 'offerer', + 'old', + 'operator', + 'order', + 'orders', + 'owner', + 'p', + 'policyId', + 'policyType', + 'previous', + 'previousOwner', + 'price', + 'primary', + 'proposer', + 'q', + 'queries', + 'quoteToken', + 'r', + 'receiver', + 'recipient', + 'refund', + 'required', + 'resolvedName', + 'resolver', + 'responses', + 'restricted', + 'returnData', + 'reverseName', + 'reverseResolver', + 's', + 'secs', + 'selector', + 'sender', + 'shares', + 'signature', + 'signer', + 'source', + 'src', + 'startTime', + 'status', + 'success', + 'symbol', + 't', + 'target', + 'timestamp', + 'to', + 'token', + 'tokenId', + 'ttl', + 'u', + 'updater', + 'user', + 'v', + 'value', + 'values', + 'valuesLength', + 'version', + 'w', + 'wad', + 'weth', + 'x', + 'y', + 'z', + 'zone', + ]) +}