Skip to content

Commit

Permalink
Add unit tests, refactor all methods
Browse files Browse the repository at this point in the history
  • Loading branch information
nicholaspai committed Dec 26, 2024
1 parent e913c75 commit ea1ee66
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 201 deletions.
13 changes: 3 additions & 10 deletions src/gasPriceOracle/adapters/arbitrum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,12 @@ import { providers } from "ethers";
import { bnOne } from "../../utils";
import { GasPriceEstimate } from "../types";
import * as ethereum from "./ethereum";
import { GasPriceEstimateOptions } from "../oracle";

// Arbitrum Nitro implements EIP-1559 pricing, but the priority fee is always refunded to the caller.
// Reference: https://docs.arbitrum.io/how-arbitrum-works/gas-fees
export async function eip1559(
provider: providers.Provider,
chainId: number,
baseFeeMultiplier: number
): Promise<GasPriceEstimate> {
const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await ethereum.eip1559(
provider,
chainId,
baseFeeMultiplier
);
export async function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise<GasPriceEstimate> {
const { maxFeePerGas: _maxFeePerGas, maxPriorityFeePerGas } = await ethereum.eip1559(provider, opts);

// eip1559() sets maxFeePerGas = lastBaseFeePerGas + maxPriorityFeePerGas, so revert that.
// The caller may apply scaling as they wish afterwards.
Expand Down
20 changes: 8 additions & 12 deletions src/gasPriceOracle/adapters/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@ import { providers } from "ethers";
import { BigNumber, bnZero, getNetworkName } from "../../utils";
import { GasPriceEstimate } from "../types";
import { gasPriceError } from "../util";
import { GasPriceEstimateOptions } from "../oracle";

/**
* @param provider ethers RPC provider instance.
* @param chainId Chain ID of provider instance.
* @returns Promise of gas price estimate object.
*/
export function eip1559(
provider: providers.Provider,
chainId: number,
baseFeeMultiplier: number
): Promise<GasPriceEstimate> {
const useRaw = process.env[`GAS_PRICE_EIP1559_RAW_${chainId}`] === "true";
return useRaw ? eip1559Raw(provider, chainId, baseFeeMultiplier) : eip1559Bad(provider, chainId, baseFeeMultiplier);
export function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise<GasPriceEstimate> {
const useRaw = process.env[`GAS_PRICE_EIP1559_RAW_${opts.chainId}`] === "true";
return useRaw
? eip1559Raw(provider, opts.chainId, opts.baseFeeMultiplier)
: eip1559Bad(provider, opts.chainId, opts.baseFeeMultiplier);
}

/**
Expand Down Expand Up @@ -67,11 +66,8 @@ export async function eip1559Bad(
return { maxPriorityFeePerGas, maxFeePerGas };
}

export async function legacy(
provider: providers.Provider,
chainId: number,
baseFeeMultiplier: number
): Promise<GasPriceEstimate> {
export async function legacy(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise<GasPriceEstimate> {
const { chainId, baseFeeMultiplier } = opts;
const gasPrice = await provider.getGasPrice();

if (!BigNumber.isBigNumber(gasPrice) || gasPrice.lt(bnZero)) gasPriceError("getGasPrice()", chainId, gasPrice);
Expand Down
15 changes: 7 additions & 8 deletions src/gasPriceOracle/adapters/linea-viem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Address, Hex, PublicClient } from "viem";
import { estimateGas } from "viem/linea";
import { DEFAULT_SIMULATED_RELAYER_ADDRESS as account } from "../../constants";
import { InternalGasPriceEstimate } from "../types";
import { PopulatedTransaction } from "ethers";
import { GasPriceEstimateOptions } from "../oracle";

/**
* @notice The Linea viem provider calls the linea_estimateGas RPC endpoint to estimate gas. Linea is unique
Expand All @@ -21,15 +21,14 @@ import { PopulatedTransaction } from "ethers";
*/
export async function eip1559(
provider: PublicClient,
_chainId: number,
baseFeeMultiplier: number,
_unsignedTx?: PopulatedTransaction
opts: GasPriceEstimateOptions
): Promise<InternalGasPriceEstimate> {
const { unsignedTx, baseFeeMultiplier } = opts;
const { baseFeePerGas, priorityFeePerGas: _priorityFeePerGas } = await estimateGas(provider, {
account: (_unsignedTx?.from as Address) ?? account,
to: (_unsignedTx?.to as Address) ?? account,
value: BigInt(_unsignedTx?.value?.toString() ?? "1"),
data: (_unsignedTx?.data as Hex) ?? "0x",
account: (unsignedTx?.from as Address) ?? account,
to: (unsignedTx?.to as Address) ?? account,
value: BigInt(unsignedTx?.value?.toString() ?? "1"),
data: (unsignedTx?.data as Hex) ?? "0x",
});
const priorityFeePerGas = _priorityFeePerGas * BigInt(baseFeeMultiplier);

Expand Down
9 changes: 3 additions & 6 deletions src/gasPriceOracle/adapters/linea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@
import { providers } from "ethers";
import { GasPriceEstimate } from "../types";
import * as ethereum from "./ethereum";
import { GasPriceEstimateOptions } from "../oracle";

export function eip1559(
provider: providers.Provider,
chainId: number,
baseFeeMultiplier: number
): Promise<GasPriceEstimate> {
return ethereum.legacy(provider, chainId, baseFeeMultiplier);
export function eip1559(provider: providers.Provider, opts: GasPriceEstimateOptions): Promise<GasPriceEstimate> {
return ethereum.legacy(provider, opts);
}
29 changes: 24 additions & 5 deletions src/gasPriceOracle/adapters/polygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CHAIN_IDs } from "../../constants";
import { GasPriceEstimate } from "../types";
import { gasPriceError } from "../util";
import { eip1559 } from "./ethereum";
import { GasPriceEstimateOptions } from "../oracle";

type Polygon1559GasPrice = {
maxPriorityFee: number | string;
Expand All @@ -27,7 +28,7 @@ type GasStationArgs = BaseHTTPAdapterArgs & {

const { POLYGON } = CHAIN_IDs;

class PolygonGasStation extends BaseHTTPAdapter {
export class PolygonGasStation extends BaseHTTPAdapter {
readonly chainId: number;

constructor({ chainId = POLYGON, host, timeout = 1500, retries = 1 }: GasStationArgs = {}) {
Expand Down Expand Up @@ -67,12 +68,30 @@ class PolygonGasStation extends BaseHTTPAdapter {
}
}

export class MockPolygonGasStation extends PolygonGasStation {
constructor(
readonly baseFee: BigNumber,
readonly priorityFee: BigNumber,
readonly getFeeDataThrows = false
) {
super();
}

getFeeData(): Promise<GasPriceEstimate> {
if (this.getFeeDataThrows) throw new Error();
return Promise.resolve({
maxPriorityFeePerGas: this.priorityFee,
maxFeePerGas: this.baseFee.add(this.priorityFee),
});
}
}

export async function gasStation(
provider: providers.Provider,
chainId: number,
baseFeeMultiplier: number
opts: GasPriceEstimateOptions
): Promise<GasPriceEstimate> {
const gasStation = new PolygonGasStation({ chainId: chainId, timeout: 2000, retries: 0 });
const { chainId, baseFeeMultiplier, polygonGasStation } = opts;
const gasStation = polygonGasStation ?? new PolygonGasStation({ chainId: chainId, timeout: 2000, retries: 0 });
let maxPriorityFeePerGas: BigNumber;
let maxFeePerGas: BigNumber;
try {
Expand All @@ -82,7 +101,7 @@ export async function gasStation(
maxFeePerGas = scaledBaseFee.add(maxPriorityFeePerGas);
} catch (err) {
// Fall back to the RPC provider. May be less accurate.
({ maxPriorityFeePerGas, maxFeePerGas } = await eip1559(provider, chainId, baseFeeMultiplier));
({ maxPriorityFeePerGas, maxFeePerGas } = await eip1559(provider, opts));

// Per the GasStation docs, the minimum priority fee on Polygon is 30 Gwei.
// https://docs.polygon.technology/tools/gas/polygon-gas-station/#interpretation
Expand Down
72 changes: 24 additions & 48 deletions src/gasPriceOracle/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,24 @@ import * as ethereum from "./adapters/ethereum";
import * as linea from "./adapters/linea";
import * as polygon from "./adapters/polygon";
import * as lineaViem from "./adapters/linea-viem";
import { PolygonGasStation } from "./adapters/polygon";

interface GasPriceEstimateOptions {
export interface GasPriceEstimateOptions {
// baseFeeMultiplier Multiplier applied to base fee for EIP1559 gas prices (or total fee for legacy).
baseFeeMultiplier: number;
// legacyFallback In the case of an unrecognized chain, fall back to type 0 gas estimation.
legacyFallback: boolean;
// chainId The chain ID to query for gas prices. If omitted can be inferred by provider.
chainId?: number;
// unsignedTx The unsigned transaction used for simulation by Viem provider to produce the priority gas fee.
chainId: number;
// unsignedTx The unsigned transaction used for simulation by Linea's Viem provider to produce the priority gas fee.
unsignedTx?: PopulatedTransaction;
// transport Viem Transport object to use for querying gas fees.
// transport Viem Transport object to use for querying gas fees used for testing.
transport?: Transport;
// polygonGasStation Custom Polygon GasStation class used for testing.
polygonGasStation?: PolygonGasStation;
}

interface EthersGasPriceEstimateOptions extends GasPriceEstimateOptions {
chainId: number;
}

interface ViemGasPriceEstimateOptions extends Partial<GasPriceEstimateOptions> {
baseFeeMultiplier: number;
}

const GAS_PRICE_ESTIMATE_DEFAULTS: GasPriceEstimateOptions = {
const GAS_PRICE_ESTIMATE_DEFAULTS = {
baseFeeMultiplier: 1,
legacyFallback: true,
};
Expand All @@ -47,32 +42,23 @@ export async function getGasPriceEstimate(
provider: providers.Provider,
opts: Partial<GasPriceEstimateOptions>
): Promise<GasPriceEstimate> {
const {
baseFeeMultiplier,
chainId: _chainId,
unsignedTx,
transport,
legacyFallback,
}: GasPriceEstimateOptions = {
...GAS_PRICE_ESTIMATE_DEFAULTS,
...opts,
};
const baseFeeMultiplier = opts.baseFeeMultiplier ?? GAS_PRICE_ESTIMATE_DEFAULTS.baseFeeMultiplier;
assert(
baseFeeMultiplier >= 1.0 && baseFeeMultiplier <= 5,
`Require 1.0 < base fee multiplier (${baseFeeMultiplier}) <= 5.0 for a total gas multiplier within [+1.0, +5.0]`
);

const chainId = _chainId ?? (await provider.getNetwork()).chainId;
const chainId = opts.chainId ?? (await provider.getNetwork()).chainId;
const optsWithDefaults: GasPriceEstimateOptions = {
...GAS_PRICE_ESTIMATE_DEFAULTS,
...opts,
chainId,
};

// We only use the unsignedTx in the viem flow.
const useViem = process.env[`NEW_GAS_PRICE_ORACLE_${chainId}`] === "true";
return useViem
? _getViemGasPriceEstimate(chainId, { baseFeeMultiplier, unsignedTx, transport })
: _getEthersGasPriceEstimate(provider, {
baseFeeMultiplier,
chainId,
legacyFallback,
});
? _getViemGasPriceEstimate(chainId, optsWithDefaults)
: _getEthersGasPriceEstimate(provider, optsWithDefaults);
}

/**
Expand All @@ -84,9 +70,9 @@ export async function getGasPriceEstimate(
*/
function _getEthersGasPriceEstimate(
provider: providers.Provider,
opts: EthersGasPriceEstimateOptions
opts: GasPriceEstimateOptions
): Promise<GasPriceEstimate> {
const { baseFeeMultiplier, chainId, legacyFallback } = opts;
const { chainId, legacyFallback } = opts;

const gasPriceFeeds = {
[CHAIN_IDs.ALEPH_ZERO]: arbitrum.eip1559,
Expand All @@ -102,7 +88,7 @@ function _getEthersGasPriceEstimate(
assert(gasPriceFeed || legacyFallback, `No suitable gas price oracle for Chain ID ${chainId}`);
gasPriceFeed ??= chainIsOPStack(chainId) ? ethereum.eip1559 : ethereum.legacy;

return gasPriceFeed(provider, chainId, baseFeeMultiplier);
return gasPriceFeed(provider, opts);
}

/**
Expand All @@ -114,35 +100,25 @@ function _getEthersGasPriceEstimate(
*/
export async function _getViemGasPriceEstimate(
providerOrChainId: providers.Provider | number,
opts: ViemGasPriceEstimateOptions
opts: GasPriceEstimateOptions
): Promise<GasPriceEstimate> {
const { baseFeeMultiplier, unsignedTx, transport } = opts;
const { baseFeeMultiplier, transport } = opts;

const chainId =
typeof providerOrChainId === "number" ? providerOrChainId : (await providerOrChainId.getNetwork()).chainId;
const viemProvider = getPublicClient(chainId, transport);

const gasPriceFeeds: Record<
number,
(
provider: PublicClient,
chainId: number,
baseFeeMultiplier: number,
unsignedTx?: PopulatedTransaction
) => Promise<InternalGasPriceEstimate>
(provider: PublicClient, opts: GasPriceEstimateOptions) => Promise<InternalGasPriceEstimate>
> = {
[CHAIN_IDs.LINEA]: lineaViem.eip1559,
} as const;

let maxFeePerGas: bigint;
let maxPriorityFeePerGas: bigint;
if (gasPriceFeeds[chainId]) {
({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId](
viemProvider,
chainId,
baseFeeMultiplier,
unsignedTx
));
({ maxFeePerGas, maxPriorityFeePerGas } = await gasPriceFeeds[chainId](viemProvider, opts));
} else {
let gasPrice: bigint | undefined;
({ maxFeePerGas, maxPriorityFeePerGas, gasPrice } = await viemProvider.estimateFeesPerGas());
Expand Down
1 change: 1 addition & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getGasPriceEstimate } from "../gasPriceOracle";
import { BigNumber, BigNumberish, BN, bnZero, 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;
Expand Down
Loading

0 comments on commit ea1ee66

Please sign in to comment.