Skip to content

[Community] Arc Testnet survival kit — RPC fallbacks, USDC decimals, EIP-7825 gas caps #305

Description

@kutluhaneth46

Summary

Community reference repo with copy-paste Arc Testnet helpers — RPC fallbacks, USDC decimal traps, EIP-7825 gas-cap parsing, and receipt retry on -32011.

Repo: https://github.com/kutluhaneth46/arc-dev-survival-kit

Motivation

Several open issues/PRs describe the same failure modes this kit encodes as portable TypeScript:

Kit module Related work
rpc.ts — fallback() across public endpoints arc-node #299 · #292 · #207
gas.ts — EIP-7825 vs balance allowance errors arc-node #299 · #297
usdc.ts — native 18-dec vs ERC-20 6-dec USDC Common silent bug on Arc Testnet
receipt.ts — retry waitForTransactionReceipt on concurrency limits arc-node #299

Content aligns with the EIP-7825 corrections in #299 (reviewed by @osr21). The survival kit is linked from the PR thread, not from upstream docs — per maintainer preference for third-party links.

What it is / isn't

Ask

  1. Is a community survival kit useful alongside developers.circle.com, or should it stay external?
  2. Any corrections to the USDC decimal / gas-cap semantics before wider sharing?

Related contributor PRs: arc-node #299, #297, #295, arc-commerce #58.

Additional context

Unofficial community resource — feedback welcome.

Activity

  1. osr21 commented on Sep 1, 2026

    @osr21

    Ran the kit's claims against live Arc Testnet (2026-09-02) and the v0.8.0 source before answering the two asks. Disclosure: I reviewed #299, which this kit cites.

    Ask 2 — verification of the USDC-decimal / gas-cap semantics

    Everything I could test independently checks out:

    Claim Result
    Chain id 5042002 ✅ eth_chainId → 0x4cef52 on all endpoints tested today
    USDC at 0x3600…0000, 6 decimals ✅ live eth_call: decimals() = 6, symbol() = USDC
    Native 18-dec / ERC-20 6-dec, shared pool, 10¹² scale ✅ live account 0x570f…1e78: eth_getBalance = 771915579006064000, balanceOf = 771915 — exact ÷10¹² truncation match, so the max-send dust trap is real
    Block gas limit 30,000,000 ✅ live eth_getBlockByNumber
    --rpc.gascap default 30M ✅ in source: crates/node/src/args.rs pins --rpc.gascap=30000000 (flips Reth's 50M default; covered by test_rpc_gascap_default_is_thirty_million and the launched-node test in crates/execution-e2e/tests/e2e/static_rpc_gas_cap.rs)
    16,777,216 = 2²⁴ is protocol-level (EIP-7825, Osaka), not endpoint policy ✅ consistent with crates/evm/src/evm.rs, whose tests explicitly disable the "EIP-7825 tx gas limit cap" to run bigger-gas fixtures — i.e. the cap is enforced in the EVM layer, so "switching RPC providers will not help" is the correct advice
    All 9 public endpoints in constants.ts ✅ all answered eth_chainId with 0x4cef52 today

    Bonus datapoint from the endpoint sweep: rpc.quicknode.testnet.arc.io returned -32011 ("request limit reached") to a single eth_chainId — the public budget was already saturated by other users' traffic at that moment. That's real-world justification for both the fallback list and waitForReceiptWithRetry; a -32011 can hit you on your very first call of the day.

    receipt.ts walking the cause chain (rather than checking error.code at the top level) is the right shape — -32011 surfaces at different wrapping depths depending on which viem call path throws, which is exactly why unwrapped waitForTransactionReceipt aborts while most read paths advance past it.

    Two small tightening suggestions, not corrections:

    1. rpc.ts — spread the first hit. Plain fallback() always starts at urls[0], so every copy-paste user of the kit hammers rpc.testnet.arc.network first while eight endpoints idle — and with per-endpoint budgets this tight, that ordering matters. Consider fallback(transports, { rank: true }) or rotating the list to a random start index at client creation.
    2. gas.ts — one-comment caveat. allowance (N) is a single message shape shared by balance-derived and cap-derived budgets; the kit's value-match against 16_777_216 / 30_000_000 is a reasonable heuristic, but a balance that coincidentally derives to exactly one of those values would misclassify. Worth a comment so nobody treats the returned cause as ground truth in funds-related error handling.

    Ask 1 — should it live upstream?

    External is the right home, and not because of quality. This repo's docs are deliberately node-operator scoped — application-developer guidance has consistently been routed to developers.circle.com rather than into arc-node, so a viem-patterns kit wouldn't land here regardless. The durable upstream fix for the shared failure modes stays #299 (endpoint table + cap semantics in docs the right audience will find); the kit works well as the copy-paste companion to it. Keeping the "not official Circle/Arc material" framing prominent in the README is the right call.

  2. kutluhaneth46 commented on Sep 2, 2026

    @kutluhaneth46
    ContributorAuthor

    Live companion tool — Arc RPC Pulse probes all 9 public testnet endpoints (chainId, latency, rate limits) and refreshes every 10 minutes:\n\nhttps://kutluhaneth46.github.io/arc-rpc-pulse/\n\nSource: https://github.com/kutluhaneth46/arc-rpc-pulse\n\nPairs with the survival kit; endpoint list matches #299 / constants.ts.

  3. osr21 commented on Sep 2, 2026

    @osr21

    Went through Arc RPC Pulse end to end — site, data.json, probe code, and workflow — and re-probed independently. It works, and its current snapshot independently reproduced the QuickNode -32011 I reported above, which is a good sign. One finding below is worth acting on, because it changes both this dashboard's math and the survival kit's fallback strategy.

    Verified

    • Page serves 200; docs/data.json was fresh at fetch time (checkedAt matched the last workflow run), expectedChainId 0x4cef52 correct, and all nine blockNumber values clustered within one block of each other — so no endpoint is silently serving a stale head right now.
    • The nine URLs in lib/endpoints.mjs match the docs: add public testnet RPC guide for gas caps and rate limits #299 table exactly — same hosts, same order, no drift. ✔️

    The finding: the nine endpoints are not nine independent budgets

    The two QuickNode hosts are the same backend behind one rate-limit budget:

    rpc.quicknode.testnet.arc.network -> 64.130.45.56
    rpc.quicknode.testnet.arc.io      -> 64.130.45.56   (identical)
    

    And empirically, not just by DNS — I sent five rapid eth_chainId calls to .network (call 1 succeeded, calls 2–5 returned -32011), then immediately hit .io, which also returned -32011. Exhausting one host exhausts the other.

    Two consequences:

    1. For the dashboard: "9 total / 8 healthy" overstates redundancy. The meaningful denominator is distinct providers, not hostnames. Grouping the .network/.io pairs (or showing a provider-level row with the hostname variants nested) would report real availability. Worth noting your own snapshot already shows the artifact of this — quicknode-network red while quicknode-io green, at the same instant, on the same backend.
    2. For the survival kit: this is the more important one. A fallback() list that rotates …quicknode.testnet.arc.network → …quicknode.testnet.arc.io after a -32011 is retrying into the same exhausted budget. Fallback ordering should hop providers, not hostnames — which strengthens the rank: true / shuffle suggestion from my earlier comment, but with the extra constraint that the pairs should be treated as one unit.

    Three smaller notes

    • Rate-limited ≠ unhealthy. probeEndpoint sets ok: false on a -32011, which drops that endpoint from avgLatencyMs and fastest, and counts it in unhealthy. But a -32011 is routine and transient — as your own kit's waitForReceiptWithRetry assumes. A distinct degraded/limited state, plus one retry with backoff before classifying, would avoid a red square for a node that's up and answering a second later.
    • Latency is single-vantage and self-contended. pulseAll fires all nine probes via Promise.all from one GitHub Actions runner, so the numbers include the tool's own concurrency and one network position. Sampled serially from a different vantage just now: primary 99ms (pulse: 380), rpc.testnet.arc.io 145ms (pulse: 176), arc-testnet.drpc.org 147ms (pulse: 575). The fastest badge in particular isn't stable across vantage points — worth labelling it "as measured from a GitHub Actions runner" so nobody picks a production endpoint off it.
    • Silent-staleness risk. app.js renders Last check · <local time> with no age check, and */10 * * * * on GitHub Actions is best-effort — scheduled runs get delayed under load, and scheduled workflows on public repos are subject to the 60-day inactivity auto-disable (don't assume the bot's own commits reset that clock). If the cron stops, the page keeps showing a plausible-looking timestamp. A rendered "updated N minutes ago" plus a stale badge past ~30 minutes would make a stalled feed obvious.

    Also worth weighing against the above: probing 9 endpoints × 2 calls every 10 minutes is ~2.6k calls/day into budgets the tool itself reports as saturated. Given how quickly the QuickNode budget emptied in my five-call test, folding the shared-backend pairs into a single probe would cut that noticeably while improving accuracy.

  4. kutluhaneth46 commented on Sep 2, 2026

    @kutluhaneth46
    ContributorAuthor

    Addressed the RPC Pulse findings in kutluhaneth46/arc-rpc-pulse@master:\n\n- Provider budgets: 5 provider rows, 9 hostnames nested; QuickNode tagged \shared budget\n- Limited ≠ down: -32011\ → \limited\ status after 1s retry; summary counts providers OK separately\n- Serial probes from one runner (no Promise.all fan-out)\n- Stale badge when snapshot >30m old; age shown as \Nm ago\n- Survival kit: \ARC_TESTNET_RPC_PROVIDER_URLS\ (5 URLs) + \ allback(..., { rank: true })\ — no .network→.io hop on same provider\n\nLive: https://kutluhaneth46.github.io/arc-rpc-pulse/

  5. osr21 commented on Sep 2, 2026

    @osr21

    Checked ad67eae (pulse) and b291e7c (kit) against the live snapshot — all four items are implemented as described:

    • Provider grouping — PROVIDERS with 5 rows / 9 hosts nested, sharedBudget: true on QuickNode, and the live JSON now reports providersOk: 5/5 alongside hostsOk. ✔️
    • limited ≠ down — probeHost retries once after 1s on -32011 and returns status: "limited", kept out of hostsError. ✔️
    • Serial probes — Promise.all fan-out gone; nested for loops. ✔️
    • Stale badge / vantage — STALE badge past 30m with Nm ago, and the vantage line renders. ✔️
    • Kit — ARC_TESTNET_RPC_PROVIDER_URLS is exactly one URL per provider budget, and arcTestnetTransport defaults to it with rank: true. ✔️

    I ran more probes against the assumptions underneath these changes, and three results are worth having:

    1. The QuickNode budget is ≈1 request per second — your 1s retry is correctly calibrated. Tested pairs of eth_chainId at increasing gaps:

    Gap between calls 2nd call
    0s (back-to-back) -32011
    1s ok
    2s ok
    5s ok

    So RETRY_DELAY_MS = 1_000 sits right at the refill boundary — worth a comment in the code so nobody "optimizes" it to 250ms later.

    2. The other three pairs show no shared-budget behavior. I re-ran my QuickNode method (8 rapid calls to host A, then immediately host B) against Arc, dRPC, and Blockdaemon: all 8 succeeded on every host A, and every host B answered fine straight after. So sharedBudget: false on those three is consistent with observation. One caveat worth encoding as "unverified" rather than "false": absence of limiting at this volume isn't proof of separate buckets — only that their budgets are far looser than QuickNode's, which limits at the second request.

    3. Residual shared-budget risk inside probeHost itself. On success it fires eth_chainId and then eth_blockNumber back-to-back with no delay. For QuickNode those two land inside the same ~1s window, and the eth_blockNumber failure is swallowed by the bare catch {} — so the host would report status: "ok" with blockNumber: null, i.e. silently missing data rather than a visible error. In practice the current snapshot has block numbers on all nine (natural per-call latency of 200–450ms is apparently enough), and I reproduced the sequence manually against a rested budget with both calls succeeding — so it's marginal, not broken. A small inter-call delay when sharedBudget is set would make it deterministic instead of latency-dependent.

    On rank: true — completing my own earlier advice, since it has a background cost. viem's ranker pings every transport every interval, and interval defaults to client.pollingInterval (4s), using net_listening as the ping.

    • Good news, and not a given: I verified all five provider endpoints implement net_listening and return true, so the default ping works on Arc — no false "unstable" deranking.
    • But at the 4s default that's ~0.25 req/s of background traffic per provider. Against QuickNode's measured ~1 req/s ceiling, ranking alone consumes roughly a quarter of the budget before the app issues a single call. For these endpoints I'd pass viem's own documented example config instead of bare true: rank: { interval: 60_000, sampleCount: 5 }.

    Two trivia to close: providerStatus has a redundant branch — every(h => limited) is fully subsumed by the some(h => limited) line right after it, so the middle line is dead. And dRPC.org came back at 50ms in this snapshot versus 575ms in the previous one — a 10× swing on one host between runs, which is a good argument for the vantage note you added and against reading fastest as a recommendation.

  6. kutluhaneth46 commented on Sep 10, 2026

    @kutluhaneth46
    ContributorAuthor

    @huklaa I've previously posted related work on this community kit / RPC pulse thread. If you're planning a PR, please check existing open PRs first so we don't duplicate — happy to coordinate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions