0xtrails 0.1.13 → 0.2.0

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 (216) hide show
  1. package/dist/aave.d.ts.map +1 -1
  2. package/dist/analytics.d.ts +11 -2
  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/{proxyCaller.d.ts → balanceInjector.d.ts} +5 -4
  7. package/dist/balanceInjector.d.ts.map +1 -0
  8. package/dist/{ccip-D3gTQONK.js → ccip-D6ToCrWc.js} +12 -12
  9. package/dist/cctp.d.ts.map +1 -1
  10. package/dist/cctpqueue.d.ts +3 -3
  11. package/dist/cctpqueue.d.ts.map +1 -1
  12. package/dist/chains.d.ts.map +1 -1
  13. package/dist/config.d.ts +17 -3
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/constants.d.ts +5 -4
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/contractUtils.d.ts +2 -0
  18. package/dist/contractUtils.d.ts.map +1 -1
  19. package/dist/customChains.d.ts +24 -0
  20. package/dist/customChains.d.ts.map +1 -0
  21. package/dist/{index-CnUM7lKf.js → index-BqgeTLL8.js} +34072 -30146
  22. package/dist/index.d.ts +5 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +411 -400
  25. package/dist/intentEntrypoint.d.ts +96 -0
  26. package/dist/intentEntrypoint.d.ts.map +1 -0
  27. package/dist/intents.d.ts +5 -3
  28. package/dist/intents.d.ts.map +1 -1
  29. package/dist/metaTxnMonitor.d.ts.map +1 -1
  30. package/dist/morpho.d.ts.map +1 -1
  31. package/dist/pools.d.ts +3 -1
  32. package/dist/pools.d.ts.map +1 -1
  33. package/dist/prepareSend.d.ts +8 -2
  34. package/dist/prepareSend.d.ts.map +1 -1
  35. package/dist/prices.d.ts +1 -1
  36. package/dist/prices.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/toast.d.ts +9 -0
  40. package/dist/toast.d.ts.map +1 -0
  41. package/dist/tokenBalances.d.ts +6 -2
  42. package/dist/tokenBalances.d.ts.map +1 -1
  43. package/dist/tokens.d.ts.map +1 -1
  44. package/dist/trails.d.ts +6 -5
  45. package/dist/trails.d.ts.map +1 -1
  46. package/dist/trailsClient.d.ts +12 -0
  47. package/dist/trailsClient.d.ts.map +1 -0
  48. package/dist/transactions.d.ts +8 -0
  49. package/dist/transactions.d.ts.map +1 -1
  50. package/dist/wallets.d.ts.map +1 -1
  51. package/dist/widget/components/AccountActionsDropdown.d.ts.map +1 -1
  52. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  53. package/dist/widget/components/AccountSettings.d.ts +7 -0
  54. package/dist/widget/components/AccountSettings.d.ts.map +1 -0
  55. package/dist/widget/components/ChainList.d.ts +0 -1
  56. package/dist/widget/components/ChainList.d.ts.map +1 -1
  57. package/dist/widget/components/ClassicSwap.d.ts +46 -0
  58. package/dist/widget/components/ClassicSwap.d.ts.map +1 -0
  59. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  60. package/dist/widget/components/ConnectedWallets.d.ts +9 -0
  61. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -0
  62. package/dist/widget/components/DebugMenu.d.ts.map +1 -1
  63. package/dist/widget/components/DebugScreensList.d.ts.map +1 -1
  64. package/dist/widget/components/DebugToast.d.ts +3 -0
  65. package/dist/widget/components/DebugToast.d.ts.map +1 -0
  66. package/dist/widget/components/Earn.d.ts.map +1 -1
  67. package/dist/widget/components/EarnPools.d.ts.map +1 -1
  68. package/dist/widget/components/Fund.d.ts +44 -0
  69. package/dist/widget/components/Fund.d.ts.map +1 -0
  70. package/dist/widget/components/Identicon.d.ts +9 -0
  71. package/dist/widget/components/Identicon.d.ts.map +1 -0
  72. package/dist/widget/components/Pay.d.ts +46 -0
  73. package/dist/widget/components/Pay.d.ts.map +1 -0
  74. package/dist/widget/components/Receive.d.ts.map +1 -1
  75. package/dist/widget/components/RecentTokens.d.ts.map +1 -1
  76. package/dist/widget/components/Recipients.d.ts +9 -0
  77. package/dist/widget/components/Recipients.d.ts.map +1 -0
  78. package/dist/widget/components/RefundWarning.d.ts +9 -0
  79. package/dist/widget/components/RefundWarning.d.ts.map +1 -0
  80. package/dist/widget/components/SimpleSwap.d.ts.map +1 -1
  81. package/dist/widget/components/Swap.d.ts.map +1 -1
  82. package/dist/widget/components/SwapSettings.d.ts +1 -5
  83. package/dist/widget/components/SwapSettings.d.ts.map +1 -1
  84. package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
  85. package/dist/widget/components/ThemeSyncer.d.ts +6 -0
  86. package/dist/widget/components/ThemeSyncer.d.ts.map +1 -0
  87. package/dist/widget/components/Toast.d.ts +24 -0
  88. package/dist/widget/components/Toast.d.ts.map +1 -0
  89. package/dist/widget/components/TokenList.d.ts.map +1 -1
  90. package/dist/widget/components/TransactionDetails.d.ts.map +1 -1
  91. package/dist/widget/components/TruncatedAddress.d.ts +2 -0
  92. package/dist/widget/components/TruncatedAddress.d.ts.map +1 -1
  93. package/dist/widget/components/UserPreferences.d.ts +7 -0
  94. package/dist/widget/components/UserPreferences.d.ts.map +1 -0
  95. package/dist/widget/hooks/useBalanceVisible.d.ts +1 -0
  96. package/dist/widget/hooks/useBalanceVisible.d.ts.map +1 -1
  97. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  98. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  99. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  100. package/dist/widget/hooks/useDebugScreens.d.ts +1 -1
  101. package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -1
  102. package/dist/widget/hooks/useDefaultTokenSelection.d.ts +54 -0
  103. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -0
  104. package/dist/widget/hooks/useIntentTransactionHistory.d.ts.map +1 -1
  105. package/dist/widget/hooks/usePayMessage.d.ts +34 -0
  106. package/dist/widget/hooks/usePayMessage.d.ts.map +1 -0
  107. package/dist/widget/hooks/useRecipients.d.ts +17 -0
  108. package/dist/widget/hooks/useRecipients.d.ts.map +1 -0
  109. package/dist/widget/hooks/useSelectedRecipient.d.ts +12 -0
  110. package/dist/widget/hooks/useSelectedRecipient.d.ts.map +1 -0
  111. package/dist/widget/hooks/useSendForm.d.ts +2 -0
  112. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  113. package/dist/widget/hooks/useSwapAmount.d.ts +13 -0
  114. package/dist/widget/hooks/useSwapAmount.d.ts.map +1 -0
  115. package/dist/widget/hooks/useSwapSettings.d.ts +16 -0
  116. package/dist/widget/hooks/useSwapSettings.d.ts.map +1 -0
  117. package/dist/widget/hooks/useTargetAmount.d.ts +5 -0
  118. package/dist/widget/hooks/useTargetAmount.d.ts.map +1 -0
  119. package/dist/widget/hooks/useTheme.d.ts +14 -0
  120. package/dist/widget/hooks/useTheme.d.ts.map +1 -0
  121. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  122. package/dist/widget/index.js +2 -2
  123. package/dist/widget/widget.d.ts +9 -0
  124. package/dist/widget/widget.d.ts.map +1 -1
  125. package/package.json +29 -28
  126. package/src/aave.ts +6 -1
  127. package/src/analytics.ts +103 -53
  128. package/src/apiClient.ts +1 -1
  129. package/src/{proxyCaller.ts → balanceInjector.ts} +22 -17
  130. package/src/cctp.ts +6 -2
  131. package/src/cctpqueue.ts +7 -7
  132. package/src/chains.ts +8 -0
  133. package/src/config.ts +40 -9
  134. package/src/constants.ts +11 -8
  135. package/src/contractUtils.ts +33 -2
  136. package/src/customChains.ts +24 -0
  137. package/src/index.ts +11 -1
  138. package/src/intentEntrypoint.ts +253 -0
  139. package/src/intents.ts +87 -54
  140. package/src/metaTxnMonitor.ts +1 -0
  141. package/src/morpho.ts +13 -2
  142. package/src/pools.ts +68 -86
  143. package/src/prepareSend.ts +437 -207
  144. package/src/prices.ts +51 -7
  145. package/src/relaySdk.ts +6 -4
  146. package/src/relayer.ts +2 -0
  147. package/src/toast.ts +110 -0
  148. package/src/tokenBalances.ts +112 -20
  149. package/src/tokens.ts +70 -7
  150. package/src/trails.ts +80 -77
  151. package/src/trailsClient.ts +45 -0
  152. package/src/transactions.ts +27 -35
  153. package/src/umd.tsx +1 -1
  154. package/src/wallets.ts +2 -1
  155. package/src/widget/assets/sequence-logo.svg +15 -0
  156. package/src/widget/compiled.css +2 -2
  157. package/src/widget/components/AccountActionsDropdown.tsx +18 -159
  158. package/src/widget/components/AccountIntentTransactionHistory.tsx +346 -63
  159. package/src/widget/components/AccountSettings.tsx +96 -0
  160. package/src/widget/components/ChainFilterDropdown.tsx +1 -1
  161. package/src/widget/components/ChainList.tsx +10 -20
  162. package/src/widget/components/ClassicSwap.tsx +923 -0
  163. package/src/widget/components/ConfigDisplay.tsx +8 -5
  164. package/src/widget/components/ConnectedWallets.tsx +260 -0
  165. package/src/widget/components/DebugMenu.tsx +2 -0
  166. package/src/widget/components/DebugScreensList.tsx +3 -0
  167. package/src/widget/components/DebugToast.tsx +63 -0
  168. package/src/widget/components/Earn.tsx +108 -116
  169. package/src/widget/components/EarnPools.tsx +2 -4
  170. package/src/widget/components/EarnPoolsFilters.tsx +6 -6
  171. package/src/widget/components/Fund.tsx +1245 -0
  172. package/src/widget/components/FundMethods.tsx +1 -1
  173. package/src/widget/components/FundSendForm.tsx +1 -1
  174. package/src/widget/components/Identicon.tsx +158 -0
  175. package/src/widget/components/Pay.tsx +1088 -0
  176. package/src/widget/components/PaySendForm.tsx +1 -1
  177. package/src/widget/components/QuoteDetails.tsx +1 -1
  178. package/src/widget/components/Receipt.tsx +1 -1
  179. package/src/widget/components/Receive.tsx +4 -2
  180. package/src/widget/components/RecentTokens.tsx +2 -1
  181. package/src/widget/components/Recipients.tsx +448 -0
  182. package/src/widget/components/RefundWarning.tsx +61 -0
  183. package/src/widget/components/ScreenHeader.tsx +1 -1
  184. package/src/widget/components/SimpleSwap.tsx +74 -58
  185. package/src/widget/components/Swap.tsx +35 -853
  186. package/src/widget/components/SwapSettings.tsx +5 -11
  187. package/src/widget/components/ThemeProvider.tsx +32 -0
  188. package/src/widget/components/ThemeSyncer.tsx +47 -0
  189. package/src/widget/components/Toast.tsx +315 -0
  190. package/src/widget/components/TokenList.tsx +2 -34
  191. package/src/widget/components/TokenSelector.tsx +3 -3
  192. package/src/widget/components/TransactionDetails.tsx +153 -13
  193. package/src/widget/components/TruncatedAddress.tsx +5 -1
  194. package/src/widget/components/UserPreferences.tsx +156 -0
  195. package/src/widget/components/WalletList.tsx +1 -1
  196. package/src/widget/hooks/useBalanceVisible.tsx +40 -2
  197. package/src/widget/hooks/useCheckout.ts +13 -0
  198. package/src/widget/hooks/useCurrentScreen.tsx +3 -0
  199. package/src/widget/hooks/useDebugScreens.ts +12 -2
  200. package/src/widget/hooks/useDefaultTokenSelection.tsx +475 -0
  201. package/src/widget/hooks/useIntentTransactionHistory.ts +212 -0
  202. package/src/widget/hooks/usePayMessage.tsx +370 -0
  203. package/src/widget/hooks/useRecipients.ts +168 -0
  204. package/src/widget/hooks/useSelectedRecipient.tsx +48 -0
  205. package/src/widget/hooks/useSendForm.ts +179 -26
  206. package/src/widget/hooks/useSwapAmount.tsx +50 -0
  207. package/src/widget/hooks/useSwapSettings.tsx +100 -0
  208. package/src/widget/hooks/useTargetAmount.ts +23 -0
  209. package/src/widget/hooks/useTheme.tsx +80 -0
  210. package/src/widget/hooks/useTokenList.ts +20 -11
  211. package/src/widget/index.css +45 -21
  212. package/src/widget/widget.tsx +164 -68
  213. package/dist/address.d.ts +0 -2
  214. package/dist/address.d.ts.map +0 -1
  215. package/dist/proxyCaller.d.ts.map +0 -1
  216. package/src/address.ts +0 -6
@@ -0,0 +1,1245 @@
1
+ import {
2
+ ChevronRight,
3
+ Search,
4
+ Loader2,
5
+ ChevronDown,
6
+ ArrowDown,
7
+ } from "lucide-react"
8
+ import { useEffect, useState, useMemo, useRef, useCallback } from "react"
9
+ import type React from "react"
10
+ import type { Account, WalletClient } from "viem"
11
+ import type { TransactionState } from "../../transactions.js"
12
+ import type { OnCompleteProps } from "../hooks/useSendForm.js"
13
+ import type { CheckoutOnHandlers } from "../hooks/useCheckout.js"
14
+ import { useSendForm } from "../hooks/useSendForm.js"
15
+ import { TradeType } from "../../prepareSend.js"
16
+ import { useOriginSelectedToken } from "../hooks/useOriginSelectedToken.js"
17
+ import { useDestinationSelectedToken } from "../hooks/useDestinationSelectedToken.js"
18
+ import { useSelectedRecipient } from "../hooks/useSelectedRecipient.js"
19
+ import { useSwapAmount } from "../hooks/useSwapAmount.js"
20
+ import { useMode } from "../hooks/useMode.js"
21
+ import { useBalanceVisible } from "../hooks/useBalanceVisible.js"
22
+ import { useCurrentScreen } from "../hooks/useCurrentScreen.js"
23
+ import { useDefaultTokenSelection } from "../hooks/useDefaultTokenSelection.js" // Context version
24
+ import { TokenImage } from "./TokenImage.js"
25
+ import { Identicon } from "./Identicon.js"
26
+ import { TokenSelector } from "./TokenSelector.js"
27
+ import { ScreenHeader } from "./ScreenHeader.js"
28
+ import { ChainList } from "./ChainList.js"
29
+ import { QuoteDetails } from "./QuoteDetails.js"
30
+ import { ErrorDisplay } from "./ErrorDisplay.js"
31
+ import { getChainInfo } from "../../chains.js"
32
+ import { formatUsdAmountDisplay } from "../../tokenBalances.js"
33
+ import { MINIMUM_USD_AMOUNT_FOR_SWAP } from "../../constants.js"
34
+ import { truncateAddress } from "../../utils.js"
35
+ import type { PrepareSendQuote } from "../../prepareSend.js"
36
+ import type { SupportedToken } from "../../tokens.js"
37
+ import { logger } from "../../logger.js"
38
+ import { RefundWarning } from "./RefundWarning.js"
39
+
40
+ interface FundProps {
41
+ onBack?: () => void
42
+ account: Account
43
+ walletClient: WalletClient
44
+ onTransactionStateChange: (transactionStates: TransactionState[]) => void
45
+ onError: (error: Error | string | null) => void
46
+ onWaitingForWalletConfirm: (props: PrepareSendQuote) => void
47
+ onConfirm: () => void
48
+ onComplete: (result: OnCompleteProps) => void
49
+ onSend: (amount: string, recipient: string) => void
50
+ paymasterUrls?: Array<{ chainId: number; url: string }>
51
+ gasless?: boolean
52
+ setWalletConfirmRetryHandler: (handler: () => Promise<void>) => void
53
+ quoteProvider?: string
54
+ fundMethod?: string
55
+ onNavigateToMeshConnect?: (
56
+ props: {
57
+ toTokenSymbol: string
58
+ toTokenAmount: string
59
+ toChainId: number
60
+ toRecipientAddress: string
61
+ },
62
+ quote?: PrepareSendQuote | null,
63
+ ) => void
64
+ checkoutOnHandlers?: CheckoutOnHandlers
65
+ recentTokens?: SupportedToken[]
66
+ onRecentTokenSelect?: (token: SupportedToken) => void
67
+ onTrackToken?: (token: any) => void
68
+ toRecipient?: string
69
+ toAmount?: string
70
+ toChainId?: number
71
+ toToken?: string
72
+ toCalldata?: string
73
+ }
74
+
75
+ export const Fund: React.FC<FundProps> = ({
76
+ onBack,
77
+ account,
78
+ walletClient,
79
+ onTransactionStateChange,
80
+ onError,
81
+ onWaitingForWalletConfirm,
82
+ onConfirm,
83
+ onComplete,
84
+ onSend,
85
+ paymasterUrls,
86
+ gasless,
87
+ setWalletConfirmRetryHandler,
88
+ quoteProvider,
89
+ fundMethod,
90
+ onNavigateToMeshConnect,
91
+ checkoutOnHandlers,
92
+ recentTokens,
93
+ onRecentTokenSelect,
94
+ onTrackToken,
95
+ toRecipient,
96
+ toChainId,
97
+ toToken,
98
+ toCalldata,
99
+ }) => {
100
+ const { mode } = useMode()
101
+ const { isBalanceVisible } = useBalanceVisible()
102
+ const { selectedToken: originToken, setSelectedToken: setOriginToken } =
103
+ useOriginSelectedToken()
104
+ const {
105
+ selectedToken: globalDestinationToken,
106
+ setSelectedToken: setDestinationToken,
107
+ } = useDestinationSelectedToken()
108
+ const { selectedRecipient, setSelectedRecipient } = useSelectedRecipient()
109
+ const { amount: globalAmount, setAmount: setGlobalAmount } = useSwapAmount()
110
+ const { setCurrentScreen } = useCurrentScreen()
111
+
112
+ // Use new default token selection hook
113
+ const {
114
+ defaultOriginToken,
115
+ defaultDestinationToken,
116
+ isLoading: isLoadingDefaults,
117
+ } = useDefaultTokenSelection()
118
+
119
+ // Local state for fund-specific functionality
120
+ const [isInputTypeUsd, setIsInputTypeUsd] = useState(false)
121
+ const [tokenAmountForBackend, setTokenAmountForBackend] = useState("")
122
+ const [inputDisplayValue, setInputDisplayValue] = useState("")
123
+ const [showOriginTokenSelector, setShowOriginTokenSelector] = useState(false)
124
+ const [showDestinationTokenSelector, setShowDestinationTokenSelector] =
125
+ useState(false)
126
+ const [showOriginChainList, setShowOriginChainList] = useState(false)
127
+ const [showDestinationChainList, setShowDestinationChainList] =
128
+ useState(false)
129
+ const inputRef = useRef<HTMLInputElement>(null)
130
+
131
+ // Use useSendForm for quote functionality
132
+ const {
133
+ amountUsdDisplay,
134
+ balanceFormatted,
135
+ isLoadingQuote,
136
+ prepareSendQuote,
137
+ setAmount: setSendFormAmount,
138
+ handleSubmit,
139
+ isSubmitting,
140
+ buttonText,
141
+ isValidRecipient,
142
+ selectedDestToken,
143
+ setSelectedDestToken,
144
+ setSelectedDestinationChain,
145
+ quoteError,
146
+ quoteErrorPrettified,
147
+ isSameTokenWithoutCustomCalldata,
148
+ destinationTokenAddress,
149
+ isValidCustomToken,
150
+ isSenderContractOnOrigin,
151
+ isSenderContractOnDestination,
152
+ } = useSendForm({
153
+ account,
154
+ toAmount: undefined, // Don't pass toAmount for fund form - user enters input amount
155
+ toRecipient: selectedRecipient || account.address,
156
+ toChainId,
157
+ toToken,
158
+ toCalldata,
159
+ walletClient,
160
+ onTransactionStateChange,
161
+ onError,
162
+ onWaitingForWalletConfirm,
163
+ paymasterUrls,
164
+ gasless,
165
+ onConfirm,
166
+ onComplete,
167
+ onSend,
168
+ selectedToken: originToken as any,
169
+ setWalletConfirmRetryHandler,
170
+ tradeType: TradeType.EXACT_INPUT,
171
+ quoteProvider,
172
+ fundMethod,
173
+ mode,
174
+ onNavigateToMeshConnect,
175
+ checkoutOnHandlers,
176
+ })
177
+
178
+ // Auto-select origin and destination tokens using new hook
179
+ useEffect(() => {
180
+ if (!originToken && !isLoadingDefaults && defaultOriginToken) {
181
+ logger.console.log(
182
+ "[trails-sdk] Auto-selecting origin token:",
183
+ defaultOriginToken,
184
+ )
185
+ setOriginToken(defaultOriginToken as any)
186
+ }
187
+ }, [originToken, isLoadingDefaults, defaultOriginToken, setOriginToken])
188
+
189
+ // Auto-select destination token using new hook
190
+ useEffect(() => {
191
+ if (
192
+ !globalDestinationToken &&
193
+ !toToken &&
194
+ originToken &&
195
+ !isLoadingDefaults &&
196
+ defaultDestinationToken
197
+ ) {
198
+ logger.console.log(
199
+ "[trails-sdk] Auto-selecting destination token:",
200
+ defaultDestinationToken,
201
+ )
202
+
203
+ const decimals =
204
+ defaultDestinationToken.contractInfo?.decimals ??
205
+ (defaultDestinationToken as any)?.decimals
206
+
207
+ setDestinationToken(defaultDestinationToken as any)
208
+ setSelectedDestToken({
209
+ symbol: defaultDestinationToken.symbol,
210
+ name: defaultDestinationToken.name,
211
+ imageUrl: defaultDestinationToken.imageUrl,
212
+ decimals,
213
+ })
214
+
215
+ // Also set the destination chain
216
+ if (setSelectedDestinationChain) {
217
+ const chainInfo = getChainInfo(defaultDestinationToken.chainId)
218
+ if (chainInfo) {
219
+ setSelectedDestinationChain(chainInfo)
220
+ }
221
+ }
222
+ }
223
+ }, [
224
+ originToken,
225
+ globalDestinationToken,
226
+ toToken,
227
+ isLoadingDefaults,
228
+ defaultDestinationToken,
229
+ setDestinationToken,
230
+ setSelectedDestToken,
231
+ setSelectedDestinationChain,
232
+ ])
233
+
234
+ // Initialize selected recipient from toRecipient prop or default to connected wallet
235
+ useEffect(() => {
236
+ if (toRecipient && !selectedRecipient) {
237
+ setSelectedRecipient(toRecipient)
238
+ } else if (!selectedRecipient && account?.address) {
239
+ // Default to connected wallet address if no recipient is set
240
+ setSelectedRecipient(account.address)
241
+ }
242
+ }, [toRecipient, selectedRecipient, setSelectedRecipient, account?.address])
243
+
244
+ // Sync global destination token with useSendForm
245
+ useEffect(() => {
246
+ if (globalDestinationToken && selectedDestToken && !toToken) {
247
+ const decimals =
248
+ globalDestinationToken.contractInfo?.decimals ??
249
+ (globalDestinationToken as any)?.decimals
250
+
251
+ // Convert global token format to useSendForm format
252
+ const formattedToken = {
253
+ symbol: globalDestinationToken.symbol,
254
+ name: globalDestinationToken.name,
255
+ imageUrl: globalDestinationToken.imageUrl,
256
+ decimals,
257
+ }
258
+
259
+ // Only update if the tokens are different (to avoid infinite loops)
260
+ if (selectedDestToken.symbol !== formattedToken.symbol) {
261
+ logger.console.log(
262
+ "[trails-sdk] Restoring destination token from global state:",
263
+ formattedToken,
264
+ )
265
+ setSelectedDestToken(formattedToken)
266
+
267
+ // Also update destination chain to match the selected token's chain
268
+ if (setSelectedDestinationChain && globalDestinationToken.chainId) {
269
+ const chainInfo = getChainInfo(globalDestinationToken.chainId)
270
+ if (chainInfo) {
271
+ setSelectedDestinationChain(chainInfo)
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }, [
277
+ globalDestinationToken,
278
+ selectedDestToken,
279
+ setSelectedDestToken,
280
+ setSelectedDestinationChain,
281
+ toToken,
282
+ ])
283
+
284
+ // Force restore global destination token on mount (runs after useSendForm initialization)
285
+ useEffect(() => {
286
+ if (globalDestinationToken && !toToken) {
287
+ const decimals =
288
+ globalDestinationToken.contractInfo?.decimals ??
289
+ (globalDestinationToken as any)?.decimals
290
+
291
+ const timeoutId = setTimeout(() => {
292
+ const formattedToken = {
293
+ symbol: globalDestinationToken.symbol,
294
+ name: globalDestinationToken.name,
295
+ imageUrl: globalDestinationToken.imageUrl,
296
+ decimals,
297
+ }
298
+ logger.console.log(
299
+ "[trails-sdk] Force restoring destination token on mount:",
300
+ formattedToken,
301
+ )
302
+ setSelectedDestToken(formattedToken)
303
+
304
+ if (setSelectedDestinationChain && globalDestinationToken.chainId) {
305
+ const chainInfo = getChainInfo(globalDestinationToken.chainId)
306
+ if (chainInfo) {
307
+ setSelectedDestinationChain(chainInfo)
308
+ }
309
+ }
310
+ }, 50)
311
+
312
+ return () => clearTimeout(timeoutId)
313
+ }
314
+ }, [
315
+ globalDestinationToken,
316
+ setSelectedDestToken,
317
+ setSelectedDestinationChain,
318
+ toToken,
319
+ ])
320
+
321
+ // Initialize local amount states from global amount
322
+ useEffect(() => {
323
+ if (globalAmount && !tokenAmountForBackend && !inputDisplayValue) {
324
+ logger.console.log(
325
+ "[trails-sdk] Restoring amount from global state:",
326
+ globalAmount,
327
+ )
328
+ setTokenAmountForBackend(globalAmount)
329
+ setInputDisplayValue(globalAmount)
330
+ setSendFormAmount(globalAmount)
331
+ }
332
+ }, [
333
+ globalAmount,
334
+ tokenAmountForBackend,
335
+ inputDisplayValue,
336
+ setSendFormAmount,
337
+ ])
338
+
339
+ // Debug logging for component mount
340
+ useEffect(() => {
341
+ logger.console.log("[trails-sdk] Fund component mounted/updated", {
342
+ originToken: originToken?.symbol,
343
+ originChainId: originToken?.chainId,
344
+ globalDestinationToken: globalDestinationToken?.symbol,
345
+ globalDestinationChainId: globalDestinationToken?.chainId,
346
+ selectedDestToken: selectedDestToken?.symbol,
347
+ toChainId,
348
+ globalAmount,
349
+ tokenAmountForBackend,
350
+ })
351
+ }, [
352
+ originToken,
353
+ globalDestinationToken,
354
+ selectedDestToken,
355
+ globalAmount,
356
+ tokenAmountForBackend,
357
+ toChainId,
358
+ ])
359
+
360
+ // Auto-focus input field on component mount
361
+ useEffect(() => {
362
+ const timer = setTimeout(() => {
363
+ if (inputRef.current) {
364
+ inputRef.current.focus()
365
+ }
366
+ }, 100)
367
+
368
+ return () => clearTimeout(timer)
369
+ }, [])
370
+
371
+ // Get source token price for USD conversions
372
+ const sourceTokenPrice = (originToken as any)?.tokenPriceUsd || 0
373
+
374
+ // Sync display value with token amount only when mode changes (not during typing)
375
+ const [lastInputMode, setLastInputMode] = useState(isInputTypeUsd)
376
+
377
+ useEffect(() => {
378
+ // Only sync when mode actually changes, not during normal typing
379
+ if (lastInputMode !== isInputTypeUsd && tokenAmountForBackend) {
380
+ const tokenAmount = parseFloat(tokenAmountForBackend) || 0
381
+ if (isInputTypeUsd && sourceTokenPrice > 0) {
382
+ // Show USD with max 2 decimals
383
+ const usdAmount = tokenAmount * sourceTokenPrice
384
+ setInputDisplayValue(Number(usdAmount.toFixed(2)).toString())
385
+ } else {
386
+ // Show token with max 8 decimals
387
+ setInputDisplayValue(Number(tokenAmount.toFixed(8)).toString())
388
+ }
389
+ setLastInputMode(isInputTypeUsd)
390
+ }
391
+ }, [isInputTypeUsd, sourceTokenPrice, tokenAmountForBackend, lastInputMode])
392
+
393
+ // Handle input amount changes with 8 decimal limit and 16 char total limit
394
+ const handleAmountChange = useCallback(
395
+ (value: string) => {
396
+ // Allow empty string
397
+ if (value === "") {
398
+ setInputDisplayValue("")
399
+ setTokenAmountForBackend("")
400
+ setSendFormAmount("")
401
+ setGlobalAmount("")
402
+ return
403
+ }
404
+
405
+ // Limit total length to 16 characters
406
+ if (value.length > 16) {
407
+ return
408
+ }
409
+
410
+ // Validate decimal places (max 8 decimals) and allow single decimal point
411
+ const decimalMatch = value.match(/^\d*\.?\d{0,8}$/)
412
+ if (!decimalMatch) {
413
+ return // Don't update if invalid format
414
+ }
415
+
416
+ // Store the display value
417
+ setInputDisplayValue(value)
418
+
419
+ // Update the token amount for backend and useSendForm
420
+ if (isInputTypeUsd && sourceTokenPrice > 0) {
421
+ const usdAmount = parseFloat(value) || 0
422
+ const tokenAmount = usdAmount / sourceTokenPrice
423
+ const tokenAmountStr = tokenAmount.toString()
424
+ setTokenAmountForBackend(tokenAmountStr)
425
+ setSendFormAmount(tokenAmountStr)
426
+ setGlobalAmount(tokenAmountStr)
427
+ } else {
428
+ setTokenAmountForBackend(value)
429
+ setSendFormAmount(value)
430
+ setGlobalAmount(value)
431
+ }
432
+ },
433
+ [setSendFormAmount, isInputTypeUsd, sourceTokenPrice, setGlobalAmount],
434
+ )
435
+
436
+ // Get display values based on input type
437
+ const displayAmount = useMemo(() => {
438
+ return inputDisplayValue
439
+ }, [inputDisplayValue])
440
+
441
+ const displayUsdValue = useMemo(() => {
442
+ if (isInputTypeUsd && sourceTokenPrice > 0) {
443
+ // Show token amount when in USD mode
444
+ const tokenAmount = parseFloat(tokenAmountForBackend) || 0
445
+ return `${tokenAmount.toFixed(8)} ${originToken?.symbol || "TOKEN"}`
446
+ }
447
+ return amountUsdDisplay || "$0.00"
448
+ }, [
449
+ tokenAmountForBackend,
450
+ isInputTypeUsd,
451
+ sourceTokenPrice,
452
+ originToken?.symbol,
453
+ amountUsdDisplay,
454
+ ])
455
+
456
+ // Handle percentage clicks for quick amounts
457
+ const handlePercentageClick = useCallback(
458
+ (percentage: number) => {
459
+ if (!balanceFormatted) {
460
+ return
461
+ }
462
+
463
+ const balance = parseFloat(balanceFormatted)
464
+ if (Number.isNaN(balance)) return
465
+
466
+ const amount = (balance * percentage) / 100
467
+ // Cap decimals to 8 places
468
+ const cappedAmount = parseFloat(amount.toFixed(8))
469
+ const tokenAmountStr = cappedAmount.toString()
470
+
471
+ // Update all states consistently
472
+ setTokenAmountForBackend(tokenAmountStr)
473
+ setSendFormAmount(tokenAmountStr)
474
+ setGlobalAmount(tokenAmountStr)
475
+
476
+ // Update display based on current mode
477
+ if (isInputTypeUsd && sourceTokenPrice > 0) {
478
+ const usdAmount = cappedAmount * sourceTokenPrice
479
+ setInputDisplayValue(Number(usdAmount.toFixed(2)).toString())
480
+ } else {
481
+ setInputDisplayValue(tokenAmountStr)
482
+ }
483
+ },
484
+ [
485
+ balanceFormatted,
486
+ setSendFormAmount,
487
+ isInputTypeUsd,
488
+ sourceTokenPrice,
489
+ setGlobalAmount,
490
+ ],
491
+ )
492
+
493
+ // Handle input type toggle (USD ↔ Token)
494
+ const handleInputTypeToggle = useCallback(() => {
495
+ // Use tokenAmountForBackend as the source of truth for conversion
496
+ const currentTokenAmount = parseFloat(tokenAmountForBackend) || 0
497
+
498
+ if (isInputTypeUsd && sourceTokenPrice > 0) {
499
+ // Switching from USD to token mode
500
+ // Display the token amount (limit to 8 decimals)
501
+ const tokenAmountStr = Number(currentTokenAmount.toFixed(8)).toString()
502
+ setInputDisplayValue(tokenAmountStr)
503
+ } else if (!isInputTypeUsd && sourceTokenPrice > 0) {
504
+ // Switching from token to USD mode
505
+ // Display USD amount (limit to 2 decimals)
506
+ const usdAmount = currentTokenAmount * sourceTokenPrice
507
+ const usdAmountStr = Number(usdAmount.toFixed(2)).toString()
508
+ setInputDisplayValue(usdAmountStr)
509
+ }
510
+
511
+ // Toggle the mode
512
+ setIsInputTypeUsd(!isInputTypeUsd)
513
+
514
+ // Focus the input field after toggling
515
+ setTimeout(() => {
516
+ if (inputRef.current) {
517
+ inputRef.current.focus()
518
+ // Select all text for easy replacement
519
+ inputRef.current.select()
520
+ }
521
+ }, 0)
522
+ }, [tokenAmountForBackend, isInputTypeUsd, sourceTokenPrice])
523
+
524
+ // Dynamic font size based on input length - matching Earn.tsx
525
+ const inputStyles = useMemo(() => {
526
+ const inputLength = displayAmount.length
527
+ let fontSize: string
528
+
529
+ if (inputLength > 12) {
530
+ fontSize = "0.875rem"
531
+ } else if (inputLength > 9) {
532
+ fontSize = "1rem"
533
+ } else if (inputLength > 6) {
534
+ fontSize = "1.125rem"
535
+ } else if (inputLength > 3) {
536
+ fontSize = "1.25rem"
537
+ } else {
538
+ fontSize = "1.5rem"
539
+ }
540
+
541
+ return {
542
+ fontSize,
543
+ transition: "all 0.1s ease-in-out",
544
+ }
545
+ }, [displayAmount.length])
546
+
547
+ const handleOriginTokenSelect = useCallback(
548
+ (token: any) => {
549
+ const formattedToken = {
550
+ ...token,
551
+ decimals: token.contractInfo?.decimals || token.decimals,
552
+ contractInfo: {
553
+ decimals: token.contractInfo?.decimals || token.decimals,
554
+ contractAddress: token.contractAddress,
555
+ symbol: token.symbol,
556
+ name: token.name,
557
+ },
558
+ } as any
559
+ setOriginToken(formattedToken)
560
+ logger.console.log("[trails-sdk] selected origin token", token)
561
+ setShowOriginTokenSelector(false)
562
+ // Track the token selection
563
+ onTrackToken?.(token)
564
+ },
565
+ [setOriginToken, onTrackToken],
566
+ )
567
+
568
+ const handleDestinationTokenSelect = useCallback(
569
+ (token: any) => {
570
+ const formattedToken = {
571
+ ...token,
572
+ decimals: token.contractInfo?.decimals || token.decimals,
573
+ contractInfo: {
574
+ decimals: token.contractInfo?.decimals || token.decimals,
575
+ contractAddress: token.contractAddress,
576
+ symbol: token.symbol,
577
+ name: token.name,
578
+ },
579
+ }
580
+
581
+ // Update both global destination token state and useSendForm state
582
+ setDestinationToken(formattedToken as any)
583
+ setSelectedDestToken(formattedToken as any)
584
+
585
+ // Update destination chain to match the selected token's chain
586
+ if (setSelectedDestinationChain && token.chainId) {
587
+ const chainInfo = getChainInfo(token.chainId)
588
+ if (chainInfo) {
589
+ setSelectedDestinationChain(chainInfo)
590
+ }
591
+ }
592
+
593
+ setShowDestinationTokenSelector(false)
594
+ logger.console.log("[trails-sdk] selected destination token", token)
595
+ // Track the token selection
596
+ onTrackToken?.(token)
597
+ },
598
+ [
599
+ setDestinationToken,
600
+ setSelectedDestToken,
601
+ setSelectedDestinationChain,
602
+ onTrackToken,
603
+ ],
604
+ )
605
+
606
+ // Show origin chain list screen
607
+ if (showOriginChainList) {
608
+ return (
609
+ <ChainList
610
+ onBack={() => {
611
+ setShowOriginChainList(false)
612
+ setShowOriginTokenSelector(true)
613
+ }}
614
+ />
615
+ )
616
+ }
617
+
618
+ // Show destination chain list screen
619
+ if (showDestinationChainList) {
620
+ return (
621
+ <ChainList
622
+ onBack={() => {
623
+ setShowDestinationChainList(false)
624
+ setShowDestinationTokenSelector(true)
625
+ }}
626
+ />
627
+ )
628
+ }
629
+
630
+ // Show origin token selector screen
631
+ if (showOriginTokenSelector) {
632
+ return (
633
+ <div className="space-y-2">
634
+ <ScreenHeader
635
+ onBack={() => setShowOriginTokenSelector(false)}
636
+ headerContent="Select From Token"
637
+ headerContentAlign="left"
638
+ showAccountActions={true}
639
+ />
640
+ <TokenSelector
641
+ onTokenSelect={handleOriginTokenSelect}
642
+ onError={onError}
643
+ fundMethod={fundMethod}
644
+ showContinueButton={false}
645
+ compactMode={false}
646
+ recentTokens={recentTokens}
647
+ onRecentTokenSelect={(token) => {
648
+ const formattedToken = {
649
+ ...token,
650
+ decimals: token.decimals,
651
+ contractInfo: {
652
+ decimals: token.decimals,
653
+ contractAddress: token.contractAddress,
654
+ symbol: token.symbol,
655
+ name: token.name,
656
+ },
657
+ } as any
658
+ setOriginToken(formattedToken)
659
+ setShowOriginTokenSelector(false)
660
+ onRecentTokenSelect?.(token)
661
+ }}
662
+ allSupportedTokens={false}
663
+ chainListScreen={true}
664
+ onNavigateToChainList={() => {
665
+ setShowOriginTokenSelector(false)
666
+ setShowOriginChainList(true)
667
+ }}
668
+ />
669
+ </div>
670
+ )
671
+ }
672
+
673
+ // Show destination token selector screen
674
+ if (showDestinationTokenSelector) {
675
+ return (
676
+ <div className="space-y-2">
677
+ <ScreenHeader
678
+ onBack={() => setShowDestinationTokenSelector(false)}
679
+ headerContent="Select To Token"
680
+ headerContentAlign="left"
681
+ showAccountActions={true}
682
+ />
683
+ <TokenSelector
684
+ onTokenSelect={handleDestinationTokenSelect}
685
+ onError={onError}
686
+ fundMethod={fundMethod}
687
+ showContinueButton={false}
688
+ compactMode={false}
689
+ recentTokens={recentTokens}
690
+ onRecentTokenSelect={(token) => {
691
+ const formattedToken = {
692
+ ...token,
693
+ decimals: token.decimals,
694
+ contractInfo: {
695
+ decimals: token.decimals,
696
+ contractAddress: token.contractAddress,
697
+ symbol: token.symbol,
698
+ name: token.name,
699
+ },
700
+ } as any
701
+ setDestinationToken(formattedToken)
702
+ setSelectedDestToken(formattedToken)
703
+ setShowDestinationTokenSelector(false)
704
+ onRecentTokenSelect?.(token)
705
+ }}
706
+ allSupportedTokens={true}
707
+ chainListScreen={true}
708
+ onNavigateToChainList={() => {
709
+ setShowDestinationTokenSelector(false)
710
+ setShowDestinationChainList(true)
711
+ }}
712
+ />
713
+ </div>
714
+ )
715
+ }
716
+
717
+ if (!originToken) {
718
+ return (
719
+ <div className="space-y-2">
720
+ <ScreenHeader
721
+ onBack={onBack}
722
+ headerContent="Fund"
723
+ headerContentAlign="left"
724
+ showAccountActions={true}
725
+ />
726
+
727
+ <div className="space-y-2">
728
+ {/* Token Selector */}
729
+ <div className="relative">
730
+ <button
731
+ type="button"
732
+ onClick={() => setShowOriginTokenSelector(true)}
733
+ className="w-full flex items-center justify-between space-x-3 trails-bg-secondary trails-hover-bg trails-border-radius-list-button px-4 py-2 transition-all duration-200 cursor-pointer"
734
+ >
735
+ <div className="flex items-center space-x-3 flex-1">
736
+ <div className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
737
+ <Search className="w-4 h-4 text-gray-400" />
738
+ </div>
739
+ <div className="text-left flex-1">
740
+ <div className="font-medium text-gray-900 dark:text-white text-sm">
741
+ From token
742
+ </div>
743
+ <div className="text-xs trails-text-muted">Select token</div>
744
+ </div>
745
+ </div>
746
+ <ChevronRight className="w-4 h-4 trails-text-muted flex-shrink-0" />
747
+ </button>
748
+ </div>
749
+ </div>
750
+ </div>
751
+ )
752
+ }
753
+
754
+ return (
755
+ <div className="space-y-1">
756
+ <ScreenHeader
757
+ onBack={onBack}
758
+ headerContent="Fund"
759
+ headerContentAlign="left"
760
+ showAccountActions={true}
761
+ />
762
+
763
+ <div className="space-y-1">
764
+ {/* Origin Amount Input Section */}
765
+ <div className="trails-bg-secondary trails-bg-secondary-hover trails-border-radius-container p-3 group transition-all duration-200 border border-transparent focus-within:!bg-white dark:focus-within:!bg-gray-800 focus-within:border-gray-400 dark:focus-within:border-gray-500">
766
+ {/* Deposit Label and Percentage Buttons */}
767
+ <div className="flex justify-between items-center mb-2">
768
+ <div className="text-sm font-medium trails-text-secondary text-left">
769
+ Deposit
770
+ </div>
771
+
772
+ {/* Percentage Buttons */}
773
+ {originToken && (
774
+ <div className="flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
775
+ <button
776
+ type="button"
777
+ onClick={() => handlePercentageClick(25)}
778
+ 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"
779
+ >
780
+ 25%
781
+ </button>
782
+ <button
783
+ type="button"
784
+ onClick={() => handlePercentageClick(50)}
785
+ 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"
786
+ >
787
+ 50%
788
+ </button>
789
+ <button
790
+ type="button"
791
+ onClick={() => handlePercentageClick(75)}
792
+ 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"
793
+ >
794
+ 75%
795
+ </button>
796
+ <button
797
+ type="button"
798
+ onClick={() => handlePercentageClick(100)}
799
+ 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"
800
+ >
801
+ Max
802
+ </button>
803
+ </div>
804
+ )}
805
+ </div>
806
+
807
+ <div className="flex items-center space-x-2">
808
+ {/* Amount Input */}
809
+ <div className="flex-1">
810
+ <div className="flex items-center justify-start">
811
+ <div className="flex items-center">
812
+ <input
813
+ ref={inputRef}
814
+ type="text"
815
+ value={displayAmount}
816
+ onChange={(e) => handleAmountChange(e.target.value)}
817
+ placeholder="0"
818
+ className={`bg-transparent border-none outline-none font-bold text-left trails-text-primary placeholder-trails-text-primary ${
819
+ isLoadingQuote ? "animate-pulse" : ""
820
+ }`}
821
+ style={{
822
+ fontSize: inputStyles.fontSize,
823
+ width: `${Math.max((displayAmount || "0").length, 1)}ch`,
824
+ minWidth: "1ch",
825
+ maxWidth: "270px",
826
+ padding: "0",
827
+ margin: "0",
828
+ transition: "all 0.1s ease-in-out",
829
+ }}
830
+ inputMode="decimal"
831
+ />
832
+ <span
833
+ className="font-bold text-gray-400 dark:text-gray-500"
834
+ style={{
835
+ fontSize: inputStyles.fontSize,
836
+ marginLeft: "0.1em",
837
+ padding: "0",
838
+ transition: "all 0.2s ease-in-out",
839
+ }}
840
+ >
841
+ {isInputTypeUsd
842
+ ? "USD"
843
+ : originToken?.symbol.slice(0, 4) || ""}
844
+ </span>
845
+ {isLoadingQuote && (
846
+ <div className="ml-2 animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
847
+ )}
848
+ </div>
849
+ </div>
850
+ </div>
851
+
852
+ {/* Token Selection Button */}
853
+ <button
854
+ type="button"
855
+ onClick={() => setShowOriginTokenSelector(true)}
856
+ 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"
857
+ >
858
+ {originToken ? (
859
+ <>
860
+ <TokenImage
861
+ symbol={originToken.symbol}
862
+ imageUrl={originToken.imageUrl}
863
+ chainId={originToken.chainId}
864
+ size={20}
865
+ />
866
+ <span className="font-medium trails-text-primary text-sm">
867
+ {originToken.symbol}
868
+ </span>
869
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
870
+ </>
871
+ ) : (
872
+ <>
873
+ <span className="font-medium trails-text-muted text-sm">
874
+ Select Token
875
+ </span>
876
+ <ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
877
+ </>
878
+ )}
879
+ </button>
880
+ </div>
881
+
882
+ {/* Bottom Info Row */}
883
+ <div className="mt-2 flex justify-between items-center">
884
+ {/* USD Amount */}
885
+ <div className="text-xs trails-text-muted">
886
+ {originToken?.symbol && displayAmount ? (
887
+ <>≈ {amountUsdDisplay || "$0.00"}</>
888
+ ) : (
889
+ <span>&nbsp;</span>
890
+ )}
891
+ </div>
892
+
893
+ {/* Origin Token Balance */}
894
+ <div className="text-xs trails-text-muted text-right">
895
+ {originToken ? (
896
+ <button
897
+ type="button"
898
+ className="text-xs trails-text-muted cursor-pointer hover:trails-hover-text transition-colors bg-transparent border-none p-0"
899
+ onClick={() => handlePercentageClick(100)}
900
+ onKeyDown={(e) => {
901
+ if (e.key === "Enter" || e.key === " ") {
902
+ e.preventDefault()
903
+ handlePercentageClick(100)
904
+ }
905
+ }}
906
+ title="Click to use full balance"
907
+ >
908
+ Balance:{" "}
909
+ {isBalanceVisible
910
+ ? `${balanceFormatted || "0.00"} ${originToken.symbol}`
911
+ : "••••••"}
912
+ </button>
913
+ ) : (
914
+ <span>&nbsp;</span>
915
+ )}
916
+ </div>
917
+ </div>
918
+ </div>
919
+
920
+ {/* USD Value Toggle */}
921
+ <div
922
+ className="flex items-center justify-start"
923
+ style={{ display: "none" }}
924
+ >
925
+ <button
926
+ type="button"
927
+ onClick={handleInputTypeToggle}
928
+ className="flex items-center justify-start gap-2 px-3 py-1.5 rounded-md transition-colors cursor-pointer text-blue-600 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-blue-700"
929
+ style={{ color: "#155DFC" }}
930
+ >
931
+ <svg
932
+ width="20"
933
+ height="21"
934
+ viewBox="0 0 20 21"
935
+ fill="none"
936
+ xmlns="http://www.w3.org/2000/svg"
937
+ aria-label="Toggle between USD and token amount"
938
+ >
939
+ <title>Toggle between USD and token amount</title>
940
+ <path
941
+ d="M11.3879 14.7175V10.4181C11.3879 10.2209 11.4547 10.0554 11.5884 9.9217C11.7216 9.78848 11.8868 9.72187 12.0841 9.72187C12.2814 9.72187 12.4469 9.78848 12.5805 9.9217C12.7138 10.0554 12.7804 10.2209 12.7804 10.4181V14.7175L14.0859 13.412C14.2135 13.2844 14.3729 13.2175 14.5642 13.2115C14.7559 13.2059 14.9214 13.2728 15.0606 13.412C15.1999 13.5397 15.2725 13.6991 15.2785 13.8904C15.2841 14.0821 15.2173 14.2475 15.078 14.3868L12.5715 16.8933C12.5019 16.963 12.4264 17.0122 12.3452 17.0409C12.264 17.0702 12.1769 17.0848 12.0841 17.0848C11.9913 17.0848 11.9042 17.0702 11.823 17.0409C11.7418 17.0122 11.6664 16.963 11.5967 16.8933L9.09021 14.3868C8.96256 14.2592 8.89874 14.0997 8.89874 13.9085C8.89874 13.7168 8.96836 13.5513 9.10761 13.412C9.24687 13.2844 9.40933 13.2175 9.59499 13.2115C9.78066 13.2059 9.94312 13.2728 10.0824 13.412L11.3879 14.7175ZM7.21031 6.11874L5.90483 7.42422C5.75397 7.57508 5.58571 7.6447 5.40004 7.6331C5.21437 7.6215 5.05772 7.55187 4.93007 7.42422C4.79082 7.28497 4.72119 7.11949 4.72119 6.92779C4.72119 6.73655 4.78501 6.57711 4.91266 6.44946L7.41919 3.94294C7.48882 3.87331 7.56424 3.82388 7.64547 3.79463C7.7267 3.76585 7.81374 3.75146 7.90657 3.75146C7.9994 3.75146 8.08644 3.76585 8.16767 3.79463C8.2489 3.82388 8.32432 3.87331 8.39395 3.94294L10.9005 6.44946C11.0281 6.57711 11.0919 6.73957 11.0919 6.93684C11.0919 7.13412 11.0281 7.29658 10.9005 7.42422C10.7612 7.56347 10.596 7.6331 10.4047 7.6331C10.213 7.6331 10.0476 7.56347 9.90831 7.42422L8.60283 6.11874V10.4181C8.60283 10.6154 8.53622 10.7806 8.403 10.9139C8.26932 11.0475 8.10384 11.1144 7.90657 11.1144C7.7093 11.1144 7.54405 11.0475 7.41083 10.9139C7.27715 10.7806 7.21031 10.6154 7.21031 10.4181V6.11874Z"
942
+ fill="#155DFC"
943
+ />
944
+ </svg>
945
+ <div
946
+ className="text-sm font-extrabold leading-[130%]"
947
+ style={{ color: "#155DFC" }}
948
+ >
949
+ {isBalanceVisible ? displayUsdValue : "••••••"}
950
+ </div>
951
+ </button>
952
+ </div>
953
+
954
+ {/* Arrow Down Between Sections */}
955
+ <div className="relative">
956
+ <div 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 transition-colors border-2 border-white dark:border-gray-800 z-1">
957
+ <ArrowDown
958
+ className="w-5 h-5 text-gray-900 dark:text-white"
959
+ strokeWidth={2.5}
960
+ />
961
+ </div>
962
+ </div>
963
+ </div>
964
+
965
+ {/* Destination Section */}
966
+ <div className="space-y-1 trails-bg-secondary trails-border-radius-container p-3">
967
+ {/* Recipient Button */}
968
+ <div className="relative">
969
+ {toRecipient ? (
970
+ /* Display only - recipient is fixed */
971
+ <div className="w-full flex items-center justify-between space-x-3 trails-bg-secondary trails-border-radius-list-button px-4 py-2">
972
+ <div className="text-left">
973
+ <div className="font-medium trails-text-primary text-sm">
974
+ Recipient address
975
+ </div>
976
+ </div>
977
+ <div className="flex items-center space-x-2">
978
+ <div
979
+ className="font-medium trails-text-primary text-sm"
980
+ title={toRecipient}
981
+ >
982
+ {truncateAddress(toRecipient)}
983
+ </div>
984
+ <Identicon
985
+ value={toRecipient}
986
+ size={24}
987
+ className="flex-shrink-0"
988
+ />
989
+ </div>
990
+ </div>
991
+ ) : (
992
+ /* Interactive button - user can select recipient */
993
+ <button
994
+ type="button"
995
+ onClick={() => setCurrentScreen("recipients")}
996
+ className="w-full flex items-center justify-between space-x-3 hover:trails-hover-bg hover:bg-gray-50 dark:hover:bg-gray-700 trails-border-radius-list-button px-4 py-2 transition-all duration-200 cursor-pointer"
997
+ >
998
+ <div className="text-left">
999
+ <div className="font-medium trails-text-primary text-sm">
1000
+ Recipient address
1001
+ </div>
1002
+ </div>
1003
+ <div className="flex items-center space-x-2">
1004
+ {selectedRecipient ? (
1005
+ <>
1006
+ <div
1007
+ className="font-medium trails-text-primary text-sm"
1008
+ title={selectedRecipient}
1009
+ >
1010
+ {truncateAddress(selectedRecipient)}
1011
+ </div>
1012
+ <Identicon
1013
+ value={selectedRecipient}
1014
+ size={24}
1015
+ className="flex-shrink-0"
1016
+ />
1017
+ </>
1018
+ ) : (
1019
+ <>
1020
+ <div className="font-medium text-gray-900 dark:text-white text-sm">
1021
+ Select address
1022
+ </div>
1023
+ <div className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
1024
+ <div className="w-4 h-4 rounded-full bg-gray-400 dark:bg-gray-500"></div>
1025
+ </div>
1026
+ </>
1027
+ )}
1028
+ <ChevronRight className="w-4 h-4 trails-text-muted flex-shrink-0" />
1029
+ </div>
1030
+ </button>
1031
+ )}
1032
+ </div>
1033
+
1034
+ {/* Destination Token Selector */}
1035
+ <div className="relative">
1036
+ {toToken ? (
1037
+ /* Display only - destination token is fixed */
1038
+ <div className="w-full flex items-center justify-between space-x-3 trails-bg-secondary trails-border-radius-list-button px-4 py-2">
1039
+ <div className="text-left">
1040
+ <div className="font-medium trails-text-primary text-sm whitespace-nowrap">
1041
+ Receiving amount
1042
+ </div>
1043
+ </div>
1044
+ <div className="flex items-center space-x-2">
1045
+ {selectedDestToken ? (
1046
+ <>
1047
+ <div className="font-medium trails-text-primary text-sm">
1048
+ {isLoadingQuote ? (
1049
+ <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
1050
+ ) : prepareSendQuote?.destinationAmountFormatted ? (
1051
+ <span className="whitespace-nowrap">
1052
+ {prepareSendQuote.destinationAmountFormatted}{" "}
1053
+ {selectedDestToken.symbol}
1054
+ </span>
1055
+ ) : (
1056
+ ""
1057
+ )}
1058
+ </div>
1059
+ <TokenImage
1060
+ symbol={selectedDestToken.symbol}
1061
+ imageUrl={selectedDestToken.imageUrl}
1062
+ chainId={globalDestinationToken?.chainId || toChainId}
1063
+ size={24}
1064
+ />
1065
+ </>
1066
+ ) : (
1067
+ <>
1068
+ <div className="font-medium text-gray-900 dark:text-white text-sm">
1069
+ Select token
1070
+ </div>
1071
+ <div className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
1072
+ <Search className="w-4 h-4 text-gray-400" />
1073
+ </div>
1074
+ </>
1075
+ )}
1076
+ </div>
1077
+ </div>
1078
+ ) : (
1079
+ /* Interactive button - user can select destination token */
1080
+ <button
1081
+ type="button"
1082
+ onClick={() => setShowDestinationTokenSelector(true)}
1083
+ className="w-full flex items-center justify-between space-x-3 hover:trails-hover-bg hover:bg-gray-50 dark:hover:bg-gray-700 trails-border-radius-list-button px-4 py-2 transition-all duration-200 cursor-pointer"
1084
+ >
1085
+ <div className="text-left">
1086
+ <div className="font-medium trails-text-primary text-sm whitespace-nowrap">
1087
+ Receiving amount
1088
+ </div>
1089
+ </div>
1090
+
1091
+ <div className="flex items-center space-x-2">
1092
+ {selectedDestToken ? (
1093
+ <>
1094
+ <div className="font-medium trails-text-primary text-sm">
1095
+ {isLoadingQuote ? (
1096
+ <div className="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
1097
+ ) : prepareSendQuote?.destinationAmountFormatted ? (
1098
+ <span className="whitespace-nowrap">
1099
+ {prepareSendQuote.destinationAmountFormatted}{" "}
1100
+ {selectedDestToken.symbol}
1101
+ </span>
1102
+ ) : (
1103
+ ""
1104
+ )}
1105
+ </div>
1106
+ <TokenImage
1107
+ symbol={selectedDestToken.symbol}
1108
+ imageUrl={selectedDestToken.imageUrl}
1109
+ chainId={globalDestinationToken?.chainId || toChainId}
1110
+ size={24}
1111
+ />
1112
+ </>
1113
+ ) : (
1114
+ <>
1115
+ <div className="font-medium text-gray-900 dark:text-white text-sm">
1116
+ Select token
1117
+ </div>
1118
+ <div className="w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
1119
+ <Search className="w-4 h-4 text-gray-400" />
1120
+ </div>
1121
+ </>
1122
+ )}
1123
+ <ChevronRight className="w-4 h-4 trails-text-muted flex-shrink-0" />
1124
+ </div>
1125
+ </button>
1126
+ )}
1127
+ </div>
1128
+ </div>
1129
+
1130
+ <div className="py-2"></div>
1131
+
1132
+ {/* Exchange/Contract Warning */}
1133
+ <RefundWarning
1134
+ fundMethod={fundMethod}
1135
+ isSenderContractOnOrigin={isSenderContractOnOrigin}
1136
+ isSenderContractOnDestination={isSenderContractOnDestination}
1137
+ />
1138
+
1139
+ {/* Error Display */}
1140
+
1141
+ <ErrorDisplay
1142
+ errorPrettified={quoteErrorPrettified}
1143
+ error={quoteError}
1144
+ severity="warning"
1145
+ />
1146
+
1147
+ {prepareSendQuote?.noSufficientBalance ? (
1148
+ <div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
1149
+ <div className="flex items-center space-x-2">
1150
+ <svg
1151
+ className="w-4 h-4 text-amber-500 flex-shrink-0"
1152
+ fill="none"
1153
+ stroke="currentColor"
1154
+ viewBox="0 0 24 24"
1155
+ aria-hidden="true"
1156
+ >
1157
+ <path
1158
+ strokeLinecap="round"
1159
+ strokeLinejoin="round"
1160
+ strokeWidth={2}
1161
+ 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"
1162
+ />
1163
+ </svg>
1164
+ <p className="text-sm text-amber-600 dark:text-amber-400">
1165
+ Insufficient balance to complete this transaction
1166
+ </p>
1167
+ </div>
1168
+ </div>
1169
+ ) : prepareSendQuote?.minimumNotMet ? (
1170
+ <div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
1171
+ <div className="flex items-center space-x-2">
1172
+ <svg
1173
+ className="w-4 h-4 text-amber-500 flex-shrink-0"
1174
+ fill="none"
1175
+ stroke="currentColor"
1176
+ viewBox="0 0 24 24"
1177
+ aria-hidden="true"
1178
+ >
1179
+ <path
1180
+ strokeLinecap="round"
1181
+ strokeLinejoin="round"
1182
+ strokeWidth={2}
1183
+ 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"
1184
+ />
1185
+ </svg>
1186
+ <p className="text-sm text-amber-600 dark:text-amber-400">
1187
+ Please enter an amount above{" "}
1188
+ {formatUsdAmountDisplay(MINIMUM_USD_AMOUNT_FOR_SWAP)} otherwise
1189
+ transfer may fail
1190
+ </p>
1191
+ </div>
1192
+ </div>
1193
+ ) : null}
1194
+
1195
+ {/* Fund Button */}
1196
+ <form onSubmit={handleSubmit}>
1197
+ <button
1198
+ type="submit"
1199
+ disabled={
1200
+ !tokenAmountForBackend ||
1201
+ parseFloat(tokenAmountForBackend) <= 0 ||
1202
+ isSubmitting ||
1203
+ isLoadingQuote ||
1204
+ !isValidRecipient ||
1205
+ !destinationTokenAddress ||
1206
+ !isValidCustomToken ||
1207
+ prepareSendQuote?.noSufficientBalance ||
1208
+ isSameTokenWithoutCustomCalldata ||
1209
+ !prepareSendQuote
1210
+ }
1211
+ 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"
1212
+ >
1213
+ {isSubmitting ? (
1214
+ <div className="flex items-center justify-center">
1215
+ <Loader2 className="w-5 h-5 animate-spin mr-2" />
1216
+ <span>{buttonText}</span>
1217
+ </div>
1218
+ ) : isSameTokenWithoutCustomCalldata ? (
1219
+ "Select Different Tokens"
1220
+ ) : prepareSendQuote?.noSufficientBalance ? (
1221
+ "Insufficient Balance"
1222
+ ) : !selectedRecipient ? (
1223
+ "Select recipient address"
1224
+ ) : !selectedDestToken ? (
1225
+ "Select destination token"
1226
+ ) : !tokenAmountForBackend ||
1227
+ parseFloat(tokenAmountForBackend) <= 0 ? (
1228
+ "Enter an amount"
1229
+ ) : (
1230
+ buttonText || "Fund"
1231
+ )}
1232
+ </button>
1233
+ </form>
1234
+
1235
+ {/* Quote Details */}
1236
+ {prepareSendQuote && (
1237
+ <div className="space-y-2">
1238
+ <QuoteDetails quote={prepareSendQuote} showContent={true} />
1239
+ </div>
1240
+ )}
1241
+ </div>
1242
+ )
1243
+ }
1244
+
1245
+ export default Fund