0xtrails 0.2.4 → 0.2.5

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 (161) hide show
  1. package/dist/aave.d.ts +8 -0
  2. package/dist/aave.d.ts.map +1 -1
  3. package/dist/{ccip-BlV1Mry3.js → ccip-CXlshvBY.js} +1 -1
  4. package/dist/config.d.ts +1 -1
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/constants.d.ts +1 -0
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/error.d.ts +1 -0
  9. package/dist/error.d.ts.map +1 -1
  10. package/dist/estimate.d.ts +52 -0
  11. package/dist/estimate.d.ts.map +1 -1
  12. package/dist/{index-BNWCIGfQ.js → index-_QuyGrjU.js} +72332 -72246
  13. package/dist/index.js +2 -2
  14. package/dist/intents.d.ts +40 -0
  15. package/dist/intents.d.ts.map +1 -1
  16. package/dist/metaTxnMonitor.d.ts +3 -3
  17. package/dist/metaTxnMonitor.d.ts.map +1 -1
  18. package/dist/metaTxns.d.ts +3 -3
  19. package/dist/metaTxns.d.ts.map +1 -1
  20. package/dist/morpho.d.ts +8 -0
  21. package/dist/morpho.d.ts.map +1 -1
  22. package/dist/prepareSend.d.ts +16 -6
  23. package/dist/prepareSend.d.ts.map +1 -1
  24. package/dist/queryParams.d.ts.map +1 -1
  25. package/dist/relayer.d.ts +6 -6
  26. package/dist/relayer.d.ts.map +1 -1
  27. package/dist/sequenceWallet.d.ts +2 -2
  28. package/dist/sequenceWallet.d.ts.map +1 -1
  29. package/dist/tokens.d.ts.map +1 -1
  30. package/dist/wallets.d.ts.map +1 -1
  31. package/dist/widget/components/AccountActionsDropdown.d.ts.map +1 -1
  32. package/dist/widget/components/AccountSettings.d.ts.map +1 -1
  33. package/dist/widget/components/ClassicSwap.d.ts +2 -0
  34. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  35. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  36. package/dist/widget/components/ConnectedWallets.d.ts +4 -0
  37. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -1
  38. package/dist/widget/components/Earn.d.ts.map +1 -1
  39. package/dist/widget/components/Fund.d.ts.map +1 -1
  40. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  41. package/dist/widget/components/{FundSendForm.d.ts → FundSwap.d.ts} +11 -5
  42. package/dist/widget/components/FundSwap.d.ts.map +1 -0
  43. package/dist/widget/components/FundingMethodSelectorButton.d.ts +4 -0
  44. package/dist/widget/components/FundingMethodSelectorButton.d.ts.map +1 -0
  45. package/dist/widget/components/Modal.d.ts.map +1 -1
  46. package/dist/widget/components/Pay.d.ts.map +1 -1
  47. package/dist/widget/components/PercentageMaxButtons.d.ts +12 -0
  48. package/dist/widget/components/PercentageMaxButtons.d.ts.map +1 -0
  49. package/dist/widget/components/{PaySendForm.d.ts → PoolDeposit.d.ts} +11 -34
  50. package/dist/widget/components/PoolDeposit.d.ts.map +1 -0
  51. package/dist/widget/components/{SimpleSwap.d.ts → PoolWithdraw.d.ts} +16 -8
  52. package/dist/widget/components/PoolWithdraw.d.ts.map +1 -0
  53. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  54. package/dist/widget/components/Receive.d.ts.map +1 -1
  55. package/dist/widget/components/RecipientSelectorButton.d.ts +4 -0
  56. package/dist/widget/components/RecipientSelectorButton.d.ts.map +1 -0
  57. package/dist/widget/components/Recipients.d.ts.map +1 -1
  58. package/dist/widget/components/RequiredPropsError.d.ts +8 -0
  59. package/dist/widget/components/RequiredPropsError.d.ts.map +1 -0
  60. package/dist/widget/components/ScreenHeader.d.ts.map +1 -1
  61. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  62. package/dist/widget/components/Swap.d.ts +1 -0
  63. package/dist/widget/components/Swap.d.ts.map +1 -1
  64. package/dist/widget/components/SwapSettings.d.ts.map +1 -1
  65. package/dist/widget/components/TokenImage.d.ts +1 -0
  66. package/dist/widget/components/TokenImage.d.ts.map +1 -1
  67. package/dist/widget/components/TokenList.d.ts.map +1 -1
  68. package/dist/widget/components/TokenSelector.d.ts.map +1 -1
  69. package/dist/widget/components/TokenSelectorButton.d.ts +16 -0
  70. package/dist/widget/components/TokenSelectorButton.d.ts.map +1 -0
  71. package/dist/widget/components/UserPreferences.d.ts.map +1 -1
  72. package/dist/widget/components/WaasFeeOptions.d.ts +8 -0
  73. package/dist/widget/components/WaasFeeOptions.d.ts.map +1 -0
  74. package/dist/widget/components/WalletConfirmation.d.ts.map +1 -1
  75. package/dist/widget/components/WalletList.d.ts.map +1 -1
  76. package/dist/widget/css/compiled.css +2 -0
  77. package/dist/widget/css/index.css +554 -0
  78. package/dist/widget/hooks/useBack.d.ts +1 -0
  79. package/dist/widget/hooks/useBack.d.ts.map +1 -1
  80. package/dist/widget/hooks/useCheckout.d.ts +1 -1
  81. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  82. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  83. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  84. package/dist/widget/hooks/useDefaultTokenSelection.d.ts +3 -3
  85. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -1
  86. package/dist/widget/hooks/usePayMessage.d.ts.map +1 -1
  87. package/dist/widget/hooks/useSelectedFundMethod.d.ts +12 -0
  88. package/dist/widget/hooks/useSelectedFundMethod.d.ts.map +1 -0
  89. package/dist/widget/hooks/useSelectedRecipient.d.ts.map +1 -1
  90. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  91. package/dist/widget/index.js +1 -1
  92. package/dist/widget/widget.d.ts +4 -4
  93. package/dist/widget/widget.d.ts.map +1 -1
  94. package/package.json +18 -12
  95. package/src/aave.ts +32 -0
  96. package/src/config.ts +12 -4
  97. package/src/constants.ts +2 -0
  98. package/src/error.ts +19 -1
  99. package/src/estimate.ts +416 -5
  100. package/src/intents.ts +161 -11
  101. package/src/metaTxnMonitor.ts +3 -3
  102. package/src/metaTxns.ts +3 -5
  103. package/src/morpho.ts +32 -0
  104. package/src/prepareSend.ts +503 -166
  105. package/src/queryParams.ts +2 -1
  106. package/src/relayer.ts +11 -11
  107. package/src/sequenceWallet.ts +2 -2
  108. package/src/tokens.ts +7 -1
  109. package/src/wallets.ts +8 -0
  110. package/src/widget/compiled.css +2 -2
  111. package/src/widget/components/AccountActionsDropdown.tsx +3 -13
  112. package/src/widget/components/AccountSettings.tsx +6 -24
  113. package/src/widget/components/ClassicSwap.tsx +111 -155
  114. package/src/widget/components/ConnectWallet.tsx +4 -37
  115. package/src/widget/components/ConnectedWallets.tsx +113 -58
  116. package/src/widget/components/Earn.tsx +73 -589
  117. package/src/widget/components/Fund.tsx +31 -82
  118. package/src/widget/components/FundMethods.tsx +82 -159
  119. package/src/widget/components/FundSwap.tsx +52 -0
  120. package/src/widget/components/FundingMethodSelectorButton.tsx +60 -0
  121. package/src/widget/components/Modal.tsx +6 -2
  122. package/src/widget/components/Pay.tsx +183 -208
  123. package/src/widget/components/PercentageMaxButtons.tsx +77 -0
  124. package/src/widget/components/PoolDeposit.tsx +593 -0
  125. package/src/widget/components/PoolWithdraw.tsx +903 -0
  126. package/src/widget/components/QuoteDetails.tsx +22 -8
  127. package/src/widget/components/Receive.tsx +0 -2
  128. package/src/widget/components/RecipientSelectorButton.tsx +42 -0
  129. package/src/widget/components/Recipients.tsx +62 -156
  130. package/src/widget/components/RequiredPropsError.tsx +33 -0
  131. package/src/widget/components/ScreenHeader.tsx +5 -1
  132. package/src/widget/components/SlippageToleranceSettings.tsx +2 -1
  133. package/src/widget/components/Swap.tsx +2 -43
  134. package/src/widget/components/SwapSettings.tsx +2 -14
  135. package/src/widget/components/TokenImage.tsx +21 -4
  136. package/src/widget/components/TokenList.tsx +0 -1
  137. package/src/widget/components/TokenSelector.tsx +1 -0
  138. package/src/widget/components/TokenSelectorButton.tsx +75 -0
  139. package/src/widget/components/UserPreferences.tsx +6 -24
  140. package/src/widget/components/WaasFeeOptions.tsx +331 -0
  141. package/src/widget/components/WalletConfirmation.tsx +55 -3
  142. package/src/widget/components/WalletList.tsx +4 -2
  143. package/src/widget/hooks/useBack.tsx +2 -0
  144. package/src/widget/hooks/useCheckout.ts +36 -20
  145. package/src/widget/hooks/useCurrentScreen.tsx +1 -0
  146. package/src/widget/hooks/useDefaultTokenSelection.tsx +104 -28
  147. package/src/widget/hooks/usePayMessage.tsx +86 -11
  148. package/src/widget/hooks/useSelectedFundMethod.tsx +41 -0
  149. package/src/widget/hooks/useSelectedRecipient.tsx +10 -0
  150. package/src/widget/hooks/useSendForm.ts +24 -2
  151. package/src/widget/index.css +27 -0
  152. package/src/widget/widget.tsx +169 -111
  153. package/dist/widget/components/FundSendForm.d.ts.map +0 -1
  154. package/dist/widget/components/PaySendForm.d.ts.map +0 -1
  155. package/dist/widget/components/SimpleSwap.d.ts.map +0 -1
  156. package/dist/widget/hooks/useSwapSettings.d.ts +0 -16
  157. package/dist/widget/hooks/useSwapSettings.d.ts.map +0 -1
  158. package/src/widget/components/FundSendForm.tsx +0 -903
  159. package/src/widget/components/PaySendForm.tsx +0 -869
  160. package/src/widget/components/SimpleSwap.tsx +0 -983
  161. package/src/widget/hooks/useSwapSettings.tsx +0 -100
@@ -1,903 +0,0 @@
1
- import { ChevronDown, Loader2, RefreshCcw } from "lucide-react"
2
- import type React from "react"
3
- import { useEffect, useRef, useState, useCallback, useMemo } from "react"
4
- import type { Account, WalletClient } from "viem"
5
- import { formatUnits, isAddress } from "viem"
6
- import type { TransactionState } from "../../transactions.js"
7
- import type { OnCompleteProps, Token, TokenInfo } from "../hooks/useSendForm.js"
8
- import { useSendForm } from "../hooks/useSendForm.js"
9
- import type { CheckoutOnHandlers } from "../hooks/useCheckout.js"
10
- import { ChainImage } from "./ChainImage.js"
11
- import { TokenImage } from "./TokenImage.js"
12
- import { QuoteDetails } from "./QuoteDetails.js"
13
- import { TruncatedAddress } from "./TruncatedAddress.js"
14
- // import { RefundAddressInput } from "./RefundAddressInput.js"
15
- import { TradeType } from "../../prepareSend.js"
16
- import type { PrepareSendQuote } from "../../prepareSend.js"
17
- import { formatAmount, formatUsdAmountDisplay } from "../../tokenBalances.js"
18
- import { ScreenHeader } from "./ScreenHeader.js"
19
- import { ErrorDisplay } from "./ErrorDisplay.js"
20
- import { useMode } from "../hooks/useMode.js"
21
- import { getExplorerUrlForAddress } from "../../explorer.js"
22
- import { logger } from "../../logger.js"
23
- import { useBalanceVisible } from "../hooks/useBalanceVisible.js"
24
-
25
- interface FundSendFormProps {
26
- selectedToken: Token
27
- onSend: (amount: string, recipient: string) => void
28
- onBack?: () => void
29
- onConfirm: () => void
30
- onComplete: (result: OnCompleteProps) => void
31
- account: Account
32
- toRecipient?: string
33
- toAmount?: string
34
- toChainId?: number
35
- toToken?: string
36
- toCalldata?: string
37
- walletClient: WalletClient
38
- onTransactionStateChange: (transactionStates: TransactionState[]) => void
39
- onError: (error: Error | string | null) => void
40
- onWaitingForWalletConfirm: (props: PrepareSendQuote) => void
41
- paymasterUrls?: Array<{ chainId: number; url: string }>
42
- gasless?: boolean
43
- setWalletConfirmRetryHandler: (handler: () => Promise<void>) => void
44
- quoteProvider?: string
45
- fundMethod?: string
46
- onNavigateToMeshConnect?: (
47
- props: {
48
- toTokenSymbol: string
49
- toTokenAmount: string
50
- toChainId: number
51
- toRecipientAddress: string
52
- },
53
- quote?: PrepareSendQuote | null,
54
- ) => void
55
- checkoutOnHandlers?: CheckoutOnHandlers
56
- }
57
-
58
- export const FundSendForm: React.FC<FundSendFormProps> = ({
59
- selectedToken,
60
- onSend,
61
- onBack,
62
- onConfirm,
63
- onComplete,
64
- account,
65
- toAmount,
66
- toRecipient,
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
- checkoutOnHandlers,
81
- }) => {
82
- const { mode } = useMode()
83
- const { isBalanceVisible } = useBalanceVisible()
84
- // Local state for fund-specific functionality
85
- const [isInputTypeUsd, setIsInputTypeUsd] = useState(false)
86
- const [refetchTrigger, setRefetchTrigger] = useState(0)
87
- // const [isRefundAddressOpen, setIsRefundAddressOpen] = useState(false)
88
- // const [refundAddress, setRefundAddress] = useState<string>(account.address)
89
-
90
- const [tokenAmountForBackend, setTokenAmountForBackend] = useState("")
91
- const [inputDisplayValue, setInputDisplayValue] = useState("")
92
- const inputRef = useRef<HTMLInputElement>(null)
93
- const chainDropdownRef = useRef<HTMLDivElement>(null)
94
- const tokenDropdownRef = useRef<HTMLDivElement>(null)
95
-
96
- // Auto-focus input field on component mount
97
- useEffect(() => {
98
- if (inputRef.current) {
99
- inputRef.current.focus()
100
- }
101
- }, [])
102
-
103
- const {
104
- amount: hookAmount,
105
- amountUsdDisplay,
106
- balanceFormatted,
107
- balanceUsdDisplay,
108
- chainInfo,
109
- isSubmitting,
110
- isLoadingQuote,
111
- selectedDestinationChain,
112
- selectedDestToken,
113
- setAmount: setHookAmount,
114
- handleSubmit,
115
- buttonText,
116
- toAmountFormatted,
117
- toAmountDisplay,
118
- sourceTokenPrices,
119
- destTokenPrices,
120
- isValidRecipient,
121
- recipient,
122
- recipientInput,
123
- setRecipient,
124
- setRecipientInput,
125
- handleRecipientInputChange,
126
- ensAddress,
127
- isChainDropdownOpen,
128
- isTokenDropdownOpen,
129
- setIsChainDropdownOpen,
130
- setIsTokenDropdownOpen,
131
- supportedChains,
132
- supportedTokens,
133
- setSelectedDestinationChain,
134
- setSelectedDestToken,
135
- prepareSendQuote,
136
- quoteError,
137
- quoteErrorPrettified,
138
- isSameTokenWithoutCustomCalldata,
139
- destinationTokenAddress,
140
- isRecipientContract,
141
- } = useSendForm({
142
- account,
143
- // Don't pass toAmount for fund form - user enters input amount
144
- toRecipient: toRecipient || account.address,
145
- toChainId,
146
- toToken,
147
- toCalldata,
148
- // refundAddress,
149
- walletClient,
150
- onTransactionStateChange,
151
- onError,
152
- onWaitingForWalletConfirm,
153
- paymasterUrls,
154
- gasless,
155
- onConfirm,
156
- onComplete,
157
- onSend,
158
- selectedToken,
159
- setWalletConfirmRetryHandler,
160
- tradeType: TradeType.EXACT_INPUT,
161
- quoteProvider,
162
- fundMethod,
163
- mode,
164
- onNavigateToMeshConnect,
165
- checkoutOnHandlers,
166
- refetchTrigger,
167
- })
168
-
169
- // Get source token price for USD conversions
170
- const sourceTokenPrice = sourceTokenPrices?.[0]?.price?.value ?? 0
171
-
172
- // Get destination token price for receive USD value
173
- const destTokenPrice = destTokenPrices?.[0]?.price?.value ?? 0
174
-
175
- // Sync display value with token amount only when mode changes (not during typing)
176
- const [lastInputMode, setLastInputMode] = useState(isInputTypeUsd)
177
-
178
- useEffect(() => {
179
- // Only sync when mode actually changes, not during normal typing
180
- if (lastInputMode !== isInputTypeUsd && tokenAmountForBackend) {
181
- const tokenAmount = parseFloat(tokenAmountForBackend) || 0
182
- if (isInputTypeUsd && sourceTokenPrice > 0) {
183
- // Show USD with max 2 decimals
184
- const usdAmount = tokenAmount * sourceTokenPrice
185
- setInputDisplayValue(Number(usdAmount.toFixed(2)).toString())
186
- } else {
187
- // Show token with max 8 decimals
188
- setInputDisplayValue(Number(tokenAmount.toFixed(8)).toString())
189
- }
190
- setLastInputMode(isInputTypeUsd)
191
- }
192
- }, [isInputTypeUsd, sourceTokenPrice, tokenAmountForBackend, lastInputMode])
193
-
194
- // Handle click outside for dropdowns
195
- useEffect(() => {
196
- const handleClickOutside = (event: MouseEvent) => {
197
- if (
198
- chainDropdownRef.current &&
199
- !chainDropdownRef.current.contains(event.target as Node)
200
- ) {
201
- setIsChainDropdownOpen(false)
202
- }
203
- if (
204
- tokenDropdownRef.current &&
205
- !tokenDropdownRef.current.contains(event.target as Node)
206
- ) {
207
- setIsTokenDropdownOpen(false)
208
- }
209
- }
210
-
211
- if (isChainDropdownOpen || isTokenDropdownOpen) {
212
- document.addEventListener("click", handleClickOutside)
213
- return () => document.removeEventListener("click", handleClickOutside)
214
- }
215
- }, [
216
- setIsChainDropdownOpen,
217
- setIsTokenDropdownOpen,
218
- isChainDropdownOpen,
219
- isTokenDropdownOpen,
220
- ])
221
-
222
- // Handle input amount changes with 8 decimal limit and 16 char total limit
223
- const handleAmountChange = useCallback(
224
- (value: string) => {
225
- // Allow empty string
226
- if (value === "") {
227
- setInputDisplayValue("")
228
- setTokenAmountForBackend("")
229
- setHookAmount("")
230
- return
231
- }
232
-
233
- // Limit total length to 16 characters
234
- if (value.length > 16) {
235
- return
236
- }
237
-
238
- // Validate decimal places (max 8 decimals) and allow single decimal point
239
- const decimalMatch = value.match(/^\d*\.?\d{0,8}$/)
240
- if (!decimalMatch) {
241
- return // Don't update if invalid format
242
- }
243
-
244
- // Store the display value
245
- setInputDisplayValue(value)
246
-
247
- // Update the token amount for backend and useSendForm
248
- if (isInputTypeUsd && sourceTokenPrice > 0) {
249
- const usdAmount = parseFloat(value) || 0
250
- const tokenAmount = usdAmount / sourceTokenPrice
251
- setTokenAmountForBackend(tokenAmount.toString())
252
- setHookAmount(tokenAmount.toString())
253
- } else {
254
- setTokenAmountForBackend(value)
255
- setHookAmount(value)
256
- }
257
- },
258
- [setHookAmount, isInputTypeUsd, sourceTokenPrice],
259
- )
260
-
261
- // Get display values based on input type
262
- const displayAmount = useMemo(() => {
263
- return inputDisplayValue
264
- }, [inputDisplayValue])
265
-
266
- const displayUsdValue = useMemo(() => {
267
- if (isInputTypeUsd && sourceTokenPrice > 0) {
268
- // Show token amount when in USD mode
269
- const tokenAmount = parseFloat(tokenAmountForBackend) || 0
270
- return `${formatAmount(tokenAmount)} ${selectedToken.symbol}`
271
- }
272
- return amountUsdDisplay
273
- }, [
274
- tokenAmountForBackend,
275
- isInputTypeUsd,
276
- sourceTokenPrice,
277
- selectedToken.symbol,
278
- amountUsdDisplay,
279
- ])
280
-
281
- // Calculate USD value for the receive section based on destination token
282
- const receiveUsdValue = useMemo(() => {
283
- if (destTokenPrice > 0) {
284
- const destinationAmount = parseFloat(toAmountFormatted) || 0
285
- const usdValue = destinationAmount * destTokenPrice
286
- logger.console.log("[trails-sdk] Receive USD calculation:", {
287
- toAmountFormatted,
288
- destinationAmount,
289
- destTokenPrice,
290
- usdValue,
291
- formatted: formatUsdAmountDisplay(usdValue),
292
- })
293
- return formatUsdAmountDisplay(usdValue)
294
- }
295
- return formatUsdAmountDisplay(0)
296
- }, [toAmountFormatted, destTokenPrice])
297
-
298
- // Handle percentage clicks for quick amounts
299
- const handlePercentageClick = useCallback(
300
- (percentage: number) => {
301
- if (!selectedToken.balance || !selectedToken.contractInfo?.decimals) {
302
- return
303
- }
304
-
305
- const totalBalance = parseFloat(
306
- formatUnits(
307
- BigInt(selectedToken.balance),
308
- selectedToken.contractInfo.decimals,
309
- ),
310
- )
311
-
312
- const calculatedAmount = (totalBalance * percentage) / 100
313
- // Cap decimals to 8 places
314
- const cappedAmount = parseFloat(calculatedAmount.toFixed(8))
315
- const tokenAmountStr = cappedAmount.toString()
316
-
317
- // Update all states consistently
318
- setTokenAmountForBackend(tokenAmountStr)
319
- setHookAmount(tokenAmountStr)
320
-
321
- // Update display based on current mode
322
- if (isInputTypeUsd && sourceTokenPrice > 0) {
323
- const usdAmount = cappedAmount * sourceTokenPrice
324
- setInputDisplayValue(Number(usdAmount.toFixed(2)).toString())
325
- } else {
326
- setInputDisplayValue(tokenAmountStr)
327
- }
328
- },
329
- [selectedToken, setHookAmount, isInputTypeUsd, sourceTokenPrice],
330
- )
331
-
332
- // Handle input type toggle (USD ↔ Token)
333
- const handleInputTypeToggle = useCallback(() => {
334
- // Use tokenAmountForBackend as the source of truth for conversion
335
- const currentTokenAmount = parseFloat(tokenAmountForBackend) || 0
336
-
337
- if (isInputTypeUsd && sourceTokenPrice > 0) {
338
- // Switching from USD to token mode
339
- // Display the token amount (limit to 8 decimals)
340
- const tokenAmountStr = Number(currentTokenAmount.toFixed(8)).toString()
341
- setInputDisplayValue(tokenAmountStr)
342
- } else if (!isInputTypeUsd && sourceTokenPrice > 0) {
343
- // Switching from token to USD mode
344
- // Display USD amount (limit to 2 decimals)
345
- const usdAmount = currentTokenAmount * sourceTokenPrice
346
- const usdAmountStr = Number(usdAmount.toFixed(2)).toString()
347
- setInputDisplayValue(usdAmountStr)
348
- }
349
-
350
- // hookAmount stays as token amount (don't change it)
351
- // tokenAmountForBackend stays as token amount (don't change it)
352
-
353
- setIsInputTypeUsd(!isInputTypeUsd)
354
- // Focus the input field after toggling
355
- setTimeout(() => {
356
- if (inputRef.current) {
357
- inputRef.current.focus()
358
- // Select all text for easy replacement
359
- inputRef.current.select()
360
- }
361
- }, 0)
362
- }, [tokenAmountForBackend, isInputTypeUsd, sourceTokenPrice])
363
-
364
- // Handle manual quote refetch
365
- const handleRefetchQuote = useCallback(() => {
366
- setRefetchTrigger((prev) => prev + 1)
367
- }, [])
368
-
369
- // Dynamic font size based on input length
370
- const inputStyles = useMemo(() => {
371
- const inputLength = displayAmount.length
372
- let fontSize = "text-6xl" // Much larger initial size
373
-
374
- if (inputLength > 12) {
375
- fontSize = "text-2xl"
376
- } else if (inputLength > 9) {
377
- fontSize = "text-3xl"
378
- } else if (inputLength > 6) {
379
- fontSize = "text-4xl"
380
- } else if (inputLength > 3) {
381
- fontSize = "text-5xl"
382
- }
383
-
384
- return {
385
- fontSize,
386
- transition: "all 0.1s ease-in-out",
387
- }
388
- }, [displayAmount.length])
389
-
390
- logger.console.log("[trails-sdk] FundForm", {
391
- hookAmount, // actual token amount used by backend
392
- displayAmount, // what user sees in input
393
- isInputTypeUsd,
394
- sourceTokenPrice,
395
- toAmount,
396
- isSubmitting,
397
- selectedDestinationChain,
398
- })
399
-
400
- if (!selectedDestinationChain) {
401
- return null
402
- }
403
-
404
- if (!selectedToken) {
405
- return null
406
- }
407
-
408
- return (
409
- <div className="space-y-2">
410
- <ScreenHeader
411
- onBack={onBack}
412
- headerContent="Fund"
413
- headerContentAlign="left"
414
- showAccountActions={true}
415
- />
416
-
417
- {/* Balance Info Section */}
418
- <div className="flex items-center space-x-4 p-4 trails-border-radius-container trails-bg-secondary">
419
- <div className="flex items-start justify-between w-full">
420
- {/* Left side - Chain and Token images with token name */}
421
- <div className="flex items-start space-x-2">
422
- <div className="flex items-center space-x-2">
423
- <div style={{ width: "32px", height: "32px" }}>
424
- <a
425
- href={getExplorerUrlForAddress({
426
- address: selectedToken.contractAddress,
427
- chainId: selectedToken.chainId,
428
- })}
429
- target="_blank"
430
- rel="noopener noreferrer"
431
- className="cursor-pointer"
432
- >
433
- <TokenImage
434
- symbol={selectedToken.symbol}
435
- imageUrl={selectedToken.imageUrl}
436
- chainId={selectedToken.chainId}
437
- size={32}
438
- />
439
- </a>
440
- </div>
441
- <div className="flex flex-col">
442
- <span className="text-sm font-medium max-w-[135px] truncate text-left text-gray-900 dark:text-white">
443
- {selectedToken.name}
444
- </span>
445
- <span className="text-sm text-gray-500 dark:text-gray-400">
446
- on {chainInfo?.name || "Unknown Chain"}
447
- </span>
448
- </div>
449
- </div>
450
- </div>
451
-
452
- {/* Right side - USD value and amount */}
453
- {fundMethod !== "qr-code" && fundMethod !== "exchange" && (
454
- <div className="text-right">
455
- <div className="text-sm font-medium text-gray-900 dark:text-white">
456
- <span className="text-gray-600 dark:text-gray-400">
457
- Balance:{" "}
458
- </span>
459
- {isBalanceVisible ? balanceUsdDisplay : "••••••"}
460
- </div>
461
- <div className="text-sm text-gray-600 dark:text-gray-400">
462
- {isBalanceVisible
463
- ? `${balanceFormatted} ${selectedToken.symbol}`
464
- : "••••••"}
465
- </div>
466
- </div>
467
- )}
468
- </div>
469
- </div>
470
-
471
- <form onSubmit={handleSubmit} className="space-y-2">
472
- {/* Origin Amount Input Section */}
473
- <div className="space-y-1">
474
- {/* Amount Input */}
475
- <div className="flex items-center justify-center">
476
- <div className="flex items-center">
477
- <input
478
- ref={inputRef}
479
- type="text"
480
- value={displayAmount}
481
- onChange={(e) => handleAmountChange(e.target.value)}
482
- placeholder="0"
483
- className={`bg-transparent border-none outline-none ${inputStyles.fontSize} font-bold text-right trails-text-primary placeholder-trails-text-primary`}
484
- style={{
485
- width: `${Math.max((displayAmount || "0").length, 1)}ch`,
486
- minWidth: "1ch",
487
- maxWidth: "270px",
488
- padding: "0",
489
- margin: "0",
490
- }}
491
- inputMode="decimal"
492
- />
493
- <span
494
- className={`${inputStyles.fontSize} font-bold text-gray-400 dark:text-gray-500`}
495
- style={{
496
- marginLeft:
497
- displayAmount && displayAmount !== "0" ? "0.2em" : "0.1em",
498
- padding: "0",
499
- transition: "all 0.2s ease-in-out",
500
- }}
501
- >
502
- {isInputTypeUsd ? "USD" : selectedToken.symbol.slice(0, 4)}
503
- </span>
504
- </div>
505
- </div>
506
-
507
- {/* USD Value centered below input */}
508
- <div className="flex items-center justify-center">
509
- <button
510
- type="button"
511
- onClick={handleInputTypeToggle}
512
- className="flex items-center justify-center gap-2 px-3 py-1.5 rounded-md transition-colors cursor-pointer text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:trails-hover-bg hover:text-gray-700 dark:hover:text-gray-200"
513
- >
514
- <span className="text-xs font-medium tracking-[-2px]">⇅</span>
515
- <div className="text-sm font-normal">{displayUsdValue}</div>
516
- </button>
517
- </div>
518
-
519
- {/* Percentage Buttons */}
520
- <div className="flex space-x-1 justify-center">
521
- {[25, 50, 75, 100].map((percentage) => (
522
- <button
523
- key={percentage}
524
- type="button"
525
- onClick={() => handlePercentageClick(percentage)}
526
- 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"
527
- >
528
- {percentage === 100 ? "MAX" : `${percentage}%`}
529
- </button>
530
- ))}
531
- </div>
532
- </div>
533
-
534
- {/* Chain Selection */}
535
- {!toChainId && (
536
- <div className="mb-4">
537
- <label
538
- htmlFor="destination-chain"
539
- className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300 text-left"
540
- >
541
- Destination Chain
542
- </label>
543
- <div className="relative" ref={chainDropdownRef}>
544
- <button
545
- type="button"
546
- onClick={() => setIsChainDropdownOpen(!isChainDropdownOpen)}
547
- className="w-full flex items-center px-4 py-3 border border-solid trails-border-radius-dropdown hover:border-gray-400 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 trails-dropdown"
548
- >
549
- <ChainImage chainId={selectedDestinationChain.id} size={24} />
550
- <span className="ml-2 flex-1 text-left">
551
- {selectedDestinationChain.name}
552
- </span>
553
- <ChevronDown
554
- className={`h-5 w-5 text-gray-400 transition-transform ${
555
- isChainDropdownOpen ? "transform rotate-180" : ""
556
- }`}
557
- />
558
- </button>
559
-
560
- {isChainDropdownOpen && (
561
- <div className="absolute z-10 w-full mt-1 border border-solid trails-border-radius-dropdown shadow-lg max-h-60 overflow-y-auto custom-scrollbar trails-dropdown">
562
- {supportedChains.map((chain) => (
563
- <button
564
- key={chain.id}
565
- type="button"
566
- onClick={(e) => {
567
- e.preventDefault()
568
- e.stopPropagation()
569
- setSelectedDestinationChain(chain)
570
- setIsChainDropdownOpen(false)
571
- }}
572
- onMouseDown={(e) => {
573
- e.preventDefault()
574
- e.stopPropagation()
575
- }}
576
- className={`w-full flex items-center px-4 py-3 cursor-pointer trails-dropdown-item ${
577
- selectedDestinationChain.id === chain.id
578
- ? "trails-dropdown-item-selected"
579
- : "hover:trails-dropdown-item"
580
- }`}
581
- >
582
- <ChainImage chainId={chain.id} size={24} />
583
- <span className="ml-2">{chain.name}</span>
584
- {selectedDestinationChain.id === chain.id && (
585
- <span className="ml-auto text-gray-900 dark:text-white">
586
-
587
- </span>
588
- )}
589
- </button>
590
- ))}
591
- </div>
592
- )}
593
- </div>
594
- </div>
595
- )}
596
-
597
- {/* Token Selection */}
598
- {!toToken && (
599
- <div className="mb-4">
600
- <label
601
- htmlFor="token"
602
- className="block text-sm font-medium mb-1 text-gray-700 dark:text-gray-300 text-left"
603
- >
604
- Receive Token
605
- </label>
606
- <div className="relative" ref={tokenDropdownRef}>
607
- <button
608
- type="button"
609
- onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)}
610
- className="w-full flex items-center px-4 py-3 border border-solid trails-border-radius-dropdown hover:border-gray-400 cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 trails-dropdown"
611
- >
612
- <div className="w-5 h-5 rounded-full flex items-center justify-center text-sm bg-gray-100 dark:bg-gray-700">
613
- <TokenImage
614
- symbol={selectedDestToken?.symbol}
615
- imageUrl={selectedDestToken?.imageUrl}
616
- size={24}
617
- />
618
- </div>
619
- <span className="ml-2 flex-1 text-left">
620
- {selectedDestToken?.name} ({selectedDestToken?.symbol})
621
- </span>
622
- <ChevronDown
623
- className={`h-5 w-5 text-gray-400 transition-transform ${
624
- isTokenDropdownOpen ? "transform rotate-180" : ""
625
- }`}
626
- />
627
- </button>
628
-
629
- {isTokenDropdownOpen && (
630
- <div className="absolute z-10 w-full mt-1 border border-solid trails-border-radius-dropdown shadow-lg max-h-60 overflow-y-auto custom-scrollbar trails-dropdown">
631
- {supportedTokens.map((token) => (
632
- <button
633
- key={`${token.contractAddress}-${token.chainId}`}
634
- type="button"
635
- onClick={() => {
636
- setSelectedDestToken(token as TokenInfo)
637
- setIsTokenDropdownOpen(false)
638
- }}
639
- className={`w-full flex items-center px-4 py-3 cursor-pointer trails-dropdown-item ${
640
- selectedDestToken?.symbol === token.symbol
641
- ? "trails-dropdown-item-selected"
642
- : "hover:trails-dropdown-item"
643
- }`}
644
- >
645
- <TokenImage
646
- symbol={token.symbol}
647
- imageUrl={token.imageUrl}
648
- size={24}
649
- />
650
- <span className="ml-2">
651
- {token.name} ({token.symbol})
652
- </span>
653
- {selectedDestToken?.symbol === token.symbol && (
654
- <span className="ml-auto text-gray-900 dark:text-white">
655
-
656
- </span>
657
- )}
658
- </button>
659
- ))}
660
- </div>
661
- )}
662
- </div>
663
- </div>
664
- )}
665
-
666
- {/* Recipient Input */}
667
- {!toRecipient && (
668
- <div className="mb-4">
669
- <div className="flex justify-between items-center mb-1">
670
- <div>
671
- <label
672
- htmlFor="recipient"
673
- className="text-sm font-medium text-gray-700 dark:text-gray-300"
674
- >
675
- {toCalldata ? "Destination Address" : "Recipient Address"}
676
- </label>
677
- {recipient &&
678
- isAddress(recipient) &&
679
- recipient.toLowerCase() === account.address.toLowerCase() && (
680
- <div className="text-xs mt-0.5 text-left text-gray-400">
681
- Same as sender
682
- </div>
683
- )}
684
- </div>
685
- <div className="h-7 flex items-center">
686
- {recipient !== account.address ? (
687
- <button
688
- type="button"
689
- onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
690
- event.preventDefault()
691
- setRecipientInput(account.address)
692
- setRecipient(account.address)
693
- }}
694
- className={`px-2 py-1 text-xs cursor-pointer trails-border-radius-button transition-colors bg-blue-500 hover:bg-blue-600 text-white`}
695
- >
696
- Use Account
697
- </button>
698
- ) : null}
699
- </div>
700
- </div>
701
- <input
702
- id="recipient"
703
- type="text"
704
- value={recipientInput}
705
- onChange={handleRecipientInputChange}
706
- placeholder="0x... or name.eth"
707
- className="block w-full px-4 py-3 border border-solid trails-border-radius-input focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm trails-input"
708
- />
709
- {ensAddress && <p className="text-sm text-gray-400">{recipient}</p>}
710
- </div>
711
- )}
712
-
713
- {/* Receive Section */}
714
- <div className="space-y-1">
715
- <div className="flex items-center justify-between">
716
- <div
717
- className={`text-lg font-semibold text-left ${"text-gray-900 dark:text-white"}`}
718
- >
719
- Receive
720
- </div>
721
- <button
722
- type="button"
723
- onClick={handleRefetchQuote}
724
- disabled={
725
- isLoadingQuote ||
726
- !tokenAmountForBackend ||
727
- !selectedDestToken ||
728
- !selectedDestinationChain ||
729
- !isValidRecipient
730
- }
731
- className={`p-2 rounded-md transition-colors cursor-pointer ${
732
- isLoadingQuote ||
733
- !tokenAmountForBackend ||
734
- !selectedDestToken ||
735
- !selectedDestinationChain ||
736
- !isValidRecipient
737
- ? "opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-500"
738
- : "text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-200"
739
- }`}
740
- title="Refetch quote"
741
- >
742
- <RefreshCcw
743
- className={`h-4 w-4 ${isLoadingQuote ? "animate-spin" : ""}`}
744
- />
745
- </button>
746
- </div>
747
-
748
- <div className="p-2">
749
- <div className="flex items-center space-x-3">
750
- <a
751
- href={getExplorerUrlForAddress({
752
- address: destinationTokenAddress || "",
753
- chainId: selectedDestinationChain.id,
754
- })}
755
- target="_blank"
756
- rel="noopener noreferrer"
757
- className="cursor-pointer"
758
- >
759
- <TokenImage
760
- symbol={selectedDestToken?.symbol}
761
- imageUrl={selectedDestToken?.imageUrl}
762
- chainId={selectedDestinationChain.id}
763
- size={32}
764
- />
765
- </a>
766
- <div>
767
- <div className="flex items-center space-x-2">
768
- <div
769
- className={`text-lg font-semibold ${"text-gray-900 dark:text-white"} ${isLoadingQuote ? "animate-pulse" : ""}`}
770
- >
771
- {toAmountDisplay} {selectedDestToken?.symbol}
772
- </div>
773
- {isLoadingQuote && (
774
- <div className="animate-spin rounded-full h-4 w-4 border-solid border-b-2 border-blue-500 dark:border-blue-400" />
775
- )}
776
- </div>
777
- <div
778
- className={`text-xs text-left ${"text-gray-500 dark:text-gray-400"} ${isLoadingQuote ? "animate-pulse" : ""}`}
779
- >
780
- ≈ {receiveUsdValue}{" "}
781
- {selectedDestinationChain
782
- ? `on ${selectedDestinationChain.name}`
783
- : ""}
784
- </div>
785
- </div>
786
- </div>
787
- </div>
788
-
789
- {/* Show recipient address if different from sender */}
790
- {recipient &&
791
- recipient.toLowerCase() !== account.address.toLowerCase() && (
792
- <div className="px-2 pb-1">
793
- <div className="text-xs text-left text-gray-500 dark:text-gray-400">
794
- {isRecipientContract ? "Destination Contract" : "Recipient"}:{" "}
795
- <TruncatedAddress
796
- address={recipient}
797
- chainId={selectedDestinationChain.id}
798
- />
799
- </div>
800
- </div>
801
- )}
802
- </div>
803
-
804
- {/* Custom Calldata */}
805
- {toCalldata && (
806
- <div className="px-2 pb-1">
807
- <p className="text-[10px] text-left text-gray-500 dark:text-gray-400">
808
- This transaction includes custom calldata for contract interaction
809
- at the destination address
810
- </p>
811
- </div>
812
- )}
813
-
814
- {/* Refund Address Input */}
815
- {/* <RefundAddressInput
816
- account={account}
817
- isOpen={isRefundAddressOpen}
818
- onToggle={() => setIsRefundAddressOpen(!isRefundAddressOpen)}
819
- refundAddress={refundAddress}
820
- onRefundAddressChange={setRefundAddress}
821
- chainId={selectedDestinationChain.id}
822
- /> */}
823
-
824
- {/* Warning Messages - Show only one at a time */}
825
- {isSameTokenWithoutCustomCalldata ? (
826
- <ErrorDisplay
827
- errorPrettified="Cannot swap to the same token on the same chain without custom calldata. Please select a different origin token."
828
- severity="error"
829
- />
830
- ) : (
831
- <ErrorDisplay
832
- errorPrettified={quoteErrorPrettified}
833
- error={quoteError}
834
- severity="warning"
835
- />
836
- )}
837
- {prepareSendQuote?.noSufficientBalance ? (
838
- <div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
839
- <div className="flex items-center space-x-2">
840
- <svg
841
- className="w-4 h-4 text-amber-500 flex-shrink-0"
842
- fill="none"
843
- stroke="currentColor"
844
- viewBox="0 0 24 24"
845
- aria-hidden="true"
846
- >
847
- <path
848
- strokeLinecap="round"
849
- strokeLinejoin="round"
850
- strokeWidth={2}
851
- 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"
852
- />
853
- </svg>
854
- <p className="text-sm text-amber-600 dark:text-amber-400">
855
- Insufficient balance to complete this transaction
856
- </p>
857
- </div>
858
- </div>
859
- ) : null}
860
-
861
- {/* Continue Button */}
862
- <button
863
- type="submit"
864
- disabled={
865
- !tokenAmountForBackend ||
866
- parseFloat(tokenAmountForBackend) <= 0 ||
867
- isSubmitting ||
868
- isLoadingQuote ||
869
- !isValidRecipient ||
870
- buttonText === "No quote available" ||
871
- buttonText === "Getting quote..." ||
872
- prepareSendQuote?.noSufficientBalance ||
873
- isSameTokenWithoutCustomCalldata
874
- }
875
- 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`}
876
- >
877
- {isSubmitting ? (
878
- <div className="flex items-center justify-center">
879
- <Loader2 className="w-5 h-5 animate-spin mr-2 text-white dark:text-gray-400" />
880
- <span>{buttonText}</span>
881
- </div>
882
- ) : isSameTokenWithoutCustomCalldata ? (
883
- "Select Different Tokens"
884
- ) : prepareSendQuote?.noSufficientBalance ? (
885
- "Insufficient Balance"
886
- ) : !tokenAmountForBackend ||
887
- parseFloat(tokenAmountForBackend) <= 0 ? (
888
- "Enter an amount"
889
- ) : (
890
- buttonText
891
- )}
892
- </button>
893
-
894
- {/* Quote Details */}
895
- {prepareSendQuote && (
896
- <div className="space-y-2">
897
- <QuoteDetails quote={prepareSendQuote} showContent={true} />
898
- </div>
899
- )}
900
- </form>
901
- </div>
902
- )
903
- }