Skip to content
This repository has been archived by the owner on Mar 5, 2025. It is now read-only.

Contract call with tuple is missing param names #5613

Merged
merged 14 commits into from
Nov 30, 2022
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/web3-eth-abi/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
51 changes: 45 additions & 6 deletions packages/web3-eth-abi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ export type PrimitiveTupleType<
: never
: never;

type ObjectToArray<T extends unknown[]> = T extends [...infer R, infer A]
? Record<R['length'], A> & ObjectToArray<R>
: T;
type ArrToObjectWithFunctions<T extends unknown[]> = Array<unknown> & ObjectToArray<T>;

export type MatchPrimitiveType<
Type extends string,
Components extends ReadonlyArray<AbiParameter> | undefined,
Expand All @@ -185,18 +190,52 @@ export type MatchPrimitiveType<
| PrimitiveTupleType<Type, Components>
| never;

export type ContractMethodOutputParameters<Params extends ReadonlyArray<unknown> | undefined> =
type ContractMethodOutputParametersRecursiveArray<
Params extends ReadonlyArray<unknown> | 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<H['type'], H['components']>, ...ContractMethodOutputParameters<R>]
: ContractMethodOutputParameters<R>
: Params extends undefined | unknown
? [
MatchPrimitiveType<H['type'], H['components']>,
...ContractMethodOutputParametersRecursiveArray<R>,
]
: []
: [];

type ContractMethodOutputParametersRecursiveRecord<
Params extends ReadonlyArray<unknown> | 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<R>
: Record<H['name'], MatchPrimitiveType<H['type'], H['components']>> & // sets key-value pair of output param name and type
ContractMethodOutputParametersRecursiveRecord<R>
: ContractMethodOutputParametersRecursiveRecord<R>
: Params extends undefined | unknown // param is not array, check if undefined
? []
: Params;

export type ContractMethodOutputParameters<Params extends ReadonlyArray<unknown> | 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<H['type'], H['components']>
: []
: // if more than one output
ArrToObjectWithFunctions<[...ContractMethodOutputParametersRecursiveArray<Params>]> &
ContractMethodOutputParametersRecursiveRecord<Params>
: [];

export type ContractMethodInputParameters<Params extends ReadonlyArray<unknown> | undefined> =
Params extends readonly []
? []
Expand Down
99 changes: 98 additions & 1 deletion packages/web3-eth-abi/test/unit/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.

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', () => {
Expand Down Expand Up @@ -157,4 +157,101 @@ describe('types', () => {
);
});
});

describe('contract', () => {
describe('outputs', () => {
typecheck('empty outputs should result in []', () =>
expectTypeOf<ContractMethodOutputParameters<[]>>().toExtend<void>(),
);

typecheck('single outputs should result in that type', () => {
const abi = [
{
name: '',
type: 'string',
},
] as const;
return expectTypeOf<
ContractMethodOutputParameters<typeof abi>
>().toExtend<string>();
});

typecheck('multiple outputs should result in object indexed by numbers', () => {
const abi = [
{
name: '',
type: 'string',
},
{
name: '',
type: 'int',
},
] as const;

return expectTypeOf<
ContractMethodOutputParameters<typeof abi>[0]
>().toExtend<string>();
});

typecheck('multiple outputs should result in object indexed by numbers', () => {
const abi = [
{
name: '',
type: 'string',
},
{
name: '',
type: 'int',
},
] as const;
return expectTypeOf<
ContractMethodOutputParameters<typeof abi>[1]
>().toExtend<Numbers>();
});

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<typeof abi>['first']
>().toExtend<string>();
});

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<typeof abi>['second']
>().toExtend<Numbers>();
});

typecheck('single output should result as in exactly one type', () => {
const abi = [
{
name: 'first',
type: 'string',
},
] as const;
return expectTypeOf<
ContractMethodOutputParameters<typeof abi>
>().toExtend<string>();
});
});
});
});
4 changes: 2 additions & 2 deletions packages/web3-eth-contract/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
61 changes: 39 additions & 22 deletions packages/web3-eth-contract/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
ContractEvent,
ContractEvents,
ContractMethod,
ContractMethodInputParameters,
ContractMethodOutputParameters,
encodeEventSignature,
encodeFunctionSignature,
FilterAbis,
Expand Down Expand Up @@ -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<Abi> = ContractMethod<Abi>,
Expand All @@ -112,6 +113,26 @@ type ContractBoundMethod<
? PayableMethodObject<Method['Inputs'], Method['Outputs']>
: NonPayableMethodObject<Method['Inputs'], Method['Outputs']>;

export type ContractOverloadedMethodInputs<AbiArr extends ReadonlyArray<unknown>> = NonNullable<
AbiArr extends readonly []
? undefined
: AbiArr extends readonly [infer A, ...infer R]
? A extends AbiFunctionFragment
? ContractMethodInputParameters<A['inputs']> | ContractOverloadedMethodInputs<R>
: undefined
: undefined
>;

export type ContractOverloadedMethodOutputs<AbiArr extends ReadonlyArray<unknown>> = NonNullable<
AbiArr extends readonly []
? undefined
: AbiArr extends readonly [infer A, ...infer R]
? A extends AbiFunctionFragment
? ContractMethodOutputParameters<A['outputs']> | ContractOverloadedMethodOutputs<R>
: undefined
: undefined
>;

// To avoid circular dependency between types and encoding, declared these types here.
export type ContractMethodsInterface<Abi extends ContractAbi> = {
[MethodAbi in FilterAbis<
Expand Down Expand Up @@ -912,25 +933,20 @@ export class Contract<Abi extends ContractAbi>
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<Abi>] = this._functions[
Expand Down Expand Up @@ -979,10 +995,11 @@ export class Contract<Abi extends ContractAbi>
);
}
}
private _createContractMethod<T extends AbiFunctionFragment, E extends AbiErrorFragment>(
abi: T,
private _createContractMethod<T extends AbiFunctionFragment[], E extends AbiErrorFragment>(
abiArr: T,
errorsAbis: E[],
): ContractBoundMethod<T> {
): ContractBoundMethod<T[0]> {
const abi = abiArr[abiArr.length - 1];
return (...params: unknown[]) => {
let abiParams!: Array<unknown>;
const abis = this._overloadedMethodAbis.get(abi.name) ?? [];
Expand Down Expand Up @@ -1036,8 +1053,8 @@ export class Contract<Abi extends ContractAbi>
}),
encodeABI: () => encodeMethodABI(methodAbi, abiParams),
} as unknown as PayableMethodObject<
ContractMethod<T>['Inputs'],
ContractMethod<T>['Outputs']
ContractOverloadedMethodInputs<T>,
ContractOverloadedMethodOutputs<T>
>;
}
return {
Expand All @@ -1058,8 +1075,8 @@ export class Contract<Abi extends ContractAbi>
}),
encodeABI: () => encodeMethodABI(methodAbi, abiParams),
} as unknown as NonPayableMethodObject<
ContractMethod<T>['Inputs'],
ContractMethod<T>['Outputs']
ContractOverloadedMethodInputs<T>,
ContractOverloadedMethodOutputs<T>
>;
};
}
Expand Down
11 changes: 9 additions & 2 deletions packages/web3-eth-contract/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,11 @@ export interface NonPayableMethodObject<Inputs = unknown[], Outputs = unknown[]>
* @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<Outputs>;

call<SpecialOutput = Outputs>(
tx?: NonPayableCallOptions,
block?: BlockNumberOrTag,
): Promise<SpecialOutput>;

/**
* This will send a transaction to the smart contract and execute its method. Note this can alter the smart contract state.
Expand Down Expand Up @@ -384,7 +388,10 @@ export interface PayableMethodObject<Inputs = unknown[], Outputs = unknown[]> {
* @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<Outputs>;
call<SpecialOutput = Outputs>(
tx?: PayableCallOptions,
block?: BlockNumberOrTag,
): Promise<SpecialOutput>;

/**
* Will send a transaction to the smart contract and execute its method. Note this can alter the smart contract state.
Expand Down
4 changes: 2 additions & 2 deletions packages/web3-eth-ens/test/integration/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down