diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c133c7462d..56745bfb868 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -903,6 +903,11 @@ should use 4.0.1-alpha.0 for testing. - Export a new function `uuidV4` that generates a random v4 Uuid (#5373). +#### web3-eth-contract + +- `SpecialOutput` type was added as a generic type into the call function to support reassigning output types (#5631) +- Overloaded functions types (`ContractOverloadedMethodInputs`, `ContractOverloadedMethodOutputs`) was added (#5631) + ### Fixed #### web3-eth-contract @@ -917,6 +922,10 @@ should use 4.0.1-alpha.0 for testing. - Use Uuid for the response id, to fix the issue "Responses get mixed up due to conflicting payload IDs" (#5373). +#### web3-eth-abi + +- Fix ContractMethodOutputParameters type to support output object types by index and string key. Also, it returns void if ABI doesn't have outputs and returns exactly one type if the output array has only one element. (#5631) + ### Removed #### web3-eth-accounts diff --git a/packages/web3-eth-abi/CHANGELOG.md b/packages/web3-eth-abi/CHANGELOG.md index deed4f9e417..4a58fa0cda4 100644 --- a/packages/web3-eth-abi/CHANGELOG.md +++ b/packages/web3-eth-abi/CHANGELOG.md @@ -47,3 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Return `BigInt` instead of `string` when decoding function parameters for large numbers, such as `uint256`. (#5435) ## [Unreleased] + +### Fixed + +- Fix ContractMethodOutputParameters type to support output object types by index and string key. Also, it returns void if ABI doesn't have outputs and returns exactly one type if the output array has only one element. (5631) diff --git a/packages/web3-eth-abi/src/types.ts b/packages/web3-eth-abi/src/types.ts index 926fdc66402..7e443862310 100644 --- a/packages/web3-eth-abi/src/types.ts +++ b/packages/web3-eth-abi/src/types.ts @@ -173,6 +173,11 @@ export type PrimitiveTupleType< : never : never; +type ObjectToArray = T extends [...infer R, infer A] + ? Record & ObjectToArray + : T; +type ArrToObjectWithFunctions = Array & ObjectToArray; + export type MatchPrimitiveType< Type extends string, Components extends ReadonlyArray | undefined, @@ -185,18 +190,52 @@ export type MatchPrimitiveType< | PrimitiveTupleType | never; -export type ContractMethodOutputParameters | undefined> = +type ContractMethodOutputParametersRecursiveArray< + Params extends ReadonlyArray | undefined, +> = + // check if params are empty array Params extends readonly [] ? [] - : Params extends readonly [infer H, ...infer R] + : Params extends readonly [infer H, ...infer R] // check if Params is an array ? H extends AbiParameter - ? // TODO: Find a way to set name for tuple item - [MatchPrimitiveType, ...ContractMethodOutputParameters] - : ContractMethodOutputParameters - : Params extends undefined | unknown + ? [ + MatchPrimitiveType, + ...ContractMethodOutputParametersRecursiveArray, + ] + : [] + : []; + +type ContractMethodOutputParametersRecursiveRecord< + Params extends ReadonlyArray | undefined, +> = + // check if params are empty array + Params extends readonly [] + ? [] + : Params extends readonly [infer H, ...infer R] // check if Params is an array + ? H extends AbiParameter + ? H['name'] extends '' // check if output param name is empty string + ? ContractMethodOutputParametersRecursiveRecord + : Record> & // sets key-value pair of output param name and type + ContractMethodOutputParametersRecursiveRecord + : ContractMethodOutputParametersRecursiveRecord + : Params extends undefined | unknown // param is not array, check if undefined ? [] : Params; +export type ContractMethodOutputParameters | undefined> = + // check if params are empty array + Params extends readonly [] + ? void + : Params extends readonly [infer H, ...infer R] // check if Params is an array + ? R extends readonly [] // if only one output in array + ? H extends AbiParameter + ? MatchPrimitiveType + : [] + : // if more than one output + ArrToObjectWithFunctions<[...ContractMethodOutputParametersRecursiveArray]> & + ContractMethodOutputParametersRecursiveRecord + : []; + export type ContractMethodInputParameters | undefined> = Params extends readonly [] ? [] diff --git a/packages/web3-eth-abi/test/unit/types.test.ts b/packages/web3-eth-abi/test/unit/types.test.ts index 773ab6e7ede..5bdce305c69 100644 --- a/packages/web3-eth-abi/test/unit/types.test.ts +++ b/packages/web3-eth-abi/test/unit/types.test.ts @@ -17,7 +17,7 @@ along with web3.js. If not, see . import { Address, Bytes, Numbers } from 'web3-types'; import { expectTypeOf, typecheck } from '@humeris/espresso-shot'; -import { MatchPrimitiveType } from '../../src/types'; +import { ContractMethodOutputParameters, MatchPrimitiveType } from '../../src'; describe('types', () => { describe('primitive types', () => { @@ -157,4 +157,101 @@ describe('types', () => { ); }); }); + + describe('contract', () => { + describe('outputs', () => { + typecheck('empty outputs should result in []', () => + expectTypeOf>().toExtend(), + ); + + typecheck('single outputs should result in that type', () => { + const abi = [ + { + name: '', + type: 'string', + }, + ] as const; + return expectTypeOf< + ContractMethodOutputParameters + >().toExtend(); + }); + + typecheck('multiple outputs should result in object indexed by numbers', () => { + const abi = [ + { + name: '', + type: 'string', + }, + { + name: '', + type: 'int', + }, + ] as const; + + return expectTypeOf< + ContractMethodOutputParameters[0] + >().toExtend(); + }); + + typecheck('multiple outputs should result in object indexed by numbers', () => { + const abi = [ + { + name: '', + type: 'string', + }, + { + name: '', + type: 'int', + }, + ] as const; + return expectTypeOf< + ContractMethodOutputParameters[1] + >().toExtend(); + }); + + typecheck('multiple outputs should result in object indexed by name', () => { + const abi = [ + { + name: 'first', + type: 'string', + }, + { + name: 'second', + type: 'int', + }, + ] as const; + return expectTypeOf< + ContractMethodOutputParameters['first'] + >().toExtend(); + }); + + typecheck('multiple outputs should result in object indexed by name', () => { + const abi = [ + { + name: 'first', + type: 'string', + }, + { + name: 'second', + type: 'int', + }, + ] as const; + return expectTypeOf< + ContractMethodOutputParameters['second'] + >().toExtend(); + }); + + typecheck('single output should result as in exactly one type', () => { + const abi = [ + { + name: 'first', + type: 'string', + }, + ] as const; + return expectTypeOf< + ContractMethodOutputParameters + >().toExtend(); + }); + }); + }); }); diff --git a/packages/web3-eth-contract/CHANGELOG.md b/packages/web3-eth-contract/CHANGELOG.md index 15a675364ce..beeec0bf709 100644 --- a/packages/web3-eth-contract/CHANGELOG.md +++ b/packages/web3-eth-contract/CHANGELOG.md @@ -177,6 +177,8 @@ const transactionHash = receipt.transactionHash; - Decoding error data, using Error ABI if available, according to EIP-838. (#5434) - The class `Web3ContractError` is moved from this package to `web3-error`. (#5434) +- `SpecialOutput` type was added as a generic type into the call function to support reassigning output types (#5631) +- Overloaded functions types (`ContractOverloadedMethodInputs`, `ContractOverloadedMethodOutputs`) was added (#5631) ### Fixed @@ -186,6 +188,4 @@ const transactionHash = receipt.transactionHash; ### Fixed -#### web3-eth-contract - - Emit past contract events based on `fromBlock` when passed to `contract.events.someEventName` (#5201) diff --git a/packages/web3-eth-contract/src/contract.ts b/packages/web3-eth-contract/src/contract.ts index b7ce2ab9f6e..6f27d4fb4cc 100644 --- a/packages/web3-eth-contract/src/contract.ts +++ b/packages/web3-eth-contract/src/contract.ts @@ -36,6 +36,8 @@ import { ContractEvent, ContractEvents, ContractMethod, + ContractMethodInputParameters, + ContractMethodOutputParameters, encodeEventSignature, encodeFunctionSignature, FilterAbis, @@ -102,7 +104,6 @@ import { isWeb3ContractContext, } from './utils'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any type ContractBoundMethod< Abi extends AbiFunctionFragment, Method extends ContractMethod = ContractMethod, @@ -112,6 +113,26 @@ type ContractBoundMethod< ? PayableMethodObject : NonPayableMethodObject; +export type ContractOverloadedMethodInputs> = NonNullable< + AbiArr extends readonly [] + ? undefined + : AbiArr extends readonly [infer A, ...infer R] + ? A extends AbiFunctionFragment + ? ContractMethodInputParameters | ContractOverloadedMethodInputs + : undefined + : undefined +>; + +export type ContractOverloadedMethodOutputs> = NonNullable< + AbiArr extends readonly [] + ? undefined + : AbiArr extends readonly [infer A, ...infer R] + ? A extends AbiFunctionFragment + ? ContractMethodOutputParameters | ContractOverloadedMethodOutputs + : undefined + : undefined +>; + // To avoid circular dependency between types and encoding, declared these types here. export type ContractMethodsInterface = { [MethodAbi in FilterAbis< @@ -912,25 +933,20 @@ export class Contract abi.constant; abi.payable = abi.stateMutability === 'payable' ?? abi.payable; - - const contractMethod = this._createContractMethod(abi, errorsAbi); - this._overloadedMethodAbis.set(abi.name, [ ...(this._overloadedMethodAbis.get(abi.name) ?? []), abi, ]); - if (methodName in this._functions) { - this._functions[methodName] = { - signature: methodSignature, - method: contractMethod, - }; - } else { - this._functions[methodName] = { - signature: methodSignature, - method: contractMethod, - }; - } + const contractMethod = this._createContractMethod( + this._overloadedMethodAbis.get(abi.name) ?? [], + errorsAbi, + ); + + this._functions[methodName] = { + signature: methodSignature, + method: contractMethod, + }; // We don't know a particular type of the Abi method so can't type check this._methods[abi.name as keyof ContractMethodsInterface] = this._functions[ @@ -979,10 +995,11 @@ export class Contract ); } } - private _createContractMethod( - abi: T, + private _createContractMethod( + abiArr: T, errorsAbis: E[], - ): ContractBoundMethod { + ): ContractBoundMethod { + const abi = abiArr[abiArr.length - 1]; return (...params: unknown[]) => { let abiParams!: Array; const abis = this._overloadedMethodAbis.get(abi.name) ?? []; @@ -1036,8 +1053,8 @@ export class Contract }), encodeABI: () => encodeMethodABI(methodAbi, abiParams), } as unknown as PayableMethodObject< - ContractMethod['Inputs'], - ContractMethod['Outputs'] + ContractOverloadedMethodInputs, + ContractOverloadedMethodOutputs >; } return { @@ -1058,8 +1075,8 @@ export class Contract }), encodeABI: () => encodeMethodABI(methodAbi, abiParams), } as unknown as NonPayableMethodObject< - ContractMethod['Inputs'], - ContractMethod['Outputs'] + ContractOverloadedMethodInputs, + ContractOverloadedMethodOutputs >; }; } diff --git a/packages/web3-eth-contract/src/types.ts b/packages/web3-eth-contract/src/types.ts index 3a02e622b51..438b8e9708f 100644 --- a/packages/web3-eth-contract/src/types.ts +++ b/packages/web3-eth-contract/src/types.ts @@ -227,7 +227,11 @@ export interface NonPayableMethodObject * @param block - If you pass this parameter it will not use the default block set with contract.defaultBlock. Pre-defined block numbers as `earliest`, `latest`, and `pending` can also be used. Useful for requesting data from or replaying transactions in past blocks. * @returns - The return value(s) of the smart contract method. If it returns a single value, it’s returned as is. If it has multiple return values they are returned as an object with properties and indices. */ - call(tx?: NonPayableCallOptions, block?: BlockNumberOrTag): Promise; + + call( + tx?: NonPayableCallOptions, + block?: BlockNumberOrTag, + ): Promise; /** * This will send a transaction to the smart contract and execute its method. Note this can alter the smart contract state. @@ -384,7 +388,10 @@ export interface PayableMethodObject { * @param block - If you pass this parameter it will not use the default block set with contract.defaultBlock. Pre-defined block numbers as `earliest`, `latest`, and `pending` can also be used. Useful for requesting data from or replaying transactions in past blocks. * @returns - The return value(s) of the smart contract method. If it returns a single value, it’s returned as is. If it has multiple return values they are returned as an object with properties and indices. */ - call(tx?: PayableCallOptions, block?: BlockNumberOrTag): Promise; + call( + tx?: PayableCallOptions, + block?: BlockNumberOrTag, + ): Promise; /** * Will send a transaction to the smart contract and execute its method. Note this can alter the smart contract state. diff --git a/packages/web3-eth-ens/test/integration/resolver.test.ts b/packages/web3-eth-ens/test/integration/resolver.test.ts index de46cc38c99..63b494458db 100644 --- a/packages/web3-eth-ens/test/integration/resolver.test.ts +++ b/packages/web3-eth-ens/test/integration/resolver.test.ts @@ -172,8 +172,8 @@ describe('ens', () => { const res = await ens.getPubkey(domain); - expect(res[0]).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); - expect(res[1]).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); + expect(res.x).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); + expect(res.y).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); }); it('permits setting public key by owner', async () => {