Skip to content

fix(swap-widget): clear stale manual receive address on chain change#12353

Merged
kaladinlight merged 2 commits into
developfrom
fix/swap-widget-receive-address-state
May 19, 2026
Merged

fix(swap-widget): clear stale manual receive address on chain change#12353
kaladinlight merged 2 commits into
developfrom
fix/swap-widget-receive-address-state

Conversation

@kaladinlight
Copy link
Copy Markdown
Member

@kaladinlight kaladinlight commented May 18, 2026

Description

Fixes SS-5660: a manual receive address persisted after switching the buy network, leaking the previous chain's address into quote requests and balance fetches. The downstream symptoms were swap failures on submit and 400 Bad Request from mempool.space/api/address/0x… when an EVM address was handed to a Bitcoin balance fetcher.

Two parts:

1. Receive-address state (primary fix)SwapWidgetCore now gates the derived receiveAddress by validity against the current buyChainId. A custom address is only consumed if it validates for the active buy chain; otherwise consumers see walletReceiveAddress. A cleanup effect clears customReceiveAddress when it becomes invalid after a chain change, so the address modal state stays consistent with what gets sent on-wire.

2. Address validation hardening (alongside fix) — the validity check that the gate relies on was a regex-only shape match; this changes the validators to proper checksum decoding so cross-chain false positives don't leak:

  • bs58check for legacy P2PKH / P2SH (BTC, LTC, DOGE) — catches typos via the SHA256d 4-byte checksum and distinguishes chains by version byte.
  • bech32 / bech32m for SegWit and Taproot (BTC, LTC) with BIP350 codec-vs-witness-version enforcement.
  • cashaddrjs for BCH CashAddr (polymod checksum + prefix match).
  • PublicKey decode for Solana (32-byte length check, replacing the 32-44-char base58 length filter that let through e.g. Bitcoin addresses).
  • Cosmos / THORChain / MAYAChain stay bech32-based but are consolidated into a COSMOS_SDK_VALIDATORS table mirroring UTXO_VALIDATORS.
  • AddressInputModal trims input once at the boundary so the validated value and stored value agree.

Covers BIP44 / BIP49 / BIP84 / Taproot where each chain supports them. Litecoin re-accepts the legacy pre-2018 P2SH 3… form (version byte 0x05) to keep older BIP49 wallets working — same intentional cross-chain ambiguity already present for BCH legacy.

Issue (if applicable)

closes #12321

Risk

Medium. Touches the swap-widget receive-address derivation and the validator used by the address input modal. Addresses with corrupted checksums that previously passed the shape-only regex will now be rejected — this is the intended behavior. Legacy 0x05 (BTC/LTC P2SH collision) and BCH legacy (BTC version bytes) are still accepted for backwards compatibility.

New deps in packages/swap-widget/package.json:

  • bs58check@^4.0.0
  • cashaddrjs@^0.3.12
  • @types/bs58check@^2.1.0

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

UTXO chains (BTC, BCH, LTC, DOGE), Cosmos SDK chains (Cosmos Hub, THORChain, MAYAChain), EVM, and Solana on the swap-widget side only. No on-chain transaction signing changes.

Testing

Engineering

Run the new unit suite:

cd packages/swap-widget && pnpm exec vitest run src/utils/__tests__/addressValidation.test.ts

65 tests covering per-chain valid forms, cross-chain rejections, checksum corruption (8 regression cases that the previous regex implementation would have accepted), BIP350 codec/witness-version mismatches, empty input, and getAddressFormatHint for each namespace.

To reproduce SS-5660 manually in the demo app:

  1. cd packages/swap-widget && pnpm dev
  2. Open the buy-side address modal and paste a manual receive address (e.g., an EVM 0x…)
  3. Change the buy asset to a different chain type (e.g., Bitcoin or Solana)
  4. Before this fix: stale 0x… stays selected; the balance fetch fires GET https://mempool.space/api/address/0x… and 400s; if the user proceeds, the swap fails on submit
  5. After this fix: the stale custom address is cleared, the wallet-derived receive address for the new chain is used, no bad request fires

Operations

  • 🏁 My feature is behind a flag and doesn't require operations testing (yet)

Functional QA in the swap-widget demo:

  1. Switch between buy assets of different chain types (EVM → Bitcoin, Bitcoin → Solana, etc.) and verify the receive address always matches the buy chain.
  2. Paste a valid address for the wrong chain into the receive modal — should be rejected with the correct chain name in the error.
  3. Paste an address with a single character changed (typo) — should now be rejected (previously regex-only would accept).
  4. For BCH, both CashAddr (bitcoincash:q…) and legacy (1…) should be accepted.
  5. For Litecoin, both modern (M…) and legacy (3…) P2SH forms should be accepted.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enhanced address validation across multiple blockchains (Bitcoin, Litecoin, Dogecoin, Bitcoin Cash, Cosmos SDK, Solana, and EVM).
    • Custom receive address now validates against the selected blockchain and falls back when invalid.
  • Bug Fixes

    • Address inputs are trimmed before validation and submission.
    • Improved validation logic and confirm-button behavior for custom receive addresses.
  • Tests

    • Added comprehensive address validation test coverage, including cross-chain and regression cases.

Review Change Stack

Closes SS-5660. Manual receive addresses persisted across buy-chain
switches, leaking the previous chain's address into quote requests and
balance fetches (e.g. an EVM 0x… address hitting mempool.space and 400ing).

Gate the derived receiveAddress by validity against the current buyChainId
in SwapWidgetCore so consumers never observe a stale value, and clear
customReceiveAddress when invalid for the new chain so modal state stays
in sync.

Replace shape-only regex validation with checksum decoding: bs58check
(BTC / LTC / DOGE legacy), bech32 + bech32m with BIP350 enforcement
(BTC / LTC SegWit + Taproot), cashaddrjs (BCH CashAddr), and PublicKey
length check (Solana). Covers BIP44/49/84 where each chain supports it.
Trim input once at the AddressInputModal boundary so validated value
and stored value agree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kaladinlight kaladinlight requested a review from a team as a code owner May 18, 2026 22:04
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9a57569-6145-4839-a7be-b6afc85eed95

📥 Commits

Reviewing files that changed from the base of the PR and between 0ccd4e1 and 420562d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • packages/swap-widget/package.json
  • packages/swap-widget/src/vite-env.d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/swap-widget/src/vite-env.d.ts

📝 Walkthrough

Walkthrough

Replaces regex validation with decoding-based validators routed by CAIP, adds bs58check/cashaddrjs and types, trims and validates modal input, gates and clears custom receive addresses by current chain, and adds comprehensive tests covering UTXO/EVM/Cosmos/Solana and edge cases.

Changes

Address Validation and Network-Aware Custom Receive Address

Layer / File(s) Summary
Dependencies and TypeScript types
packages/swap-widget/package.json, packages/swap-widget/src/vite-env.d.ts
Introduces bs58check and cashaddrjs and adds an ambient TypeScript module declaration for cashaddrjs.
Core validation implementation with decoding-based validators
packages/swap-widget/src/utils/addressValidation.ts
Replaces regex validation with decoding-based validators (base58check, bech32/bech32m, CashAddr), routes by CAIP fromChainId, adds UTXO_VALIDATORS and COSMOS_SDK_VALIDATORS, and updates validateAddress and getAddressFormatHint.
Address input trimming in AddressInputModal
packages/swap-widget/src/components/AddressInputModal.tsx
Memoizes trimmed input, validates the trimmed value, revalidates on confirm, and updates confirm-button disabled state to use trimmed validity.
Custom receive address validation and network-aware clearing in SwapWidget
packages/swap-widget/src/components/SwapWidget.tsx
Validates customReceiveAddress against the current buyChainId, uses it only when valid, and clears it when it becomes invalid for the selected chain.
Comprehensive test suite for address validation across all chains
packages/swap-widget/src/utils/__tests__/addressValidation.test.ts
Adds deterministic fixtures and Vitest coverage for BTC/LTC/DOGE/BCH UTXO validators, EVM/Cosmos/Solana validations, edge cases (whitespace/empty), checksum/BIP350 regression tests, and getAddressFormatHint expectations.

Sequence Diagram(s)

sequenceDiagram
  participant User as User Input
  participant AddressModal as AddressInputModal
  participant Validator as validateAddress
  participant SwapWidget
  participant State as Component State

  User->>AddressModal: Enter address (may include whitespace)
  AddressModal->>AddressModal: useMemo trim input
  AddressModal->>Validator: validateAddress(trimmedInput, chainId)
  Validator-->>AddressModal: { valid, error? }
  AddressModal->>AddressModal: Update confirm disabled state

  User->>AddressModal: Click confirm
  AddressModal->>Validator: Re-validate trimmedInput
  Validator-->>AddressModal: { valid, error? }
  alt valid
    AddressModal->>SwapWidget: onAddressChange(trimmedAddress)
    SwapWidget->>State: set customReceiveAddress
  else invalid
    AddressModal->>AddressModal: Show error, keep open
  end

  User->>SwapWidget: Switch network (buyChainId changes)
  SwapWidget->>Validator: validateAddress(customReceiveAddress, newChainId)
  Validator-->>SwapWidget: { valid: false }
  alt invalid for new chain
    SwapWidget->>SwapWidget: clear customReceiveAddress
    SwapWidget->>State: receiveAddress = walletReceiveAddress
  else still valid
    SwapWidget->>State: receiveAddress = customReceiveAddress
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I trimmed the spaces, checked each byte,
Decoded bech32 through the night,
When chains swap places, bad addresses flee—
No ghost txs for you or me!
Hopped, validated, all set right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main fix: clearing stale manual receive addresses when the chain/network changes, which is the core objective of this PR.
Linked Issues check ✅ Passed The PR fully addresses issue #12321 by implementing address validation on chain change, clearing invalid manual addresses, and trimming inputs to prevent stale addresses.
Out of Scope Changes check ✅ Passed All changes are scoped to address validation improvements and the core fix; new dependencies and comprehensive test coverage directly support the stated objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/swap-widget-receive-address-state

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/swap-widget/package.json (1)

88-88: ⚡ Quick win

Remove redundant @types/bs58check to avoid type declaration conflicts.

Line 88 adds @types/bs58check, but bs58check@4.0.0 already includes built-in TypeScript declarations. Installing both packages simultaneously creates unnecessary duplication and risks type inconsistencies as the packages evolve independently.

♻️ Proposed fix
-    "`@types/bs58check`": "^2.1.0",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/package.json` at line 88, Remove the redundant type
package by deleting the "`@types/bs58check`" dependency entry from package.json;
keep the runtime package "bs58check@4.0.0" which already provides its own
TypeScript declarations to avoid duplicate type definitions and potential
conflicts during compilation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/swap-widget/src/utils/addressValidation.ts`:
- Around line 23-27: The isValidBase58Check function currently only checks the
version byte; update it to also validate payload length by ensuring
bs58check.decode(address) returns a Buffer of exactly 21 bytes (1 version byte +
20-byte hash160) before returning allowedVersionBytes.includes(decoded[0]); keep
the existing try/catch and failure semantics, and reference the function name
isValidBase58Check, the bs58check.decode call and allowedVersionBytes to locate
where to add this length check.
- Around line 55-61: The current bech32 decode block returns true without
checking SegWit witness version and program length; after decoding (the
variables prefix, words, witnessVersion from codec.decode(lower)), validate that
witnessVersion is an integer between 0 and 16 inclusive, convert the remaining
words to a witness program byte array (using the appropriate fromWords utility
for the codec) and ensure the program length is between 2 and 40 bytes, and
additionally enforce that if witnessVersion === 0 then the program length is
exactly 20 or 32 bytes; only return true if all these checks pass and keep the
existing codec (bech32 vs bech32m) checks in place.

---

Nitpick comments:
In `@packages/swap-widget/package.json`:
- Line 88: Remove the redundant type package by deleting the "`@types/bs58check`"
dependency entry from package.json; keep the runtime package "bs58check@4.0.0"
which already provides its own TypeScript declarations to avoid duplicate type
definitions and potential conflicts during compilation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f62dc2d5-d491-4019-8f2d-b229b3187ca3

📥 Commits

Reviewing files that changed from the base of the PR and between 38a0b55 and 0ccd4e1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • packages/swap-widget/package.json
  • packages/swap-widget/src/components/AddressInputModal.tsx
  • packages/swap-widget/src/components/SwapWidget.tsx
  • packages/swap-widget/src/utils/__tests__/addressValidation.test.ts
  • packages/swap-widget/src/utils/addressValidation.ts
  • packages/swap-widget/src/vite-env.d.ts

Comment thread packages/swap-widget/src/utils/addressValidation.ts
Comment thread packages/swap-widget/src/utils/addressValidation.ts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kaladinlight kaladinlight merged commit a068e0f into develop May 19, 2026
4 checks passed
@kaladinlight kaladinlight deleted the fix/swap-widget-receive-address-state branch May 19, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Manual address stays after switching network

1 participant