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 modal, address input modal, real-time market & balance data, theming (dark/light), and demo app for previewing/customization.
  • Documentation

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

    • Package & build setup for the widget, Docker image support, and new 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 package @shapeshiftoss/swap-widget with complete widget source (components, hooks, types, utils, API client), demo app, Vite/TS configs, Dockerfile, railway deploy config, and a root npm script to run the widget workspace.

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
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, Vite config supporting lib vs app builds, and strict TypeScript configs.
Docker & deploy
packages/swap-widget/Dockerfile
Multi-stage Dockerfile: build stage (Node) and serve stage (serve) 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 (Asset, Chain, TradeRate, ThemeConfig, etc.), chain ID groups, chain metadata, swapper icon/color mappings, and helpers (isEvmChainId, format/parse/truncate).
API client
packages/swap-widget/src/api/client.ts
New createApiClient(factory) exposing getAssets, getRates, getQuote with baseUrl/apiKey, timeout and error handling.
Utilities
packages/swap-widget/src/utils/addressValidation.ts, packages/swap-widget/src/utils/redirect.ts
Cross-chain address validators and format 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, searchable 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 (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) and many CSS files; full UI, modals, 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 files
packages/swap-widget/src/*
Additional helpers, hooks, typings, and re-exports (vite-env, helpers, utilities, index).

Sequence Diagram(s)

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

    User->>UI: open token selector
    UI->>Assets: useAssets/useChains/useEvmBalances
    Assets-->>UI: assets, chains, balances
    User->>UI: choose sell token & amount
    UI->>API: getRates(sell,buy,amount)
    API-->>UI: rates[]
    User->>UI: view/select rate
    UI->>Quotes: open modal / list rates
    Quotes-->>UI: onSelectRate(rate)
    User->>UI: confirm swap
    UI->>API: getQuote(params)
    API-->>UI: quote with tx data
    UI->>Wallet: send transaction
    Wallet-->>UI: tx hash / status updates
    UI->>User: show success / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A curious rabbit hops to code,
New widgets bloom along the road,
Tokens, quotes, and modals sing,
Wallets click — the demo springs,
Hooray — the swap-widget's on the go!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'feat: swap widget' is overly generic and vague. While it refers to the primary change, it lacks specificity about what the swap widget does or its significance. Consider a more descriptive title such as 'feat: add embeddable swap widget package' or 'feat: introduce swap-widget package with multi-chain support' to clarify the scope and nature of the feature.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch widget-poc

🧹 Recent nitpick comments
packages/swap-widget/src/utils/addressValidation.ts (1)

134-159: Consider extracting UTXO chain detection to constants.

The genesis block hash substrings are duplicated in both validateAddress and getAddressFormatHint. Extracting these to named constants would improve maintainability and reduce the risk of inconsistencies.

♻️ Suggested refactor
// At top of file
const UTXO_CHAIN_IDENTIFIERS = {
  bitcoin: '000000000019d6689c085ae165831e93',
  bitcoinCash: '000000000000000000651ef99cb9fcbe',
  litecoin: '12a765e31ffd4059bada1e25190f6e98',
  dogecoin: '00000000001a91e3dace36e2be3bf030',
} as const

// Then in validateAddress:
if (chainId.includes(UTXO_CHAIN_IDENTIFIERS.bitcoin)) {
  // ...
}
packages/swap-widget/src/components/AddressInputModal.tsx (4)

60-65: Minor duplication in validation logic.

isConfirmDisabled calls validateAddress separately from the validation memo. While this is intentional (to validate regardless of hasInteracted state), consider restructuring to avoid the duplicate call:

♻️ Optional refactor
-  const validation = useMemo(() => {
-    if (!inputValue || !hasInteracted) {
-      return { valid: true, error: undefined }
-    }
-    return validateAddress(inputValue, chainId)
-  }, [inputValue, chainId, hasInteracted])
+  const rawValidation = useMemo(() => {
+    if (!inputValue) return { valid: false, error: undefined }
+    return validateAddress(inputValue, chainId)
+  }, [inputValue, chainId])
+
+  const validation = useMemo(() => {
+    if (!hasInteracted) return { valid: true, error: undefined }
+    return rawValidation
+  }, [hasInteracted, rawValidation])

-  const isConfirmDisabled = useMemo(() => {
-    if (!inputValue) return true
-    return !validateAddress(inputValue, chainId).valid
-  }, [inputValue, chainId])
+  const isConfirmDisabled = useMemo(() => {
+    return !rawValidation.valid
+  }, [rawValidation])

Also applies to: 114-117


105-112: Redundant Escape key handler.

handleBackdropKeyDown duplicates the Escape key handling already performed by the global keydown listener in lines 51-58. The global listener ensures reliable Escape handling even when focus is elsewhere.

♻️ Consider removing redundant handler
-  const handleBackdropKeyDown = useCallback(
-    (e: React.KeyboardEvent) => {
-      if (e.key === 'Escape') {
-        onClose()
-      }
-    },
-    [onClose],
-  )

// And in JSX:
-      onKeyDown={handleBackdropKeyDown}

The global event listener is sufficient and more reliable for Escape key handling.


173-192: Inline handler creates new function on each render.

The clear button uses an inline arrow function. Per coding guidelines, prefer useCallback for event handlers passed as props.

♻️ Suggested refactor
+  const handleInputClear = useCallback(() => {
+    setInputValue('')
+    setHasInteracted(false)
+  }, [])

// In JSX:
   <button
     className='ssw-address-clear-btn'
-    onClick={() => {
-      setInputValue('')
-      setHasInteracted(false)
-    }}
+    onClick={handleInputClear}
     type='button'
   >

162-171: Consider linking error messages to input for screen readers.

The input could benefit from aria-describedby pointing to the error message element for better screen reader support.

♻️ Suggested accessibility improvement
   <input
     type='text'
     className='ssw-address-input'
     placeholder={formatHint}
     value={inputValue}
     onChange={e => handleInputChange(e.target.value)}
     autoFocus
     spellCheck={false}
     autoComplete='off'
+    aria-invalid={!validation.valid && hasInteracted}
+    aria-describedby={!validation.valid && hasInteracted ? 'address-error' : undefined}
   />

// And on the error div:
-  <div className='ssw-address-error'>
+  <div id='address-error' className='ssw-address-error' role='alert'>
📜 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 09cb3da and 3caa39b.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (8)
  • packages/swap-widget/src/api/client.ts
  • packages/swap-widget/src/components/AddressInputModal.tsx
  • packages/swap-widget/src/components/SwapWidget.tsx
  • packages/swap-widget/src/constants/chains.ts
  • packages/swap-widget/src/hooks/useMarketData.ts
  • packages/swap-widget/src/hooks/useSwapQuote.ts
  • packages/swap-widget/src/utils/addressValidation.ts
  • packages/swap-widget/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/swap-widget/src/hooks/useSwapQuote.ts
  • packages/swap-widget/src/constants/chains.ts
  • packages/swap-widget/src/components/SwapWidget.tsx
  • packages/swap-widget/src/api/client.ts
  • packages/swap-widget/src/hooks/useMarketData.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Never assume a library is available - always check imports/package.json first
Prefer composition over inheritance
Write self-documenting code with clear variable and function names
Keep functions small and focused on a single responsibility
Avoid deep nesting - use early returns instead
Prefer procedural and easy to understand code
Never expose, log, or commit secrets, API keys, or credentials
Validate all inputs, especially user inputs
Handle errors gracefully with meaningful messages
Don't silently catch and ignore exceptions
Log errors appropriately for debugging
Provide fallback behavior when possible
Use appropriate data structures for the task
Never add code comments unless explicitly requested
When modifying code, do not add comments that reference previous implementations or explain what changed. Comments should only describe the current logic and functionality.
Use meaningful names for branches, variables, and functions
Always run yarn lint --fix and yarn type-check after making changes
Avoid let variable assignments - prefer const with inline IIFE switch statements or extract to functions for conditional logic

Files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
  • packages/swap-widget/src/utils/addressValidation.ts
  • packages/swap-widget/vite.config.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Avoid useEffect where practical - use it only when necessary and following best practices
Avoid 'any' types - use specific type annotations instead
For default values with user overrides, use computed values (useMemo) instead of useEffect - pattern: userSelected ?? smartDefault ?? fallback
When function parameters are unused due to interface requirements, refactor the interface or implementation to remove them rather than prefixing with underscore
Sanitize data before displaying to prevent XSS
Memoize aggressively - wrap component variables in useMemo and callbacks in useCallback where possible
For static JSX icon elements (e.g., <TbCopy />) that don't depend on state/props, define them as constants outside the component to avoid re-renders instead of using useMemo
Account for light/dark mode using useColorModeValue hook
Account for responsive mobile designs in all UI components
When applying styles, use the existing standards and conventions of the codebase
Use Chakra UI components and conventions
All copy/text must use translation keys - never hardcode strings
Use the translation hook: useTranslate() from react-polyglot
Use useFeatureFlag('FlagName') hook to access feature flag values in components
Prefer type over interface for type definitions
Use strict typing - avoid any
Use Nominal types for domain identifiers (e.g., WalletId, AccountId)
Import types from @shapeshiftoss/caip for chain/account/asset IDs
Use useAppSelector for Redux state
Use useAppDispatch for Redux actions
Memoize expensive computations with useMemo
Memoize callbacks with useCallback

**/*.{ts,tsx}: Use Result<T, E> pattern for error handling in swappers and APIs; ALWAYS use Ok() and Err() from @sniptt/monads; AVOID throwing within swapper API implementations
ALWAYS use custom error classes from @shapeshiftoss/errors with meaningful error codes for internationalization and relevant details in error objects
ALWAYS wrap async op...

Files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
  • packages/swap-widget/src/utils/addressValidation.ts
  • packages/swap-widget/vite.config.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (.cursor/rules/error-handling.mdc)

**/*.{tsx,jsx}: ALWAYS wrap React components in error boundaries and provide user-friendly fallback components with error logging
ALWAYS use useErrorToast hook for displaying errors with translated error messages and handle different error types appropriately

Use PascalCase for React component names and match the component name to the file name

Files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/naming-conventions.mdc)

**/*.{js,jsx,ts,tsx}: Use camelCase for variables, functions, and methods with descriptive names that explain the purpose
Use verb prefixes for functions that perform actions (e.g., fetch, validate, execute, update, calculate)
Use UPPER_SNAKE_CASE for constants and configuration values with descriptive names
Use handle prefix for event handlers with descriptive names in camelCase
Use descriptive boolean variable names with is, has, can, should prefixes
Use named exports for components, functions, and utilities instead of default exports
Use descriptive import names and avoid renaming imports unless necessary
Avoid non-descriptive variable names like data, item, obj, and single-letter variable names except in loops
Avoid abbreviations in names unless they are widely understood
Avoid generic function names like fn, func, or callback

Files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
  • packages/swap-widget/src/utils/addressValidation.ts
  • packages/swap-widget/vite.config.ts
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/react-best-practices.mdc)

**/*.{jsx,tsx}: ALWAYS use useMemo for expensive computations, object/array creations, and filtered data
ALWAYS use useMemo for derived values and computed properties
ALWAYS use useMemo for conditional values and simple transformations
ALWAYS use useCallback for event handlers and functions passed as props
ALWAYS use useCallback for any function that could be passed as a prop or dependency
ALWAYS include all dependencies in useEffect, useMemo, useCallback dependency arrays
NEVER use // eslint-disable-next-line react-hooks/exhaustive-deps unless absolutely necessary, and ALWAYS explain why dependencies are excluded if using eslint disable
ALWAYS use named exports for components; NEVER use default exports for components
KEEP component files under 200 lines when possible; BREAK DOWN large components into smaller, reusable pieces
EXTRACT complex logic into custom hooks
ALWAYS wrap components in error boundaries for production
ALWAYS handle async errors properly in async operations
ALWAYS provide user-friendly error messages in error handling
ALWAYS use virtualization for lists with 100+ items
ALWAYS implement proper key props for list items
ALWAYS lazy load heavy components using React.lazy for code splitting
ALWAYS use Suspense wrapper for lazy loaded components
USE local state for component-level state; LIFT state up when needed across multiple components; USE Context for avoiding prop drilling; USE Redux only for global state shared across multiple places
Wrap components receiving props with memo for performance optimization

Files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/react-best-practices.mdc)

Ensure TypeScript types are explicit and proper; avoid use of any type

Files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
🧠 Learnings (42)
📓 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: NeOMakinG
Repo: shapeshift/web PR: 10323
File: src/pages/Fox/components/RFOXSection.tsx:234-248
Timestamp: 2025-08-25T12:59:43.842Z
Learning: NeOMakinG considers suggestions to refactor working modal state management from useState to useDisclosure as too nitpicky, preferring to keep such cosmetic code improvements out of feature PRs unless they address actual issues.
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: 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: 2025-10-21T17:11:18.087Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10871
File: src/components/Modals/Send/hooks/useSendDetails/useSendDetails.tsx:426-428
Timestamp: 2025-10-21T17:11:18.087Z
Learning: In src/components/Modals/Send/hooks/useSendDetails/useSendDetails.tsx, within the handleInputChange function, use .toFixed() without arguments (not .toString()) when converting BigNumber amounts for input field synchronization. This avoids exponential notation in the input while preserving precision for presentational components like <Amount.Crypto /> and <Amount.Fiat /> to format appropriately.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-09-12T12:00:33.924Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10461
File: src/plugins/walletConnectToDapps/components/modals/SendTransactionConfirmation.tsx:42-50
Timestamp: 2025-09-12T12:00:33.924Z
Learning: gomesalexandre prefers maintaining consistency with existing code patterns across WalletConnect modal components, including side-effects-during-render for error handling (showErrorToast + handleReject calls before return null), rather than introducing isolated refactors that would create inconsistency in the codebase.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-10-17T07:51:58.374Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10783
File: src/context/WalletProvider/NewWalletViews/NewWalletViewsSwitch.tsx:344-349
Timestamp: 2025-10-17T07:51:58.374Z
Learning: In the shapeshift/web codebase, Chakra UI's ModalContent component supports the containerProps prop. When using the useModalRegistration hook from ModalStackProvider, spreading {...modalContentProps} directly onto ModalContent is correct and properly applies z-index and pointer-events through containerProps.sx. Do not suggest extracting sx from modalContentProps.containerProps.sx.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-10-07T03:44:27.350Z
Learnt from: 0xApotheosis
Repo: shapeshift/web PR: 10760
File: src/components/ManageHiddenAssets/ManageHiddenAssetsList.tsx:78-84
Timestamp: 2025-10-07T03:44:27.350Z
Learning: In the ShapeShift web codebase, the following are stable references and do not need to be included in useCallback/useMemo dependency arrays:
- `navigate` from `useBrowserRouter()` hook
- Modal control objects (like `walletDrawer`) from `useModal()` hook (including their `isOpen`, `close`, and `open` methods)
- These are backed by stable context providers

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-09-12T12:00:33.924Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10461
File: src/plugins/walletConnectToDapps/components/modals/SendTransactionConfirmation.tsx:42-50
Timestamp: 2025-09-12T12:00:33.924Z
Learning: gomesalexandre prefers maintaining consistency with existing code patterns in WalletConnect modals, including side-effects-during-render for error handling (showErrorToast + handleReject), rather than introducing isolated refactors that would make the codebase inconsistent.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-08-27T09:47:59.394Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10323
File: src/components/Modal/components/DialogCloseButton.tsx:29-37
Timestamp: 2025-08-27T09:47:59.394Z
Learning: In shapeshift/web, for DialogCloseButton.tsx, the onClick handler must remain on Drawer.Close (not moved to IconButton with asChild) because stopPropagation() needs to be called at the Drawer.Close level to properly handle drawer closing behavior; moving event handling to the child IconButton would break the event flow that Drawer.Close expects.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-08-08T15:00:49.887Z
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.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-08-25T23:32:13.876Z
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.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-08-08T14:59:40.422Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10231
File: src/pages/Explore/ExploreCategory.tsx:231-238
Timestamp: 2025-08-08T14:59:40.422Z
Learning: In src/pages/Explore/ExploreCategory.tsx, for the PageHeader filter trigger, NeOMakinG considers changing a clickable Chakra Icon to IconButton too nitpicky for this PR and prefers to keep the current Icon-based trigger; such minor a11y/UI nitpicks should be deferred to a follow-up if needed.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-09-16T09:32:14.405Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10490
File: src/components/Layout/Header/NavBar/ShapeShiftMenu.tsx:0-0
Timestamp: 2025-09-16T09:32:14.405Z
Learning: gomesalexandre dismissed keyboard accessibility suggestion for hover dropdown in ShapeShiftMenu.tsx, referencing his previous comment about hover dropdowns not being suitable for keyboard navigation and apologizing to "vimium users", consistent with his pattern of deferring minor a11y improvements to keep PR scope focused.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-09-16T09:32:14.405Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10490
File: src/components/Layout/Header/NavBar/ShapeShiftMenu.tsx:0-0
Timestamp: 2025-09-16T09:32:14.405Z
Learning: gomesalexandre dismissed keyboard accessibility suggestion for hover dropdown in ShapeShiftMenu.tsx by referencing his previous comment on NavigationDropdown.tsx where he said "meh keyboard, it's a hover dropdown, how are you going to see it with keyboard nav? sry vimium users", consistent with his pattern of deferring minor a11y improvements to keep PR scope focused.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-09-16T09:31:15.378Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10490
File: src/components/Layout/Header/NavBar/NavigationDropdown.tsx:75-90
Timestamp: 2025-09-16T09:31:15.378Z
Learning: gomesalexandre dismissed keyboard accessibility suggestion for hover dropdown in NavigationDropdown.tsx, noting that hover dropdowns aren't suitable for keyboard navigation and apologizing to "vimium users", consistent with his pattern of deferring minor a11y improvements to keep PR scope focused.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-09-08T22:00:48.005Z
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.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-08-25T12:59:43.842Z
Learnt from: NeOMakinG
Repo: shapeshift/web PR: 10323
File: src/pages/Fox/components/RFOXSection.tsx:234-248
Timestamp: 2025-08-25T12:59:43.842Z
Learning: NeOMakinG considers suggestions to refactor working modal state management from useState to useDisclosure as too nitpicky, preferring to keep such cosmetic code improvements out of feature PRs unless they address actual issues.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-12-27T16:02:52.792Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11536
File: src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeExecution.tsx:252-265
Timestamp: 2025-12-27T16:02:52.792Z
Learning: When reviewing bug fixes, especially in shapeshift/web, prefer minimal changes that fix correctness over introducing broader refactors or quality-of-life improvements (e.g., extracting duplicated logic) unless such improvements are essential to the fix. Apply this guideline broadly to TSX files and related components, not just the specific location, to keep changes focused and maintainable.

Applied to files:

  • packages/swap-widget/src/components/AddressInputModal.tsx
📚 Learning: 2025-11-24T21:20:57.909Z
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/**/*.ts : Use TypeScript with explicit types (e.g., SupportedChainIds) for all code in the Swapper system

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
  • packages/swap-widget/vite.config.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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 : Reuse executeEvmTransaction utility for EVM-based swappers instead of implementing custom transaction execution

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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/*/endpoints.ts : Reuse checkEvmSwapStatus utility for checking EVM swap status instead of implementing custom status checks

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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/*/utils/constants.ts : Define supported chain IDs for each swapper in utils/constants.ts with both 'sell' and 'buy' properties following the pattern: SupportedChainIds type

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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 : Avoid side effects in swap logic; ensure swap methods are deterministic and stateless

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
  • packages/swap-widget/vite.config.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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 : Validate inputs and log errors for debugging in Swapper system implementations

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-11-12T12:49:17.895Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11016
File: packages/swapper/src/swappers/NearIntentsSwapper/swapperApi/getTradeQuote.ts:109-125
Timestamp: 2025-11-12T12:49:17.895Z
Learning: In packages/chain-adapters/src/evm/utils.ts, the getErc20Data function already includes a guard that returns an empty string when contractAddress is undefined (line 8: `if (!contractAddress) return ''`). This built-in handling means callers don't need to conditionally invoke getErc20Data—it safely handles both ERC20 tokens and native assets.

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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 : Implement filterAssetIdsBySellable method to filter assets by supported chain IDs in the sell property

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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 : Implement filterBuyAssetsBySellAssetId method to filter assets by supported chain IDs in the buy property

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 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:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-09-12T11:56:19.437Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10461
File: src/plugins/walletConnectToDapps/utils/tenderly/index.ts:0-0
Timestamp: 2025-09-12T11:56:19.437Z
Learning: gomesalexandre rejected verbose try/catch error handling for address validation in Tenderly integration (PR #10461), calling the approach "ugly" but still implemented safety measures in commit ad7e424b89, preferring cleaner safety implementations over defensive programming patterns.

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2026-01-08T23:17:05.494Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11578
File: src/pages/Yields/hooks/useYieldTransactionFlow.ts:183-185
Timestamp: 2026-01-08T23:17:05.494Z
Learning: In the Yield.xyz API integration (src/pages/Yields/hooks/useYieldTransactionFlow.ts), the cosmosPubKey argument field accepts the user's Cosmos address (userAddress), not the raw public key. This is the expected format for the Yield.xyz API endpoints (enterYield/exitYield/manageYield).

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-10-15T15:57:39.956Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10810
File: src/plugins/walletConnectToDapps/utils/tenderly/index.ts:212-0
Timestamp: 2025-10-15T15:57:39.956Z
Learning: gomesalexandre uses discriminated union patterns (e.g., `isEIP1559 ? { max_fee_per_gas, max_priority_fee_per_gas } : { gas_price }`) in WalletConnect flows without additional validation guards, trusting that the runtime data structure ensures mutual exclusivity between EIP-1559 and legacy gas pricing fields.

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 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} : Avoid `let` variable assignments - prefer `const` with inline IIFE switch statements or extract to functions for conditional logic

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-11-19T16:59:50.569Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11012
File: src/context/WalletProvider/Vultisig/components/Connect.tsx:24-59
Timestamp: 2025-11-19T16:59:50.569Z
Learning: In src/context/WalletProvider/*/components/Connect.tsx files across the ShapeShift web codebase, the established pattern for handling null/undefined adapter from getAdapter() is to simply check `if (adapter) { ... }` without an else clause. All wallet Connect components (Coinbase, Keplr, Phantom, Ledger, MetaMask, WalletConnectV2, KeepKey, Vultisig) follow this pattern—they reset loading state after the if block but do not show error messages when adapter is null. This is an intentional design decision and should be maintained for consistency.

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-11-20T12:00:45.005Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11078
File: src/setupVitest.ts:11-15
Timestamp: 2025-11-20T12:00:45.005Z
Learning: In shapeshift/web, src/setupVitest.ts must redirect 'ethers' to 'ethers5' for shapeshiftoss/hdwallet-trezor (and -trezor-connect), same as ledger and shapeshift-multichain. Removing 'trezor' from the regex causes CI/Vitest failures due to ethers v6 vs v5 API differences.

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2026-01-07T15:36:13.236Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11569
File: headers/csps/chains/katana.ts:8-10
Timestamp: 2026-01-07T15:36:13.236Z
Learning: In CSP chain files (headers/csps/chains/*.ts), explicitly filtering undefined environment variables with .filter(Boolean) is unnecessary because serializeCsp() in headers/util.ts filters all falsy values during serialization at line 51: `.map(([k, v]) => [k, v.filter(x => !!x)])`. The direct pattern [env.VITE_*_NODE_URL] is sufficient and preferred for consistency with second-class chain CSP files like plasma.ts and monad.ts.
<!-- </add_learning>

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-11-24T21:20:17.804Z
Learnt from: CR
Repo: shapeshift/web PR: 0
File: .cursor/rules/error-handling.mdc:0-0
Timestamp: 2025-11-24T21:20:17.804Z
Learning: Applies to **/swapper{s,}/**/*.{ts,tsx} : ALWAYS use `makeSwapErrorRight` for swapper errors with `TradeQuoteError` enum for error codes and provide detailed error information

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-12-01T22:01:37.982Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11240
File: packages/swapper/src/swappers/CetusSwapper/endpoints.ts:56-58
Timestamp: 2025-12-01T22:01:37.982Z
Learning: In packages/swapper/src/swappers/CetusSwapper/endpoints.ts, gomesalexandre is comfortable with mutating the shared Cetus SDK singleton instance (sdk.senderAddress = from) when required by the SDK API, preferring pragmatic working code over theoretical statelessness concerns.

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-12-04T22:57:50.850Z
Learnt from: kaladinlight
Repo: shapeshift/web PR: 11290
File: packages/chain-adapters/src/utxo/zcash/ZcashChainAdapter.ts:48-51
Timestamp: 2025-12-04T22:57:50.850Z
Learning: In packages/chain-adapters/src/**/*ChainAdapter.ts files, the getName() method uses the pattern `const enumIndex = Object.values(ChainAdapterDisplayName).indexOf(ChainAdapterDisplayName.XXX); return Object.keys(ChainAdapterDisplayName)[enumIndex]` to reverse-lookup the enum key from its value. This is the established pattern used consistently across almost all chain adapters (Bitcoin, Ethereum, Litecoin, Dogecoin, Polygon, Arbitrum, Cosmos, etc.) and should be preserved for consistency when adding new chain adapters.

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 Learning: 2025-10-23T14:27:19.073Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 10857
File: src/plugins/walletConnectToDapps/eventsManager/useWalletConnectEventsHandler.ts:101-104
Timestamp: 2025-10-23T14:27:19.073Z
Learning: In WalletConnect wallet_switchEthereumChain and wallet_addEthereumChain requests, the chainId parameter is always present as per the protocol spec. Type guards checking for missing chainId in these handlers (like `if (!evmNetworkIdHex) return`) are solely for TypeScript compiler satisfaction, not real runtime edge cases.

Applied to files:

  • packages/swap-widget/src/utils/addressValidation.ts
📚 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 src/config.ts : Default values always come from environment variables prefixed with `VITE_FEATURE_`

Applied to files:

  • packages/swap-widget/vite.config.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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

Applied to files:

  • packages/swap-widget/vite.config.ts
📚 Learning: 2025-12-03T23:19:39.158Z
Learnt from: gomesalexandre
Repo: shapeshift/web PR: 11275
File: headers/csps/chains/plasma.ts:1-10
Timestamp: 2025-12-03T23:19:39.158Z
Learning: For CSP files in headers/csps/chains/, gomesalexandre prefers using Vite's loadEnv() pattern directly to load environment variables (e.g., VITE_PLASMA_NODE_URL, VITE_MONAD_NODE_URL) for consistency with existing second-class chain CSP files, rather than using getConfig() from src/config.ts, even though other parts of the codebase use validated config values.

Applied to files:

  • packages/swap-widget/vite.config.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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)

Applied to files:

  • packages/swap-widget/vite.config.ts
📚 Learning: 2025-11-24T21:20:57.909Z
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/**/*.ts : Use camelCase for variable and function names in the Swapper system

Applied to files:

  • packages/swap-widget/vite.config.ts
🧬 Code graph analysis (1)
packages/swap-widget/src/components/AddressInputModal.tsx (2)
packages/swap-widget/src/types/index.ts (1)
  • ChainId (1-1)
packages/swap-widget/src/utils/addressValidation.ts (2)
  • validateAddress (116-183)
  • getAddressFormatHint (188-214)
🔇 Additional comments (10)
packages/swap-widget/vite.config.ts (1)

1-40: Clean Vite configuration for dual-mode builds.

The configuration correctly handles both library and application builds:

  • Library mode externalizes react and react-dom appropriately to avoid duplicate React instances when consumed as a dependency.
  • The define: { 'process.env': {} } pattern is standard for browser builds to prevent Node.js reference errors.
  • Server/preview ports are sensibly configured to avoid conflicts with the main app.
packages/swap-widget/src/utils/addressValidation.ts (5)

9-11: LGTM!

Using viem's isAddress with strict: false is appropriate for EVM address validation, allowing both checksummed and non-checksummed addresses.


16-32: LGTM!

The Bitcoin address validation covers all major formats (Legacy P2PKH, P2SH, Native SegWit, and Taproot). The regex patterns appropriately exclude ambiguous base58 characters (0, O, I, l) and handle case-insensitivity for Bech32 addresses.


37-70: LGTM!

The Bitcoin Cash, Litecoin, and Dogecoin validators appropriately cover their respective address formats with correct prefix handling.


75-111: LGTM!

The Cosmos Bech32 and Solana base58 validators are well-implemented with appropriate character sets and length constraints.


188-214: LGTM!

The format hints provide clear, user-friendly guidance for each chain type. The implementation correctly mirrors the chain detection logic from validateAddress.

packages/swap-widget/src/components/AddressInputModal.tsx (4)

8-17: LGTM!

The useLockBodyScroll hook correctly captures and restores the original overflow style, following proper cleanup patterns.


43-58: LGTM!

Both effects are well-implemented with correct dependency arrays. The global keydown listener for Escape follows the pattern mentioned in the PR comments (commit 666f219), and proper cleanup is performed.


135-137: Hardcoded UI strings may limit internationalization.

The modal contains several hardcoded English strings ("Receive Address", "Enter {chainName} address", "Use connected wallet", etc.). If the widget needs i18n support in the future, these should use translation keys.

Is internationalization planned for this widget? If so, consider extracting strings to a translation system.


29-37: LGTM overall!

The component is well-structured with proper state management, memoization, and accessibility attributes. The integration with address validation utilities is clean and provides good user feedback.


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.

2 participants