@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,431 @@
1
+ import { useMemo } from 'react'
2
+
3
+ import type { SwapMachineContext, SwapMachineEvent } from '../machines/types'
4
+ import type { TradeRate } from '../types'
5
+ import { formatAmount, truncateAddress } from '../types'
6
+ import { QuoteSelector } from './QuoteSelector'
7
+
8
+ type InputStepProps = {
9
+ context: SwapMachineContext
10
+ send: (event: SwapMachineEvent) => void
11
+ rates: TradeRate[]
12
+ isLoadingRates: boolean
13
+ ratesError: Error | null
14
+ sellAssetBalance: { balance: string; balanceFormatted: string } | undefined
15
+ buyAssetBalance: { balance: string; balanceFormatted: string } | undefined
16
+ isSellBalanceLoading: boolean
17
+ isBuyBalanceLoading: boolean
18
+ sellUsdValue: string
19
+ buyUsdValue: string
20
+ sellChainInfo: { name: string } | undefined
21
+ buyChainInfo: { name: string } | undefined
22
+ displayRate: TradeRate | undefined
23
+ walletAddress: string | undefined
24
+ bitcoinAddress: string | undefined
25
+ solanaAddress: string | undefined
26
+ defaultReceiveAddress: string | undefined
27
+ buyAssetUsdPrice: string | undefined
28
+ onOpenTokenModal: (type: 'sell' | 'buy') => void
29
+ onOpenAddressModal: () => void
30
+ enableWalletConnection: boolean
31
+ onConnectWallet?: () => void
32
+ bitcoinState: { isLoading: boolean }
33
+ solanaState: { isLoading: boolean }
34
+ isQuoting: boolean
35
+ isExecuting: boolean
36
+ effectiveReceiveAddress: string
37
+ isCustomAddress: boolean
38
+ sellAmount: string
39
+ onSellAmountChange: (value: string) => void
40
+ onSwapTokens: () => void
41
+ onSelectRate: (rate: TradeRate) => void
42
+ onButtonClick: () => void
43
+ sellAmountBaseUnit: string | undefined
44
+ networkFeeDisplay: string | undefined
45
+ sellBalanceFiatValue: string | undefined
46
+ buyBalanceFiatValue: string | undefined
47
+ isBuyAssetLocked: boolean
48
+ }
49
+
50
+ export const InputStep = ({
51
+ context,
52
+ send: _send,
53
+ rates,
54
+ isLoadingRates,
55
+ ratesError,
56
+ sellAssetBalance,
57
+ buyAssetBalance,
58
+ isSellBalanceLoading,
59
+ isBuyBalanceLoading,
60
+ sellUsdValue,
61
+ buyUsdValue,
62
+ sellChainInfo,
63
+ buyChainInfo,
64
+ displayRate,
65
+ walletAddress,
66
+ bitcoinAddress,
67
+ solanaAddress,
68
+ defaultReceiveAddress,
69
+ buyAssetUsdPrice,
70
+ onOpenTokenModal,
71
+ onOpenAddressModal,
72
+ enableWalletConnection: _enableWalletConnection,
73
+ onConnectWallet: _onConnectWallet,
74
+ bitcoinState,
75
+ solanaState,
76
+ isQuoting,
77
+ isExecuting,
78
+ effectiveReceiveAddress,
79
+ isCustomAddress,
80
+ sellAmount,
81
+ onSellAmountChange,
82
+ onSwapTokens,
83
+ onSelectRate,
84
+ onButtonClick,
85
+ sellAmountBaseUnit,
86
+ networkFeeDisplay,
87
+ sellBalanceFiatValue,
88
+ buyBalanceFiatValue,
89
+ isBuyAssetLocked,
90
+ }: InputStepProps) => {
91
+ const { sellAsset, buyAsset, selectedRate, isSellAssetEvm, isSellAssetUtxo, isSellAssetSolana } =
92
+ context
93
+
94
+ const canExecuteDirectly = isSellAssetEvm
95
+ const canExecuteUtxo = isSellAssetUtxo
96
+ const canExecuteSolana = isSellAssetSolana
97
+ const isUnsupportedChain = !isSellAssetEvm && !isSellAssetUtxo && !isSellAssetSolana
98
+
99
+ const isBitcoinConnected = !!bitcoinAddress
100
+ const isSolanaConnected = !!solanaAddress
101
+
102
+ const buyAmount = displayRate?.buyAmountCryptoBaseUnit
103
+
104
+ const buttonText = useMemo(() => {
105
+ if (isSellAssetUtxo && canExecuteUtxo) {
106
+ if (!sellAmount) return 'Enter an amount'
107
+ if (!isBitcoinConnected) return 'Connect Bitcoin Wallet'
108
+ if (!effectiveReceiveAddress) return 'Enter receive address'
109
+ if (bitcoinState.isLoading || isExecuting) return 'Executing...'
110
+ if (isLoadingRates) return 'Finding rates...'
111
+ if (ratesError) return 'No routes available'
112
+ if (!rates?.length) return 'No routes found'
113
+ return 'Swap'
114
+ }
115
+ if (isSellAssetSolana && canExecuteSolana) {
116
+ if (!sellAmount) return 'Enter an amount'
117
+ if (!isSolanaConnected) return 'Connect Solana Wallet'
118
+ if (!effectiveReceiveAddress) return 'Enter receive address'
119
+ if (solanaState.isLoading || isExecuting) return 'Executing...'
120
+ if (isLoadingRates) return 'Finding rates...'
121
+ if (ratesError) return 'No routes available'
122
+ if (!rates?.length) return 'No routes found'
123
+ return 'Swap'
124
+ }
125
+ if (!isSellAssetEvm) return 'Proceed on ShapeShift'
126
+ if (!sellAmount) return 'Enter an amount'
127
+ if (!walletAddress && canExecuteDirectly) return 'Connect Wallet'
128
+ if (!effectiveReceiveAddress) return 'Enter receive address'
129
+ if (isLoadingRates) return 'Finding rates...'
130
+ if (ratesError) return 'No routes available'
131
+ if (!rates?.length) return 'No routes found'
132
+ if (isExecuting) return 'Executing...'
133
+ return 'Swap'
134
+ }, [
135
+ walletAddress,
136
+ canExecuteDirectly,
137
+ canExecuteUtxo,
138
+ canExecuteSolana,
139
+ isSellAssetEvm,
140
+ isSellAssetUtxo,
141
+ isSellAssetSolana,
142
+ isBitcoinConnected,
143
+ isSolanaConnected,
144
+ bitcoinState.isLoading,
145
+ solanaState.isLoading,
146
+ sellAmount,
147
+ isLoadingRates,
148
+ ratesError,
149
+ rates,
150
+ isExecuting,
151
+ effectiveReceiveAddress,
152
+ ])
153
+
154
+ const isButtonDisabled = useMemo(() => {
155
+ if (isUnsupportedChain) return false
156
+
157
+ if (!sellAmount || isLoadingRates || ratesError || !rates?.length || isExecuting) {
158
+ return true
159
+ }
160
+
161
+ if (!effectiveReceiveAddress) {
162
+ return true
163
+ }
164
+
165
+ if (isSellAssetUtxo && canExecuteUtxo) {
166
+ return !isBitcoinConnected || bitcoinState.isLoading
167
+ }
168
+
169
+ if (isSellAssetSolana && canExecuteSolana) {
170
+ return !isSolanaConnected || solanaState.isLoading
171
+ }
172
+
173
+ return false
174
+ }, [
175
+ isUnsupportedChain,
176
+ isSellAssetUtxo,
177
+ isSellAssetSolana,
178
+ canExecuteUtxo,
179
+ canExecuteSolana,
180
+ isBitcoinConnected,
181
+ isSolanaConnected,
182
+ bitcoinState.isLoading,
183
+ solanaState.isLoading,
184
+ sellAmount,
185
+ isLoadingRates,
186
+ ratesError,
187
+ rates,
188
+ isExecuting,
189
+ effectiveReceiveAddress,
190
+ ])
191
+
192
+ return (
193
+ <>
194
+ <div className='ssw-swap-container'>
195
+ <div className='ssw-token-section ssw-sell'>
196
+ <div className='ssw-section-header'>
197
+ <span className='ssw-section-label'>Sell</span>
198
+ {walletAddress && isSellAssetEvm && (
199
+ <span className='ssw-wallet-badge'>{truncateAddress(walletAddress)}</span>
200
+ )}
201
+ {bitcoinAddress && isSellAssetUtxo && (
202
+ <span className='ssw-wallet-badge'>{truncateAddress(bitcoinAddress)}</span>
203
+ )}
204
+ {solanaAddress && isSellAssetSolana && (
205
+ <span className='ssw-wallet-badge'>{truncateAddress(solanaAddress)}</span>
206
+ )}
207
+ </div>
208
+
209
+ <div className='ssw-input-row'>
210
+ <input
211
+ type='text'
212
+ className='ssw-amount-input'
213
+ placeholder='0'
214
+ value={sellAmount}
215
+ onChange={e => {
216
+ const raw = e.target.value.replace(/[^0-9.]/g, '')
217
+ const parts = raw.split('.')
218
+ const sanitized = parts.length > 2 ? parts[0] + '.' + parts.slice(1).join('') : raw
219
+ onSellAmountChange(sanitized)
220
+ }}
221
+ />
222
+ <button
223
+ className='ssw-token-btn'
224
+ onClick={() => onOpenTokenModal('sell')}
225
+ type='button'
226
+ >
227
+ {sellAsset.icon ? (
228
+ <img src={sellAsset.icon} alt={sellAsset.symbol} className='ssw-token-icon' />
229
+ ) : (
230
+ <div className='ssw-token-icon-placeholder'>{sellAsset.symbol.charAt(0)}</div>
231
+ )}
232
+ <div className='ssw-token-info'>
233
+ <span className='ssw-token-symbol'>{sellAsset.symbol}</span>
234
+ <span className='ssw-token-chain'>
235
+ {sellChainInfo?.name ?? sellAsset.networkName ?? sellAsset.name}
236
+ </span>
237
+ </div>
238
+ <svg
239
+ width='16'
240
+ height='16'
241
+ viewBox='0 0 24 24'
242
+ fill='none'
243
+ stroke='currentColor'
244
+ strokeWidth='2'
245
+ >
246
+ <path d='M9 18l6-6-6-6' />
247
+ </svg>
248
+ </button>
249
+ </div>
250
+
251
+ <div className='ssw-section-footer'>
252
+ <span className='ssw-usd-value'>{sellUsdValue}</span>
253
+ {(walletAddress || bitcoinAddress || solanaAddress) &&
254
+ (isSellBalanceLoading ? (
255
+ <span className='ssw-balance-skeleton' />
256
+ ) : sellAssetBalance ? (
257
+ <span className='ssw-balance'>
258
+ Balance: {sellAssetBalance.balanceFormatted}
259
+ {sellBalanceFiatValue && (
260
+ <span className='ssw-balance-fiat'> ({sellBalanceFiatValue})</span>
261
+ )}
262
+ </span>
263
+ ) : null)}
264
+ </div>
265
+ </div>
266
+
267
+ <div className='ssw-swap-divider'>
268
+ <button
269
+ className='ssw-swap-btn'
270
+ onClick={isBuyAssetLocked ? undefined : onSwapTokens}
271
+ disabled={isBuyAssetLocked}
272
+ type='button'
273
+ >
274
+ <svg
275
+ width='16'
276
+ height='16'
277
+ viewBox='0 0 24 24'
278
+ fill='none'
279
+ stroke='currentColor'
280
+ strokeWidth='2'
281
+ >
282
+ <path d='M12 5v14M5 12l7 7 7-7' />
283
+ </svg>
284
+ </button>
285
+ </div>
286
+
287
+ <div className='ssw-token-section ssw-buy'>
288
+ <div className='ssw-section-header'>
289
+ <span className='ssw-section-label'>Buy</span>
290
+ {defaultReceiveAddress ? (
291
+ <span className='ssw-receive-address-btn ssw-receive-address-readonly'>
292
+ <span className='ssw-receive-address-text'>
293
+ {truncateAddress(defaultReceiveAddress, 4)}
294
+ </span>
295
+ </span>
296
+ ) : (
297
+ <button
298
+ className={`ssw-receive-address-btn ${isCustomAddress ? 'ssw-custom' : ''}`}
299
+ onClick={() => onOpenAddressModal()}
300
+ type='button'
301
+ >
302
+ <span className='ssw-receive-address-text'>
303
+ {effectiveReceiveAddress
304
+ ? truncateAddress(effectiveReceiveAddress, 4)
305
+ : 'Enter address'}
306
+ </span>
307
+ <svg
308
+ width='12'
309
+ height='12'
310
+ viewBox='0 0 24 24'
311
+ fill='none'
312
+ stroke='currentColor'
313
+ strokeWidth='2'
314
+ >
315
+ <path d='M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7' />
316
+ <path d='M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z' />
317
+ </svg>
318
+ </button>
319
+ )}
320
+ </div>
321
+
322
+ <div className='ssw-input-row'>
323
+ <input
324
+ type='text'
325
+ className='ssw-amount-input'
326
+ placeholder='0'
327
+ value={buyAmount ? formatAmount(buyAmount, buyAsset.precision) : ''}
328
+ readOnly
329
+ />
330
+ <button
331
+ className={`ssw-token-btn${isBuyAssetLocked ? ' ssw-token-btn-locked' : ''}`}
332
+ onClick={isBuyAssetLocked ? undefined : () => onOpenTokenModal('buy')}
333
+ disabled={isBuyAssetLocked}
334
+ type='button'
335
+ >
336
+ {buyAsset.icon ? (
337
+ <img src={buyAsset.icon} alt={buyAsset.symbol} className='ssw-token-icon' />
338
+ ) : (
339
+ <div className='ssw-token-icon-placeholder'>{buyAsset.symbol.charAt(0)}</div>
340
+ )}
341
+ <div className='ssw-token-info'>
342
+ <span className='ssw-token-symbol'>{buyAsset.symbol}</span>
343
+ <span className='ssw-token-chain'>
344
+ {buyChainInfo?.name ?? buyAsset.networkName ?? buyAsset.name}
345
+ </span>
346
+ </div>
347
+ {!isBuyAssetLocked && (
348
+ <svg
349
+ width='16'
350
+ height='16'
351
+ viewBox='0 0 24 24'
352
+ fill='none'
353
+ stroke='currentColor'
354
+ strokeWidth='2'
355
+ >
356
+ <path d='M9 18l6-6-6-6' />
357
+ </svg>
358
+ )}
359
+ </button>
360
+ </div>
361
+
362
+ <div className='ssw-section-footer'>
363
+ <span className='ssw-usd-value'>{buyUsdValue}</span>
364
+ {(walletAddress || bitcoinAddress || solanaAddress) &&
365
+ (isBuyBalanceLoading ? (
366
+ <span className='ssw-balance-skeleton' />
367
+ ) : buyAssetBalance ? (
368
+ <span className='ssw-balance'>
369
+ Balance: {buyAssetBalance.balanceFormatted}
370
+ {buyBalanceFiatValue && (
371
+ <span className='ssw-balance-fiat'> ({buyBalanceFiatValue})</span>
372
+ )}
373
+ </span>
374
+ ) : null)}
375
+ </div>
376
+ </div>
377
+ </div>
378
+
379
+ {sellAmountBaseUnit && sellAmountBaseUnit !== '0' && (rates?.length || isLoadingRates) && (
380
+ <div className='ssw-quotes'>
381
+ <QuoteSelector
382
+ rates={rates ?? []}
383
+ selectedRate={selectedRate}
384
+ onSelectRate={onSelectRate}
385
+ buyAsset={buyAsset}
386
+ sellAsset={sellAsset}
387
+ sellAmountBaseUnit={sellAmountBaseUnit}
388
+ isLoading={isLoadingRates}
389
+ buyAssetUsdPrice={buyAssetUsdPrice}
390
+ />
391
+ </div>
392
+ )}
393
+
394
+ {networkFeeDisplay && (
395
+ <div className='ssw-network-fee'>
396
+ <span className='ssw-network-fee-label'>Est. network fee</span>
397
+ <span className='ssw-network-fee-value'>{networkFeeDisplay}</span>
398
+ </div>
399
+ )}
400
+
401
+ <button
402
+ className={`ssw-action-btn ${isUnsupportedChain ? 'ssw-secondary' : ''}`}
403
+ disabled={isButtonDisabled || isQuoting}
404
+ onClick={onButtonClick}
405
+ type='button'
406
+ style={isQuoting ? { opacity: 0.7 } : undefined}
407
+ >
408
+ {isQuoting ? (
409
+ <>
410
+ <svg
411
+ className='ssw-spinner'
412
+ width='16'
413
+ height='16'
414
+ viewBox='0 0 24 24'
415
+ fill='none'
416
+ stroke='currentColor'
417
+ strokeWidth='2'
418
+ style={{ display: 'inline-block', verticalAlign: 'middle', marginRight: '6px' }}
419
+ >
420
+ <circle cx='12' cy='12' r='10' opacity='0.25' />
421
+ <path d='M12 2a10 10 0 0 1 10 10' />
422
+ </svg>
423
+ Fetching Quote…
424
+ </>
425
+ ) : (
426
+ buttonText
427
+ )}
428
+ </button>
429
+ </>
430
+ )
431
+ }
@@ -0,0 +1,146 @@
1
+ .ssw-quote-selector {
2
+ width: 100%;
3
+ padding: 14px 16px;
4
+ border-radius: var(--ssw-radius-md);
5
+ background: var(--ssw-bg-tertiary);
6
+ border: 1px solid var(--ssw-border);
7
+ cursor: pointer;
8
+ transition: all 0.15s ease;
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: space-between;
12
+ gap: 12px;
13
+ }
14
+
15
+ .ssw-quote-selector:hover {
16
+ border-color: var(--ssw-border-hover);
17
+ background: var(--ssw-bg-hover);
18
+ }
19
+
20
+ .ssw-quote-selector.ssw-loading {
21
+ cursor: default;
22
+ justify-content: center;
23
+ }
24
+
25
+ .ssw-quote-selector.ssw-loading:hover {
26
+ border-color: var(--ssw-border);
27
+ background: var(--ssw-bg-tertiary);
28
+ }
29
+
30
+ .ssw-quote-loading {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ gap: 10px;
35
+ color: var(--ssw-text-secondary);
36
+ font-size: 14px;
37
+ }
38
+
39
+ .ssw-spinner-small {
40
+ width: 16px;
41
+ height: 16px;
42
+ border: 2px solid var(--ssw-border);
43
+ border-top-color: var(--ssw-accent);
44
+ border-radius: 50%;
45
+ animation: ssw-spin 0.8s linear infinite;
46
+ }
47
+
48
+ @keyframes ssw-spin {
49
+ to {
50
+ transform: rotate(360deg);
51
+ }
52
+ }
53
+
54
+ .ssw-quote-left {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: 4px;
58
+ align-items: flex-start;
59
+ }
60
+
61
+ .ssw-quote-provider {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 10px;
65
+ }
66
+
67
+ .ssw-quote-provider-icon {
68
+ width: 20px;
69
+ height: 20px;
70
+ border-radius: var(--ssw-radius-xs);
71
+ object-fit: contain;
72
+ background: var(--ssw-bg-secondary);
73
+ }
74
+
75
+ .ssw-quote-provider-icon-placeholder {
76
+ width: 20px;
77
+ height: 20px;
78
+ border-radius: var(--ssw-radius-xs);
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ font-size: 12px;
83
+ font-weight: 600;
84
+ color: white;
85
+ }
86
+
87
+ .ssw-quote-provider-name {
88
+ font-size: 14px;
89
+ font-weight: 600;
90
+ color: var(--ssw-text-primary);
91
+ }
92
+
93
+ .ssw-quote-best-tag {
94
+ padding: 3px 7px;
95
+ border-radius: var(--ssw-radius-xs);
96
+ background: var(--ssw-accent);
97
+ color: white;
98
+ font-size: 10px;
99
+ font-weight: 600;
100
+ text-transform: uppercase;
101
+ letter-spacing: 0.03em;
102
+ }
103
+
104
+ .ssw-quote-usd {
105
+ font-size: 13px;
106
+ color: var(--ssw-text-secondary);
107
+ padding-left: 38px;
108
+ }
109
+
110
+ .ssw-quote-right {
111
+ display: flex;
112
+ flex-direction: column;
113
+ align-items: flex-end;
114
+ gap: 4px;
115
+ }
116
+
117
+ .ssw-quote-amount-row {
118
+ display: flex;
119
+ align-items: baseline;
120
+ gap: 5px;
121
+ }
122
+
123
+ .ssw-quote-amount {
124
+ font-size: 17px;
125
+ font-weight: 600;
126
+ color: var(--ssw-text-primary);
127
+ }
128
+
129
+ .ssw-quote-symbol {
130
+ font-size: 13px;
131
+ color: var(--ssw-text-secondary);
132
+ font-weight: 500;
133
+ }
134
+
135
+ .ssw-quote-more {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 3px;
139
+ font-size: 12px;
140
+ color: var(--ssw-accent);
141
+ font-weight: 500;
142
+ }
143
+
144
+ .ssw-quote-more svg {
145
+ opacity: 0.9;
146
+ }
@@ -0,0 +1,137 @@
1
+ import './QuoteSelector.css'
2
+
3
+ import { useCallback, useMemo, useState } from 'react'
4
+
5
+ import { getSwapperColor, getSwapperIcon } from '../constants/swappers'
6
+ import { formatUsdValue } from '../hooks/useMarketData'
7
+ import type { Asset, TradeRate } from '../types'
8
+ import { formatAmount } from '../types'
9
+ import { QuotesModal } from './QuotesModal'
10
+
11
+ type QuoteSelectorProps = {
12
+ rates: TradeRate[]
13
+ selectedRate: TradeRate | null
14
+ onSelectRate: (rate: TradeRate) => void
15
+ buyAsset: Asset
16
+ sellAsset: Asset
17
+ sellAmountBaseUnit: string
18
+ isLoading: boolean
19
+ buyAssetUsdPrice?: string
20
+ }
21
+
22
+ export const QuoteSelector = ({
23
+ rates,
24
+ selectedRate,
25
+ onSelectRate,
26
+ buyAsset,
27
+ sellAsset,
28
+ sellAmountBaseUnit,
29
+ isLoading,
30
+ buyAssetUsdPrice,
31
+ }: QuoteSelectorProps) => {
32
+ const [isModalOpen, setIsModalOpen] = useState(false)
33
+
34
+ const bestRate = useMemo(() => rates[0], [rates])
35
+ const alternativeRatesCount = useMemo(() => Math.max(0, rates.length - 1), [rates])
36
+
37
+ const handleOpenModal = useCallback(() => {
38
+ if (rates.length > 0) {
39
+ setIsModalOpen(true)
40
+ }
41
+ }, [rates.length])
42
+
43
+ const handleCloseModal = useCallback(() => {
44
+ setIsModalOpen(false)
45
+ }, [])
46
+
47
+ const handleSelectRate = useCallback(
48
+ (rate: TradeRate) => {
49
+ onSelectRate(rate)
50
+ },
51
+ [onSelectRate],
52
+ )
53
+
54
+ if (isLoading) {
55
+ return (
56
+ <div className='ssw-quote-selector ssw-loading'>
57
+ <div className='ssw-quote-loading'>
58
+ <div className='ssw-spinner-small' />
59
+ <span>Finding best rates...</span>
60
+ </div>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ if (!bestRate) {
66
+ return null
67
+ }
68
+
69
+ const displayRate = selectedRate ?? bestRate
70
+ const buyAmount = displayRate.buyAmountCryptoBaseUnit ?? '0'
71
+ const swapperIcon = getSwapperIcon(displayRate.swapperName)
72
+ const swapperColor = getSwapperColor(displayRate.swapperName)
73
+ const formattedBuyAmount = formatAmount(buyAmount, buyAsset.precision)
74
+ const usdValue = formatUsdValue(buyAmount, buyAsset.precision, buyAssetUsdPrice)
75
+
76
+ return (
77
+ <>
78
+ <button className='ssw-quote-selector' onClick={handleOpenModal} type='button'>
79
+ <div className='ssw-quote-left'>
80
+ <div className='ssw-quote-provider'>
81
+ {swapperIcon ? (
82
+ <img
83
+ src={swapperIcon}
84
+ alt={displayRate.swapperName}
85
+ className='ssw-quote-provider-icon'
86
+ />
87
+ ) : (
88
+ <div
89
+ className='ssw-quote-provider-icon-placeholder'
90
+ style={{ backgroundColor: swapperColor }}
91
+ >
92
+ {displayRate.swapperName.charAt(0)}
93
+ </div>
94
+ )}
95
+ <span className='ssw-quote-provider-name'>{displayRate.swapperName}</span>
96
+ {displayRate === bestRate && <span className='ssw-quote-best-tag'>Best</span>}
97
+ </div>
98
+ <span className='ssw-quote-usd'>{usdValue}</span>
99
+ </div>
100
+
101
+ <div className='ssw-quote-right'>
102
+ <div className='ssw-quote-amount-row'>
103
+ <span className='ssw-quote-amount'>{formattedBuyAmount}</span>
104
+ <span className='ssw-quote-symbol'>{buyAsset.symbol}</span>
105
+ </div>
106
+ {alternativeRatesCount > 0 && (
107
+ <span className='ssw-quote-more'>
108
+ +{alternativeRatesCount} more
109
+ <svg
110
+ width='12'
111
+ height='12'
112
+ viewBox='0 0 24 24'
113
+ fill='none'
114
+ stroke='currentColor'
115
+ strokeWidth='2.5'
116
+ >
117
+ <path d='M9 18l6-6-6-6' />
118
+ </svg>
119
+ </span>
120
+ )}
121
+ </div>
122
+ </button>
123
+
124
+ <QuotesModal
125
+ isOpen={isModalOpen}
126
+ onClose={handleCloseModal}
127
+ rates={rates}
128
+ selectedRate={selectedRate}
129
+ onSelectRate={handleSelectRate}
130
+ buyAsset={buyAsset}
131
+ sellAsset={sellAsset}
132
+ sellAmountBaseUnit={sellAmountBaseUnit}
133
+ buyAssetUsdPrice={buyAssetUsdPrice}
134
+ />
135
+ </>
136
+ )
137
+ }