From 1a554c0eb088946f9f50ef64dfbb8f7a0701e6d9 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Sat, 28 Sep 2024 01:42:16 +0200 Subject: [PATCH 01/13] experimental: gasPriceOracle overhaul This change updates the gasPriceOracle to leverage viem's gas pricing strategies. This is beneficial because viem has an active community that are actively adding support for new chains and improving support for existing chains. An example of this is Linea, where linea_estimateGas is supported out of the box after a recent update. In order to maintain some degree of compatibility with existing users of the gasPriceOracle, the external interface still accepts an ethers Provider instance. It then internally instantiates a viem PublicClient instance and uses that instead. There is still a residual issue of how to resolve the relevant provider URLs, since it's not necessarily reliable to pull from the provider due to the uncertain type (i.e. StaticJsonRpcProvider or one of the extended variants that wraps multiple providers). --- e2e/oracle.e2e.ts | 195 ++++-------------------- package.json | 3 +- src/gasPriceOracle/adapters/arbitrum.ts | 37 ++--- src/gasPriceOracle/adapters/ethereum.ts | 25 +-- src/gasPriceOracle/adapters/linea.ts | 6 +- src/gasPriceOracle/adapters/polygon.ts | 37 ++--- src/gasPriceOracle/oracle.ts | 59 +++---- src/gasPriceOracle/types.ts | 6 +- src/gasPriceOracle/util.ts | 5 +- src/utils/common.ts | 2 +- yarn.lock | 96 ++++++++++++ 11 files changed, 205 insertions(+), 266 deletions(-) diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts index df2b0e3de..8d399f468 100644 --- a/e2e/oracle.e2e.ts +++ b/e2e/oracle.e2e.ts @@ -1,10 +1,11 @@ // @note: This test is _not_ run automatically as part of git hooks or CI. import dotenv from "dotenv"; import winston from "winston"; -import { providers, utils as ethersUtils } from "ethers"; +import { custom } from "viem"; +import { providers } from "ethers"; import { getGasPriceEstimate } from "../src/gasPriceOracle"; -import { BigNumber } from "../src/utils"; -import { assertPromiseError, expect } from "../test/utils"; +import { BigNumber, bnZero, parseUnits } from "../src/utils"; +import { expect } from "../test/utils"; dotenv.config({ path: ".env" }); const dummyLogger = winston.createLogger({ @@ -12,86 +13,46 @@ const dummyLogger = winston.createLogger({ transports: [new winston.transports.Console()], }); -type FeeData = providers.FeeData; - -class MockedProvider extends providers.StaticJsonRpcProvider { - // Unknown type => exercise our validation logic - public testFeeData: unknown; - public testGasPrice: unknown; - - constructor(url: string) { - super(url); - } - - override async getFeeData(): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (this.testFeeData as any) ?? (await super.getFeeData()); - } - - override async getGasPrice(): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return this.testGasPrice !== undefined ? (this.testGasPrice as any) : await super.getGasPrice(); +const stdLastBaseFeePerGas = parseUnits("12", 9); +const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only +const stdMaxFeePerGas = stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas); +const stdGasPrice = stdMaxFeePerGas; + +const customTransport = custom({ + async request({ method, params}: { method: string; params: unknown }) { + params; // lint + switch (method) { + case "eth_gasPrice": + return BigInt(stdGasPrice.toString()); + case "eth_getBlockByNumber": + return { baseFeePerGas: BigInt((stdLastBaseFeePerGas.mul(100).div(120)).toString()) }; + case "eth_maxPriorityFeePerGas": + return BigInt(stdMaxPriorityFeePerGas.toString()); + default: + console.log(`Unsupported method: ${method}.`); + } } -} - -/** - * Note: If NODE_URL_ envvars exist, they will be used. The - * RPCs defined below are otherwise used as default/fallback options. - * These may be subject to rate-limiting, in which case the retrieved - * price will revert to 0. - * - * Note also that Optimism is only supported as a fallback/legacy test - * case. It works, but is not the recommended method for conjuring gas - * prices on Optimism. - */ -const networks: { [chainId: number]: string } = { - 1: "https://eth.llamarpc.com", - 10: "https://mainnet.optimism.io", - 137: "https://polygon.llamarpc.com", - 288: "https://mainnet.boba.network", - 324: "https://mainnet.era.zksync.io", - 8453: "https://mainnet.base.org", - 42161: "https://rpc.ankr.com/arbitrum", - 534352: "https://rpc.scroll.io", -}; - -const stdGasPrice = ethersUtils.parseUnits("10", 9); -const stdMaxPriorityFeePerGas = ethersUtils.parseUnits("1.5", 9); // EIP-1559 chains only -const stdLastBaseFeePerGas = stdGasPrice.sub(stdMaxPriorityFeePerGas); -const stdMaxFeePerGas = stdGasPrice; -const eip1559Chains = [1, 10, 137, 8453, 42161]; -const legacyChains = [288, 324, 534352]; +}); -let providerInstances: { [chainId: number]: MockedProvider } = {}; +const eip1559Chains = [1, 10, 137, 324, 8453, 42161, 534352]; +const chainIds = [ ...eip1559Chains, 1337 ]; +let providerInstances: { [chainId: number]: providers.StaticJsonRpcProvider } = {}; describe("Gas Price Oracle", function () { before(() => { providerInstances = Object.fromEntries( - Object.entries(networks).map(([_chainId, _rpcUrl]) => { - const chainId = Number(_chainId); - const rpcUrl: string = process.env[`NODE_URL_${chainId}`] ?? _rpcUrl; - const provider = new MockedProvider(rpcUrl); + chainIds.map((chainId) => { + const provider = new providers.StaticJsonRpcProvider("https://eth.llamarpc.com"); return [chainId, provider]; }) ); }); - beforeEach(() => { - for (const provider of Object.values(providerInstances)) { - provider.testFeeData = { - gasPrice: stdGasPrice, - lastBaseFeePerGas: stdLastBaseFeePerGas, - maxPriorityFeePerGas: stdMaxPriorityFeePerGas, - }; - provider.testGasPrice = stdGasPrice; // Required: same as provider.feeData.gasPrice. - } - }); - it("Gas Price Retrieval", async function () { for (const [_chainId, provider] of Object.entries(providerInstances)) { const chainId = Number(_chainId); - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider); + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, customTransport); dummyLogger.debug({ at: "Gas Price Oracle#Gas Price Retrieval", message: `Retrieved gas price estimate for chain ID ${chainId}`, @@ -113,104 +74,14 @@ describe("Gas Price Oracle", function () { expect(maxFeePerGas.eq(stdLastBaseFeePerGas.add(1))).to.be.true; expect(maxPriorityFeePerGas.eq(1)).to.be.true; } else { - expect(maxFeePerGas.eq(stdMaxFeePerGas)).to.be.true; - expect(maxPriorityFeePerGas.eq(stdMaxPriorityFeePerGas)).to.be.true; + expect(maxFeePerGas.gt(bnZero)).to.be.true; + expect(maxPriorityFeePerGas.gt(bnZero)).to.be.true; } } else { // Defaults to Legacy (Type 0) - expect(maxFeePerGas.eq(stdGasPrice)).to.be.true; - expect(maxPriorityFeePerGas.eq(0)).to.be.true; + expect(maxFeePerGas.eq(stdMaxFeePerGas)).to.be.true; + expect(maxPriorityFeePerGas.eq(bnZero)).to.be.true; } } }); - - it("Gas Price Retrieval Failure", async function () { - const feeDataFields = ["gasPrice", "lastBaseFeePerGas", "maxPriorityFeePerGas"]; - const feeDataValues = [null, "test", "1234", 5678, BigNumber.from(-1)]; - - // Iterate over various faulty values for gasPrice & feeData. - // Loop one chain at a time to minimise rate-limiting in case a public RPC is being used. - for (const field of feeDataFields) { - for (const value of feeDataValues) { - for (const [_chainId, provider] of Object.entries(providerInstances)) { - const chainId = Number(_chainId); - - provider.testGasPrice = field === "gasPrice" ? value : stdGasPrice; - provider.testFeeData = { - gasPrice: field === "gasPrice" ? value : stdGasPrice, - lastBaseFeePerGas: field === "lastBaseFeePerGas" ? value : stdLastBaseFeePerGas, - maxPriorityFeePerGas: field === "maxPriorityFeePerGas" ? value : stdMaxPriorityFeePerGas, - }; - - // Malformed inputs were supplied; ensure an exception is thrown. - if ( - (legacyChains.includes(chainId) && ["gasPrice"].includes(field)) || - (chainId !== 137 && - eip1559Chains.includes(chainId) && - ["lastBaseFeePerGas", "maxPriorityFeePerGas"].includes(field)) - ) { - provider.testGasPrice = field === "gasPrice" ? value : stdGasPrice; - await assertPromiseError(getGasPriceEstimate(provider, chainId)); - } else { - // Expect sane results to be returned; validate them. - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, chainId); - - dummyLogger.debug({ - at: "Gas Price Oracle#Gas Price Retrieval Failure", - message: `Retrieved gas price estimate for chain ID ${chainId}.`, - maxFeePerGas, - maxPriorityFeePerGas, - }); - - expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; - expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; - - if (eip1559Chains.includes(chainId)) { - if (chainId === 137) { - 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) { - expect(maxFeePerGas.eq(stdLastBaseFeePerGas.add(1))).to.be.true; - expect(maxPriorityFeePerGas.eq(1)).to.be.true; - } else { - expect(maxFeePerGas.eq(stdMaxFeePerGas)).to.be.true; - expect(maxPriorityFeePerGas.eq(stdMaxPriorityFeePerGas)).to.be.true; - } - } else { - // Legacy - expect(maxFeePerGas.eq(stdGasPrice)).to.be.true; - expect(maxPriorityFeePerGas.eq(0)).to.be.true; - } - } - } - } - } - }); - - it("Gas Price Fallback Behaviour", async function () { - for (const provider of Object.values(providerInstances)) { - const fakeChainId = 1337; - - const chainId = (await provider.getNetwork()).chainId; - dummyLogger.debug({ - at: "Gas Price Oracle#Gas Price Fallback Behaviour", - message: `Testing on chainId ${chainId}.`, - }); - - provider.testGasPrice = stdMaxFeePerGas; // Suppress RPC lookup. - - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, fakeChainId, true); - - // Require legacy pricing when fallback is permitted. - expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; - expect(maxFeePerGas.eq(stdMaxFeePerGas)).to.be.true; - - expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; - expect(maxPriorityFeePerGas.eq(0)).to.be.true; - - // Verify an assertion is thrown when fallback is not permitted. - await assertPromiseError(getGasPriceEstimate(provider, fakeChainId, false)); - } - }); }); diff --git a/package.json b/package.json index 2a6018797..23d6952f7 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,8 @@ "lodash": "^4.17.21", "lodash.get": "^4.4.2", "superstruct": "^0.15.4", - "tslib": "^2.6.2" + "tslib": "^2.6.2", + "viem": "^2.21.15" }, "publishConfig": { "registry": "https://registry.npmjs.com/", diff --git a/src/gasPriceOracle/adapters/arbitrum.ts b/src/gasPriceOracle/adapters/arbitrum.ts index 88d38f66e..2ff629bb8 100644 --- a/src/gasPriceOracle/adapters/arbitrum.ts +++ b/src/gasPriceOracle/adapters/arbitrum.ts @@ -1,25 +1,14 @@ -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 { - 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 }; +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 { + let { maxFeePerGas, maxPriorityFeePerGas } = await provider.estimateFeesPerGas(); + console.log(`arbitrum: got maxFeePerGas ${maxFeePerGas}, maxPriorityFeePerGas: ${maxPriorityFeePerGas}.`); + maxFeePerGas = BigInt(maxFeePerGas) - maxPriorityFeePerGas + MAX_PRIORITY_FEE_PER_GAS; + return { maxFeePerGas, maxPriorityFeePerGas: MAX_PRIORITY_FEE_PER_GAS }; } diff --git a/src/gasPriceOracle/adapters/ethereum.ts b/src/gasPriceOracle/adapters/ethereum.ts index 06eb67ca4..2ffe122c1 100644 --- a/src/gasPriceOracle/adapters/ethereum.ts +++ b/src/gasPriceOracle/adapters/ethereum.ts @@ -1,28 +1,15 @@ -import { providers } from "ethers"; -import { BigNumber, bnZero } from "../../utils"; -import { GasPriceEstimate } from "../types"; -import { gasPriceError } from "../util"; +import { PublicClient } from "viem"; +import { InternalGasPriceEstimate } from "../types"; -export async function eip1559(provider: providers.Provider, chainId: number): Promise { - 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 function eip1559(provider: PublicClient, _chainId: number): Promise { + return provider.estimateFeesPerGas(); } -export async function legacy(provider: providers.Provider, chainId: number): Promise { +export async function legacy(provider: PublicClient, _chainId: number): Promise { const gasPrice = await provider.getGasPrice(); - if (!BigNumber.isBigNumber(gasPrice) || gasPrice.lt(bnZero)) gasPriceError("getGasPrice()", chainId, gasPrice); - return { maxFeePerGas: gasPrice, - maxPriorityFeePerGas: bnZero, + maxPriorityFeePerGas: BigInt(0), }; } diff --git a/src/gasPriceOracle/adapters/linea.ts b/src/gasPriceOracle/adapters/linea.ts index e87ec5dfc..c1c859b6a 100644 --- a/src/gasPriceOracle/adapters/linea.ts +++ b/src/gasPriceOracle/adapters/linea.ts @@ -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 { providers } from "ethers"; -import { GasPriceEstimate } from "../types"; +import { PublicClient } from "viem"; +import { InternalGasPriceEstimate } from "../types"; import * as ethereum from "./ethereum"; -export function eip1559(provider: providers.Provider, chainId: number): Promise { +export function eip1559(provider: PublicClient, chainId: number): Promise { return ethereum.legacy(provider, chainId); } diff --git a/src/gasPriceOracle/adapters/polygon.ts b/src/gasPriceOracle/adapters/polygon.ts index 9b8c93ea5..8bdf7a114 100644 --- a/src/gasPriceOracle/adapters/polygon.ts +++ b/src/gasPriceOracle/adapters/polygon.ts @@ -1,8 +1,8 @@ -import { providers } from "ethers"; +import { PublicClient } from "viem"; import { BaseHTTPAdapter, BaseHTTPAdapterArgs } from "../../priceClient/adapters/baseAdapter"; -import { BigNumber, bnZero, isDefined, parseUnits } from "../../utils"; +import { isDefined } from "../../utils"; import { CHAIN_IDs } from "../../constants"; -import { GasPriceEstimate } from "../types"; +import { InternalGasPriceEstimate } from "../types"; import { gasPriceError } from "../util"; import { eip1559 } from "./ethereum"; @@ -27,6 +27,7 @@ type GasStationArgs = BaseHTTPAdapterArgs & { const { POLYGON } = CHAIN_IDs; +const GWEI = BigInt(1_000_000_000); class PolygonGasStation extends BaseHTTPAdapter { readonly chainId: number; @@ -37,23 +38,17 @@ class PolygonGasStation extends BaseHTTPAdapter { this.chainId = chainId; } - async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise { + async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise { 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, bnZero); + gasPriceError("getFeeData()", this.chainId, gasPrice); } - [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); + const maxPriorityFeePerGas = BigInt(gasPrice.maxPriorityFee) * GWEI; + const maxFeePerGas = BigInt(gasPrice.maxFee) * GWEI; return { maxPriorityFeePerGas, maxFeePerGas }; } @@ -67,10 +62,10 @@ class PolygonGasStation extends BaseHTTPAdapter { } } -export async function gasStation(provider: providers.Provider, chainId: number): Promise { - const gasStation = new PolygonGasStation({ chainId: chainId, timeout: 2000, retries: 0 }); - let maxPriorityFeePerGas: BigNumber; - let maxFeePerGas: BigNumber; +export async function gasStation(provider: PublicClient, chainId: number): Promise { + const gasStation = new PolygonGasStation({ chainId, timeout: 2000, retries: 0 }); + let maxPriorityFeePerGas: bigint; + let maxFeePerGas: bigint; try { ({ maxPriorityFeePerGas, maxFeePerGas } = await gasStation.getFeeData()); } catch (err) { @@ -79,11 +74,11 @@ export async function gasStation(provider: providers.Provider, chainId: number): // 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 = parseUnits("30", 9); - if (maxPriorityFeePerGas.lt(minPriorityFee)) { - const priorityDelta = minPriorityFee.sub(maxPriorityFeePerGas); + const minPriorityFee = BigInt(30) * GWEI; + if (minPriorityFee > maxPriorityFeePerGas) { + const priorityDelta = minPriorityFee -maxPriorityFeePerGas; maxPriorityFeePerGas = minPriorityFee; - maxFeePerGas = maxFeePerGas.add(priorityDelta); + maxFeePerGas = maxFeePerGas + priorityDelta; } } diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index fc05f6d4d..4852821c6 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -1,10 +1,10 @@ +import { createPublicClient, extractChain, http, Transport } from "viem"; +import * as chains from "viem/chains"; import { providers } from "ethers"; import { CHAIN_IDs } from "../constants"; -import { chainIsOPStack } from "../utils"; -import { GasPriceEstimate, GasPriceFeed } from "./types"; +import { BigNumber } from "../utils"; +import { GasPriceEstimate } from "./types"; import * as arbitrum from "./adapters/arbitrum"; -import * as ethereum from "./adapters/ethereum"; -import * as linea from "./adapters/linea"; import * as polygon from "./adapters/polygon"; /** @@ -14,35 +14,36 @@ import * as polygon from "./adapters/polygon"; * @param legacyFallback In the case of an unrecognised chain, fall back to type 0 gas estimation. * @returns Am object of type GasPriceEstimate. */ -export async function getGasPriceEstimate( - provider: providers.Provider, - chainId?: number, - legacyFallback = true -): Promise { - if (chainId === undefined) { - ({ chainId } = await provider.getNetwork()); - } +export async function getGasPriceEstimate(provider: providers.Provider, transport?: Transport): Promise { + const { chainId } = await provider.getNetwork(); + + transport ??= http(); // @todo: Inherit URL from provider. + const chain = extractChain({ chains: Object.values(chains), id: chainId as any }); + const viemProvider = createPublicClient({ chain, transport }); - const gasPriceFeeds: { [chainId: number]: GasPriceFeed } = { + const gasPriceFeeds = { [CHAIN_IDs.ARBITRUM]: arbitrum.eip1559, - [CHAIN_IDs.BASE]: ethereum.eip1559, - [CHAIN_IDs.BOBA]: ethereum.legacy, - [CHAIN_IDs.LINEA]: linea.eip1559, // @todo: Support linea_estimateGas in adapter. - [CHAIN_IDs.MAINNET]: ethereum.eip1559, - [CHAIN_IDs.MODE]: ethereum.eip1559, - [CHAIN_IDs.OPTIMISM]: ethereum.eip1559, [CHAIN_IDs.POLYGON]: polygon.gasStation, - [CHAIN_IDs.ZK_SYNC]: ethereum.legacy, - [CHAIN_IDs.SCROLL]: ethereum.legacy, - }; + } as const; - let gasPriceFeed = gasPriceFeeds[chainId]; - if (gasPriceFeed === undefined) { - if (!legacyFallback) { - throw new Error(`No suitable gas price oracle for Chain ID ${chainId}`); - } - gasPriceFeed = chainIsOPStack(chainId) ? ethereum.eip1559 : ethereum.legacy; + let maxFeePerGas: bigint; + let maxPriorityFeePerGas: bigint; + if (gasPriceFeeds[chainId]) { + // declare function action(client: Client): void + // action(viemProvider); + // https://github.com/wevm/viem/issues/2763 + ({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId](viemProvider as any, chainId)); + } else { + let gasPrice: bigint | undefined; + ({ maxFeePerGas, maxPriorityFeePerGas, gasPrice } = await viemProvider.estimateFeesPerGas()); + // console.log(`Got maxFeePerGas: ${maxFeePerGas}, maxPriorityFeePerGas: ${maxPriorityFeePerGas}, gasPrice: ${gasPrice}.`); + + maxFeePerGas ??= gasPrice!; + maxPriorityFeePerGas ??= BigInt(0); } - return gasPriceFeed(provider, chainId); + return { + maxFeePerGas: BigNumber.from((maxFeePerGas).toString()), + maxPriorityFeePerGas: BigNumber.from((maxPriorityFeePerGas).toString()) + }; } diff --git a/src/gasPriceOracle/types.ts b/src/gasPriceOracle/types.ts index 3385eb6c8..d6af3662f 100644 --- a/src/gasPriceOracle/types.ts +++ b/src/gasPriceOracle/types.ts @@ -1,11 +1,13 @@ -import { providers } from "ethers"; +import { PublicClient, FeeValuesEIP1559 } from "viem"; import { BigNumber } from "../utils"; +export type InternalGasPriceEstimate = FeeValuesEIP1559; + export type GasPriceEstimate = { maxFeePerGas: BigNumber; maxPriorityFeePerGas: BigNumber; }; export interface GasPriceFeed { - (provider: providers.Provider, chainId: number): Promise; + (provider: PublicClient, chainId: number): Promise; } diff --git a/src/gasPriceOracle/util.ts b/src/gasPriceOracle/util.ts index debea4c61..5eb6cb697 100644 --- a/src/gasPriceOracle/util.ts +++ b/src/gasPriceOracle/util.ts @@ -1,6 +1,3 @@ -import { providers } from "ethers"; -import { BigNumber } from "../utils"; - -export function gasPriceError(method: string, chainId: number, data: providers.FeeData | BigNumber): void { +export function gasPriceError(method: string, chainId: number, data: unknown): void { throw new Error(`Malformed ${method} response on chain ID ${chainId} (${JSON.stringify(data)})`); } diff --git a/src/utils/common.ts b/src/utils/common.ts index f5973bc3c..122c365dc 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -281,7 +281,7 @@ export async function estimateTotalGasRequiredByUnsignedTransaction( tokenGasCost = l1GasCost.add(l2GasCost); } else { if (!gasPrice) { - const gasPriceEstimate = await getGasPriceEstimate(provider, chainId); + const gasPriceEstimate = await getGasPriceEstimate(provider); gasPrice = gasPriceEstimate.maxFeePerGas; } tokenGasCost = nativeGasCost.mul(gasPrice); diff --git a/yarn.lock b/yarn.lock index 49ae9b3ad..f564aca5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,6 +55,11 @@ axios "^1.6.2" zksync-web3 "^0.14.3" +"@adraffy/ens-normalize@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" + integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== + "@apollo/protobufjs@1.2.6": version "1.2.6" resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.6.tgz#d601e65211e06ae1432bf5993a1a0105f2862f27" @@ -1476,6 +1481,27 @@ dependencies: "@noble/hashes" "1.3.1" +"@noble/curves@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.0.tgz#f05771ef64da724997f69ee1261b2417a49522d6" + integrity sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg== + dependencies: + "@noble/hashes" "1.4.0" + +"@noble/curves@^1.4.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + +"@noble/curves@~1.4.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" + integrity sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw== + dependencies: + "@noble/hashes" "1.4.0" + "@noble/ed25519@^1.6.1": version "1.7.3" resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.3.tgz#57e1677bf6885354b466c38e2b620c62f45a7123" @@ -1491,6 +1517,16 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== +"@noble/hashes@1.4.0", "@noble/hashes@~1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + +"@noble/hashes@1.5.0", "@noble/hashes@^1.4.0", "@noble/hashes@~1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" + integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== + "@noble/hashes@~1.1.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.3.tgz#360afc77610e0a61f3417e497dcf36862e4f8111" @@ -2133,6 +2169,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== +"@scure/base@~1.1.6", "@scure/base@~1.1.8": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== + "@scure/bip32@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.0.tgz#dea45875e7fbc720c2b4560325f1cf5d2246d95b" @@ -2151,6 +2192,15 @@ "@noble/hashes" "~1.3.1" "@scure/base" "~1.1.0" +"@scure/bip32@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.4.0.tgz#4e1f1e196abedcef395b33b9674a042524e20d67" + integrity sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg== + dependencies: + "@noble/curves" "~1.4.0" + "@noble/hashes" "~1.4.0" + "@scure/base" "~1.1.6" + "@scure/bip39@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.0.tgz#92f11d095bae025f166bef3defcc5bf4945d419a" @@ -2167,6 +2217,14 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" +"@scure/bip39@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.4.0.tgz#664d4f851564e2e1d4bffa0339f9546ea55960a6" + integrity sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw== + dependencies: + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.8" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -3426,6 +3484,11 @@ abbrev@1.0.x: web3-eth-abi "^1.2.1" web3-utils "^1.2.1" +abitype@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.5.tgz#29d0daa3eea867ca90f7e4123144c1d1270774b6" + integrity sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -9281,6 +9344,11 @@ isomorphic-unfetch@^3.0.0: node-fetch "^2.6.1" unfetch "^4.2.0" +isows@1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" + integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -14609,6 +14677,21 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +viem@^2.21.15: + version "2.21.15" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.15.tgz#068c010946151e7f256bb7da601ab9b8287c583b" + integrity sha512-Ae05NQzMsqPWRwuAHf1OfmL0SjI+1GBgiFB0JA9BAbK/61nJXsTPsQxfV5CbLe4c3ct8IEZTX89rdeW4dqf97g== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.4.0" + "@noble/hashes" "1.4.0" + "@scure/bip32" "1.4.0" + "@scure/bip39" "1.4.0" + abitype "1.0.5" + isows "1.0.4" + webauthn-p256 "0.0.5" + ws "8.17.1" + vlq@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/vlq/-/vlq-2.0.4.tgz#6057b85729245b9829e3cc7755f95b228d4fe041" @@ -15102,6 +15185,14 @@ web3@1.8.2, web3@^1.6.0: web3-shh "1.8.2" web3-utils "1.8.2" +webauthn-p256@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.5.tgz#0baebd2ba8a414b21cc09c0d40f9dd0be96a06bd" + integrity sha512-drMGNWKdaixZNobeORVIqq7k5DsRC9FnG201K2QjeOoQLmtSDaSsVZdkg6n5jUALJKcAG++zBPJXmv6hy0nWFg== + dependencies: + "@noble/curves" "^1.4.0" + "@noble/hashes" "^1.4.0" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -15301,6 +15392,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + ws@^3.0.0: version "3.3.3" resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" From a5b38245699f71a476212c61f2fe082e85821e6c Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:23:53 +0200 Subject: [PATCH 02/13] Refactor w/ G's suggestions --- src/gasPriceOracle/oracle.ts | 17 +++++++---------- src/gasPriceOracle/types.ts | 4 ++-- src/gasPriceOracle/util.ts | 12 ++++++++++++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index 4852821c6..4557513d4 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -1,9 +1,9 @@ -import { createPublicClient, extractChain, http, Transport } from "viem"; -import * as chains from "viem/chains"; +import { Transport } from "viem"; import { providers } from "ethers"; import { CHAIN_IDs } from "../constants"; import { BigNumber } from "../utils"; import { GasPriceEstimate } from "./types"; +import { getPublicClient } from "./util"; import * as arbitrum from "./adapters/arbitrum"; import * as polygon from "./adapters/polygon"; @@ -16,10 +16,7 @@ import * as polygon from "./adapters/polygon"; */ export async function getGasPriceEstimate(provider: providers.Provider, transport?: Transport): Promise { const { chainId } = await provider.getNetwork(); - - transport ??= http(); // @todo: Inherit URL from provider. - const chain = extractChain({ chains: Object.values(chains), id: chainId as any }); - const viemProvider = createPublicClient({ chain, transport }); + const viemProvider = getPublicClient(chainId, transport); const gasPriceFeeds = { [CHAIN_IDs.ARBITRUM]: arbitrum.eip1559, @@ -29,10 +26,10 @@ export async function getGasPriceEstimate(provider: providers.Provider, transpor let maxFeePerGas: bigint; let maxPriorityFeePerGas: bigint; if (gasPriceFeeds[chainId]) { - // declare function action(client: Client): void - // action(viemProvider); - // https://github.com/wevm/viem/issues/2763 - ({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId](viemProvider as any, chainId)); + // declare function action(client: Client): void + // action(viemProvider); + // https://github.com/wevm/viem/issues/2763 + ({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId](viemProvider, chainId)); } else { let gasPrice: bigint | undefined; ({ maxFeePerGas, maxPriorityFeePerGas, gasPrice } = await viemProvider.estimateFeesPerGas()); diff --git a/src/gasPriceOracle/types.ts b/src/gasPriceOracle/types.ts index d6af3662f..f2ac56369 100644 --- a/src/gasPriceOracle/types.ts +++ b/src/gasPriceOracle/types.ts @@ -1,4 +1,4 @@ -import { PublicClient, FeeValuesEIP1559 } from "viem"; +import { type Chain, type Transport, PublicClient, FeeValuesEIP1559 } from "viem"; import { BigNumber } from "../utils"; export type InternalGasPriceEstimate = FeeValuesEIP1559; @@ -9,5 +9,5 @@ export type GasPriceEstimate = { }; export interface GasPriceFeed { - (provider: PublicClient, chainId: number): Promise; + (provider: PublicClient, chainId: number): Promise; } diff --git a/src/gasPriceOracle/util.ts b/src/gasPriceOracle/util.ts index 5eb6cb697..98e299fe4 100644 --- a/src/gasPriceOracle/util.ts +++ b/src/gasPriceOracle/util.ts @@ -1,3 +1,15 @@ +import assert from "assert"; +import { type Chain, type PublicClient, createPublicClient, http, Transport } from "viem"; +import * as chains from "viem/chains"; + export function gasPriceError(method: string, chainId: number, data: unknown): void { throw new Error(`Malformed ${method} response on chain ID ${chainId} (${JSON.stringify(data)})`); } + +export function getPublicClient(chainId: number, transport?: Transport): PublicClient { + transport ??= http(); // @todo: Inherit URL from provider. + const chain: Chain | undefined = Object.values(chains).find((chain) => chain.id === chainId); + assert(chain); + + return createPublicClient({ chain, transport }); +} From 02ec1623fc166d78612032bf29e41a18a8e2c1e0 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:27:45 +0200 Subject: [PATCH 03/13] lint --- e2e/oracle.e2e.ts | 8 ++++---- src/gasPriceOracle/adapters/polygon.ts | 2 +- src/gasPriceOracle/oracle.ts | 9 ++++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts index 8d399f468..8c0bb2a99 100644 --- a/e2e/oracle.e2e.ts +++ b/e2e/oracle.e2e.ts @@ -19,23 +19,23 @@ const stdMaxFeePerGas = stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas); const stdGasPrice = stdMaxFeePerGas; const customTransport = custom({ - async request({ method, params}: { method: string; params: unknown }) { + async request({ method, params }: { method: string; params: unknown }) { params; // lint switch (method) { case "eth_gasPrice": return BigInt(stdGasPrice.toString()); case "eth_getBlockByNumber": - return { baseFeePerGas: BigInt((stdLastBaseFeePerGas.mul(100).div(120)).toString()) }; + return { baseFeePerGas: BigInt(stdLastBaseFeePerGas.mul(100).div(120).toString()) }; case "eth_maxPriorityFeePerGas": return BigInt(stdMaxPriorityFeePerGas.toString()); default: console.log(`Unsupported method: ${method}.`); } - } + }, }); const eip1559Chains = [1, 10, 137, 324, 8453, 42161, 534352]; -const chainIds = [ ...eip1559Chains, 1337 ]; +const chainIds = [...eip1559Chains, 1337]; let providerInstances: { [chainId: number]: providers.StaticJsonRpcProvider } = {}; describe("Gas Price Oracle", function () { diff --git a/src/gasPriceOracle/adapters/polygon.ts b/src/gasPriceOracle/adapters/polygon.ts index 8bdf7a114..3b4104a4e 100644 --- a/src/gasPriceOracle/adapters/polygon.ts +++ b/src/gasPriceOracle/adapters/polygon.ts @@ -76,7 +76,7 @@ export async function gasStation(provider: PublicClient, chainId: number): Promi // https://docs.polygon.technology/tools/gas/polygon-gas-station/#interpretation const minPriorityFee = BigInt(30) * GWEI; if (minPriorityFee > maxPriorityFeePerGas) { - const priorityDelta = minPriorityFee -maxPriorityFeePerGas; + const priorityDelta = minPriorityFee - maxPriorityFeePerGas; maxPriorityFeePerGas = minPriorityFee; maxFeePerGas = maxFeePerGas + priorityDelta; } diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index 4557513d4..1c2c00e4f 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -14,7 +14,10 @@ import * as polygon from "./adapters/polygon"; * @param legacyFallback In the case of an unrecognised chain, fall back to type 0 gas estimation. * @returns Am object of type GasPriceEstimate. */ -export async function getGasPriceEstimate(provider: providers.Provider, transport?: Transport): Promise { +export async function getGasPriceEstimate( + provider: providers.Provider, + transport?: Transport +): Promise { const { chainId } = await provider.getNetwork(); const viemProvider = getPublicClient(chainId, transport); @@ -40,7 +43,7 @@ export async function getGasPriceEstimate(provider: providers.Provider, transpor } return { - maxFeePerGas: BigNumber.from((maxFeePerGas).toString()), - maxPriorityFeePerGas: BigNumber.from((maxPriorityFeePerGas).toString()) + maxFeePerGas: BigNumber.from(maxFeePerGas.toString()), + maxPriorityFeePerGas: BigNumber.from(maxPriorityFeePerGas.toString()), }; } From e5f4cd77ff6755deab548caa885c72fb0719af99 Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:30:53 +0200 Subject: [PATCH 04/13] Moar lint --- src/gasPriceOracle/adapters/arbitrum.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gasPriceOracle/adapters/arbitrum.ts b/src/gasPriceOracle/adapters/arbitrum.ts index 2ff629bb8..fc920b33b 100644 --- a/src/gasPriceOracle/adapters/arbitrum.ts +++ b/src/gasPriceOracle/adapters/arbitrum.ts @@ -7,8 +7,7 @@ const MAX_PRIORITY_FEE_PER_GAS = BigInt(1); // 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 { - let { maxFeePerGas, maxPriorityFeePerGas } = await provider.estimateFeesPerGas(); - console.log(`arbitrum: got maxFeePerGas ${maxFeePerGas}, maxPriorityFeePerGas: ${maxPriorityFeePerGas}.`); - maxFeePerGas = BigInt(maxFeePerGas) - maxPriorityFeePerGas + MAX_PRIORITY_FEE_PER_GAS; + 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 }; } From b0350e8b011c349c6f94f4bac12a2a31293ede1b Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:35:35 +0200 Subject: [PATCH 05/13] lint --- src/gasPriceOracle/adapters/ethereum.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gasPriceOracle/adapters/ethereum.ts b/src/gasPriceOracle/adapters/ethereum.ts index 2ffe122c1..e3e66816f 100644 --- a/src/gasPriceOracle/adapters/ethereum.ts +++ b/src/gasPriceOracle/adapters/ethereum.ts @@ -5,7 +5,11 @@ export function eip1559(provider: PublicClient, _chainId: number): Promise { +export async function legacy( + provider: PublicClient, + _chainId: number, + test?: number +): Promise { const gasPrice = await provider.getGasPrice(); return { From 142e0fbf98cb14d157396c22f387d65f52e1539e Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Wed, 23 Oct 2024 09:36:44 +0200 Subject: [PATCH 06/13] fixup --- src/gasPriceOracle/adapters/ethereum.ts | 2 +- src/gasPriceOracle/oracle.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/gasPriceOracle/adapters/ethereum.ts b/src/gasPriceOracle/adapters/ethereum.ts index e3e66816f..050cadbaa 100644 --- a/src/gasPriceOracle/adapters/ethereum.ts +++ b/src/gasPriceOracle/adapters/ethereum.ts @@ -8,7 +8,7 @@ export function eip1559(provider: PublicClient, _chainId: number): Promise { const gasPrice = await provider.getGasPrice(); diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index 1c2c00e4f..b5e2b6059 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -29,14 +29,10 @@ export async function getGasPriceEstimate( let maxFeePerGas: bigint; let maxPriorityFeePerGas: bigint; if (gasPriceFeeds[chainId]) { - // declare function action(client: Client): void - // action(viemProvider); - // https://github.com/wevm/viem/issues/2763 ({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId](viemProvider, chainId)); } else { let gasPrice: bigint | undefined; ({ maxFeePerGas, maxPriorityFeePerGas, gasPrice } = await viemProvider.estimateFeesPerGas()); - // console.log(`Got maxFeePerGas: ${maxFeePerGas}, maxPriorityFeePerGas: ${maxPriorityFeePerGas}, gasPrice: ${gasPrice}.`); maxFeePerGas ??= gasPrice!; maxPriorityFeePerGas ??= BigInt(0); From 82787d71ce6394c0bdf5d2274f597795aa8c0407 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Thu, 24 Oct 2024 15:05:24 +0200 Subject: [PATCH 07/13] fixup --- e2e/oracle.e2e.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts index 8c0bb2a99..863a37fd5 100644 --- a/e2e/oracle.e2e.ts +++ b/e2e/oracle.e2e.ts @@ -19,8 +19,8 @@ const stdMaxFeePerGas = stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas); const stdGasPrice = stdMaxFeePerGas; const customTransport = custom({ - async request({ method, params }: { method: string; params: unknown }) { - params; // lint + // eslint-disable-next-line require-await + async request({ method }: { method: string; params: unknown }) { switch (method) { case "eth_gasPrice": return BigInt(stdGasPrice.toString()); From 370b4ee10c6487185e501a728b5fbe7ee240dd87 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 28 Oct 2024 14:12:01 +0100 Subject: [PATCH 08/13] test: fix with custom transport (#755) --- e2e/oracle.e2e.ts | 20 ++------------ src/gasPriceOracle/oracle.ts | 10 +++---- .../chain-queries/baseQuery.ts | 26 +++++++++++++----- src/relayFeeCalculator/relayFeeCalculator.ts | 27 ++++++++++--------- src/utils/common.ts | 21 ++++++++++----- test/relayFeeCalculator.test.ts | 7 ++++- test/utils/index.ts | 1 + test/utils/transport.ts | 26 ++++++++++++++++++ 8 files changed, 89 insertions(+), 49 deletions(-) create mode 100644 test/utils/transport.ts diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts index 863a37fd5..64be79c1b 100644 --- a/e2e/oracle.e2e.ts +++ b/e2e/oracle.e2e.ts @@ -1,11 +1,10 @@ // @note: This test is _not_ run automatically as part of git hooks or CI. import dotenv from "dotenv"; import winston from "winston"; -import { custom } from "viem"; import { providers } from "ethers"; import { getGasPriceEstimate } from "../src/gasPriceOracle"; import { BigNumber, bnZero, parseUnits } from "../src/utils"; -import { expect } from "../test/utils"; +import { expect, makeCustomTransport } from "../test/utils"; dotenv.config({ path: ".env" }); const dummyLogger = winston.createLogger({ @@ -16,23 +15,8 @@ const dummyLogger = winston.createLogger({ const stdLastBaseFeePerGas = parseUnits("12", 9); const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only const stdMaxFeePerGas = stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas); -const stdGasPrice = stdMaxFeePerGas; -const customTransport = custom({ - // eslint-disable-next-line require-await - async request({ method }: { method: string; params: unknown }) { - switch (method) { - case "eth_gasPrice": - return BigInt(stdGasPrice.toString()); - case "eth_getBlockByNumber": - return { baseFeePerGas: BigInt(stdLastBaseFeePerGas.mul(100).div(120).toString()) }; - case "eth_maxPriorityFeePerGas": - return BigInt(stdMaxPriorityFeePerGas.toString()); - default: - console.log(`Unsupported method: ${method}.`); - } - }, -}); +const customTransport = makeCustomTransport({ stdLastBaseFeePerGas, stdMaxPriorityFeePerGas }); const eip1559Chains = [1, 10, 137, 324, 8453, 42161, 534352]; const chainIds = [...eip1559Chains, 1337]; diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index b5e2b6059..14eb2783a 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -9,16 +9,16 @@ import * as polygon from "./adapters/polygon"; /** * Provide an estimate for the current gas price for a particular chain. - * @param chainId The chain ID to query for gas prices. - * @param provider A valid ethers provider. - * @param legacyFallback In the case of an unrecognised chain, fall back to type 0 gas estimation. + * @param providerOrChainId A valid ethers provider or a chain ID. + * @param transport An optional transport object for custom gas price retrieval. * @returns Am object of type GasPriceEstimate. */ export async function getGasPriceEstimate( - provider: providers.Provider, + providerOrChainId: providers.Provider | number, transport?: Transport ): Promise { - const { chainId } = await provider.getNetwork(); + const chainId = + typeof providerOrChainId === "number" ? providerOrChainId : (await providerOrChainId.getNetwork()).chainId; const viemProvider = getPublicClient(chainId, transport); const gasPriceFeeds = { diff --git a/src/relayFeeCalculator/chain-queries/baseQuery.ts b/src/relayFeeCalculator/chain-queries/baseQuery.ts index f6f0db0ed..514075eb6 100644 --- a/src/relayFeeCalculator/chain-queries/baseQuery.ts +++ b/src/relayFeeCalculator/chain-queries/baseQuery.ts @@ -14,6 +14,7 @@ import { toBNWei, } from "../../utils"; import { Logger, QueryInterface } from "../relayFeeCalculator"; +import { Transport } from "viem"; type Provider = providers.Provider; type OptimismProvider = L2Provider; @@ -61,17 +62,25 @@ export class QueryBase implements QueryInterface { * Retrieves the current gas costs of performing a fillRelay contract at the referenced SpokePool. * @param deposit V3 deposit instance. * @param relayerAddress Relayer address to simulate with. - * @param gasPrice Optional gas price to use for the simulation. - * @param gasUnits Optional gas units to use for the simulation. + * @param options + * @param options.gasPrice Optional gas price to use for the simulation. + * @param options.gasUnits Optional gas units to use for the simulation. + * @param options.omitMarkup Optional flag to omit the gas markup. + * @param options.transport Optional transport object for custom gas price retrieval. * @returns The gas estimate for this function call (multiplied with the optional buffer). */ async getGasCosts( deposit: Deposit, relayer = DEFAULT_SIMULATED_RELAYER_ADDRESS, - gasPrice = this.fixedGasPrice, - gasUnits?: BigNumberish, - omitMarkup?: boolean + options: Partial<{ + gasPrice: BigNumberish; + gasUnits: BigNumberish; + omitMarkup: boolean; + transport: Transport; + }> = {} ): Promise { + const { gasPrice = this.fixedGasPrice, gasUnits, omitMarkup, transport } = options; + const gasMarkup = omitMarkup ? 0 : this.gasMarkup; assert( gasMarkup > -1 && gasMarkup <= 4, @@ -84,8 +93,11 @@ export class QueryBase implements QueryInterface { tx, relayer, this.provider, - gasPrice, - gasUnits + { + gasPrice, + gasUnits, + transport, + } ); return { diff --git a/src/relayFeeCalculator/relayFeeCalculator.ts b/src/relayFeeCalculator/relayFeeCalculator.ts index 0ea827470..8974d303d 100644 --- a/src/relayFeeCalculator/relayFeeCalculator.ts +++ b/src/relayFeeCalculator/relayFeeCalculator.ts @@ -17,14 +17,14 @@ import { toBN, toBNWei, } from "../utils"; +import { Transport } from "viem"; // This needs to be implemented for every chain and passed into RelayFeeCalculator export interface QueryInterface { getGasCosts: ( deposit: Deposit, relayer: string, - gasPrice?: BigNumberish, - gasLimit?: BigNumberish + options?: Partial<{ gasPrice: BigNumberish; gasUnits: BigNumberish; omitMarkup: boolean; transport: Transport }> ) => Promise; getTokenPrice: (tokenSymbol: string) => Promise; getTokenDecimals: (tokenSymbol: string) => number; @@ -230,7 +230,8 @@ export class RelayFeeCalculator { _tokenPrice?: number, tokenMapping = TOKEN_SYMBOLS_MAP, gasPrice?: BigNumberish, - gasLimit?: BigNumberish + gasLimit?: BigNumberish, + transport?: Transport ): Promise { if (toBN(amountToRelay).eq(bnZero)) return MAX_BIG_INT; @@ -245,16 +246,18 @@ export class RelayFeeCalculator { const simulatedAmount = simulateZeroFill ? safeOutputAmount : toBN(amountToRelay); deposit = { ...deposit, outputAmount: simulatedAmount }; - const getGasCosts = this.queries.getGasCosts(deposit, relayerAddress, gasPrice, gasLimit).catch((error) => { - this.logger.error({ - at: "sdk/gasFeePercent", - message: "Error while fetching gas costs", - error, - simulateZeroFill, - deposit, + const getGasCosts = this.queries + .getGasCosts(deposit, relayerAddress, { gasPrice, gasUnits: gasLimit, transport }) + .catch((error) => { + this.logger.error({ + at: "sdk/gasFeePercent", + message: "Error while fetching gas costs", + error, + simulateZeroFill, + deposit, + }); + throw error; }); - throw error; - }); const [{ tokenGasCost }, tokenPrice] = await Promise.all([ getGasCosts, _tokenPrice ?? diff --git a/src/utils/common.ts b/src/utils/common.ts index b585ef888..dea2c1879 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -8,6 +8,7 @@ import { TypedMessage } from "../interfaces/TypedData"; import { BigNumber, BigNumberish, BN, formatUnits, parseUnits, toBN } from "./BigNumberUtils"; import { ConvertDecimals } from "./FormattingUtils"; import { chainIsOPStack } from "./NetworkUtils"; +import { Transport } from "viem"; export type Decimalish = string | number | Decimal; export const AddressZero = ethers.constants.AddressZero; @@ -239,17 +240,24 @@ export type TransactionCostEstimate = { * @param unsignedTx The unsigned transaction that this function will estimate. * @param senderAddress The address that the transaction will be submitted from. * @param provider A valid ethers provider - will be used to reason the gas price. - * @param gasPrice A manually provided gas price - if set, this function will not resolve the current gas price. - * @param gasUnits A manually provided gas units - if set, this function will not estimate the gas units. + * @param options + * @param options.gasPrice A manually provided gas price - if set, this function will not resolve the current gas price. + * @param options.gasUnits A manually provided gas units - if set, this function will not estimate the gas units. + * @param options.transport A custom transport object for custom gas price retrieval. * @returns Estimated cost in units of gas and the underlying gas token (gasPrice * estimatedGasUnits). */ export async function estimateTotalGasRequiredByUnsignedTransaction( unsignedTx: PopulatedTransaction, senderAddress: string, provider: providers.Provider | L2Provider, - gasPrice?: BigNumberish, - gasUnits?: BigNumberish + options: Partial<{ + gasPrice: BigNumberish; + gasUnits: BigNumberish; + transport: Transport; + }> = {} ): Promise { + const { gasPrice: _gasPrice, gasUnits, transport } = options || {}; + const { chainId } = await provider.getNetwork(); const voidSigner = new VoidSigner(senderAddress, provider); @@ -268,13 +276,14 @@ export async function estimateTotalGasRequiredByUnsignedTransaction( // `provider.estimateTotalGasCost` to improve performance. const [l1GasCost, l2GasPrice] = await Promise.all([ provider.estimateL1GasCost(populatedTransaction), - gasPrice || provider.getGasPrice(), + _gasPrice || provider.getGasPrice(), ]); const l2GasCost = nativeGasCost.mul(l2GasPrice); tokenGasCost = l1GasCost.add(l2GasCost); } else { + let gasPrice = _gasPrice; if (!gasPrice) { - const gasPriceEstimate = await getGasPriceEstimate(provider); + const gasPriceEstimate = await getGasPriceEstimate(chainId, transport); gasPrice = gasPriceEstimate.maxFeePerGas; } tokenGasCost = nativeGasCost.mul(gasPrice); diff --git a/test/relayFeeCalculator.test.ts b/test/relayFeeCalculator.test.ts index e72e9b89d..7b020f251 100644 --- a/test/relayFeeCalculator.test.ts +++ b/test/relayFeeCalculator.test.ts @@ -25,6 +25,7 @@ import { getContractFactory, randomAddress, setupTokensForWallet, + makeCustomTransport, } from "./utils"; import { TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; import { EMPTY_MESSAGE, ZERO_ADDRESS } from "../src/constants"; @@ -286,6 +287,7 @@ describe("RelayFeeCalculator: Composable Bridging", function () { let owner: SignerWithAddress, relayer: SignerWithAddress, depositor: SignerWithAddress; let tokenMap: typeof TOKEN_SYMBOLS_MAP; let testGasFeePct: (message?: string) => Promise; + const customTransport = makeCustomTransport(); beforeEach(async function () { [owner, relayer, depositor] = await ethers.getSigners(); @@ -345,7 +347,10 @@ describe("RelayFeeCalculator: Composable Bridging", function () { false, relayer.address, 1, - tokenMap + tokenMap, + undefined, + undefined, + customTransport ); }); it("should not revert if no message is passed", async () => { diff --git a/test/utils/index.ts b/test/utils/index.ts index 91f17f9c0..fb9d65a2d 100644 --- a/test/utils/index.ts +++ b/test/utils/index.ts @@ -6,3 +6,4 @@ export * from "./utils"; export * from "./BlockchainUtils"; export * from "./SpokePoolUtils"; export * from "./SpyTransport"; +export * from "./transport"; diff --git a/test/utils/transport.ts b/test/utils/transport.ts new file mode 100644 index 000000000..38915d979 --- /dev/null +++ b/test/utils/transport.ts @@ -0,0 +1,26 @@ +import { custom } from "viem"; +import { BigNumber, parseUnits } from "../../src/utils"; + +export const makeCustomTransport = ( + params: Partial<{ stdLastBaseFeePerGas: BigNumber; stdMaxPriorityFeePerGas: BigNumber }> = {} +) => { + const { stdLastBaseFeePerGas = parseUnits("12", 9), stdMaxPriorityFeePerGas = parseUnits("1", 9) } = params; + const stdMaxFeePerGas = stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas); + const stdGasPrice = stdMaxFeePerGas; + + return custom({ + // eslint-disable-next-line require-await + async request({ method }: { method: string; params: unknown }) { + switch (method) { + case "eth_gasPrice": + return BigInt(stdGasPrice.toString()); + case "eth_getBlockByNumber": + return { baseFeePerGas: BigInt(stdLastBaseFeePerGas.toString()) }; + case "eth_maxPriorityFeePerGas": + return BigInt(stdMaxPriorityFeePerGas.toString()); + default: + throw new Error(`Unsupported method: ${method}.`); + } + }, + }); +}; From 5239c0373a9ce3db51b186bfe6f8063df51f325a Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:49:02 +0100 Subject: [PATCH 09/13] Fix test --- e2e/oracle.e2e.ts | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts index 64be79c1b..ee329d75a 100644 --- a/e2e/oracle.e2e.ts +++ b/e2e/oracle.e2e.ts @@ -14,29 +14,14 @@ const dummyLogger = winston.createLogger({ const stdLastBaseFeePerGas = parseUnits("12", 9); const stdMaxPriorityFeePerGas = parseUnits("1", 9); // EIP-1559 chains only -const stdMaxFeePerGas = stdLastBaseFeePerGas.add(stdMaxPriorityFeePerGas); +const chainIds = [1, 10, 137, 324, 8453, 42161, 534352]; const customTransport = makeCustomTransport({ stdLastBaseFeePerGas, stdMaxPriorityFeePerGas }); -const eip1559Chains = [1, 10, 137, 324, 8453, 42161, 534352]; -const chainIds = [...eip1559Chains, 1337]; -let providerInstances: { [chainId: number]: providers.StaticJsonRpcProvider } = {}; - describe("Gas Price Oracle", function () { - before(() => { - providerInstances = Object.fromEntries( - chainIds.map((chainId) => { - const provider = new providers.StaticJsonRpcProvider("https://eth.llamarpc.com"); - return [chainId, provider]; - }) - ); - }); - it("Gas Price Retrieval", async function () { - for (const [_chainId, provider] of Object.entries(providerInstances)) { - const chainId = Number(_chainId); - - const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(provider, customTransport); + for (const chainId of chainIds) { + const { maxFeePerGas, maxPriorityFeePerGas } = await getGasPriceEstimate(chainId, customTransport); dummyLogger.debug({ at: "Gas Price Oracle#Gas Price Retrieval", message: `Retrieved gas price estimate for chain ID ${chainId}`, @@ -47,7 +32,7 @@ describe("Gas Price Oracle", function () { expect(BigNumber.isBigNumber(maxFeePerGas)).to.be.true; expect(BigNumber.isBigNumber(maxPriorityFeePerGas)).to.be.true; - if (eip1559Chains.includes(chainId)) { + 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; @@ -55,16 +40,13 @@ describe("Gas Price Oracle", function () { expect(maxPriorityFeePerGas.lt(maxFeePerGas)).to.be.true; } else if (chainId === 42161) { // Arbitrum priority fees are refunded, so drop the priority fee from estimates. - expect(maxFeePerGas.eq(stdLastBaseFeePerGas.add(1))).to.be.true; + // 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; } - } else { - // Defaults to Legacy (Type 0) - expect(maxFeePerGas.eq(stdMaxFeePerGas)).to.be.true; - expect(maxPriorityFeePerGas.eq(bnZero)).to.be.true; } } }); From 9f962f4a022cf82afafb6990f7cc9674a360c05d Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 31 Oct 2024 20:18:38 +0100 Subject: [PATCH 10/13] lint --- e2e/oracle.e2e.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts index ee329d75a..58f482431 100644 --- a/e2e/oracle.e2e.ts +++ b/e2e/oracle.e2e.ts @@ -1,7 +1,6 @@ // @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"; From a278a956f85d717337de0ca8507996faba10f0eb Mon Sep 17 00:00:00 2001 From: Paul <108695806+pxrl@users.noreply.github.com> Date: Thu, 31 Oct 2024 12:47:53 +0100 Subject: [PATCH 11/13] Support selective override to viem --- e2e/oracle.e2e.ts | 37 +++++---- src/gasPriceOracle/adapters/arbitrum-viem.ts | 13 +++ src/gasPriceOracle/adapters/arbitrum.ts | 36 +++++--- src/gasPriceOracle/adapters/ethereum-viem.ts | 19 +++++ src/gasPriceOracle/adapters/ethereum.ts | 29 ++++--- src/gasPriceOracle/adapters/linea.ts | 6 +- src/gasPriceOracle/adapters/polygon-viem.ts | 86 ++++++++++++++++++++ src/gasPriceOracle/adapters/polygon.ts | 37 +++++---- src/gasPriceOracle/oracle.ts | 74 ++++++++++++++++- src/utils/common.ts | 2 +- 10 files changed, 277 insertions(+), 62 deletions(-) create mode 100644 src/gasPriceOracle/adapters/arbitrum-viem.ts create mode 100644 src/gasPriceOracle/adapters/ethereum-viem.ts create mode 100644 src/gasPriceOracle/adapters/polygon-viem.ts diff --git a/e2e/oracle.e2e.ts b/e2e/oracle.e2e.ts index 58f482431..360cfa6d7 100644 --- a/e2e/oracle.e2e.ts +++ b/e2e/oracle.e2e.ts @@ -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"; @@ -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}`, @@ -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]; } }); }); diff --git a/src/gasPriceOracle/adapters/arbitrum-viem.ts b/src/gasPriceOracle/adapters/arbitrum-viem.ts new file mode 100644 index 000000000..fc920b33b --- /dev/null +++ b/src/gasPriceOracle/adapters/arbitrum-viem.ts @@ -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 { + 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 }; +} diff --git a/src/gasPriceOracle/adapters/arbitrum.ts b/src/gasPriceOracle/adapters/arbitrum.ts index fc920b33b..88d38f66e 100644 --- a/src/gasPriceOracle/adapters/arbitrum.ts +++ b/src/gasPriceOracle/adapters/arbitrum.ts @@ -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 { - 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 { + 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 }; } diff --git a/src/gasPriceOracle/adapters/ethereum-viem.ts b/src/gasPriceOracle/adapters/ethereum-viem.ts new file mode 100644 index 000000000..050cadbaa --- /dev/null +++ b/src/gasPriceOracle/adapters/ethereum-viem.ts @@ -0,0 +1,19 @@ +import { PublicClient } from "viem"; +import { InternalGasPriceEstimate } from "../types"; + +export function eip1559(provider: PublicClient, _chainId: number): Promise { + return provider.estimateFeesPerGas(); +} + +export async function legacy( + provider: PublicClient, + _chainId: number, + _test?: number +): Promise { + const gasPrice = await provider.getGasPrice(); + + return { + maxFeePerGas: gasPrice, + maxPriorityFeePerGas: BigInt(0), + }; +} diff --git a/src/gasPriceOracle/adapters/ethereum.ts b/src/gasPriceOracle/adapters/ethereum.ts index 050cadbaa..06eb67ca4 100644 --- a/src/gasPriceOracle/adapters/ethereum.ts +++ b/src/gasPriceOracle/adapters/ethereum.ts @@ -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 { - return provider.estimateFeesPerGas(); +export async function eip1559(provider: providers.Provider, chainId: number): Promise { + 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 { +export async function legacy(provider: providers.Provider, chainId: number): Promise { const gasPrice = await provider.getGasPrice(); + if (!BigNumber.isBigNumber(gasPrice) || gasPrice.lt(bnZero)) gasPriceError("getGasPrice()", chainId, gasPrice); + return { maxFeePerGas: gasPrice, - maxPriorityFeePerGas: BigInt(0), + maxPriorityFeePerGas: bnZero, }; } diff --git a/src/gasPriceOracle/adapters/linea.ts b/src/gasPriceOracle/adapters/linea.ts index c1c859b6a..e87ec5dfc 100644 --- a/src/gasPriceOracle/adapters/linea.ts +++ b/src/gasPriceOracle/adapters/linea.ts @@ -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 { +export function eip1559(provider: providers.Provider, chainId: number): Promise { return ethereum.legacy(provider, chainId); } diff --git a/src/gasPriceOracle/adapters/polygon-viem.ts b/src/gasPriceOracle/adapters/polygon-viem.ts new file mode 100644 index 000000000..c1ea717dc --- /dev/null +++ b/src/gasPriceOracle/adapters/polygon-viem.ts @@ -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 { + 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 { + 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 }; +} diff --git a/src/gasPriceOracle/adapters/polygon.ts b/src/gasPriceOracle/adapters/polygon.ts index 3b4104a4e..9b8c93ea5 100644 --- a/src/gasPriceOracle/adapters/polygon.ts +++ b/src/gasPriceOracle/adapters/polygon.ts @@ -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"; @@ -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; @@ -38,17 +37,23 @@ class PolygonGasStation extends BaseHTTPAdapter { this.chainId = chainId; } - async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise { + async getFeeData(strategy: "safeLow" | "standard" | "fast" = "fast"): Promise { 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 }; } @@ -62,10 +67,10 @@ class PolygonGasStation extends BaseHTTPAdapter { } } -export async function gasStation(provider: PublicClient, chainId: number): Promise { - 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 { + const gasStation = new PolygonGasStation({ chainId: chainId, timeout: 2000, retries: 0 }); + let maxPriorityFeePerGas: BigNumber; + let maxFeePerGas: BigNumber; try { ({ maxPriorityFeePerGas, maxFeePerGas } = await gasStation.getFeeData()); } catch (err) { @@ -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); } } diff --git a/src/gasPriceOracle/oracle.ts b/src/gasPriceOracle/oracle.ts index 14eb2783a..205b79e66 100644 --- a/src/gasPriceOracle/oracle.ts +++ b/src/gasPriceOracle/oracle.ts @@ -1,11 +1,77 @@ import { Transport } from "viem"; import { providers } from "ethers"; import { CHAIN_IDs } from "../constants"; -import { BigNumber } from "../utils"; +import { BigNumber, chainIsOPStack } from "../utils"; import { GasPriceEstimate } from "./types"; import { getPublicClient } from "./util"; import * as arbitrum from "./adapters/arbitrum"; +import * as ethereum from "./adapters/ethereum"; +import * as linea from "./adapters/linea"; import * as polygon from "./adapters/polygon"; +import * as arbitrumViem from "./adapters/arbitrum-viem"; +import * as polygonViem from "./adapters/polygon-viem"; + +/** + * Provide an estimate for the current gas price for a particular chain. + * @param chainId The chain ID to query for gas prices. + * @param provider A valid ethers provider. + * @param legacyFallback In the case of an unrecognised chain, fall back to type 0 gas estimation. + * @returns Am object of type GasPriceEstimate. + */ +export async function getGasPriceEstimate( + provider: providers.Provider, + chainId?: number, + transport?: Transport, + legacyFallback = true +): Promise { + if (chainId === undefined) { + ({ chainId } = await provider.getNetwork()); + } + + const useViem = process.env[`NEW_GAS_PRICE_ORACLE_${chainId}`] === "true"; + return useViem + ? getViemGasPriceEstimate(chainId, transport) + : getEthersGasPriceEstimate(provider, chainId, legacyFallback); +} + +/** + * Provide an estimate for the current gas price for a particular chain. + * @param chainId The chain ID to query for gas prices. + * @param provider A valid ethers provider. + * @param legacyFallback In the case of an unrecognised chain, fall back to type 0 gas estimation. + * @returns Am object of type GasPriceEstimate. + */ +async function getEthersGasPriceEstimate( + provider: providers.Provider, + chainId?: number, + legacyFallback = true +): Promise { + if (chainId === undefined) { + ({ chainId } = await provider.getNetwork()); + } + + const gasPriceFeeds = { + [CHAIN_IDs.ARBITRUM]: arbitrum.eip1559, + [CHAIN_IDs.BASE]: ethereum.eip1559, + [CHAIN_IDs.LINEA]: linea.eip1559, // @todo: Support linea_estimateGas in adapter. + [CHAIN_IDs.MAINNET]: ethereum.eip1559, + [CHAIN_IDs.MODE]: ethereum.eip1559, + [CHAIN_IDs.OPTIMISM]: ethereum.eip1559, + [CHAIN_IDs.POLYGON]: polygon.gasStation, + [CHAIN_IDs.ZK_SYNC]: ethereum.legacy, + [CHAIN_IDs.SCROLL]: ethereum.legacy, + } as const; + + let gasPriceFeed = gasPriceFeeds[chainId]; + if (gasPriceFeed === undefined) { + if (!legacyFallback) { + throw new Error(`No suitable gas price oracle for Chain ID ${chainId}`); + } + gasPriceFeed = chainIsOPStack(chainId) ? ethereum.eip1559 : ethereum.legacy; + } + + return gasPriceFeed(provider, chainId); +} /** * Provide an estimate for the current gas price for a particular chain. @@ -13,7 +79,7 @@ import * as polygon from "./adapters/polygon"; * @param transport An optional transport object for custom gas price retrieval. * @returns Am object of type GasPriceEstimate. */ -export async function getGasPriceEstimate( +export async function getViemGasPriceEstimate( providerOrChainId: providers.Provider | number, transport?: Transport ): Promise { @@ -22,8 +88,8 @@ export async function getGasPriceEstimate( const viemProvider = getPublicClient(chainId, transport); const gasPriceFeeds = { - [CHAIN_IDs.ARBITRUM]: arbitrum.eip1559, - [CHAIN_IDs.POLYGON]: polygon.gasStation, + [CHAIN_IDs.ARBITRUM]: arbitrumViem.eip1559, + [CHAIN_IDs.POLYGON]: polygonViem.gasStation, } as const; let maxFeePerGas: bigint; diff --git a/src/utils/common.ts b/src/utils/common.ts index dea2c1879..4c8ff27a4 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -283,7 +283,7 @@ export async function estimateTotalGasRequiredByUnsignedTransaction( } else { let gasPrice = _gasPrice; if (!gasPrice) { - const gasPriceEstimate = await getGasPriceEstimate(chainId, transport); + const gasPriceEstimate = await getGasPriceEstimate(provider, chainId, transport); gasPrice = gasPriceEstimate.maxFeePerGas; } tokenGasCost = nativeGasCost.mul(gasPrice); From 5f3df097f75a403591144b65503add6c663b5d4f Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 1 Nov 2024 14:37:37 +0100 Subject: [PATCH 12/13] refactor: replace deprecated `@eth-op/sdk` with viem --- package.json | 1 - .../chain-queries/baseQuery.ts | 4 +- .../chain-queries/factory.ts | 5 +- src/utils/common.ts | 31 +++++--- yarn.lock | 77 ++----------------- 5 files changed, 27 insertions(+), 91 deletions(-) diff --git a/package.json b/package.json index 2b53e7d5c..17e37b047 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,6 @@ "@across-protocol/across-token": "^1.0.0", "@across-protocol/constants": "^3.1.16", "@across-protocol/contracts": "^3.0.11", - "@eth-optimism/sdk": "^3.3.1", "@ethersproject/bignumber": "^5.7.0", "@pinata/sdk": "^2.1.0", "@types/mocha": "^10.0.1", diff --git a/src/relayFeeCalculator/chain-queries/baseQuery.ts b/src/relayFeeCalculator/chain-queries/baseQuery.ts index 514075eb6..6faf643cd 100644 --- a/src/relayFeeCalculator/chain-queries/baseQuery.ts +++ b/src/relayFeeCalculator/chain-queries/baseQuery.ts @@ -1,4 +1,3 @@ -import { L2Provider } from "@eth-optimism/sdk/dist/interfaces/l2-provider"; import { providers } from "ethers"; import assert from "assert"; import { Coingecko } from "../../coingecko"; @@ -17,7 +16,6 @@ import { Logger, QueryInterface } from "../relayFeeCalculator"; import { Transport } from "viem"; type Provider = providers.Provider; -type OptimismProvider = L2Provider; type SymbolMappingType = Record< string, { @@ -45,7 +43,7 @@ export class QueryBase implements QueryInterface { * @param coingeckoBaseCurrency The basis currency that CoinGecko will use to resolve pricing */ constructor( - readonly provider: Provider | OptimismProvider, + readonly provider: Provider, readonly symbolMapping: SymbolMappingType, readonly spokePoolAddress: string, readonly simulatedRelayerAddress: string, diff --git a/src/relayFeeCalculator/chain-queries/factory.ts b/src/relayFeeCalculator/chain-queries/factory.ts index f3bd2e9ac..94bb9639e 100644 --- a/src/relayFeeCalculator/chain-queries/factory.ts +++ b/src/relayFeeCalculator/chain-queries/factory.ts @@ -1,10 +1,9 @@ import assert from "assert"; import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; import { getDeployedAddress } from "@across-protocol/contracts"; -import { asL2Provider } from "@eth-optimism/sdk"; import { providers } from "ethers"; import { DEFAULT_SIMULATED_RELAYER_ADDRESS } from "../../constants"; -import { chainIsMatic, chainIsOPStack, isDefined } from "../../utils"; +import { chainIsMatic, isDefined } from "../../utils"; import { QueryBase } from "./baseQuery"; import { PolygonQueries } from "./polygon"; import { DEFAULT_LOGGER, Logger } from "../relayFeeCalculator"; @@ -43,8 +42,6 @@ export class QueryBase__factory { gasMarkup ); } - // For OPStack chains, we need to wrap the provider in an L2Provider - provider = chainIsOPStack(chainId) ? asL2Provider(provider) : provider; return new QueryBase( provider, diff --git a/src/utils/common.ts b/src/utils/common.ts index 4c8ff27a4..42a58f035 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,5 +1,3 @@ -import { L2Provider } from "@eth-optimism/sdk/dist/interfaces/l2-provider"; -import { isL2Provider as isOptimismL2Provider } from "@eth-optimism/sdk/dist/l2-provider"; import assert from "assert"; import Decimal from "decimal.js"; import { ethers, PopulatedTransaction, providers, VoidSigner } from "ethers"; @@ -8,7 +6,9 @@ import { TypedMessage } from "../interfaces/TypedData"; import { BigNumber, BigNumberish, BN, formatUnits, parseUnits, toBN } from "./BigNumberUtils"; import { ConvertDecimals } from "./FormattingUtils"; import { chainIsOPStack } from "./NetworkUtils"; -import { Transport } from "viem"; +import { Address, createPublicClient, Hex, http, Transport } from "viem"; +import * as chains from "viem/chains"; +import { publicActionsL2 } from "viem/op-stack"; export type Decimalish = string | number | Decimal; export const AddressZero = ethers.constants.AddressZero; @@ -249,7 +249,7 @@ export type TransactionCostEstimate = { export async function estimateTotalGasRequiredByUnsignedTransaction( unsignedTx: PopulatedTransaction, senderAddress: string, - provider: providers.Provider | L2Provider, + provider: providers.Provider, options: Partial<{ gasPrice: BigNumberish; gasUnits: BigNumberish; @@ -267,19 +267,28 @@ export async function estimateTotalGasRequiredByUnsignedTransaction( // OP stack is a special case; gas cost is computed by the SDK, without having to query price. if (chainIsOPStack(chainId)) { - assert(isOptimismL2Provider(provider), `Unexpected provider for chain ID ${chainId}.`); + const chain = Object.values(chains).find((chain) => chain.id === chainId); + assert(chain, `Chain ID ${chainId} not supported`); + const opStackClient = createPublicClient({ + chain, + transport: transport ?? http(), + }).extend(publicActionsL2()); const populatedTransaction = await voidSigner.populateTransaction({ ...unsignedTx, gasLimit: nativeGasCost, // prevents additional gas estimation call }); - // Concurrently estimate the gas cost on L1 and L2 instead of calling - // `provider.estimateTotalGasCost` to improve performance. const [l1GasCost, l2GasPrice] = await Promise.all([ - provider.estimateL1GasCost(populatedTransaction), - _gasPrice || provider.getGasPrice(), + opStackClient.estimateL1Fee({ + account: senderAddress as Address, + to: populatedTransaction.to as Address, + value: BigInt(populatedTransaction.value?.toString() ?? 0), + data: populatedTransaction.data as Hex, + gas: populatedTransaction.gasLimit ? BigInt(populatedTransaction.gasLimit.toString()) : undefined, + }), + _gasPrice ? BigInt(_gasPrice.toString()) : opStackClient.getGasPrice(), ]); - const l2GasCost = nativeGasCost.mul(l2GasPrice); - tokenGasCost = l1GasCost.add(l2GasCost); + const l2GasCost = nativeGasCost.mul(l2GasPrice.toString()); + tokenGasCost = BigNumber.from(l1GasCost.toString()).add(l2GasCost); } else { let gasPrice = _gasPrice; if (!gasPrice) { diff --git a/yarn.lock b/yarn.lock index 73828907c..2eb8f9631 100644 --- a/yarn.lock +++ b/yarn.lock @@ -511,15 +511,6 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.49.0.tgz#86f79756004a97fa4df866835093f1df3d03c333" integrity sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w== -"@eth-optimism/contracts@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@eth-optimism/contracts/-/contracts-0.6.0.tgz#15ae76222a9b4d958a550cafb1960923af613a31" - integrity sha512-vQ04wfG9kMf1Fwy3FEMqH2QZbgS0gldKhcBeBUPfO8zu68L61VI97UDXmsMQXzTsEAxK8HnokW3/gosl4/NW3w== - dependencies: - "@eth-optimism/core-utils" "0.12.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/abstract-signer" "^5.7.0" - "@eth-optimism/contracts@^0.5.37", "@eth-optimism/contracts@^0.5.40", "@eth-optimism/contracts@^0.5.5": version "0.5.40" resolved "https://registry.yarnpkg.com/@eth-optimism/contracts/-/contracts-0.5.40.tgz#d13a04a15ea947a69055e6fc74d87e215d4c936a" @@ -551,26 +542,6 @@ bufio "^1.0.7" chai "^4.3.4" -"@eth-optimism/core-utils@0.13.2": - version "0.13.2" - resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.13.2.tgz#c0187c3abf6d86dad039edf04ff81299253214fe" - integrity sha512-u7TOKm1RxH1V5zw7dHmfy91bOuEAZU68LT/9vJPkuWEjaTl+BgvPDRDTurjzclHzN0GbWdcpOqPZg4ftjkJGaw== - dependencies: - "@ethersproject/abi" "^5.7.0" - "@ethersproject/abstract-provider" "^5.7.0" - "@ethersproject/address" "^5.7.0" - "@ethersproject/bignumber" "^5.7.0" - "@ethersproject/bytes" "^5.7.0" - "@ethersproject/constants" "^5.7.0" - "@ethersproject/contracts" "^5.7.0" - "@ethersproject/keccak256" "^5.7.0" - "@ethersproject/properties" "^5.7.0" - "@ethersproject/rlp" "^5.7.0" - "@ethersproject/web" "^5.7.1" - chai "^4.3.10" - ethers "^5.7.2" - node-fetch "^2.6.7" - "@eth-optimism/core-utils@^0.7.7": version "0.7.7" resolved "https://registry.yarnpkg.com/@eth-optimism/core-utils/-/core-utils-0.7.7.tgz#c993d45d2be7a1956284621ad18129a88880c658" @@ -584,18 +555,6 @@ ethers "^5.5.4" lodash "^4.17.21" -"@eth-optimism/sdk@^3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@eth-optimism/sdk/-/sdk-3.3.1.tgz#f72b6f93b58e2a2943f10aca3be91dfc23d9839f" - integrity sha512-zf8qL+KwYWUUwvdcjF1HpBxgKSt5wsKr8oa6jwqUhdPkQHUtVK5SRKtqXqYplnAgKtxDQYwlK512GU16odEl1w== - dependencies: - "@eth-optimism/contracts" "0.6.0" - "@eth-optimism/core-utils" "0.13.2" - lodash "^4.17.21" - merkletreejs "^0.3.11" - rlp "^2.2.7" - semver "^7.6.0" - "@ethereum-waffle/chai@4.0.10": version "4.0.10" resolved "https://registry.yarnpkg.com/@ethereum-waffle/chai/-/chai-4.0.10.tgz#6f600a40b6fdaed331eba42b8625ff23f3a0e59a" @@ -1088,7 +1047,7 @@ "@ethersproject/transactions" "^5.7.0" "@ethersproject/wordlists" "^5.7.0" -"@ethersproject/web@5.7.1", "@ethersproject/web@^5.5.1", "@ethersproject/web@^5.7.0", "@ethersproject/web@^5.7.1": +"@ethersproject/web@5.7.1", "@ethersproject/web@^5.5.1", "@ethersproject/web@^5.7.0": version "5.7.1" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== @@ -4613,11 +4572,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer-reverse@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-reverse/-/buffer-reverse-1.0.1.tgz#49283c8efa6f901bc01fa3304d06027971ae2f60" - integrity sha1-SSg8jvpvkBvAH6MwTQYCeXGuL2A= - buffer-to-arraybuffer@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz#6064a40fa76eb43c723aba9ef8f6e1216d10511a" @@ -4863,7 +4817,7 @@ chai-exclude@^2.1.0: dependencies: fclone "^1.0.11" -chai@^4.3.0, chai@^4.3.10, chai@^4.3.4, chai@^4.3.8: +chai@^4.3.0, chai@^4.3.4, chai@^4.3.8: version "4.4.1" resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== @@ -5603,11 +5557,6 @@ crypto-browserify@3.12.0: randombytes "^2.0.0" randomfill "^1.0.3" -crypto-js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" - integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== - css-color-names@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67" @@ -10584,17 +10533,6 @@ merkle-patricia-tree@^4.2.2, merkle-patricia-tree@^4.2.4: readable-stream "^3.6.0" semaphore-async-await "^1.5.1" -merkletreejs@^0.3.11: - version "0.3.11" - resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.3.11.tgz#e0de05c3ca1fd368de05a12cb8efb954ef6fc04f" - integrity sha512-LJKTl4iVNTndhL+3Uz/tfkjD0klIWsHlUzgtuNnNrsf7bAlXR30m+xYB7lHr5Z/l6e/yAIsr26Dabx6Buo4VGQ== - dependencies: - bignumber.js "^9.0.1" - buffer-reverse "^1.0.1" - crypto-js "^4.2.0" - treeify "^1.1.0" - web3-utils "^1.3.4" - methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -12792,7 +12730,7 @@ rlp@2.2.6: dependencies: bn.js "^4.11.1" -rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4, rlp@^2.2.7: +rlp@^2.0.0, rlp@^2.2.3, rlp@^2.2.4: version "2.2.7" resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf" integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ== @@ -13010,7 +12948,7 @@ semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.2, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: +semver@^7.0.0, semver@^7.1.2, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -14117,11 +14055,6 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -treeify@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" - integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== - triple-beam@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" @@ -15149,7 +15082,7 @@ web3-utils@1.8.2: randombytes "^2.1.0" utf8 "3.0.0" -web3-utils@^1.0.0-beta.31, web3-utils@^1.2.1, web3-utils@^1.3.0, web3-utils@^1.3.4, web3-utils@^1.3.6: +web3-utils@^1.0.0-beta.31, web3-utils@^1.2.1, web3-utils@^1.3.0, web3-utils@^1.3.6: version "1.10.2" resolved "https://registry.yarnpkg.com/web3-utils/-/web3-utils-1.10.2.tgz#361103d28a94d5e2a87ba15d776a62c33303eb44" integrity sha512-TdApdzdse5YR+5GCX/b/vQnhhbj1KSAtfrDtRW7YS0kcWp1gkJsN62gw6GzCaNTeXookB7UrLtmDUuMv65qgow== From e34e28d281bae643a80445615d7c376b684fe05f Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Wed, 4 Dec 2024 15:41:08 +0100 Subject: [PATCH 13/13] fixup --- yarn.lock | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3bcd8852f..fa1163c46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4842,11 +4842,6 @@ buffer-layout@^1.2.0, buffer-layout@^1.2.2: resolved "https://registry.yarnpkg.com/buffer-layout/-/buffer-layout-1.2.2.tgz#b9814e7c7235783085f9ca4966a0cfff112259d5" integrity sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA== -buffer-reverse@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-reverse/-/buffer-reverse-1.0.1.tgz#49283c8efa6f901bc01fa3304d06027971ae2f60" - integrity sha1-SSg8jvpvkBvAH6MwTQYCeXGuL2A= - buffer-to-arraybuffer@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/buffer-to-arraybuffer/-/buffer-to-arraybuffer-0.0.5.tgz#6064a40fa76eb43c723aba9ef8f6e1216d10511a" @@ -5847,11 +5842,6 @@ crypto-hash@^1.3.0: resolved "https://registry.yarnpkg.com/crypto-hash/-/crypto-hash-1.3.0.tgz#b402cb08f4529e9f4f09346c3e275942f845e247" integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg== -crypto-js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" - integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== - css-color-names@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67"