Ethernauta

Gas estimation — Arbitrum

calculate_gas_arbitrum({ tx }) issues a single eth_call against the Nitro NodeInterface precompile at 0x00000000000000000000000000000000000000C8. The method gasEstimateComponents(to, false, data) is virtual — the node intercepts it and returns the four numbers Arbitrum cares about in one shot:

  • gasEstimate — total gas units needed.
  • gasEstimateForL1 — the portion that pays for posting the transaction's calldata to L1.
  • baseFee — the L2 base fee at quote time.
  • l1BaseFeeEstimate — ArbOS's current estimate of the L1 base fee it'll be charged.

The coarse helper drops gasEstimateForL1 (we already have the total) and surfaces { kind: "arbitrum", gas_estimate, l1_base_fee_estimate, l2_base_fee }.

gas_estimate
l1_base_fee_estimate
l2_base_fee
View on GitHub

The code

// `calculate_gas_arbitrum({ tx })` against Arbitrum One. Single read
// of the NodeInterface precompile's
// gasEstimateComponents(to, false, data) — the Nitro node hands back
// the L2 execution + L1 batch-posting split in one shot.

import "./demo.css"
import { eip155_42161 } from "@ethernauta/chain/eip155-42161"
import { AddressSchema, type Uint } from "@ethernauta/core"
import { calculate_gas_arbitrum } from "@ethernauta/gas"
import {
  create_reader,
  encode_chain_id,
  http,
} from "@ethernauta/transport"
import { hex_to_bigint } from "@ethernauta/utils"
import { useState } from "react"
import { parse } from "valibot"

import { Button } from "../../components/button"

const CHAIN_ID = encode_chain_id({
  namespace: "eip155",
  reference: eip155_42161.chainId,
})

const reader = create_reader([
  {
    chainId: CHAIN_ID,
    transports: [http("https://arb1.arbitrum.io/rpc")],
  },
])

const DEFAULT_TO = parse(
  AddressSchema,
  "0x000000000000000000000000000000000000dEaD",
)

type Fees = {
  gas_estimate: Uint
  l1_base_fee_estimate: Uint
  l2_base_fee: Uint
}

export function GasEstimateArbitrumDemo() {
  const [fees, set_fees] = useState<Fees | null>(null)
  const [error, set_error] = useState<string | null>(null)
  const [in_flight, set_in_flight] = useState(false)

  async function run() {
    set_in_flight(true)
    set_error(null)
    try {
      const result = await calculate_gas_arbitrum({
        tx: { to: DEFAULT_TO },
      })(reader({ chain_id: CHAIN_ID }))
      set_fees({
        gas_estimate: result.gas_estimate,
        l1_base_fee_estimate: result.l1_base_fee_estimate,
        l2_base_fee: result.l2_base_fee,
      })
    } catch (e) {
      set_error(
        e instanceof Error ? e.message : "Unknown error",
      )
    } finally {
      set_in_flight(false)
    }
  }

  return (
    <div className="gas-estimate-arbitrum-card">
      <Button onClick={run} disabled={in_flight}>
        {in_flight
          ? "Estimating…"
          : "Estimate on Arbitrum One"}
      </Button>
      <ResultRow
        label="gas_estimate"
        value={fees?.gas_estimate ?? null}
      />
      <ResultRow
        label="l1_base_fee_estimate"
        value={fees?.l1_base_fee_estimate ?? null}
      />
      <ResultRow
        label="l2_base_fee"
        value={fees?.l2_base_fee ?? null}
      />
      {error && (
        <div className="gas-estimate-arbitrum-error">
          {error}
        </div>
      )}
    </div>
  )
}

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