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
    • Added a comprehensive swap widget enabling asset exchange across multiple blockchain networks.
    • Integrated wallet connection and transaction execution capabilities.
    • Introduced customizable theming support with dark and light modes.
    • Added real-time market data display and quote selection for optimal rates.
    • Implemented cross-chain address validation for secure transactions.
    • Included Docker containerization for deployment.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 12, 2026

📝 Walkthrough

Walkthrough

This PR introduces a complete cryptocurrency swap widget package (@shapeshiftoss/swap-widget) with React components for asset selection, rate quotes, address validation, and transaction execution. Includes TypeScript types, API client, React hooks for market data and balances, wallet integration via Wagmi/RainbowKit, demo app with theming, and Vite-based library build configuration.

Changes

Cohort / File(s) Summary
Root Configuration
package.json, railway.json
Added dev:swap-widget npm script; introduced Railway deployment config with Dockerfile path and restart policy for swap-widget
Package Setup & Build
packages/swap-widget/package.json, packages/swap-widget/tsconfig.json, packages/swap-widget/tsconfig.node.json, packages/swap-widget/vite.config.ts
Declared @shapeshiftoss/swap-widget npm package (v0.0.1, private) with dev/build/lint scripts; configured TypeScript strict mode and Vite with conditional library build (entry src/index.ts, external React/ReactDOM)
Deployment & Documentation
packages/swap-widget/Dockerfile, packages/swap-widget/README.md
Multi-stage Docker build (node:20-slim, legacy-peer-deps); comprehensive README covering installation, peer dependencies, Props/Types, theming (Simple & Custom modes), usage examples, supported chains, and limitations
Core Types & Constants
packages/swap-widget/src/types/index.ts, packages/swap-widget/src/constants/chains.ts, packages/swap-widget/src/constants/swappers.ts
Defined domain types (ChainId, AssetId, Asset, Chain, TradeRate, ThemeConfig, SwapWidgetProps); chain metadata mappings with accessors (getChainMeta, getChainName); swapper icons/colors; cross-chain helpers (isEvmChainId, formatAmount, truncateAddress)
API Client
packages/swap-widget/src/api/client.ts
Factory function createApiClient(config) exposing getAssets, getRates, getQuote; supports configurable baseUrl and API key; enforces HTTP success and JSON parsing
Utility Modules
packages/swap-widget/src/utils/addressValidation.ts, packages/swap-widget/src/utils/redirect.ts
Cross-chain address validation (EVM, Bitcoin, Bitcoin Cash, Litecoin, Dogecoin, Cosmos, Solana); ShapeShift trade URL builder and chain-type inference; canExecuteInWidget gate
Data Hooks
packages/swap-widget/src/hooks/useAssets.ts, packages/swap-widget/src/hooks/useBalances.ts, packages/swap-widget/src/hooks/useMarketData.ts
useAssets/useAssetById/useChains for asset manifest loading; useAssetBalance/useEvmBalances with queued balance fetching (ERC-20 & native); useMarketData with Coingecko integration and pagination
Swap Hooks
packages/swap-widget/src/hooks/useSwapRates.ts, packages/swap-widget/src/hooks/useSwapQuote.ts
useSwapRates fetches and sorts rates by best amount; useSwapQuote fetches transaction-ready quote; both configured with staleTime/refetch intervals
UI Components & Styling
packages/swap-widget/src/components/{SwapWidget,TokenSelectModal,QuoteSelector,QuotesModal,AddressInputModal,SettingsModal}.tsx, packages/swap-widget/src/components/*.css
SwapWidget orchestrates swap flow (asset selection, rate fetching, quote selection, address input, execution, transaction status); TokenSelectModal with virtualized lists and balance display; QuoteSelector/QuotesModal for rate comparison; AddressInputModal with cross-chain validation; SettingsModal for slippage tolerance; WalletProvider wraps Wagmi/RainbowKit/TanStack Query; comprehensive themable CSS (dark/light modes, animations, responsive layout)
Demo Application
packages/swap-widget/src/demo/App.tsx, packages/swap-widget/src/demo/App.css, packages/swap-widget/src/demo/main.tsx
Demo showcases SwapWidget with customizable theming (6 presets: Blue, Rose, Purple, Cyan, Green, Orange), color picker UI, copy-to-clipboard for config; main.tsx bootstraps QueryClient and renders App with strict mode
Entry Points & Environment
packages/swap-widget/index.html, packages/swap-widget/src/index.ts, packages/swap-widget/src/vite-env.d.ts
HTML entry with root div and module script to /src/demo/main.tsx; index.ts re-exports public API (SwapWidget, types, hooks, chain utilities); vite-env.d.ts declares EthereumProvider interface for window.ethereum

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SwapWidget
    participant TokenSelectModal
    participant QuotesModal
    participant AddressInputModal
    participant API as API Client
    participant WalletClient
    
    User->>SwapWidget: Load widget with theme
    User->>SwapWidget: Click select sell asset
    SwapWidget->>TokenSelectModal: Open modal
    TokenSelectModal->>TokenSelectModal: Fetch assets, chains, balances
    User->>TokenSelectModal: Select asset (e.g., ETH)
    TokenSelectModal->>SwapWidget: onSelect(asset)
    
    User->>SwapWidget: Click select buy asset
    SwapWidget->>TokenSelectModal: Open modal
    User->>TokenSelectModal: Select asset (e.g., USDC)
    TokenSelectModal->>SwapWidget: onSelect(asset)
    
    User->>SwapWidget: Enter sell amount
    SwapWidget->>API: getRates(sellAsset, buyAsset, amount)
    API-->>SwapWidget: Return rates sorted by buy amount
    SwapWidget->>QuoteSelector: Display best rate & alternatives
    
    User->>SwapWidget: Click view all rates
    SwapWidget->>QuotesModal: Open with rates
    User->>QuotesModal: Select preferred rate
    QuotesModal->>SwapWidget: onSelectRate(rate)
    
    User->>SwapWidget: Click swap (non-EVM or address needed)
    SwapWidget->>AddressInputModal: Open modal (validate by chainId)
    User->>AddressInputModal: Enter receive address
    AddressInputModal->>SwapWidget: onAddressChange(address)
    
    User->>SwapWidget: Click execute swap
    SwapWidget->>API: getQuote(params with address)
    API-->>SwapWidget: Return quote with transaction data
    SwapWidget->>WalletClient: Send transaction
    WalletClient-->>SwapWidget: Transaction hash
    SwapWidget->>SwapWidget: Poll tx status, update UI
    SwapWidget->>User: Display success/error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

high risk

Poem

🐰 A widget hops into the light,
With swaps and rates and colors bright,
Through chains it leaps with balance checks,
And quotes selected—what comes next?
Addresses validated, themes customized true,
The swap ecosystem now breaks through!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'feat: swap widget' is vague and generic. It refers to a general feature (swap widget) without clarifying the specific purpose or scope of the addition. Consider a more descriptive title that captures the key purpose, such as 'feat: add embeddable swap widget component' or 'feat: introduce @shapeshiftoss/swap-widget package'.
✅ 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

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
@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'>

Comment on lines +17 to +27
"dependencies": {
"@rainbow-me/rainbowkit": "^2.2.3",
"@shapeshiftoss/caip": "^8.16.5",
"@tanstack/react-query": "^5.60.0",
"p-queue": "^8.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-virtuoso": "^4.18.1",
"viem": "^2.21.0",
"wagmi": "^2.14.0"
},
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

React should only be in peerDependencies, not dependencies.

Having react and react-dom in both dependencies and peerDependencies can cause duplicate React instances at runtime, leading to hooks failures and other issues. Remove them from dependencies and keep only the peer dependency declaration.

Suggested fix
   "dependencies": {
     "@rainbow-me/rainbowkit": "^2.2.3",
     "@shapeshiftoss/caip": "^8.16.5",
     "@tanstack/react-query": "^5.60.0",
     "p-queue": "^8.0.1",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0",
     "react-virtuoso": "^4.18.1",
     "viem": "^2.21.0",
     "wagmi": "^2.14.0"
   },
🤖 Prompt for AI Agents
In @packages/swap-widget/package.json around lines 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.

Comment on lines +25 to +26
"viem": "^2.21.0",
"wagmi": "^2.14.0"
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all viem and wagmi version declarations in package.json files
rg -n '"(viem|wagmi)":\s*"[^"]*"' --type json

Repository: shapeshift/web

Length of output: 548


Viem and wagmi versions diverge from root and other packages; wagmi mismatch is particularly problematic.

The swap-widget declares viem: ^2.21.0 and wagmi: ^2.14.0, while:

  • Root package.json has viem: 2.40.3 and wagmi: ^2.9.2
  • Other packages use viem: ^2.40.3

For viem, swap-widget's ^2.21.0 is compatible with the root's 2.40.3, but it's less strict than most other packages which pin to ^2.40.3.

For wagmi, the constraint is problematic: swap-widget requires ^2.14.0 (minimum 2.14.0) while root allows ^2.9.2. In a monorepo dependency resolution, this could cause version conflicts. Additionally, swap-widget is the only package declaring wagmi, making this divergence stand out.

Align the versions or document why swap-widget intentionally uses older constraints.

🤖 Prompt for AI Agents
In @packages/swap-widget/package.json around lines 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.

| `defaultSlippage` | `string` | `"0.5"` | Default slippage tolerance percentage. |
| `showPoweredBy` | `boolean` | `true` | Show "Powered by ShapeShift" branding. |
| `enableWalletConnection` | `boolean` | `false` | Enable built-in wallet connection UI using RainbowKit. Requires `walletConnectProjectId`. |
| `walletConnectProjectId` | `string` | - | WalletConnect project ID for the built-in wallet connection. Get one at https://cloud.walletconnect.com. |
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

Wrap bare URL in angle brackets or as a markdown link.

Static analysis flagged a bare URL. Markdown best practice is to wrap URLs.

📝 Suggested fix
-| `walletConnectProjectId` | `string`                                        | -                | WalletConnect project ID for the built-in wallet connection. Get one at https://cloud.walletconnect.com. |
+| `walletConnectProjectId` | `string`                                        | -                | WalletConnect project ID for the built-in wallet connection. Get one at <https://cloud.walletconnect.com>. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `walletConnectProjectId` | `string` | - | WalletConnect project ID for the built-in wallet connection. Get one at https://cloud.walletconnect.com. |
| `walletConnectProjectId` | `string` | - | WalletConnect project ID for the built-in wallet connection. Get one at <https://cloud.walletconnect.com>. |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

79-79: Bare URL used

(MD034, no-bare-urls)

🤖 Prompt for AI Agents
In @packages/swap-widget/README.md at 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.

Comment on lines +36 to +40
const response = await fetch(url.toString(), fetchOptions)
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`)
}
return response.json() as Promise<T>
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

API calls lack timeout handling.

Per coding guidelines for API files, timeouts should be used for API calls. The current implementation has no timeout, which could cause requests to hang indefinitely.

🔧 Add timeout using AbortController
   const fetchWithConfig = async <T>(
     endpoint: string,
     params?: Record<string, string>,
     method: 'GET' | 'POST' = 'GET',
+    timeoutMs = 30000,
   ): Promise<T> => {
     const url = new URL(`${baseUrl}${endpoint}`)
     const headers: Record<string, string> = {
       'Content-Type': 'application/json',
     }
     if (config.apiKey) {
       headers['x-api-key'] = config.apiKey
     }

+    const controller = new AbortController()
+    const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
+
-    const fetchOptions: RequestInit = { headers, method }
+    const fetchOptions: RequestInit = { headers, method, signal: controller.signal }

     // ... rest of the function

-    const response = await fetch(url.toString(), fetchOptions)
+    const response = await fetch(url.toString(), fetchOptions).finally(() => {
+      clearTimeout(timeoutId)
+    })
🤖 Prompt for AI Agents
In @packages/swap-widget/src/api/client.ts around lines 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.

Comment on lines +39 to +41
.ssw-address-input-wrapper.ssw-invalid {
border-color: #ef4444;
}
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

Use the theme variable for error color consistency.

The hardcoded #ef4444 differs from --ssw-error: #f44336 defined in SwapWidget.css. Using the CSS variable ensures consistent theming and easier maintenance.

🎨 Proposed fix
 .ssw-address-input-wrapper.ssw-invalid {
-  border-color: #ef4444;
+  border-color: var(--ssw-error);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.ssw-address-input-wrapper.ssw-invalid {
border-color: #ef4444;
}
.ssw-address-input-wrapper.ssw-invalid {
border-color: var(--ssw-error);
}
🤖 Prompt for AI Agents
In @packages/swap-widget/src/components/AddressInputModal.css around lines 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.

Comment on lines +246 to +253
await client.sendTransaction({
to: sellAssetAddress as `0x${string}`,
data: approvalData,
value: BigInt(0),
chain,
account: walletAddress as `0x${string}`,
})
}
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

Approval transaction does not wait for confirmation before proceeding.

The code sends the approval transaction but doesn't wait for it to be mined before sending the swap transaction. This could cause the swap to fail if the approval hasn't been confirmed yet.

🐛 Proposed fix to wait for approval confirmation
+import { waitForTransactionReceipt } from 'viem/actions'

-          await client.sendTransaction({
+          const approvalHash = await client.sendTransaction({
             to: sellAssetAddress as `0x${string}`,
             data: approvalData,
             value: BigInt(0),
             chain,
             account: walletAddress as `0x${string}`,
           })
+
+          // Wait for approval to be mined
+          await waitForTransactionReceipt(client, { hash: approvalHash })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await client.sendTransaction({
to: sellAssetAddress as `0x${string}`,
data: approvalData,
value: BigInt(0),
chain,
account: walletAddress as `0x${string}`,
})
}
import { waitForTransactionReceipt } from 'viem/actions'
const approvalHash = await client.sendTransaction({
to: sellAssetAddress as `0x${string}`,
data: approvalData,
value: BigInt(0),
chain,
account: walletAddress as `0x${string}`,
})
// Wait for approval to be mined
await waitForTransactionReceipt(client, { hash: approvalHash })
}
🤖 Prompt for AI Agents
In @packages/swap-widget/src/components/SwapWidget.tsx around lines 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.

Comment on lines +66 to +72
const testResponse = await fetch(
`${COINGECKO_PROXY_URL}/coins/markets?vs_currency=usd&per_page=1&page=1`,
).catch(() => null)

if (!testResponse?.ok) {
baseUrl = COINGECKO_DIRECT_URL
}
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

Proxy availability check lacks timeout.

The test fetch to check if the proxy is available has no timeout, which could cause the entire market data fetch to hang indefinitely if the proxy is slow to respond.

🔧 Add timeout using AbortController
+  const controller = new AbortController()
+  const timeoutId = setTimeout(() => controller.abort(), 5000)
+
   const testResponse = await fetch(
     `${COINGECKO_PROXY_URL}/coins/markets?vs_currency=usd&per_page=1&page=1`,
+    { signal: controller.signal }
   ).catch(() => null)
+
+  clearTimeout(timeoutId)
🤖 Prompt for AI Agents
In @packages/swap-widget/src/hooks/useMarketData.ts around lines 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.

Comment on lines +29 to +38
return useQuery({
queryKey: [
'swapQuote',
sellAssetId,
buyAssetId,
sellAmountCryptoBaseUnit,
sendAddress,
receiveAddress,
swapperName,
],
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

Missing slippageTolerancePercentageDecimal in queryKey causes stale cache issues.

The slippageTolerancePercentageDecimal parameter is passed to the API (line 57) but not included in the queryKey. When users change slippage tolerance, React Query will return the cached result instead of fetching a new quote with the updated slippage.

🐛 Proposed fix
   return useQuery({
     queryKey: [
       'swapQuote',
       sellAssetId,
       buyAssetId,
       sellAmountCryptoBaseUnit,
       sendAddress,
       receiveAddress,
       swapperName,
+      slippageTolerancePercentageDecimal,
     ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return useQuery({
queryKey: [
'swapQuote',
sellAssetId,
buyAssetId,
sellAmountCryptoBaseUnit,
sendAddress,
receiveAddress,
swapperName,
],
return useQuery({
queryKey: [
'swapQuote',
sellAssetId,
buyAssetId,
sellAmountCryptoBaseUnit,
sendAddress,
receiveAddress,
swapperName,
slippageTolerancePercentageDecimal,
],
🤖 Prompt for AI Agents
In @packages/swap-widget/src/hooks/useSwapQuote.ts around lines 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.

Comment on lines +161 to +169
case 'cosmos':
const expectedPrefix = getCosmosPrefix(chainId)
if (!isValidCosmosAddress(trimmedAddress, expectedPrefix)) {
const chainName = expectedPrefix
? expectedPrefix.charAt(0).toUpperCase() + expectedPrefix.slice(1)
: 'Cosmos'
return { valid: false, error: `Invalid ${chainName} address` }
}
break
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

Wrap case block in braces to fix lexical scoping.

The const expectedPrefix declaration in the switch case can be erroneously accessed by other cases. As per the static analysis hint, wrap it in a block.

🔧 Suggested fix
     case 'cosmos':
+    {
       const expectedPrefix = getCosmosPrefix(chainId)
       if (!isValidCosmosAddress(trimmedAddress, expectedPrefix)) {
         const chainName = expectedPrefix
           ? expectedPrefix.charAt(0).toUpperCase() + expectedPrefix.slice(1)
           : 'Cosmos'
         return { valid: false, error: `Invalid ${chainName} address` }
       }
       break
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'cosmos':
const expectedPrefix = getCosmosPrefix(chainId)
if (!isValidCosmosAddress(trimmedAddress, expectedPrefix)) {
const chainName = expectedPrefix
? expectedPrefix.charAt(0).toUpperCase() + expectedPrefix.slice(1)
: 'Cosmos'
return { valid: false, error: `Invalid ${chainName} address` }
}
break
case 'cosmos':
{
const expectedPrefix = getCosmosPrefix(chainId)
if (!isValidCosmosAddress(trimmedAddress, expectedPrefix)) {
const chainName = expectedPrefix
? expectedPrefix.charAt(0).toUpperCase() + expectedPrefix.slice(1)
: 'Cosmos'
return { valid: false, error: `Invalid ${chainName} address` }
}
break
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 162-162: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
In @packages/swap-widget/src/utils/addressValidation.ts around lines 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.

Comment on lines +204 to +206
case 'cosmos':
const prefix = getCosmosPrefix(chainId)
return prefix ? `${prefix}1...` : 'Enter address'
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

Wrap case block in braces to fix lexical scoping.

Same issue as in validateAddress - the const prefix declaration needs block scoping.

🔧 Suggested fix
     case 'cosmos':
+    {
       const prefix = getCosmosPrefix(chainId)
       return prefix ? `${prefix}1...` : 'Enter address'
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 'cosmos':
const prefix = getCosmosPrefix(chainId)
return prefix ? `${prefix}1...` : 'Enter address'
case 'cosmos':
{
const prefix = getCosmosPrefix(chainId)
return prefix ? `${prefix}1...` : 'Enter address'
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 205-205: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
In @packages/swap-widget/src/utils/addressValidation.ts around lines 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.

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