hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
228 lines (187 loc) • 6.67 kB
text/typescript
import type {
EthereumProvider,
JsonRpcRequest,
JsonRpcResponse,
} from "../../../../../../types/providers.js";
import type { RequestHandler } from "../../types.js";
import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
import {
hexStringToBigInt,
numberToHexString,
} from "@nomicfoundation/hardhat-utils/hex";
import { isObject } from "@nomicfoundation/hardhat-utils/lang";
import { getRequestParams } from "../../../json-rpc.js";
/**
* This class automatically adjusts transaction requests to include appropriately estimated gas prices.
* It ensures that gas prices are set correctly.
*/
export class AutomaticGasPriceHandler implements RequestHandler {
readonly #methods: ReadonlySet<string> = new Set(["eth_sendTransaction"]);
readonly #provider: EthereumProvider;
// We pay the max base fee that can be required if the next
// EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE are full.
public static readonly EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE: bigint =
3n;
// See eth_feeHistory for an explanation of what this means
public static readonly EIP1559_REWARD_PERCENTILE = 50;
constructor(provider: EthereumProvider) {
this.#provider = provider;
}
#nodeHasFeeHistory?: boolean;
#nodeSupportsEIP1559?: boolean;
public isSupportedMethod(jsonRpcRequest: JsonRpcRequest): boolean {
return this.#methods.has(jsonRpcRequest.method);
}
public async handle(
jsonRpcRequest: JsonRpcRequest,
): Promise<JsonRpcRequest | JsonRpcResponse> {
if (!this.isSupportedMethod(jsonRpcRequest)) {
return jsonRpcRequest;
}
const params = getRequestParams(jsonRpcRequest);
const [tx] = params;
if (!isObject(tx)) {
return jsonRpcRequest;
}
// We don't need to do anything in these cases
if (
tx.gasPrice !== undefined ||
(tx.maxFeePerGas !== undefined && tx.maxPriorityFeePerGas !== undefined)
) {
return jsonRpcRequest;
}
let suggestedEip1559Values = await this.#suggestEip1559FeePriceValues();
// eth_feeHistory failed, so we send a legacy one
if (
tx.maxFeePerGas === undefined &&
tx.maxPriorityFeePerGas === undefined &&
suggestedEip1559Values === undefined
) {
tx.gasPrice = numberToHexString(await this.#getGasPrice());
return jsonRpcRequest;
}
// If eth_feeHistory failed, but the user still wants to send an EIP-1559 tx
// we use the gasPrice as default values.
if (suggestedEip1559Values === undefined) {
const gasPrice = await this.#getGasPrice();
suggestedEip1559Values = {
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: gasPrice,
};
}
let maxFeePerGas =
typeof tx.maxFeePerGas === "string"
? hexStringToBigInt(tx.maxFeePerGas)
: suggestedEip1559Values.maxFeePerGas;
const maxPriorityFeePerGas =
typeof tx.maxPriorityFeePerGas === "string"
? hexStringToBigInt(tx.maxPriorityFeePerGas)
: suggestedEip1559Values.maxPriorityFeePerGas;
if (maxFeePerGas < maxPriorityFeePerGas) {
maxFeePerGas += maxPriorityFeePerGas;
}
tx.maxFeePerGas = numberToHexString(maxFeePerGas);
tx.maxPriorityFeePerGas = numberToHexString(maxPriorityFeePerGas);
return jsonRpcRequest;
}
async #getGasPrice(): Promise<bigint> {
const response = await this.#provider.request({
method: "eth_gasPrice",
});
assertHardhatInvariant(
typeof response === "string",
"response should return a string",
);
return hexStringToBigInt(response);
}
async #suggestEip1559FeePriceValues(): Promise<
| {
maxFeePerGas: bigint;
maxPriorityFeePerGas: bigint;
}
| undefined
> {
if (this.#nodeSupportsEIP1559 === undefined) {
const block = await this.#provider.request({
method: "eth_getBlockByNumber",
params: ["latest", false],
});
assertHardhatInvariant(
isObject(block),
"block should be a non null object",
);
this.#nodeSupportsEIP1559 =
"baseFeePerGas" in block && block.baseFeePerGas !== undefined;
}
if (
this.#nodeHasFeeHistory === false ||
this.#nodeSupportsEIP1559 === false
) {
return;
}
try {
const response = await this.#provider.request({
method: "eth_feeHistory",
params: [
"0x1",
"latest",
[AutomaticGasPriceHandler.EIP1559_REWARD_PERCENTILE],
],
});
assertHardhatInvariant(
typeof response === "object" &&
response !== null &&
"baseFeePerGas" in response &&
"reward" in response &&
Array.isArray(response.baseFeePerGas) &&
Array.isArray(response.reward),
"response should be an object with baseFeePerGas and reward properties",
);
let maxPriorityFeePerGas = hexStringToBigInt(response.reward[0][0]);
if (maxPriorityFeePerGas === 0n) {
try {
const suggestedMaxPriorityFeePerGas = await this.#provider.request({
method: "eth_maxPriorityFeePerGas",
params: [],
});
assertHardhatInvariant(
typeof suggestedMaxPriorityFeePerGas === "string",
"suggestedMaxPriorityFeePerGas should be a string",
);
maxPriorityFeePerGas = hexStringToBigInt(
suggestedMaxPriorityFeePerGas,
);
} catch {
// if eth_maxPriorityFeePerGas does not exist, use 1 wei
maxPriorityFeePerGas = 1n;
}
}
// If after all of these we still have a 0 wei maxPriorityFeePerGas, we
// use 1 wei. This is to improve the UX of the automatic gas price
// on chains that are very empty (i.e local testnets). This will be very
// unlikely to trigger on a live chain.
if (maxPriorityFeePerGas === 0n) {
maxPriorityFeePerGas = 1n;
}
return {
// Each block increases the base fee by 1/8 at most, when full.
// We have the next block's base fee, so we compute a cap for the
// next N blocks here.
maxFeePerGas:
(hexStringToBigInt(
response.baseFeePerGas[response.baseFeePerGas.length - 1],
) *
9n **
(AutomaticGasPriceHandler.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE -
1n)) /
8n **
(AutomaticGasPriceHandler.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE -
1n),
maxPriorityFeePerGas,
};
} catch {
this.#nodeHasFeeHistory = false;
return undefined;
}
}
}