Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions benchmarks/gas-estimation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ abstract: |

methodology:
- "Chains: Ethereum mainnet (chainid=1), Polygon PoS (chainid=137). Both are EIP-1559 with proper dynamic base fee, so priority-fee prediction is apples-to-apples comparable. BNB Chain excluded (not EIP-1559). Avalanche C-Chain excluded (auto-tuning drives priority fee to ~0, prediction-error collapses to ~0)."
- "Oracles per chain. Ethereum + Polygon: PublicNode feeHistory, Owlracle, Etherscan v2 (free tier)."
- "Per-oracle endpoints. PublicNode `eth_feeHistory` on each chain's RPC; Owlracle `/v4/{eth|poly}/gas` (slug per chain); Etherscan v2 `?chainid=<N>&module=gastracker&action=gasoracle`."
- "Cadences. PublicNode feeHistory every 12s per chain; Owlracle every 60s per chain (free quota ceiling); Etherscan every 15s per chain with a global rate-gate enforcing ≥6s between any two Etherscan requests across chains (no-key limit is 1 req/5s per IP, shared)."
- "Oracles per chain. Ethereum + Polygon: PublicNode feeHistory, Owlracle, Etherscan v2 (free tier), MetaMask. Avalanche: same set minus Etherscan (free plan restricted for chainid 43114)."
- "Per-oracle endpoints. PublicNode `eth_feeHistory` on each chain's RPC; Owlracle `/v4/{eth|poly|avax}/gas` (slug per chain); Etherscan v2 `?chainid=<N>&module=gastracker&action=gasoracle`; MetaMask `gas.api.cx.metamask.io/networks/<N>/suggestedGasFees` (public no-key, EIP-1559 low/medium/high mapped to p25/p50/p90)."
- "Cadences. PublicNode feeHistory and MetaMask every 12s per chain; Owlracle every 60s per chain (free quota ceiling); Etherscan every 15s per chain with a global rate-gate enforcing ≥6s between any two Etherscan requests across chains (no-key limit is 1 req/5s per IP, shared)."
- "Tier normalization. Each oracle's named tiers (fast / standard / slow / safe / propose) are mapped onto a unified p25 / p50 / p75 / p90 / p99 scheme. The leaderboard ranks the p50 tier (the standard speed wallets use by default)."
- "Realized priority fee. For each pending block, the harness pulls the full block via `eth_getBlockByNumber(.., true)` on the chain's primary RPC and computes p25/p50/p75/p90/p99 directly from the actual effective `maxPriorityFeePerGas` values across every included transaction. Every tier is a real percentile of the mined block, none are interpolated. Empty or low-tx blocks (`gas_realized_tx_count` near zero) are surfaced separately because the realized percentile is noisy when tx count is low."
- "Realized-side independence check. Each chain configures a second RPC on a different upstream (dRPC / native chain RPC) as `GAS_REALIZED_RPC_VERIFY_<CHAIN>`; on each block the harness cross-checks baseFee and increments `gas_realized_quorum_disagreement_total{chain, kind}` on mismatch. Primary is still trusted; the counter makes visible whether our ground truth is actually independent across upstreams."
Expand Down Expand Up @@ -217,3 +217,16 @@ providers:
success: sum(rate(gas_oracle_call_total{oracle="etherscan", result="ok"}[24h])) / sum(rate(gas_oracle_call_total{oracle="etherscan"}[24h]))
sample_size: sum(increase(gas_oracle_call_total{oracle="etherscan"}[24h]))
series: gas_error_priority_gwei{oracle="etherscan", tier="p50"}

- slug: metamask
name: MetaMask
tag: Wallet-native, no key, EIP-1559 low/medium/high
formula: "p99 over 24h of |MetaMask's medium-tier suggestedMaxPriorityFeePerGas mapped to p50 − realized p50 priority fee| in gwei, polled every 12s and matched to the next-mined block on the active chain."
queries:
p50: quantile_over_time(0.99, gas_error_priority_gwei{oracle="metamask", tier="p50"}[24h])
p90: quantile_over_time(0.90, gas_error_priority_gwei{oracle="metamask", tier="p50"}[24h])
p99: quantile_over_time(0.50, gas_error_priority_gwei{oracle="metamask", tier="p50"}[24h])
mean: avg_over_time(gas_error_priority_gwei{oracle="metamask", tier="p50"}[24h])
success: sum(rate(gas_oracle_call_total{oracle="metamask", result="ok"}[24h])) / sum(rate(gas_oracle_call_total{oracle="metamask"}[24h]))
sample_size: sum(increase(gas_oracle_call_total{oracle="metamask"}[24h]))
series: gas_error_priority_gwei{oracle="metamask", tier="p50"}
14 changes: 10 additions & 4 deletions harnesses/gas-estimation/cmd/script/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
OraclePublicNode Oracle = "publicnode-feehistory"
OracleOwlracle Oracle = "owlracle"
OracleEtherscan Oracle = "etherscan"
OracleMetaMask Oracle = "metamask"
)

type Oracle string
Expand All @@ -48,6 +49,7 @@ var pollIntervals = map[Oracle]time.Duration{
OraclePublicNode: 12 * time.Second,
OracleOwlracle: 60 * time.Second,
OracleEtherscan: 15 * time.Second,
OracleMetaMask: 12 * time.Second,
}

// Realized-block poll cadence per chain. Picked close to each chain's
Expand Down Expand Up @@ -94,7 +96,7 @@ func chains() []Chain {
OwlracleSlug: "eth",
BlockTimeSec: 12,
// Etherscan v2 free tier covers chainid=1. All four oracles work.
SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleEtherscan},
SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleEtherscan, OracleMetaMask},
},
{
Slug: "polygon",
Expand All @@ -104,7 +106,7 @@ func chains() []Chain {
OwlracleSlug: "poly",
BlockTimeSec: 2,
// Etherscan v2 free tier covers chainid=137 (verified). All four oracles work.
SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleEtherscan},
SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleEtherscan, OracleMetaMask},
},
{
Slug: "avalanche",
Expand All @@ -114,8 +116,8 @@ func chains() []Chain {
OwlracleSlug: "avax",
BlockTimeSec: 2,
// Etherscan v2 returns "Free API access is not supported for this chain" on chainid=43114 — paid plan required.
// Three oracles only (Blocknative + PublicNode feeHistory + Owlracle).
SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle},
// Three oracles only (PublicNode feeHistory + Owlracle + MetaMask).
SupportedSet: []Oracle{OraclePublicNode, OracleOwlracle, OracleMetaMask},
},
}
}
Expand Down Expand Up @@ -161,6 +163,10 @@ func endpointForChain(o Oracle, c Chain) OracleEndpoint {
return OracleEndpoint{
URL: fmt.Sprintf("https://api.etherscan.io/v2/api?chainid=%d&module=gastracker&action=gasoracle", c.ChainID),
}
case OracleMetaMask:
return OracleEndpoint{
URL: fmt.Sprintf("https://gas.api.cx.metamask.io/networks/%d/suggestedGasFees", c.ChainID),
}
}
return OracleEndpoint{}
}
Expand Down
73 changes: 73 additions & 0 deletions harnesses/gas-estimation/cmd/script/oracles.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func pollOracle(ctx context.Context, o Oracle, ep OracleEndpoint) pollResult {
return pollOwlracle(ctx, ep)
case OracleEtherscan:
return pollEtherscan(ctx, ep)
case OracleMetaMask:
return pollMetaMask(ctx, ep)
default:
return pollResult{Err: fmt.Errorf("unknown oracle: %s", o)}
}
Expand Down Expand Up @@ -401,6 +403,77 @@ func pollEtherscan(ctx context.Context, ep OracleEndpoint) pollResult {
}
}

// ─── MetaMask (gas.api.cx.metamask.io) ────────────────────────────
//
// Public no-key endpoint that powers the MetaMask wallet's
// suggested-fee UI at real production scale. EIP-1559 native:
// returns explicit low/medium/high tiers each with their own
// `suggestedMaxPriorityFeePerGas` and `suggestedMaxFeePerGas`
// (gwei decimal strings), plus a baseline `estimatedBaseFee`.
// We map low→p25, medium→p50, high→p90 — the same mapping the
// wallet's own slow/market/fast selector uses. Tiers collapse
// during calm blocks (medium and high often equal), which
// mirrors the upstream model's documented behaviour; the bench
// surfaces this as-is rather than masking it.

type mmTier struct {
SuggestedMaxPriorityFeePerGas string `json:"suggestedMaxPriorityFeePerGas"`
SuggestedMaxFeePerGas string `json:"suggestedMaxFeePerGas"`
}

type mmResp struct {
Low mmTier `json:"low"`
Medium mmTier `json:"medium"`
High mmTier `json:"high"`
EstimatedBaseFee string `json:"estimatedBaseFee"`
}

func pollMetaMask(ctx context.Context, ep OracleEndpoint) pollResult {
req, _ := http.NewRequestWithContext(ctx, "GET", ep.URL, nil)
body, status, err := httpDo(ctx, req)
if err != nil {
return pollResult{Err: err}
}
if status == 429 {
return pollResult{Err: fmt.Errorf("throttled (HTTP 429)")}
}
if status != 200 {
return pollResult{Err: fmt.Errorf("http %d", status)}
}
var r mmResp
if err := json.Unmarshal(body, &r); err != nil {
return pollResult{Err: fmt.Errorf("parse: %w", err)}
}
base, err := strconv.ParseFloat(r.EstimatedBaseFee, 64)
if err != nil {
return pollResult{Err: fmt.Errorf("baseFee parse: %w", err)}
}
parseTier := func(field, name string) (float64, error) {
v, err := strconv.ParseFloat(field, 64)
if err != nil {
return 0, fmt.Errorf("%s parse: %w", name, err)
}
return v, nil
}
low, e1 := parseTier(r.Low.SuggestedMaxPriorityFeePerGas, "low")
med, e2 := parseTier(r.Medium.SuggestedMaxPriorityFeePerGas, "medium")
high, e3 := parseTier(r.High.SuggestedMaxPriorityFeePerGas, "high")
if e1 != nil || e2 != nil || e3 != nil {
return pollResult{Err: fmt.Errorf("tier parse: %v / %v / %v", e1, e2, e3)}
}
// No explicit target block: the API applies to "the next block"
// but never names it. Realizer grafts head+1 the same way it
// does for Owlracle.
return pollResult{
BaseGwei: base,
Predictions: []Prediction{
{Oracle: OracleMetaMask, Tier: TierP25, PriorityGwei: low, BaseGwei: base},
{Oracle: OracleMetaMask, Tier: TierP50, PriorityGwei: med, BaseGwei: base},
{Oracle: OracleMetaMask, Tier: TierP90, PriorityGwei: high, BaseGwei: base},
},
}
}

// ─── helpers ───────────────────────────────────────────────────────

func httpDo(ctx context.Context, req *http.Request) ([]byte, int, error) {
Expand Down
Loading