Added basic Gas Station support via a NetworkPlugin (#2828).
This commit is contained in:
parent
ec39abe067
commit
229145ddf5
@ -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>;
|
||||||
|
Loading…
Reference in New Issue
Block a user