diff --git a/src.ts/providers/abstract-provider.ts b/src.ts/providers/abstract-provider.ts index 36df994ab..2e6396775 100644 --- a/src.ts/providers/abstract-provider.ts +++ b/src.ts/providers/abstract-provider.ts @@ -44,6 +44,7 @@ import type { BigNumberish, BytesLike } from "../utils/index.js"; import type { Listener } from "../utils/index.js"; import type { Networkish } from "./network.js"; +import type { FetchUrlFeeDataNetworkPlugin } from "./plugins-network.js"; //import type { MaxPriorityFeePlugin } from "./plugins-network.js"; import type { BlockParams, LogParams, TransactionReceiptParams, @@ -410,10 +411,12 @@ type _PerformAccountRequest = { */ export type AbstractProviderOptions = { cacheTimeout?: number; + pollingInterval?: number; }; const defaultOptions = { - cacheTimeout: 250 + cacheTimeout: 250, + pollingInterval: 4000 }; type CcipArgs = { @@ -493,6 +496,8 @@ export class AbstractProvider implements Provider { this.#disableCcipRead = false; } + get pollingInterval(): number { return this.#options.pollingInterval; } + /** * Returns ``this``, to allow an **AbstractProvider** to implement * the [[ContractRunner]] interface. @@ -888,34 +893,41 @@ export class AbstractProvider implements Provider { } async getFeeData(): Promise { - const { block, gasPrice } = await resolveProperties({ - block: this.getBlock("latest"), - gasPrice: ((async () => { - try { - const gasPrice = await this.#perform({ method: "getGasPrice" }); - return getBigInt(gasPrice, "%response"); - } catch (error) { } - return null - })()) - }); + const network = await this.getNetwork(); - let maxFeePerGas = null, maxPriorityFeePerGas = null; + const getFeeDataFunc = async () => { + const { _block, gasPrice } = await resolveProperties({ + _block: this.#getBlock("latest", false), + gasPrice: ((async () => { + try { + const gasPrice = await this.#perform({ method: "getGasPrice" }); + return getBigInt(gasPrice, "%response"); + } catch (error) { } + return null + })()) + }); - if (block && block.baseFeePerGas) { - // We may want to compute this more accurately in the future, - // using the formula "check if the base fee is correct". - // See: https://eips.ethereum.org/EIPS/eip-1559 - maxPriorityFeePerGas = BigInt("1000000000"); + let maxFeePerGas = null, maxPriorityFeePerGas = null; - // Allow a network to override their maximum priority fee per gas - //const priorityFeePlugin = (await this.getNetwork()).getPlugin("org.ethers.plugins.max-priority-fee"); - //if (priorityFeePlugin) { - // maxPriorityFeePerGas = await priorityFeePlugin.getPriorityFee(this); - //} - maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas; + // These are the recommended EIP-1559 heuristics for fee data + const block = this._wrapBlock(_block, network); + if (block && block.baseFeePerGas) { + maxPriorityFeePerGas = BigInt("1000000000"); + maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas; + } + + return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas); + }; + + // Check for a FeeDataNetWorkPlugin + const plugin = network.getPlugin("org.ethers.plugins.network.FetchUrlFeeDataPlugin"); + if (plugin) { + const req = new FetchRequest(plugin.url); + const feeData = await plugin.processFunc(getFeeDataFunc, this, req); + return new FeeData(feeData.gasPrice, feeData.maxFeePerGas, feeData.maxPriorityFeePerGas); } - return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas); + return await getFeeDataFunc(); } @@ -1301,8 +1313,11 @@ export class AbstractProvider implements Provider { case "error": case "network": return new UnmanagedSubscriber(sub.type); - case "block": - return new PollingBlockSubscriber(this); + case "block": { + const subscriber = new PollingBlockSubscriber(this); + subscriber.pollingInterval = this.pollingInterval; + return subscriber; + } case "event": return new PollingEventSubscriber(this, sub.filter); case "transaction": diff --git a/src.ts/providers/network.ts b/src.ts/providers/network.ts index 552d5eeea..1a5cc0b98 100644 --- a/src.ts/providers/network.ts +++ b/src.ts/providers/network.ts @@ -6,10 +6,11 @@ */ import { accessListify } from "../transaction/index.js"; -import { getBigInt, assertArgument } from "../utils/index.js"; +import { getBigInt, assert, assertArgument } from "../utils/index.js"; -import { EnsPlugin, GasCostPlugin } from "./plugins-network.js"; -//import { EtherscanPlugin } from "./provider-etherscan-base.js"; +import { + EnsPlugin, FetchUrlFeeDataNetworkPlugin, GasCostPlugin +} from "./plugins-network.js"; import type { BigNumberish } from "../utils/index.js"; import type { TransactionLike } from "../transaction/index.js"; @@ -53,44 +54,9 @@ export class LayerOneConnectionPlugin extends NetworkPlugin { } */ -/* * * * -export class PriceOraclePlugin extends NetworkPlugin { - readonly address!: string; - - constructor(address: string) { - super("org.ethers.plugins.price-oracle"); - defineProperties(this, { address }); - } - - clone(): PriceOraclePlugin { - return new PriceOraclePlugin(this.address); - } -} -*/ - -// Networks or clients with a higher need for security (such as clients -// that may automatically make CCIP requests without user interaction) -// can use this plugin to anonymize requests or intercept CCIP requests -// to notify and/or receive authorization from the user -/* * * * -export type FetchDataFunc = (req: Frozen) => Promise; -export class CcipPreflightPlugin extends NetworkPlugin { - readonly fetchData!: FetchDataFunc; - - constructor(fetchData: FetchDataFunc) { - super("org.ethers.plugins.ccip-preflight"); - defineProperties(this, { fetchData }); - } - - clone(): CcipPreflightPlugin { - return new CcipPreflightPlugin(this.fetchData); - } -} -*/ const Networks: Map Network> = new Map(); -// @TODO: Add a _ethersNetworkObj variable to better detect network ovjects /** * A **Network** provides access to a chain's properties and allows @@ -318,11 +284,61 @@ export class Network { type Options = { ensNetwork?: number; - priorityFee?: number altNames?: Array; - etherscan?: { url: string }; + plugins?: Array; }; +// We don't want to bring in formatUnits because it is backed by +// FixedNumber and we want to keep Networks tiny. The values +// included by the Gas Stations are also IEEE 754 with lots of +// rounding issues and exceed the strict checks formatUnits has. +function parseUnits(_value: number | string, decimals: number): bigint { + const value = String(_value); + if (!value.match(/^[0-9.]+$/)) { + throw new Error(`invalid gwei value: ${ _value }`); + } + + // Break into [ whole, fraction ] + const comps = value.split("."); + if (comps.length === 1) { comps.push(""); } + + // More than 1 decimal point or too many fractional positions + if (comps.length !== 2) { + throw new Error(`invalid gwei value: ${ _value }`); + } + + // Pad the fraction to 9 decimalplaces + while (comps[1].length < decimals) { comps[1] += "0"; } + + // Too many decimals and some non-zero ending, take the ceiling + if (comps[1].length > 9 && !comps[1].substring(9).match(/^0+$/)) { + comps[1] = (BigInt(comps[1].substring(0, 9)) + BigInt(1)).toString(); + } + + return BigInt(comps[0] + comps[1]); +} + +function getGasStationPlugin(url: string) { + return new FetchUrlFeeDataNetworkPlugin(url, async (fetchFeeData, provider, request) => { + + // Prevent Cloudflare from blocking our request in node.js + request.setHeader("User-Agent", "ethers"); + + let response; + try { + response = await request.send(); + const payload = response.bodyJson.standard; + const feeData = { + maxFeePerGas: parseUnits(payload.maxFee, 9), + maxPriorityFeePerGas: parseUnits(payload.maxPriorityFee, 9), + }; + return feeData; + } catch (error) { + assert(false, `error encountered with polygon gas station (${ JSON.stringify(request.url) })`, "SERVER_ERROR", { request, response, info: { error } }); + } + }); +} + // See: https://chainlist.org let injected = false; function injectCommonNetworks(): void { @@ -339,17 +355,12 @@ function injectCommonNetworks(): void { network.attachPlugin(new EnsPlugin(null, options.ensNetwork)); } - if (options.priorityFee) { -// network.attachPlugin(new MaxPriorityFeePlugin(options.priorityFee)); - } -/* - if (options.etherscan) { - const { url, apiKey } = options.etherscan; - network.attachPlugin(new EtherscanPlugin(url, apiKey)); - } -*/ network.attachPlugin(new GasCostPlugin()); + (options.plugins || []).forEach((plugin) => { + network.attachPlugin(plugin); + }); + return network; }; @@ -378,49 +389,28 @@ function injectCommonNetworks(): void { registerEth("optimism", 10, { ensNetwork: 1, - etherscan: { url: "https:/\/api-optimistic.etherscan.io/" } - }); - registerEth("optimism-goerli", 420, { - etherscan: { url: "https:/\/api-goerli-optimistic.etherscan.io/" } }); + registerEth("optimism-goerli", 420, { }); registerEth("arbitrum", 42161, { ensNetwork: 1, - etherscan: { url: "https:/\/api.arbiscan.io/" } - }); - registerEth("arbitrum-goerli", 421613, { - etherscan: { url: "https:/\/api-goerli.arbiscan.io/" } }); + registerEth("arbitrum-goerli", 421613, { }); // Polygon has a 35 gwei maxPriorityFee requirement registerEth("matic", 137, { ensNetwork: 1, -// priorityFee: 35000000000, - etherscan: { -// apiKey: "W6T8DJW654GNTQ34EFEYYP3EZD9DD27CT7", - url: "https:/\/api.polygonscan.com/" - } + plugins: [ + getGasStationPlugin("https:/\/gasstation.polygon.technology/v2") + ] }); registerEth("matic-mumbai", 80001, { altNames: [ "maticMumbai", "maticmum" ], // @TODO: Future remove these alts -// priorityFee: 35000000000, - etherscan: { -// apiKey: "W6T8DJW654GNTQ34EFEYYP3EZD9DD27CT7", - url: "https:/\/api-testnet.polygonscan.com/" - } + plugins: [ + getGasStationPlugin("https:/\/gasstation-testnet.polygon.technology/v2") + ] }); - registerEth("bnb", 56, { - ensNetwork: 1, - etherscan: { -// apiKey: "EVTS3CU31AATZV72YQ55TPGXGMVIFUQ9M9", - url: "http:/\/api.bscscan.com" - } - }); - registerEth("bnbt", 97, { - etherscan: { -// apiKey: "EVTS3CU31AATZV72YQ55TPGXGMVIFUQ9M9", - url: "http:/\/api-testnet.bscscan.com" - } - }); + registerEth("bnb", 56, { ensNetwork: 1 }); + registerEth("bnbt", 97, { }); } diff --git a/src.ts/providers/plugins-network.ts b/src.ts/providers/plugins-network.ts index 8a42ec17b..6df83df91 100644 --- a/src.ts/providers/plugins-network.ts +++ b/src.ts/providers/plugins-network.ts @@ -2,10 +2,8 @@ import { defineProperties } from "../utils/properties.js"; import { assertArgument } from "../utils/index.js"; -import type { - FeeData, Provider -} from "./provider.js"; - +import type { FeeData, Provider } from "./provider.js"; +import type { FetchRequest } from "../utils/fetch.js"; const EnsAddress = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; @@ -229,6 +227,23 @@ export class FeeDataNetworkPlugin extends NetworkPlugin { } } +export class FetchUrlFeeDataNetworkPlugin extends NetworkPlugin { + readonly #url: string; + readonly #processFunc: (f: () => Promise, p: Provider, r: FetchRequest) => Promise<{ gasPrice?: null | bigint, maxFeePerGas?: null | bigint, maxPriorityFeePerGas?: null | bigint }>; + + get url() { return this.#url; } + get processFunc() { return this.#processFunc; } + + constructor(url: string, processFunc: (f: () => Promise, p: Provider, r: FetchRequest) => Promise<{ gasPrice?: null | bigint, maxFeePerGas?: null | bigint, maxPriorityFeePerGas?: null | bigint }>) { + super("org.ethers.plugins.network.FetchUrlFeeDataPlugin"); + this.#url = url; + this.#processFunc = processFunc; + } + + // We are immutable, so we can serve as our own clone + clone(): FetchUrlFeeDataNetworkPlugin { return this; } +} + /* export class CustomBlockNetworkPlugin extends NetworkPlugin { readonly #blockFunc: (provider: Provider, block: BlockParams) => Block;