0xtrails 0.1.2 → 0.1.4

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