Skip to content

Commit

Permalink
feat: Implement eth.calculateFeeData (#6795)
Browse files Browse the repository at this point in the history
* implement and test `calculateFeeData`

* add integration test for `calculateFeeData`

* update CHANGELOG.md file
  • Loading branch information
Muhammad-Altabba committed Feb 16, 2024
1 parent f696e47 commit ec65468
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 11 deletions.
99 changes: 89 additions & 10 deletions packages/web3-eth/src/web3_eth.ts
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(
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
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
@@ -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
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
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 {
/**
* 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;
}

1 comment on commit ec65468

@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: ec65468 Previous: 6c075db Ratio
processingTx 9390 ops/sec (±4.95%) 9301 ops/sec (±4.81%) 0.99
processingContractDeploy 38448 ops/sec (±7.17%) 39129 ops/sec (±7.62%) 1.02
processingContractMethodSend 18987 ops/sec (±7.36%) 19443 ops/sec (±5.19%) 1.02
processingContractMethodCall 38741 ops/sec (±5.85%) 38971 ops/sec (±6.34%) 1.01
abiEncode 43859 ops/sec (±6.79%) 44252 ops/sec (±6.92%) 1.01
abiDecode 29305 ops/sec (±7.57%) 30419 ops/sec (±8.89%) 1.04
sign 1611 ops/sec (±1.06%) 1656 ops/sec (±4.08%) 1.03
verify 370 ops/sec (±0.68%) 373 ops/sec (±0.78%) 1.01

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

Please sign in to comment.