gasOracle.ts
Overview
The `gasOracle.ts` file implements a **Gas Oracle** for Ethereum Virtual Machine (EVM) compatible blockchains. Its main purpose is to collect, analyze, and estimate gas fees based on recent blockchain data to provide accurate and dynamic gas fee recommendations.
Gas fees on EVM chains fluctuate with network demand, and this module continuously tracks recent blocks, extracting gas prices and priority fees from transactions to generate percentile-based fee estimates (e.g., slow, average, fast). It supports multiple chains (Ethereum, Avalanche, Polygon, Optimism, etc.) with chain-specific adaptations such as different RPC querying behaviors and fee buffering.
The Gas Oracle maintains an internal sliding window cache of recent block fee data, synchronizes with the blockchain state, and updates dynamically as new blocks arrive (e.g., via WebSocket events). It provides methods to start the oracle, process new blocks, estimate fees at requested percentiles, and expose the current base fee per gas.
Classes and Interfaces
Interfaces
GasOracleArgs
interface GasOracleArgs { logger: Logger client: PublicClient coinstack: string totalBlocks?: number }Purpose: Input parameters for creating a
GasOracleinstance.Parameters:
logger: A logger instance for diagnostics.client: A blockchain client (viem'sPublicClient) for RPC calls.coinstack: The target blockchain identifier (e.g., 'ethereum', 'avalanche').totalBlocks(optional): Number of recent blocks to track (default 20).
TxFees
interface TxFees { gasPrices: number[] maxPriorityFees: number[] }Purpose: Holds arrays of gas prices and max priority fees extracted from transactions.
BlockFees
interface BlockFees extends TxFees { pending: boolean baseFeePerGas?: number }Purpose: Extends
TxFeeswith meta information about a block:pending: Whether the block is pending.baseFeePerGas: The base fee per gas unit for the block, if available.
EstimatedFees
type EstimatedFees = Record<string, Fees>Maps percentile strings (e.g.,
"1","60","90") toFees(gas fee estimates).
BlockTag
type BlockTag = 'latest' | 'pending'Represents block identifiers for querying.
Class: GasOracle
The core class responsible for managing gas fee data collection and estimation.
class GasOracle {
// Properties
public readonly coinstack: string
private readonly totalBlocks: number
private readonly logger: Logger
private readonly client: PublicClient
private feesByBlock: Map<string, BlockFees>
private newBlocksQueue: NewBlock[]
private baseFeePerGas?: string
private baseFeeBuffer: boolean
private latestBlockTag: BlockTag
private canQueryPendingBlockByHeight: boolean
// Constructor
constructor(args: GasOracleArgs)
// Methods
async start(): Promise<void>
getBaseFeePerGas(): string | undefined
async estimateFees(percentiles: number[], blockCount?: number): Promise<EstimatedFees>
async onBlock(newBlock: NewBlock): Promise<void>
async processBlocks(): Promise<void>
// Private methods
private async update(blockNumber: number, blockTag?: BlockTag, retryCount?: number): Promise<void>
private async sync(): Promise<void>
private averageAtPercentile(blockFees: BlockFees[], percentile: number): { gasPrice: number, maxPriorityFee: number }
}
Constructor
constructor(args: GasOracleArgs)
Purpose: Initializes the oracle with chain-specific configurations and sets up internal state.
Parameters:
args.logger: Logger instance scoped togasOracle.args.client: Blockchain RPC client.args.coinstack: Chain identifier.args.totalBlocks: (optional) Number of blocks to track.
Behavior:
Sets chain-specific parameters:
baseFeeBuffer: Whether to apply a buffer multiplier to the base fee (e.g., double for Optimism).latestBlockTag: Which block tag to query ('latest' or 'pending').canQueryPendingBlockByHeight: Whether pending blocks can be queried by block number.
Throws an error if the chain is unsupported.
**Example:**
const gasOracle = new GasOracle({
logger: myLogger,
client: viemClient,
coinstack: 'ethereum',
totalBlocks: 30,
})
Method: start
async start(): Promise<void>
Purpose: Initializes the oracle by loading recent blocks and starts processing new blocks.
Behavior:
Fetches the current block number.
Loads detailed fee info for the last
totalBlocksblocks.Begins the async loop to process new blocks from the queue.
Method: getBaseFeePerGas
getBaseFeePerGas(): string | undefined
Purpose: Returns the latest known base fee per gas as a string or
undefinedif unavailable.
Method: estimateFees
async estimateFees(percentiles: number[], blockCount = 20): Promise<EstimatedFees>
Purpose: Estimates gas fees at given percentiles based on cached block data.
Parameters:
percentiles: Array of percentiles (e.g.,[1, 60, 90]) to calculate fees for.blockCount: Number of recent blocks to include in calculation (default 20).
Returns: A mapping from percentile strings to
Feescontaining gas price and, if available, EIP-1559 fee fields.Behavior:
Synchronizes state to ensure freshness.
Collects recent
blockCountblock fees.Calculates average gas price and max priority fee at each percentile using
averageAtPercentile.Applies base fee buffering if configured.
Usage Example:
const fees = await gasOracle.estimateFees([1, 60, 90])
console.log(fees['60']) // fee estimate at 60th percentile
Method: onBlock
async onBlock(newBlock: NewBlock): Promise<void>
Purpose: WebSocket (or similar) handler to enqueue new blocks for processing.
Parameters:
newBlock: A new block object received from the blockchain event stream.
Behavior:
Adds the new block to the internal queue
newBlocksQueue.
Method: processBlocks
async processBlocks(): Promise<void>
Purpose: Processes new blocks sequentially from the queue.
Behavior:
If no new block in the queue, waits and retries after 1 second.
Otherwise, calls
sync()to update internal state with the new block.Logs errors on failure but continues processing.
Private Method: update
private async update(blockNumber: number, blockTag?: BlockTag, retryCount = 0): Promise<void>
Purpose: Fetches and processes fee data for a specific block.
Parameters:
blockNumber: Target block number.blockTag: Optional block tag ('latest'or'pending').retryCount: Current retry attempt count.
Behavior:
Retrieves block data with transactions from the blockchain client.
Extracts transaction gas prices and max priority fees.
Stores sorted fees into
feesByBlockmap.Updates
baseFeePerGasif the block corresponds to the latest queried block tag.Retries with exponential backoff up to 5 times on failures.
Notes:
Handles chain-specific behaviors for querying blocks and fetching base fees (e.g., Avalanche uses a separate RPC method for base fee).
Private Method: sync
private async sync(): Promise<void>
Purpose: Synchronizes oracle state by ensuring the fee cache contains the latest
totalBlocksand pruning outdated blocks.Behavior:
Fetches the latest block number.
Iterates over expected block range, updating missing or pending blocks.
Removes blocks older than the sliding window.
Logs errors but continues operation.
Private Method: averageAtPercentile
private averageAtPercentile(blockFees: BlockFees[], percentile: number): { gasPrice: number, maxPriorityFee: number }
Purpose: Calculates the average gas price and max priority fee at a specified percentile across multiple blocks.
Parameters:
blockFees: Array of block fee data.percentile: Percentile to compute (e.g., 60).
Returns: Object with
gasPriceandmaxPriorityFeeaverages.Algorithm:
Defines a helper to get the value at percentile rank within a sorted fee array.
Calculates thresholds to filter out outliers using
getFeeThreshold.Sums the percentile values across blocks for gas prices and priority fees below thresholds.
Computes average by dividing sums by number of blocks.
Important: Outlier filtering improves estimate stability by ignoring extreme gas fee values.
Utility Function: getFeeThreshold
const getFeeThreshold = (arr: number[]): number => {
const magnitudeThreshold = 10
arr = arr.sort((a, b) => a - b)
const mid = Math.floor(arr.length / 2)
const median = arr.length % 2 === 0 ? (arr[mid - 1] + arr[mid]) / 2 : arr[mid]
return median * magnitudeThreshold
}
Purpose: Calculates an outlier threshold for fees based on median value multiplied by a factor.
Usage: Used to filter excessive gas prices or priority fees before averaging.
Important Implementation Details and Algorithms
Chain-Specific Configuration: The constructor configures behavior based on the
coinstackto:Select the appropriate block tag (
latestorpending) for queries.Decide whether to buffer the base fee (e.g., Optimism doubles it).
Determine if pending blocks can be queried by block height.
Sliding Window of Blocks: The oracle tracks a fixed number of recent blocks (default 20), pruning older blocks to limit memory and focus on recent network conditions.
Percentile Fee Estimation: Gas fees are estimated by calculating average gas prices at requested percentiles, after filtering outliers. This statistical approach balances responsiveness and stability.
Retry Logic with Exponential Backoff: The
updatemethod retries fetching block data up to 5 times with increasing delays to handle temporary RPC failures gracefully.Block Processing Queue: New blocks received via
onBlockare queued and processed sequentially to avoid race conditions.Base Fee Management: The oracle maintains the latest base fee per gas, which can be fetched directly from blocks or via a chain-specific RPC method (e.g., Avalanche).
Interaction with Other System Components
Blockchain Client (
viemPublicClient): The oracle depends on a blockchain RPC client to fetch block data and base fees.Logger: Used throughout for error reporting and diagnostic messages.
WebSocket or Event Source: The oracle expects new block notifications (
NewBlock) to be pushed viaonBlock(), typically from a WebSocket subscription managed externally.API Service Layer: An external API service integrates the oracle to serve gas fee estimates to clients (e.g., wallets, UIs) for setting transaction fees.
Fee Models: Uses a
Feesmodel to standardize fee data structures returned byestimateFees.
Visual Diagram: Class Structure of GasOracle
classDiagram
class GasOracle {
+coinstack: string
-totalBlocks: number
-logger: Logger
-client: PublicClient
-feesByBlock: Map<string, BlockFees>
-newBlocksQueue: NewBlock[]
-baseFeePerGas?: string
-baseFeeBuffer: boolean
-latestBlockTag: BlockTag
-canQueryPendingBlockByHeight: boolean
+constructor(args: GasOracleArgs)
+start(): Promise<void>
+getBaseFeePerGas(): string | undefined
+estimateFees(percentiles: number[], blockCount?: number): Promise<EstimatedFees>
+onBlock(newBlock: NewBlock): Promise<void>
+processBlocks(): Promise<void>
-update(blockNumber: number, blockTag?: BlockTag, retryCount?: number): Promise<void>
-sync(): Promise<void>
-averageAtPercentile(blockFees: BlockFees[], percentile: number): { gasPrice: number, maxPriorityFee: number }
}
Usage Example
import { GasOracle } from './gasOracle'
import { Logger } from '@shapeshiftoss/logger'
import { PublicClient } from 'viem'
// Initialize dependencies
const logger = new Logger({ level: 'info' })
const client = new PublicClient({ /* ...config... */ })
// Create oracle instance for Ethereum
const gasOracle = new GasOracle({
logger,
client,
coinstack: 'ethereum',
totalBlocks: 20,
})
// Start the oracle
await gasOracle.start()
// Subscribe to new blocks (e.g., via WebSocket)
async function onNewBlock(newBlock: NewBlock) {
await gasOracle.onBlock(newBlock)
}
// Estimate fees at 10th, 50th and 90th percentiles
const fees = await gasOracle.estimateFees([10, 50, 90])
console.log('Fee estimates:', fees)
// Get current base fee per gas
const baseFee = gasOracle.getBaseFeePerGas()
console.log('Current base fee per gas:', baseFee)
Summary
The `gasOracle.ts` file implements a robust and extensible gas fee oracle for EVM-compatible blockchains. It continuously ingests recent block data, extracts transaction fees, filters outliers, and calculates percentile-based gas fee estimates. Through chain-specific adaptations, retry logic, and event-driven block processing, it provides reliable and up-to-date gas price recommendations critical for transaction fee management in blockchain applications.
This module integrates with blockchain RPC clients and event sources, serving as a foundational component for higher-level API services and user-facing wallets that require dynamic gas fee estimation.
Appendix: Core Workflow Flowchart
flowchart TD
Start[Start Oracle] --> FetchBlocks[Fetch Latest N Blocks]
FetchBlocks --> ExtractFees[Extract Tx Gas Prices & Priority Fees]
ExtractFees --> UpdateCache[Update Fee Cache per Block]
UpdateCache --> SyncState[Sync & Prune Cache]
SyncState --> Estimate[Estimate Fees at Requested Percentiles]
Estimate --> ReturnFees[Return Fee Estimates]
ReturnFees --> AwaitNewBlocks[Wait for New Blocks]
AwaitNewBlocks -->|New Block via WebSocket| EnqueueBlock[Enqueue New Block]
EnqueueBlock --> ProcessBlock[Process Next Block]
ProcessBlock --> UpdateCache
ProcessBlock --> SyncState
SyncState --> Estimate
This diagram illustrates the continuous cycle of data collection, caching, synchronization, and fee estimation in the Gas Oracle.
If you need further clarification or examples, feel free to ask!