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 { 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<FeeData> {
|
||||
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<MaxPriorityFeePlugin>("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 = <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 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":
|
||||
|
@ -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<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();
|
||||
|
||||
// @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<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
|
||||
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, { });
|
||||
}
|
||||
|
@ -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<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 {
|
||||
readonly #blockFunc: (provider: Provider, block: BlockParams<string>) => Block<string>;
|
||||
|
Loading…
Reference in New Issue
Block a user