Skip to content

Commit

Permalink
Allow the deploy function to accept parameters even when no ABI was…
Browse files Browse the repository at this point in the history
… provided to the Contract (#6635)

* allow deploy to accept parameters even when no ABI

* update CHANGELOG.md
  • Loading branch information
Muhammad-Altabba authored Dec 15, 2023
1 parent f7d9349 commit fa4c72b
Show file tree
Hide file tree
Showing 11 changed files with 760 additions and 40 deletions.
22 changes: 3 additions & 19 deletions packages/web3-eth-abi/src/api/parameters_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,10 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
import { AbiError } from 'web3-errors';
import { AbiInput, HexString } from 'web3-types';
import { decodeParameters as decodeParametersInternal } from '../coders/decode.js';
import { encodeParameters as encodeParametersInternal } from '../coders/encode.js';
import { encodeParameters } from '../coders/encode.js';

export { encodeParameters, inferTypesAndEncodeParameters } from '../coders/encode.js';

/**
* Encodes a parameter based on its type to its ABI representation.
* @param abi - An array of {@link AbiInput}. See [Solidity's documentation](https://solidity.readthedocs.io/en/v0.5.3/abi-spec.html#json) for more details.
* @param params - The actual parameters to encode.
* @returns - The ABI encoded parameters
* @example
* ```ts
* const res = web3.eth.abi.encodeParameters(
* ["uint256", "string"],
* ["2345675643", "Hello!%"]
* );
*
* console.log(res);
* > 0x000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000
* ```
*/
export const encodeParameters = (abi: ReadonlyArray<AbiInput>, params: unknown[]): string =>
encodeParametersInternal(abi, params);

/**
* Encodes a parameter based on its type to its ABI representation.
Expand Down
86 changes: 83 additions & 3 deletions packages/web3-eth-abi/src/coders/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,100 @@ You should have received a copy of the GNU Lesser General Public License
along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*/
import { AbiError } from 'web3-errors';
import { AbiInput } from 'web3-types';
import { AbiInput, AbiParameter } from 'web3-types';
import { toHex } from 'web3-utils';
import { utils } from 'web3-validator';
import { encodeTuple } from './base/index.js';
import { toAbiParams } from './utils.js';

/**
* @param params - The params to infer the ABI from
* @returns The inferred ABI
* @example
* ```
* inferParamsAbi([1, -1, 'hello', '0x1234', ])
* ```
* > [{ type: 'int256' }, { type: 'uint256' }, { type: 'string' }, { type: 'bytes' }]
* ```
*/
function inferParamsAbi(params: unknown[]): ReadonlyArray<AbiParameter> {
const abi: AbiParameter[] = [];
params.forEach(param => {
if (Array.isArray(param)) {
const inferredParams = inferParamsAbi(param);
abi.push({
type: 'tuple',
components: inferredParams,
name: '',
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
} as AbiParameter);
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
abi.push({ type: toHex(param as any, true) } as AbiParameter);
}
});
return abi;
}

/**
* Encodes a parameter based on its type to its ABI representation.
* @param abi - An array of {@link AbiInput}. See [Solidity's documentation](https://solidity.readthedocs.io/en/v0.5.3/abi-spec.html#json) for more details.
* @param params - The actual parameters to encode.
* @returns - The ABI encoded parameters
* @example
* ```ts
* const res = web3.eth.abi.encodeParameters(
* ["uint256", "string"],
* ["2345675643", "Hello!%"]
* );
*
* console.log(res);
* > 0x000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000
* ```
*/
export function encodeParameters(abi: ReadonlyArray<AbiInput>, params: unknown[]): string {
if (abi.length !== params.length) {
if (abi?.length !== params.length) {
throw new AbiError('Invalid number of values received for given ABI', {
expected: abi.length,
expected: abi?.length,
received: params.length,
});
}

const abiParams = toAbiParams(abi);
return utils.uint8ArrayToHexString(
encodeTuple({ type: 'tuple', name: '', components: abiParams }, params).encoded,
);
}

/**
* Infer a smart contract method parameter type and then encode this parameter.
* @param params - The parameters to encode.
* @returns - The ABI encoded parameters
*
* @remarks
* This method is useful when you don't know the type of the parameters you want to encode. It will infer the type of the parameters and then encode them.
* However, it is not recommended to use this method when you know the type of the parameters you want to encode. In this case, use the {@link encodeParameters} method instead.
* The type inference is not perfect and can lead to unexpected results. Especially when you want to encode an array, uint that is not uint256 or bytes....
* @example
* ```ts
* const res = web3.eth.abi.encodeParameters(
* ["2345675643", "Hello!%"]
* );
*
* console.log(res);
* > 0x000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000
* ```
*/
export function inferTypesAndEncodeParameters(params: unknown[]): string {
try {
const abiParams = inferParamsAbi(params);
return utils.uint8ArrayToHexString(
encodeTuple({ type: 'tuple', name: '', components: abiParams }, params).encoded,
);
} catch (e) {
// throws If the inferred params type caused an error
throw new AbiError('Could not infer types from given params', {
params,
});
}
}
13 changes: 12 additions & 1 deletion packages/web3-eth-abi/test/unit/encodeDecodeParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*/

import { AbiInput } from 'web3-types';
import { decodeParameters, encodeParameters } from '../../src';
import { decodeParameters, encodeParameters, inferTypesAndEncodeParameters } from '../../src';
import testsData from '../fixtures/abitestsdata.json';
import { deepEqualTolerateBigInt, removeKey } from './test_utils';

Expand All @@ -26,6 +26,17 @@ describe('encodeParameters decodeParameters tests should pass', () => {
expect(encodedResult).toEqual(encoderTestObj.encoded);
});

it.each(testsData)(`unit test of encodeParameters - $name`, encoderTestObj => {
// skip for types that are not supported by infer-types
// the unsupported types are uint(other than 256), int(other than 256), bytes(that has a number like bytes1 or bytes2), and arrays
if (/((?<!u)int)|((?<!uint\d)uint(?!256))|(bytes\d)|(\[.*?\])/.test(encoderTestObj.type)) {
return;
}

const encodedResult = inferTypesAndEncodeParameters([encoderTestObj.value]);
expect(encodedResult).toEqual(encoderTestObj.encoded);
});

it.each(testsData)('unit test of decodeParameters - $name', decoderTestObj => {
const decodedResult = decodeParameters(
[decoderTestObj.type] as AbiInput[],
Expand Down
4 changes: 4 additions & 0 deletions packages/web3-eth-contract/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,10 @@ Documentation:

## [Unreleased]

### Changed

- Allow the `deploy` function to accept parameters, even when no ABI was provided to the `Contract`(#6635)

### Fixed

- Fix and error that happen when trying to get past events by calling `contract.getPastEvents` or `contract.events.allEvents()`, if there is no matching events. (#6647)
Expand Down
1 change: 0 additions & 1 deletion packages/web3-eth-contract/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,6 @@ export class Contract<Abi extends ContractAbi>
if (!abi) {
abi = {
type: 'constructor',
inputs: [],
stateMutability: '',
} as AbiConstructorFragment;
}
Expand Down
17 changes: 12 additions & 5 deletions packages/web3-eth-contract/src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
encodeFunctionSignature,
encodeParameter,
encodeParameters,
inferTypesAndEncodeParameters,
isAbiConstructorFragment,
jsonInterfaceMethodToString,
} from 'web3-eth-abi';
Expand Down Expand Up @@ -120,16 +121,22 @@ export const encodeMethodABI = (
deployData?: HexString,
) => {
const inputLength = Array.isArray(abi.inputs) ? abi.inputs.length : 0;
if (inputLength !== args.length) {
if (abi.inputs && inputLength !== args.length) {
throw new Web3ContractError(
`The number of arguments is not matching the methods required number. You need to pass ${inputLength} arguments.`,
);
}

const params = encodeParameters(Array.isArray(abi.inputs) ? abi.inputs : [], args).replace(
'0x',
'',
);
let params: string;
if (abi.inputs) {
params = encodeParameters(Array.isArray(abi.inputs) ? abi.inputs : [], args).replace(
'0x',
'',
);
} else {
params = inferTypesAndEncodeParameters(args).replace('0x', '');
}


if (isAbiConstructorFragment(abi)) {
if (!deployData)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ describe('contract', () => {

const myContract = contract.deploy({
data: '608060405234801561001057600080fd5b506040516101d93803806101d983398181016040528101906100329190610054565b806000819055505061009e565b60008151905061004e81610087565b92915050565b60006020828403121561006657600080fd5b60006100748482850161003f565b91505092915050565b6000819050919050565b6100908161007d565b811461009b57600080fd5b50565b61012c806100ad6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c806323fd0e401460375780636ffd773c146051575b600080fd5b603d6069565b6040516048919060bf565b60405180910390f35b6067600480360381019060639190608c565b606f565b005b60005481565b8060008190555050565b60008135905060868160e2565b92915050565b600060208284031215609d57600080fd5b600060a9848285016079565b91505092915050565b60b98160d8565b82525050565b600060208201905060d2600083018460b2565b92915050565b6000819050919050565b60e98160d8565b811460f357600080fd5b5056fea2646970667358221220d28cf161457f7936995800eb9896635a02a559a0561bff6a09a40bfb81cd056564736f6c63430008000033',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
arguments: [1],
});

Expand Down
33 changes: 33 additions & 0 deletions packages/web3-eth-contract/test/unit/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,39 @@ describe('Contract', () => {
sendTransactionSpy.mockClear();
});

it('should deploy contract with input property with no ABI', async () => {
const input = `${GreeterBytecode}0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b4d79204772656574696e67000000000000000000000000000000000000000000`;
const contract = new Contract([]);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const sendTransactionSpy = jest
.spyOn(eth, 'sendTransaction')
.mockImplementation((_objInstance, tx) => {
expect(tx.to).toBeUndefined();
expect(tx.gas).toStrictEqual(sendOptions.gas);
expect(tx.gasPrice).toBeUndefined();
expect(tx.from).toStrictEqual(sendOptions.from);
expect(tx.input).toStrictEqual(input); // padded data

const newContract = contract.clone();
newContract.options.address = deployedAddr;

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Promise.resolve(newContract) as any;
});

const deployedContract = await contract
.deploy({
input: GreeterBytecode,
arguments: ['My Greeting'],
})
.send(sendOptions);

expect(deployedContract).toBeDefined();
expect(deployedContract.options.address).toStrictEqual(deployedAddr);
sendTransactionSpy.mockClear();
});

it('should deploy contract with data property', async () => {
const data = `${GreeterBytecode}0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b4d79204772656574696e67000000000000000000000000000000000000000000`;
const contract = new Contract(GreeterAbi);
Expand Down
21 changes: 15 additions & 6 deletions packages/web3-types/src/eth_abi_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,21 @@ export type ContractConstructor<Abis extends ContractAbi> = {
};
}['constructor'];

export type ContractConstructorArgs<Abis extends ContractAbi> = {
[Abi in FilterAbis<
Abis,
AbiConstructorFragment & { type: 'constructor' }
> as 'constructor']: ContractMethodInputParameters<Abi['inputs']>;
}['constructor'];
export type ContractConstructorArgs<Abis extends ContractAbi> = FilterAbis<
Abis,
AbiConstructorFragment & {
type: 'constructor';
}
> extends never
? any
: {
[Abi in FilterAbis<
Abis,
AbiConstructorFragment & {
type: 'constructor';
}
> as 'constructor']: ContractMethodInputParameters<Abi['inputs']>;
}['constructor'];

export type ContractMethod<Abi extends AbiFunctionFragment> = {
readonly Abi: Abi;
Expand Down
6 changes: 5 additions & 1 deletion packages/web3-utils/src/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
isHex,
isHexStrict,
isInt,
isUInt,
isNullish,
utils,
utils as validatorUtils,
Expand Down Expand Up @@ -372,9 +373,12 @@ export const toHex = (
if (isHexStrict(value)) {
return returnType ? 'bytes' : value;
}
if (isHex(value) && !isInt(value)) {
if (isHex(value) && !isInt(value) && !isUInt(value)) {
return returnType ? 'bytes' : `0x${value}`;
}
if (isHex(value) && !isInt(value) && isUInt(value)) {
return returnType ? 'uint' : numberToHex(value);
}

if (!Number.isFinite(value)) {
return returnType ? 'string' : utf8ToHex(value);
Expand Down
Loading

0 comments on commit fa4c72b

Please sign in to comment.