Skip to content

feat: #2230 add Arc mainnet and testnet to wallet with USDC - #2228

Open
Dami904 wants to merge 12 commits into
KeeperHub:stagingfrom
Dami904:feat/arc-testnet-chain
Open

feat: #2230 add Arc mainnet and testnet to wallet with USDC#2228
Dami904 wants to merge 12 commits into
KeeperHub:stagingfrom
Dami904:feat/arc-testnet-chain

Conversation

@Dami904

@Dami904 Dami904 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Circle's Arc as a supported chain in the wallet -- testnet (5042002) and mainnet (5042) -- following the same onboarding pattern used for Robinhood Chain, Plasma, and 0G (RPC config -> chain seed -> explorer config -> supported-token seed).

Arc is Circle's EVM-compatible L1 purpose-built for stablecoin finance. Its defining quirk relative to every other chain in this repo: USDC is the native gas token, not ETH. Circle also exposes a fixed ERC-20-interface precompile at 0x3600000000000000000000000000000000000000 for programmatic balance/transfer access to that same native asset (6 decimals, distinct from the 18-decimal native-currency gas accounting) -- this repo's supported_tokens model expects an ERC-20 address, so that precompile is what's registered as the stablecoin entry.

Testnet Mainnet
Chain ID 5042002 5042
Native symbol USDC USDC
Status experimental experimental
Primary RPC rpc.testnet.arc.io rpc.arc-scan.org
WSS yes none
Explorer Blockscout (testnet.arcscan.app) none

Config-side entries for both chains are already merged in chain-config (#46), keyed arc-testnet / arc-mainnet, so the overrides land as soon as this merges.

Read this before approving the mainnet half

Mainnet is being onboarded over two known, unresolved gaps. They are not oversights and they are not fixed here.

1. No websocket, so event and block triggers will never fire on Arc mainnet. No public WSS endpoint exists for chain 5042: Circle's rpc.mainnet.arc.io is Cloudflare-gated (403), dRPC answers Unknown network for arc and arc-mainnet, and rpc.arc-scan.org returns 405 to an upgrade request. Both trigger services are WebSocket-only with no HTTP polling fallback, and the resulting failure is silent:

  • Block triggers: chain-monitor.ts:440-452 throws No WSS URLs configured, index.ts:174-188 catches it and deletes the monitor from this.monitors, and health.ts:31-38 computes liveness only over that map -- so the dispatcher reports healthy: true, status: "ok" while Arc has no monitor. The only machine-readable trace is a stuck is_alive=0 gauge.
  • Event triggers: workflow-mapper.ts:70-82 returns null on an empty defaultPrimaryWss; the skip lands in a local counter that reaches a pass-summary log line but not Prometheus and not /healthz.
  • There is no authoring-time guard anywhere in the workflow save path or the trigger-config UI. A user can build an event or block trigger on Arc mainnet, see it saved and active, and it will never fire and never say why.

Arc mainnet will also be the only chain in the fleet with a null defaultPrimaryWss -- all other entries have one -- so this code path has never run in a deployed environment. chain-config #47 disables Arc mainnet on staging as well as production for that reason; merging that first means mainnet seeds disabled everywhere and the untested path stays dormant until someone deliberately enables it.

2. No explorer. arcscan.app does not resolve, arc-scan.org is a Next.js app rather than Blockscout (/api/v2/stats returns 404 HTML) and 403s on /tx/ and /address/, and explorer.arc.io redirects to Circle SSO. Every consumer guards on null so nothing breaks, but the consequences are real: /api/chains/[chainId]/abi returns 404 Explorer not configured for this chain, so automatic ABI fetching is unavailable and users must paste ABIs manually; enrich-explorer-links early-returns so executions carry no transactionLink / addressLink; wallet rows render without links.

Both are documented for users in docs/platform-reference.md in this PR.

Changes

Testnet (5042002):

  • lib/rpc/rpc-config.ts: PUBLIC_RPCS.ARC_TESTNET / _FALLBACK / _WSS and a CHAIN_CONFIG[5042002] entry (primary RPC rpc.testnet.arc.io, fallback via dRPC).
  • scripts/seed/seed-chains.ts: DEFAULT_CHAINS entry, chainToDefaultIdMap["Arc Testnet"], and a Blockscout EXPLORER_CONFIG_TEMPLATES[5042002] entry.
  • scripts/seed/seed-tokens.ts: TOKEN_CONFIGS entry for the USDC precompile address, verified on-chain (see Testing).

Mainnet (5042), added by a maintainer on top of the original testnet-only diff:

  • lib/rpc/rpc-config.ts: PUBLIC_RPCS.ARC_MAINNET and CHAIN_CONFIG[5042] with jsonKey: "arc-mainnet". publicFallback and the WSS fields are deliberately absent -- no second RPC and no websocket exist, and getWssUrl returning undefined is the sanctioned path for a chain with no reliable public WSS. Comments at both sites record why each field is missing.
  • scripts/seed/seed-chains.ts: DEFAULT_CHAINS row (name: "Arc", aliases: ["arc"], isTestnet: false). No explorer template and no chainToDefaultIdMap entry, since there is no explorer -- the seed handles the miss by warning and skipping, verified.
  • scripts/seed/seed-tokens.ts: the same 0x3600...0000 precompile on 5042.
  • lib/wallet/build-withdrawable-assets.ts: NATIVE_MIRROR_TOKEN_ADDRESS gains 5042, without which mainnet double-counts native + token exactly as the testnet did.
  • app/api/supported-tokens/route.ts and components/overlays/wallet/chain-utils.ts: 5042 added to both copies of INDEPENDENT_TOKEN_LIST_CHAIN_IDS.
  • docs/platform-reference.md: Arc mainnet row plus a plain-language note that event and block triggers do not fire there and ABIs must be supplied manually.

Deliberately not changed, with reasons:

  • Safe support. eth_getCode on Arc mainnet shows the Safe proxy factory and L2 singleton deployed, but the Allowance Module is absent (0xCFbFaC74C26F8647cBDb8c5caf80BB5b32E43134 returns 0x). lib/safe/contracts.ts:50-59 requires it for SUPPORTED_SAFE_CHAIN_IDS, so Arc cannot enter that set and can never reach deployableChainIds. Adding it to deploy-safe-card.tsx's MAINNET_CHAIN_IDS would be unreachable code.
  • Gas overrides. No multiplier was invented. The fee market was measured rather than guessed: baseFeePerGas flat at 20 gwei across six consecutive blocks, gasUsedRatio 0.0 in every one, eth_maxPriorityFeePerGas and all eth_feeHistory reward percentiles 0x0, latest block 76,787 gas of a 30M limit. On a chain 0.25% full the global default (gasLimitMultiplier: 2.0, minPriorityFeeGwei: 0.1) cannot cause non-inclusion; the floor is an inert ~0.5% overpay (~0.00042 USDC per simple transfer). Worth setting minPriorityFeeGwei: 0 once someone benchmarks real inclusion.
  • lib/rpc/types.ts SUPPORTED_CHAIN_IDS. Arc is absent, so getNetworkName(5042) returns "Chain 5042" and EIP-712 signing is refused -- identical to Plasma, Robinhood, 0G, Polygon, Arbitrum, Optimism and Avalanche, all of which are also absent. That gap deserves its own ticket covering all eight, not a partial fix smuggled in here.

Testing

Testnet, by the original author:

  • On-chain verification: pnpm tsx scripts/verify-token.ts 5042002 0x3600000000000000000000000000000000000000 against the real Arc testnet RPC confirmed symbol: USDC, name: USDC, decimals: 6.
  • Real DB seed: brought up Postgres via WSL Docker, ran pnpm dev:bootstrap, pnpm tsx scripts/seed/seed-chains.ts, and pnpm tsx scripts/seed/seed-tokens.ts against it. Arc Testnet and its explorer config landed correctly in chains / explorer_configs; the USDC token seed matched the standalone verification exactly.
  • API logic: replayed /api/supported-tokens's actual master-list-overlay query logic against the seeded data. Arc's USDC correctly resolves available: true with the right decimals and explorer URL; USDT/USDS (mainnet-only symbols that don't exist on Arc) correctly resolve available: false -- same behavior as every other single-asset testnet (Sepolia, Base Sepolia, etc.).
  • Not covered: could not visually verify the wallet-modal chain card renders in a browser -- pnpm dev fails to boot in that environment on an unrelated pre-existing Windows/Node bug (ERR_UNSUPPORTED_ESM_URL_SCHEME in the plugin/protocol discovery step, unrelated to this diff).
  • pnpm vitest run tests/unit/rpc-config.test.ts: 112/112 pass, including the new ARC_TESTNET* entries automatically covered by the existing PUBLIC_RPCS contains no KeeperHub-operated endpoint parameterized check.

Mainnet, for the maintainer commit:

  • Chain identity verified against a live node rather than an announcement: eth_chainId returns 0x13b2 and net_version returns 5042, at block 0x131fbc7. Block time measured at ~0.506s over 10,000 blocks.
  • The 0x3600...0000 precompile independently confirmed on mainnet via eth_call: decimals() = 6, symbol() = USDC, name() = USDC.
  • The absence of WSS, of a dRPC route, and of a Blockscout instance were each verified by direct probe, not inferred.
  • tsc --noEmit: 0 errors. Full vitest run tests/unit: 595/595 files, 21,995/21,995 tests passing, exit 0. biome check clean on the changed files.
  • Confirmed the seed tolerates the missing explorer template: seed-chains.ts:1072 warns No explorer config template for chain Arc (5042), skipping, returns null, and the entry is filtered out. Nothing throws.

No new test file was added -- consistent with every prior chain addition (Robinhood, Plasma, 0G, Avalanche, BNB), none of which added coverage for seed-chains.ts / seed-tokens.ts / the supported-tokens route either.

Follow-ups (not blocking)

  • Wallet-modal visual check once a working dev-server environment is available.
  • Re-enable Arc mainnet (and add WSS config) once any endpoint serves a websocket for chain 5042.
  • Add an explorer once one exists that is reachable and exposes a Blockscout or Etherscan API.
  • Set minPriorityFeeGwei: 0 for Arc once real inclusion behaviour is benchmarked.
  • Consider an authoring-time guard rejecting event/block triggers on chains with no WSS URL -- Arc mainnet is the first chain to make this reachable, but the gap is general.

Tempo display change (intentional)

This PR's native/token dedup refactor (hidesNativeRow in lib/wallet/build-withdrawable-assets.ts) also changes what renders for Tempo (chain ID 4217, live mainnet) on three surfaces: the wallet chain card (components/overlays/wallet/chain-balance-item.tsx), the toolbar wallet digest and USD total (lib/wallet/use-wallet-digest.ts), and the Settings assets table (components/settings/hub/wallets/use-account-assets.ts). Staging currently renders Tempo's native row unconditionally on all three; this PR removes it.

This is intentional, not incidental. Tempo uses stablecoins for gas and has no native gas token to withdraw -- that's the long-standing rule in chain-utils.ts (isTempoChain), and the withdraw path has always refused Tempo native withdrawals. This PR just makes the display consistent with that rule across all three surfaces instead of only some of them.

Wires up Circle's Arc testnet (chain 5042002) as a new supported chain:
RPC/WSS config, chain + Blockscout explorer seed entries, and the native
USDC gas-token precompile as a supported stablecoin. USDC metadata verified
on-chain (symbol/name/decimals) via scripts/verify-token.ts.
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Resolved. References accepted issue #2230.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

About the build check on this pull request

This pull request comes from a fork, so GitHub does not pass it the credentials build normally uses for our image registry cache and staging build configuration. The build still runs and still compiles the image, so a red build here is real; it just takes longer than on team branches.

Every workflow run on a pull request from a fork also waits for a maintainer to approve it, so checks can sit at "awaiting approval" for a while after each push. Nothing is needed from you for either of these.

@suisuss suisuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What this changes

Registers chain 5042002 across the three chain-onboarding files: PUBLIC_RPCS and a CHAIN_CONFIG entry in lib/rpc/rpc-config.ts; a DEFAULT_CHAINS row, a Blockscout EXPLORER_CONFIG_TEMPLATES entry and a chainToDefaultIdMap line in scripts/seed/seed-chains.ts; and a TOKEN_CONFIGS row for 0x3600...0000 in scripts/seed/seed-tokens.ts. Additive, 64 lines, no deletions.

I checked the data against the live chain rather than the docs. eth_chainId returns 0x4cef52, which is 5042002, on both RPC hosts. The WSS host completes a real 101 Switching Protocols handshake. testnet.arcscan.app is a CNAME to arc-testnet.cname.blockscout.com and both /api and /api/v2/stats answer. 0x3600...0000 returns symbol() USDC, name() USDC, decimals() 6. Every value in the diff is correct.

Field for field it also matches precedent exactly: the DEFAULT_CHAINS entry carries all thirteen fields 0G Galileo carries, the explorer entry all seven Robinhood's Blockscout entry carries, and the token row all five Plasma's carries. Measured against .claude/commands/add-chain.md, whose success criteria name these three files, it is complete. The findings below are all in surfaces that checklist does not cover, because Arc is the first chain of its kind here.

Does it match the description

Matches. The description is accurate about what was verified and honest about what was not.

Single unit - the three files cannot be split. scripts/seed/seed-chains.ts dereferences CHAIN_CONFIG[5_042_002].jsonKey directly and unguarded, so reverting the rpc-config.ts hunk throws at module load, and the token row targets a chain with no chains row. One line is independently revertible: aliases, and see below.

Blocking

  • lib/wallet/build-withdrawable-assets.ts:43 and components/overlays/wallet/chain-balance-item.tsx:443 - Arc's native balance and its supported-token row are the same money, and nothing dedups them. I checked one live account: eth_getBalance returns 83134536318811116 and balanceOf(0x3600...) returns 83134, which is 0.083134 USDC at 18 decimals and at 6. buildWithdrawableAssets skips the native asset only for TEMPO_CHAIN_IDS (:30), then concatenates natives and supported tokens with no symbol or value dedup, so Arc yields two WithdrawableAsset entries both labelled USDC. The wallet renders USDC - Native - 21.7512853968 directly above USDC - 21.751285, with a Withdraw button on each. A user reads roughly double their balance, and a withdraw-everything flow driven off that list attempts both. Generalise the Tempo skip into "the native token is a registered ERC-20 on this chain" keyed on 5042002, and suppress the native row and its button the way isTempo does.

  • scripts/seed/seed-chains.ts - aliases: ["arc"] on a testnet row. No other testnet in the file carries aliases; the bare chain name goes on the mainnet row for Plasma, 0G and Robinhood. classifyChainTag (lib/agentic-wallet/workflow-binding.ts:296-300) resolves an alias with rows.find over loadEnabledChains(), whose query at :167-175 has no ORDER BY. Today chain: "arc" resolves to the testnet and returns a harmless unrecognised. When Arc mainnet lands with aliases: ["arc"] per mainnet precedent, two rows share the alias and the unordered find can pick either. That is the Tempo incident, recorded in this file at workflow-binding.ts:225-230: "seed-chains.ts stamps aliases ["tempo"] on both Tempo rows ... chain: "tempo" collapsed to unrecognised and 403'd every Tempo payment." Drop aliases here, or use ["arc-testnet"] and reserve "arc" for the mainnet row.

Mechanical - actionable as-is

  • components/overlays/wallet/chain-utils.ts:9-11 and app/api/supported-tokens/route.ts:15 - Arc is missing from both copies of INDEPENDENT_TOKEN_LIST_CHAIN_IDS, so its wallet card renders the Ethereum master list and shows USDT and USDS as "Not available" on a chain where they do not exist. That is the output the Plasma carve-out was written to prevent (route.ts:11-14). Add 5_042_002 to both; they are duplicated.

  • plugins/blockscout/chains.ts:14-24 - Arc's explorer is a hosted Blockscout instance, so 5_042_002: "https://testnet.arcscan.app" belongs in BLOCKSCOUT_INSTANCES alongside Robinhood's 4663. Without it, Blockscout steps on Arc need a hand-supplied BLOCKSCOUT_API_URL.

  • docs/platform-reference.md:165-168 - the experimental-chains table lists 0G and Galileo and would list Arc. No CI check covers that table, so it is a convention rather than a gate.

With the team

  • The daily native value cap is denominated in wei with no chain dimension. reserveOrgValue (lib/execute/value-ledger.ts:242-254) takes an organization and a wei amount, and the default is 20000000000000000 (lib/execute/spend-cap-defaults.ts:52), around 0.02 ETH. On Arc, 1e18 wei is one dollar, so moving 20 USDC reserves a thousand times the org's entire daily cross-chain budget and starves every other native transfer that day. It fails closed, so this is unusable rather than unsafe. I'm weighing making the cap per-chain against converting to a USD-denominated budget at reservation time - the first is a smaller change, the second stops the same problem recurring on the next stablecoin-gas chain. I'm taking it to the core team and will come back. It does not block this PR; it decides whether Arc should ship with isEnabled: true.

  • #2230 is not labelled accepted yet, so check-issue-link is red. That is mine to clear, not yours. The reference is also missing from the title, so once the label lands, retitle to feat: #2230 add Arc testnet to wallet with USDC.

Verdict

Changes requested: Arc's native gas token and its supported-token row are the same balance, and the wallet currently lists both.

@suisuss suisuss added the changes-requested Triage: reviewed, changes needed from the contributor label Sep 2, 2026
@suisuss suisuss added the decision-needed Blocked on a maintainer decision, not on the contributor label Sep 2, 2026
@suisuss

suisuss commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

@Dami904 Please raise and link your issue please

…lias collision

Arc's native gas token and its ERC-20 supported-token row are the same
underlying USDC balance at two decimal precisions (18 vs 6), so the wallet
was listing it twice with two Withdraw buttons. Generalized the existing
Tempo native-row suppression into a native-mirrors-token check that also
covers Arc, in both the wallet UI and the withdrawable-assets builder.

Also dropped the arc alias from the testnet seed row -- reserved for the
mainnet row per existing precedent (Plasma/0G/Robinhood), avoiding the
same alias-collision class that previously broke Tempo payment routing.

Mechanical follow-ups from review: added Arc to
INDEPENDENT_TOKEN_LIST_CHAIN_IDS (both copies) so USDT/USDS don't render
as false "Not available" entries, registered Arc's Blockscout instance,
and added it to the experimental chains table in platform-reference.md.
fundedAssets() in use-wallet-digest.ts had the same unguarded
native-plus-token push as build-withdrawable-assets.ts and
chain-balance-item.tsx, feeding wallet-digest-menu.tsx's asset list with
a duplicate USDC row for Arc. Applies the same native-mirrors-token skip.
@Dami904 Dami904 changed the title feat: add Arc testnet to wallet with USDC feat: #2230 add Arc testnet to wallet with USDC Sep 2, 2026

@suisuss suisuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All four items from the last round are addressed, and you found a fifth site I had missed.

  • Duplicate asset in the wallet: fixed. lib/wallet/build-withdrawable-assets.ts:36-39 adds NATIVE_MIRRORS_TOKEN_CHAIN_IDS and :52 skips the native asset for it. The UI follows through nativeMirrorsSupportedToken (components/overlays/wallet/chain-utils.ts:29-31), used at chain-balance-item.tsx:389 for the row and both Withdraw buttons. The per-token Withdraw is untouched, so the balance stays movable through the USDC row.
  • aliases: ["arc"]: removed. The Arc row ends at defaultPrivateRpcUrl with no aliases, so the unordered rows.find in classifyChainTag can no longer bind the bare name to a testnet row when the mainnet arrives.
  • INDEPENDENT_TOKEN_LIST_CHAIN_IDS: added to both copies, chain-utils.ts:19-21 and app/api/supported-tokens/route.ts:16.
  • BLOCKSCOUT_INSTANCES: added at plugins/blockscout/chains.ts:24, and SUPPORTED_BLOCKSCOUT_CHAIN_IDS picks it up from Object.keys.

The fifth is the one worth calling out: lib/wallet/use-wallet-digest.ts:158-161 double-counted Arc in the wallet menu and its USD total, and nothing in my review pointed at it. You found it and shipped the follow-up.

What this changes since the last round

Three dedup sites, both independent-token-list copies, the Blockscout instance, the alias removal, and a docs/platform-reference.md:168 row. specs/api-coverage.json carries no reference to that file, so no doc-drift check fires.

Blocking

  • lib/wallet/build-withdrawable-assets.ts:52 - the native asset is suppressed on chain id alone, not on a supported-token row actually being present. scripts/seed/seed-tokens.ts:439-445 catches per-token errors and continues, so an RPC failure while seeding this chain leaves it enabled with no USDC row. The wallet then shows Arc with no native row, no token row and "No tokens on this chain", and buildWithdrawableAssets returns nothing for it, so a real balance is both invisible and unwithdrawable. Suppress the native row only when a supported-token row for that chain is in the feed.

Mechanical - actionable as-is

  • The same chain-id set now exists three times, and the copy at lib/wallet/use-wallet-digest.ts:49-53 re-hardcodes 42_431, 4217 rather than spreading TEMPO_CHAIN_IDS. This diff is its own evidence: the first commit updated two of the three and shipped a menu that still double-counted, and the second was needed to patch the third. Export one set from lib/wallet/ and have chain-utils.ts re-export it, so nativeMirrorsSupportedToken is the only reader.
  • tests/unit/build-withdrawable-assets.test.ts:169 covers the Tempo skip and has no parallel case for 5_042_002, and the test file is untouched here. Dropping the id from any of the three sets would ship silently.
  • use-wallet-digest.ts:50-51 puts the // Tempo testnet/mainnet comment after 4217, so it reads as annotating only that id.

With the team

  • Settled: Arc ships with isEnabled: true. The daily native value cap is a single chain-agnostic wei budget (lib/execute/value-ledger.ts, default in lib/execute/spend-cap-defaults.ts), so on a chain where 1e18 wei is one dollar a 20 USDC transfer reserves about a thousand times an organisation's whole daily budget. That is real, but it fails closed, nothing is over-spent, and Arc is an experimental testnet - so it is not a reason to hold your work. It is tracked as KEEP-1309 and is ours. Nothing on this pull request is blocked on it.

Verdict

Changes requested: one item only, the native row being suppressed on chain id rather than on a supported-token row existing, which can strand a balance in the wallet if the token seed partially fails.

@suisuss suisuss removed the decision-needed Blocked on a maintainer decision, not on the contributor label Sep 2, 2026
@Dami904

Dami904 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Pushed fixes for all review items:

  • Dedup native/token balance (blocking) - Arc's native gas balance and its ERC-20 supported_tokens row are the same USDC at two precisions (18 vs 6 decimals). Generalized the existing Tempo native-row suppression into a nativeMirrorsSupportedToken check keyed on 5042002, applied in build-withdrawable-assets.ts and chain-balance-item.tsx. Also found and fixed the same unguarded push in use-wallet-digest.ts (fundedAssets), which feeds the toolbar wallet-menu dropdown - that one wasn't in the original review but has the identical bug.
  • Testnet alias collision (blocking) - dropped aliases: ["arc"] from the testnet row in seed-chains.ts; reserved for the mainnet row when it lands, matching the Plasma/0G/Robinhood precedent.
  • INDEPENDENT_TOKEN_LIST_CHAIN_IDS - added 5_042_002 to both copies (chain-utils.ts, app/api/supported-tokens/route.ts).
  • Blockscout instance - added 5_042_002: "https://testnet.arcscan.app" to BLOCKSCOUT_INSTANCES.
  • docs/platform-reference.md - added Arc to the experimental chains table.
  • Retitled to #2230 per the check-issue-link requirement.

Not done: the chain-config entry. I don't have access to that repo as an outside contributor - it doesn't appear in anything my GitHub account can see, so I can't open a PR there. Understood that's the maintainer-side half of this.

Everything else above is pushed to this branch.

The native-row suppression for Arc/Tempo was keyed on chain id only. If a
chain's token seed partially failed, the native balance disappeared from
the wallet with no supported-token row to replace it -- invisible and
unwithdrawable. nativeMirrorsSupportedToken now also requires a matching
supported-token row to be present in the feed before suppressing native.

Consolidated the chain-id set into lib/wallet/build-withdrawable-assets.ts
as the single source of truth (it existed independently in three files;
one of the earlier fixes only touched two of them). chain-utils.ts now
re-exports it instead of re-declaring it.

Added test coverage for Arc specifically (previously only Tempo was
covered) and a regression test for the partial-seed-failure case.

@suisuss suisuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously raised, one line each: the existence-test suppressing native on a "0" placeholder - addressed, nativeMirrorsSupportedToken now requires hasFundedMirrorRow (lib/wallet/build-withdrawable-assets.ts:69-73,86-96) with a regression test. The digest re-declaring the predicate - addressed, the shape-agnostic helper is exported and used at use-wallet-digest.ts:159. Tempo's categorical rule merged into Arc's arithmetic one - addressed, isTempoChain is separate at :38 and composed at the four call sites. The missing USDC column in platform-reference.md - addressed at :161-176. The single hidesNativeRow(chainId, rows) helper - not addressed, and now hand-assembled at four sites, which is how surface four came to be missed. A fundedAssets test in use-wallet-digest.ts - not addressed.

Your claims check out, including the fourth surface: staging's use-account-assets.ts has no isTempo or mirror reference at all, so the native push really was unguarded there. I re-verified the chain facts live this round rather than carrying them forward - eth_chainId is 0x4cef52 on both RPCs, 0x3600...0000 reports symbol USDC, name USDC and 6 decimals, and the Blockscout v2 instance agrees. The WSS endpoint, which I marked UNSURE last time, now checks out: it completes the handshake and answers eth_chainId over the socket. Runbook coverage is complete, including the two surfaces beyond it (plugins/blockscout/chains.ts:24 and app/api/supported-tokens/route.ts:16), and isTestnet: true with status: "experimental" is right.

The auth label is a false positive from the filename regex matching seed-tokens.ts and supported-tokens/route.ts. I read the only permission-adjacent expression in the diff in full - isAdmin at chain-balance-item.tsx:485 and :536 - and it is unchanged; the new left operand only narrows what renders.

Blocking

  • lib/wallet/build-withdrawable-assets.ts:50-52,86-96 - the mirror is keyed on chainId rather than on the mirroring token, so it re-creates the bug this round fixed as soon as a second token is seeded. nativeMirrorsSupportedToken suppresses the native row when any supported-token row on that chain is funded. Arc testnet already carries EURC at 0x89B50855Aa3bE2F677cD6303Cec089B5F319D72a - live on the Blockscout token list, 6 decimals - and for a Circle chain seeding it is the obvious next step. -> A user holding EURC and USDC gets their native USDC row suppressed by the EURC row, and if the USDC balanceOf then fails (app/api/user/wallet/balances/route.ts:301-311 pushes "0"), the balance is invisible and unwithdrawable again. Same at the other three sites. -> Make it a Map<number, string> of chainId to mirror-token address and require a funded row at that address.

Mechanical - actionable as-is

  • Fold the four hand-composed call sites into one hidesNativeRow(chainId, rows). use-wallet-digest.ts:158 reading the set directly is fine - its rows carry no chainId - but the other three are the same two-liner written out three times, and the fourth surface was missed precisely because there was no single place to add it.

  • A fundedAssets test in use-wallet-digest.ts. It is the only changed surface with no coverage, and the only one whose Tempo behaviour changes silently.

With the team

  • Whether Tempo's native row should disappear from three surfaces. The diff removes it from the wallet chain card (chain-balance-item.tsx:450, where staging :443 renders it unconditionally), from fundedAssets and therefore the toolbar wallet menu and the prices request (use-wallet-digest.ts:157), and from the Settings assets table (use-account-assets.ts:60). Staging has no Tempo handling on any of the three. That is arguably right - it matches chain-utils.ts:1's long-standing "stablecoins only, no native token" and matches the withdraw path, which has always refused Tempo natives - but it is a display change to a live mainnet chain inside a PR titled for an Arc testnet, and I would rather decide it than let it land unstated. I'll come back. Please add it to the PR body either way.

Verdict

Changes requested - on the chainId-keyed mirror, which re-creates the fixed bug on the next token seed.

No migration and no operator step: pnpm db:seed runs in the Helm pre-upgrade hook on both environments, so the chain and token land on deploy, and a flaky Arc RPC at seed time cannot block it because seed-tokens.ts:439-445 catches per token and exits 0. I confirmed that rather than assuming, since a missed seed step is the usual way a chain addition half-lands.

Screenshots

The Arc half of the ask is discharged and I have taken requested-evidence down to one item. Covered: the chain card settled and funded, the digest in both states, the placeholder state - which I did not ask for and which is the most valuable frame here, since it is the exact regression the funded gate exists to prevent - and the fourth surface before and after.

Still outstanding: a Tempo card with tokens seeded. It was one of the four things asked for, and the decision above has made it the highest-value shot, because Tempo is now the only surface where this PR removes a row that staging renders, on a live mainnet chain, with no test covering it.

Not blocking, but worth knowing: all five are mocked rather than a live Arc wallet - image 03 shows the unit-test fixture address rather than the seeded precompile, which you labelled. They prove the render gate, which is what I asked for. They do not exercise the seeded data path from seed-tokens.ts through /api/supported-tokens to the card, so that remains unverified end to end. The loading frame I asked for is moot - I retracted the premise last round, since useWalletBalances seeds natives at "0" so no button renders.

@suisuss suisuss added the decision-needed Blocked on a maintainer decision, not on the contributor label Sep 7, 2026
…ain id

nativeMirrorsSupportedToken suppressed the native row whenever any
supported-token row on the chain was funded, so a second seeded token
(e.g. EURC on Arc) would suppress the native USDC row even with USDC's
own balance unfunded or failing to load. Key the dedup on a
chainId -> mirror token address map instead, and require the funded
row to be that specific token.

Also consolidates the isTempoChain || nativeMirrorsSupportedToken
check into a single hidesNativeRow helper used at the three call
sites that carry chainId-tagged rows, and adds test coverage for
lib/wallet/use-wallet-digest.ts, which previously had none.
@Dami904

Dami904 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Tempo mainnet + testnet, native row still hidden after the consolidation.
tempo-filtered

Arc with EURC funded and USDC unfunded — native USDC row stays visible (mirror now keyed on the token address, not chain id).
arc-eurc-scenario

@suisuss suisuss left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What this changes

One commit of your own, 0380a723, plus a merge of staging - the compare spans 332 commits but 331 of them are the merge, and the eight files that move are all yours.

NATIVE_MIRRORS_TOKEN_CHAIN_IDS becomes NATIVE_MIRROR_TOKEN_ADDRESS, a ReadonlyMap<number,string> pinning Arc to the USDC precompile; hasFundedMirrorRow takes that address and matches tokenAddress ?? address case-insensitively with a positive balance; nativeMirrorsSupportedToken returns false for unmapped chains; a new exported hidesNativeRow(chainId, rows) composes it with isTempoChain and is consumed at build-withdrawable-assets.ts:146, use-account-assets.ts:53 and chain-balance-item.tsx:389. Plus 213 lines of tests, including a new use-wallet-digest.test.ts.

Every item from the last round lands. The mirror is keyed on the token address rather than the chain id, so the EURC case is closed and regression-tested on all three surfaces; three of the four call sites are folded into the helper, with use-wallet-digest.ts:156-160 documenting why the fourth cannot be; and fundedAssets now has its own tests.

I traced the address end to end rather than taking the map on trust: app/api/user/wallet/balances/route.ts:282,302 populates tokenAddress on both the success and the caught-failure push, lib/wallet/use-wallet-balances.ts:183 and use-wallet-digest.ts:259 carry it through, the seeded address is lowercase and the helper lowercases both sides. It holds.

Does it match the description

Matches. No dependency, no CI or config file, no migration, no network call, no changed default. The auth flag on this diff is the filename regex hitting seed-tokens.ts and supported-tokens/route.ts; I read both changed guards in full - chain-balance-item.tsx:483 and :534 keep isAdmin untouched in position and value, and the operand that changed only ever narrows what renders.

Mechanical - actionable as-is

  • lib/wallet/build-withdrawable-assets.ts:77-79 - the row type is { tokenAddress?: string; address?: string; balance: string }, so hasFundedMirrorRow([{ balance: "1" }], addr) type-checks and returns false where before the increment it returned true. -> A future caller that loses its address field fails silently rather than at compile time. The failure direction is the safe one - both rows render, so a double-count rather than a stranded balance - which is why this is here and not above. Requiring at least one address field, or taking a getAddress accessor, makes it a type error.

  • lib/wallet/build-withdrawable-assets.ts:68 - the docstring still opens "Shape-agnostic" while the body now requires one of two named address fields. The sentence after it is accurate; the lead is not.

  • components/overlays/wallet/chain-utils.ts:7 - nativeMirrorsSupportedToken is now a dead re-export. Its only remaining caller is hidesNativeRow, inside the defining module.

  • lib/wallet/use-wallet-digest.ts:162 - the local const hidesNativeRow shadows the name of the exported helper it deliberately does not use. Correct today, but this is the one file that must not silently start importing the helper, and a distinct local name - the way chain-balance-item.tsx:389 chose hidesNativeBalanceRow - makes that impossible rather than merely unlikely.

Screenshots

The funded, placeholder, EURC and Tempo captures cover what they claim, and the before-fix double-count shot is the useful one. What is still missing is the loading frame: useWalletBalances seeds balances with loading: true while supportedTokenBalances is empty, so hidesNativeRow is false for that frame and the Arc native row renders with a spinner before disappearing. One capture of the Arc card in that frame closes the request.

With the team

  • Whether Tempo's native row should disappear from the chain card, the settings assets table and the wallet digest at once. hidesNativeRow:136 still composes isTempoChain, so all three suppress a row staging renders. It is now pinned by tests on all three surfaces, which is an improvement over last round either way. I'm weighing consistency with the mirror rule against the fact that Tempo's suppression is categorical rather than derived from a funded row - the cost of getting it wrong is a hidden balance on one side and a double-count on the other. I'm settling it with the core team and will come back. Nothing here is blocked on you.

Verdict

Changes requested on the four mechanical items only - the mirror rewrite is correct on every surface I could reach, and the one open question is mine.

hasFundedMirrorRow now requires a row carry one of its two address
fields at compile time instead of silently reading as unfunded when
both are absent. Drops the unused nativeMirrorsSupportedToken
re-export from chain-utils.ts, and renames the local hides-native-row
calculation in use-wallet-digest.ts so it can't collide with the
exported helper of the same name it deliberately doesn't call.
@suisuss suisuss removed the decision-needed Blocked on a maintainer decision, not on the contributor label Sep 10, 2026
@suisuss suisuss changed the title feat: #2230 add Arc testnet to wallet with USDC feat: #2230 add Arc chain with USDC Tracking in Wallet Sep 10, 2026
@suisuss

suisuss commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

@Dami904 We are going to setup this PR to setup both mainnet (when it goes live on the 16th of Sep) and testnet.

@suisuss suisuss added the hold waiting on something else to go live label Sep 10, 2026
Registers Circle's Arc mainnet alongside the testnet this PR already adds.
Same USDC-as-gas model and the same USDC precompile at
0x3600000000000000000000000000000000000000, verified at 6 decimals with
eth_call against https://rpc.arc-scan.org.

Two Arc mainnet limitations shape the diff.

No public WebSocket endpoint. CHAIN_CONFIG[5042] omits publicWssDefault, so
getWssUrl returns undefined and seed-chains writes NULL into
chains.defaultPrimaryWss - the sanctioned path for a WSS-less chain. Event
triggers are then skipped per workflow by the event-tracker's workflow-mapper
and the block dispatcher's chain monitor throws and is dropped from the
liveness map. Both log, neither crashes, and neither falls back to HTTP
polling, so event and block triggers do not fire on Arc mainnet until an
operator supplies a WSS URL through chain-config.

No reachable block explorer. arcscan.app has no DNS record, arc-scan.org
serves a Next.js app rather than a Blockscout API and returns 403 on /tx and
/address, and explorer.arc.io is behind Circle's Cloudflare Access. The chain
is seeded with no EXPLORER_CONFIG_TEMPLATES entry and no chainToDefaultIdMap
line, so no explorer_configs row is written. The seed warns and skips rather
than throwing, /api/chains returns null explorer fields, the ABI route answers
404, and every wallet explorer link is guarded on null.

Arc mainnet is left on the global gas defaults deliberately. Its base fee is a
flat 20 gwei across recent blocks with zero observed priority tips and a
gasUsedRatio of 0.0, so the 2.0x limit multiplier and 0.1 gwei priority floor
are safe; no multiplier was invented for a chain nobody has benchmarked.
@suisuss suisuss changed the title feat: #2230 add Arc chain with USDC Tracking in Wallet feat: #2230 add Arc mainnet and testnet to wallet with USDC Sep 10, 2026
@Dami904

Dami904 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

ok then, sure

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

Labels

changes-requested Triage: reviewed, changes needed from the contributor hold waiting on something else to go live requested-evidence Screenshots or video requested from the contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants