@shapeshiftoss/swap-widget 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/Dockerfile +87 -0
  2. package/README.md +519 -0
  3. package/dist/assets/PhArrowCircleDown-B305t-Sh.js +14 -0
  4. package/dist/assets/PhArrowClockwise-CJPxQhr_.js +14 -0
  5. package/dist/assets/PhArrowDown-C4hT9BVu.js +14 -0
  6. package/dist/assets/PhArrowLeft-Way4Cu-r.js +14 -0
  7. package/dist/assets/PhArrowRight-Cp5ZME0F.js +14 -0
  8. package/dist/assets/PhArrowSquareOut-NnrK69c5.js +14 -0
  9. package/dist/assets/PhArrowUp-Dph6j8rm.js +14 -0
  10. package/dist/assets/PhArrowUpRight-BpJjRRRh.js +14 -0
  11. package/dist/assets/PhArrowsClockwise-CFUT8X7R.js +14 -0
  12. package/dist/assets/PhArrowsDownUp-BjBI4ckC.js +14 -0
  13. package/dist/assets/PhArrowsLeftRight-CuLpbKRk.js +14 -0
  14. package/dist/assets/PhBank-Dchr4XQJ.js +14 -0
  15. package/dist/assets/PhBrowser-dUu2TrC1.js +14 -0
  16. package/dist/assets/PhCaretDown-78IuKU3V.js +14 -0
  17. package/dist/assets/PhCaretLeft-_aiyH7n1.js +14 -0
  18. package/dist/assets/PhCaretRight-8zJQEuDX.js +14 -0
  19. package/dist/assets/PhCaretUp-CiCxKWwN.js +14 -0
  20. package/dist/assets/PhCheck-CcADx8br.js +14 -0
  21. package/dist/assets/PhCircleHalf-BYzSzd2h.js +14 -0
  22. package/dist/assets/PhClock-UItiXeJq.js +14 -0
  23. package/dist/assets/PhCompass-CUy3uA7t.js +14 -0
  24. package/dist/assets/PhCopy-C-PDgSlZ.js +14 -0
  25. package/dist/assets/PhCreditCard-CWRaUT6F.js +14 -0
  26. package/dist/assets/PhCurrencyDollar-Cqla3AXT.js +14 -0
  27. package/dist/assets/PhDesktop-BBIPrYLl.js +14 -0
  28. package/dist/assets/PhDeviceMobile-BdsPaATg.js +14 -0
  29. package/dist/assets/PhDotsThree-BMXCQl-0.js +14 -0
  30. package/dist/assets/PhEnvelope-yUsis6yU.js +14 -0
  31. package/dist/assets/PhFunnelSimple-Ba7BbWTp.js +14 -0
  32. package/dist/assets/PhGlobe-DgLxBk5U.js +14 -0
  33. package/dist/assets/PhIdentificationCard-Czjmq9Z3.js +14 -0
  34. package/dist/assets/PhImage-BAJ_erz6.js +14 -0
  35. package/dist/assets/PhInfo-Cr4DhVUE.js +14 -0
  36. package/dist/assets/PhLightbulb-BX_7duTr.js +14 -0
  37. package/dist/assets/PhMagnifyingGlass-Mygfryvz.js +14 -0
  38. package/dist/assets/PhPaperPlaneRight-BeuCA660.js +14 -0
  39. package/dist/assets/PhPlus-BMjcHjsT.js +14 -0
  40. package/dist/assets/PhPower-CGffGmdb.js +14 -0
  41. package/dist/assets/PhPuzzlePiece-wm_GpVSi.js +14 -0
  42. package/dist/assets/PhQrCode-DRwRzXCH.js +14 -0
  43. package/dist/assets/PhQuestion-M09j2U0T.js +14 -0
  44. package/dist/assets/PhQuestionMark-BP9EMB6h.js +14 -0
  45. package/dist/assets/PhSealCheck-BfG80yKn.js +14 -0
  46. package/dist/assets/PhSignOut-C5YOWlPT.js +14 -0
  47. package/dist/assets/PhSpinner-BXooDYzU.js +14 -0
  48. package/dist/assets/PhTrash-Biq6rmmL.js +14 -0
  49. package/dist/assets/PhUser-CAgZSpGe.js +14 -0
  50. package/dist/assets/PhVault-l2PHF38a.js +14 -0
  51. package/dist/assets/PhWallet-w25D16It.js +14 -0
  52. package/dist/assets/PhWarning-DPEydIPw.js +14 -0
  53. package/dist/assets/PhWarningCircle-BqjT745H.js +14 -0
  54. package/dist/assets/PhX-s-SmVExa.js +14 -0
  55. package/dist/assets/SwapController-If0bFrNc.js +1 -0
  56. package/dist/assets/ccip-BBB7uCdL.js +1 -0
  57. package/dist/assets/ccip-CkkdfKKI.js +1 -0
  58. package/dist/assets/data-capture-D4gAy-Yc.js +242 -0
  59. package/dist/assets/email-B1AJehis.js +90 -0
  60. package/dist/assets/embedded-wallet-Q6DQopDk.js +410 -0
  61. package/dist/assets/features-B6JAPAFb.js +5 -0
  62. package/dist/assets/index-5jd8RPN9.js +7428 -0
  63. package/dist/assets/index-B4kNtSTk.js +1080 -0
  64. package/dist/assets/index-B753eAob.js +1 -0
  65. package/dist/assets/index-BZoLTv77.js +3 -0
  66. package/dist/assets/index-Bcw9UURX.js +3 -0
  67. package/dist/assets/index-BkrV-ahK.js +2 -0
  68. package/dist/assets/index-CRQzQ_up.js +9 -0
  69. package/dist/assets/index-CSW0Gt5y.js +132 -0
  70. package/dist/assets/index-CZ4jTIsk.js +138 -0
  71. package/dist/assets/index-Cwa8SB3h.js +61 -0
  72. package/dist/assets/index-DEwQgT4r.js +78 -0
  73. package/dist/assets/index-DVfQiQ1d.js +1 -0
  74. package/dist/assets/index-cV3UdaMs.css +1 -0
  75. package/dist/assets/index-hAmu622O.js +43 -0
  76. package/dist/assets/onramp-XcPNCLRU.js +422 -0
  77. package/dist/assets/pay-with-exchange-BeHt4OF4.js +280 -0
  78. package/dist/assets/property-C9JG5tag.js +27 -0
  79. package/dist/assets/receive-7ff4he2S.js +131 -0
  80. package/dist/assets/send-CkMBIRXT.js +554 -0
  81. package/dist/assets/socials-DmmxuLrP.js +250 -0
  82. package/dist/assets/swaps-IHAYRQ0x.js +1050 -0
  83. package/dist/assets/transactions-9Cxdk7o9.js +16 -0
  84. package/dist/assets/v4-BOvFkHkt.js +1 -0
  85. package/dist/assets/w3m-modal-kJzFnBfB.js +891 -0
  86. package/dist/assets/wallet-standard-Dv8LMoUB.js +1 -0
  87. package/dist/favicon.png +0 -0
  88. package/dist/favicon.svg +11 -0
  89. package/dist/icon-192x192.png +0 -0
  90. package/dist/icon-512x512.png +0 -0
  91. package/dist/index.html +33 -0
  92. package/index.html +32 -0
  93. package/package.json +59 -0
  94. package/public/favicon.png +0 -0
  95. package/public/favicon.svg +11 -0
  96. package/public/icon-192x192.png +0 -0
  97. package/public/icon-512x512.png +0 -0
  98. package/railway.toml +10 -0
  99. package/src/api/client.ts +95 -0
  100. package/src/components/AddressInputModal.css +173 -0
  101. package/src/components/AddressInputModal.tsx +250 -0
  102. package/src/components/ApprovalStep.tsx +92 -0
  103. package/src/components/ExecutionStep.tsx +27 -0
  104. package/src/components/InputStep.tsx +431 -0
  105. package/src/components/QuoteSelector.css +146 -0
  106. package/src/components/QuoteSelector.tsx +137 -0
  107. package/src/components/QuotesModal.css +250 -0
  108. package/src/components/QuotesModal.tsx +182 -0
  109. package/src/components/SettingsModal.css +145 -0
  110. package/src/components/SettingsModal.tsx +180 -0
  111. package/src/components/StatusStep.tsx +139 -0
  112. package/src/components/SwapWidget.css +713 -0
  113. package/src/components/SwapWidget.tsx +607 -0
  114. package/src/components/TokenSelectModal.css +438 -0
  115. package/src/components/TokenSelectModal.tsx +487 -0
  116. package/src/components/WalletProvider.tsx +92 -0
  117. package/src/config/appkit.ts +104 -0
  118. package/src/config/wagmi.ts +27 -0
  119. package/src/constants/chains.ts +72 -0
  120. package/src/constants/defaults.ts +28 -0
  121. package/src/constants/swappers.ts +48 -0
  122. package/src/constants/viemChains.ts +69 -0
  123. package/src/contexts/SwapWalletContext.tsx +25 -0
  124. package/src/demo/App.css +458 -0
  125. package/src/demo/App.tsx +481 -0
  126. package/src/demo/main.tsx +25 -0
  127. package/src/hooks/useAssets.ts +180 -0
  128. package/src/hooks/useBalances.ts +822 -0
  129. package/src/hooks/useBitcoinSigning.ts +276 -0
  130. package/src/hooks/useMarketData.ts +169 -0
  131. package/src/hooks/useSolanaSigning.ts +275 -0
  132. package/src/hooks/useStatusPolling.ts +137 -0
  133. package/src/hooks/useSwapApproval.ts +106 -0
  134. package/src/hooks/useSwapDisplayValues.ts +157 -0
  135. package/src/hooks/useSwapExecution.ts +199 -0
  136. package/src/hooks/useSwapHandlers.ts +119 -0
  137. package/src/hooks/useSwapQuote.ts +71 -0
  138. package/src/hooks/useSwapQuoting.ts +102 -0
  139. package/src/hooks/useSwapRates.ts +61 -0
  140. package/src/index.ts +43 -0
  141. package/src/machines/SwapMachineContext.ts +5 -0
  142. package/src/machines/__tests__/guards.test.ts +228 -0
  143. package/src/machines/__tests__/setup.test.ts +7 -0
  144. package/src/machines/__tests__/swapMachine.test.ts +544 -0
  145. package/src/machines/__tests__/types.test.ts +124 -0
  146. package/src/machines/guards.ts +33 -0
  147. package/src/machines/swapMachine.ts +288 -0
  148. package/src/machines/types.ts +54 -0
  149. package/src/services/transactionStatus.ts +322 -0
  150. package/src/types/index.ts +361 -0
  151. package/src/utils/addressValidation.ts +201 -0
  152. package/src/utils/redirect.ts +45 -0
  153. package/src/vite-env.d.ts +10 -0
  154. package/tsconfig.json +23 -0
  155. package/tsconfig.node.json +10 -0
  156. package/vite.config.ts +101 -0
  157. package/vitest.config.ts +18 -0
@@ -0,0 +1,607 @@
1
+ import './SwapWidget.css'
2
+
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
5
+ import type { WalletClient } from 'viem'
6
+
7
+ import { createApiClient } from '../api/client'
8
+ import { DEFAULT_BUY_ASSET, DEFAULT_SELL_ASSET } from '../constants/defaults'
9
+ import type { SwapWalletContextValue } from '../contexts/SwapWalletContext'
10
+ import { SwapWalletProvider, useSwapWallet } from '../contexts/SwapWalletContext'
11
+ import { useBitcoinSigning } from '../hooks/useBitcoinSigning'
12
+ import { useSolanaSigning } from '../hooks/useSolanaSigning'
13
+ import { useStatusPolling } from '../hooks/useStatusPolling'
14
+ import { useSwapApproval } from '../hooks/useSwapApproval'
15
+ import { useSwapDisplayValues } from '../hooks/useSwapDisplayValues'
16
+ import { useSwapExecution } from '../hooks/useSwapExecution'
17
+ import { useSwapHandlers } from '../hooks/useSwapHandlers'
18
+ import { useSwapQuoting } from '../hooks/useSwapQuoting'
19
+ import { SwapMachineCtx } from '../machines/SwapMachineContext'
20
+ import type { Asset, SwapWidgetProps, ThemeMode } from '../types'
21
+ import { getChainType } from '../types'
22
+ import { AddressInputModal } from './AddressInputModal'
23
+ import { ApprovalStep } from './ApprovalStep'
24
+ import { ExecutionStep } from './ExecutionStep'
25
+ import { InputStep } from './InputStep'
26
+ import { SettingsModal } from './SettingsModal'
27
+ import { StatusStep } from './StatusStep'
28
+ import { TokenSelectModal } from './TokenSelectModal'
29
+ import { ConnectWalletButton, InternalWalletProvider } from './WalletProvider'
30
+
31
+ const queryClient = new QueryClient({
32
+ defaultOptions: {
33
+ queries: {
34
+ refetchOnWindowFocus: false,
35
+ staleTime: 30_000,
36
+ },
37
+ },
38
+ })
39
+
40
+ type SwapWidgetContentProps = {
41
+ apiClient: ReturnType<typeof createApiClient>
42
+ theme: SwapWidgetProps['theme']
43
+ showPoweredBy: boolean
44
+ defaultReceiveAddress?: string
45
+ enableWalletConnection: boolean
46
+ isBuyAssetLocked: boolean
47
+ onConnectWallet?: () => void
48
+ onSwapSuccess?: (txHash: string) => void
49
+ onSwapError?: (error: Error) => void
50
+ onAssetSelect?: (type: 'sell' | 'buy', asset: Asset) => void
51
+ sellDisabledAssetIds: string[]
52
+ buyDisabledAssetIds: string[]
53
+ sellDisabledChainIds: string[]
54
+ buyDisabledChainIds: string[]
55
+ sellAllowedChainIds?: string[]
56
+ buyAllowedChainIds?: string[]
57
+ sellAllowedAssetIds?: string[]
58
+ buyAllowedAssetIds?: string[]
59
+ }
60
+
61
+ const SwapWidgetContent = ({
62
+ apiClient,
63
+ theme = 'dark',
64
+ showPoweredBy,
65
+ defaultReceiveAddress,
66
+ enableWalletConnection,
67
+ isBuyAssetLocked,
68
+ onConnectWallet,
69
+ onSwapSuccess,
70
+ onSwapError,
71
+ onAssetSelect,
72
+ sellDisabledAssetIds,
73
+ buyDisabledAssetIds,
74
+ sellDisabledChainIds,
75
+ buyDisabledChainIds,
76
+ sellAllowedChainIds,
77
+ buyAllowedChainIds,
78
+ sellAllowedAssetIds,
79
+ buyAllowedAssetIds,
80
+ }: SwapWidgetContentProps) => {
81
+ const state = SwapMachineCtx.useSelector(s => s)
82
+ const actorRef = SwapMachineCtx.useActorRef()
83
+
84
+ const {
85
+ walletAddress,
86
+ effectiveReceiveAddress,
87
+ isCustomAddress,
88
+ customReceiveAddress,
89
+ setCustomReceiveAddress,
90
+ bitcoin,
91
+ solana,
92
+ } = useSwapWallet()
93
+
94
+ const [tokenModalType, setTokenModalType] = useState<'sell' | 'buy' | null>(null)
95
+ const [isSettingsOpen, setIsSettingsOpen] = useState(false)
96
+ const [isAddressModalOpen, setIsAddressModalOpen] = useState(false)
97
+
98
+ const themeMode: ThemeMode = typeof theme === 'string' ? theme : theme.mode
99
+ const themeConfig = typeof theme === 'object' ? theme : undefined
100
+
101
+ const buyChainType = getChainType(state.context.buyAsset.chainId)
102
+
103
+ const {
104
+ rates,
105
+ isLoadingRates,
106
+ ratesError,
107
+ sellAssetBalance,
108
+ isSellBalanceLoading,
109
+ refetchSellBalance,
110
+ buyAssetBalance,
111
+ isBuyBalanceLoading,
112
+ refetchBuyBalance,
113
+ sellChainInfo,
114
+ buyChainInfo,
115
+ displayRate,
116
+ networkFeeDisplay,
117
+ sellUsdValue,
118
+ buyUsdValue,
119
+ buyAssetUsdPrice,
120
+ sellBalanceFiatValue,
121
+ buyBalanceFiatValue,
122
+ } = useSwapDisplayValues({ apiClient })
123
+
124
+ const {
125
+ handleSwapTokens,
126
+ handleSellAssetSelect,
127
+ handleBuyAssetSelect,
128
+ handleSellAmountChange,
129
+ handleSelectRate,
130
+ handleSlippageChange,
131
+ handleButtonClick,
132
+ } = useSwapHandlers({ onConnectWallet, onAssetSelect })
133
+
134
+ useSwapQuoting({ apiClient, rates, sellAssetBalance })
135
+
136
+ useSwapApproval()
137
+
138
+ useSwapExecution()
139
+
140
+ useStatusPolling({ onSwapSuccess, onSwapError, refetchSellBalance, refetchBuyBalance })
141
+
142
+ const widgetStyle = useMemo(() => {
143
+ if (!themeConfig) return undefined
144
+ const style: Record<string, string> = {}
145
+ if (themeConfig.accentColor) {
146
+ style['--ssw-accent'] = themeConfig.accentColor
147
+ style['--ssw-accent-light'] = `${themeConfig.accentColor}1a`
148
+ }
149
+ if (themeConfig.backgroundColor) {
150
+ style['--ssw-bg-secondary'] = themeConfig.backgroundColor
151
+ style['--ssw-bg-primary'] = themeConfig.backgroundColor
152
+ }
153
+ if (themeConfig.cardColor) {
154
+ style['--ssw-bg-tertiary'] = themeConfig.cardColor
155
+ }
156
+ if (themeConfig.textColor) {
157
+ style['--ssw-text-primary'] = themeConfig.textColor
158
+ }
159
+ if (themeConfig.borderRadius) {
160
+ const base = parseFloat(themeConfig.borderRadius) || 0
161
+ style['--ssw-radius-widget'] = `${base * 1.25}px`
162
+ style['--ssw-radius-lg'] = `${base}px`
163
+ style['--ssw-radius-md'] = `${Math.round(base * 0.875)}px`
164
+ style['--ssw-radius-sm'] = `${Math.round(base * 0.625)}px`
165
+ style['--ssw-radius-xs'] = `${Math.round(base * 0.375)}px`
166
+ style['--ssw-radius-2xs'] = `${Math.round(base * 0.25)}px`
167
+ }
168
+ if (themeConfig.fontFamily) {
169
+ style['--ssw-font-family'] = themeConfig.fontFamily
170
+ }
171
+ if (themeConfig.borderColor) {
172
+ style['--ssw-border'] = themeConfig.borderColor
173
+ style['--ssw-border-hover'] = themeConfig.borderColor
174
+ }
175
+ if (themeConfig.secondaryTextColor) {
176
+ style['--ssw-text-secondary'] = themeConfig.secondaryTextColor
177
+ }
178
+ if (themeConfig.mutedTextColor) {
179
+ style['--ssw-text-muted'] = themeConfig.mutedTextColor
180
+ }
181
+ if (themeConfig.inputColor) {
182
+ style['--ssw-bg-input'] = themeConfig.inputColor
183
+ }
184
+ if (themeConfig.hoverColor) {
185
+ style['--ssw-bg-hover'] = themeConfig.hoverColor
186
+ }
187
+ return Object.keys(style).length > 0 ? (style as React.CSSProperties) : undefined
188
+ }, [themeConfig])
189
+
190
+ const openAddressModal = useCallback(() => setIsAddressModalOpen(true), [])
191
+
192
+ return (
193
+ <div
194
+ className={`ssw-widget ${themeMode === 'light' ? 'ssw-light' : 'ssw-dark'}${
195
+ themeConfig?.buttonVariant === 'outline' ? ' ssw-btn-outline' : ''
196
+ }`}
197
+ style={widgetStyle}
198
+ >
199
+ <div className='ssw-header'>
200
+ <span className='ssw-header-title'>Swap</span>
201
+ <div className='ssw-header-actions'>
202
+ {enableWalletConnection && <ConnectWalletButton />}
203
+ <button
204
+ className='ssw-settings-btn'
205
+ onClick={() => setIsSettingsOpen(true)}
206
+ type='button'
207
+ title='Settings'
208
+ >
209
+ <svg
210
+ width='20'
211
+ height='20'
212
+ viewBox='0 0 24 24'
213
+ fill='none'
214
+ stroke='currentColor'
215
+ strokeWidth='2'
216
+ >
217
+ <circle cx='12' cy='12' r='3' />
218
+ <path d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z' />
219
+ </svg>
220
+ </button>
221
+ </div>
222
+ </div>
223
+
224
+ <div className='ssw-step-container'>
225
+ {(state.matches('idle') || state.matches('input') || state.matches('quoting')) && (
226
+ <InputStep
227
+ context={state.context}
228
+ send={actorRef.send}
229
+ rates={rates ?? []}
230
+ isLoadingRates={isLoadingRates}
231
+ ratesError={ratesError}
232
+ sellAssetBalance={sellAssetBalance}
233
+ buyAssetBalance={buyAssetBalance}
234
+ isSellBalanceLoading={isSellBalanceLoading}
235
+ isBuyBalanceLoading={isBuyBalanceLoading}
236
+ sellUsdValue={sellUsdValue}
237
+ buyUsdValue={buyUsdValue}
238
+ sellChainInfo={sellChainInfo}
239
+ buyChainInfo={buyChainInfo}
240
+ displayRate={displayRate}
241
+ walletAddress={walletAddress}
242
+ bitcoinAddress={bitcoin.address}
243
+ solanaAddress={solana.address}
244
+ defaultReceiveAddress={defaultReceiveAddress}
245
+ buyAssetUsdPrice={buyAssetUsdPrice}
246
+ onOpenTokenModal={setTokenModalType}
247
+ onOpenAddressModal={openAddressModal}
248
+ enableWalletConnection={enableWalletConnection}
249
+ onConnectWallet={onConnectWallet}
250
+ bitcoinState={bitcoin.state}
251
+ solanaState={solana.state}
252
+ isQuoting={state.matches('quoting')}
253
+ isExecuting={false}
254
+ effectiveReceiveAddress={effectiveReceiveAddress}
255
+ isCustomAddress={isCustomAddress}
256
+ sellAmount={state.context.sellAmount}
257
+ onSellAmountChange={handleSellAmountChange}
258
+ onSwapTokens={handleSwapTokens}
259
+ onSelectRate={handleSelectRate}
260
+ onButtonClick={handleButtonClick}
261
+ sellAmountBaseUnit={state.context.sellAmountBaseUnit}
262
+ networkFeeDisplay={networkFeeDisplay}
263
+ sellBalanceFiatValue={sellBalanceFiatValue}
264
+ buyBalanceFiatValue={buyBalanceFiatValue}
265
+ isBuyAssetLocked={isBuyAssetLocked}
266
+ />
267
+ )}
268
+
269
+ {(state.matches('approval_needed') || state.matches('approving')) && (
270
+ <ApprovalStep
271
+ context={state.context}
272
+ send={actorRef.send}
273
+ isApproving={state.matches('approving')}
274
+ />
275
+ )}
276
+
277
+ {state.matches('executing') && (
278
+ <ExecutionStep context={state.context} send={actorRef.send} />
279
+ )}
280
+
281
+ {(state.matches('polling_status') ||
282
+ state.matches('complete') ||
283
+ state.matches('error')) && (
284
+ <StatusStep
285
+ context={state.context}
286
+ send={actorRef.send}
287
+ isPolling={state.matches('polling_status')}
288
+ isComplete={state.matches('complete')}
289
+ isError={state.matches('error')}
290
+ />
291
+ )}
292
+ </div>
293
+
294
+ {showPoweredBy && (
295
+ <div className='ssw-powered-by'>
296
+ Powered by{' '}
297
+ <a
298
+ href='https://shapeshift.com'
299
+ target='_blank'
300
+ rel='noopener noreferrer'
301
+ className='ssw-powered-by-link'
302
+ >
303
+ <svg width='16' height='16' viewBox='0 0 57 62' fill='currentColor'>
304
+ <path d='M51.67 5.1L48.97 21.3L39.37 10L51.67 5.1ZM49.03 28.27L51.43 37.14L33.06 42.2L49.03 28.27ZM9.03 23.8L18.88 10.93H35.99L46.92 23.8H9.03ZM45.66 26.99L27.85 42.53L9.7 26.99H45.66ZM15.58 10.01L6.78 21.51L4.08 5.17L15.58 10.01ZM22.57 42.2L4.02 37.15L6.56 28.48L22.57 42.2ZM25.99 46.43L22.49 50.28C19.53 47.46 16.26 44.96 12.78 42.83L25.99 46.43ZM42.98 42.77C39.5 44.94 36.24 47.47 33.29 50.32L29.72 46.42L42.98 42.77ZM55.73 0.06L36.42 7.75H18.42L0 0L4.18 25.3L0.17 38.99L10.65 45.26C15.61 48.23 20.06 51.94 23.86 56.3L27.94 60.97L32.23 56.06C35.9 51.84 40.18 48.22 44.95 45.29L55.23 38.99L51.52 25.31L55.73 0.06Z' />
305
+ </svg>
306
+ ShapeShift
307
+ </a>
308
+ </div>
309
+ )}
310
+
311
+ <TokenSelectModal
312
+ isOpen={tokenModalType !== null}
313
+ onClose={() => setTokenModalType(null)}
314
+ onSelect={tokenModalType === 'sell' ? handleSellAssetSelect : handleBuyAssetSelect}
315
+ disabledAssetIds={tokenModalType === 'buy' ? buyDisabledAssetIds : sellDisabledAssetIds}
316
+ disabledChainIds={tokenModalType === 'buy' ? buyDisabledChainIds : sellDisabledChainIds}
317
+ allowedChainIds={tokenModalType === 'buy' ? buyAllowedChainIds : sellAllowedChainIds}
318
+ allowedAssetIds={tokenModalType === 'buy' ? buyAllowedAssetIds : sellAllowedAssetIds}
319
+ walletAddress={walletAddress}
320
+ currentAssetIds={[state.context.sellAsset.assetId, state.context.buyAsset.assetId]}
321
+ />
322
+
323
+ <SettingsModal
324
+ isOpen={isSettingsOpen}
325
+ onClose={() => setIsSettingsOpen(false)}
326
+ slippage={state.context.slippage}
327
+ onSlippageChange={handleSlippageChange}
328
+ />
329
+
330
+ <AddressInputModal
331
+ isOpen={isAddressModalOpen}
332
+ onClose={() => setIsAddressModalOpen(false)}
333
+ chainId={state.context.buyAsset.chainId}
334
+ chainName={
335
+ buyChainInfo?.name ?? state.context.buyAsset.networkName ?? state.context.buyAsset.name
336
+ }
337
+ currentAddress={customReceiveAddress || effectiveReceiveAddress || ''}
338
+ onAddressChange={setCustomReceiveAddress}
339
+ walletAddress={
340
+ buyChainType === 'evm'
341
+ ? walletAddress
342
+ : buyChainType === 'utxo'
343
+ ? bitcoin.address
344
+ : buyChainType === 'solana'
345
+ ? solana.address
346
+ : undefined
347
+ }
348
+ />
349
+ </div>
350
+ )
351
+ }
352
+
353
+ type SwapWidgetCoreProps = {
354
+ walletClient: unknown
355
+ defaultSellAsset: Asset
356
+ defaultBuyAsset: Asset
357
+ defaultSlippage: string
358
+ defaultReceiveAddress?: string
359
+ apiClient: ReturnType<typeof createApiClient>
360
+ theme: SwapWidgetProps['theme']
361
+ showPoweredBy: boolean
362
+ enableWalletConnection: boolean
363
+ isBuyAssetLocked: boolean
364
+ onConnectWallet?: () => void
365
+ onSwapSuccess?: (txHash: string) => void
366
+ onSwapError?: (error: Error) => void
367
+ onAssetSelect?: (type: 'sell' | 'buy', asset: Asset) => void
368
+ sellDisabledAssetIds: string[]
369
+ buyDisabledAssetIds: string[]
370
+ sellDisabledChainIds: string[]
371
+ buyDisabledChainIds: string[]
372
+ sellAllowedChainIds?: string[]
373
+ buyAllowedChainIds?: string[]
374
+ sellAllowedAssetIds?: string[]
375
+ buyAllowedAssetIds?: string[]
376
+ }
377
+
378
+ const SwapWidgetCore = ({
379
+ walletClient,
380
+ defaultSellAsset,
381
+ defaultBuyAsset,
382
+ defaultSlippage,
383
+ defaultReceiveAddress,
384
+ apiClient,
385
+ theme,
386
+ showPoweredBy,
387
+ enableWalletConnection,
388
+ isBuyAssetLocked,
389
+ onConnectWallet,
390
+ onSwapSuccess,
391
+ onSwapError,
392
+ onAssetSelect,
393
+ sellDisabledAssetIds,
394
+ buyDisabledAssetIds,
395
+ sellDisabledChainIds,
396
+ buyDisabledChainIds,
397
+ sellAllowedChainIds,
398
+ buyAllowedChainIds,
399
+ sellAllowedAssetIds,
400
+ buyAllowedAssetIds,
401
+ }: SwapWidgetCoreProps) => {
402
+ const actorRef = SwapMachineCtx.useActorRef()
403
+
404
+ const bitcoin = useBitcoinSigning()
405
+ const solana = useSolanaSigning()
406
+
407
+ const [customReceiveAddress, setCustomReceiveAddress] = useState<string>('')
408
+
409
+ const walletAddress = useMemo(() => {
410
+ if (!walletClient) return undefined
411
+ return (walletClient as WalletClient).account?.address
412
+ }, [walletClient])
413
+
414
+ const buyChainId = SwapMachineCtx.useSelector(s => s.context.buyAsset.chainId)
415
+ const buyChainType = getChainType(buyChainId)
416
+
417
+ const effectiveReceiveAddress = useMemo(() => {
418
+ if (customReceiveAddress) return customReceiveAddress
419
+ if (defaultReceiveAddress) return defaultReceiveAddress
420
+
421
+ if (buyChainType === 'utxo') return bitcoin.address ?? ''
422
+ if (buyChainType === 'solana') return solana.address ?? ''
423
+ if (buyChainType === 'evm') return walletAddress ?? ''
424
+
425
+ return ''
426
+ }, [
427
+ customReceiveAddress,
428
+ defaultReceiveAddress,
429
+ buyChainType,
430
+ bitcoin.address,
431
+ solana.address,
432
+ walletAddress,
433
+ ])
434
+
435
+ const isCustomAddress = useMemo(
436
+ () => !!customReceiveAddress && customReceiveAddress !== walletAddress,
437
+ [customReceiveAddress, walletAddress],
438
+ )
439
+
440
+ const initialSyncRef = useRef(false)
441
+ useEffect(() => {
442
+ if (initialSyncRef.current) return
443
+ initialSyncRef.current = true
444
+ actorRef.send({ type: 'SET_SELL_ASSET', asset: defaultSellAsset })
445
+ actorRef.send({ type: 'SET_BUY_ASSET', asset: defaultBuyAsset })
446
+ actorRef.send({ type: 'SET_SLIPPAGE', slippage: defaultSlippage })
447
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- defaults are initial-only, ref guard ensures single execution
448
+ }, [actorRef])
449
+
450
+ useEffect(() => {
451
+ actorRef.send({ type: 'SET_WALLET_ADDRESS', address: walletAddress })
452
+ }, [walletAddress, actorRef])
453
+
454
+ useEffect(() => {
455
+ actorRef.send({ type: 'SET_RECEIVE_ADDRESS', address: effectiveReceiveAddress })
456
+ }, [effectiveReceiveAddress, actorRef])
457
+
458
+ const walletValue: SwapWalletContextValue = useMemo(
459
+ () => ({
460
+ walletClient,
461
+ walletAddress,
462
+ effectiveReceiveAddress,
463
+ isCustomAddress,
464
+ customReceiveAddress,
465
+ setCustomReceiveAddress,
466
+ bitcoin,
467
+ solana,
468
+ }),
469
+ [
470
+ walletClient,
471
+ walletAddress,
472
+ effectiveReceiveAddress,
473
+ isCustomAddress,
474
+ customReceiveAddress,
475
+ bitcoin,
476
+ solana,
477
+ ],
478
+ )
479
+
480
+ return (
481
+ <SwapWalletProvider value={walletValue}>
482
+ <SwapWidgetContent
483
+ apiClient={apiClient}
484
+ theme={theme}
485
+ showPoweredBy={showPoweredBy}
486
+ defaultReceiveAddress={defaultReceiveAddress}
487
+ enableWalletConnection={enableWalletConnection}
488
+ isBuyAssetLocked={isBuyAssetLocked}
489
+ onConnectWallet={onConnectWallet}
490
+ onSwapSuccess={onSwapSuccess}
491
+ onSwapError={onSwapError}
492
+ onAssetSelect={onAssetSelect}
493
+ sellDisabledAssetIds={sellDisabledAssetIds}
494
+ buyDisabledAssetIds={buyDisabledAssetIds}
495
+ sellDisabledChainIds={sellDisabledChainIds}
496
+ buyDisabledChainIds={buyDisabledChainIds}
497
+ sellAllowedChainIds={sellAllowedChainIds}
498
+ buyAllowedChainIds={buyAllowedChainIds}
499
+ sellAllowedAssetIds={sellAllowedAssetIds}
500
+ buyAllowedAssetIds={buyAllowedAssetIds}
501
+ />
502
+ </SwapWalletProvider>
503
+ )
504
+ }
505
+
506
+ const SwapWidgetWithExternalWallet = (props: SwapWidgetProps) => {
507
+ const apiClient = useMemo(
508
+ () =>
509
+ createApiClient({
510
+ baseUrl: props.apiBaseUrl,
511
+ affiliateAddress: props.affiliateAddress,
512
+ }),
513
+ [props.apiBaseUrl, props.affiliateAddress],
514
+ )
515
+
516
+ return (
517
+ <QueryClientProvider client={queryClient}>
518
+ <SwapMachineCtx.Provider>
519
+ <SwapWidgetCore
520
+ walletClient={props.walletClient}
521
+ defaultSellAsset={props.defaultSellAsset ?? DEFAULT_SELL_ASSET}
522
+ defaultBuyAsset={props.defaultBuyAsset ?? DEFAULT_BUY_ASSET}
523
+ defaultSlippage={props.defaultSlippage ?? '0.5'}
524
+ defaultReceiveAddress={props.defaultReceiveAddress}
525
+ apiClient={apiClient}
526
+ theme={props.theme}
527
+ showPoweredBy={props.showPoweredBy ?? true}
528
+ enableWalletConnection={false}
529
+ isBuyAssetLocked={props.isBuyAssetLocked ?? false}
530
+ onConnectWallet={props.onConnectWallet}
531
+ onSwapSuccess={props.onSwapSuccess}
532
+ onSwapError={props.onSwapError}
533
+ onAssetSelect={props.onAssetSelect}
534
+ sellDisabledAssetIds={props.sellDisabledAssetIds ?? props.disabledAssetIds ?? []}
535
+ buyDisabledAssetIds={props.buyDisabledAssetIds ?? props.disabledAssetIds ?? []}
536
+ sellDisabledChainIds={props.sellDisabledChainIds ?? props.disabledChainIds ?? []}
537
+ buyDisabledChainIds={props.buyDisabledChainIds ?? props.disabledChainIds ?? []}
538
+ sellAllowedChainIds={props.sellAllowedChainIds ?? props.allowedChainIds}
539
+ buyAllowedChainIds={props.buyAllowedChainIds ?? props.allowedChainIds}
540
+ sellAllowedAssetIds={props.sellAllowedAssetIds}
541
+ buyAllowedAssetIds={props.buyAllowedAssetIds}
542
+ />
543
+ </SwapMachineCtx.Provider>
544
+ </QueryClientProvider>
545
+ )
546
+ }
547
+
548
+ const SwapWidgetWithInternalWallet = (
549
+ props: SwapWidgetProps & { walletConnectProjectId: string },
550
+ ) => {
551
+ const apiClient = useMemo(
552
+ () =>
553
+ createApiClient({
554
+ baseUrl: props.apiBaseUrl,
555
+ affiliateAddress: props.affiliateAddress,
556
+ }),
557
+ [props.apiBaseUrl, props.affiliateAddress],
558
+ )
559
+
560
+ return (
561
+ <InternalWalletProvider projectId={props.walletConnectProjectId}>
562
+ {walletClient => (
563
+ <QueryClientProvider client={queryClient}>
564
+ <SwapMachineCtx.Provider>
565
+ <SwapWidgetCore
566
+ walletClient={walletClient}
567
+ defaultSellAsset={props.defaultSellAsset ?? DEFAULT_SELL_ASSET}
568
+ defaultBuyAsset={props.defaultBuyAsset ?? DEFAULT_BUY_ASSET}
569
+ defaultSlippage={props.defaultSlippage ?? '0.5'}
570
+ defaultReceiveAddress={props.defaultReceiveAddress}
571
+ apiClient={apiClient}
572
+ theme={props.theme}
573
+ showPoweredBy={props.showPoweredBy ?? true}
574
+ enableWalletConnection={true}
575
+ onConnectWallet={props.onConnectWallet}
576
+ onSwapSuccess={props.onSwapSuccess}
577
+ onSwapError={props.onSwapError}
578
+ onAssetSelect={props.onAssetSelect}
579
+ sellDisabledAssetIds={props.sellDisabledAssetIds ?? props.disabledAssetIds ?? []}
580
+ buyDisabledAssetIds={props.buyDisabledAssetIds ?? props.disabledAssetIds ?? []}
581
+ sellDisabledChainIds={props.sellDisabledChainIds ?? props.disabledChainIds ?? []}
582
+ buyDisabledChainIds={props.buyDisabledChainIds ?? props.disabledChainIds ?? []}
583
+ sellAllowedChainIds={props.sellAllowedChainIds ?? props.allowedChainIds}
584
+ buyAllowedChainIds={props.buyAllowedChainIds ?? props.allowedChainIds}
585
+ sellAllowedAssetIds={props.sellAllowedAssetIds}
586
+ buyAllowedAssetIds={props.buyAllowedAssetIds}
587
+ isBuyAssetLocked={props.isBuyAssetLocked ?? false}
588
+ />
589
+ </SwapMachineCtx.Provider>
590
+ </QueryClientProvider>
591
+ )}
592
+ </InternalWalletProvider>
593
+ )
594
+ }
595
+
596
+ export const SwapWidget = (props: SwapWidgetProps) => {
597
+ if (props.enableWalletConnection && props.walletConnectProjectId) {
598
+ return (
599
+ <SwapWidgetWithInternalWallet
600
+ {...props}
601
+ walletConnectProjectId={props.walletConnectProjectId}
602
+ />
603
+ )
604
+ }
605
+
606
+ return <SwapWidgetWithExternalWallet {...props} />
607
+ }