-
Notifications
You must be signed in to change notification settings - Fork 198
feat: swap widget #11630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
feat: swap widget #11630
Conversation
📝 WalkthroughWalkthroughAdds a new Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as SwapWidget
participant Assets as Asset Hooks
participant API as ApiClient
participant Wallet as WalletClient
User->>UI: open token selector
UI->>Assets: fetch assets/chains/balances
Assets-->>UI: assets, chains, balances
User->>UI: choose sell token & amount
UI->>API: getRates(sell,buy,amount)
API-->>UI: rates[]
User->>UI: open quotes modal / select rate
UI->>UI: compute selection, close modal
User->>UI: confirm swap
UI->>API: getQuote(params)
API-->>UI: quote with tx data
UI->>Wallet: submit transaction
Wallet-->>UI: tx hash / status
UI->>User: show success / error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Organization UI Review profile: CHILL Plan: Pro Disabled knowledge base sources:
⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
🧰 Additional context used🧠 Learnings (6)📓 Common learnings📚 Learning: 2026-01-03T15:12:57.968ZApplied to files:
📚 Learning: 2025-08-08T20:16:12.898ZApplied to files:
📚 Learning: 2025-11-19T22:20:25.661ZApplied to files:
📚 Learning: 2025-11-24T21:20:04.979ZApplied to files:
📚 Learning: 2025-08-29T18:09:45.982ZApplied to files:
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (1)
✏️ Tip: You can disable this entire section by setting 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. Comment |
- 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
There was a problem hiding this 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
onmethod signature only supports callbacks withstring[](accounts), but EIP-1193 providers emit various events with different payload types (e.g.,chainChangedreturns a hex string,disconnectreturns 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: Hardcodedcolor: whitemay 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
reactandreact-dom, butwagmiandviemare 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:
- Externalizing
wagmiandviemin rollupOptions- 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-depsusage and consider adding a non-root user.Using
--legacy-peer-depsmasks 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 typingwalletClientmore explicitly.The
unknowntype forwalletClientin 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: noneon 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
parseFloatfor 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 useRecordinstead ofPartial<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 forSWAPPER_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 inlineonKeyDownhandler 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 === bestRateuses reference equality. IfselectedRateis 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.
handleSelectRatesimply delegates toonSelectRatewithout any transformation. Consider passingonSelectRatedirectly 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 usingMaxUint256for 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.writeTextcall 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
filteredDatais computed using an IIFE that runs on every render. SinceuseMarketDatamay 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
useMemofrom 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
chainsvariable is computed inside an IIFE that runs on every render of components usinguseChains. 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, thesearchResultsinuseAssetSearchis 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,
Nominaltypes are preferred for domain identifiers likeChainIdandAssetIdto 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
Addressed CodeRabbit Review CommentsI've addressed all the actionable CodeRabbit comments in commit 666f219: High Priority Fixes
Medium Priority Fixes
Low Priority Fixes
|
There was a problem hiding this 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
1separator is restricted toqpzry9x8gf2tvdw0s3jn54khce6mua7l(32 specific characters), not[a-z0-9]. This regex will accept addresses with invalid characters likeb,i,o, or digits1in 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
validateAddressandgetAddressFormatHint. 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 usinguseMemofor filtered data.The IIFE pattern works but using
useMemowould 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 exceedingNumber.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
queryClientis created at module scope, meaning multipleSwapWidgetinstances 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
setTimeoutwith a 3-second delay may not align with actual transaction confirmation times, which vary by network. Consider usingwaitForTransactionReceiptfor the swap transaction or implementing a polling strategy.
| "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" | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -20Repository: 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.
| 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, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
| const chain = { | ||
| id: requiredChainId, | ||
| name: chainMeta?.name ?? "Chain", | ||
| nativeCurrency, | ||
| rpcUrls: { default: { http: [] } }, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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
walletClientpropenableWalletConnectionpropallowedChainIds,disabledChainIds,allowedAssetIds,disabledAssetIdspropsdefaultReceiveAddressprop for locking destination addressComponents
SwapWidget- Main entry pointTokenSelectModal- Virtualized token selector with searchQuotesModal- Compare quotes from multiple swappersSettingsModal- Slippage and receive address configurationAddressInputModal- External receive address inputWalletProvider- RainbowKit wallet connection wrapperAPI Hooks
useAssets/useAssetById/useAssetSearch/useAssetsByChainId- Asset data fetchinguseChains- Chain metadatauseBalances- Wallet balance fetching (EVM)useMarketData- USD price datauseSwapRates- Rate/quote fetching with throttlinguseSwapQuote- Full quote fetching for executionDeployment
Risk
Low - This is a new standalone package that does not affect the main web application.
Testing
Engineering
yarn dev:swap-widgetto start the demo appOperations
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
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.