Skip to content

Commit

Permalink
Added basic Gas Station support via a NetworkPlugin (ethers-io#2828).
Browse files Browse the repository at this point in the history
  • Loading branch information
ricmoo authored and Woodpile37 committed Jan 14, 2024
1 parent 2063e21 commit 2f807f1
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 110 deletions.
67 changes: 41 additions & 26 deletions src.ts/providers/abstract-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -410,10 +411,12 @@ type _PerformAccountRequest = {
*/
export type AbstractProviderOptions = {
cacheTimeout?: number;
pollingInterval?: number;
};

const defaultOptions = {
cacheTimeout: 250
cacheTimeout: 250,
pollingInterval: 4000
};

type CcipArgs = {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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();

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
})())
});

let maxFeePerGas = null, maxPriorityFeePerGas = null;
let maxFeePerGas = null, maxPriorityFeePerGas = 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");
// 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);
};

// 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;
// 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();
}


Expand Down Expand Up @@ -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":
Expand Down
150 changes: 70 additions & 80 deletions src.ts/providers/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
};

Expand Down Expand Up @@ -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, { });
}
23 changes: 19 additions & 4 deletions src.ts/providers/plugins-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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>;
Expand Down

0 comments on commit 2f807f1

Please sign in to comment.