Skip to content

Conversation

@NeOMakinG
Copy link
Collaborator

@NeOMakinG NeOMakinG commented Jan 12, 2026

Description

Introduces an embeddable swap widget package (@shapeshiftoss/swap-widget) that enables multi-chain token swaps using ShapeShift's aggregation API. This is a standalone React component that can be integrated into third-party applications with minimal configuration.

Key Features

  • Multi-chain Support: Supports 17+ chains including Ethereum, Arbitrum, Optimism, Polygon, Base, Avalanche, BSC, Gnosis, Bitcoin, Litecoin, Dogecoin, Bitcoin Cash, Cosmos, THORChain, MAYAChain, and Solana
  • DEX Aggregation: Fetches quotes from multiple DEXs/aggregators: THORChain, MAYAChain, CoW Swap, 0x, 1inch, Portals, Chainflip, Jupiter, Bebop, Relay, ButterSwap, ArbitrumBridge
  • EVM Swap Execution: Direct swap execution for EVM chains when wallet is connected via walletClient prop
  • Non-EVM Redirect: Graceful redirect to app.shapeshift.com for non-EVM swaps (BTC, SOL, Cosmos, etc.)
  • Built-in Wallet Connection: Optional RainbowKit integration via enableWalletConnection prop
  • Theming: Dark/light mode support with full theme customization (colors, fonts, border radius)
  • Asset Filtering: allowedChainIds, disabledChainIds, allowedAssetIds, disabledAssetIds props
  • Fixed Receive Address: defaultReceiveAddress prop for locking destination address
  • Performance Optimizations: Request throttling with p-queue, virtualized token list with react-virtuoso
  • Mobile Responsive: Designed for both desktop and mobile viewports

Components

  • SwapWidget - Main entry point
  • TokenSelectModal - Virtualized token selector with search
  • QuotesModal - Compare quotes from multiple swappers
  • SettingsModal - Slippage and receive address configuration
  • AddressInputModal - External receive address input
  • WalletProvider - RainbowKit wallet connection wrapper

API Hooks

  • useAssets / useAssetById / useAssetSearch / useAssetsByChainId - Asset data fetching
  • useChains - Chain metadata
  • useBalances - Wallet balance fetching (EVM)
  • useMarketData - USD price data
  • useSwapRates - Rate/quote fetching with throttling
  • useSwapQuote - Full quote fetching for execution

Deployment

  • Deployed to widget.shapeshift.com via Railway
  • Standalone Dockerfile for containerized deployment
  • Independent package with own dependencies (React 18, wagmi, viem, RainbowKit)

Risk

Low - This is a new standalone package that does not affect the main web application.

Testing

Engineering

  1. Run yarn dev:swap-widget to start the demo app
  2. Test token selection, quote fetching, and swap execution
  3. Verify EVM swaps work with connected wallet
  4. Verify non-EVM assets redirect to app.shapeshift.com
  5. Test theming (dark/light mode)
  6. Test mobile responsiveness

Operations

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

This is a standalone widget package and doesn't affect the main app.

Screenshots

Widget is deployed at: https://widget.shapeshift.com

Summary by CodeRabbit

  • New Features

    • Full-featured Swap Widget: multi-chain swaps, wallet connect, token picker, quote/route selector, settings & address modals, market/balance fetching hooks, address validation, redirect helpers, theming, and a demo app.
  • Documentation

    • Comprehensive README with installation, usage, props, examples, theming, and supported-chains guidance.
  • Chores

    • New package manifest, build/Vite and TypeScript configs, Docker/deploy support, and root dev script to run the widget locally.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 12, 2026

📝 Walkthrough

Walkthrough

Adds a new @shapeshiftoss/swap-widget package including components, hooks, types, utils, API client, demo app, build/config (Vite, TS, Docker, railway), and a root npm script to run the widget workspace. No changes to existing packages outside root script addition.

Changes

Cohort / File(s) Summary
Root config
package.json, railway.json
Added dev:swap-widget root npm script; added Railway config referencing packages/swap-widget/Dockerfile and restart policy.
Package manifest & build config
packages/swap-widget/package.json, packages/swap-widget/vite.config.ts, packages/swap-widget/tsconfig.json, packages/swap-widget/tsconfig.node.json
New package manifest and configs; Vite supports lib vs app builds; strict TypeScript settings.
Docker & deploy
packages/swap-widget/Dockerfile
Multi-stage Dockerfile: build stage and serve stage exposing port 3000.
Public entry & env
packages/swap-widget/index.html, packages/swap-widget/src/vite-env.d.ts, packages/swap-widget/src/index.ts
Added demo HTML entry, Vite environment typings (window.ethereum), and public re-exports.
Types & core constants
packages/swap-widget/src/types/index.ts, packages/swap-widget/src/constants/...
New domain types and helpers (Asset, Chain, TradeRate, ThemeConfig, chain groups), chain metadata, swapper icons/colors, format/parse/truncate utilities.
API client
packages/swap-widget/src/api/client.ts
New ApiClient factory with getAssets, getRates, getQuote; supports baseUrl, apiKey, timeout and error handling.
Utilities
packages/swap-widget/src/utils/addressValidation.ts, packages/swap-widget/src/utils/redirect.ts
Multi-chain address validators and hints; ShapeShift trade URL builder, redirect helper, and widget execution gate.
Data & market hooks
packages/swap-widget/src/hooks/useAssets.ts, packages/swap-widget/src/hooks/useMarketData.ts
Asset manifest loading, chain lists, search scoring, and CoinGecko-backed market data hooks with caching.
Balance hooks
packages/swap-widget/src/hooks/useBalances.ts
useAssetBalance and batched useEvmBalances with concurrency queue, native/ERC-20 handling, formatting and loading tracking.
Swap hooks
packages/swap-widget/src/hooks/useSwapRates.ts, packages/swap-widget/src/hooks/useSwapQuote.ts
React Query hooks to fetch/sort rates and fetch swap quotes (with query keys, enable flags, stale times).
UI components & styles
packages/swap-widget/src/components/*.tsx, packages/swap-widget/src/components/*.css, packages/swap-widget/src/components/WalletProvider.tsx
New SwapWidget and companion components (TokenSelectModal, QuoteSelector, QuotesModal, AddressInputModal, SettingsModal, WalletProvider) plus comprehensive CSS files and wallet integration.
Demo app
packages/swap-widget/src/demo/{App.tsx,App.css,main.tsx}
Demo harness with theme presets, live customizer, copy-to-clipboard, and QueryClient/RainbowKit/Wagmi integration.
Support & entry files
packages/swap-widget/src/demo/index.html, packages/swap-widget/src/*
Additional helpers, hooks, typings, index exports, and demo entry (main.tsx).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as SwapWidget
    participant Assets as Asset Hooks
    participant API as ApiClient
    participant Wallet as WalletClient

    User->>UI: open token selector
    UI->>Assets: fetch assets/chains/balances
    Assets-->>UI: assets, chains, balances
    User->>UI: choose sell token & amount
    UI->>API: getRates(sell,buy,amount)
    API-->>UI: rates[]
    User->>UI: open quotes modal / select rate
    UI->>UI: compute selection, close modal
    User->>UI: confirm swap
    UI->>API: getQuote(params)
    API-->>UI: quote with tx data
    UI->>Wallet: submit transaction
    Wallet-->>UI: tx hash / status
    UI->>User: show success / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • 0xApotheosis

Poem

🐰 I hopped through code and found a nest,
New widgets sprouting, styled the best,
Tokens, quotes, and demos bright,
Wallets buzz into the night,
Swap away — the rabbit cheers with zest!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: swap widget' clearly and concisely summarizes the main change: adding a new swap widget feature. It is specific enough to convey the primary change without being overly verbose.
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 unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch widget-poc


📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3caa39b and 32cea56.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (2)
  • package.json
  • packages/swap-widget/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/swap-widget/package.json
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10323
File: src/components/ButtonWalletPredicate/ButtonWalletPredicate.tsx:7-7
Timestamp: 2025-08-27T09:47:06.275Z
Learning: In shapeshift/web project, NeOMakinG consistently prefers to defer UI/UX improvements and refactoring work (like the Drawer.Close hack fix in ButtonWalletPredicate.tsx) to follow-up PRs rather than expanding the scope of feature PRs, even when the improvements would enhance robustness.
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10231
File: src/components/AssetSearch/components/AssetList.tsx:2-2
Timestamp: 2025-08-08T15:00:49.887Z
Learning: Project shapeshift/web: NeOMakinG prefers avoiding minor a11y/UI nitpicks (e.g., adding aria-hidden to decorative icons in empty states like src/components/AssetSearch/components/AssetList.tsx) within feature PRs; defer such suggestions to a follow-up instead of blocking the PR.
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10231
File: src/components/MultiHopTrade/components/TradeInput/components/HighlightedTokens.tsx:14-14
Timestamp: 2025-08-08T15:00:22.321Z
Learning: In shapeshift/web reviews for NeOMakinG, avoid nitpicks to change deep-relative imports to '@/…' alias paths within feature/non-refactor PRs; defer such style-only changes to a dedicated follow-up refactor unless they fix an issue.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10418
File: src/plugins/walletConnectToDapps/components/header/WalletConnectToDappsHeaderButton.tsx:0-0
Timestamp: 2025-09-08T22:00:48.005Z
Learning: gomesalexandre dismissed an aria-label accessibility suggestion with "meh" in PR `#10418` for WalletConnectToDappsHeaderButton.tsx, consistent with the team's pattern of deferring minor a11y improvements to follow-up PRs rather than expanding feature PR scope.
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10376
File: packages/contracts/src/viemClient.ts:39-40
Timestamp: 2025-08-27T09:51:36.901Z
Learning: NeOMakinG confirmed that issues flagged by CodeRabbit that predate the PR changes and are unrelated to the PR's core objective should be addressed separately, not within the current PR scope.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11170
File: patches/@shapeshiftoss+bitcoinjs-lib+7.0.0-shapeshift.0.patch:9-19
Timestamp: 2025-11-25T21:43:10.838Z
Learning: In shapeshift/web, gomesalexandre will not expand PR scope to fix latent bugs in unused API surface (like bitcoinjs-lib patch validation methods) when comprehensive testing proves the actual used code paths work correctly, preferring to avoid costly hdwallet/web verdaccio publish cycles and full regression testing for conceptual issues with zero runtime impact.
Learnt from: premiumjibles
Repo: shapeshift/web PR: 10361
File: src/pages/Markets/components/CardWithSparkline.tsx:83-92
Timestamp: 2025-08-25T23:32:13.876Z
Learning: In shapeshift/web PR `#10361`, premiumjibles considered the nested button accessibility issue (ChartErrorFallback retry Button inside Card rendered as Button in CardWithSparkline.tsx) out of scope for the error boundaries feature PR, consistent with deferring minor a11y improvements to follow-up PRs.
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11335
File: packages/swapper/src/swappers/CetusSwapper/utils/helpers.ts:3-3
Timestamp: 2025-12-09T21:07:22.474Z
Learning: In packages/swapper/src/swappers/CetusSwapper, mysten/sui types (SuiClient, Transaction) must be imported from the nested path within cetusprotocol/aggregator-sdk (e.g., 'cetusprotocol/aggregator-sdk/node_modules/mysten/sui/client') because the aggregator SDK bundles its own version of mysten/sui. Direct imports from 'mysten/sui' break at runtime even when specified in package.json.
Learnt from: CR
Repo: shapeshift/web PR: 0
File: .cursor/rules/swapper.mdc:0-0
Timestamp: 2025-11-24T21:20:57.909Z
Learning: Applies to packages/swapper/src/swappers/**/*.ts : Adhere to the Swapper directory structure: each swapper resides in packages/swapper/src/swappers/<SwapperName>/ with required files (SwapperName.ts, endpoints.ts, types.ts, utils/constants.ts, utils/helpers.ts)
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10222
File: package.json:202-202
Timestamp: 2025-08-08T20:16:12.898Z
Learning: In shapeshift/web, the semver package must be included in dependencies (not devDependencies) because hdwallet packages have transient dependencies that require semver but don't ship it themselves. This ensures semver is available at runtime for hdwallet functionality.
Learnt from: CR
Repo: shapeshift/web PR: 0
File: .cursor/rules/swapper.mdc:0-0
Timestamp: 2025-11-24T21:20:57.909Z
Learning: Applies to packages/swapper/src/index.ts : Export unique functions and types from packages/swapper/src/index.ts only if needed for external consumption
Learnt from: kaladinlight
Repo: shapeshift/web PR: 10376
File: vite.config.mts:136-137
Timestamp: 2025-08-29T18:09:45.982Z
Learning: In the ShapeShift web repository vite.config.mts, the commonjsOptions.exclude configuration using bare package name strings like ['shapeshiftoss/caip', 'shapeshiftoss/types'] works correctly for excluding specific packages from CommonJS transformation, despite theoretical concerns about module ID matching patterns.
Learnt from: CR
Repo: shapeshift/web PR: 0
File: .cursor/rules/swapper.mdc:0-0
Timestamp: 2025-11-24T21:20:57.909Z
Learning: Applies to packages/swapper/src/constants.ts : Register new swappers in packages/swapper/src/constants.ts with an entry in the swappers registry mapping SwapperName enum to swapper implementation
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11537
File: packages/caip/src/adapters/coincap/generate.ts:7-7
Timestamp: 2026-01-03T15:12:57.968Z
Learning: In shapeshift/web, when scripts in packages/caip run via `yarn workspace shapeshiftoss/caip generate`, the working directory is `packages/caip`, so dotenv paths should use `../../.env` to reach the monorepo root .env file, not `.env`. The `-T` flag only affects which node_modules is used (root's), not the working directory.
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10323
File: src/pages/RFOX/components/Stake/components/StakeSummary.tsx:112-114
Timestamp: 2025-08-22T13:00:44.879Z
Learning: NeOMakinG prefers to keep PR changes minimal and focused on the core objectives, avoiding cosmetic or defensive code improvements that aren't directly related to the PR scope, even when they would improve robustness.
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10128
File: .cursor/rules/error-handling.mdc:266-274
Timestamp: 2025-07-29T10:35:22.059Z
Learning: NeOMakinG prefers less nitpicky suggestions on documentation and best practices files, finding overly detailed suggestions on minor implementation details (like console.error vs logger.error) too granular for cursor rules documentation.
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10380
File: src/pages/Dashboard/components/AccountList/AccountTable.tsx:60-0
Timestamp: 2025-09-02T08:34:08.157Z
Learning: NeOMakinG prefers code review comments to focus only on actual PR changes, not pre-existing code issues, unless there are critical security or correctness concerns directly related to the new functionality.
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10234
File: src/components/MultiHopTrade/hooks/useGetTradeQuotes/hooks/useTrackTradeQuotes.ts:42-86
Timestamp: 2025-08-08T11:41:22.794Z
Learning: NeOMakinG prefers not to include refactors in move-only PRs; such suggestions should be deferred to follow-up issues instead of being applied within the same PR.
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10380
File: src/components/Table/Table.theme.ts:177-180
Timestamp: 2025-09-02T12:38:46.940Z
Learning: NeOMakinG prefers to defer technical debt and CSS correctness issues (like improper hover selectors) to follow-up PRs when the current PR is already large and focused on major feature implementation, even when the issues are valid from a usability/technical perspective.
📚 Learning: 2026-01-03T15:12:57.968Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11537
File: packages/caip/src/adapters/coincap/generate.ts:7-7
Timestamp: 2026-01-03T15:12:57.968Z
Learning: In shapeshift/web, when scripts in packages/caip run via `yarn workspace shapeshiftoss/caip generate`, the working directory is `packages/caip`, so dotenv paths should use `../../.env` to reach the monorepo root .env file, not `.env`. The `-T` flag only affects which node_modules is used (root's), not the working directory.

Applied to files:

  • package.json
📚 Learning: 2025-08-08T20:16:12.898Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10222
File: package.json:202-202
Timestamp: 2025-08-08T20:16:12.898Z
Learning: In shapeshift/web, the semver package must be included in dependencies (not devDependencies) because hdwallet packages have transient dependencies that require semver but don't ship it themselves. This ensures semver is available at runtime for hdwallet functionality.

Applied to files:

  • package.json
📚 Learning: 2025-11-19T22:20:25.661Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10767
File: package.json:324-324
Timestamp: 2025-11-19T22:20:25.661Z
Learning: In shapeshift/web package.json, the resolution "gridplus-sdk/bs58check": "2.1.2" is intentional and must not be removed. It forces gridplus-sdk's transitive bs58check dependency from 4.0.0 down to 2.1.2 because bs58check 4.0.0 breaks legacy address validation (due to bs58 v6.0.0 and noble/hash vs 2.1.2's bs58 v4.0.0 and create-hash).

Applied to files:

  • package.json
📚 Learning: 2025-11-24T21:20:04.979Z
Learnt from: CR
Repo: shapeshift/web PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T21:20:04.979Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Always run `yarn lint --fix` and `yarn type-check` after making changes

Applied to files:

  • package.json
📚 Learning: 2025-08-29T18:09:45.982Z
Learnt from: kaladinlight
Repo: shapeshift/web PR: 10376
File: vite.config.mts:136-137
Timestamp: 2025-08-29T18:09:45.982Z
Learning: In the ShapeShift web repository vite.config.mts, the commonjsOptions.exclude configuration using bare package name strings like ['shapeshiftoss/caip', 'shapeshiftoss/types'] works correctly for excluding specific packages from CommonJS transformation, despite theoretical concerns about module ID matching patterns.

Applied to files:

  • package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install and Cache
🔇 Additional comments (1)
package.json (1)

32-32: LGTM!

The new script follows the existing naming conventions (dev:*) and uses the correct yarn workspace command syntax to enable standalone development of the swap-widget package.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

@railway-app railway-app bot temporarily deployed to radiant-wisdom / production January 12, 2026 21:16 Inactive
@railway-app railway-app bot temporarily deployed to radiant-wisdom / production January 12, 2026 21:25 Inactive
@railway-app railway-app bot temporarily deployed to radiant-wisdom / production January 12, 2026 21:35 Inactive
@railway-app railway-app bot temporarily deployed to radiant-wisdom / production January 12, 2026 21:39 Inactive
@railway-app railway-app bot temporarily deployed to radiant-wisdom / production January 12, 2026 21:46 Inactive
@railway-app railway-app bot temporarily deployed to radiant-wisdom / production January 12, 2026 21:57 Inactive
@railway-app railway-app bot temporarily deployed to radiant-wisdom / production January 12, 2026 22:02 Inactive
@railway-app railway-app bot temporarily deployed to widget / production January 13, 2026 00:22 Inactive
- Fix all ESLint and Prettier errors across all files
- Replace custom ThrottledQueue with p-queue library
- Add proper ARIA roles and accessibility to modals
- Remove non-null assertions in favor of proper guards
- Add lint/type-check scripts to package.json
- Update README with new props (allowedChainIds, defaultReceiveAddress, enableWalletConnection, walletConnectProjectId)
- Fix demo app to use ShapeShift WalletConnect project ID
- Add min-height to token modal for better UX
@railway-app railway-app bot temporarily deployed to widget / production January 13, 2026 10:43 Inactive
@railway-app railway-app bot temporarily deployed to widget / production January 13, 2026 10:53 Inactive
@NeOMakinG NeOMakinG changed the title feat: widget poc feat: swap widget Jan 13, 2026
@railway-app railway-app bot temporarily deployed to widget / production January 13, 2026 10:57 Inactive
@NeOMakinG NeOMakinG marked this pull request as ready for review January 13, 2026 10:58
@NeOMakinG NeOMakinG requested a review from a team as a code owner January 13, 2026 10:58
Copy link
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: 13

🤖 Fix all issues with AI agents
In @packages/swap-widget/package.json:
- Around line 17-27: Remove "react" and "react-dom" from the "dependencies"
object in package.json and ensure they remain declared only in
"peerDependencies" (and optionally in "devDependencies" for build/test tooling);
update the package.json by deleting the entries for "react" and "react-dom"
under "dependencies" so the project will not install its own React copy and will
rely solely on the host app's peer versions.
- Around line 25-26: The swap-widget package.json declares mismatched dependency
constraints for viem and wagmi compared to the monorepo; update the "viem" and
"wagmi" entries in swap-widget's package.json to align with the root and other
packages (use viem ^2.40.3 to match others and set wagmi to the same caret range
as root, e.g., ^2.9.2), or if a different version is required, add a clear
comment in swap-widget's package.json and a short rationale in the package
README explaining why it needs a unique wagmi version; ensure the change
references the swap-widget package.json dependencies block so monorepo
resolution remains consistent.

In @packages/swap-widget/README.md:
- Line 79: The table cell documenting `walletConnectProjectId` contains a bare
URL; update the README row so the URL is either wrapped in angle brackets like
<https://cloud.walletconnect.com> or formatted as a Markdown link such as
https://cloud.walletconnect.com (display text)[https://cloud.walletconnect.com]
to eliminate the bare-URL lint warning and preserve link behavior in the
`walletConnectProjectId` description.

In @packages/swap-widget/src/api/client.ts:
- Around line 36-40: Add timeout handling to the fetch by creating an
AbortController, attaching its signal to the existing fetchOptions before
calling fetch(url.toString(), fetchOptions), and starting a setTimeout that
calls controller.abort() after the configured timeout (e.g., default or
provided). Ensure you clear the timeout on success/failure, handle the abort
error (throw a clear timeout-specific Error or rethrow), and keep the existing
response.ok check and return response.json() as Promise<T>; update usage of
fetchOptions and url and reference the AbortController signal when initiating
fetch.

In @packages/swap-widget/src/components/AddressInputModal.css:
- Around line 75-81: The .ssw-address-error rule uses a hardcoded color
(#ef4444); update this to the CSS variable var(--ssw-error) so the error text
color is consistent with the theme by replacing the color property value in the
.ssw-address-error selector with var(--ssw-error).
- Around line 39-41: Replace the hardcoded error color in the
.ssw-address-input-wrapper.ssw-invalid rule by using the theme CSS variable to
ensure consistency: change the value from #ef4444 to var(--ssw-error) in the
.ssw-address-input-wrapper.ssw-invalid selector so it uses the --ssw-error value
defined in SwapWidget.css.

In @packages/swap-widget/src/components/AddressInputModal.tsx:
- Around line 103-110: The backdrop div's onKeyDown won't fire because a plain
div is not focusable; make the backdrop focusable by adding a tabIndex (e.g.,
tabIndex={-1}) to the same element that uses handleBackdropClick, and ensure it
receives focus when opened (call focus on that element when isOpen becomes
true); additionally add a document-level keydown listener in a useEffect that
watches isOpen and onClose to reliably call onClose() when Escape is pressed,
and clean up the listener on unmount.

In @packages/swap-widget/src/components/SwapWidget.tsx:
- Around line 246-253: The approval sendTransaction call currently doesn't wait
for mining; capture its returned transaction object (from client.sendTransaction
when sending to sellAssetAddress with approvalData and account walletAddress)
into a variable (e.g., approvalTx) and await its confirmation before proceeding
to create/send the swap transaction—use the client-provided confirmation method
(approvalTx.wait() or client.waitForTransaction(approvalTx.hash)) and
handle/errors/timeouts accordingly so the swap only runs after the approval is
mined.
- Around line 220-225: The hardcoded chain object in SwapWidget.tsx sets
nativeCurrency to ETH which is wrong for non-Ethereum chains; update the
creation of the chain object (the variable named "chain" built using
requiredChainId) to populate nativeCurrency dynamically from the chain metadata
or the sell asset's chain info (e.g., read nativeCurrency/symbol/decimals from
your chain registry or sellAsset.chain data), falling back to a sensible default
if missing, and ensure rpcUrls is preserved; locate where "chain" is constructed
and replace the hardcoded nativeCurrency with the derived values.

In @packages/swap-widget/src/hooks/useMarketData.ts:
- Around line 66-72: The proxy availability check using the fetch to
`${COINGECKO_PROXY_URL}/coins/markets?...` (the testResponse logic in
useMarketData) needs a timeout to avoid hanging; create an AbortController, pass
controller.signal into the fetch call, set a short timeout (e.g., 2–5s) to call
controller.abort(), and ensure the catch branch treats abort/errors as an
unavailable proxy (so baseUrl = COINGECKO_DIRECT_URL when the fetch is aborted
or fails). Also clear the timeout after fetch completes to avoid leaks and keep
the existing logic that checks testResponse?.ok to decide the baseUrl.

In @packages/swap-widget/src/hooks/useSwapQuote.ts:
- Around line 29-38: The queryKey used in useSwapQuote is missing the
slippageTolerancePercentageDecimal so React Query returns stale quotes when
slippage changes; update the queryKey array in the useQuery call inside
useSwapQuote to include slippageTolerancePercentageDecimal (the same param you
pass to the API) so the cache is keyed by slippage and a new quote is fetched
whenever it changes.

In @packages/swap-widget/src/utils/addressValidation.ts:
- Around line 204-206: The 'cosmos' switch case declares const prefix without
block scope causing lexical scoping issues; wrap the case body in braces so the
prefix is block-scoped — i.e., change the case 'cosmos' branch to: case
'cosmos': { const prefix = getCosmosPrefix(chainId); return prefix ?
`${prefix}1...` : 'Enter address'; } — ensuring the const is scoped to that
case.
- Around line 161-169: The 'cosmos' switch case in validateAddress (or the
function containing this switch) declares const expectedPrefix which can leak to
other cases; wrap the entire 'case "cosmos":' body in braces { ... } so
expectedPrefix (and any other const/let declarations like trimmedAddress usage)
are block-scoped, keeping the call sites getCosmosPrefix and
isValidCosmosAddress unchanged and returning the same { valid: false, error: ...
} on failure.
🧹 Nitpick comments (25)
packages/swap-widget/src/vite-env.d.ts (1)

3-6: Consider extending EthereumProvider for broader event support.

The on method signature only supports callbacks with string[] (accounts), but EIP-1193 providers emit various events with different payload types (e.g., chainChanged returns a hex string, disconnect returns an error object). If the widget needs to handle other events, this type will be insufficient.

♻️ Suggested improvement for broader compatibility
 interface EthereumProvider {
   request: (args: { method: string; params?: unknown[] }) => Promise<unknown>
-  on: (event: string, callback: (accounts: string[]) => void) => void
+  on: (event: string, callback: (...args: unknown[]) => void) => void
+  removeListener?: (event: string, callback: (...args: unknown[]) => void) => void
 }
packages/swap-widget/src/components/AddressInputModal.css (1)

130-134: Hardcoded color: white may not work well in all theme contexts.

Consider using a CSS variable for the button text color to maintain theme consistency, especially if a custom accent color is configured that doesn't contrast well with white.

packages/swap-widget/src/components/SwapWidget.css (1)

429-437: Consider using CSS variables for status background colors.

The hardcoded rgba() values for success and error backgrounds could use CSS variables with opacity applied, maintaining consistency with the theming approach used elsewhere.

♻️ Example using CSS variables with color-mix
 .ssw-tx-status-success {
   border-color: var(--ssw-success);
-  background: rgba(0, 211, 149, 0.1);
+  background: color-mix(in srgb, var(--ssw-success) 10%, transparent);
 }
 
 .ssw-tx-status-error {
   border-color: var(--ssw-error);
-  background: rgba(244, 67, 54, 0.1);
+  background: color-mix(in srgb, var(--ssw-error) 10%, transparent);
 }
packages/swap-widget/vite.config.ts (1)

19-38: Consider externalizing wagmi/viem for library builds.

The library build correctly externalizes react and react-dom, but wagmi and viem are listed as dependencies rather than peer dependencies in package.json. If consumers already use wagmi/viem, this could lead to duplicate instances and version conflicts. Consider either:

  1. Externalizing wagmi and viem in rollupOptions
  2. Moving them to peerDependencies in package.json

Also, explicitly specifying output formats (e.g., formats: ['es', 'cjs']) would make the build output clearer.

Suggested externals expansion
         rollupOptions: {
-          external: ["react", "react-dom"],
+          external: ["react", "react-dom", "wagmi", "viem"],
           output: {
             globals: {
               react: "React",
               "react-dom": "ReactDOM",
+              wagmi: "wagmi",
+              viem: "viem",
             },
           },
         },
packages/swap-widget/Dockerfile (1)

5-8: Review --legacy-peer-deps usage and consider adding a non-root user.

Using --legacy-peer-deps masks peer dependency conflicts that could cause runtime issues. Consider resolving the underlying peer dependency mismatches if possible.

Additionally, for better container security, consider running as a non-root user:

Suggested security improvement
 FROM node:20-slim

+RUN addgroup --system --gid 1001 nodejs && \
+    adduser --system --uid 1001 widget
+
 RUN npm install -g serve

 WORKDIR /app

 COPY --from=builder /app/dist ./dist

+USER widget
+
 EXPOSE 3000

 CMD ["serve", "-s", "dist", "-l", "3000"]
packages/swap-widget/src/components/WalletProvider.tsx (1)

18-31: Consider typing walletClient more explicitly.

The unknown type for walletClient in the render prop pattern loses type safety. While this works, consumers won't get IntelliSense or type checking.

♻️ Suggested improvement
+import type { WalletClient } from 'viem'
+
 type InternalWalletProviderProps = {
   projectId: string
-  children: (walletClient: unknown) => ReactNode
+  children: (walletClient: WalletClient | undefined) => ReactNode
   themeMode: ThemeMode
 }

 const InternalWalletContent = ({
   children,
 }: {
-  children: (walletClient: unknown) => ReactNode
+  children: (walletClient: WalletClient | undefined) => ReactNode
 }) => {
packages/swap-widget/src/demo/App.css (1)

343-346: Consider preserving focus indication for accessibility.

Removing outline: none on focus can harm keyboard navigation. The border-color change may be subtle for some users.

♻️ Alternative that preserves accessibility
 .demo-color-text:focus {
-  outline: none;
+  outline: 2px solid var(--demo-accent);
+  outline-offset: 2px;
   border-color: var(--demo-accent);
 }

Or keep the current approach if the border change provides sufficient visual feedback for this demo context.

packages/swap-widget/src/components/QuotesModal.css (1)

14-45: Consider adding reduced motion support for accessibility.

The animations look clean. For users who prefer reduced motion, consider adding a media query to disable or minimize these animations.

♿ Optional: Add prefers-reduced-motion support
@media (prefers-reduced-motion: reduce) {
  .ssw-quotes-modal-backdrop,
  .ssw-quotes-modal {
    animation: none;
  }
}
packages/swap-widget/src/hooks/useSwapRates.ts (1)

34-38: Consider BigInt for precise comparison of large crypto amounts.

Using parseFloat for sorting works for most cases, but crypto base unit amounts can exceed JavaScript's safe integer limit (2^53). For very large amounts, precision could be lost.

♻️ Optional: Use BigInt for precise sorting
         .sort((a, b) => {
-          const aAmount = parseFloat(a.buyAmountCryptoBaseUnit)
-          const bAmount = parseFloat(b.buyAmountCryptoBaseUnit)
-          return bAmount - aAmount
+          const aAmount = BigInt(a.buyAmountCryptoBaseUnit)
+          const bAmount = BigInt(b.buyAmountCryptoBaseUnit)
+          return bAmount > aAmount ? 1 : bAmount < aAmount ? -1 : 0
         })
packages/swap-widget/src/constants/swappers.ts (1)

28-41: SWAPPER_COLORS could use Record instead of Partial<Record>.

All SwapperName values have color entries defined. Using Record<SwapperName, string> would provide compile-time enforcement that all swappers have colors, matching the pattern used for SWAPPER_ICONS.

♻️ Optional refinement
-export const SWAPPER_COLORS: Partial<Record<SwapperName, string>> = {
+export const SWAPPER_COLORS: Record<SwapperName, string> = {
packages/swap-widget/src/components/AddressInputModal.tsx (1)

113-114: Hardcoded English strings should use translation keys.

Per coding guidelines, all user-facing text should use translation keys. Strings like "Receive Address", "Enter {chainName} address", "Use connected wallet", "Reset to Wallet", and "Confirm" are hardcoded.

This can be addressed in a follow-up PR to avoid expanding scope.

Also applies to: 131-132, 203-203, 213-213, 221-221

packages/swap-widget/src/constants/chains.ts (1)

89-95: Use TrustWallet icon source for consistency with other chains.

Dogecoin currently uses CoinGecko while all other chains use TrustWallet assets. While the CoinGecko URL works, standardizing on TrustWallet maintains consistency across the metadata.

  'bip122:00000000001a91e3dace36e2be3bf030': {
    chainId: 'bip122:00000000001a91e3dace36e2be3bf030',
    name: 'Dogecoin',
    shortName: 'DOGE',
    color: '#FFC107',
-   icon: 'https://assets.coingecko.com/coins/images/5/large/dogecoin.png',
+   icon: 'https://rawcdn.githack.com/trustwallet/assets/b7a5f12d893fcf58e0eb1dd64478f076857b720b/blockchains/doge/info/logo.png',
  },
packages/swap-widget/src/components/SettingsModal.css (1)

119-133: Consider using CSS variables for warning/error colors for theming consistency.

The warning and error states use hardcoded colors (#ffc107, #f44336) while the rest of the file uses CSS custom properties (--ssw-*). This could cause visual inconsistencies when the widget is embedded in different theme contexts.

♻️ Suggested refactor
 .ssw-slippage-warning {
   display: flex;
   align-items: flex-start;
   gap: 8px;
   padding: 10px 12px;
   border-radius: 10px;
-  background: rgba(255, 193, 7, 0.1);
-  color: #ffc107;
+  background: var(--ssw-warning-bg, rgba(255, 193, 7, 0.1));
+  color: var(--ssw-warning-text, #ffc107);
   font-size: 13px;
   line-height: 1.4;
 }

 .ssw-slippage-warning.ssw-error {
-  background: rgba(244, 67, 54, 0.1);
-  color: #f44336;
+  background: var(--ssw-error-bg, rgba(244, 67, 54, 0.1));
+  color: var(--ssw-error-text, #f44336);
 }
packages/swap-widget/src/components/SettingsModal.tsx (2)

81-81: Extract inline onKeyDown handler to a memoized callback.

The inline arrow function creates a new reference on each render. Per coding guidelines, callbacks should be wrapped in useCallback.

♻️ Suggested refactor
+  const handleKeyDown = useCallback(
+    (e: React.KeyboardEvent) => {
+      if (e.key === 'Escape') {
+        onClose()
+      }
+    },
+    [onClose],
+  )
+
   return (
     // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
     <div
       className='ssw-modal-backdrop'
       onClick={handleBackdropClick}
-      onKeyDown={e => e.key === 'Escape' && onClose()}
+      onKeyDown={handleKeyDown}
       role='dialog'

88-89: Hardcoded UI strings may limit internationalization.

The component contains hardcoded English strings ("Settings", "Slippage Tolerance", warning messages). If i18n support is planned for the widget, consider introducing a translation mechanism or accepting string props for customization.

Also applies to: 108-108, 168-171

packages/swap-widget/src/components/QuoteSelector.tsx (2)

96-96: Object reference comparison may be unreliable.

displayRate === bestRate uses reference equality. If selectedRate is reconstructed (e.g., from a new API response with the same data), this comparison could incorrectly show "Best" on a non-best rate, or fail to show it on the actual best rate.

Consider comparing by a stable identifier:

♻️ Suggested refactor
-            {displayRate === bestRate && <span className='ssw-quote-best-tag'>Best</span>}
+            {displayRate.id === bestRate.id && <span className='ssw-quote-best-tag'>Best</span>}

47-52: Redundant callback wrapper.

handleSelectRate simply delegates to onSelectRate without any transformation. Consider passing onSelectRate directly to simplify.

♻️ Suggested refactor
-  const handleSelectRate = useCallback(
-    (rate: TradeRate) => {
-      onSelectRate(rate)
-    },
-    [onSelectRate],
-  )
-
 ...
       <QuotesModal
         isOpen={isModalOpen}
         onClose={handleCloseModal}
         rates={rates}
         selectedRate={selectedRate}
-        onSelectRate={handleSelectRate}
+        onSelectRate={onSelectRate}
         buyAsset={buyAsset}
packages/swap-widget/src/components/SwapWidget.tsx (1)

243-243: Approval uses exact amount instead of max allowance.

Approving only BigInt(sellAmountBaseUnit) means users will need to re-approve for every swap. Consider using MaxUint256 for unlimited approval, or provide a toggle for users to choose between exact and unlimited approval.

♻️ Suggested fix for unlimited approval
+const MAX_UINT256 = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
+
 // In the approval section:
-            args: [quoteResponse.approval.spender as `0x${string}`, BigInt(sellAmountBaseUnit)],
+            args: [quoteResponse.approval.spender as `0x${string}`, MAX_UINT256],
packages/swap-widget/src/demo/App.tsx (1)

130-134: Clipboard API error handling is minimal.

The navigator.clipboard.writeText call uses .then() but doesn't handle potential failures (e.g., when clipboard access is denied). Consider adding error handling.

♻️ Add error handling for clipboard
-    navigator.clipboard.writeText(code).then(() => {
-      setCopied(true)
-      setTimeout(() => setCopied(false), 2000)
-    })
+    navigator.clipboard.writeText(code)
+      .then(() => {
+        setCopied(true)
+        setTimeout(() => setCopied(false), 2000)
+      })
+      .catch(() => {
+        console.warn('Failed to copy to clipboard')
+      })
packages/swap-widget/src/hooks/useMarketData.ts (1)

119-135: Filter logic is not memoized and recomputes on every render.

The filteredData is computed using an IIFE that runs on every render. Since useMarketData may be called frequently, this could cause unnecessary recomputations.

♻️ Memoize the filtered data
 export const useMarketData = (assetIds: AssetId[]) => {
   const { data: allMarketData, ...rest } = useAllMarketData()

-  const filteredData = (() => {
+  const filteredData = useMemo(() => {
     if (!allMarketData) return {}

     const result: MarketDataById = {}
     for (const assetId of assetIds) {
       if (allMarketData[assetId]) {
         result[assetId] = allMarketData[assetId]
       }
     }
     return result
-  })()
+  }, [allMarketData, assetIds])

   return { data: filteredData, ...rest }
 }

Note: This requires importing useMemo from React.

packages/swap-widget/src/components/TokenSelectModal.tsx (1)

14-23: useLockBodyScroll hook is duplicated across modal components.

This hook is also defined in QuotesModal.tsx. Consider extracting it to a shared hooks file to avoid duplication.

This is a minor DRY improvement that can be deferred to a follow-up PR. Based on learnings, NeOMakinG prefers keeping PRs focused.

packages/swap-widget/src/api/client.ts (1)

37-39: Error response body is not included in error message.

When the API returns an error, only the status code and status text are included. The response body often contains useful error details that would help with debugging.

♻️ Include response body in error
     const response = await fetch(url.toString(), fetchOptions)
     if (!response.ok) {
-      throw new Error(`API error: ${response.status} ${response.statusText}`)
+      const errorBody = await response.text().catch(() => '')
+      throw new Error(`API error: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ''}`)
     }
packages/swap-widget/src/hooks/useAssets.ts (2)

80-102: Chains computation is not memoized.

The chains variable is computed inside an IIFE that runs on every render of components using useChains. Since this involves iterating over all assets and building a Map, it should be memoized.

♻️ Memoize chains computation
+import { useMemo } from 'react'

 export const useChains = () => {
   const { data: assets, ...rest } = useAssets()

-  const chains = (() => {
+  const chains = useMemo(() => {
     if (!assets.length) return []

     const chainMap = new Map<ChainId, ChainInfo>()
     // ... rest of the computation
     return Array.from(chainMap.values()).sort((a, b) => a.name.localeCompare(b.name))
-  })()
+  }, [assets])

   return { data: chains, ...rest }
 }

148-173: Search results computation is not memoized.

Similar to useChains, the searchResults in useAssetSearch is computed in an IIFE without memoization. This could cause performance issues with frequent re-renders.

♻️ Memoize search results
 export const useAssetSearch = (query: string, chainId?: ChainId) => {
   const { data: assets, ...rest } = useAssets()

-  const searchResults = (() => {
+  const searchResults = useMemo(() => {
     let filtered = chainId ? assets.filter(a => a.chainId === chainId) : assets
     // ... rest of the computation
     return matched
-  })()
+  }, [assets, query, chainId])

   return { data: searchResults, ...rest }
 }
packages/swap-widget/src/types/index.ts (1)

1-2: Consider using branded/nominal types for domain identifiers.

Per coding guidelines, Nominal types are preferred for domain identifiers like ChainId and AssetId to prevent accidental misuse. However, since this is a standalone package with its own dependency footprint, plain string aliases may be acceptable for simplicity.

Example pattern if stronger typing is desired:

type Brand<K, T> = K & { __brand: T }
export type ChainId = Brand<string, 'ChainId'>
export type AssetId = Brand<string, 'AssetId'>

- Remove react/react-dom from dependencies (peer deps only)
- Align viem/wagmi versions with monorepo root
- Add timeout handling to API calls using AbortController
- Use CSS variables for error colors in AddressInputModal
- Fix Escape key handling with useEffect and tabIndex
- Derive nativeCurrency from chain metadata instead of hardcoding ETH
- Wait for approval tx confirmation before swap tx
- Add slippageTolerancePercentageDecimal to useSwapQuote queryKey
- Wrap switch case blocks in braces for proper lexical scoping
- Add timeout to proxy availability check in useMarketData
@NeOMakinG
Copy link
Collaborator Author

Addressed CodeRabbit Review Comments

I've addressed all the actionable CodeRabbit comments in commit 666f219:

High Priority Fixes

  1. package.json - Removed react/react-dom from dependencies (keeping only in peerDependencies)
  2. package.json - Aligned viem (^2.40.3) and wagmi (^2.9.2) versions with monorepo root
  3. SwapWidget.tsx - Derive nativeCurrency from chain metadata instead of hardcoding ETH. Added nativeCurrency to chain metadata and use it dynamically based on the sell asset's chain.
  4. SwapWidget.tsx - Wait for approval transaction confirmation before executing swap transaction using publicClient.waitForTransactionReceipt()
  5. useSwapQuote.ts - Added slippageTolerancePercentageDecimal to queryKey to prevent stale cache issues

Medium Priority Fixes

  1. client.ts - Added timeout handling to API calls using AbortController (30s default)
  2. AddressInputModal.tsx - Added tabIndex={-1} and global useEffect keydown listener for reliable Escape key handling
  3. useMarketData.ts - Added 5 second timeout to proxy availability check using AbortController
  4. addressValidation.ts - Wrapped switch case blocks in braces for proper lexical scoping (cosmos cases at lines 161-169 and 204-206)

Low Priority Fixes

  1. README.md - Wrapped bare URL in angle brackets: <https://cloud.walletconnect.com>
  2. AddressInputModal.css - Replaced hardcoded #ef4444 with var(--ssw-error) for consistency (both occurrences)

Copy link
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: 3

🤖 Fix all issues with AI agents
In @packages/swap-widget/package.json:
- Around line 17-25: Update the viem dependency in package.json from a caret
range to an exact version by changing "viem": "^2.40.3" to "viem": "2.40.3" so
it is pinned to the monorepo root version; locate the dependency entry for viem
in the dependencies block of the package.json and replace the version string
accordingly, then run install to verify lockfile consistency.

In @packages/swap-widget/src/components/SwapWidget.tsx:
- Around line 237-242: The constructed chain object uses rpcUrls: { default: {
http: [] } } which is empty and can break createPublicClient when used later
(see createPublicClient usage where the chain is passed); update the chain
creation in SwapWidget (the variable chain) to supply a valid fallback RPC URL
(e.g., from chainMeta.publicRpc or CHAIN_METADATA[requiredChainId].rpcUrls) or
use the chain's default public RPC if available, and/or add proper RPC entries
to CHAIN_METADATA in constants/chains.ts so createPublicClient receives a
non-empty http URL list.
- Around line 220-229: The quote request is incorrectly using walletAddress for
both sendAddress and receiveAddress; update the apiClient.getQuote call (where
quoteResponse is assigned) to pass the computed effectiveReceiveAddress instead
of walletAddress for the receiveAddress field so
customReceiveAddress/defaultReceiveAddress are respected (i.e., replace
receiveAddress: walletAddress with receiveAddress: effectiveReceiveAddress in
the getQuote call).
🧹 Nitpick comments (8)
packages/swap-widget/src/utils/addressValidation.ts (3)

84-84: Bech32 regex is overly permissive.

The bech32 character set after the 1 separator is restricted to qpzry9x8gf2tvdw0s3jn54khce6mua7l (32 specific characters), not [a-z0-9]. This regex will accept addresses with invalid characters like b, i, o, or digits 1 in the data portion.

For basic UI validation this may be acceptable, but consider using the correct character class for stricter validation.

🔧 Suggested stricter regex
- const bech32Regex = /^[a-z]{1,83}1[a-z0-9]{38,58}$/i;
+ const bech32Regex = /^[a-z]{1,83}1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{38,58}$/;

143-165: Extract UTXO chain identifiers to named constants.

The magic strings for identifying UTXO chains (Bitcoin, Bitcoin Cash, Litecoin, Dogecoin) are duplicated between validateAddress and getAddressFormatHint. Extract these to named constants for maintainability and to reduce the risk of typos.

🔧 Suggested refactor

Add constants near the top of the file:

const UTXO_CHAIN_IDENTIFIERS = {
  bitcoin: "000000000019d6689c085ae165831e93",
  bitcoinCash: "000000000000000000651ef99cb9fcbe",
  litecoin: "12a765e31ffd4059bada1e25190f6e98",
  dogecoin: "00000000001a91e3dace36e2be3bf030",
} as const;

Then use in both functions:

-     if (chainId.includes("000000000019d6689c085ae165831e93")) {
+     if (chainId.includes(UTXO_CHAIN_IDENTIFIERS.bitcoin)) {

Also applies to: 202-210


123-126: Consider extracting the return type to a named type alias.

Per coding guidelines, explicit types should be used for function return values. The inline { valid: boolean; error?: string } could be extracted to a named type for reusability and clarity.

🔧 Suggested type extraction
type AddressValidationResult = {
  valid: boolean;
  error?: string;
};

export const validateAddress = (
  address: string,
  chainId: ChainId,
): AddressValidationResult => {
packages/swap-widget/src/components/AddressInputModal.tsx (1)

115-246: Hardcoded strings should use translation keys.

Per coding guidelines, all copy/text must use translation keys. The strings "Receive Address", "Enter {chainName} address", "Use connected wallet", "Reset to Wallet", and "Confirm" are hardcoded.

Based on learnings, this can be deferred to a follow-up PR if desired to keep scope focused.

packages/swap-widget/src/hooks/useMarketData.ts (2)

129-145: Consider using useMemo for filtered data.

The IIFE pattern works but using useMemo would be more consistent with the codebase conventions and React patterns for derived data.

♻️ Suggested refactor
 export const useMarketData = (assetIds: AssetId[]) => {
   const { data: allMarketData, ...rest } = useAllMarketData();

-  const filteredData = (() => {
+  const filteredData = useMemo(() => {
     if (!allMarketData) return {};

     const result: MarketDataById = {};
     for (const assetId of assetIds) {
       if (allMarketData[assetId]) {
         result[assetId] = allMarketData[assetId];
       }
     }
     return result;
-  })();
+  }, [allMarketData, assetIds]);

   return { data: filteredData, ...rest };
 };

156-170: Potential precision loss with large crypto amounts.

Number(cryptoAmount) may lose precision for amounts exceeding Number.MAX_SAFE_INTEGER (approximately 9×10¹⁵). For very large token balances or low-precision tokens, this could produce inaccurate USD values.

For a display-only utility this is likely acceptable, but worth noting for future consideration.

packages/swap-widget/src/components/SwapWidget.tsx (2)

46-53: Module-level QueryClient is shared across all widget instances.

The queryClient is created at module scope, meaning multiple SwapWidget instances on the same page will share the same cache. This could cause unintended data sharing between widgets with different configurations.

Consider creating the QueryClient inside the wrapper components or using a unique key per widget instance.


327-330: Fixed 3-second delay for balance refresh is arbitrary.

The setTimeout with a 3-second delay may not align with actual transaction confirmation times, which vary by network. Consider using waitForTransactionReceipt for the swap transaction or implementing a polling strategy.

Comment on lines 17 to 25
"dependencies": {
"@rainbow-me/rainbowkit": "^2.2.3",
"@shapeshiftoss/caip": "^8.16.5",
"@tanstack/react-query": "^5.60.0",
"p-queue": "^8.0.1",
"react-virtuoso": "^4.18.1",
"viem": "^2.40.3",
"wagmi": "^2.9.2"
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check viem/wagmi versions in swap-widget vs root package.json
echo "=== swap-widget dependencies ==="
if [ -f packages/swap-widget/package.json ]; then
  cat packages/swap-widget/package.json | jq '.dependencies | {viem, wagmi}'
else
  echo "File not found"
fi

echo ""
echo "=== Root package.json dependencies ==="
cat package.json | jq '.dependencies | {viem, wagmi}'

echo ""
echo "=== Checking if viem/wagmi are transitive deps in other packages ==="
rg '"viem"|"wagmi"' --type json | head -20

Repository: shapeshift/web

Length of output: 750


Pin viem to exact version 2.40.3 to align with monorepo root.

The root package.json has viem pinned to 2.40.3, but swap-widget uses ^2.40.3 (caret). Change to "viem": "2.40.3" to match the monorepo alignment mentioned in the PR objectives.

The wagmi version ^2.9.2 is correct and matches root.

🤖 Prompt for AI Agents
In @packages/swap-widget/package.json around lines 17 - 25, Update the viem
dependency in package.json from a caret range to an exact version by changing
"viem": "^2.40.3" to "viem": "2.40.3" so it is pinned to the monorepo root
version; locate the dependency entry for viem in the dependencies block of the
package.json and replace the version string accordingly, then run install to
verify lockfile consistency.

Comment on lines 220 to 229
const slippageDecimal = (parseFloat(slippage) / 100).toString();
const quoteResponse = await apiClient.getQuote({
sellAssetId: sellAsset.assetId,
buyAssetId: buyAsset.assetId,
sellAmountCryptoBaseUnit: sellAmountBaseUnit,
sendAddress: walletAddress,
receiveAddress: walletAddress,
swapperName: rateToUse.swapperName,
slippageTolerancePercentageDecimal: slippageDecimal,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

receiveAddress should use effectiveReceiveAddress instead of walletAddress.

The quote request uses walletAddress for both sendAddress and receiveAddress, which ignores the customReceiveAddress and defaultReceiveAddress props. This means users who set a custom receive address will still have swaps sent to their connected wallet.

🐛 Proposed fix
       const slippageDecimal = (parseFloat(slippage) / 100).toString();
       const quoteResponse = await apiClient.getQuote({
         sellAssetId: sellAsset.assetId,
         buyAssetId: buyAsset.assetId,
         sellAmountCryptoBaseUnit: sellAmountBaseUnit,
         sendAddress: walletAddress,
-        receiveAddress: walletAddress,
+        receiveAddress: effectiveReceiveAddress || walletAddress,
         swapperName: rateToUse.swapperName,
         slippageTolerancePercentageDecimal: slippageDecimal,
       });
🤖 Prompt for AI Agents
In @packages/swap-widget/src/components/SwapWidget.tsx around lines 220 - 229,
The quote request is incorrectly using walletAddress for both sendAddress and
receiveAddress; update the apiClient.getQuote call (where quoteResponse is
assigned) to pass the computed effectiveReceiveAddress instead of walletAddress
for the receiveAddress field so customReceiveAddress/defaultReceiveAddress are
respected (i.e., replace receiveAddress: walletAddress with receiveAddress:
effectiveReceiveAddress in the getQuote call).

Comment on lines 237 to 242
const chain = {
id: requiredChainId,
name: chainMeta?.name ?? "Chain",
nativeCurrency,
rpcUrls: { default: { http: [] } },
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Empty rpcUrls may cause issues with createPublicClient.

The chain object has rpcUrls: { default: { http: [] } } which is an empty array. When createPublicClient is called with this chain (line 274-277), it may fail or use unexpected defaults when waiting for the approval transaction receipt.

Consider providing a fallback RPC URL or using the chain's default public RPC.

🐛 Proposed fix
       const chain = {
         id: requiredChainId,
         name: chainMeta?.name ?? "Chain",
         nativeCurrency,
-        rpcUrls: { default: { http: [] } },
+        rpcUrls: {
+          default: {
+            http: chainMeta?.rpcUrl ? [chainMeta.rpcUrl] : [`https://rpc.ankr.com/${requiredChainId === 1 ? 'eth' : 'multichain/' + requiredChainId}`],
+          },
+        },
       };

Alternatively, consider adding RPC URLs to CHAIN_METADATA in constants/chains.ts.

🤖 Prompt for AI Agents
In @packages/swap-widget/src/components/SwapWidget.tsx around lines 237 - 242,
The constructed chain object uses rpcUrls: { default: { http: [] } } which is
empty and can break createPublicClient when used later (see createPublicClient
usage where the chain is passed); update the chain creation in SwapWidget (the
variable chain) to supply a valid fallback RPC URL (e.g., from
chainMeta.publicRpc or CHAIN_METADATA[requiredChainId].rpcUrls) or use the
chain's default public RPC if available, and/or add proper RPC entries to
CHAIN_METADATA in constants/chains.ts so createPublicClient receives a non-empty
http URL list.

React needs to be in devDependencies for the build process to resolve
react/jsx-runtime, while remaining in peerDependencies for consumers.
@railway-app railway-app bot temporarily deployed to widget / production January 13, 2026 12:34 Inactive
@railway-app railway-app bot temporarily deployed to widget / production January 13, 2026 12:38 Inactive
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.

3 participants