Ethernauta

@ethernauta/gas — fee estimation (EIP-1559)

estimate_1559_fees({ base_fee_multiplier, priority_percentile }) is the primitive for any EIP-1559 EVM chain that isn't part of an L2 fee family. Mainnet, every testnet, every sidechain — anything that prices gas exclusively via eth_feeHistory lands here.

For chains in an L2 fee family the consumer imports the matching helper directly:

  • OP-stack (Base, Optimism, Mode, Zora, Mantle, World Chain, Soneium, Lisk) → calculate_gas_op_stack.
  • Arbitrum (One, Nova) → calculate_gas_arbitrum.
  • zkSync Era (mainnet, Sepolia) → calculate_gas_zksync.

There is no central dispatcher — the consumer picks the helper the same way they pick a chain. @ethernauta/gas also exports gas_family(chain) and the FamilyForChainId<Id> conditional type for consumers who want to branch on family at runtime or type level.

The 1559 return shape:

  • base_fee_per_gas — read from the latest entry of baseFeePerGas.
  • max_priority_fee_per_gas — averaged from the requested percentile column of reward[][].
  • max_fee_per_gasbase × multiplier + priority, integer math via a 1e6 fixed-point scale.

The multiplier and the percentile are caller knobs. The library ships no default — the dapp / wallet picks what they want.

Pick a wallet first (try the EIP-6963 example).
View on GitHub

The code

// Calls `estimate_1559_fees(parameters)` directly. Mainnet (and any
// other EIP-1559 EVM chain that isn't an L2 family member) takes the
// vanilla `eth_feeHistory` path — no L1 surcharge to factor in.
// L2 demos (op-stack / arbitrum / zksync) import their own family
// helper from `@ethernauta/gas`; there is no central dispatcher.

import "./demo.css"
import { eip155_1 } from "@ethernauta/chain/eip155-1"
import type { Uint } from "@ethernauta/core"
import { estimate_1559_fees } from "@ethernauta/gas"
import { useProvider } from "@ethernauta/react"
import { encode_chain_id } from "@ethernauta/transport"
import { hex_to_bigint } from "@ethernauta/utils"
import { useState } from "react"
import { Button } from "../../components/button"
import { PROVIDER_STORE_KEY } from "../../lib/provider-store"

// The injected provider answers from its own selected-chain
// state — this CAIP-2 id just satisfies the resolver context.
const DISCOVERY_CHAIN_ID = encode_chain_id({
  namespace: "eip155",
  reference: eip155_1.chainId,
})

export function GasEstimateDemo() {
  const provider = useProvider({ key: PROVIDER_STORE_KEY })
  const [multiplier, set_multiplier] = useState(1.5)
  const [percentile, set_percentile] = useState(10)
  const [base_fee, set_base_fee] = useState<Uint | null>(
    null,
  )
  const [priority, set_priority] = useState<Uint | null>(
    null,
  )
  const [max_fee, set_max_fee] = useState<Uint | null>(null)
  const [error, set_error] = useState<string | null>(null)
  const [in_flight, set_in_flight] = useState(false)

  if (!provider) {
    return (
      <div className="gas-estimate-empty">
        Pick a wallet first (try the <code>EIP-6963</code>{" "}
        example).
      </div>
    )
  }

  async function run() {
    if (!provider) return
    set_in_flight(true)
    set_error(null)
    try {
      const fees = await estimate_1559_fees({
        base_fee_multiplier: multiplier,
        priority_percentile: percentile,
      })(provider.reader({ chain_id: DISCOVERY_CHAIN_ID }))
      set_base_fee(fees.base_fee_per_gas)
      set_priority(fees.max_priority_fee_per_gas)
      set_max_fee(fees.max_fee_per_gas)
    } catch (e) {
      set_error(
        e instanceof Error ? e.message : "Unknown error",
      )
    } finally {
      set_in_flight(false)
    }
  }

  return (
    <div className="gas-estimate-card">
      <KnobRow
        label="base_fee_multiplier"
        value={multiplier}
        step={0.1}
        min={1}
        max={3}
        onChange={set_multiplier}
      />
      <KnobRow
        label="priority_percentile"
        value={percentile}
        step={5}
        min={1}
        max={99}
        onChange={set_percentile}
      />
      <Button onClick={run} disabled={in_flight}>
        {in_flight ? "Estimating…" : "Estimate 1559 fees"}
      </Button>
      <ResultRow
        label="base_fee_per_gas"
        value={base_fee}
      />
      <ResultRow
        label="max_priority_fee_per_gas"
        value={priority}
      />
      <ResultRow label="max_fee_per_gas" value={max_fee} />
      {error && (
        <div className="gas-estimate-error">{error}</div>
      )}
    </div>
  )
}

function KnobRow({
  label,
  value,
  step,
  min,
  max,
  onChange,
}: {
  label: string
  value: number
  step: number
  min: number
  max: number
  onChange: (_value: number) => void
}) {
  return (
    <label className="gas-estimate-knob-row">
      <span className="gas-estimate-knob-label">
        {label}
      </span>
      <input
        type="number"
        value={value}
        step={step}
        min={min}
        max={max}
        onChange={(e) =>
          onChange(Number(e.currentTarget.value))
        }
        className="gas-estimate-knob-input"
      />
    </label>
  )
}

function ResultRow({
  label,
  value,
}: {
  label: string
  value: Uint | null
}) {
  return (
    <div className="gas-estimate-result-row">
      <span className="gas-estimate-result-label">
        {label}
      </span>
      <span className="gas-estimate-result-value">
        {value
          ? `${value} (${hex_to_bigint(value).toLocaleString("en-US")})`
          : "—"}
      </span>
    </div>
  )
}

Path-2 (no wallet) is the same call

Same primitive against a public RPC reader — only the transport-construction line changes:

// Calls `estimate_1559_fees(parameters)` directly. Mainnet (and any
// other EIP-1559 EVM chain that isn't an L2 family member) takes the
// vanilla `eth_feeHistory` path — no L1 surcharge to factor in.
// L2 demos (op-stack / arbitrum / zksync) import their own family
// helper from `@ethernauta/gas`; there is no central dispatcher.

import "./demo.css"
import { eip155_1 } from "@ethernauta/chain/eip155-1"
import type { Uint } from "@ethernauta/core"
import { estimate_1559_fees } from "@ethernauta/gas"
import { useProvider } from "@ethernauta/react"
import { encode_chain_id } from "@ethernauta/transport"
import { hex_to_bigint } from "@ethernauta/utils"
import { useState } from "react"
import { Button } from "../../components/button"
import { PROVIDER_STORE_KEY } from "../../lib/provider-store"

// The injected provider answers from its own selected-chain
// state — this CAIP-2 id just satisfies the resolver context.
const DISCOVERY_CHAIN_ID = encode_chain_id({
  namespace: "eip155",
  reference: eip155_1.chainId,
})

export function GasEstimateDemo() {
  const provider = useProvider({ key: PROVIDER_STORE_KEY })
  const [multiplier, set_multiplier] = useState(1.5)
  const [percentile, set_percentile] = useState(10)
  const [base_fee, set_base_fee] = useState<Uint | null>(
    null,
  )
  const [priority, set_priority] = useState<Uint | null>(
    null,
  )
  const [max_fee, set_max_fee] = useState<Uint | null>(null)
  const [error, set_error] = useState<string | null>(null)
  const [in_flight, set_in_flight] = useState(false)

  if (!provider) {
    return (
      <div className="gas-estimate-empty">
        Pick a wallet first (try the <code>EIP-6963</code>{" "}
        example).
      </div>
    )
  }

  async function run() {
    if (!provider) return
    set_in_flight(true)
    set_error(null)
    try {
      const fees = await estimate_1559_fees({
        base_fee_multiplier: multiplier,
        priority_percentile: percentile,
      })(provider.reader({ chain_id: DISCOVERY_CHAIN_ID }))
      set_base_fee(fees.base_fee_per_gas)
      set_priority(fees.max_priority_fee_per_gas)
      set_max_fee(fees.max_fee_per_gas)
    } catch (e) {
      set_error(
        e instanceof Error ? e.message : "Unknown error",
      )
    } finally {
      set_in_flight(false)
    }
  }

  return (
    <div className="gas-estimate-card">
      <KnobRow
        label="base_fee_multiplier"
        value={multiplier}
        step={0.1}
        min={1}
        max={3}
        onChange={set_multiplier}
      />
      <KnobRow
        label="priority_percentile"
        value={percentile}
        step={5}
        min={1}
        max={99}
        onChange={set_percentile}
      />
      <Button onClick={run} disabled={in_flight}>
        {in_flight ? "Estimating…" : "Estimate 1559 fees"}
      </Button>
      <ResultRow
        label="base_fee_per_gas"
        value={base_fee}
      />
      <ResultRow
        label="max_priority_fee_per_gas"
        value={priority}
      />
      <ResultRow label="max_fee_per_gas" value={max_fee} />
      {error && (
        <div className="gas-estimate-error">{error}</div>
      )}
    </div>
  )
}

function KnobRow({
  label,
  value,
  step,
  min,
  max,
  onChange,
}: {
  label: string
  value: number
  step: number
  min: number
  max: number
  onChange: (_value: number) => void
}) {
  return (
    <label className="gas-estimate-knob-row">
      <span className="gas-estimate-knob-label">
        {label}
      </span>
      <input
        type="number"
        value={value}
        step={step}
        min={min}
        max={max}
        onChange={(e) =>
          onChange(Number(e.currentTarget.value))
        }
        className="gas-estimate-knob-input"
      />
    </label>
  )
}

function ResultRow({
  label,
  value,
}: {
  label: string
  value: Uint | null
}) {
  return (
    <div className="gas-estimate-result-row">
      <span className="gas-estimate-result-label">
        {label}
      </span>
      <span className="gas-estimate-result-value">
        {value
          ? `${value} (${hex_to_bigint(value).toLocaleString("en-US")})`
          : "—"}
      </span>
    </div>
  )
}