Skip to content

Commit

Permalink
Support selective override to viem
Browse files Browse the repository at this point in the history
  • Loading branch information
pxrl committed Oct 31, 2024
1 parent 9f962f4 commit a278a95
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 62 deletions.
37 changes: 21 additions & 16 deletions e2e/oracle.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @note: This test is _not_ run automatically as part of git hooks or CI.
import dotenv from "dotenv";
import winston from "winston";
import { providers } from "ethers";
import { getGasPriceEstimate } from "../src/gasPriceOracle";
import { BigNumber, bnZero, parseUnits } from "../src/utils";
import { expect, makeCustomTransport } from "../test/utils";
Expand All @@ -17,10 +18,14 @@ const chainIds = [1, 10, 137, 324, 8453, 42161, 534352];

const customTransport = makeCustomTransport({ stdLastBaseFeePerGas, stdMaxPriorityFeePerGas });

const provider = new providers.StaticJsonRpcProvider("https://eth.llamarpc.com");

describe("Gas Price Oracle", function () {
it("Gas Price Retrieval", async function () {
for (const chainId of chainIds) {
const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(chainId, customTransport);
const chainKey = `NEW_GAS_PRICE_ORACLE_${chainId}`;
process.env[chainKey] = "true";
const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, chainId, customTransport);
dummyLogger.debug({
at: "Gas Price Oracle#Gas Price Retrieval",
message: `Retrieved gas price estimate for chain ID ${chainId}`,
Expand All @@ -31,22 +36,22 @@ describe("Gas Price Oracle", function () {
expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true;
expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true;

if (chainIds.includes(chainId)) {
if (chainId === 137) {
// The Polygon gas station isn't mocked, so just ensure that the fees have a valid relationship.
expect(maxFeePerGas.gt(0)).to.be.true;
expect(maxPriorityFeePerGas.gt(0)).to.be.true;
expect(maxPriorityFeePerGas.lt(maxFeePerGas)).to.be.true;
} else if (chainId === 42161) {
// Arbitrum priority fees are refunded, so drop the priority fee from estimates.
// Expect a 1.2x multiplier on the last base fee.
expect(maxFeePerGas.eq(stdLastBaseFeePerGas.mul("120").div("100").add(1))).to.be.true;
expect(maxPriorityFeePerGas.eq(1)).to.be.true;
} else {
expect(maxFeePerGas.gt(bnZero)).to.be.true;
expect(maxPriorityFeePerGas.gt(bnZero)).to.be.true;
}
if (chainId === 137) {
// The Polygon gas station isn't mocked, so just ensure that the fees have a valid relationship.
expect(maxFeePerGas.gt(0)).to.be.true;
expect(maxPriorityFeePerGas.gt(0)).to.be.true;
expect(maxPriorityFeePerGas.lt(maxFeePerGas)).to.be.true;
} else if (chainId === 42161) {
// Arbitrum priority fees are refunded, so drop the priority fee from estimates.
// Expect a 1.2x multiplier on the last base fee.
expect(maxFeePerGas.eq(stdLastBaseFeePerGas.mul("120").div("100").add(1))).to.be.true;
expect(maxPriorityFeePerGas.eq(1)).to.be.true;
} else {
expect(maxFeePerGas.gt(bnZero)).to.be.true;
expect(maxPriorityFeePerGas.gt(bnZero)).to.be.true;
}

delete process.env[chainKey];
}
});
});
13 changes: 13 additions & 0 deletions src/gasPriceOracle/adapters/arbitrum-viem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PublicClient } from "viem";
import { InternalGasPriceEstimate } from "../types";

const MAX_PRIORITY_FEE_PER_GAS = BigInt(1);

// Arbitrum Nitro implements EIP-1559 pricing, but the priority fee is always refunded to the caller.
// Swap it for 1 Wei to avoid inaccurate transaction cost estimates.
// Reference: https://developer.arbitrum.io/faqs/gas-faqs#q-priority
export async function eip1559(provider: PublicClient, _chainId: number): Promise<InternalGasPriceEstimate> {
const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await provider.estimateFeesPerGas();
const maxFeePerGas = BigInt(_maxFeePerGas) - maxPriorityFeePerGas + MAX_PRIORITY_FEE_PER_GAS;
return { maxFeePerGas, maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS };
}
36 changes: 24 additions & 12 deletions src/gasPriceOracle/adapters/arbitrum.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import { PublicClient } from "viem";
import { InternalGasPriceEstimate } from "../types";

const MAX_PRIORITY_FEE_PER_GAS = BigInt(1);

// Arbitrum Nitro implements EIP-1559 pricing, but the priority fee is always refunded to the caller.
// Swap it for 1 Wei to avoid inaccurate transaction cost estimates.
// Reference: https://developer.arbitrum.io/faqs/gas-faqs#q-priority
export async function eip1559(provider: PublicClient, _chainId: number): Promise<InternalGasPriceEstimate> {
const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await provider.estimateFeesPerGas();
const maxFeePerGas = BigInt(_maxFeePerGas) - maxPriorityFeePerGas + MAX_PRIORITY_FEE_PER_GAS;
return { maxFeePerGas, maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS };
import { providers } from "ethers";
import { BigNumber, bnOne, parseUnits } from "../../utils";
import { GasPriceEstimate } from "../types";
import * as ethereum from "./ethereum";

let DEFAULT_PRIORITY_FEE: BigNumber | undefined = undefined;

// Arbitrum Nitro implements EIP-1559 pricing, but the priority fee is always refunded to the caller. Further,
// ethers typically hardcodes the priority fee to 1.5 Gwei. So, confirm that the priority fee supplied was 1.5
// Gwei, and then drop it to 1 Wei. Reference: https://developer.arbitrum.io/faqs/gas-faqs#q-priority
export async function eip1559(provider: providers.Provider, chainId: number): Promise<GasPriceEstimate> {
DEFAULT_PRIORITY_FEE ??= parseUnits("1.5", 9);
const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await ethereum.eip1559(provider, chainId);

// If this throws, ethers default behaviour has changed, or Arbitrum RPCs are returning something more sensible.
if (!maxPriorityFeePerGas.eq(DEFAULT_PRIORITY_FEE)) {
throw new Error(`Expected hardcoded 1.5 Gwei priority fee on Arbitrum, got ${maxPriorityFeePerGas}`);
}

// eip1559() sets maxFeePerGas = lastBaseFeePerGas + maxPriorityFeePerGas, so revert that.
// The caller may apply scaling as they wish afterwards.
const maxFeePerGas = _maxFeePerGas.sub(maxPriorityFeePerGas).add(bnOne);

return { maxPriorityFeePerGas: bnOne, maxFeePerGas };
}
19 changes: 19 additions & 0 deletions src/gasPriceOracle/adapters/ethereum-viem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { PublicClient } from "viem";
import { InternalGasPriceEstimate } from "../types";

export function eip1559(provider: PublicClient, _chainId: number): Promise<InternalGasPriceEstimate> {
return provider.estimateFeesPerGas();
}

export async function legacy(
provider: PublicClient,
_chainId: number,
_test?: number
): Promise<InternalGasPriceEstimate> {
const gasPrice = await provider.getGasPrice();

return {
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: BigInt(0),
};
}
29 changes: 19 additions & 10 deletions src/gasPriceOracle/adapters/ethereum.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { PublicClient } from "viem";
import { InternalGasPriceEstimate } from "../types";
import { providers } from "ethers";
import { BigNumber, bnZero } from "../../utils";
import { GasPriceEstimate } from "../types";
import { gasPriceError } from "../util";

export function eip1559(provider: PublicClient, _chainId: number): Promise<InternalGasPriceEstimate> {
return provider.estimateFeesPerGas();
export async function eip1559(provider: providers.Provider, chainId: number): Promise<GasPriceEstimate> {
const feeData = await provider.getFeeData();

[feeData.lastBaseFeePerGas, feeData.maxPriorityFeePerGas].forEach((field: BigNumber | null) => {
if (!BigNumber.isBigNumber(field) || field.lt(bnZero)) gasPriceError("getFeeData()", chainId, feeData);
});

const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas as BigNumber;
const maxFeePerGas = maxPriorityFeePerGas.add(feeData.lastBaseFeePerGas as BigNumber);

return { maxPriorityFeePerGas, maxFeePerGas };
}

export async function legacy(
provider: PublicClient,
_chainId: number,
_test?: number
): Promise<InternalGasPriceEstimate> {
export async function legacy(provider: providers.Provider, chainId: number): Promise<GasPriceEstimate> {
const gasPrice = await provider.getGasPrice();

if (!BigNumber.isBigNumber(gasPrice) || gasPrice.lt(bnZero)) gasPriceError("getGasPrice()", chainId, gasPrice);

return {
maxFeePerGas: gasPrice,
maxPriorityFeePerGas: BigInt(0),
maxPriorityFeePerGas: bnZero,
};
}
6 changes: 3 additions & 3 deletions src/gasPriceOracle/adapters/linea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// This query is currently only available on Linea Sepolia, ETA mainnet 30 July.
// Until then, just parrot the existing Ethereum EIP-1559 pricing strategy.
// See also: https://docs.linea.build/developers/reference/api/linea-estimategas
import { PublicClient } from "viem";
import { InternalGasPriceEstimate } from "../types";
import { providers } from "ethers";
import { GasPriceEstimate } from "../types";
import * as ethereum from "./ethereum";

export function eip1559(provider: PublicClient, chainId: number): Promise<InternalGasPriceEstimate> {
export function eip1559(provider: providers.Provider, chainId: number): Promise<GasPriceEstimate> {
return ethereum.legacy(provider, chainId);
}
86 changes: 86 additions & 0 deletions src/gasPriceOracle/adapters/polygon-viem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { PublicClient } from "viem";
import { BaseHTTPAdapter, BaseHTTPAdapterArgs } from "../../priceClient/adapters/baseAdapter";
import { isDefined } from "../../utils";
import { CHAIN_IDs } from "../../constants";
import { InternalGasPriceEstimate } from "../types";
import { gasPriceError } from "../util";
import { eip1559 } from "./ethereum-viem";

type Polygon1559GasPrice = {
maxPriorityFee: number | string;
maxFee: number | string;
};

type GasStationV2Response = {
safeLow: Polygon1559GasPrice;
standard: Polygon1559GasPrice;
fast: Polygon1559GasPrice;
estimatedBaseFee: number | string;
blockTime: number | string;
blockNumber: number | string;
};

type GasStationArgs = BaseHTTPAdapterArgs & {
chainId?: number;
host?: string;
};

const { POLYGON } = CHAIN_IDs;

const GWEI = BigInt(1_000_000_000);
class PolygonGasStation extends BaseHTTPAdapter {
readonly chainId: number;

constructor({ chainId = POLYGON, host, timeout = 1500, retries = 1 }: GasStationArgs = {}) {
host = host ?? chainId === POLYGON ? "gasstation.polygon.technology" : "gasstation-testnet.polygon.technology";

super("Polygon Gas Station", host, { timeout, retries });
this.chainId = chainId;
}

async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise<InternalGasPriceEstimate> {
const gas = await this.query("v2", {});

const gasPrice = (gas as GasStationV2Response)?.[strategy];
if (!this.isPolygon1559GasPrice(gasPrice)) {
// @todo: generalise gasPriceError() to accept a reason/cause?
gasPriceError("getFeeData()", this.chainId, gasPrice);
}

const maxPriorityFeePerGas = BigInt(gasPrice.maxPriorityFee) * GWEI;
const maxFeePerGas = BigInt(gasPrice.maxFee) * GWEI;

return { maxPriorityFeePerGas, maxFeePerGas };
}

protected isPolygon1559GasPrice(gasPrice: unknown): gasPrice is Polygon1559GasPrice {
if (!isDefined(gasPrice)) {
return false;
}
const _gasPrice = gasPrice as Polygon1559GasPrice;
return [_gasPrice.maxPriorityFee, _gasPrice.maxFee].every((field) => ["number", "string"].includes(typeof field));
}
}

export async function gasStation(provider: PublicClient, chainId: number): Promise<InternalGasPriceEstimate> {
const gasStation = new PolygonGasStation({ chainId, timeout: 2000, retries: 0 });
let maxPriorityFeePerGas: bigint;
let maxFeePerGas: bigint;
try {
({ maxPriorityFeePerGas, maxFeePerGas } = await gasStation.getFeeData());
} catch (err) {
// Fall back to the RPC provider. May be less accurate.
({ maxPriorityFeePerGas, maxFeePerGas } = await eip1559(provider, chainId));

// Per the GasStation docs, the minimum priority fee on Polygon is 30 Gwei.
// https://docs.polygon.technology/tools/gas/polygon-gas-station/#interpretation
const minPriorityFee = BigInt(30) * GWEI;
if (minPriorityFee > maxPriorityFeePerGas) {
const priorityDelta = minPriorityFee - maxPriorityFeePerGas;
maxPriorityFeePerGas = minPriorityFee;
maxFeePerGas = maxFeePerGas + priorityDelta;
}
}

return { maxPriorityFeePerGas, maxFeePerGas };
}
37 changes: 21 additions & 16 deletions src/gasPriceOracle/adapters/polygon.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { PublicClient } from "viem";
import { providers } from "ethers";
import { BaseHTTPAdapter, BaseHTTPAdapterArgs } from "../../priceClient/adapters/baseAdapter";
import { isDefined } from "../../utils";
import { BigNumber, bnZero, isDefined, parseUnits } from "../../utils";
import { CHAIN_IDs } from "../../constants";
import { InternalGasPriceEstimate } from "../types";
import { GasPriceEstimate } from "../types";
import { gasPriceError } from "../util";
import { eip1559 } from "./ethereum";

Expand All @@ -27,7 +27,6 @@ type GasStationArgs = BaseHTTPAdapterArgs & {

const { POLYGON } = CHAIN_IDs;

const GWEI = BigInt(1_000_000_000);
class PolygonGasStation extends BaseHTTPAdapter {
readonly chainId: number;

Expand All @@ -38,17 +37,23 @@ class PolygonGasStation extends BaseHTTPAdapter {
this.chainId = chainId;
}

async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise<InternalGasPriceEstimate> {
async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise<GasPriceEstimate> {
const gas = await this.query("v2", {});

const gasPrice = (gas as GasStationV2Response)?.[strategy];
if (!this.isPolygon1559GasPrice(gasPrice)) {
// @todo: generalise gasPriceError() to accept a reason/cause?
gasPriceError("getFeeData()", this.chainId, gasPrice);
gasPriceError("getFeeData()", this.chainId, bnZero);
}

const maxPriorityFeePerGas = BigInt(gasPrice.maxPriorityFee) * GWEI;
const maxFeePerGas = BigInt(gasPrice.maxFee) * GWEI;
[gasPrice.maxFee, gasPrice.maxPriorityFee].forEach((gasPrice) => {
if (Number(gasPrice) < 0) {
gasPriceError("getFeeData()", this.chainId, parseUnits(gasPrice.toString(), 9));
}
});

const maxPriorityFeePerGas = parseUnits(gasPrice.maxPriorityFee.toString(), 9);
const maxFeePerGas = parseUnits(gasPrice.maxFee.toString(), 9);

return { maxPriorityFeePerGas, maxFeePerGas };
}
Expand All @@ -62,10 +67,10 @@ class PolygonGasStation extends BaseHTTPAdapter {
}
}

export async function gasStation(provider: PublicClient, chainId: number): Promise<InternalGasPriceEstimate> {
const gasStation = new PolygonGasStation({ chainId, timeout: 2000, retries: 0 });
let maxPriorityFeePerGas: bigint;
let maxFeePerGas: bigint;
export async function gasStation(provider: providers.Provider, chainId: number): Promise<GasPriceEstimate> {
const gasStation = new PolygonGasStation({ chainId: chainId, timeout: 2000, retries: 0 });
let maxPriorityFeePerGas: BigNumber;
let maxFeePerGas: BigNumber;
try {
({ maxPriorityFeePerGas, maxFeePerGas } = await gasStation.getFeeData());
} catch (err) {
Expand All @@ -74,11 +79,11 @@ export async function gasStation(provider: PublicClient, chainId: number): Promi

// Per the GasStation docs, the minimum priority fee on Polygon is 30 Gwei.
// https://docs.polygon.technology/tools/gas/polygon-gas-station/#interpretation
const minPriorityFee = BigInt(30) * GWEI;
if (minPriorityFee > maxPriorityFeePerGas) {
const priorityDelta = minPriorityFee - maxPriorityFeePerGas;
const minPriorityFee = parseUnits("30", 9);
if (maxPriorityFeePerGas.lt(minPriorityFee)) {
const priorityDelta = minPriorityFee.sub(maxPriorityFeePerGas);
maxPriorityFeePerGas = minPriorityFee;
maxFeePerGas = maxFeePerGas + priorityDelta;
maxFeePerGas = maxFeePerGas.add(priorityDelta);
}
}

Expand Down
Loading

0 comments on commit a278a95

Please sign in to comment.