Added basic Gas Station support via a NetworkPlugin (#2828).

This commit is contained in:
Richard Moore 2023-07-27 18:04:46 -04:00
parent ec39abe067
commit 229145ddf5
3 changed files with 130 additions and 110 deletions

@ -44,6 +44,7 @@ import type { BigNumberish, BytesLike } from "../utils/index.js";
import type { Listener } from "../utils/index.js"; import type { Listener } from "../utils/index.js";
import type { Networkish } from "./network.js"; import type { Networkish } from "./network.js";
import type { FetchUrlFeeDataNetworkPlugin } from "./plugins-network.js";
//import type { MaxPriorityFeePlugin } from "./plugins-network.js"; //import type { MaxPriorityFeePlugin } from "./plugins-network.js";
import type { import type {
BlockParams, LogParams, TransactionReceiptParams, BlockParams, LogParams, TransactionReceiptParams,
@ -410,10 +411,12 @@ type _PerformAccountRequest = {
*/ */
export type AbstractProviderOptions = { export type AbstractProviderOptions = {
cacheTimeout?: number; cacheTimeout?: number;
pollingInterval?: number;
}; };
const defaultOptions = { const defaultOptions = {
cacheTimeout: 250 cacheTimeout: 250,
pollingInterval: 4000
}; };
type CcipArgs = { type CcipArgs = {
@ -493,6 +496,8 @@ export class AbstractProvider implements Provider {
this.#disableCcipRead = false; this.#disableCcipRead = false;
} }
get pollingInterval(): number { return this.#options.pollingInterval; }
/** /**
* Returns ``this``, to allow an **AbstractProvider** to implement * Returns ``this``, to allow an **AbstractProvider** to implement
* the [[ContractRunner]] interface. * the [[ContractRunner]] interface.
@ -888,8 +893,11 @@ export class AbstractProvider implements Provider {
} }
async getFeeData(): Promise<FeeData> { async getFeeData(): Promise<FeeData> {
const { block, gasPrice } = await resolveProperties({ const network = await this.getNetwork();
block: this.getBlock("latest"),
const getFeeDataFunc = async () => {
const { _block, gasPrice } = await resolveProperties({
_block: this.#getBlock("latest", false),
gasPrice: ((async () => { gasPrice: ((async () => {
try { try {
const gasPrice = await this.#perform({ method: "getGasPrice" }); const gasPrice = await this.#perform({ method: "getGasPrice" });
@ -901,21 +909,25 @@ export class AbstractProvider implements Provider {
let maxFeePerGas = null, maxPriorityFeePerGas = null; let maxFeePerGas = null, maxPriorityFeePerGas = null;
// These are the recommended EIP-1559 heuristics for fee data
const block = this._wrapBlock(_block, network);
if (block && block.baseFeePerGas) { 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"); maxPriorityFeePerGas = BigInt("1000000000");
// Allow a network to override their maximum priority fee per gas
//const priorityFeePlugin = (await this.getNetwork()).getPlugin<MaxPriorityFeePlugin>("org.ethers.plugins.max-priority-fee");
//if (priorityFeePlugin) {
// maxPriorityFeePerGas = await priorityFeePlugin.getPriorityFee(this);
//}
maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas; maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas;
} }
return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas); return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas);
};
// Check for a FeeDataNetWorkPlugin
const plugin = <FetchUrlFeeDataNetworkPlugin>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 await getFeeDataFunc();
} }
@ -1301,8 +1313,11 @@ export class AbstractProvider implements Provider {
case "error": case "error":
case "network": case "network":
return new UnmanagedSubscriber(sub.type); return new UnmanagedSubscriber(sub.type);
case "block": case "block": {
return new PollingBlockSubscriber(this); const subscriber = new PollingBlockSubscriber(this);
subscriber.pollingInterval = this.pollingInterval;
return subscriber;
}
case "event": case "event":
return new PollingEventSubscriber(this, sub.filter); return new PollingEventSubscriber(this, sub.filter);
case "transaction": case "transaction":

@ -6,10 +6,11 @@
*/ */
import { accessListify } from "../transaction/index.js"; 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 {
//import { EtherscanPlugin } from "./provider-etherscan-base.js"; EnsPlugin, FetchUrlFeeDataNetworkPlugin, GasCostPlugin
} from "./plugins-network.js";
import type { BigNumberish } from "../utils/index.js"; import type { BigNumberish } from "../utils/index.js";
import type { TransactionLike } from "../transaction/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<PriceOraclePlugin>(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<FetchRequest>) => Promise<FetchRequest>;
export class CcipPreflightPlugin extends NetworkPlugin {
readonly fetchData!: FetchDataFunc;
constructor(fetchData: FetchDataFunc) {
super("org.ethers.plugins.ccip-preflight");
defineProperties<CcipPreflightPlugin>(this, { fetchData });
}
clone(): CcipPreflightPlugin {
return new CcipPreflightPlugin(this.fetchData);
}
}
*/
const Networks: Map<string | bigint, () => Network> = new Map(); const Networks: Map<string | bigint, () => Network> = new Map();
// @TODO: Add a _ethersNetworkObj variable to better detect network ovjects
/** /**
* A **Network** provides access to a chain's properties and allows * A **Network** provides access to a chain's properties and allows
@ -318,11 +284,61 @@ export class Network {
type Options = { type Options = {
ensNetwork?: number; ensNetwork?: number;
priorityFee?: number
altNames?: Array<string>; altNames?: Array<string>;
etherscan?: { url: string }; plugins?: Array<NetworkPlugin>;
}; };
// 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 // See: https://chainlist.org
let injected = false; let injected = false;
function injectCommonNetworks(): void { function injectCommonNetworks(): void {
@ -339,17 +355,12 @@ function injectCommonNetworks(): void {
network.attachPlugin(new EnsPlugin(null, options.ensNetwork)); 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()); network.attachPlugin(new GasCostPlugin());
(options.plugins || []).forEach((plugin) => {
network.attachPlugin(plugin);
});
return network; return network;
}; };
@ -378,49 +389,28 @@ function injectCommonNetworks(): void {
registerEth("optimism", 10, { registerEth("optimism", 10, {
ensNetwork: 1, 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, { registerEth("arbitrum", 42161, {
ensNetwork: 1, 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 // Polygon has a 35 gwei maxPriorityFee requirement
registerEth("matic", 137, { registerEth("matic", 137, {
ensNetwork: 1, ensNetwork: 1,
// priorityFee: 35000000000, plugins: [
etherscan: { getGasStationPlugin("https:/\/gasstation.polygon.technology/v2")
// apiKey: "W6T8DJW654GNTQ34EFEYYP3EZD9DD27CT7", ]
url: "https:/\/api.polygonscan.com/"
}
}); });
registerEth("matic-mumbai", 80001, { registerEth("matic-mumbai", 80001, {
altNames: [ "maticMumbai", "maticmum" ], // @TODO: Future remove these alts altNames: [ "maticMumbai", "maticmum" ], // @TODO: Future remove these alts
// priorityFee: 35000000000, plugins: [
etherscan: { getGasStationPlugin("https:/\/gasstation-testnet.polygon.technology/v2")
// apiKey: "W6T8DJW654GNTQ34EFEYYP3EZD9DD27CT7", ]
url: "https:/\/api-testnet.polygonscan.com/"
}
}); });
registerEth("bnb", 56, { registerEth("bnb", 56, { ensNetwork: 1 });
ensNetwork: 1, registerEth("bnbt", 97, { });
etherscan: {
// apiKey: "EVTS3CU31AATZV72YQ55TPGXGMVIFUQ9M9",
url: "http:/\/api.bscscan.com"
}
});
registerEth("bnbt", 97, {
etherscan: {
// apiKey: "EVTS3CU31AATZV72YQ55TPGXGMVIFUQ9M9",
url: "http:/\/api-testnet.bscscan.com"
}
});
} }

@ -2,10 +2,8 @@ import { defineProperties } from "../utils/properties.js";
import { assertArgument } from "../utils/index.js"; import { assertArgument } from "../utils/index.js";
import type { import type { FeeData, Provider } from "./provider.js";
FeeData, Provider import type { FetchRequest } from "../utils/fetch.js";
} from "./provider.js";
const EnsAddress = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; const EnsAddress = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e";
@ -229,6 +227,23 @@ export class FeeDataNetworkPlugin extends NetworkPlugin {
} }
} }
export class FetchUrlFeeDataNetworkPlugin extends NetworkPlugin {
readonly #url: string;
readonly #processFunc: (f: () => Promise<FeeData>, 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<FeeData>, 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 { export class CustomBlockNetworkPlugin extends NetworkPlugin {
readonly #blockFunc: (provider: Provider, block: BlockParams<string>) => Block<string>; readonly #blockFunc: (provider: Provider, block: BlockParams<string>) => Block<string>;