Skip to content

Commit

Permalink
6355 - allow data to be used in contract methods. (#6377)
Browse files Browse the repository at this point in the history
* update formatsubscriptionResult

* update changelog

* data and input typings updated

* debug contract

* debug

* update debug

* add `{fillInputAndData: true,}` to `eth_call` on method_wrappers

* fixing tests

* fix unit tests

* update tests

* remove console log

* clean up

* update test case and reformat

* address feedback

* update else statement

* adding tests and refactoring

* fix builds

* address feedback

* address feedback

* add dataInputFill flag

* add datafillinput config

* update changelogs

* updating docs

* fixing linter

* add provider example

* updating docs

* updating docs

---------

Co-authored-by: moshmage <[email protected]>
  • Loading branch information
Alex and moshmage authored Sep 11, 2023
1 parent 3a9f7d7 commit 355a548
Show file tree
Hide file tree
Showing 14 changed files with 741 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ node index.js

If everything is working correctly, you should see the current block number logged to the console. However, if you got an error with the reason `connect ECONNREFUSED 127.0.0.1:7545` then double check that you are running Ganache locally on port `7545`.

## Step 5: Deploy the smart contract to the Ganache network using web3.js
## Step 6: Deploy the smart contract to the Ganache network using web3.js

In this step, we will use web3.js to deploy the smart contract to the Ganache network.

Expand Down Expand Up @@ -282,7 +282,7 @@ Estimated gas: 142748n
Contract deployed at address: 0x16447837D4A572d0a8b419201bdcD91E6e428Df1
```

## Step 6: Interact with the smart contract using web3.js
## Step 7: Interact with the smart contract using web3.js

In this step, we will use web3.js to interact with the smart contract on the Ganache network.

Expand Down Expand Up @@ -353,6 +353,60 @@ Transaction Hash: 0x9825e2a2115896728d0c9c04c2deaf08dfe1f1ff634c4b0e6eeb2f504372
my number updated value: 2
```

## Troubleshooting and errors

If you are running into errors when executing contract methods such as `myContract.methods.call` or `myContract.deploy.estimateGas()` you might be seeing a contract execution revert error such as: `value transfer did not complete from a contract execution reverted`

or response error: ResponseError: Returned error: unknown field `input`, expected one of `from`, `to`, `gasPrice`, `maxFeePerGas`, `maxPriorityFeePerGas`, `gas`, `value`, `data`, `nonce`, `chainId`, `accessList`, `type`.

This could be due to the node you are connected to and is expecting the `data` property to be populated in your contract instead of `input`, for example this issue will happen with an Anvil node from Foundry. Web3 version >4.0.3 will always populate `input` when sending transactions.
To fix this, configure the `contractDataInputFill` in `Web3Config` or when initializing your contract to specify `data` in `dataInputFill` to be filled.
Another way to fix this is to provide `data` when using the send or call method.
If you want both `data` and `input` filled, set the property to `both`.

Here are examples:

```typescript

// Configuring Web3Context with `contractDataInputFill`
import { Web3Context } from 'web3-core';

const expectedProvider = 'http://127.0.0.1:8545';
const web3Context = new Web3Context({
provider: expectedProvider,
config: {contractDataInputFill: 'data'} // all new contracts created to populate `data` field
});

const contract = new Contract(GreeterAbi, web3Context);

// data will now be populated when using the call method
const res = await contract.methods.greet().call();

// Another way to do this is to set it within the contract using `dataInputFill`

const contract = new Contract(
erc721Abi,
'0x1230B93ffd14F2F022039675fA3fc3A46eE4C701',
{ gas: '123', dataInputFill: "data" }, // methods will now be populating `data` field
);

// `data` will now be populated instead of `input`
contract.methods.approve('0x00000000219ab540356cBB839Cbe05303d7705Fa', 1).call(),

// Another way to do this is to set `data` when calling methods

const contract = new Contract(
erc721Abi,
'0x1230B93ffd14F2F022039675fA3fc3A46eE4C701',
);

contract.methods.approve('0x00000000219ab540356cBB839Cbe05303d7705Fa', 1).call(
{data: contract.methods.approve('0x00000000219ab540356cBB839Cbe05303d7705Fa', 1).encodeABI()}
)


```

## Conclusion

In this tutorial, we learned how to generate the ABI and the Bytecode of a smart contract, deploy it to the Ethereum network, and interact with it using web3.js version 4.x.
Expand Down
20 changes: 20 additions & 0 deletions packages/web3-core/src/web3_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface Web3ConfigOptions {
transactionConfirmationPollingInterval?: number;
blockHeaderTimeout: number;
maxListenersWarningThreshold: number;
contractDataInputFill: 'data' | 'input' | 'both';
defaultNetworkId?: Numbers;
defaultChain: string;
defaultHardfork: string;
Expand Down Expand Up @@ -78,6 +79,7 @@ export abstract class Web3Config
transactionConfirmationPollingInterval: undefined,
blockHeaderTimeout: 10,
maxListenersWarningThreshold: 100,
contractDataInputFill: 'input',
defaultNetworkId: undefined,
defaultChain: 'mainnet',
defaultHardfork: 'london',
Expand Down Expand Up @@ -126,6 +128,24 @@ export abstract class Web3Config
this.config.handleRevert = val;
}

/**
* The `contractDataInputFill` options property will allow you to set the hash of the method signature and encoded parameters to the property
* either `data`, `input` or both within your contract.
* This will affect the contracts send, call and estimateGas methods
* Default is `input`.
*/
public get contractDataInputFill() {
return this.config.contractDataInputFill;
}

/**
* Will set the contractDataInputFill
*/
public set contractDataInputFill(val) {
this._triggerConfigChange('contractDataInputFill', val);
this.config.contractDataInputFill = val;
}

/**
* This default address is used as the default `from` property, if no `from` property is specified in for the following methods:
* - web3.eth.sendTransaction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Object {
"accountProvider": undefined,
"config": Object {
"blockHeaderTimeout": 10,
"contractDataInputFill": "input",
"defaultAccount": undefined,
"defaultBlock": "latest",
"defaultChain": "mainnet",
Expand Down
1 change: 1 addition & 0 deletions packages/web3-core/test/unit/web3_config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const defaultConfig = {
useRpcCallSpecification: false,
},
handleRevert: false,
contractDataInputFill: 'input',
maxListenersWarningThreshold: 100,
transactionBlockTimeout: 50,
transactionConfirmationBlocks: 24,
Expand Down
7 changes: 7 additions & 0 deletions packages/web3-eth-contract/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,10 @@ Documentation:
- In case of error events there will be inner error also available for details

## [Unreleased]

### Fixed

### Added

- Added `dataInputFill` as a ContractInitOption, allowing users to have the choice using property `data`, `input` or `both` for contract methods to be sent to the RPC provider. (#6355)
- Added to `Web3Config` property `contractDataInputFill` allowing users to have the choice using property `data`, `input` or `both` for contract methods to be sent to the RPC provider when creating contracts. (#6377)
78 changes: 49 additions & 29 deletions packages/web3-eth-contract/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export class Contract<Abi extends ContractAbi>
* myContract.options.gas = 5000000; // provide as fallback always 5M gas
* ```
*/

public readonly options: ContractOptions;

/**
Expand All @@ -233,6 +234,12 @@ export class Contract<Abi extends ContractAbi>
private readonly _overloadedMethodAbis: Map<string, AbiFunctionFragment[]>;
private _methods!: ContractMethodsInterface<Abi>;
private _events!: ContractEventsInterface<Abi>;
/**
* Set property to `data`, `input`, or `both` to change the property of the contract being sent to the
* RPC provider when using contract methods.
* Default is `input`
*/
private readonly _dataInputFill?: 'data' | 'input' | 'both';

private context?: Web3Context;
/**
Expand Down Expand Up @@ -308,12 +315,6 @@ export class Contract<Abi extends ContractAbi>
? optionsOrContextOrReturnFormat
: undefined;

if (!isNullish(options) && !isNullish(options.data) && !isNullish(options.input))
throw new ContractTransactionDataAndInputError({
data: options.data as HexString,
input: options.input as HexString,
});

let contractContext;
if (isWeb3ContractContext(addressOrOptionsOrContext)) {
contractContext = addressOrOptionsOrContext;
Expand Down Expand Up @@ -348,7 +349,16 @@ export class Contract<Abi extends ContractAbi>
provider,
registeredSubscriptions: contractSubscriptions,
});

if (
!isNullish(options) &&
!isNullish(options.data) &&
!isNullish(options.input) &&
this.config.contractDataInputFill !== 'both'
)
throw new ContractTransactionDataAndInputError({
data: options.data as HexString,
input: options.input as HexString,
});
this._overloadedMethodAbis = new Map<string, AbiFunctionFragment[]>();

// eslint-disable-next-line no-nested-ternary
Expand All @@ -361,6 +371,13 @@ export class Contract<Abi extends ContractAbi>
const address =
typeof addressOrOptionsOrContext === 'string' ? addressOrOptionsOrContext : undefined;

if (this.config.contractDataInputFill === 'both') {
this._dataInputFill = this.config.contractDataInputFill;
} else {
this._dataInputFill =
(options as ContractInitOptions)?.dataInputFill ??
this.config.contractDataInputFill;
}
this._parseAndSetJsonInterface(jsonInterface, returnDataFormat);

if (!isNullish(address)) {
Expand All @@ -373,14 +390,14 @@ export class Contract<Abi extends ContractAbi>
gas: options?.gas ?? options?.gasLimit,
gasPrice: options?.gasPrice,
from: options?.from,
input: options?.input ?? options?.data,
input: options?.input,
data: options?.data,
};

this.syncWithContext = (options as ContractInitOptions)?.syncWithContext ?? false;
if (contractContext instanceof Web3Context) {
this.subscribeToContextEvents(contractContext);
}

Object.defineProperty(this.options, 'address', {
set: (value: Address) => this._parseAndSetAddress(value, returnDataFormat),
get: () => this._address,
Expand Down Expand Up @@ -470,7 +487,6 @@ export class Contract<Abi extends ContractAbi>
*/
public clone() {
let newContract: Contract<any>;

if (this.options.address) {
newContract = new Contract<Abi>(
[...this._jsonInterface, ...this._errorsInterface] as unknown as Abi,
Expand All @@ -480,8 +496,10 @@ export class Contract<Abi extends ContractAbi>
gasPrice: this.options.gasPrice,
from: this.options.from,
input: this.options.input,
data: this.options.data,
provider: this.currentProvider,
syncWithContext: this.syncWithContext,
dataInputFill: this._dataInputFill,
},
this.getContextObject(),
);
Expand All @@ -493,8 +511,10 @@ export class Contract<Abi extends ContractAbi>
gasPrice: this.options.gasPrice,
from: this.options.from,
input: this.options.input,
data: this.options.data,
provider: this.currentProvider,
syncWithContext: this.syncWithContext,
dataInputFill: this._dataInputFill,
},
this.getContextObject(),
);
Expand Down Expand Up @@ -577,7 +597,6 @@ export class Contract<Abi extends ContractAbi>
arguments?: ContractConstructorArgs<Abi>;
}) {
let abi = this._jsonInterface.find(j => j.type === 'constructor') as AbiConstructorFragment;

if (!abi) {
abi = {
type: 'constructor',
Expand All @@ -588,18 +607,24 @@ export class Contract<Abi extends ContractAbi>

const _input = format(
{ format: 'bytes' },
deployOptions?.input ?? deployOptions?.data ?? this.options.input,
deployOptions?.input ?? this.options.input,
DEFAULT_RETURN_FORMAT,
);

const _data = format(
{ format: 'bytes' },
deployOptions?.data ?? this.options.data,
DEFAULT_RETURN_FORMAT,
);

if (!_input || _input.trim() === '0x') {
if ((!_input || _input.trim() === '0x') && (!_data || _data.trim() === '0x')) {
throw new Web3ContractError('contract creation without any data provided.');
}

const args = deployOptions?.arguments ?? [];

const contractOptions: ContractOptions = { ...this.options, input: _input };

const contractOptions: ContractOptions = { ...this.options, input: _input, data: _data };
const deployData = _input ?? _data;
return {
arguments: args,
send: (
Expand All @@ -623,7 +648,6 @@ export class Contract<Abi extends ContractAbi>
returnFormat: ReturnFormat = DEFAULT_RETURN_FORMAT as ReturnFormat,
) => {
const modifiedOptions = { ...options };

return this._contractMethodEstimateGas({
abi: abi as AbiFunctionFragment,
params: args as unknown[],
Expand All @@ -636,7 +660,7 @@ export class Contract<Abi extends ContractAbi>
encodeMethodABI(
abi as AbiFunctionFragment,
args as unknown[],
format({ format: 'bytes' }, _input as Bytes, DEFAULT_RETURN_FORMAT),
format({ format: 'bytes' }, deployData as Bytes, DEFAULT_RETURN_FORMAT),
),
};
}
Expand Down Expand Up @@ -783,7 +807,6 @@ export class Contract<Abi extends ContractAbi>
returnFormat: DataFormat = DEFAULT_RETURN_FORMAT,
) {
this._functions = {};

this._methods = {} as ContractMethodsInterface<Abi>;
this._events = {} as ContractEventsInterface<Abi>;

Expand Down Expand Up @@ -913,7 +936,6 @@ export class Contract<Abi extends ContractAbi>
throw new Web3ValidatorError(errors);
}
}

const methods = {
arguments: abiParams,

Expand Down Expand Up @@ -981,7 +1003,10 @@ export class Contract<Abi extends ContractAbi>
const tx = getEthTxCallParams({
abi,
params,
options,
options: {
...options,
dataInputFill: this._dataInputFill,
},
contractOptions: {
...this.options,
from: this.options.from ?? this.config.defaultAccount,
Expand Down Expand Up @@ -1011,7 +1036,7 @@ export class Contract<Abi extends ContractAbi>
const tx = getCreateAccessListParams({
abi,
params,
options,
options: { ...options, dataInputFill: this.config.contractDataInputFill },
contractOptions: {
...this.options,
from: this.options.from ?? this.config.defaultAccount,
Expand Down Expand Up @@ -1042,11 +1067,10 @@ export class Contract<Abi extends ContractAbi>
input: undefined,
from: modifiedContractOptions.from ?? this.defaultAccount ?? undefined,
};

const tx = getSendTxParams({
abi,
params,
options,
options: { ...options, dataInputFill: this.config.contractDataInputFill },
contractOptions: modifiedContractOptions,
});
const transactionToSend = sendTransaction(this, tx, DEFAULT_RETURN_FORMAT, {
Expand All @@ -1061,7 +1085,6 @@ export class Contract<Abi extends ContractAbi>
decodeContractErrorData(errorsAbi, error.innerError);
}
});

return transactionToSend;
}

Expand All @@ -1076,14 +1099,12 @@ export class Contract<Abi extends ContractAbi>
...modifiedContractOptions,
from: modifiedContractOptions.from ?? this.defaultAccount ?? undefined,
};

const tx = getSendTxParams({
abi,
params,
options,
options: { ...options, dataInputFill: this.config.contractDataInputFill },
contractOptions: modifiedContractOptions,
});

return sendTransaction(this, tx, DEFAULT_RETURN_FORMAT, {
transactionResolver: receipt => {
if (receipt.status === BigInt(0)) {
Expand Down Expand Up @@ -1120,10 +1141,9 @@ export class Contract<Abi extends ContractAbi>
const tx = getEstimateGasParams({
abi,
params,
options,
options: { ...options, dataInputFill: this.config.contractDataInputFill },
contractOptions: contractOptions ?? this.options,
});

return estimateGas(this, tx, BlockTags.LATEST, returnFormat);
}

Expand Down
Loading

0 comments on commit 355a548

Please sign in to comment.