Skip to content

Commit

Permalink
Add contract.decodeMethodData(...) method to decode contract method…
Browse files Browse the repository at this point in the history
…s and to `contract.deploy(...).decodeData(...)` decode constructor parameters (#6950)


* add contract.deploy(...).decodeData(...) and contract.decodeMethodData(...)

* add unit tests

* update CHANGELOG.md

* add method documentation
  • Loading branch information
Muhammad-Altabba authored Apr 7, 2024
1 parent 5a579fa commit ce59737
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 50 deletions.
3 changes: 2 additions & 1 deletion packages/web3-eth-contract/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,4 +378,5 @@ Documentation:

### Added

- Add a console warning in case of an ambiguous call to a solidity method with parameter overloading (#6942)
- Added a console warning in case of an ambiguous call to a solidity method with parameter overloading (#6942)
- Added contract.deploy(...).decodeData(...) and contract.decodeMethodData(...) that decode data based on the ABI (#6950)
169 changes: 120 additions & 49 deletions packages/web3-eth-contract/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ import {
ContractOptions,
TransactionReceipt,
FormatType,
DecodedParams,
} from 'web3-types';
import {
format,
Expand All @@ -99,7 +100,12 @@ import {
ValidationSchemaInput,
Web3ValidatorError,
} from 'web3-validator';
import { decodeMethodReturn, encodeEventABI, encodeMethodABI } from './encoding.js';
import {
decodeMethodReturn,
decodeMethodParams,
encodeEventABI,
encodeMethodABI,
} from './encoding.js';
import { LogsSubscription } from './log_subscription.js';
import {
ContractEventOptions,
Expand Down Expand Up @@ -323,6 +329,67 @@ const contractSubscriptions = {
*
* ```
*
* ### decodeMethodData
* Decodes the given ABI-encoded data, revealing both the method name and the parameters used in the smart contract call.
* This function reverses the encoding process happens at the method `encodeABI`.
* It's particularly useful for debugging and understanding the interactions with and between smart contracts.
*
* #### Parameters
*
* - `data` **HexString**: The string of ABI-encoded data that needs to be decoded. This should include the method signature and the encoded parameters.
*
* #### Returns
*
* - **Object**: This object combines both the decoded parameters and the method name in a readable format. Specifically, the returned object contains:
* - `__method__` **String**: The name of the contract method, reconstructed from the ABI.
* - `__length__` **Number**: The number of parameters decoded.
* - Additional properties representing each parameter by name, as well as their position and values.
*
* #### Example
*
* Given an ABI-encoded string from a transaction, you can decode this data to identify the method called and the parameters passed.
* Here's a simplified example:
*
*
* ```typescript
* const GreeterAbi = [
* {
* inputs: [
* {
* internalType: 'string',
* name: '_greeting',
* type: 'string',
* },
* ],
* name: 'setGreeting',
* outputs: [],
* type: 'function',
* },
* ];
* const contract = new Contract(GreeterAbi); // Initialize with your contract's ABI
*
* // The ABI-encoded data string for "setGreeting('Hello World')"
* const encodedData =
* '0xa41368620000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b48656c6c6f20576f726c64000000000000000000000000000000000000000000';
*
* try {
* const decoded = contract.decodeMethodData(encodedData);
* console.log(decoded.__method__); // Outputs: "setGreeting(string)"
* console.log(decoded); // Outputs the detailed parameter data
* // This tells that the method called was `setGreeting` with a single string parameter "Hello World":
* // {
* // __method__: 'setGreeting(string)',
* // __length__: 1,
* // '0': 'Hello World',
* // _greeting: 'Hello World'
* // }
* } catch (error) {
* console.error(error);
* }
* ```
*
* ### createAccessList
* This will create an access list a method execution will access when executed in the EVM.
* Note: You must specify a from address and gas if it’s not specified in options when instantiating parent contract object.
Expand Down Expand Up @@ -446,11 +513,7 @@ export class Contract<Abi extends ContractAbi>
);
public constructor(
jsonInterface: Abi,
addressOrOptionsOrContext?:
| Address
| ContractInitOptions
| Web3ContractContext
| Web3Context,
addressOrOptionsOrContext?: Address | ContractInitOptions | Web3ContractContext | Web3Context,
optionsOrContextOrReturnFormat?:
| ContractInitOptions
| Web3ContractContext
Expand All @@ -476,20 +539,14 @@ export class Contract<Abi extends ContractAbi>
}

let provider;
if (
typeof addressOrOptionsOrContext === 'object' &&
'provider' in addressOrOptionsOrContext
) {
if (typeof addressOrOptionsOrContext === 'object' && 'provider' in addressOrOptionsOrContext) {
provider = addressOrOptionsOrContext.provider;
} else if (
typeof optionsOrContextOrReturnFormat === 'object' &&
'provider' in optionsOrContextOrReturnFormat
) {
provider = optionsOrContextOrReturnFormat.provider;
} else if (
typeof contextOrReturnFormat === 'object' &&
'provider' in contextOrReturnFormat
) {
} else if (typeof contextOrReturnFormat === 'object' && 'provider' in contextOrReturnFormat) {
provider = contextOrReturnFormat.provider;
} else {
provider = Contract.givenProvider;
Expand Down Expand Up @@ -733,6 +790,22 @@ export class Contract<Abi extends ContractAbi>
* > '0x12345...0000012345678765432'
*
*
* // decoding
* myContract.deploy({
* input: '0x12345...',
* // arguments: [123, 'My Greeting'] if you just need to decode the data, you can skip the arguments
* })
* .decodeData('0x12345...0000012345678765432');
* > {
* __method__: 'constructor',
* __length__: 2,
* '0': '123',
* _id: '123',
* '1': 'My Greeting',
* _greeting: 'My Greeting',
* }
*
*
* // Gas estimation
* myContract.deploy({
* input: '0x12345...',
Expand Down Expand Up @@ -816,6 +889,14 @@ export class Contract<Abi extends ContractAbi>
args as unknown[],
format({ format: 'bytes' }, deployData as Bytes, DEFAULT_RETURN_FORMAT),
),
decodeData: (data: HexString) => ({
...decodeMethodParams(
abi as AbiFunctionFragment,
data.replace(deployData as string, ''),
false,
),
__method__: abi.type, // abi.type is constructor
}),
};
}

Expand Down Expand Up @@ -947,10 +1028,7 @@ export class Contract<Abi extends ContractAbi>
if (hashedIndexedString === String(log.returnValues[key])) return true;
}

return (
String(log.returnValues[key]).toUpperCase() ===
String(filter[key]).toUpperCase()
);
return String(log.returnValues[key]).toUpperCase() === String(filter[key]).toUpperCase();
});
});
}
Expand All @@ -964,6 +1042,21 @@ export class Contract<Abi extends ContractAbi>
: value;
}

public decodeMethodData(data: HexString): DecodedParams & { __method__: string } {
const methodSignature = data.slice(0, 10);
const functionsAbis = this._jsonInterface.filter(j => j.type !== 'error');

const abi = functionsAbis.find(
a => methodSignature === encodeFunctionSignature(jsonInterfaceMethodToString(a)),
);
if (!abi) {
throw new Web3ContractError(
`The ABI for the provided method signature ${methodSignature} was not found.`,
);
}
return { ...decodeMethodParams(abi, data), __method__: jsonInterfaceMethodToString(abi) };
}

private _parseAndSetJsonInterface(
abis: ContractAbi,
returnFormat: DataFormat = DEFAULT_RETURN_FORMAT,
Expand All @@ -975,9 +1068,7 @@ export class Contract<Abi extends ContractAbi>
let result: ContractAbi = [];

const functionsAbi = abis.filter(abi => abi.type !== 'error');
const errorsAbi = abis.filter(abi =>
isAbiErrorFragment(abi),
) as unknown as AbiErrorFragment[];
const errorsAbi = abis.filter(abi => isAbiErrorFragment(abi)) as unknown as AbiErrorFragment[];

for (const a of functionsAbi) {
const abi: Mutable<AbiFragment & { signature: HexString }> = {
Expand All @@ -993,9 +1084,7 @@ export class Contract<Abi extends ContractAbi>

// make constant and payable backwards compatible
abi.constant =
abi.stateMutability === 'view' ??
abi.stateMutability === 'pure' ??
abi.constant;
abi.stateMutability === 'view' ?? abi.stateMutability === 'pure' ?? abi.constant;

abi.payable = abi.stateMutability === 'payable' ?? abi.payable;
this._overloadedMethodAbis.set(abi.name, [
Expand All @@ -1015,13 +1104,11 @@ export class Contract<Abi extends ContractAbi>

this._functions[methodName] = {
signature: methodSignature,
method: contractMethod,
method: exactContractMethod,
};

// 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[
methodName
].method as never;
this._methods[abi.name as keyof ContractMethodsInterface<Abi>] = contractMethod as never;

// We don't know a particular type of the Abi method so can't type check
this._methods[methodName as keyof ContractMethodsInterface<Abi>] =
Expand Down Expand Up @@ -1137,14 +1224,7 @@ export class Contract<Abi extends ContractAbi>
call: async (
options?: PayableCallOptions | NonPayableCallOptions,
block?: BlockNumberOrTag,
) =>
this._contractMethodCall(
methodAbi,
abiParams,
internalErrorsAbis,
options,
block,
),
) => this._contractMethodCall(methodAbi, abiParams, internalErrorsAbis, options, block),

send: (options?: PayableTxOptions | NonPayableTxOptions): ContractMethodSend =>
this._contractMethodSend(methodAbi, abiParams, internalErrorsAbis, options),
Expand All @@ -1161,6 +1241,7 @@ export class Contract<Abi extends ContractAbi>
}),

encodeABI: () => encodeMethodABI(methodAbi, abiParams),
decodeData: (data: HexString) => decodeMethodParams(methodAbi, data),

createAccessList: async (
options?: PayableCallOptions | NonPayableCallOptions,
Expand Down Expand Up @@ -1351,11 +1432,7 @@ export class Contract<Abi extends ContractAbi>
returnFormat: DataFormat = DEFAULT_RETURN_FORMAT,
): ContractBoundEvent {
return (...params: unknown[]) => {
const { topics, fromBlock } = encodeEventABI(
this.options,
abi,
params[0] as EventParameters,
);
const { topics, fromBlock } = encodeEventABI(this.options, abi, params[0] as EventParameters);
const sub = new LogsSubscription(
{
address: this.options.address,
Expand All @@ -1365,10 +1442,7 @@ export class Contract<Abi extends ContractAbi>
},
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
subscriptionManager: this.subscriptionManager as Web3SubscriptionManager<
unknown,
any
>,
subscriptionManager: this.subscriptionManager as Web3SubscriptionManager<unknown, any>,
returnFormat,
},
);
Expand All @@ -1381,10 +1455,7 @@ export class Contract<Abi extends ContractAbi>
}
})
.catch((error: Error) => {
sub.emit(
'error',
new SubscriptionError('Failed to get past events.', error),
);
sub.emit('error', new SubscriptionError('Failed to get past events.', error));
});
}
this.subscriptionManager?.addSubscription(sub).catch((error: Error) => {
Expand Down
16 changes: 16 additions & 0 deletions packages/web3-eth-contract/src/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,22 @@ export const encodeMethodABI = (
return `${encodeFunctionSignature(abi)}${params}`;
};

export const decodeMethodParams = (
abi: AbiFunctionFragment | AbiConstructorFragment,
data: HexString,
methodSignatureProvided = true,
) => {
const value =
methodSignatureProvided && data && data.length >= 10 && data.startsWith('0x')
? data.slice(10)
: data;
if (!abi.inputs) {
throw new Web3ContractError('No inputs found in the ABI');
}
const result = decodeParameters([...abi.inputs], value);
return result;
};

export const decodeMethodReturn = (abi: AbiFunctionFragment, returnValues?: HexString) => {
// If it was constructor then we need to return contract address
if (abi.type === 'constructor') {
Expand Down
16 changes: 16 additions & 0 deletions packages/web3-eth-contract/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,14 @@ export interface NonPayableMethodObject<Inputs = unknown[], Outputs = unknown[]>
*/
encodeABI(): string;

/**
* Decode raw result of method call into readable value(s).
*
* @param data - The data to decode.
* @returns - The decoded data.
*/
decodeData<SpecialInputs = Inputs>(data: HexString): SpecialInputs;

/**
* This method generates an access list for a transaction. You must specify a `from` address and `gas` if it’s not specified in options.
*
Expand Down Expand Up @@ -400,6 +408,14 @@ export interface PayableMethodObject<Inputs = unknown[], Outputs = unknown[]> {
*/
encodeABI(): HexString;

/**
* Decode raw result of method call into readable value(s).
*
* @param data - The data to decode.
* @returns - The decoded data.
*/
decodeData<SpecialInputs = Inputs>(data: HexString): SpecialInputs;

/**
* This method generates an access list for a transaction. You must specify a `from` address and `gas` if it’s not specified in options.
*
Expand Down
Loading

1 comment on commit ce59737

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: ce59737 Previous: 6c075db Ratio
processingTx 9539 ops/sec (±4.00%) 9301 ops/sec (±4.81%) 0.98
processingContractDeploy 39528 ops/sec (±6.32%) 39129 ops/sec (±7.62%) 0.99
processingContractMethodSend 20249 ops/sec (±7.33%) 19443 ops/sec (±5.19%) 0.96
processingContractMethodCall 41027 ops/sec (±6.04%) 38971 ops/sec (±6.34%) 0.95
abiEncode 44826 ops/sec (±8.08%) 44252 ops/sec (±6.92%) 0.99
abiDecode 31482 ops/sec (±6.62%) 30419 ops/sec (±8.89%) 0.97
sign 1593 ops/sec (±0.84%) 1656 ops/sec (±4.08%) 1.04
verify 380 ops/sec (±0.45%) 373 ops/sec (±0.78%) 0.98

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.