Skip to content

fix(checkout-widgets): parse hex chain ids so the bridge stops demanding a network switch - #2944

Merged
alex-connolly merged 1 commit into
mainfrom
fix/bridge-chain-id-hex-parsing
Aug 7, 2026
Merged

fix(checkout-widgets): parse hex chain ids so the bridge stops demanding a network switch#2944
alex-connolly merged 1 commit into
mainfrom
fix/bridge-chain-id-hex-parsing

Conversation

@alex-connolly

Copy link
Copy Markdown
Contributor

Customer impact

Illuvium users cannot bridge ILV from Immutable zkEVM back to Ethereum. The bridge prompts "Switch to Immutable zkEVM", and neither the in-widget switch nor a manual MetaMask switch clears it. Reported against toolkit.immutable.com/ethereum-bridge/, reproduced by the reporter and confirmed here against the deployed bundle.

Root cause

eth_chainId returns a hex quantity per EIP-695 (0x343b). BridgeReviewSummary parsed it with parseInt(value, 10), which returns 0 for any hex input. The current chain therefore never matched from.network, so the network-switch drawer opened on every submit — every chain, every wallet.

Introduced in #2928 (chore(build): upgrade repo tooling to latest versions), which removed an eslint-disable-next-line radix and satisfied the rule by hardcoding radix 10:

-      // eslint-disable-next-line radix
-      const parsedChainId = parseInt(currentChainId.toString());
+      const parsedChainId = parseInt(String(currentChainId), 10);

The suppression was load-bearing. The radix was omitted deliberately so 0x would auto-detect.

For Passport the failure was terminal rather than merely annoying. Passport rejects wallet_switchEthereumChain by design, and NetworkSwitchDrawer was the only checkout.switchNetwork call site without a catch, so every click produced an unhandled promise rejection. That is the Switching networks with Passport provider is not supported flood in the customer's console; the accompanying 429s are Sentry rate-limiting that flood, not an API limit.

Verified against production: the chunk hash in the customer's stack trace (WalletApproveHero-Btpgg9el.js) matches @imtbl/checkout-widgets@2.24.4 exactly, and line 500 of that deployed file is the uncaught switchNetwork call.

Affected versions

Introduced 2.24.0
Affected 2.24.02.24.5
Last good 2.23.0

Changes

  • Add parseChainId to lib/chains, with tests pinning the hex cases
  • Use it at every eth_chainId call site. The other four (SwapForm, WalletList, ReadyToConnect, SwitchNetworkZkEVM) were not broken, but each still carried the same fragile eslint-disable radix and was one autofix away from this bug
  • Catch switch failures in NetworkSwitchDrawer and surface them as copy instead of an unhandled rejection
  • Hide the switch button for Passport, which cannot switch networks

Testing

  • pnpm typecheck clean
  • 26 suites / 213 tests pass across lib, bridge, swap, connect
  • New parseChainId cases cover hex, decimal, number, bigint, and invalid input

Follow-ups (not in this PR)

no-floating-promises is set to warn, which is why the uncaught switchNetwork shipped silently — there are ~230 more repo-wide. A separate PR promotes that and related rules to error and burns them down.

Note for that PR: do not enable the radix rule. It is currently off (style category), and its autofix is what caused this outage.

🤖 Generated with Claude Code

…ing a network switch

`eth_chainId` returns a hex quantity per EIP-695. BridgeReviewSummary parsed it
with `parseInt(value, 10)`, which yields 0 for any hex input, so the current
chain never matched `from.network` and the "Switch to <chain>" drawer opened on
every submit — on every chain, for every wallet. Withdrawals from zkEVM were
unusable.

This was introduced in #2928: the tooling upgrade removed an
`eslint-disable-next-line radix` and satisfied the rule by hardcoding radix 10.
The suppression was load-bearing — the radix was omitted deliberately so hex
would auto-detect.

For Passport the failure was terminal rather than merely annoying: Passport
rejects `wallet_switchEthereumChain` by design, and NetworkSwitchDrawer was the
only `checkout.switchNetwork` call site without a catch, so each click produced
an unhandled rejection and flooded error reporting instead of telling the user
anything.

- Add `parseChainId` to lib/chains, with tests pinning the hex cases
- Use it at every `eth_chainId` call site; the other four still carried the same
  fragile `eslint-disable radix` and were one autofix away from this bug
- Catch switch failures in NetworkSwitchDrawer and surface them as copy
- Hide the switch button for Passport, which cannot switch networks

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alex-connolly
alex-connolly requested a review from a team as a code owner August 7, 2026 07:45
@nx-cloud

nx-cloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 68070f6

Command Status Duration Result
nx affected -t build,test ✅ Succeeded 1m 9s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-08-07 07:47:58 UTC

@alex-connolly
alex-connolly added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit ae559df Aug 7, 2026
7 checks passed
@alex-connolly
alex-connolly deleted the fix/bridge-chain-id-hex-parsing branch August 7, 2026 07:55
alex-connolly added a commit that referenced this pull request Aug 7, 2026
…fallout

Follow-up to #2944. That bug shipped because the rule which would have caught
it was set to `warn`, so nothing failed CI. This promotes three rules that
catch runtime defects rather than style preferences, and clears the codebase
of them so the gate stays green.

Promoted to error:
- typescript/no-non-null-asserted-optional-chain — `a?.b!` is undefined at
  runtime while typed as present
- typescript/no-base-to-string — stringifying an object yields "[object
  Object]", silently gutting error messages and analytics payloads
- eslint/preserve-caught-error — rethrowing without `cause` discards the
  original stack

Two real defects surfaced by this:

- tokenBridge.getFee passed `async () => {...}` to Promise.all without
  invoking it, so Promise.all resolved the function object and the chain-id
  validation never ran. Note this restores validation that has been dead:
  callers passing mismatched chain ids will now get the intended error.
- PayWithCoins reported sale failures via `error.toString()` on a
  SignOrderError ({ type, data }), so every failure event carried the literal
  string "[object Object]" instead of the failure type.

Also fixed: an undefined provider could reach NetworkSwitchDrawer via
`from?.browserProvider!`; OrderSummary dereferenced a possibly-missing
smartCheckoutResult; SaleWidget built block-explorer links containing
"undefined"; fundingBalanceFees pushed fees with a missing required token.

`oxlint --fix` was not used. On this repo it rewrites `.sort()` to `.toSorted()`
(ES2023, while tsconfig targets ES2022), drops entries from React dependency
arrays, and leaves dead whitespace. Every change here is hand-written.

Deferred, with counts, to keep this reviewable:
- no-floating-promises (230) — the rule closest to #2944's root cause. Needs
  per-site judgment (`void` for fire-and-forget, real handling for
  user-triggered actions), so it gets its own PR.
- no-unsafe-enum-comparison (93) — needs a canonical type per comparison
- react-hooks/exhaustive-deps (235) — every fix changes render behaviour

Do not enable the `radix` rule. It is off (style category), and its autofix is
what caused #2944.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants