Skip to content

Commit

Permalink
draft implementation and test for 838 error codes
Browse files Browse the repository at this point in the history
  • Loading branch information
Muhammad-Altabba committed Sep 13, 2022
1 parent bc39fe9 commit bfa0831
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/web3-core/src/web3_request_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export class Web3RequestManager<
// This is the majority of the cases so check these first
// A valid JSON-RPC response with error object
if (jsonRpc.isResponseWithError<ErrorType>(response)) {
// TODO: check if there is a need to call decodeParameters() somewhere around here to throw another type of error
throw new InvalidResponseError<ErrorType>(response);
}

Expand Down
8 changes: 8 additions & 0 deletions packages/web3-eth-abi/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,19 @@ export type AbiEventFragment = AbiBaseFragment & {
readonly anonymous?: boolean;
};

// https://docs.soliditylang.org/en/latest/abi-spec.html#errors
export type AbiErrorFragment = AbiBaseFragment & {
readonly name: string;
readonly type: string | 'error';
readonly inputs?: ReadonlyArray<AbiParameter>;
};

// https://docs.soliditylang.org/en/latest/abi-spec.html#json
export type AbiFragment =
| AbiConstructorFragment
| AbiFunctionFragment
| AbiEventFragment
| AbiErrorFragment
| AbiFallbackFragment;

export type ContractAbi = ReadonlyArray<AbiFragment>;
Expand Down
1 change: 1 addition & 0 deletions packages/web3-eth-contract/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"test:e2e:firefox": "npx cypress run --headless --browser firefox"
},
"dependencies": {
"@ethersproject/bytes": "^5.7.0",
"web3-core": "^4.0.1-alpha.0",
"web3-errors": "^0.1.1-alpha.0",
"web3-eth": "^4.0.1-alpha.0",
Expand Down
47 changes: 35 additions & 12 deletions packages/web3-eth-contract/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from 'web3-eth';
import {
AbiConstructorFragment,
AbiErrorFragment,
AbiEventFragment,
AbiFragment,
AbiFunctionFragment,
Expand Down Expand Up @@ -62,7 +63,13 @@ import {
} from 'web3-utils';
import { isNullish, validator, utils as validatorUtils } from 'web3-validator';
import { ALL_EVENTS_ABI } from './constants';
import { decodeEventABI, decodeMethodReturn, encodeEventABI, encodeMethodABI } from './encoding';
import {
decodeEventABI,
decodeMethodReturn,
encodeEventABI,
encodeMethodABI,
throwDecodedError,
} from './encoding';
import { Web3ContractError } from './errors';
import { LogsSubscription } from './log_subscription';
import {
Expand Down Expand Up @@ -865,7 +872,11 @@ export class Contract<Abi extends ContractAbi>

let result: ContractAbi = [];

for (const a of abis) {
const abisOfFunctions = abis.filter(abi => abi.type !== 'error');
const abisOfErrors = abis.filter(
abi => abi.type === 'error',
) as unknown as AbiErrorFragment[];
for (const a of abisOfFunctions) {
const abi: Mutable<AbiFragment & { signature: HexString }> = {
...a,
signature: '',
Expand All @@ -887,13 +898,13 @@ export class Contract<Abi extends ContractAbi>
if (methodName in this._functions) {
this._functions[methodName] = {
signature: methodSignature,
method: this._createContractMethod(abi),
method: this._createContractMethod(abi, abisOfErrors),
cascadeFunction: this._functions[methodName].method,
};
} else {
this._functions[methodName] = {
signature: methodSignature,
method: this._createContractMethod(abi),
method: this._createContractMethod(abi, abisOfErrors),
};
}

Expand Down Expand Up @@ -934,7 +945,10 @@ export class Contract<Abi extends ContractAbi>
this._jsonInterface = [...result] as unknown as ContractAbiWithSignature;
}

private _createContractMethod<T extends AbiFunctionFragment>(abi: T): ContractBoundMethod<T> {
private _createContractMethod<T extends AbiFunctionFragment, E extends AbiErrorFragment>(
abi: T,
errorsAbis: E[],
): ContractBoundMethod<T> {
return (...params: unknown[]) => {
let abiParams!: Array<unknown>;

Expand All @@ -952,9 +966,9 @@ export class Contract<Abi extends ContractAbi>
return {
arguments: params,
call: async (options?: PayableCallOptions, block?: BlockNumberOrTag) =>
this._contractMethodCall(abi, params, options, block),
this._contractMethodCall(abi, params, errorsAbis, options, block),
send: (options?: PayableTxOptions) =>
this._contractMethodSend(abi, params, options),
this._contractMethodSend(abi, params, options), // TODO: refactor to parse errorsAbi
estimateGas: async <
ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT,
>(
Expand All @@ -971,9 +985,9 @@ export class Contract<Abi extends ContractAbi>
return {
arguments: abiParams,
call: async (options?: NonPayableCallOptions, block?: BlockNumberOrTag) =>
this._contractMethodCall(abi, params, options, block),
this._contractMethodCall(abi, params, errorsAbis, options, block),
send: (options?: NonPayableTxOptions) =>
this._contractMethodSend(abi, params, options),
this._contractMethodSend(abi, params, options), // TODO: refactor to parse errorsAbi
estimateGas: async <ReturnFormat extends DataFormat = typeof DEFAULT_RETURN_FORMAT>(
options?: NonPayableCallOptions,
returnFormat: ReturnFormat = DEFAULT_RETURN_FORMAT as ReturnFormat,
Expand All @@ -986,9 +1000,13 @@ export class Contract<Abi extends ContractAbi>
};
}

private async _contractMethodCall<Options extends PayableCallOptions | NonPayableCallOptions>(
private async _contractMethodCall<
E extends AbiErrorFragment,
Options extends PayableCallOptions | NonPayableCallOptions,
>(
abi: AbiFunctionFragment,
params: unknown[],
errorsAbi: E[],
options?: Options,
block?: BlockNumberOrTag,
) {
Expand All @@ -998,8 +1016,13 @@ export class Contract<Abi extends ContractAbi>
options,
contractOptions: this.options,
});

return decodeMethodReturn(abi, await call(this, tx, block, DEFAULT_RETURN_FORMAT));
try {
const result = await call(this, tx, block, DEFAULT_RETURN_FORMAT);
return decodeMethodReturn(abi, result);
} catch (error: unknown) {
// decode abi error inputs for EIP-838
return throwDecodedError(errorsAbi, error as Error);
}
}

private _contractMethodSend<Options extends PayableCallOptions | NonPayableCallOptions>(
Expand Down
53 changes: 50 additions & 3 deletions packages/web3-eth-contract/src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ 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 { DataFormat, DEFAULT_RETURN_FORMAT, format, isNullish } from 'web3-utils';
// TODO: possibly rewrite those method and then also delete this dependency from package.json
import { arrayify, hexlify } from '@ethersproject/bytes';

import { DataFormat, DEFAULT_RETURN_FORMAT, format, isNullish, sha3Raw } from 'web3-utils';

import { LogsInput, BlockNumberOrTag, Filter, HexString, Topic, Numbers } from 'web3-types';

import {
AbiConstructorFragment,
AbiErrorFragment,
AbiEventFragment,
AbiFunctionFragment,
decodeLog,
Expand All @@ -35,7 +39,7 @@ import {

import { blockSchema, logSchema } from 'web3-eth/dist/schemas';

import { Web3ContractError } from './errors';
import { Eip838Error, Web3ContractError } from './errors';
// eslint-disable-next-line import/no-cycle
import { ContractAbiWithSignature, ContractOptions, EventLog } from './types';

Expand Down Expand Up @@ -225,11 +229,54 @@ export const decodeMethodReturn = (abi: AbiFunctionFragment, returnValues?: HexS
// eslint-disable-next-line no-null/no-null
return null;
}
const result = decodeParameters([...abi.outputs], value);
const result = decodeParameters([...abi.outputs], value); // todo: decode abi.input for EIP-838

if (result.__length__ === 1) {
return result[0];
}

return result;
};

function getErrorSignature(abi: AbiErrorFragment) {
return `${abi.name}(${abi.inputs?.map(a => a.type).join(',') ?? ''})`;
}

export const throwDecodedError = (
abisOfErrors: AbiErrorFragment[],
error: { code: number; message: string; data: HexString } | Error,
) => {
if (typeof error === 'object' && error !== undefined && 'data' in error) {
let errorArgs;
let errorName;
let errorSignature;
try {
const bytes = arrayify(error.data);
const errorData = bytes.slice(4);

const errorSha = error.data.slice(0, 10);
const errorAbi = abisOfErrors.find(abi =>
sha3Raw(getErrorSignature(abi)).startsWith(errorSha),
);

if (errorAbi?.inputs) {
errorName = errorAbi.name;
errorSignature = getErrorSignature(errorAbi);
errorArgs = decodeParameters([...errorAbi.inputs], hexlify(errorData)); // decode abi.input for EIP-838
}
} catch (err) {
console.error(err);
}

throw new Eip838Error(
error.code,
error.message,
error.data,
errorName,
errorSignature,
errorArgs,
);
} else {
throw error;
}
};
28 changes: 27 additions & 1 deletion packages/web3-eth-contract/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ 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 { TransactionReceipt } from 'web3-types';
/* eslint-disable max-classes-per-file */

import { TransactionReceipt, HexString } from 'web3-types';
import { ERR_CONTRACT, Web3Error } from 'web3-errors';

export class Web3ContractError extends Web3Error {
Expand All @@ -28,3 +30,27 @@ export class Web3ContractError extends Web3Error {
this.receipt = receipt;
}
}

export class Eip838Error extends Web3ContractError {
// public code: string; // TODO: check if there is a need of overriding the `code` into a string
public data: HexString;
public errorName?: string;
public errorSignature?: string;
public errorArgs?: { [K in string]: unknown };

public constructor(
code: number,
message: string,
data: HexString,
errorName?: string,
errorSignature?: string,
errorArgs?: { [K in string]: unknown },
) {
super(message);
this.code = code;
this.data = data;
this.errorName = errorName;
this.errorSignature = errorSignature;
this.errorArgs = errorArgs;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
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 { Contract } from '../../src';
import { Eip838Error } from '../../src/errors';
import { createTempAccount } from '../fixtures/system_test_utils';

describe('contract errors', () => {
let sendOptions: Record<string, unknown>;

beforeAll(async () => {
const acc = await createTempAccount();
sendOptions = { from: acc.address };
});

describe('Test EIP-838 Error Codes', () => {
const addr = '0xbd0B4B009a76CA97766360F04f75e05A3E449f1E';
it('testError1', async () => {
const abi = [
{
inputs: [
{ internalType: 'address', name: 'addr', type: 'address' },
{ internalType: 'uint256', name: 'value', type: 'uint256' },
],
name: 'TestError1',
type: 'error',
},
{
inputs: [
{ internalType: 'bool', name: 'pass', type: 'bool' },
{ internalType: 'address', name: 'addr', type: 'address' },
{ internalType: 'uint256', name: 'value', type: 'uint256' },
],
name: 'testError1',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'pure',
type: 'function',
},
] as const;
const contract = new Contract(abi, addr);
contract.setProvider('https://ropsten.infura.io/v3/49a0efa3aaee4fd99797bfa94d8ce2f1');

let error: Eip838Error | undefined;
try {
await contract.methods.testError1(false, addr, 42).call(sendOptions);

// execution should throw before this line, if not throw here to indicate an issue.
} catch (err: any) {
error = err;
}

expect(error).toBeDefined();
expect(error).toBeInstanceOf(Eip838Error);

// TODO: do we need something like the following?
// expect(error.code).toEqual('CALL_EXCEPTION');
expect(error?.errorArgs && error?.errorArgs[0]).toEqual(addr);
expect(error?.errorArgs?.addr).toEqual(addr);
expect(error?.errorArgs && error?.errorArgs[1]).toEqual(BigInt(42));
expect(error?.errorArgs?.value).toEqual(BigInt(42));
expect(error?.errorName).toBe('TestError1');
expect(error?.errorSignature).toBe('TestError1(address,uint256)');
});
});
});

0 comments on commit bfa0831

Please sign in to comment.