Skip to content

feat(swap-widget): external wallet demo + chain-aware send/receive addresses#12346

Merged
kaladinlight merged 11 commits into
developfrom
feat/swap-widget-external-wallet-demo
May 18, 2026
Merged

feat(swap-widget): external wallet demo + chain-aware send/receive addresses#12346
kaladinlight merged 11 commits into
developfrom
feat/swap-widget-external-wallet-demo

Conversation

@kaladinlight
Copy link
Copy Markdown
Member

@kaladinlight kaladinlight commented May 15, 2026

Description

Adds an external-wallet demo for the swap-widget and lands the supporting refactors that came out of building it. The widget now models the wallet boundary symmetrically across chains: a chain-aware sendAddress (derived from the sell asset's chain) mirrors the existing receive-side behaviour, dropping the "effective" prefix and the EVM-only walletAddress leak.

Commit-level overview (in order):

  • feat(swap-widget): add external wallet demo with shared customizer — new ExternalWalletApp route where the host page owns AppKit and the widget reads it via the singleton. Demo customizer is now shared between internal and external apps.
  • refactor: collapse internal/external modes onto AppKit singleton — drop the dual-mode branching; both demos go through the same provider plumbing.
  • refactor: collapse asset/chain filter props into sellFilters/buyFilters — single nested-object props instead of four flat ones.
  • refactor: drop onConnectWallet and onAssetSelect callbacks — partners interact via the widget's own UI; the callback surface wasn't load-bearing.
  • refactor: drop defaultReceiveAddress prop, simplify AppKit gatedefaultReceiveAddress was unused after the chain-aware receive plumbing; the AppKit gate no longer needs the explicit guard.
  • refactor: chain-aware send/receive addresseswalletAddresssendAddress (chain-aware via sell chain), effectiveReceiveAddressreceiveAddress, both string | undefined. New useEvmSigning hook colocates EVM signer state alongside useBitcoinSigning/useSolanaSigning, exposed as evm on SwapWalletContext. Approval and execution hooks read evm.{walletClient,address} instead of context-level fields. Tests and machine event names updated.
  • refactor: slim AppKit config, scope connect button to sell chain — delete dead src/config/wagmi.ts (only export was a number alias), internalize Bitcoin/Solana adapter factories in initializeAppKit, drop unused exports. ConnectWalletButton now derives label state from sendAddress so it correctly tracks the sell chain rather than mixing EVM-scoped evm.* with the unscoped AppKit account (which previously surfaced BTC/SOL addresses in the EVM-flavoured button).
  • chore: prettier auto-fix

Issue (if applicable)

closes #

Risk

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

Low-to-medium. No on-chain transaction shape changes. The signing call sites (useSwapApproval, useSwapExecution) now read the EVM signer through evm.walletClient/evm.address rather than top-level context fields — same underlying AppKit hook output, just routed through a named field. Quote sendAddress is now chain-aware end-to-end; previously the quoting hook branched per chain to pick the right address, so the on-wire payload is identical.

Affected surface:

  • All swappers that accept EVM, UTXO (Bitcoin), and Solana sells (signing paths reach the same hooks)
  • AppKit (Reown) wallet connect flow on both internal and external demos
  • swap-widget public prop surface — walletAddress / effectiveReceiveAddress context fields renamed; not exported to consumers

Testing

Engineering

  1. pnpm -w run type-check — clean.
  2. pnpm -w run lint — clean for swap-widget (only pre-existing warnings in hdwallet-* packages remain).
  3. pnpm --filter @shapeshiftoss/swap-widget test — machine + guard tests cover the renamed events/fields.
  4. Internal demo (/): connect via the widget's own button; verify the badge in the sell section shows the chain-correct address as you flip the sell asset between EVM / BTC / SOL. Run a small EVM → EVM and EVM → BTC swap end-to-end.
  5. External demo (#external): connect via the host header button; verify the widget reflects the same connection state and that quote → approve → execute still works without the widget owning init.

Operations

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

The swap-widget package is a standalone embed; it isn't surfaced in the main web app's runtime paths. Internal demo at / and external demo at #external of the widget's dev server.

Screenshots (if applicable)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Mayachain support to available swappers
    • Introduced theme customization options in demo with preset themes and editable colors
    • Added demo routing with separate internal and external wallet modes
  • Refactor

    • Simplified widget configuration to use unified filter objects for asset selection
    • Enhanced wallet address handling with improved custom receive address support
    • Streamlined AppKit initialization and wallet connection flow
  • Documentation

    • Added environment variable example for swap widget API configuration

Review Change Stack

kaladinlight and others added 9 commits May 14, 2026 14:48
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a second demo route (#external) that exercises the widget's external
wallet path — the demo page itself owns Reown AppKit and feeds walletClient
into <SwapWidget>. Both demos share a single theme/partner customizer panel
extracted to DemoCustomizer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ingleton

Drops the dual-tree dispatcher in SwapWidget: there's no longer a "host
brings its own walletClient" path. The widget always reads from AppKit's
global state for EVM, BTC and Solana, with the only mode difference being
whether the widget itself calls createAppKit (when walletConnectProjectId
is passed) or the host did so first. AppKitWalletProvider replaces the
prior render-prop InternalWalletProvider and gates render on AppKit init.

Public API cleanups:
- Removed walletClient prop (now derived via useWalletClient internally).
- Removed appUrl prop; the ShapeShift redirect URL is hardcoded.
- Added allowShapeshiftRedirect (default true). When false, unsupported
  routes render a disabled "Route not supported" button instead of opening
  ShapeShift in a new tab.
- Renamed enableWalletConnection -> showConnectButton; post-collapse it's
  purely a UI toggle for the widget's built-in Connect button.

Drops the standaloneWagmiConfig file. Adds .env.example documenting the
VITE_SWAP_WIDGET_API_URL override for local dev.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ters/buyFilters

Public API previously had 11 flat filter props (bilateral defaults plus
sell/buy variants) with implicit precedence rules. Replaced with a single
SwapWidgetFilters type taken per-side via sellFilters and buyFilters.
What you pass is what's used — no fallbacks to remember, no inconsistency
between which lists had bilateral shortcuts.

Also wires up allowedSwapperNames, which was declared on SwapWidgetProps
but never plumbed: now threads SwapWidget → useSwapDisplayValues →
useSwapRates so partners can actually restrict the swapper set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
onConnectWallet was vestigial: the widget can now call useAppKit().open()
directly when the user clicks the action button without a connected EVM
wallet, so no partner wiring is required. Previously the button silently
did nothing in that state unless a partner happened to pass the callback.
Also fixes the disabled-state logic so the action button stays enabled in
the "Connect X Wallet" states (EVM / BTC / Solana) instead of being
gated by the rate-fetch and receive-address checks.

onAssetSelect was a fine-grained funnel-analytics hook with no consumer.
Drop until a partner asks for an event stream.

onSwapSuccess and onSwapError remain — those have real value (analytics,
toasts, balance refresh on the partner side).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…it gate

defaultReceiveAddress let a partner pre-fill the receive-address slot AND
silently make it read-only — the user could not change where their swap
proceeds were sent without noticing a 4-char-truncated address. Niche
AA / vault flows can build that on top of the widget; the default
behavior should always route swap proceeds to the user's wallet with a
manual override available.

The receive-address slot is now a button in all cases: defaults to the
wallet-derived address for the buy chain, and the address modal handles
overrides.

Also trims AppKitWalletProvider: removes the polling fallback and the
useState-init optimization. Single source of truth: useEffect inits
AppKit when projectId is provided and flips isReady once the singleton
is set up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace EVM-only walletAddress with sendAddress derived from the sell
chain, and drop the "effective" prefix from receiveAddress. Both fields
are now string | undefined throughout context, machine, and props.
Introduce useEvmSigning to colocate EVM signer state alongside bitcoin
and solana, exposed as evm on the wallet context so approval/execution
hooks read the EVM signer the same way they read the others.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ll chain

Drop the unused wagmi.ts config file (SupportedChainId was a glorified
number alias; inlined). In appkit.ts, internalize the BTC/SOL adapter
factories into initializeAppKit since they had no external readers, and
drop unused EVM_NETWORKS/ALL_NETWORKS/SupportedNetwork/EvmNetwork exports.

ConnectWalletButton now derives its state purely from sendAddress so the
label tracks the sell chain's wallet rather than mixing EVM-scoped state
with the unscoped AppKit account (which previously surfaced a BTC/SOL
address in the EVM connect button when only those were connected).

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

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

This PR refactors the swap widget's address management from a wallet-address/effective-receive-address model to a per-chain send/receive-address model, centralizes AppKit initialization with guard-based readiness, and restructures the demo with hash-based routing and theme customization.

Changes

Swap Widget Address & Wallet Model Refactor

Layer / File(s) Summary
State Machine Address Model
src/machines/types.ts, src/machines/guards.ts, src/machines/swapMachine.ts
SwapMachineContext replaces walletAddress/effectiveReceiveAddress with sendAddress/receiveAddress. SET_WALLET_ADDRESS event becomes SET_SEND_ADDRESS. Guards updated to check sendAddress presence and receiveAddress validation.
Machine Tests & Guards
src/machines/__tests__/guards.test.ts, src/machines/__tests__/swapMachine.test.ts, src/machines/__tests__/types.test.ts
Test fixtures and unit tests updated to validate new sendAddress/receiveAddress fields and SET_SEND_ADDRESS event handling.
Wallet Context Shape
src/contexts/SwapWalletContext.tsx
SwapWalletContextValue refactored to expose sendAddress, receiveAddress, isCustomReceiveAddress, and per-chain signing objects (evm, bitcoin, solana) replacing prior walletClient/effectiveReceiveAddress.
Input & Address Selection Components
src/components/InputStep.tsx, src/components/AddressInputModal.tsx, src/components/TokenSelectModal.tsx
Components refactored to use sendAddress/receiveAddress from context. InputStep derives hasAnyWalletAddress for balance visibility. AddressInputModal shows "Use connected wallet" button conditionally on walletReceiveAddress. TokenSelectModal uses evmAddress instead of walletAddress.
AppKit & Wallet Provider Setup
src/config/appkit.ts, src/components/WalletProvider.tsx
AppKit initialization centralized via initializeAppKit(projectId) with idempotent guard. AppKitWalletProvider replaces InternalWalletProvider and gates rendering on isAppKitInitialized(). ConnectWalletButton derives address display from useSwapWallet().sendAddress.
EVM Signing Hook
src/hooks/useEvmSigning.ts
New hook creates and memoizes viem WalletClient from EIP-155 provider, exposing walletClient, checksummed address, and isConnected status.
SwapWidget Core Address Routing
src/components/SwapWidget.tsx
SwapWidgetCore computes sendAddress/receiveAddress per-chain (EVM/Bitcoin/Solana) via useEvmSigning, useBitcoinSigning, useSolanaSigning. Effects dispatch SET_SEND_ADDRESS/SET_RECEIVE_ADDRESS. SwapWalletProvider value updated with new shape.
Swap Execution & Display Hooks
src/hooks/useSwapQuoting.ts, src/hooks/useSwapDisplayValues.ts, src/hooks/useSwapExecution.ts, src/hooks/useSwapApproval.ts, src/hooks/useBalances.ts
Hooks updated to destructure sendAddress/receiveAddress and per-chain signing objects from useSwapWallet(). SupportedChainId type dependency removed from balances.
Handler Hooks & Redirect Logic
src/hooks/useSwapHandlers.ts, src/utils/redirect.ts
useSwapHandlers refactored to accept only partnerCode and allowShapeshiftRedirect, using useAppKit().openAppKit() for EVM connection. Asset selection dispatches only state events. Redirect utils replace configurable appUrl with fixed SHAPESHIFT_APP_URL.
Widget Props & Type Contracts
src/types/index.ts, src/index.ts
SwapWidgetProps refactored to use sellFilters/buyFilters, adds allowShapeshiftRedirect and showConnectButton. Removes appUrl, walletClient, onConnectWallet, enableWalletConnection, defaultReceiveAddress. SwapperName enum adds Mayachain.
Demo Hash Routing & Theme System
src/demo/main.tsx, src/demo/DemoCustomizer.tsx, src/demo/App.css
main.tsx replaces react-query with hash-based Router conditionally rendering ExternalWalletApp or InternalWalletApp. DemoCustomizer introduces useDemoTheme hook managing partner code and per-theme colors, derives themeConfig, provides clipboard copy of theme snippet. Demo CSS adds header separator and connect button styling.
External & Internal Demo Wallets
src/demo/ExternalWalletApp.tsx, src/demo/InternalWalletApp.tsx
ExternalWalletApp initializes AppKit on mount, renders HostConnectButton using useAppKit, embeds SwapWidget with showConnectButton=false. InternalWalletApp owns theme state, renders InternalDemoBody with header, customizer toggle, and SwapWidget with internal wallet flow.
Config Cleanup & Environment Setup
src/config/standaloneWagmi.ts, src/config/wagmi.ts, .env.example
standaloneWagmi and wagmi exports removed. .env.example adds VITE_SWAP_WIDGET_API_URL pointing to localhost:3005.

Sequence Diagram

sequenceDiagram
  participant SwapWidgetCore
  participant useEvmSigning
  participant SwapMachine
  participant SwapWalletContext as SwapWallet<br/>Context
  participant InputStep
  
  SwapWidgetCore->>useEvmSigning: get evm.address<br/>per sell/buy chain
  useEvmSigning-->>SwapWidgetCore: walletClient, address, isConnected
  SwapWidgetCore->>SwapMachine: dispatch SET_SEND_ADDRESS
  SwapWidgetCore->>SwapMachine: dispatch SET_RECEIVE_ADDRESS
  SwapWidgetCore->>SwapWalletContext: update context value<br/>sendAddress, receiveAddress
  InputStep->>SwapWalletContext: read sendAddress,<br/>receiveAddress
  InputStep->>InputStep: render button states based<br/>on address availability
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • shapeshift/web#12345: Both PRs directly update packages/swap-widget/src/constants/swappers.ts and the SwapperName enum to re-enable the Mayachain swapper by adding its icon/color entries.

Poem

🐰 The wallet hops from state to state,
sendAddress and receiveAddress wait,
Per-chain AppKit now takes the lead,
With signing hooks, we plant the seed,
Demo routes by hash now flow,
Watch this swap widget dance and glow! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes in this PR: adding an external wallet demo and refactoring addresses to be chain-aware with new send/receive address terminology.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/swap-widget-external-wallet-demo

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (7)
packages/swap-widget/src/components/InputStep.tsx (1)

72-72: 💤 Low value

Unused prop showConnectButton.

The showConnectButton prop is destructured as _showConnectButton but never used in the component. Either use it to conditionally control UI behavior or remove it from the props interface if it's no longer needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/components/InputStep.tsx` at line 72, The prop
showConnectButton is destructured as _showConnectButton in the InputStep
component but never used; either remove it from the props interface/parameter
list to avoid dead code (delete _showConnectButton from the destructuring and
the prop type) or use it to control the UI by replacing the hardcoded
connect-button render with a conditional render tied to showConnectButton (e.g.,
in InputStep's JSX, wrap the ConnectButton/its container with a check using the
showConnectButton prop). Ensure you update both the destructuring (remove or
rename) and the props type/usage (Prop interface or callers) accordingly.
packages/swap-widget/src/hooks/useSwapQuoting.ts (1)

63-68: 💤 Low value

Unreachable dead code.

The check if (!resolvedReceiveAddress) at line 65 is unreachable. Since sendAddress is guaranteed to be truthy at this point (line 58 returns early if it's falsy), resolvedReceiveAddress will always be receiveAddress || sendAddress, which is at minimum sendAddress (truthy).

This block can be safely removed.

♻️ Suggested simplification
         if (!sendAddress) {
           actorRef.send({ type: 'QUOTE_ERROR', error: 'No wallet address available' })
           return
         }
 
         const resolvedReceiveAddress = receiveAddress || sendAddress
 
-        if (!resolvedReceiveAddress) {
-          actorRef.send({ type: 'QUOTE_ERROR', error: 'No receive address available' })
-          return
-        }
-
         const response = await apiClient.getQuote({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/hooks/useSwapQuoting.ts` around lines 63 - 68, The
conditional checking for `resolvedReceiveAddress` is dead code because
`sendAddress` is guaranteed truthy earlier, so remove the unreachable block that
sends `actorRef.send({ type: 'QUOTE_ERROR', error: 'No receive address
available' })`; instead, keep the assignment const resolvedReceiveAddress =
receiveAddress || sendAddress and proceed with the existing quoting logic in
useSwapQuoting (referencing resolvedReceiveAddress, receiveAddress, sendAddress,
and actorRef) without the redundant null-check to simplify the flow.
packages/swap-widget/src/demo/InternalWalletApp.tsx (1)

25-27: ⚡ Quick win

Surface swap failures through toast feedback.

Line 25 only logs errors; users won’t get immediate failure feedback. Use useErrorToast in handleSwapError.

As per coding guidelines "ALWAYS use useErrorToast hook for displaying errors with translated error messages and handle different error types appropriately".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/demo/InternalWalletApp.tsx` around lines 25 - 27,
The handleSwapError callback currently only logs errors to console; update it to
use the useErrorToast hook to display a translated toast to users and handle
different error shapes (Error, string, or custom error objects). Import and call
useErrorToast inside the component, then inside handleSwapError invoke the toast
with a user-friendly, translated message (include error.message or mapped
messages for known error types) and still keep console.error for debugging;
ensure the handler signature remains handleSwapError(error: Error | unknown) so
it safely normalizes the error before passing text to useErrorToast.
packages/swap-widget/src/demo/ExternalWalletApp.tsx (1)

74-76: ⚡ Quick win

Show swap errors to users, not only the console.

Line 74 currently logs failures only. Wire onSwapError to useErrorToast so failed swaps are visible in the demo UI.

As per coding guidelines "ALWAYS use useErrorToast hook for displaying errors with translated error messages and handle different error types appropriately".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/demo/ExternalWalletApp.tsx` around lines 74 - 76,
The handler currently only logs errors to console; import and call the
useErrorToast hook in ExternalWalletApp.tsx, create a showError function (e.g.,
const showError = useErrorToast()) and update handleSwapError to call
showError(error, 'Swap failed') (and optionally keep console.error) so users see
a translated UI toast. Then wire that handler into the swap component by passing
handleSwapError to the onSwapError prop (ensure the function name
handleSwapError and the onSwapError prop are used so the demo UI surfaces failed
swaps).
packages/swap-widget/src/components/WalletProvider.tsx (1)

19-39: 💤 Low value

Potential rendering issue when projectId is omitted but AppKit was initialized externally.

If projectId is undefined, initializeAppKit is not called, but isAppKitInitialized() is still checked. If AppKit was initialized elsewhere (e.g., external wallet demo), this works. However, if neither provides initialization, the component returns null forever. This seems intentional for the external-wallet case but may warrant a comment for clarity.

📝 Suggested clarifying comment
 export const AppKitWalletProvider = ({ projectId, children }: AppKitWalletProviderProps) => {
   const [isReady, setIsReady] = useState(false)

   useEffect(() => {
+    // Initialize AppKit only if projectId is provided (internal wallet mode).
+    // External wallet mode expects AppKit to be initialized by the host app.
     if (projectId) initializeAppKit(projectId)
     if (isAppKitInitialized()) setIsReady(true)
   }, [projectId])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/components/WalletProvider.tsx` around lines 19 - 39,
The AppKitWalletProvider currently only calls initializeAppKit(projectId) when
projectId is provided but still checks isAppKitInitialized(), which means the
component intentionally allows external initialization but will return null if
neither initializes AppKit; add a clear comment inside AppKitWalletProvider near
the useEffect and the early return (referencing initializeAppKit,
isAppKitInitialized, projectId, isReady, and the wagmiConfig early return)
explaining that omission of projectId expects external initialization and that
the component will render null until AppKit is initialized to avoid silently
mounting with an invalid wagmiConfig.
packages/swap-widget/src/components/SwapWidget.tsx (1)

424-430: 💤 Low value

Consider consolidating address dispatch effects.

Two separate useEffect hooks dispatch address changes to the actor. While functionally correct, combining them could reduce effect overhead when both addresses change simultaneously (e.g., on initial wallet connection).

♻️ Optional consolidation
-  useEffect(() => {
-    actorRef.send({ type: 'SET_SEND_ADDRESS', address: sendAddress })
-  }, [sendAddress, actorRef])
-
-  useEffect(() => {
-    actorRef.send({ type: 'SET_RECEIVE_ADDRESS', address: receiveAddress })
-  }, [receiveAddress, actorRef])
+  useEffect(() => {
+    actorRef.send({ type: 'SET_SEND_ADDRESS', address: sendAddress })
+    actorRef.send({ type: 'SET_RECEIVE_ADDRESS', address: receiveAddress })
+  }, [sendAddress, receiveAddress, actorRef])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/components/SwapWidget.tsx` around lines 424 - 430,
Two separate useEffect hooks dispatch SET_SEND_ADDRESS and SET_RECEIVE_ADDRESS
to actorRef; consolidate them into a single useEffect that watches [sendAddress,
receiveAddress, actorRef] and sends both updates in one effect (call
actorRef.send for SET_SEND_ADDRESS and SET_RECEIVE_ADDRESS sequentially or send
a combined action if the actor supports it) so address updates emitted at the
same time (e.g., on wallet connect) only trigger one effect invocation.
packages/swap-widget/src/config/appkit.ts (1)

65-68: 💤 Low value

Remove unnecessary as any cast on Solana wallet adapters array.

The as any cast is not required by the SolanaAdapter API and violates TypeScript strict mode type checking. All official examples show the wallets array works correctly without the cast. Removing it will maintain proper type safety:

const solanaAdapter = new SolanaAdapter({
  wallets: [new PhantomWalletAdapter(), new SolflareWalletAdapter()],
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/config/appkit.ts` around lines 65 - 68, Remove the
unnecessary type cast on the Solana wallets array: in the SolanaAdapter
instantiation (SolanaAdapter) where wallets is set with new
PhantomWalletAdapter() and new SolflareWalletAdapter(), drop the trailing "as
any" so the wallets property is passed with its natural type; ensure the wallets
array uses [new PhantomWalletAdapter(), new SolflareWalletAdapter()] directly to
satisfy TypeScript strict mode and the SolanaAdapter API.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/swap-widget/src/demo/DemoCustomizer.tsx`:
- Around line 167-170: The clipboard write call in DemoCustomizer using
navigator.clipboard.writeText(code) can reject and is currently unhandled; add
explicit rejection handling by attaching a .catch to the promise (or use
async/try-catch if converting the handler to async) to set a user-visible error
state/feedback (e.g., setCopyError or show a toast/alert) and ensure setCopied
is not set true on failure, and implement a fallback copy method (create a
hidden textarea, select, document.execCommand('copy')) invoked when
navigator.clipboard.writeText fails; update the copy handler that calls
navigator.clipboard.writeText, and keep using setCopied to indicate success only
after the operation or fallback succeeds.

In `@packages/swap-widget/src/demo/main.tsx`:
- Around line 25-28: The demo root currently renders <Router /> directly via
ReactDOM.createRoot(rootElement).render(...) so a runtime error in Router will
blank the app; wrap the root render with an Error Boundary component (e.g.,
create a simple ErrorBoundary class or use an existing ErrorBoundary utility)
and provide a user-friendly fallback UI and error logging inside its
componentDidCatch (or onError handler) so that errors from Router are caught,
logged (include the error and info), and a fallback UI is shown instead of a
blank screen; update the ReactDOM.createRoot(...).render call to render
<ErrorBoundary><Router /></ErrorBoundary> (using rootElement and Router names to
locate the code).

---

Nitpick comments:
In `@packages/swap-widget/src/components/InputStep.tsx`:
- Line 72: The prop showConnectButton is destructured as _showConnectButton in
the InputStep component but never used; either remove it from the props
interface/parameter list to avoid dead code (delete _showConnectButton from the
destructuring and the prop type) or use it to control the UI by replacing the
hardcoded connect-button render with a conditional render tied to
showConnectButton (e.g., in InputStep's JSX, wrap the ConnectButton/its
container with a check using the showConnectButton prop). Ensure you update both
the destructuring (remove or rename) and the props type/usage (Prop interface or
callers) accordingly.

In `@packages/swap-widget/src/components/SwapWidget.tsx`:
- Around line 424-430: Two separate useEffect hooks dispatch SET_SEND_ADDRESS
and SET_RECEIVE_ADDRESS to actorRef; consolidate them into a single useEffect
that watches [sendAddress, receiveAddress, actorRef] and sends both updates in
one effect (call actorRef.send for SET_SEND_ADDRESS and SET_RECEIVE_ADDRESS
sequentially or send a combined action if the actor supports it) so address
updates emitted at the same time (e.g., on wallet connect) only trigger one
effect invocation.

In `@packages/swap-widget/src/components/WalletProvider.tsx`:
- Around line 19-39: The AppKitWalletProvider currently only calls
initializeAppKit(projectId) when projectId is provided but still checks
isAppKitInitialized(), which means the component intentionally allows external
initialization but will return null if neither initializes AppKit; add a clear
comment inside AppKitWalletProvider near the useEffect and the early return
(referencing initializeAppKit, isAppKitInitialized, projectId, isReady, and the
wagmiConfig early return) explaining that omission of projectId expects external
initialization and that the component will render null until AppKit is
initialized to avoid silently mounting with an invalid wagmiConfig.

In `@packages/swap-widget/src/config/appkit.ts`:
- Around line 65-68: Remove the unnecessary type cast on the Solana wallets
array: in the SolanaAdapter instantiation (SolanaAdapter) where wallets is set
with new PhantomWalletAdapter() and new SolflareWalletAdapter(), drop the
trailing "as any" so the wallets property is passed with its natural type;
ensure the wallets array uses [new PhantomWalletAdapter(), new
SolflareWalletAdapter()] directly to satisfy TypeScript strict mode and the
SolanaAdapter API.

In `@packages/swap-widget/src/demo/ExternalWalletApp.tsx`:
- Around line 74-76: The handler currently only logs errors to console; import
and call the useErrorToast hook in ExternalWalletApp.tsx, create a showError
function (e.g., const showError = useErrorToast()) and update handleSwapError to
call showError(error, 'Swap failed') (and optionally keep console.error) so
users see a translated UI toast. Then wire that handler into the swap component
by passing handleSwapError to the onSwapError prop (ensure the function name
handleSwapError and the onSwapError prop are used so the demo UI surfaces failed
swaps).

In `@packages/swap-widget/src/demo/InternalWalletApp.tsx`:
- Around line 25-27: The handleSwapError callback currently only logs errors to
console; update it to use the useErrorToast hook to display a translated toast
to users and handle different error shapes (Error, string, or custom error
objects). Import and call useErrorToast inside the component, then inside
handleSwapError invoke the toast with a user-friendly, translated message
(include error.message or mapped messages for known error types) and still keep
console.error for debugging; ensure the handler signature remains
handleSwapError(error: Error | unknown) so it safely normalizes the error before
passing text to useErrorToast.

In `@packages/swap-widget/src/hooks/useSwapQuoting.ts`:
- Around line 63-68: The conditional checking for `resolvedReceiveAddress` is
dead code because `sendAddress` is guaranteed truthy earlier, so remove the
unreachable block that sends `actorRef.send({ type: 'QUOTE_ERROR', error: 'No
receive address available' })`; instead, keep the assignment const
resolvedReceiveAddress = receiveAddress || sendAddress and proceed with the
existing quoting logic in useSwapQuoting (referencing resolvedReceiveAddress,
receiveAddress, sendAddress, and actorRef) without the redundant null-check to
simplify the flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6c33dc7-9c80-41d2-9971-4dbc49cdf5eb

📥 Commits

Reviewing files that changed from the base of the PR and between 7d71d44 and 7582096.

📒 Files selected for processing (33)
  • packages/swap-widget/.env.example
  • packages/swap-widget/src/components/AddressInputModal.tsx
  • packages/swap-widget/src/components/InputStep.tsx
  • packages/swap-widget/src/components/SwapWidget.tsx
  • packages/swap-widget/src/components/TokenSelectModal.tsx
  • packages/swap-widget/src/components/WalletProvider.tsx
  • packages/swap-widget/src/config/appkit.ts
  • packages/swap-widget/src/config/standaloneWagmi.ts
  • packages/swap-widget/src/config/wagmi.ts
  • packages/swap-widget/src/constants/swappers.ts
  • packages/swap-widget/src/contexts/SwapWalletContext.tsx
  • packages/swap-widget/src/demo/App.css
  • packages/swap-widget/src/demo/App.tsx
  • packages/swap-widget/src/demo/DemoCustomizer.tsx
  • packages/swap-widget/src/demo/ExternalWalletApp.tsx
  • packages/swap-widget/src/demo/InternalWalletApp.tsx
  • packages/swap-widget/src/demo/main.tsx
  • packages/swap-widget/src/hooks/useBalances.ts
  • packages/swap-widget/src/hooks/useEvmSigning.ts
  • packages/swap-widget/src/hooks/useSwapApproval.ts
  • packages/swap-widget/src/hooks/useSwapDisplayValues.ts
  • packages/swap-widget/src/hooks/useSwapExecution.ts
  • packages/swap-widget/src/hooks/useSwapHandlers.ts
  • packages/swap-widget/src/hooks/useSwapQuoting.ts
  • packages/swap-widget/src/index.ts
  • packages/swap-widget/src/machines/__tests__/guards.test.ts
  • packages/swap-widget/src/machines/__tests__/swapMachine.test.ts
  • packages/swap-widget/src/machines/__tests__/types.test.ts
  • packages/swap-widget/src/machines/guards.ts
  • packages/swap-widget/src/machines/swapMachine.ts
  • packages/swap-widget/src/machines/types.ts
  • packages/swap-widget/src/types/index.ts
  • packages/swap-widget/src/utils/redirect.ts
💤 Files with no reviewable changes (3)
  • packages/swap-widget/src/demo/App.tsx
  • packages/swap-widget/src/config/wagmi.ts
  • packages/swap-widget/src/config/standaloneWagmi.ts

Comment thread packages/swap-widget/src/demo/DemoCustomizer.tsx
Comment thread packages/swap-widget/src/demo/main.tsx
@kaladinlight kaladinlight merged commit 38a0b55 into develop May 18, 2026
4 checks passed
@kaladinlight kaladinlight deleted the feat/swap-widget-external-wallet-demo branch May 18, 2026 17:22
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.

1 participant