Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement eth.calculateFeeData #6795

Merged
merged 25 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0563919
add getMaxPriorityFeePerGas method
Muhammad-Altabba Jan 23, 2024
db81ec2
update CHANGELOG.md files
Muhammad-Altabba Jan 23, 2024
3281621
Merge branch '4.x' into 4195-plans-to-support-eth_maxpriorityfeeperga…
Muhammad-Altabba Jan 24, 2024
c1acca7
remove unnecessary cleaning step at github workflows
Muhammad-Altabba Jan 24, 2024
d7ffa4d
Merge remote-tracking branch 'origin/4.x' into 4195-plans-to-support-…
Muhammad-Altabba Jan 25, 2024
a6bc23d
revert unneeded changes
Muhammad-Altabba Jan 25, 2024
585510d
implement and test `calculateFeeData`
Muhammad-Altabba Jan 30, 2024
66a6933
Merge branch '4195-plans-to-support-eth_maxpriorityfeepergas-method' …
Muhammad-Altabba Jan 30, 2024
d3f97d8
fix lint issues
Muhammad-Altabba Jan 30, 2024
cee695a
update a test snapshot
Muhammad-Altabba Jan 30, 2024
7d83d96
Merge branch '4195-plans-to-support-eth_maxpriorityfeepergas-method' …
Muhammad-Altabba Jan 30, 2024
9cb1a3f
resolve snapshot issue between node 18 vs 20 & 21
Muhammad-Altabba Feb 7, 2024
efc14de
Merge branch '4.x' into 6599-implement-providergetfeedata
Muhammad-Altabba Feb 7, 2024
3aecfa4
update a comment
Muhammad-Altabba Feb 7, 2024
d2fb9d3
Merge branch '6756-web3-core-snapshot-issue' into 6599-implement-prov…
Muhammad-Altabba Feb 7, 2024
fc3d121
disable a ts error
Muhammad-Altabba Feb 7, 2024
bf7c170
fix linting issues
Muhammad-Altabba Feb 7, 2024
9b5b2ec
update test file name
Muhammad-Altabba Feb 7, 2024
0ca633f
update a test snapshot
Muhammad-Altabba Feb 7, 2024
0b28bb8
Merge branch '4.x' into 6599-implement-providergetfeedata
Muhammad-Altabba Feb 7, 2024
8077bce
Merge branch '4.x' into 6599-implement-providergetfeedata
Muhammad-Altabba Feb 13, 2024
140194a
add integration test for `calculateFeeData`
Muhammad-Altabba Feb 13, 2024
bb035dd
update CHANGELOG.md file
Muhammad-Altabba Feb 13, 2024
d9bb9f7
Merge branch '4.x' into 6599-implement-providergetfeedata
Muhammad-Altabba Feb 15, 2024
f51e94b
Merge branch '4.x' into 6599-implement-providergetfeedata
Muhammad-Altabba Feb 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 89 additions & 10 deletions packages/web3-eth/src/web3_eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
SupportedProviders,
Address,
Bytes,
FeeData,
Filter,
HexString32Bytes,
HexString8Bytes,
Expand All @@ -38,10 +39,12 @@ import {
DataFormat,
DEFAULT_RETURN_FORMAT,
Eip712TypedData,
FMT_BYTES,
FMT_NUMBER,
} from 'web3-types';
import { isSupportedProvider, Web3Context, Web3ContextInitOptions } from 'web3-core';
import { TransactionNotFound } from 'web3-errors';
import { toChecksumAddress, isNullish } from 'web3-utils';
import { toChecksumAddress, isNullish, ethUnitMap } from 'web3-utils';
import { ethRpcMethods } from 'web3-rpc-methods';

import * as rpcMethodsWrappers from './rpc_method_wrappers.js';
Expand Down Expand Up @@ -72,27 +75,27 @@ export const registeredSubscriptions = {
};

/**
*
*
* The Web3Eth allows you to interact with an Ethereum blockchain.
*
*
* For using Web3 Eth functions, first install Web3 package using `npm i web3` or `yarn add web3` based on your package manager usage.
* After that, Web3 Eth functions will be available as mentioned in following snippet.
* After that, Web3 Eth functions will be available as mentioned in following snippet.
* ```ts
* import { Web3 } from 'web3';
* const web3 = new Web3('https://mainnet.infura.io/v3/<YOURPROJID>');
*
*
* const block = await web3.eth.getBlock(0);
*
*
* ```
*
*
* For using individual package install `web3-eth` package using `npm i web3-eth` or `yarn add web3-eth` and only import required functions.
* This is more efficient approach for building lightweight applications.
* This is more efficient approach for building lightweight applications.
* ```ts
* import { Web3Eth } from 'web3-eth';
*
*
* const eth = new Web3Eth('https://mainnet.infura.io/v3/<YOURPROJID>');
* const block = await eth.getBlock(0);
*
*
* ```
*/
export class Web3Eth extends Web3Context<Web3EthExecutionAPI, RegisteredSubscription> {
Expand All @@ -103,6 +106,7 @@ export class Web3Eth extends Web3Context<Web3EthExecutionAPI, RegisteredSubscrip
typeof providerOrContext === 'string' ||
isSupportedProvider(providerOrContext as SupportedProviders<any>)
) {
// @ts-expect-error disable the error: "A 'super' call must be a root-level statement within a constructor of a derived class that contains initialized properties, parameter properties, or private identifiers."
super({
provider: providerOrContext as SupportedProviders<any>,
registeredSubscriptions,
Expand Down Expand Up @@ -256,6 +260,81 @@ export class Web3Eth extends Web3Context<Web3EthExecutionAPI, RegisteredSubscrip
return rpcMethodsWrappers.getMaxPriorityFeePerGas(this, returnFormat);
}

/**
* Calculates the current Fee Data.
* If the node supports EIP-1559, then the `maxFeePerGas` and `maxPriorityFeePerGas` will be calculated.
* If the node does not support EIP-1559, then the `gasPrice` will be returned and the rest are `null`s.
*
* @param baseFeePerGasFactor The factor to multiply the baseFeePerGas with, if the node supports EIP-1559.
* @param alternativeMaxPriorityFeePerGas The alternative maxPriorityFeePerGas to use, if the node supports EIP-1559, but does not support the method `eth_maxPriorityFeePerGas`.
* @returns The current fee data.
*
* ```ts
* web3.eth.calculateFeeData().then(console.log);
* > {
* gasPrice: 20000000000n,
* maxFeePerGas: 20000000000n,
* maxPriorityFeePerGas: 20000000000n,
* baseFeePerGas: 20000000000n
* }
*
* web3.eth.calculateFeeData(ethUnitMap.Gwei, 2n).then(console.log);
* > {
* gasPrice: 20000000000n,
* maxFeePerGas: 40000000000n,
* maxPriorityFeePerGas: 20000000000n,
* baseFeePerGas: 20000000000n
* }
* ```
*/
public async calculateFeeData(
Muhammad-Altabba marked this conversation as resolved.
Show resolved Hide resolved
baseFeePerGasFactor = BigInt(2),
alternativeMaxPriorityFeePerGas = ethUnitMap.Gwei,
): Promise<FeeData> {
const block = await this.getBlock<{ number: FMT_NUMBER.BIGINT; bytes: FMT_BYTES.HEX }>(
undefined,
false,
);

const baseFeePerGas: bigint | undefined = block?.baseFeePerGas ?? undefined; // use undefined if it was null

let gasPrice: bigint | undefined;
try {
gasPrice = await this.getGasPrice<{ number: FMT_NUMBER.BIGINT; bytes: FMT_BYTES.HEX }>();
} catch (error) {
// do nothing
}

let maxPriorityFeePerGas: bigint | undefined;
try {
maxPriorityFeePerGas = await this.getMaxPriorityFeePerGas<{
number: FMT_NUMBER.BIGINT;
bytes: FMT_BYTES.HEX;
}>();
} catch (error) {
// do nothing
}

let maxFeePerGas: bigint | undefined;
// if the `block.baseFeePerGas` is available, then EIP-1559 is supported
// and we can calculate the `maxFeePerGas` from the `block.baseFeePerGas`
if (baseFeePerGas) {
// tip the miner with alternativeMaxPriorityFeePerGas, if no value available from getMaxPriorityFeePerGas
maxPriorityFeePerGas = maxPriorityFeePerGas ?? alternativeMaxPriorityFeePerGas;
// basically maxFeePerGas = (baseFeePerGas +- 12.5%) + maxPriorityFeePerGas
// and we multiply the `baseFeePerGas` by `baseFeePerGasFactor`, to allow
// trying to include the transaction in the next few blocks even if the
// baseFeePerGas is increasing fast
maxFeePerGas = baseFeePerGas * baseFeePerGasFactor + maxPriorityFeePerGas;
}

return { gasPrice, maxFeePerGas, maxPriorityFeePerGas, baseFeePerGas };
}

// an alias for calculateFeeData
// eslint-disable-next-line
public getFeeData = this.calculateFeeData;

/**
* @returns A list of accounts the node controls (addresses are checksummed).
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,46 @@ describe('Web3Eth.sendTransaction', () => {
expect(minedTransactionData).toMatchObject(transaction);
});

it('should send a successful type 0x2 transaction (gas = estimateGas)', async () => {
const transaction: Transaction = {
from: tempAcc.address,
to: '0x0000000000000000000000000000000000000000',
value: BigInt(1),
type: BigInt(2),
};

transaction.gas = await web3Eth.estimateGas(transaction);

const response = await web3Eth.sendTransaction(transaction);
expect(response.events).toBeUndefined();
expect(response.type).toBe(BigInt(2));
expect(response.status).toBe(BigInt(1));

const minedTransactionData = await web3Eth.getTransaction(response.transactionHash);
expect(minedTransactionData).toMatchObject(transaction);
});

it('should send a successful type 0x2 transaction (fee per gas from: calculateFeeData)', async () => {
const transaction: Transaction = {
from: tempAcc.address,
to: '0x0000000000000000000000000000000000000000',
value: BigInt(1),
type: BigInt(2),
};

const feeData = await web3Eth.calculateFeeData();
transaction.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
transaction.maxFeePerGas = feeData.maxFeePerGas;

const response = await web3Eth.sendTransaction(transaction);
expect(response.events).toBeUndefined();
expect(response.type).toBe(BigInt(2));
expect(response.status).toBe(BigInt(1));

const minedTransactionData = await web3Eth.getTransaction(response.transactionHash);
expect(minedTransactionData).toMatchObject(transaction);
});

it('should send a successful type 0x0 transaction with data', async () => {
const transaction: Transaction = {
from: tempAcc.address,
Expand Down
84 changes: 84 additions & 0 deletions packages/web3-eth/test/unit/web3_eth_calculate_fee_data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
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 { ethRpcMethods } from 'web3-rpc-methods';

import Web3Eth from '../../src/index';

jest.mock('web3-rpc-methods');

describe('Web3Eth.calculateFeeData', () => {
let web3Eth: Web3Eth;

beforeAll(() => {
web3Eth = new Web3Eth('http://127.0.0.1:8545');
});

it('should return call getBlockByNumber, getGasPrice and getMaxPriorityFeePerGas', async () => {
await web3Eth.calculateFeeData();
// web3Eth.getBlock = jest.fn();
expect(ethRpcMethods.getBlockByNumber).toHaveBeenCalledWith(
web3Eth.requestManager,
'latest',
false,
);
expect(ethRpcMethods.getGasPrice).toHaveBeenCalledWith(web3Eth.requestManager);
expect(ethRpcMethods.getMaxPriorityFeePerGas).toHaveBeenCalledWith(web3Eth.requestManager);
});

it('should calculate fee data', async () => {
const gasPrice = BigInt(20 * 1000);
const baseFeePerGas = BigInt(1000);
const maxPriorityFeePerGas = BigInt(100);
const baseFeePerGasFactor = BigInt(3);

jest.spyOn(ethRpcMethods, 'getBlockByNumber').mockReturnValueOnce({ baseFeePerGas } as any);
jest.spyOn(ethRpcMethods, 'getGasPrice').mockReturnValueOnce(gasPrice as any);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
jest
.spyOn(ethRpcMethods, 'getMaxPriorityFeePerGas')
.mockReturnValueOnce(maxPriorityFeePerGas as any);

const feeData = await web3Eth.calculateFeeData(baseFeePerGasFactor, maxPriorityFeePerGas);
expect(feeData).toMatchObject({
gasPrice,
maxFeePerGas: baseFeePerGas * baseFeePerGasFactor + maxPriorityFeePerGas,
maxPriorityFeePerGas,
baseFeePerGas,
});
});

it('should calculate fee data based on `alternativeMaxPriorityFeePerGas` if `getMaxPriorityFeePerGas` did not return a value', async () => {
const gasPrice = BigInt(20 * 1000);
const baseFeePerGas = BigInt(1000);
const alternativeMaxPriorityFeePerGas = BigInt(700);
const baseFeePerGasFactor = BigInt(3);

jest.spyOn(ethRpcMethods, 'getBlockByNumber').mockReturnValueOnce({ baseFeePerGas } as any);
jest.spyOn(ethRpcMethods, 'getGasPrice').mockReturnValueOnce(gasPrice as any);
const feeData = await web3Eth.calculateFeeData(
baseFeePerGasFactor,
alternativeMaxPriorityFeePerGas,
);
expect(feeData).toMatchObject({
gasPrice,
maxFeePerGas: baseFeePerGas * baseFeePerGasFactor + alternativeMaxPriorityFeePerGas,
maxPriorityFeePerGas: alternativeMaxPriorityFeePerGas,
baseFeePerGas,
});
});
});
6 changes: 5 additions & 1 deletion packages/web3-types/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,4 +183,8 @@ Documentation:

- Adds missing exported type `AbiItem` from 1.x to v4 for compatabiltiy (#6678)

## [Unreleased]
## [Unreleased]

### Added

- Type `FeeData` to be filled by `await web3.eth.calculateFeeData()` to be used with EIP-1559 transactions (#6795)
45 changes: 45 additions & 0 deletions packages/web3-types/src/eth_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -529,3 +529,48 @@ export interface Eip712TypedData {
readonly domain: Record<string, string | number>;
readonly message: Record<string, unknown>;
}

/**
* To contain the gas Fee Data to be used with EIP-1559 transactions.
* EIP-1559 was applied to Ethereum after London hardfork.
*
* Typically you will only need `maxFeePerGas` and `maxPriorityFeePerGas` for a transaction following EIP-1559.
* However, if you want to get informed about the fees of last block, you can use `baseFeePerGas` too.
*
*
* @see https://eips.ethereum.org/EIPS/eip-1559
*
*/
export interface FeeData {
Muhammad-Altabba marked this conversation as resolved.
Show resolved Hide resolved
/**
* This filed is used for legacy networks that does not support EIP-1559.
*/
readonly gasPrice?: Numbers;

/**
* The baseFeePerGas returned from the the last available block.
*
* If EIP-1559 is not supported, this will be `undefined`
*
* However, the user will only pay (the future baseFeePerGas + the maxPriorityFeePerGas).
* And this value is just for getting informed about the fees of last block.
*/
readonly baseFeePerGas?: Numbers;

/**
* The maximum fee that the user would be willing to pay per-gas.
*
* However, the user will only pay (the future baseFeePerGas + the maxPriorityFeePerGas).
* And the `maxFeePerGas` could be used to prevent paying more than it, if `baseFeePerGas` went too high.
*
* If EIP-1559 is not supported, this will be `undefined`
*/
readonly maxFeePerGas?: Numbers;

/**
* The validator's tip for including a transaction in a block.
*
* If EIP-1559 is not supported, this will be `undefined`
*/
readonly maxPriorityFeePerGas?: Numbers;
}
Loading