Skip to content

Commit

Permalink
feat: replace ethers abi coder with ours (#6385)
Browse files Browse the repository at this point in the history
* chore: improve building speed and corectness

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* feat: web3js abi encoder replacing ethers

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* fix: bytes array decoding

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* fix: add negative number decoding

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* fix: coding numbers should be in increments of 8

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* address PR comments

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* add more tests

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* add array unit tests

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* fix: optimisations

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* fix typo

* encodeParameters decodeParameters tests

* fix ethers tests

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>

* decodeParameters ethers tests

---------

Signed-off-by: Marin Petrunic <marin.petrunic@gmail.com>
Co-authored-by: jdevcs <junaid@chainsafe.io>
Co-authored-by: Muhammad Altabba <24407834+Muhammad-Altabba@users.noreply.github.com>
Co-authored-by: Junaid <86780488+jdevcs@users.noreply.github.com>
  • Loading branch information
4 people authored Sep 28, 2023
1 parent b8fa712 commit c490c18
Show file tree
Hide file tree
Showing 60 changed files with 89,025 additions and 152 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"buffer": "^6.0.3",
"bufferutil": "^4.0.6",
"clean-webpack-plugin": "^4.0.0",
"concurrently": "^8.2.0",
"cypress-jest-adapter": "^0.1.1",
"declaration-bundler-webpack-plugin": "^1.0.3",
"eslint": "^8.20.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/web3-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"scripts": {
"clean": "rimraf dist && rimraf lib",
"prebuild": "yarn clean",
"build": "yarn build:cjs & yarn build:esm & yarn build:types",
"build": "concurrently --kill-others-on-fail \"yarn:build:*(!check)\"",
"build:cjs": "tsc --build tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > ./lib/commonjs/package.json",
"build:esm": "tsc --build tsconfig.esm.json && echo '{\"type\": \"module\"}' > ./lib/esm/package.json",
"build:types": "tsc --build tsconfig.types.json",
Expand Down
2 changes: 1 addition & 1 deletion packages/web3-errors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"scripts": {
"clean": "rimraf dist && rimraf lib",
"prebuild": "yarn clean",
"build": "yarn build:cjs & yarn build:esm & yarn build:types",
"build": "concurrently --kill-others-on-fail \"yarn:build:*(!check)\"",
"build:cjs": "tsc --build tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > ./lib/commonjs/package.json",
"build:esm": "tsc --build tsconfig.esm.json && echo '{\"type\": \"module\"}' > ./lib/esm/package.json",
"build:types": "tsc --build tsconfig.types.json",
Expand Down
6 changes: 6 additions & 0 deletions packages/web3-errors/src/errors/generic_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ export class OperationAbortError extends BaseWeb3Error {

export class AbiError extends BaseWeb3Error {
public code = ERR_ABI_ENCODING;
public readonly props: Record<string, unknown> & { name?: string };

public constructor(message: string, props?: Record<string, unknown> & { name?: string }) {
super(message);
this.props = props ?? {};
}
}

export class ExistingPluginNamespaceError extends BaseWeb3Error {
Expand Down
6 changes: 3 additions & 3 deletions packages/web3-eth-abi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"scripts": {
"clean": "rimraf dist && rimraf lib",
"prebuild": "yarn clean",
"build": "yarn build:cjs & yarn build:esm & yarn build:types",
"build": "concurrently --kill-others-on-fail \"yarn:build:*(!check)\"",
"build:cjs": "tsc --build tsconfig.cjs.json && echo '{\"type\": \"commonjs\"}' > ./lib/commonjs/package.json",
"build:esm": "tsc --build tsconfig.esm.json && echo '{\"type\": \"module\"}' > ./lib/esm/package.json",
"build:types": "tsc --build tsconfig.types.json",
Expand All @@ -42,8 +42,8 @@
"test:integration": "jest --config=./test/integration/jest.config.js --passWithNoTests"
},
"dependencies": {
"@ethersproject/abi": "^5.7.0",
"@ethersproject/bignumber": "^5.7.0",
"abitype": "0.7.1",
"web3-validator": "^2.0.2",
"web3-errors": "^1.1.2",
"web3-types": "^1.2.0",
"web3-utils": "^4.0.6"
Expand Down
79 changes: 9 additions & 70 deletions packages/web3-eth-abi/src/api/parameters_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ GNU Lesser General Public License for more details.
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 { ParamType, Result } from '@ethersproject/abi';
import { HexString, AbiInput, DecodedParams } from 'web3-types';
import ethersAbiCoder from '../ethers_abi_coder.js';
import { formatParam, isAbiFragment, mapTypes, modifyParams } from '../utils.js';
import { AbiInput, HexString } from 'web3-types';
import { decodeParameters as decodeParametersInternal } from '../coders/decode.js';
import { encodeParameters as encodeParametersInternal } from '../coders/encode.js';

/**
* Encodes a parameter based on its type to its ABI representation.
Expand All @@ -37,40 +35,8 @@ import { formatParam, isAbiFragment, mapTypes, modifyParams } from '../utils.js'
* > 0x000000000000000000000000000000000000000000000000000000008bd02b7b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000748656c6c6f212500000000000000000000000000000000000000000000000000
* ```
*/
export const encodeParameters = (abi: ReadonlyArray<AbiInput>, params: unknown[]): string => {
try {
const modifiedTypes = mapTypes(
Array.isArray(abi) ? (abi as AbiInput[]) : ([abi] as unknown as AbiInput[]),
);
const modifiedParams: Array<unknown> = [];
for (const [index, param] of params.entries()) {
const item = modifiedTypes[index];
let type: string;

if (isAbiFragment(item) && item.type) {
// We may get a named type of shape {name, type}
type = item.type;
} else {
type = item as unknown as string;
}

const newParam = formatParam(type, param);

if (typeof type === 'string' && type.includes('tuple')) {
const coder = ethersAbiCoder._getCoder(ParamType.from(type));
modifyParams(coder, [newParam]);
}

modifiedParams.push(newParam);
}
return ethersAbiCoder.encode(
modifiedTypes.map(p => ParamType.from(p)),
modifiedParams,
);
} catch (err) {
throw new AbiError(`Parameter encoding error`, err as Error);
}
};
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 Expand Up @@ -130,30 +96,6 @@ export const encodeParameters = (abi: ReadonlyArray<AbiInput>, params: unknown[]
*/
export const encodeParameter = (abi: AbiInput, param: unknown): string =>
encodeParameters([abi], [param]);

// If encoded param is an array and there are mixed on integer and string keys
const isParamRequiredToConvert = (data: Result): boolean =>
Array.isArray(data) &&
Object.keys(data).filter(k => Number.isInteger(+k)).length !== Object.keys(data).length;

// Ethers-Encoder return the decoded result as an array with additional string indexes for named params
// We want these to be converted to an object with named keys
const formatArrayResToObject = (data: Result): DecodedParams => {
const returnValue: DecodedParams = {
__length__: 0,
};

for (const key of Object.keys(data)) {
returnValue[key] =
Array.isArray(data[key]) && isParamRequiredToConvert(data[key] as Result)
? formatArrayResToObject(data[key] as Result)
: data[key];

returnValue.__length__ += Number.isInteger(+key) ? 1 : 0;
}
return returnValue;
};

/**
* Should be used to decode list of params
*/
Expand All @@ -172,14 +114,11 @@ export const decodeParametersWith = (
'or querying a node which is not fully synced.',
);
}
const res = ethersAbiCoder.decode(
mapTypes(abis).map(p => ParamType.from(p)),
`0x${bytes.replace(/0x/i, '')}`,
loose,
);
return formatArrayResToObject(res);
return decodeParametersInternal(abis, `0x${bytes.replace(/0x/i, '')}`, loose);
} catch (err) {
throw new AbiError(`Parameter decoding error: ${(err as Error).message}`);
throw new AbiError(`Parameter decoding error: ${(err as Error).message}`, {
internalErr: err,
});
}
};

Expand Down
75 changes: 75 additions & 0 deletions packages/web3-eth-abi/src/coders/base/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
This file is part of web3.js.
web3.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
web3.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
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 { AbiParameter } from 'web3-types';
import { toChecksumAddress } from 'web3-utils';
import { isAddress, utils } from 'web3-validator';
import { DecoderResult, EncoderResult } from '../types.js';
import { alloc, WORD_SIZE } from '../utils.js';

const ADDRESS_BYTES_COUNT = 20;
const ADDRESS_OFFSET = WORD_SIZE - ADDRESS_BYTES_COUNT;

export function encodeAddress(param: AbiParameter, input: unknown): EncoderResult {
if (typeof input !== 'string') {
throw new AbiError('address type expects string as input type', {
value: input,
name: param.name,
type: param.type,
});
}
let address = input.toLowerCase();
if (!address.startsWith('0x')) {
address = `0x${address}`;
}
if (!isAddress(address)) {
throw new AbiError('provided input is not valid address', {
value: input,
name: param.name,
type: param.type,
});
}
// for better performance, we could convert hex to destination bytes directly (encoded var)
const addressBytes = utils.hexToUint8Array(address);
// expand address to WORD_SIZE
const encoded = alloc(WORD_SIZE);
encoded.set(addressBytes, ADDRESS_OFFSET);
return {
dynamic: false,
encoded,
};
}

export function decodeAddress(_param: AbiParameter, bytes: Uint8Array): DecoderResult<string> {
const addressBytes = bytes.subarray(ADDRESS_OFFSET, WORD_SIZE);
if (addressBytes.length !== ADDRESS_BYTES_COUNT) {
throw new AbiError('Invalid decoding input, not enough bytes to decode address', { bytes });
}
const result = utils.uint8ArrayToHexString(addressBytes);

// should we check is decoded value is valid address?
// if(!isAddress(result)) {
// throw new AbiError("encoded data is not valid address", {
// address: result,
// });
// }
return {
result: toChecksumAddress(result),
encoded: bytes.subarray(WORD_SIZE),
consumed: WORD_SIZE,
};
}
120 changes: 120 additions & 0 deletions packages/web3-eth-abi/src/coders/base/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
This file is part of web3.js.
web3.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
web3.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
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 { AbiParameter } from 'web3-types';
import { uint8ArrayConcat } from 'web3-utils';
// eslint-disable-next-line import/no-cycle
import { decodeParamFromAbiParameter, encodeNumber, encodeParamFromAbiParameter } from '.';
import { DecoderResult, EncoderResult } from '../types.js';
import { extractArrayType, isDynamic, WORD_SIZE } from '../utils.js';
import { decodeNumber } from './number.js';
import { encodeDynamicParams } from './utils.js';

export function encodeArray(param: AbiParameter, values: unknown): EncoderResult {
if (!Array.isArray(values)) {
throw new AbiError('Expected value to be array', { abi: param, values });
}
const { size, param: arrayItemParam } = extractArrayType(param);
const encodedParams = values.map(v => encodeParamFromAbiParameter(arrayItemParam, v));
const dynamic = size === -1;
const dynamicItems = encodedParams.length > 0 && encodedParams[0].dynamic;
if (!dynamic && values.length !== size) {
throw new AbiError("Given arguments count doesn't match array length", {
arrayLength: size,
argumentsLength: values.length,
});
}
if (dynamic || dynamicItems) {
const encodingResult = encodeDynamicParams(encodedParams);
if (dynamic) {
const encodedLength = encodeNumber(
{ type: 'uint256', name: '' },
encodedParams.length,
).encoded;
return {
dynamic: true,
encoded:
encodedParams.length > 0
? uint8ArrayConcat(encodedLength, encodingResult)
: encodedLength,
};
}
return {
dynamic: true,
encoded: encodingResult,
};
}

return {
dynamic: false,
encoded: uint8ArrayConcat(...encodedParams.map(p => p.encoded)),
};
}

export function decodeArray(param: AbiParameter, bytes: Uint8Array): DecoderResult<unknown[]> {
// eslint-disable-next-line prefer-const
let { size, param: arrayItemParam } = extractArrayType(param);
const dynamic = size === -1;

let consumed = 0;
const result: unknown[] = [];
let remaining = bytes;
// dynamic array, we need to decode length
if (dynamic) {
const lengthResult = decodeNumber({ type: 'uint32', name: '' }, bytes);
size = Number(lengthResult.result);
consumed = lengthResult.consumed;
remaining = lengthResult.encoded;
}
const hasDynamicChild = isDynamic(arrayItemParam);
if (hasDynamicChild) {
// known length but dynamic child, each child is actually head element with encoded offset
for (let i = 0; i < size; i += 1) {
const offsetResult = decodeNumber(
{ type: 'uint32', name: '' },
remaining.subarray(i * WORD_SIZE),
);
consumed += offsetResult.consumed;
const decodedChildResult = decodeParamFromAbiParameter(
arrayItemParam,
remaining.subarray(Number(offsetResult.result)),
);
consumed += decodedChildResult.consumed;
result.push(decodedChildResult.result);
}
return {
result,
encoded: remaining.subarray(consumed),
consumed,
};
}

for (let i = 0; i < size; i += 1) {
// decode static params
const decodedChildResult = decodeParamFromAbiParameter(
arrayItemParam,
bytes.subarray(consumed),
);
consumed += decodedChildResult.consumed;
result.push(decodedChildResult.result);
}
return {
result,
encoded: bytes.subarray(consumed),
consumed,
};
}
Loading

0 comments on commit c490c18

Please sign in to comment.