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.
- package/dist/analytics.d.ts +68 -1
- package/dist/analytics.d.ts.map +1 -1
- package/dist/{ccip-BmFTEOaB.js → ccip-CWd4g9uZ.js} +1 -1
- package/dist/chains.d.ts +9 -3
- package/dist/chains.d.ts.map +1 -1
- package/dist/ens.d.ts +7 -0
- package/dist/ens.d.ts.map +1 -0
- package/dist/error.d.ts +2 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/{index-BPsVj7zK.js → index-BTUBzx4R.js} +23624 -21770
- package/dist/index.js +2 -2
- package/dist/lifi.d.ts +4 -0
- package/dist/lifi.d.ts.map +1 -0
- package/dist/mode.d.ts +1 -1
- package/dist/mode.d.ts.map +1 -1
- package/dist/prepareSend.d.ts +3 -1
- package/dist/prepareSend.d.ts.map +1 -1
- package/dist/prices.d.ts +2 -0
- package/dist/prices.d.ts.map +1 -1
- package/dist/relaySdk.d.ts.map +1 -1
- package/dist/relayer.d.ts.map +1 -1
- package/dist/tokenBalances.d.ts.map +1 -1
- package/dist/tokens.d.ts +2 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/trails.d.ts +3 -3
- package/dist/trails.d.ts.map +1 -1
- package/dist/transactions.d.ts.map +1 -1
- package/dist/wallets.d.ts +247 -5
- package/dist/wallets.d.ts.map +1 -1
- package/dist/widget/components/ChainFilterDropdown.d.ts +2 -0
- package/dist/widget/components/ChainFilterDropdown.d.ts.map +1 -1
- package/dist/widget/components/ConnectWallet.d.ts +1 -0
- package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
- package/dist/widget/components/DebugScreensDropdown.d.ts.map +1 -1
- package/dist/widget/components/FundSendForm.d.ts +2 -2
- package/dist/widget/components/FundSendForm.d.ts.map +1 -1
- package/dist/widget/components/PaySendForm.d.ts +2 -2
- package/dist/widget/components/PaySendForm.d.ts.map +1 -1
- package/dist/widget/components/QrCode.d.ts +1 -1
- package/dist/widget/components/QrCode.d.ts.map +1 -1
- package/dist/widget/components/RefundAddressInput.d.ts +13 -0
- package/dist/widget/components/RefundAddressInput.d.ts.map +1 -0
- package/dist/widget/components/Swap.d.ts +43 -0
- package/dist/widget/components/Swap.d.ts.map +1 -0
- package/dist/widget/components/TokenList.d.ts +0 -2
- package/dist/widget/components/TokenList.d.ts.map +1 -1
- package/dist/widget/components/TokenSelector.d.ts +26 -0
- package/dist/widget/components/TokenSelector.d.ts.map +1 -0
- package/dist/widget/components/WalletConnect.d.ts.map +1 -1
- package/dist/widget/components/WalletConnectionPending.d.ts +12 -0
- package/dist/widget/components/WalletConnectionPending.d.ts.map +1 -0
- package/dist/widget/components/WalletList.d.ts.map +1 -1
- package/dist/widget/hooks/useAmountUsd.d.ts +1 -3
- package/dist/widget/hooks/useAmountUsd.d.ts.map +1 -1
- package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
- package/dist/widget/hooks/useSendForm.d.ts +6 -4
- package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
- package/dist/widget/hooks/useTokenList.d.ts +2 -3
- package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
- package/dist/widget/index.js +1 -1
- package/dist/widget/widget.d.ts.map +1 -1
- package/package.json +9 -6
- package/src/aave.ts +13 -13
- package/src/analytics.ts +87 -4
- package/src/chains.ts +45 -7
- package/src/constants.ts +4 -4
- package/src/ens.ts +17 -0
- package/src/error.ts +16 -1
- package/src/lifi.ts +58 -0
- package/src/mode.ts +1 -1
- package/src/morpho.ts +3 -3
- package/src/pools.ts +18 -18
- package/src/prepareSend.ts +35 -3
- package/src/prices.ts +21 -0
- package/src/relaySdk.ts +1 -0
- package/src/relayer.ts +8 -0
- package/src/tokenBalances.ts +3 -0
- package/src/tokens.ts +85 -19
- package/src/trails.ts +2 -2
- package/src/transactions.ts +1 -0
- package/src/wallets.ts +275 -35
- package/src/widget/compiled.css +1 -1
- package/src/widget/components/ChainFilterDropdown.tsx +42 -33
- package/src/widget/components/ChainImage.tsx +1 -1
- package/src/widget/components/ConnectWallet.tsx +92 -128
- package/src/widget/components/DebugScreensDropdown.tsx +3 -0
- package/src/widget/components/FundSendForm.tsx +17 -3
- package/src/widget/components/PaySendForm.tsx +16 -2
- package/src/widget/components/QRCodeDeposit.tsx +1 -1
- package/src/widget/components/QrCode.tsx +277 -16
- package/src/widget/components/Receipt.tsx +1 -1
- package/src/widget/components/RefundAddressInput.tsx +149 -0
- package/src/widget/components/Swap.tsx +648 -0
- package/src/widget/components/TokenList.tsx +27 -363
- package/src/widget/components/TokenSelector.tsx +405 -0
- package/src/widget/components/WalletConnect.tsx +9 -7
- package/src/widget/components/WalletConnectionPending.tsx +157 -0
- package/src/widget/components/WalletList.tsx +6 -5
- package/src/widget/hooks/useAmountUsd.ts +3 -8
- package/src/widget/hooks/useCheckout.ts +3 -2
- package/src/widget/hooks/useSendForm.ts +66 -32
- package/src/widget/hooks/useTokenList.ts +158 -106
- 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
|