UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

307 lines (249 loc) 9.13 kB
import { EIP1193Provider, RequestArguments } from "../../../types"; import { numberToRpcQuantity, rpcQuantityToNumber, rpcQuantityToBigInt, } from "../jsonrpc/types/base-types"; import { ProviderWrapper } from "./wrapper"; const DEFAULT_GAS_MULTIPLIER = 1; export class FixedGasProvider extends ProviderWrapper { constructor(provider: EIP1193Provider, private readonly _gasLimit: number) { super(provider); } public async request(args: RequestArguments): Promise<unknown> { if (args.method === "eth_sendTransaction") { const params = this._getParams(args); // TODO: Should we validate this type? const tx = params[0]; if (tx !== undefined && tx.gas === undefined) { tx.gas = numberToRpcQuantity(this._gasLimit); } } return this._wrappedProvider.request(args); } } export class FixedGasPriceProvider extends ProviderWrapper { constructor(provider: EIP1193Provider, private readonly _gasPrice: number) { super(provider); } public async request(args: RequestArguments): Promise<unknown> { if (args.method === "eth_sendTransaction") { const params = this._getParams(args); // TODO: Should we validate this type? const tx = params[0]; // temporary change to ignore EIP-1559 if ( tx !== undefined && tx.gasPrice === undefined && tx.maxFeePerGas === undefined && tx.maxPriorityFeePerGas === undefined ) { tx.gasPrice = numberToRpcQuantity(this._gasPrice); } } return this._wrappedProvider.request(args); } } abstract class MultipliedGasEstimationProvider extends ProviderWrapper { private _blockGasLimit: number | undefined; constructor( provider: EIP1193Provider, private readonly _gasMultiplier: number ) { super(provider); } protected async _getMultipliedGasEstimation(params: any[]): Promise<string> { try { const realEstimation = (await this._wrappedProvider.request({ method: "eth_estimateGas", params, })) as string; if (this._gasMultiplier === 1) { return realEstimation; } const normalGas = rpcQuantityToNumber(realEstimation); const gasLimit = await this._getBlockGasLimit(); const multiplied = Math.floor(normalGas * this._gasMultiplier); const gas = multiplied > gasLimit ? gasLimit - 1 : multiplied; return numberToRpcQuantity(gas); } catch (error) { if (error instanceof Error) { if (error.message.toLowerCase().includes("execution error")) { const blockGasLimit = await this._getBlockGasLimit(); return numberToRpcQuantity(blockGasLimit); } } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw error; } } private async _getBlockGasLimit(): Promise<number> { if (this._blockGasLimit === undefined) { const latestBlock = (await this._wrappedProvider.request({ method: "eth_getBlockByNumber", params: ["latest", false], })) as { gasLimit: string }; const fetchedGasLimit = rpcQuantityToNumber(latestBlock.gasLimit); // We store a lower value in case the gas limit varies slightly this._blockGasLimit = Math.floor(fetchedGasLimit * 0.95); } return this._blockGasLimit; } } export class AutomaticGasProvider extends MultipliedGasEstimationProvider { constructor( provider: EIP1193Provider, gasMultiplier: number = DEFAULT_GAS_MULTIPLIER ) { super(provider, gasMultiplier); } public async request(args: RequestArguments): Promise<unknown> { if (args.method === "eth_sendTransaction") { const params = this._getParams(args); // TODO: Should we validate this type? const tx = params[0]; if (tx !== undefined && tx.gas === undefined) { tx.gas = await this._getMultipliedGasEstimation(params); } } return this._wrappedProvider.request(args); } } export class AutomaticGasPriceProvider extends ProviderWrapper { // 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; private _nodeHasFeeHistory?: boolean; private _nodeSupportsEIP1559?: boolean; public async request(args: RequestArguments): Promise<unknown> { if (args.method !== "eth_sendTransaction") { return this._wrappedProvider.request(args); } const params = this._getParams(args); // TODO: Should we validate this type? const tx = params[0]; if (tx === undefined) { return this._wrappedProvider.request(args); } // We don't need to do anything in these cases if ( tx.gasPrice !== undefined || (tx.maxFeePerGas !== undefined && tx.maxPriorityFeePerGas !== undefined) ) { return this._wrappedProvider.request(args); } 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 = numberToRpcQuantity(await this._getGasPrice()); return this._wrappedProvider.request(args); } // 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 = tx.maxFeePerGas !== undefined ? rpcQuantityToBigInt(tx.maxFeePerGas) : suggestedEip1559Values.maxFeePerGas; const maxPriorityFeePerGas = tx.maxPriorityFeePerGas !== undefined ? rpcQuantityToBigInt(tx.maxPriorityFeePerGas) : suggestedEip1559Values.maxPriorityFeePerGas; if (maxFeePerGas < maxPriorityFeePerGas) { maxFeePerGas += maxPriorityFeePerGas; } tx.maxFeePerGas = numberToRpcQuantity(maxFeePerGas); tx.maxPriorityFeePerGas = numberToRpcQuantity(maxPriorityFeePerGas); return this._wrappedProvider.request(args); } private async _getGasPrice(): Promise<bigint> { const response = (await this._wrappedProvider.request({ method: "eth_gasPrice", })) as string; return rpcQuantityToBigInt(response); } private async _suggestEip1559FeePriceValues(): Promise< | { maxFeePerGas: bigint; maxPriorityFeePerGas: bigint; } | undefined > { if (this._nodeSupportsEIP1559 === undefined) { const block = (await this._wrappedProvider.request({ method: "eth_getBlockByNumber", params: ["latest", false], })) as any; this._nodeSupportsEIP1559 = block.baseFeePerGas !== undefined; } if ( this._nodeHasFeeHistory === false || this._nodeSupportsEIP1559 === false ) { return; } try { const response = (await this._wrappedProvider.request({ method: "eth_feeHistory", params: [ "0x1", "latest", [AutomaticGasPriceProvider.EIP1559_REWARD_PERCENTILE], ], })) as { baseFeePerGas: string[]; reward: string[][] }; let maxPriorityFeePerGas = rpcQuantityToBigInt(response.reward[0][0]); if (maxPriorityFeePerGas === 0n) { try { const suggestedMaxPriorityFeePerGas = (await this._wrappedProvider.request({ method: "eth_maxPriorityFeePerGas", params: [], })) as string; maxPriorityFeePerGas = rpcQuantityToBigInt( 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: (rpcQuantityToBigInt(response.baseFeePerGas[1]) * 9n ** (AutomaticGasPriceProvider.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE - 1n)) / 8n ** (AutomaticGasPriceProvider.EIP1559_BASE_FEE_MAX_FULL_BLOCKS_PREFERENCE - 1n), maxPriorityFeePerGas, }; } catch { this._nodeHasFeeHistory = false; return undefined; } } }