From 00d038baeafab404a9faa5bf46301ced796eb97e Mon Sep 17 00:00:00 2001 From: Tom Meagher Date: Thu, 31 Aug 2023 11:59:33 -0400 Subject: [PATCH] refactor: generics --- src/actions/public/readContract.ts | 37 +++++-- src/index.ts | 5 + src/types/contract.test-d.ts | 123 ++++++++++++++++++++- src/types/contract.ts | 165 +++++++++++++++++++++++++++++ src/types/utils.test-d.ts | 11 ++ src/types/utils.ts | 26 +++++ 6 files changed, 357 insertions(+), 10 deletions(-) diff --git a/src/actions/public/readContract.ts b/src/actions/public/readContract.ts index 5cefff74a2..2ca7a6d562 100644 --- a/src/actions/public/readContract.ts +++ b/src/actions/public/readContract.ts @@ -5,8 +5,10 @@ import type { Transport } from '../../clients/transports/createTransport.js' import type { BaseError } from '../../errors/base.js' import type { Chain } from '../../types/chain.js' import type { - ContractFunctionConfig, - ContractFunctionResult, + ContractFunctionArgs, + ContractFunctionName, + ContractFunctionParameters, + ContractFunctionReturnType, } from '../../types/contract.js' import { type DecodeFunctionResultParameters, @@ -22,14 +24,30 @@ import { type CallParameters, call } from './call.js' export type ReadContractParameters< TAbi extends Abi | readonly unknown[] = Abi, - TFunctionName extends string = string, + TFunctionName extends ContractFunctionName< + TAbi, + 'pure' | 'view' + > = ContractFunctionName, + TArgs extends ContractFunctionArgs< + TAbi, + 'pure' | 'view', + TFunctionName + > = ContractFunctionArgs, > = Pick & - ContractFunctionConfig + ContractFunctionParameters export type ReadContractReturnType< TAbi extends Abi | readonly unknown[] = Abi, - TFunctionName extends string = string, -> = ContractFunctionResult + TFunctionName extends ContractFunctionName< + TAbi, + 'pure' | 'view' + > = ContractFunctionName, + TArgs extends ContractFunctionArgs< + TAbi, + 'pure' | 'view', + TFunctionName + > = ContractFunctionArgs, +> = ContractFunctionReturnType /** * Calls a read-only function on a contract, and returns the response. @@ -65,7 +83,8 @@ export type ReadContractReturnType< export async function readContract< TChain extends Chain | undefined, const TAbi extends Abi | readonly unknown[], - TFunctionName extends string, + TFunctionName extends ContractFunctionName, + TArgs extends ContractFunctionArgs, >( client: Client, { @@ -74,8 +93,8 @@ export async function readContract< args, functionName, ...callRequest - }: ReadContractParameters, -): Promise> { + }: ReadContractParameters, +): Promise> { const calldata = encodeFunctionData({ abi, args, diff --git a/src/index.ts b/src/index.ts index e8504c0079..aabd161008 100644 --- a/src/index.ts +++ b/src/index.ts @@ -562,6 +562,11 @@ export type { InferEventName, InferFunctionName, InferItemName, + /// + ContractFunctionName, + ContractFunctionArgs, + ContractFunctionParameters, + ContractFunctionReturnType, } from './types/contract.js' export type { AccessList, diff --git a/src/types/contract.test-d.ts b/src/types/contract.test-d.ts index a80a813f0c..938b326d17 100644 --- a/src/types/contract.test-d.ts +++ b/src/types/contract.test-d.ts @@ -1,4 +1,4 @@ -import type { Abi, ResolvedConfig } from 'abitype' +import { type Abi, type Address, type ResolvedConfig, parseAbi } from 'abitype' import type { seaportAbi } from 'abitype/test' import { expectTypeOf, test } from 'vitest' @@ -6,8 +6,11 @@ import type { AbiEventParameterToPrimitiveType, AbiEventParametersToPrimitiveTypes, AbiEventTopicToPrimitiveType, + ContractFunctionArgs, ContractFunctionConfig, + ContractFunctionName, ContractFunctionResult, + ContractFunctionReturnType, GetConstructorArgs, GetErrorArgs, GetEventArgs, @@ -19,6 +22,7 @@ import type { InferFunctionName, InferItemName, LogTopicType, + Widen, } from './contract.js' import type { Hex } from './misc.js' @@ -525,3 +529,120 @@ test('AbiEventParametersToPrimitiveTypes', () => { | readonly [string | string[] | null, number | number[] | null] >() }) + +test('ContractFunctionName', () => { + expectTypeOf>().toEqualTypeOf< + | 'cancel' + | 'fulfillBasicOrder' + | 'fulfillBasicOrder_efficient_6GL6yc' + | 'fulfillOrder' + | 'fulfillAdvancedOrder' + | 'fulfillAvailableOrders' + | 'fulfillAvailableAdvancedOrders' + | 'getContractOffererNonce' + | 'getOrderHash' + | 'getOrderStatus' + | 'getCounter' + | 'incrementCounter' + | 'information' + | 'name' + | 'matchAdvancedOrders' + | 'matchOrders' + | 'validate' + >() + + expectTypeOf< + ContractFunctionName + >().toEqualTypeOf< + | 'name' + | 'getContractOffererNonce' + | 'getCounter' + | 'getOrderHash' + | 'getOrderStatus' + | 'information' + >() +}) + +test('ContractFunctionArgs', () => { + expectTypeOf< + ContractFunctionArgs + >().toEqualTypeOf() + + const abi = parseAbi([ + 'function foo() view returns (int8)', + 'function foo(address) view returns (string)', + 'function foo(address, address) view returns ((address foo, address bar))', + 'function bar() view returns (int8)', + ]) + expectTypeOf< + ContractFunctionArgs + >().toEqualTypeOf< + readonly [] | readonly [Address] | readonly [Address, Address] + >() +}) + +test('Widen', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf
() + expectTypeOf>().toEqualTypeOf< + ResolvedConfig['BytesType']['inputs'] + >() + + expectTypeOf>().toEqualTypeOf() + + expectTypeOf>().toEqualTypeOf<{ + foo: string + boo: bigint + }>() + expectTypeOf>().toEqualTypeOf<{ + foo: string + boo: readonly [bigint] + }>() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf< + readonly [{ foo: string; boo: bigint }] + >() + + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf< + readonly (bigint | Address)[] + >() + + expectTypeOf>().toEqualTypeOf() +}) + +test('ContractFunctionReturnType', () => { + const abi = parseAbi([ + 'function foo() view returns (int8)', + 'function foo(address) view returns (string)', + 'function foo(address, address) view returns ((address foo, address bar))', + 'function bar() view returns (int8)', + ]) + + expectTypeOf< + ContractFunctionReturnType + >().toEqualTypeOf() + expectTypeOf< + ContractFunctionReturnType + >().toEqualTypeOf() + expectTypeOf< + ContractFunctionReturnType< + typeof abi, + 'pure' | 'view', + 'foo', + readonly ['0x'] + > + >().toEqualTypeOf() + expectTypeOf< + ContractFunctionReturnType< + typeof abi, + 'pure' | 'view', + 'foo', + readonly ['0x', '0x'] + > + >().toEqualTypeOf<{ foo: Address; bar: Address }>() +}) diff --git a/src/types/contract.ts b/src/types/contract.ts index ea764bea83..0d776dfcc7 100644 --- a/src/types/contract.ts +++ b/src/types/contract.ts @@ -16,6 +16,7 @@ import type { ExtractAbiFunction, ExtractAbiFunctionNames, Narrow, + ResolvedConfig, } from 'abitype' import type { Hex, LogTopic } from './misc.js' @@ -23,9 +24,12 @@ import type { TransactionRequest } from './transaction.js' import type { Filter, IsNarrowable, + IsUnion, MaybeRequired, NoUndefined, Prettify, + UnionEvaluate, + UnionToTuple, } from './utils.js' export type AbiItem = Abi[number] @@ -371,3 +375,164 @@ export type AbiEventParametersToPrimitiveTypes< > : never : never + +////////////////////////////////////////////////////////////////////////////////////////////////// + +export type ContractFunctionName< + abi extends Abi | readonly unknown[] = Abi, + mutability extends AbiStateMutability = AbiStateMutability, +> = ExtractAbiFunctionNames< + abi extends Abi ? abi : Abi, + mutability +> extends infer functionName extends string + ? [functionName] extends [never] + ? string + : functionName + : string + +export type ContractFunctionArgs< + abi extends Abi | readonly unknown[] = Abi, + mutability extends AbiStateMutability = AbiStateMutability, + functionName extends ContractFunctionName< + abi, + mutability + > = ContractFunctionName, +> = AbiParametersToPrimitiveTypes< + ExtractAbiFunction< + abi extends Abi ? abi : Abi, + functionName, + mutability + >['inputs'], + 'inputs' +> extends infer args + ? [args] extends [never] + ? readonly unknown[] + : args + : readonly unknown[] + +export type ExtractAbiFunctionForArgs< + abi extends Abi, + mutability extends AbiStateMutability, + functionName extends ContractFunctionName, + args extends ContractFunctionArgs, +> = ExtractAbiFunction< + abi, + functionName, + mutability +> extends infer abiFunction extends AbiFunction + ? IsUnion extends true // narrow overloads using `args` by converting to tuple and filtering out overloads that don't match + ? UnionToTuple extends infer abiFunctions extends readonly AbiFunction[] + ? { + [k in keyof abiFunctions]: ( + readonly [] extends args + ? readonly [] // fallback to `readonly []` if `args` has no value (e.g. `args` property not provided) + : args + ) extends AbiParametersToPrimitiveTypes< + abiFunctions[k]['inputs'], + 'inputs' + > + ? abiFunctions[k] + : never + }[number] // convert back to union (removes `never` tuple entries: `['foo', never, 'bar'][number]` => `'foo' | 'bar'`) + : never + : abiFunction + : never + +export type ContractFunctionParameters< + abi extends Abi | readonly unknown[] = Abi, + mutability extends AbiStateMutability = AbiStateMutability, + functionName extends ContractFunctionName< + abi, + mutability + > = ContractFunctionName, + args extends ContractFunctionArgs< + abi, + mutability, + functionName + > = ContractFunctionArgs, + /// + allArgs = ContractFunctionArgs, + allFunctionNames = ContractFunctionName, + // when `args` is inferred to `readonly []` ("inputs": []) or `never` (`abi` declared as `Abi` or not inferrable), allow `args` to be optional. + // important that both branches return same structural type +> = { + address: Address + abi: abi + functionName: + | allFunctionNames // show all options + | (functionName extends allFunctionNames ? functionName : never) // infer value +} & UnionEvaluate< + readonly [] extends allArgs + ? { + args?: + | allArgs // show all options + // infer value, widen inferred value of `args` conditionally to match `allArgs` + | (abi extends Abi + ? args extends allArgs + ? Widen + : never + : never) + | undefined + } + : { + args: + | allArgs // show all options + | (Widen & (args extends allArgs ? unknown : never)) // infer value, widen inferred value of `args` match `allArgs` (e.g. avoid union `args: readonly [123n] | readonly [bigint]`) + } +> + +export type Widen = + | ([unknown] extends [type] ? unknown : never) + | (type extends Function ? type : never) + | (type extends ResolvedConfig['BigIntType'] ? bigint : never) + | (type extends boolean ? boolean : never) + | (type extends ResolvedConfig['IntType'] ? number : never) + | (type extends string + ? type extends ResolvedConfig['AddressType'] + ? ResolvedConfig['AddressType'] + : type extends ResolvedConfig['BytesType']['inputs'] + ? ResolvedConfig['BytesType'] + : string + : never) + | (type extends readonly [] ? readonly [] : never) + | (type extends Record + ? { [K in keyof type]: Widen } + : never) + | (type extends { length: number } + ? { + [K in keyof type]: Widen + } extends infer Val extends readonly unknown[] + ? readonly [...Val] + : never + : never) + +export type ContractFunctionReturnType< + abi extends Abi | readonly unknown[] = Abi, + mutability extends AbiStateMutability = AbiStateMutability, + functionName extends ContractFunctionName< + abi, + mutability + > = ContractFunctionName, + args extends ContractFunctionArgs< + abi, + mutability, + functionName + > = ContractFunctionArgs, +> = abi extends Abi + ? Abi extends abi + ? unknown + : AbiParametersToPrimitiveTypes< + ExtractAbiFunctionForArgs< + abi, + mutability, + functionName, + args + >['outputs'] + > extends infer types + ? types extends readonly [] + ? void + : types extends readonly [infer type] + ? type + : types + : never + : unknown diff --git a/src/types/utils.test-d.ts b/src/types/utils.test-d.ts index 2c0f16e0c4..3ede94766e 100644 --- a/src/types/utils.test-d.ts +++ b/src/types/utils.test-d.ts @@ -5,8 +5,10 @@ import type { IsNarrowable, IsNever, IsUndefined, + IsUnion, Or, RequiredBy, + UnionToTuple, } from './utils.js' test('Filter', () => { @@ -56,3 +58,12 @@ test('RequiredBy', () => { RequiredBy<{ a?: number; b?: string; c: boolean }, 'a' | 'c'> >().toEqualTypeOf<{ a: number; b?: string; c: boolean }>() }) + +test('UnionToTuple', () => { + expectTypeOf>().toEqualTypeOf<['foo', 'bar']>() +}) + +test('IsUnion', () => { + expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() +}) diff --git a/src/types/utils.ts b/src/types/utils.ts index b07f9a5a94..48729e756f 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -242,3 +242,29 @@ export type Trim = TrimLeft< * => string | number */ export type ValueOf = T[keyof T] + +export type UnionEvaluate = type extends object ? Prettify : type + +export type UnionToTuple< + union, + /// + last = LastInUnion, +> = [union] extends [never] ? [] : [...UnionToTuple>, last] +type LastInUnion = UnionToIntersection< + U extends unknown ? (x: U) => 0 : never +> extends (x: infer l) => 0 + ? l + : never +type UnionToIntersection = ( + union extends unknown + ? (arg: union) => 0 + : never +) extends (arg: infer i) => 0 + ? i + : never + +export type IsUnion< + union, + /// + union2 = union, +> = union extends union2 ? ([union2] extends [union] ? false : true) : never