0xtrails 0.1.2 → 0.1.3

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 (103) hide show
  1. package/dist/analytics.d.ts +68 -1
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/{ccip-BmFTEOaB.js → ccip-CWd4g9uZ.js} +1 -1
  4. package/dist/chains.d.ts +9 -3
  5. package/dist/chains.d.ts.map +1 -1
  6. package/dist/ens.d.ts +7 -0
  7. package/dist/ens.d.ts.map +1 -0
  8. package/dist/error.d.ts +2 -0
  9. package/dist/error.d.ts.map +1 -1
  10. package/dist/{index-BPsVj7zK.js → index-BTUBzx4R.js} +23624 -21770
  11. package/dist/index.js +2 -2
  12. package/dist/lifi.d.ts +4 -0
  13. package/dist/lifi.d.ts.map +1 -0
  14. package/dist/mode.d.ts +1 -1
  15. package/dist/mode.d.ts.map +1 -1
  16. package/dist/prepareSend.d.ts +3 -1
  17. package/dist/prepareSend.d.ts.map +1 -1
  18. package/dist/prices.d.ts +2 -0
  19. package/dist/prices.d.ts.map +1 -1
  20. package/dist/relaySdk.d.ts.map +1 -1
  21. package/dist/relayer.d.ts.map +1 -1
  22. package/dist/tokenBalances.d.ts.map +1 -1
  23. package/dist/tokens.d.ts +2 -1
  24. package/dist/tokens.d.ts.map +1 -1
  25. package/dist/trails.d.ts +3 -3
  26. package/dist/trails.d.ts.map +1 -1
  27. package/dist/transactions.d.ts.map +1 -1
  28. package/dist/wallets.d.ts +247 -5
  29. package/dist/wallets.d.ts.map +1 -1
  30. package/dist/widget/components/ChainFilterDropdown.d.ts +2 -0
  31. package/dist/widget/components/ChainFilterDropdown.d.ts.map +1 -1
  32. package/dist/widget/components/ConnectWallet.d.ts +1 -0
  33. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  34. package/dist/widget/components/DebugScreensDropdown.d.ts.map +1 -1
  35. package/dist/widget/components/FundSendForm.d.ts +2 -2
  36. package/dist/widget/components/FundSendForm.d.ts.map +1 -1
  37. package/dist/widget/components/PaySendForm.d.ts +2 -2
  38. package/dist/widget/components/PaySendForm.d.ts.map +1 -1
  39. package/dist/widget/components/QrCode.d.ts +1 -1
  40. package/dist/widget/components/QrCode.d.ts.map +1 -1
  41. package/dist/widget/components/RefundAddressInput.d.ts +13 -0
  42. package/dist/widget/components/RefundAddressInput.d.ts.map +1 -0
  43. package/dist/widget/components/Swap.d.ts +43 -0
  44. package/dist/widget/components/Swap.d.ts.map +1 -0
  45. package/dist/widget/components/TokenList.d.ts +0 -2
  46. package/dist/widget/components/TokenList.d.ts.map +1 -1
  47. package/dist/widget/components/TokenSelector.d.ts +26 -0
  48. package/dist/widget/components/TokenSelector.d.ts.map +1 -0
  49. package/dist/widget/components/WalletConnect.d.ts.map +1 -1
  50. package/dist/widget/components/WalletConnectionPending.d.ts +12 -0
  51. package/dist/widget/components/WalletConnectionPending.d.ts.map +1 -0
  52. package/dist/widget/components/WalletList.d.ts.map +1 -1
  53. package/dist/widget/hooks/useAmountUsd.d.ts +1 -3
  54. package/dist/widget/hooks/useAmountUsd.d.ts.map +1 -1
  55. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  56. package/dist/widget/hooks/useSendForm.d.ts +6 -4
  57. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  58. package/dist/widget/hooks/useTokenList.d.ts +2 -3
  59. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  60. package/dist/widget/index.js +1 -1
  61. package/dist/widget/widget.d.ts.map +1 -1
  62. package/package.json +9 -6
  63. package/src/aave.ts +13 -13
  64. package/src/analytics.ts +87 -4
  65. package/src/chains.ts +45 -7
  66. package/src/constants.ts +4 -4
  67. package/src/ens.ts +17 -0
  68. package/src/error.ts +16 -1
  69. package/src/lifi.ts +58 -0
  70. package/src/mode.ts +1 -1
  71. package/src/morpho.ts +3 -3
  72. package/src/pools.ts +18 -18
  73. package/src/prepareSend.ts +35 -3
  74. package/src/prices.ts +21 -0
  75. package/src/relaySdk.ts +1 -0
  76. package/src/relayer.ts +8 -0
  77. package/src/tokenBalances.ts +3 -0
  78. package/src/tokens.ts +85 -19
  79. package/src/trails.ts +2 -2
  80. package/src/transactions.ts +1 -0
  81. package/src/wallets.ts +275 -35
  82. package/src/widget/compiled.css +1 -1
  83. package/src/widget/components/ChainFilterDropdown.tsx +42 -33
  84. package/src/widget/components/ChainImage.tsx +1 -1
  85. package/src/widget/components/ConnectWallet.tsx +92 -128
  86. package/src/widget/components/DebugScreensDropdown.tsx +3 -0
  87. package/src/widget/components/FundSendForm.tsx +17 -3
  88. package/src/widget/components/PaySendForm.tsx +16 -2
  89. package/src/widget/components/QRCodeDeposit.tsx +1 -1
  90. package/src/widget/components/QrCode.tsx +277 -16
  91. package/src/widget/components/Receipt.tsx +1 -1
  92. package/src/widget/components/RefundAddressInput.tsx +149 -0
  93. package/src/widget/components/Swap.tsx +648 -0
  94. package/src/widget/components/TokenList.tsx +27 -363
  95. package/src/widget/components/TokenSelector.tsx +405 -0
  96. package/src/widget/components/WalletConnect.tsx +9 -7
  97. package/src/widget/components/WalletConnectionPending.tsx +157 -0
  98. package/src/widget/components/WalletList.tsx +6 -5
  99. package/src/widget/hooks/useAmountUsd.ts +3 -8
  100. package/src/widget/hooks/useCheckout.ts +3 -2
  101. package/src/widget/hooks/useSendForm.ts +66 -32
  102. package/src/widget/hooks/useTokenList.ts +158 -106
  103. package/src/widget/widget.tsx +335 -72
@@ -0,0 +1,648 @@
1
+ import { ChevronDown, Loader2, ArrowUpDown } from "lucide-react"
2
+ import type React from "react"
3
+ import { useCallback, useEffect, useRef, useState } from "react"
4
+ import type { Account, WalletClient } from "viem"
5
+ import type { TransactionState } from "../../transactions.js"
6
+ import type { OnCompleteProps, Token } from "../hooks/useSendForm.js"
7
+ import type { SupportedToken } from "../../tokens.js"
8
+ import { useSendForm } from "../hooks/useSendForm.js"
9
+ import type { CheckoutOnHandlers } from "../hooks/useCheckout.js"
10
+ import { FeeOptions } from "./FeeOptions.js"
11
+ import { TokenImage } from "./TokenImage.js"
12
+ import { QuoteDetails } from "./QuoteDetails.js"
13
+ import { type PrepareSendQuote, TradeType } from "../../prepareSend.js"
14
+ import { getChainInfo } from "../../chains.js"
15
+ import { formatUsdAmountDisplay } from "../../tokenBalances.js"
16
+ import { MINIMUM_USD_AMOUNT_FOR_SWAP } from "../../constants.js"
17
+ import { ScreenHeader } from "./ScreenHeader.js"
18
+ import { TokenSelector } from "./TokenSelector.js"
19
+
20
+ interface SwapProps {
21
+ selectedToken: Token | null
22
+ onSend: (amount: string, recipient: string) => void
23
+ onBack: () => void
24
+ onConfirm: () => void
25
+ onComplete: (result: OnCompleteProps) => void
26
+ account: Account
27
+ toRecipient?: string
28
+ toAmount?: string
29
+ toChainId?: number
30
+ toToken?: string
31
+ toCalldata?: string
32
+ walletClient: WalletClient
33
+ onTransactionStateChange: (transactionStates: TransactionState[]) => void
34
+ onError: (error: Error | string | null) => void
35
+ onWaitingForWalletConfirm: (props: PrepareSendQuote) => void
36
+ paymasterUrls?: Array<{ chainId: number; url: string }>
37
+ gasless?: boolean
38
+ setWalletConfirmRetryHandler: (handler: () => Promise<void>) => void
39
+ quoteProvider?: string
40
+ fundMethod?: string
41
+ onNavigateToMeshConnect?: (
42
+ props: {
43
+ toTokenSymbol: string
44
+ toTokenAmount: string
45
+ toChainId: number
46
+ toRecipientAddress: string
47
+ },
48
+ quote?: PrepareSendQuote | null,
49
+ ) => void
50
+ onAmountUpdate?: (amount: string) => void
51
+ mode?: "pay" | "fund" | "earn" | "swap"
52
+ checkoutOnHandlers?: CheckoutOnHandlers
53
+ }
54
+
55
+ export const Swap: React.FC<SwapProps> = ({
56
+ selectedToken: initialSelectedToken,
57
+ onSend,
58
+ onBack,
59
+ onConfirm,
60
+ onComplete,
61
+ account,
62
+ toAmount,
63
+ toChainId,
64
+ toToken,
65
+ toCalldata,
66
+ walletClient,
67
+ onTransactionStateChange,
68
+ onError,
69
+ onWaitingForWalletConfirm,
70
+ paymasterUrls,
71
+ gasless,
72
+ setWalletConfirmRetryHandler,
73
+ quoteProvider,
74
+ fundMethod,
75
+ onNavigateToMeshConnect,
76
+ onAmountUpdate,
77
+ mode,
78
+ checkoutOnHandlers,
79
+ }) => {
80
+ const [isFlipped, setIsFlipped] = useState(false)
81
+ const [originChainId, setOriginChainId] = useState<number | null | undefined>(
82
+ initialSelectedToken?.chainId,
83
+ )
84
+ const [originToken, setOriginToken] = useState<SupportedToken | null>(
85
+ initialSelectedToken as SupportedToken | null,
86
+ )
87
+
88
+ const {
89
+ amount,
90
+ amountRaw,
91
+ amountUsdDisplay,
92
+ balanceFormatted,
93
+ handleSubmit,
94
+ isSubmitting,
95
+ isLoadingQuote,
96
+ isTokenDropdownOpen,
97
+ selectedDestinationChain,
98
+ selectedDestToken,
99
+ setAmount,
100
+ setSelectedDestinationChain,
101
+ setSelectedDestToken,
102
+ buttonText,
103
+ selectedFeeToken,
104
+ setSelectedFeeToken,
105
+ FEE_TOKENS,
106
+ setIsTokenDropdownOpen,
107
+ toAmountDisplay,
108
+ destinationTokenAddress,
109
+ isValidCustomToken,
110
+ prepareSendQuote,
111
+ } = useSendForm({
112
+ account,
113
+ toAmount,
114
+ toRecipient: account.address,
115
+ toChainId,
116
+ toToken,
117
+ toCalldata,
118
+ walletClient,
119
+ onTransactionStateChange,
120
+ onError,
121
+ onWaitingForWalletConfirm,
122
+ paymasterUrls,
123
+ gasless,
124
+ onConfirm,
125
+ onComplete,
126
+ onSend,
127
+ selectedToken: originToken as any,
128
+ setWalletConfirmRetryHandler,
129
+ tradeType: TradeType.EXACT_INPUT,
130
+ quoteProvider,
131
+ fundMethod,
132
+ mode,
133
+ onNavigateToMeshConnect,
134
+ checkoutOnHandlers,
135
+ })
136
+
137
+ // Handle amount input changes with decimal validation
138
+ const handleAmountChange = useCallback(
139
+ (value: string) => {
140
+ // Validate decimal places (max 8 decimals)
141
+ const decimalMatch = value.match(/^\d*\.?\d{0,8}$/)
142
+ if (!decimalMatch && value !== "") {
143
+ return // Don't update if more than 8 decimals
144
+ }
145
+ setAmount(value)
146
+ },
147
+ [setAmount],
148
+ )
149
+
150
+ // Handle percentage button clicks
151
+ const handlePercentageClick = useCallback(
152
+ (percentage: number) => {
153
+ if (!originToken || !balanceFormatted) return
154
+
155
+ // Parse the balance and calculate percentage
156
+ const balance = parseFloat(balanceFormatted)
157
+ if (Number.isNaN(balance)) return
158
+
159
+ const amount = (balance * percentage) / 100
160
+ setAmount(amount.toFixed(6))
161
+ },
162
+ [originToken, balanceFormatted, setAmount],
163
+ )
164
+
165
+ // Call onAmountUpdate when amountRaw changes
166
+ useEffect(() => {
167
+ if (onAmountUpdate) {
168
+ onAmountUpdate(amountRaw)
169
+ }
170
+ }, [amountRaw, onAmountUpdate])
171
+
172
+ // Auto-focus the input field when component mounts
173
+ useEffect(() => {
174
+ if (inputRef.current) {
175
+ inputRef.current.focus()
176
+ }
177
+ }, [])
178
+
179
+ const originTokenDropdownRef = useRef<HTMLDivElement>(null)
180
+ const tokenDropdownRef = useRef<HTMLInputElement>(null)
181
+ const inputRef = useRef<HTMLInputElement>(null)
182
+
183
+ const [isOriginTokenDropdownOpen, setIsOriginTokenDropdownOpen] =
184
+ useState(false)
185
+
186
+ useEffect(() => {
187
+ if (selectedDestToken) {
188
+ setSelectedDestinationChain(
189
+ getChainInfo((selectedDestToken as any)?.chainId as any) as any,
190
+ )
191
+ }
192
+ }, [selectedDestToken, setSelectedDestinationChain])
193
+
194
+ useEffect(() => {
195
+ const handleClickOutside = (event: MouseEvent) => {
196
+ if (
197
+ tokenDropdownRef.current &&
198
+ !tokenDropdownRef.current.contains(event.target as Node)
199
+ ) {
200
+ setIsTokenDropdownOpen(false)
201
+ }
202
+ if (
203
+ originTokenDropdownRef.current &&
204
+ !originTokenDropdownRef.current.contains(event.target as Node)
205
+ ) {
206
+ setIsOriginTokenDropdownOpen(false)
207
+ }
208
+ }
209
+
210
+ if (isTokenDropdownOpen || isOriginTokenDropdownOpen) {
211
+ document.addEventListener("click", handleClickOutside)
212
+ return () => document.removeEventListener("click", handleClickOutside)
213
+ }
214
+ }, [isTokenDropdownOpen, isOriginTokenDropdownOpen, setIsTokenDropdownOpen])
215
+
216
+ // Handle flip functionality
217
+ const handleFlip = () => {
218
+ if (!selectedDestinationChain || !originToken || !selectedDestToken) return
219
+
220
+ setIsFlipped(!isFlipped)
221
+
222
+ // Store current values
223
+ const tempOriginToken = originToken
224
+ const tempOriginChainId = originChainId
225
+
226
+ // Swap origin and destination
227
+ setOriginToken(selectedDestToken as any)
228
+ setOriginChainId((selectedDestToken as any)?.chainId as any)
229
+
230
+ // Set destination to previous origin (convert to TokenInfo)
231
+ setSelectedDestToken(tempOriginToken as any)
232
+
233
+ // Update destination chain
234
+ const newChain = getChainInfo(tempOriginChainId as any)
235
+ if (newChain) {
236
+ setSelectedDestinationChain(newChain)
237
+ }
238
+ }
239
+
240
+ return (
241
+ <div className="space-y-2">
242
+ <ScreenHeader
243
+ onBack={onBack}
244
+ headerContent="Swap"
245
+ headerContentAlign="center"
246
+ />
247
+
248
+ <form onSubmit={handleSubmit} className="space-y-1">
249
+ {/* Input Section - Amount + Token Selection */}
250
+ <div className="trails-bg-secondary trails-border-radius-container p-3 group">
251
+ {/* Sell Label and Percentage Buttons */}
252
+ <div className="flex justify-between items-center mb-2">
253
+ <div className="text-sm font-medium trails-text-secondary">
254
+ Sell
255
+ </div>
256
+
257
+ {/* Percentage Buttons */}
258
+ {originToken && (
259
+ <div className="flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
260
+ <button
261
+ type="button"
262
+ onClick={() => handlePercentageClick(25)}
263
+ className="py-1 px-2 text-xs font-medium trails-border-radius-container border border-solid transition-colors cursor-pointer border-gray-300 text-gray-600 dark:border-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:trails-hover-bg hover:border-gray-400 dark:hover:border-gray-500"
264
+ >
265
+ 25%
266
+ </button>
267
+ <button
268
+ type="button"
269
+ onClick={() => handlePercentageClick(50)}
270
+ className="py-1 px-2 text-xs font-medium trails-border-radius-container border border-solid transition-colors cursor-pointer border-gray-300 text-gray-600 dark:border-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:trails-hover-bg hover:border-gray-400 dark:hover:border-gray-500"
271
+ >
272
+ 50%
273
+ </button>
274
+ <button
275
+ type="button"
276
+ onClick={() => handlePercentageClick(75)}
277
+ className="py-1 px-2 text-xs font-medium trails-border-radius-container border border-solid transition-colors cursor-pointer border-gray-300 text-gray-600 dark:border-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:trails-hover-bg hover:border-gray-400 dark:hover:border-gray-500"
278
+ >
279
+ 75%
280
+ </button>
281
+ <button
282
+ type="button"
283
+ onClick={() => handlePercentageClick(100)}
284
+ className="py-1 px-2 text-xs font-medium trails-border-radius-container border border-solid transition-colors cursor-pointer border-gray-300 text-gray-600 dark:border-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:trails-hover-bg hover:border-gray-400 dark:hover:border-gray-500"
285
+ >
286
+ Max
287
+ </button>
288
+ </div>
289
+ )}
290
+ </div>
291
+
292
+ <div className="flex items-center space-x-2">
293
+ {/* Amount Input */}
294
+ <div className="flex-1">
295
+ <input
296
+ ref={inputRef}
297
+ id="amount"
298
+ type="text"
299
+ value={amount}
300
+ onChange={(e) => handleAmountChange(e.target.value)}
301
+ placeholder="0.00"
302
+ className="w-full bg-transparent text-xl font-medium trails-text-primary placeholder:trails-text-muted border-none outline-none"
303
+ />
304
+ </div>
305
+
306
+ {/* Token Selection Button */}
307
+ <div className="relative" ref={originTokenDropdownRef}>
308
+ <button
309
+ type="button"
310
+ onClick={() =>
311
+ setIsOriginTokenDropdownOpen(!isOriginTokenDropdownOpen)
312
+ }
313
+ className="flex items-center space-x-2 trails-bg-card hover:trails-hover-bg trails-border-radius-input px-2.5 py-1.5 border trails-border-primary transition-colors cursor-pointer"
314
+ >
315
+ {originToken ? (
316
+ <>
317
+ <TokenImage
318
+ symbol={originToken.symbol}
319
+ imageUrl={originToken.imageUrl}
320
+ chainId={originChainId}
321
+ size={20}
322
+ />
323
+ <span className="font-medium trails-text-primary text-sm">
324
+ {originToken.symbol}
325
+ </span>
326
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
327
+ </>
328
+ ) : (
329
+ <>
330
+ <span className="font-medium trails-text-muted text-sm">
331
+ Select Token
332
+ </span>
333
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
334
+ </>
335
+ )}
336
+ </button>
337
+
338
+ {/* Token Selector Modal */}
339
+ {isOriginTokenDropdownOpen && (
340
+ <div className="absolute right-0 top-full mt-2 w-80 trails-bg-card rounded-lg shadow-lg border trails-border-primary max-h-80 overflow-hidden z-10">
341
+ <div className="p-2">
342
+ <TokenSelector
343
+ onTokenSelect={(token: any) => {
344
+ setOriginToken({
345
+ ...token,
346
+ decimals:
347
+ token.contractInfo?.decimals || token.decimals,
348
+ contractInfo: {
349
+ decimals:
350
+ token.contractInfo?.decimals || token.decimals,
351
+ contractAddress: token.contractAddress,
352
+ symbol: token.symbol,
353
+ name: token.name,
354
+ },
355
+ } as any)
356
+ setOriginChainId(token.chainId)
357
+ setIsOriginTokenDropdownOpen(false)
358
+ }}
359
+ onError={onError}
360
+ mode="swap"
361
+ fundMethod={fundMethod}
362
+ showContinueButton={false}
363
+ compactMode={false}
364
+ />
365
+ </div>
366
+ </div>
367
+ )}
368
+ </div>
369
+ </div>
370
+
371
+ {/* Bottom Info Row */}
372
+ <div className="mt-2 flex justify-between items-center">
373
+ {/* USD Amount */}
374
+ {amountUsdDisplay && originToken?.symbol && (
375
+ <div className="text-xs trails-text-muted">
376
+ ≈ {amountUsdDisplay}
377
+ </div>
378
+ )}
379
+
380
+ {/* Origin Token Balance */}
381
+ {originToken && (
382
+ <button
383
+ type="button"
384
+ className="text-xs trails-text-muted cursor-pointer hover:trails-hover-text transition-colors bg-transparent border-none p-0"
385
+ onClick={() => {
386
+ if (balanceFormatted) {
387
+ const balance = parseFloat(balanceFormatted)
388
+ if (!Number.isNaN(balance)) {
389
+ setAmount(balance.toFixed(6))
390
+ }
391
+ }
392
+ }}
393
+ onKeyDown={(e) => {
394
+ if (e.key === "Enter" || e.key === " ") {
395
+ e.preventDefault()
396
+ if (balanceFormatted) {
397
+ const balance = parseFloat(balanceFormatted)
398
+ if (!Number.isNaN(balance)) {
399
+ setAmount(balance.toFixed(6))
400
+ }
401
+ }
402
+ }
403
+ }}
404
+ title="Click to use full balance"
405
+ >
406
+ Balance: {balanceFormatted || "0.00"} {originToken.symbol}
407
+ </button>
408
+ )}
409
+ </div>
410
+ </div>
411
+
412
+ {/* Flip Button - Absolutely Positioned */}
413
+ <div className="relative">
414
+ <button
415
+ type="button"
416
+ onClick={handleFlip}
417
+ className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 p-1.5 trails-border-radius-button trails-bg-tertiary hover:trails-hover-bg transition-colors cursor-pointer"
418
+ >
419
+ <ArrowUpDown className="w-4 h-4 trails-text-secondary" />
420
+ </button>
421
+ </div>
422
+
423
+ {/* Output Section - Amount + Token Selection */}
424
+ <div className="trails-bg-secondary trails-border-radius-container p-3">
425
+ {/* Buy Label */}
426
+ <div className="text-sm font-medium trails-text-secondary mb-2">
427
+ Buy
428
+ </div>
429
+
430
+ <div className="flex items-center space-x-2">
431
+ {/* Output Amount */}
432
+ <div className="flex-1">
433
+ <div className="flex items-center space-x-2">
434
+ <div
435
+ className={`text-xl font-medium ${!amount ? "text-gray-400 dark:text-gray-500" : "trails-text-primary"} ${isLoadingQuote ? "animate-pulse" : ""}`}
436
+ >
437
+ {toAmountDisplay || "0.00"}
438
+ </div>
439
+ {isLoadingQuote && (
440
+ <div className="animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
441
+ )}
442
+ </div>
443
+ </div>
444
+
445
+ {/* Destination Token Selection */}
446
+ <div className="relative" ref={tokenDropdownRef}>
447
+ <button
448
+ type="button"
449
+ onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)}
450
+ className="flex items-center space-x-2 trails-bg-card hover:trails-hover-bg trails-border-radius-input px-2.5 py-1.5 border trails-border-primary transition-colors cursor-pointer"
451
+ >
452
+ {selectedDestToken ? (
453
+ <>
454
+ <TokenImage
455
+ symbol={selectedDestToken.symbol}
456
+ imageUrl={selectedDestToken.imageUrl}
457
+ chainId={(selectedDestToken as any)?.chainId}
458
+ size={20}
459
+ />
460
+ <span className="font-medium trails-text-primary text-sm">
461
+ {selectedDestToken.symbol}
462
+ </span>
463
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
464
+ </>
465
+ ) : (
466
+ <>
467
+ <span className="font-medium trails-text-muted text-sm">
468
+ Select Token
469
+ </span>
470
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
471
+ </>
472
+ )}
473
+ </button>
474
+
475
+ {/* Token Selector Modal */}
476
+ {isTokenDropdownOpen && (
477
+ <div className="absolute right-0 top-full mt-2 w-80 trails-bg-card rounded-lg shadow-lg border trails-border-primary max-h-80 overflow-hidden z-10">
478
+ <div className="p-2">
479
+ <TokenSelector
480
+ onTokenSelect={(token: any) => {
481
+ setSelectedDestToken({
482
+ ...token,
483
+ decimals:
484
+ token.contractInfo?.decimals || token.decimals,
485
+ contractInfo: {
486
+ decimals:
487
+ token.contractInfo?.decimals || token.decimals,
488
+ },
489
+ } as any)
490
+ setIsTokenDropdownOpen(false)
491
+ }}
492
+ onError={onError}
493
+ mode="swap"
494
+ fundMethod={fundMethod}
495
+ allSupportedTokens={true}
496
+ showContinueButton={false}
497
+ compactMode={false}
498
+ />
499
+ </div>
500
+ </div>
501
+ )}
502
+ </div>
503
+ </div>
504
+
505
+ {/* Bottom Info Row */}
506
+ <div className="mt-2 flex justify-between items-center">
507
+ {/* Destination Amount USD from Quote */}
508
+ {prepareSendQuote?.destinationAmountUsdDisplay && (
509
+ <div className="text-xs trails-text-muted">
510
+ ≈ {prepareSendQuote.destinationAmountUsdDisplay}
511
+ </div>
512
+ )}
513
+ </div>
514
+ </div>
515
+
516
+ {/* Receive Section - Show what user will receive */}
517
+ {(toAmount || toChainId || toToken) && selectedDestinationChain && (
518
+ <div className="space-y-1">
519
+ <div className="text-lg font-semibold text-left trails-text-primary">
520
+ You Will Receive
521
+ </div>
522
+
523
+ <div className="p-2">
524
+ <div className="flex items-center space-x-3">
525
+ <TokenImage
526
+ symbol={selectedDestToken?.symbol}
527
+ imageUrl={selectedDestToken?.imageUrl}
528
+ chainId={selectedDestinationChain.id}
529
+ size={32}
530
+ />
531
+ <div>
532
+ <div className="flex items-center space-x-2">
533
+ <div
534
+ className={`text-lg font-semibold trails-text-primary ${isLoadingQuote ? "animate-pulse" : ""}`}
535
+ >
536
+ {toAmountDisplay} {selectedDestToken?.symbol}
537
+ </div>
538
+ {isLoadingQuote && (
539
+ <div className="animate-spin trails-border-radius-button h-4 w-4 border-solid border-b-2 trails-primary" />
540
+ )}
541
+ </div>
542
+ <div
543
+ className={`text-xs trails-text-muted ${isLoadingQuote ? "animate-pulse" : ""}`}
544
+ >
545
+ ≈ {amountUsdDisplay}{" "}
546
+ {selectedDestinationChain
547
+ ? `on ${selectedDestinationChain.name}`
548
+ : ""}
549
+ </div>
550
+ </div>
551
+ </div>
552
+ </div>
553
+ </div>
554
+ )}
555
+
556
+ {/* Fee Options */}
557
+ <FeeOptions
558
+ options={FEE_TOKENS}
559
+ selectedOption={selectedFeeToken ?? undefined}
560
+ onSelect={setSelectedFeeToken}
561
+ />
562
+
563
+ {/* Warning Messages - Show only one at a time */}
564
+ {prepareSendQuote?.noSufficientBalance ? (
565
+ <div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
566
+ <div className="flex items-center space-x-2">
567
+ <svg
568
+ className="w-4 h-4 text-amber-500 flex-shrink-0"
569
+ fill="none"
570
+ stroke="currentColor"
571
+ viewBox="0 0 24 24"
572
+ aria-hidden="true"
573
+ >
574
+ <path
575
+ strokeLinecap="round"
576
+ strokeLinejoin="round"
577
+ strokeWidth={2}
578
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
579
+ />
580
+ </svg>
581
+ <p className="text-sm text-amber-600 dark:text-amber-400">
582
+ Insufficient balance to complete this transaction
583
+ </p>
584
+ </div>
585
+ </div>
586
+ ) : prepareSendQuote?.minimumNotMet ? (
587
+ <div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
588
+ <div className="flex items-center space-x-2">
589
+ <svg
590
+ className="w-4 h-4 text-amber-500 flex-shrink-0"
591
+ fill="none"
592
+ stroke="currentColor"
593
+ viewBox="0 0 24 24"
594
+ aria-hidden="true"
595
+ >
596
+ <path
597
+ strokeLinecap="round"
598
+ strokeLinejoin="round"
599
+ strokeWidth={2}
600
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
601
+ />
602
+ </svg>
603
+ <p className="text-sm text-amber-600 dark:text-amber-400">
604
+ Please enter an amount above{" "}
605
+ {formatUsdAmountDisplay(MINIMUM_USD_AMOUNT_FOR_SWAP)} otherwise
606
+ transfer may fail
607
+ </p>
608
+ </div>
609
+ </div>
610
+ ) : null}
611
+
612
+ <button
613
+ type="submit"
614
+ disabled={
615
+ !amount ||
616
+ isSubmitting ||
617
+ !destinationTokenAddress ||
618
+ !isValidCustomToken ||
619
+ isLoadingQuote ||
620
+ !prepareSendQuote ||
621
+ prepareSendQuote?.noSufficientBalance
622
+ }
623
+ className="w-full font-semibold py-4 px-4 trails-border-radius-button transition-colors bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white disabled:text-gray-500 disabled:cursor-not-allowed cursor-pointer relative"
624
+ >
625
+ {isSubmitting ? (
626
+ <div className="flex items-center justify-center">
627
+ <Loader2 className="w-5 h-5 animate-spin mr-2 trails-text-muted" />
628
+ <span>{buttonText}</span>
629
+ </div>
630
+ ) : prepareSendQuote?.noSufficientBalance ? (
631
+ "Insufficient Balance"
632
+ ) : (
633
+ buttonText
634
+ )}
635
+ </button>
636
+
637
+ {/* Quote Details */}
638
+ {prepareSendQuote && (
639
+ <div className="space-y-2">
640
+ <QuoteDetails quote={prepareSendQuote} showContent={true} />
641
+ </div>
642
+ )}
643
+ </form>
644
+ </div>
645
+ )
646
+ }
647
+
648
+ export default Swap