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.
- package/dist/aave.d.ts.map +1 -1
- package/dist/analytics.d.ts +11 -2
- package/dist/analytics.d.ts.map +1 -1
- package/dist/apiClient.d.ts +1 -1
- package/dist/apiClient.d.ts.map +1 -1
- package/dist/{proxyCaller.d.ts → balanceInjector.d.ts} +5 -4
- package/dist/balanceInjector.d.ts.map +1 -0
- package/dist/{ccip-D3gTQONK.js → ccip-D6ToCrWc.js} +12 -12
- package/dist/cctp.d.ts.map +1 -1
- package/dist/cctpqueue.d.ts +3 -3
- package/dist/cctpqueue.d.ts.map +1 -1
- package/dist/chains.d.ts.map +1 -1
- package/dist/config.d.ts +17 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/constants.d.ts +5 -4
- package/dist/constants.d.ts.map +1 -1
- package/dist/contractUtils.d.ts +2 -0
- package/dist/contractUtils.d.ts.map +1 -1
- package/dist/customChains.d.ts +24 -0
- package/dist/customChains.d.ts.map +1 -0
- package/dist/{index-CnUM7lKf.js → index-BqgeTLL8.js} +34072 -30146
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +411 -400
- package/dist/intentEntrypoint.d.ts +96 -0
- package/dist/intentEntrypoint.d.ts.map +1 -0
- package/dist/intents.d.ts +5 -3
- package/dist/intents.d.ts.map +1 -1
- package/dist/metaTxnMonitor.d.ts.map +1 -1
- package/dist/morpho.d.ts.map +1 -1
- package/dist/pools.d.ts +3 -1
- package/dist/pools.d.ts.map +1 -1
- package/dist/prepareSend.d.ts +8 -2
- package/dist/prepareSend.d.ts.map +1 -1
- package/dist/prices.d.ts +1 -1
- package/dist/prices.d.ts.map +1 -1
- package/dist/relaySdk.d.ts.map +1 -1
- package/dist/relayer.d.ts.map +1 -1
- package/dist/toast.d.ts +9 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/tokenBalances.d.ts +6 -2
- package/dist/tokenBalances.d.ts.map +1 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/trails.d.ts +6 -5
- package/dist/trails.d.ts.map +1 -1
- package/dist/trailsClient.d.ts +12 -0
- package/dist/trailsClient.d.ts.map +1 -0
- package/dist/transactions.d.ts +8 -0
- package/dist/transactions.d.ts.map +1 -1
- package/dist/wallets.d.ts.map +1 -1
- package/dist/widget/components/AccountActionsDropdown.d.ts.map +1 -1
- package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
- package/dist/widget/components/AccountSettings.d.ts +7 -0
- package/dist/widget/components/AccountSettings.d.ts.map +1 -0
- package/dist/widget/components/ChainList.d.ts +0 -1
- package/dist/widget/components/ChainList.d.ts.map +1 -1
- package/dist/widget/components/ClassicSwap.d.ts +46 -0
- package/dist/widget/components/ClassicSwap.d.ts.map +1 -0
- package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
- package/dist/widget/components/ConnectedWallets.d.ts +9 -0
- package/dist/widget/components/ConnectedWallets.d.ts.map +1 -0
- package/dist/widget/components/DebugMenu.d.ts.map +1 -1
- package/dist/widget/components/DebugScreensList.d.ts.map +1 -1
- package/dist/widget/components/DebugToast.d.ts +3 -0
- package/dist/widget/components/DebugToast.d.ts.map +1 -0
- package/dist/widget/components/Earn.d.ts.map +1 -1
- package/dist/widget/components/EarnPools.d.ts.map +1 -1
- package/dist/widget/components/Fund.d.ts +44 -0
- package/dist/widget/components/Fund.d.ts.map +1 -0
- package/dist/widget/components/Identicon.d.ts +9 -0
- package/dist/widget/components/Identicon.d.ts.map +1 -0
- package/dist/widget/components/Pay.d.ts +46 -0
- package/dist/widget/components/Pay.d.ts.map +1 -0
- package/dist/widget/components/Receive.d.ts.map +1 -1
- package/dist/widget/components/RecentTokens.d.ts.map +1 -1
- package/dist/widget/components/Recipients.d.ts +9 -0
- package/dist/widget/components/Recipients.d.ts.map +1 -0
- package/dist/widget/components/RefundWarning.d.ts +9 -0
- package/dist/widget/components/RefundWarning.d.ts.map +1 -0
- package/dist/widget/components/SimpleSwap.d.ts.map +1 -1
- package/dist/widget/components/Swap.d.ts.map +1 -1
- package/dist/widget/components/SwapSettings.d.ts +1 -5
- package/dist/widget/components/SwapSettings.d.ts.map +1 -1
- package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
- package/dist/widget/components/ThemeSyncer.d.ts +6 -0
- package/dist/widget/components/ThemeSyncer.d.ts.map +1 -0
- package/dist/widget/components/Toast.d.ts +24 -0
- package/dist/widget/components/Toast.d.ts.map +1 -0
- package/dist/widget/components/TokenList.d.ts.map +1 -1
- package/dist/widget/components/TransactionDetails.d.ts.map +1 -1
- package/dist/widget/components/TruncatedAddress.d.ts +2 -0
- package/dist/widget/components/TruncatedAddress.d.ts.map +1 -1
- package/dist/widget/components/UserPreferences.d.ts +7 -0
- package/dist/widget/components/UserPreferences.d.ts.map +1 -0
- package/dist/widget/hooks/useBalanceVisible.d.ts +1 -0
- package/dist/widget/hooks/useBalanceVisible.d.ts.map +1 -1
- package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
- package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
- package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
- package/dist/widget/hooks/useDebugScreens.d.ts +1 -1
- package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -1
- package/dist/widget/hooks/useDefaultTokenSelection.d.ts +54 -0
- package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -0
- package/dist/widget/hooks/useIntentTransactionHistory.d.ts.map +1 -1
- package/dist/widget/hooks/usePayMessage.d.ts +34 -0
- package/dist/widget/hooks/usePayMessage.d.ts.map +1 -0
- package/dist/widget/hooks/useRecipients.d.ts +17 -0
- package/dist/widget/hooks/useRecipients.d.ts.map +1 -0
- package/dist/widget/hooks/useSelectedRecipient.d.ts +12 -0
- package/dist/widget/hooks/useSelectedRecipient.d.ts.map +1 -0
- package/dist/widget/hooks/useSendForm.d.ts +2 -0
- package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
- package/dist/widget/hooks/useSwapAmount.d.ts +13 -0
- package/dist/widget/hooks/useSwapAmount.d.ts.map +1 -0
- package/dist/widget/hooks/useSwapSettings.d.ts +16 -0
- package/dist/widget/hooks/useSwapSettings.d.ts.map +1 -0
- package/dist/widget/hooks/useTargetAmount.d.ts +5 -0
- package/dist/widget/hooks/useTargetAmount.d.ts.map +1 -0
- package/dist/widget/hooks/useTheme.d.ts +14 -0
- package/dist/widget/hooks/useTheme.d.ts.map +1 -0
- package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
- package/dist/widget/index.js +2 -2
- package/dist/widget/widget.d.ts +9 -0
- package/dist/widget/widget.d.ts.map +1 -1
- package/package.json +29 -28
- package/src/aave.ts +6 -1
- package/src/analytics.ts +103 -53
- package/src/apiClient.ts +1 -1
- package/src/{proxyCaller.ts → balanceInjector.ts} +22 -17
- package/src/cctp.ts +6 -2
- package/src/cctpqueue.ts +7 -7
- package/src/chains.ts +8 -0
- package/src/config.ts +40 -9
- package/src/constants.ts +11 -8
- package/src/contractUtils.ts +33 -2
- package/src/customChains.ts +24 -0
- package/src/index.ts +11 -1
- package/src/intentEntrypoint.ts +253 -0
- package/src/intents.ts +87 -54
- package/src/metaTxnMonitor.ts +1 -0
- package/src/morpho.ts +13 -2
- package/src/pools.ts +68 -86
- package/src/prepareSend.ts +437 -207
- package/src/prices.ts +51 -7
- package/src/relaySdk.ts +6 -4
- package/src/relayer.ts +2 -0
- package/src/toast.ts +110 -0
- package/src/tokenBalances.ts +112 -20
- package/src/tokens.ts +70 -7
- package/src/trails.ts +80 -77
- package/src/trailsClient.ts +45 -0
- package/src/transactions.ts +27 -35
- package/src/umd.tsx +1 -1
- package/src/wallets.ts +2 -1
- package/src/widget/assets/sequence-logo.svg +15 -0
- package/src/widget/compiled.css +2 -2
- package/src/widget/components/AccountActionsDropdown.tsx +18 -159
- package/src/widget/components/AccountIntentTransactionHistory.tsx +346 -63
- package/src/widget/components/AccountSettings.tsx +96 -0
- package/src/widget/components/ChainFilterDropdown.tsx +1 -1
- package/src/widget/components/ChainList.tsx +10 -20
- package/src/widget/components/ClassicSwap.tsx +923 -0
- package/src/widget/components/ConfigDisplay.tsx +8 -5
- package/src/widget/components/ConnectedWallets.tsx +260 -0
- package/src/widget/components/DebugMenu.tsx +2 -0
- package/src/widget/components/DebugScreensList.tsx +3 -0
- package/src/widget/components/DebugToast.tsx +63 -0
- package/src/widget/components/Earn.tsx +108 -116
- package/src/widget/components/EarnPools.tsx +2 -4
- package/src/widget/components/EarnPoolsFilters.tsx +6 -6
- package/src/widget/components/Fund.tsx +1245 -0
- package/src/widget/components/FundMethods.tsx +1 -1
- package/src/widget/components/FundSendForm.tsx +1 -1
- package/src/widget/components/Identicon.tsx +158 -0
- package/src/widget/components/Pay.tsx +1088 -0
- package/src/widget/components/PaySendForm.tsx +1 -1
- package/src/widget/components/QuoteDetails.tsx +1 -1
- package/src/widget/components/Receipt.tsx +1 -1
- package/src/widget/components/Receive.tsx +4 -2
- package/src/widget/components/RecentTokens.tsx +2 -1
- package/src/widget/components/Recipients.tsx +448 -0
- package/src/widget/components/RefundWarning.tsx +61 -0
- package/src/widget/components/ScreenHeader.tsx +1 -1
- package/src/widget/components/SimpleSwap.tsx +74 -58
- package/src/widget/components/Swap.tsx +35 -853
- package/src/widget/components/SwapSettings.tsx +5 -11
- package/src/widget/components/ThemeProvider.tsx +32 -0
- package/src/widget/components/ThemeSyncer.tsx +47 -0
- package/src/widget/components/Toast.tsx +315 -0
- package/src/widget/components/TokenList.tsx +2 -34
- package/src/widget/components/TokenSelector.tsx +3 -3
- package/src/widget/components/TransactionDetails.tsx +153 -13
- package/src/widget/components/TruncatedAddress.tsx +5 -1
- package/src/widget/components/UserPreferences.tsx +156 -0
- package/src/widget/components/WalletList.tsx +1 -1
- package/src/widget/hooks/useBalanceVisible.tsx +40 -2
- package/src/widget/hooks/useCheckout.ts +13 -0
- package/src/widget/hooks/useCurrentScreen.tsx +3 -0
- package/src/widget/hooks/useDebugScreens.ts +12 -2
- package/src/widget/hooks/useDefaultTokenSelection.tsx +475 -0
- package/src/widget/hooks/useIntentTransactionHistory.ts +212 -0
- package/src/widget/hooks/usePayMessage.tsx +370 -0
- package/src/widget/hooks/useRecipients.ts +168 -0
- package/src/widget/hooks/useSelectedRecipient.tsx +48 -0
- package/src/widget/hooks/useSendForm.ts +179 -26
- package/src/widget/hooks/useSwapAmount.tsx +50 -0
- package/src/widget/hooks/useSwapSettings.tsx +100 -0
- package/src/widget/hooks/useTargetAmount.ts +23 -0
- package/src/widget/hooks/useTheme.tsx +80 -0
- package/src/widget/hooks/useTokenList.ts +20 -11
- package/src/widget/index.css +45 -21
- package/src/widget/widget.tsx +164 -68
- package/dist/address.d.ts +0 -2
- package/dist/address.d.ts.map +0 -1
- package/dist/proxyCaller.d.ts.map +0 -1
- package/src/address.ts +0 -6
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
import { ChevronDown, Loader2, ArrowDown } from "lucide-react"
|
|
2
|
+
import type React from "react"
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, useMemo } from "react"
|
|
4
|
+
import type { Account, WalletClient } from "viem"
|
|
5
|
+
import type { TransactionState } from "../../transactions.js"
|
|
6
|
+
import type { OnCompleteProps, Token } from "../hooks/useSendForm.js"
|
|
7
|
+
import type { SupportedToken } from "../../tokens.js"
|
|
8
|
+
import { useSendForm } from "../hooks/useSendForm.js"
|
|
9
|
+
import type { CheckoutOnHandlers } from "../hooks/useCheckout.js"
|
|
10
|
+
import { FeeOptions } from "./FeeOptions.js"
|
|
11
|
+
import { TokenImage } from "./TokenImage.js"
|
|
12
|
+
import { QuoteDetails } from "./QuoteDetails.js"
|
|
13
|
+
import { type PrepareSendQuote, TradeType } from "../../prepareSend.js"
|
|
14
|
+
import { getChainInfo } from "../../chains.js"
|
|
15
|
+
import { formatUsdAmountDisplay } from "../../tokenBalances.js"
|
|
16
|
+
import { MINIMUM_USD_AMOUNT_FOR_SWAP } from "../../constants.js"
|
|
17
|
+
import { ScreenHeader } from "./ScreenHeader.js"
|
|
18
|
+
import { TokenSelector } from "./TokenSelector.js"
|
|
19
|
+
import { ErrorDisplay } from "./ErrorDisplay.js"
|
|
20
|
+
import { useMode } from "../hooks/useMode.js"
|
|
21
|
+
import { logger } from "../../logger.js"
|
|
22
|
+
import { SwapSettings } from "./SwapSettings.js"
|
|
23
|
+
import { useOriginSelectedToken } from "../hooks/useOriginSelectedToken.js"
|
|
24
|
+
import { useDestinationSelectedToken } from "../hooks/useDestinationSelectedToken.js"
|
|
25
|
+
import { useBalanceVisible } from "../hooks/useBalanceVisible.js"
|
|
26
|
+
import { useDefaultTokenSelection } from "../hooks/useDefaultTokenSelection.js"
|
|
27
|
+
|
|
28
|
+
interface ClassicSwapProps {
|
|
29
|
+
selectedToken: Token | null
|
|
30
|
+
onSend: (amount: string, recipient: string) => void
|
|
31
|
+
onBack?: () => void
|
|
32
|
+
onConfirm: () => void
|
|
33
|
+
onComplete: (result: OnCompleteProps) => void
|
|
34
|
+
account: Account
|
|
35
|
+
toRecipient?: string
|
|
36
|
+
toAmount?: string
|
|
37
|
+
toChainId?: number
|
|
38
|
+
toToken?: string
|
|
39
|
+
toCalldata?: string
|
|
40
|
+
walletClient: WalletClient
|
|
41
|
+
onTransactionStateChange: (transactionStates: TransactionState[]) => void
|
|
42
|
+
onError: (error: Error | string | null) => void
|
|
43
|
+
onWaitingForWalletConfirm: (props: PrepareSendQuote) => void
|
|
44
|
+
paymasterUrls?: Array<{ chainId: number; url: string }>
|
|
45
|
+
gasless?: boolean
|
|
46
|
+
setWalletConfirmRetryHandler: (handler: () => Promise<void>) => void
|
|
47
|
+
quoteProvider?: string
|
|
48
|
+
fundMethod?: string
|
|
49
|
+
onNavigateToMeshConnect?: (
|
|
50
|
+
props: {
|
|
51
|
+
toTokenSymbol: string
|
|
52
|
+
toTokenAmount: string
|
|
53
|
+
toChainId: number
|
|
54
|
+
toRecipientAddress: string
|
|
55
|
+
},
|
|
56
|
+
quote?: PrepareSendQuote | null,
|
|
57
|
+
) => void
|
|
58
|
+
onAmountUpdate?: (amount: string) => void
|
|
59
|
+
checkoutOnHandlers?: CheckoutOnHandlers
|
|
60
|
+
recentTokens?: SupportedToken[]
|
|
61
|
+
onRecentTokenSelect?: (token: SupportedToken) => void
|
|
62
|
+
onTrackToken?: (token: any) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const ClassicSwap: React.FC<ClassicSwapProps> = ({
|
|
66
|
+
selectedToken: initialSelectedToken,
|
|
67
|
+
onSend,
|
|
68
|
+
onBack,
|
|
69
|
+
onConfirm,
|
|
70
|
+
onComplete,
|
|
71
|
+
account,
|
|
72
|
+
toAmount,
|
|
73
|
+
toChainId,
|
|
74
|
+
toToken,
|
|
75
|
+
toCalldata,
|
|
76
|
+
walletClient,
|
|
77
|
+
onTransactionStateChange,
|
|
78
|
+
onError,
|
|
79
|
+
onWaitingForWalletConfirm,
|
|
80
|
+
paymasterUrls,
|
|
81
|
+
gasless,
|
|
82
|
+
setWalletConfirmRetryHandler,
|
|
83
|
+
quoteProvider,
|
|
84
|
+
fundMethod,
|
|
85
|
+
onNavigateToMeshConnect,
|
|
86
|
+
onAmountUpdate,
|
|
87
|
+
checkoutOnHandlers,
|
|
88
|
+
recentTokens,
|
|
89
|
+
onRecentTokenSelect,
|
|
90
|
+
onTrackToken,
|
|
91
|
+
}) => {
|
|
92
|
+
const { mode } = useMode()
|
|
93
|
+
const { isBalanceVisible } = useBalanceVisible()
|
|
94
|
+
const { selectedToken: originToken, setSelectedToken: setOriginToken } =
|
|
95
|
+
useOriginSelectedToken()
|
|
96
|
+
const {
|
|
97
|
+
selectedToken: destinationToken,
|
|
98
|
+
setSelectedToken: setDestinationToken,
|
|
99
|
+
} = useDestinationSelectedToken()
|
|
100
|
+
const [isFlipped, setIsFlipped] = useState(false)
|
|
101
|
+
|
|
102
|
+
// Use new default token selection hook
|
|
103
|
+
const {
|
|
104
|
+
defaultOriginToken,
|
|
105
|
+
defaultDestinationToken,
|
|
106
|
+
isLoading: isLoadingDefaults,
|
|
107
|
+
} = useDefaultTokenSelection()
|
|
108
|
+
|
|
109
|
+
const [originChainId, setOriginChainId] = useState<number | null | undefined>(
|
|
110
|
+
initialSelectedToken?.chainId || originToken?.chainId,
|
|
111
|
+
)
|
|
112
|
+
const [tradeType, setTradeType] = useState<TradeType>(TradeType.EXACT_INPUT)
|
|
113
|
+
const [sellAmount, setSellAmount] = useState("")
|
|
114
|
+
const [buyAmount, setBuyAmount] = useState("")
|
|
115
|
+
const [lastInputType, setLastInputType] = useState<"sell" | "buy">("sell")
|
|
116
|
+
|
|
117
|
+
const {
|
|
118
|
+
amount,
|
|
119
|
+
amountRaw,
|
|
120
|
+
amountUsdDisplay,
|
|
121
|
+
balanceFormatted,
|
|
122
|
+
handleSubmit,
|
|
123
|
+
isSubmitting,
|
|
124
|
+
isLoadingQuote,
|
|
125
|
+
isTokenDropdownOpen,
|
|
126
|
+
selectedDestinationChain,
|
|
127
|
+
selectedDestToken,
|
|
128
|
+
setAmount,
|
|
129
|
+
setSelectedDestinationChain,
|
|
130
|
+
setSelectedDestToken,
|
|
131
|
+
buttonText,
|
|
132
|
+
selectedFeeToken,
|
|
133
|
+
setSelectedFeeToken,
|
|
134
|
+
feeOptions,
|
|
135
|
+
setIsTokenDropdownOpen,
|
|
136
|
+
toAmountDisplay,
|
|
137
|
+
destinationTokenAddress,
|
|
138
|
+
isValidCustomToken,
|
|
139
|
+
prepareSendQuote,
|
|
140
|
+
quoteError,
|
|
141
|
+
quoteErrorPrettified,
|
|
142
|
+
isSameTokenWithoutCustomCalldata,
|
|
143
|
+
} = useSendForm({
|
|
144
|
+
account,
|
|
145
|
+
toAmount: tradeType === TradeType.EXACT_OUTPUT ? buyAmount : toAmount,
|
|
146
|
+
toRecipient: account.address,
|
|
147
|
+
toChainId,
|
|
148
|
+
toToken,
|
|
149
|
+
toCalldata,
|
|
150
|
+
walletClient,
|
|
151
|
+
onTransactionStateChange,
|
|
152
|
+
onError,
|
|
153
|
+
onWaitingForWalletConfirm,
|
|
154
|
+
paymasterUrls,
|
|
155
|
+
gasless,
|
|
156
|
+
onConfirm,
|
|
157
|
+
onComplete,
|
|
158
|
+
onSend,
|
|
159
|
+
selectedToken: originToken as any,
|
|
160
|
+
setWalletConfirmRetryHandler,
|
|
161
|
+
tradeType: tradeType,
|
|
162
|
+
quoteProvider,
|
|
163
|
+
fundMethod,
|
|
164
|
+
mode,
|
|
165
|
+
onNavigateToMeshConnect,
|
|
166
|
+
checkoutOnHandlers,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Update amount based on last input type
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (lastInputType === "sell") {
|
|
172
|
+
setAmount(sellAmount)
|
|
173
|
+
} else {
|
|
174
|
+
setAmount(buyAmount)
|
|
175
|
+
}
|
|
176
|
+
}, [sellAmount, buyAmount, lastInputType, setAmount])
|
|
177
|
+
|
|
178
|
+
// Handle sell amount input changes with decimal validation
|
|
179
|
+
const handleSellAmountChange = useCallback((value: string) => {
|
|
180
|
+
// Validate decimal places (max 8 decimals)
|
|
181
|
+
const decimalMatch = value.match(/^\d*\.?\d{0,8}$/)
|
|
182
|
+
if (!decimalMatch && value !== "") {
|
|
183
|
+
return // Don't update if more than 8 decimals
|
|
184
|
+
}
|
|
185
|
+
setTradeType(TradeType.EXACT_INPUT)
|
|
186
|
+
setSellAmount(value)
|
|
187
|
+
setBuyAmount("")
|
|
188
|
+
setLastInputType("sell")
|
|
189
|
+
}, [])
|
|
190
|
+
|
|
191
|
+
// Handle buy amount input changes with decimal validation
|
|
192
|
+
const handleBuyAmountChange = useCallback((value: string) => {
|
|
193
|
+
// Validate decimal places (max 8 decimals)
|
|
194
|
+
const decimalMatch = value.match(/^\d*\.?\d{0,8}$/)
|
|
195
|
+
if (!decimalMatch && value !== "") {
|
|
196
|
+
return // Don't update if more than 8 decimals
|
|
197
|
+
}
|
|
198
|
+
setTradeType(TradeType.EXACT_OUTPUT)
|
|
199
|
+
setBuyAmount(value)
|
|
200
|
+
setSellAmount("")
|
|
201
|
+
setLastInputType("buy")
|
|
202
|
+
}, [])
|
|
203
|
+
|
|
204
|
+
// Handle buy input focus - clear field when user wants to type
|
|
205
|
+
const handleBuyInputFocus = useCallback(() => {
|
|
206
|
+
// When focusing on buy field and it's in EXACT_INPUT mode (showing calculated value), switch to EXACT_OUTPUT mode
|
|
207
|
+
if (tradeType === TradeType.EXACT_INPUT && !buyAmount) {
|
|
208
|
+
setBuyAmount("")
|
|
209
|
+
setTradeType(TradeType.EXACT_OUTPUT)
|
|
210
|
+
setLastInputType("buy")
|
|
211
|
+
}
|
|
212
|
+
}, [tradeType, buyAmount])
|
|
213
|
+
|
|
214
|
+
// Update amounts when quote is received
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (!prepareSendQuote) return
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
tradeType === TradeType.EXACT_OUTPUT &&
|
|
220
|
+
prepareSendQuote.originAmountDisplay
|
|
221
|
+
) {
|
|
222
|
+
setSellAmount(prepareSendQuote.originAmountDisplay)
|
|
223
|
+
} else if (
|
|
224
|
+
tradeType === TradeType.EXACT_INPUT &&
|
|
225
|
+
prepareSendQuote.destinationAmountDisplay
|
|
226
|
+
) {
|
|
227
|
+
setBuyAmount(prepareSendQuote.destinationAmountDisplay)
|
|
228
|
+
}
|
|
229
|
+
}, [prepareSendQuote, tradeType])
|
|
230
|
+
|
|
231
|
+
// Handle percentage button clicks
|
|
232
|
+
const handlePercentageClick = useCallback(
|
|
233
|
+
(percentage: number) => {
|
|
234
|
+
if (!originToken || !balanceFormatted) return
|
|
235
|
+
|
|
236
|
+
// Parse the balance and calculate percentage
|
|
237
|
+
const balance = parseFloat(balanceFormatted)
|
|
238
|
+
if (Number.isNaN(balance)) return
|
|
239
|
+
|
|
240
|
+
const amount = (balance * percentage) / 100
|
|
241
|
+
setTradeType(TradeType.EXACT_INPUT)
|
|
242
|
+
setSellAmount(amount.toFixed(6))
|
|
243
|
+
setBuyAmount("")
|
|
244
|
+
setLastInputType("sell")
|
|
245
|
+
},
|
|
246
|
+
[originToken, balanceFormatted],
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
// Call onAmountUpdate when amountRaw changes
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (onAmountUpdate) {
|
|
252
|
+
onAmountUpdate(amountRaw)
|
|
253
|
+
}
|
|
254
|
+
}, [amountRaw, onAmountUpdate])
|
|
255
|
+
|
|
256
|
+
// Auto-select origin token using new hook
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
if (!originToken && !isLoadingDefaults && defaultOriginToken) {
|
|
259
|
+
logger.console.log(
|
|
260
|
+
"[trails-sdk] Auto-selecting origin token:",
|
|
261
|
+
defaultOriginToken,
|
|
262
|
+
)
|
|
263
|
+
setOriginToken(defaultOriginToken as any)
|
|
264
|
+
setOriginChainId(defaultOriginToken.chainId)
|
|
265
|
+
}
|
|
266
|
+
}, [originToken, isLoadingDefaults, defaultOriginToken, setOriginToken])
|
|
267
|
+
|
|
268
|
+
// Auto-select destination token using new hook
|
|
269
|
+
// Note: In swap mode, we still auto-select a destination even if toToken is provided,
|
|
270
|
+
// since the user needs a starting point to modify
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (!destinationToken && !isLoadingDefaults && defaultDestinationToken) {
|
|
273
|
+
logger.console.log(
|
|
274
|
+
"[trails-sdk] Auto-selecting destination token:",
|
|
275
|
+
defaultDestinationToken,
|
|
276
|
+
)
|
|
277
|
+
setDestinationToken(defaultDestinationToken as any)
|
|
278
|
+
setSelectedDestToken(defaultDestinationToken as any)
|
|
279
|
+
|
|
280
|
+
// Also set the destination chain
|
|
281
|
+
const chainInfo = getChainInfo(defaultDestinationToken.chainId)
|
|
282
|
+
if (chainInfo) {
|
|
283
|
+
setSelectedDestinationChain(chainInfo)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}, [
|
|
287
|
+
destinationToken,
|
|
288
|
+
isLoadingDefaults,
|
|
289
|
+
defaultDestinationToken,
|
|
290
|
+
setDestinationToken,
|
|
291
|
+
setSelectedDestToken,
|
|
292
|
+
setSelectedDestinationChain,
|
|
293
|
+
])
|
|
294
|
+
|
|
295
|
+
// Auto-focus the input field when component mounts
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (inputRef.current) {
|
|
298
|
+
inputRef.current.focus()
|
|
299
|
+
}
|
|
300
|
+
}, [])
|
|
301
|
+
|
|
302
|
+
const originTokenDropdownRef = useRef<HTMLDivElement>(null)
|
|
303
|
+
const tokenDropdownRef = useRef<HTMLInputElement>(null)
|
|
304
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
305
|
+
|
|
306
|
+
const [isOriginTokenDropdownOpen, setIsOriginTokenDropdownOpen] =
|
|
307
|
+
useState(false)
|
|
308
|
+
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
if (selectedDestToken) {
|
|
311
|
+
setSelectedDestinationChain(
|
|
312
|
+
getChainInfo((selectedDestToken as any)?.chainId as any) as any,
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
}, [selectedDestToken, setSelectedDestinationChain])
|
|
316
|
+
|
|
317
|
+
// Sync destination token from hook with useSendForm
|
|
318
|
+
// This ensures the destination token auto-selection flows into useSendForm
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (destinationToken) {
|
|
321
|
+
logger.console.log(
|
|
322
|
+
"[trails-sdk] Syncing destination token to useSendForm:",
|
|
323
|
+
destinationToken,
|
|
324
|
+
)
|
|
325
|
+
setSelectedDestToken(destinationToken as any)
|
|
326
|
+
}
|
|
327
|
+
}, [destinationToken, setSelectedDestToken])
|
|
328
|
+
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
331
|
+
if (
|
|
332
|
+
tokenDropdownRef.current &&
|
|
333
|
+
!tokenDropdownRef.current.contains(event.target as Node)
|
|
334
|
+
) {
|
|
335
|
+
setIsTokenDropdownOpen(false)
|
|
336
|
+
}
|
|
337
|
+
if (
|
|
338
|
+
originTokenDropdownRef.current &&
|
|
339
|
+
!originTokenDropdownRef.current.contains(event.target as Node)
|
|
340
|
+
) {
|
|
341
|
+
setIsOriginTokenDropdownOpen(false)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isTokenDropdownOpen || isOriginTokenDropdownOpen) {
|
|
346
|
+
document.addEventListener("click", handleClickOutside)
|
|
347
|
+
return () => document.removeEventListener("click", handleClickOutside)
|
|
348
|
+
}
|
|
349
|
+
}, [isTokenDropdownOpen, isOriginTokenDropdownOpen, setIsTokenDropdownOpen])
|
|
350
|
+
|
|
351
|
+
// Handle flip functionality
|
|
352
|
+
const handleFlip = () => {
|
|
353
|
+
if (!selectedDestinationChain || !originToken || !selectedDestToken) return
|
|
354
|
+
|
|
355
|
+
setIsFlipped(!isFlipped)
|
|
356
|
+
|
|
357
|
+
// Store current values
|
|
358
|
+
const tempOriginToken = originToken
|
|
359
|
+
const tempOriginChainId = originChainId
|
|
360
|
+
const tempBuyAmount = buyAmount || toAmountDisplay || ""
|
|
361
|
+
|
|
362
|
+
// Swap origin and destination using hooks
|
|
363
|
+
setOriginToken(selectedDestToken as any)
|
|
364
|
+
setDestinationToken(tempOriginToken as any)
|
|
365
|
+
setOriginChainId((selectedDestToken as any)?.chainId as any)
|
|
366
|
+
setSelectedDestToken(tempOriginToken as any)
|
|
367
|
+
|
|
368
|
+
// Update destination chain
|
|
369
|
+
const newChain = getChainInfo(tempOriginChainId as any)
|
|
370
|
+
if (newChain) {
|
|
371
|
+
setSelectedDestinationChain(newChain)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// When flipping, always set the current visible amount as the new sell amount
|
|
375
|
+
if (tradeType === TradeType.EXACT_INPUT) {
|
|
376
|
+
// If we were in EXACT_INPUT, use the buy amount (from quote) as the new sell amount
|
|
377
|
+
setSellAmount(tempBuyAmount)
|
|
378
|
+
setBuyAmount("")
|
|
379
|
+
setTradeType(TradeType.EXACT_INPUT)
|
|
380
|
+
setLastInputType("sell")
|
|
381
|
+
} else {
|
|
382
|
+
// If we were in EXACT_OUTPUT, use the current buy amount as the new sell amount
|
|
383
|
+
setSellAmount(tempBuyAmount)
|
|
384
|
+
setBuyAmount("")
|
|
385
|
+
setTradeType(TradeType.EXACT_INPUT)
|
|
386
|
+
setLastInputType("sell")
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Dynamic font size based on input length - matching SimpleSwap.tsx
|
|
391
|
+
const sellInputStyles = useMemo(() => {
|
|
392
|
+
const inputLength = sellAmount.length
|
|
393
|
+
let fontSize: string
|
|
394
|
+
|
|
395
|
+
if (inputLength > 12) {
|
|
396
|
+
fontSize = "1rem"
|
|
397
|
+
} else if (inputLength > 9) {
|
|
398
|
+
fontSize = "1.25rem"
|
|
399
|
+
} else if (inputLength > 6) {
|
|
400
|
+
fontSize = "1.5rem"
|
|
401
|
+
} else if (inputLength > 3) {
|
|
402
|
+
fontSize = "1.75rem"
|
|
403
|
+
} else {
|
|
404
|
+
fontSize = "2rem"
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
fontSize,
|
|
409
|
+
transition: "all 0.1s ease-in-out",
|
|
410
|
+
}
|
|
411
|
+
}, [sellAmount.length])
|
|
412
|
+
|
|
413
|
+
const buyInputStyles = useMemo(() => {
|
|
414
|
+
const inputValue =
|
|
415
|
+
tradeType === TradeType.EXACT_OUTPUT ? buyAmount : toAmountDisplay || ""
|
|
416
|
+
const inputLength = inputValue.length
|
|
417
|
+
let fontSize: string
|
|
418
|
+
|
|
419
|
+
if (inputLength > 12) {
|
|
420
|
+
fontSize = "1rem"
|
|
421
|
+
} else if (inputLength > 9) {
|
|
422
|
+
fontSize = "1.25rem"
|
|
423
|
+
} else if (inputLength > 6) {
|
|
424
|
+
fontSize = "1.5rem"
|
|
425
|
+
} else if (inputLength > 3) {
|
|
426
|
+
fontSize = "1.75rem"
|
|
427
|
+
} else {
|
|
428
|
+
fontSize = "2rem"
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
fontSize,
|
|
433
|
+
transition: "all 0.1s ease-in-out",
|
|
434
|
+
}
|
|
435
|
+
}, [tradeType, buyAmount, toAmountDisplay])
|
|
436
|
+
|
|
437
|
+
return (
|
|
438
|
+
<div className="space-y-2">
|
|
439
|
+
<ScreenHeader
|
|
440
|
+
onBack={onBack}
|
|
441
|
+
headerContent="Swap"
|
|
442
|
+
headerContentAlign="left"
|
|
443
|
+
showAccountActions={true}
|
|
444
|
+
rightSideContent={<SwapSettings />}
|
|
445
|
+
/>
|
|
446
|
+
|
|
447
|
+
<form onSubmit={handleSubmit} className="space-y-1">
|
|
448
|
+
{/* Input Section - Amount + Token Selection */}
|
|
449
|
+
<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 trails-focus-border-secondary min-h-[120px] flex flex-col">
|
|
450
|
+
{/* Sell Label and Percentage Buttons */}
|
|
451
|
+
<div className="flex justify-between items-center mb-2">
|
|
452
|
+
<div className="text-sm font-medium trails-text-secondary text-left">
|
|
453
|
+
Sell
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
{/* Percentage Buttons */}
|
|
457
|
+
{originToken && (
|
|
458
|
+
<div className="flex space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
459
|
+
<button
|
|
460
|
+
type="button"
|
|
461
|
+
onClick={() => handlePercentageClick(25)}
|
|
462
|
+
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"
|
|
463
|
+
>
|
|
464
|
+
25%
|
|
465
|
+
</button>
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
onClick={() => handlePercentageClick(50)}
|
|
469
|
+
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"
|
|
470
|
+
>
|
|
471
|
+
50%
|
|
472
|
+
</button>
|
|
473
|
+
<button
|
|
474
|
+
type="button"
|
|
475
|
+
onClick={() => handlePercentageClick(75)}
|
|
476
|
+
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"
|
|
477
|
+
>
|
|
478
|
+
75%
|
|
479
|
+
</button>
|
|
480
|
+
<button
|
|
481
|
+
type="button"
|
|
482
|
+
onClick={() => handlePercentageClick(100)}
|
|
483
|
+
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"
|
|
484
|
+
>
|
|
485
|
+
Max
|
|
486
|
+
</button>
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
<div className="flex items-center space-x-2 flex-1">
|
|
492
|
+
{/* Amount Input */}
|
|
493
|
+
<div className="flex-1">
|
|
494
|
+
<div className="flex items-center space-x-2">
|
|
495
|
+
<input
|
|
496
|
+
ref={inputRef}
|
|
497
|
+
id="amount"
|
|
498
|
+
type="text"
|
|
499
|
+
value={sellAmount}
|
|
500
|
+
onChange={(e) => handleSellAmountChange(e.target.value)}
|
|
501
|
+
placeholder={`0 ${originToken?.symbol || ""}`}
|
|
502
|
+
className={`w-full bg-transparent font-bold trails-text-primary placeholder:trails-text-muted border-none outline-none ${
|
|
503
|
+
isLoadingQuote && tradeType === TradeType.EXACT_OUTPUT
|
|
504
|
+
? "animate-pulse"
|
|
505
|
+
: ""
|
|
506
|
+
}`}
|
|
507
|
+
style={sellInputStyles}
|
|
508
|
+
readOnly={
|
|
509
|
+
tradeType === TradeType.EXACT_OUTPUT && isLoadingQuote
|
|
510
|
+
}
|
|
511
|
+
/>
|
|
512
|
+
{isLoadingQuote && tradeType === TradeType.EXACT_OUTPUT && (
|
|
513
|
+
<div className="animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
</div>
|
|
517
|
+
|
|
518
|
+
{/* Token Selection Button */}
|
|
519
|
+
<div className="relative" ref={originTokenDropdownRef}>
|
|
520
|
+
<button
|
|
521
|
+
type="button"
|
|
522
|
+
onClick={() =>
|
|
523
|
+
setIsOriginTokenDropdownOpen(!isOriginTokenDropdownOpen)
|
|
524
|
+
}
|
|
525
|
+
className={`flex items-center space-x-2 trails-border-radius-input px-2.5 py-1.5 border transition-colors cursor-pointer ${
|
|
526
|
+
originToken
|
|
527
|
+
? "trails-bg-card hover:trails-hover-bg trails-border-primary"
|
|
528
|
+
: "bg-blue-500 hover:bg-blue-600 border-blue-500 text-white"
|
|
529
|
+
}`}
|
|
530
|
+
>
|
|
531
|
+
{originToken ? (
|
|
532
|
+
<>
|
|
533
|
+
<TokenImage
|
|
534
|
+
symbol={originToken.symbol}
|
|
535
|
+
imageUrl={originToken.imageUrl}
|
|
536
|
+
chainId={originChainId}
|
|
537
|
+
size={20}
|
|
538
|
+
/>
|
|
539
|
+
<span className="font-medium trails-text-primary text-sm">
|
|
540
|
+
{originToken.symbol}
|
|
541
|
+
</span>
|
|
542
|
+
<ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
|
|
543
|
+
</>
|
|
544
|
+
) : (
|
|
545
|
+
<>
|
|
546
|
+
<span className="font-medium text-sm text-white">
|
|
547
|
+
Select Token
|
|
548
|
+
</span>
|
|
549
|
+
<ChevronDown className="w-3.5 h-3.5 text-white" />
|
|
550
|
+
</>
|
|
551
|
+
)}
|
|
552
|
+
</button>
|
|
553
|
+
|
|
554
|
+
{/* Token Selector Modal */}
|
|
555
|
+
{isOriginTokenDropdownOpen && (
|
|
556
|
+
<div className="absolute right-0 top-full mt-2 w-80 trails-bg-card rounded-lg shadow-lg border trails-border-primary max-h-80 overflow-hidden z-10 -translate-y-28">
|
|
557
|
+
<div className="p-2">
|
|
558
|
+
<TokenSelector
|
|
559
|
+
onTokenSelect={(token: any) => {
|
|
560
|
+
const formattedToken = {
|
|
561
|
+
...token,
|
|
562
|
+
decimals:
|
|
563
|
+
token.contractInfo?.decimals || token.decimals,
|
|
564
|
+
contractInfo: {
|
|
565
|
+
decimals:
|
|
566
|
+
token.contractInfo?.decimals || token.decimals,
|
|
567
|
+
contractAddress: token.contractAddress,
|
|
568
|
+
symbol: token.symbol,
|
|
569
|
+
name: token.name,
|
|
570
|
+
},
|
|
571
|
+
} as any
|
|
572
|
+
setOriginToken(formattedToken)
|
|
573
|
+
logger.console.log(
|
|
574
|
+
"[trails-sdk] selected origin token",
|
|
575
|
+
token,
|
|
576
|
+
)
|
|
577
|
+
setOriginChainId(token.chainId)
|
|
578
|
+
setIsOriginTokenDropdownOpen(false)
|
|
579
|
+
// Track the token selection
|
|
580
|
+
onTrackToken?.(token)
|
|
581
|
+
}}
|
|
582
|
+
onError={onError}
|
|
583
|
+
fundMethod={fundMethod}
|
|
584
|
+
showContinueButton={false}
|
|
585
|
+
compactMode={false}
|
|
586
|
+
recentTokens={recentTokens}
|
|
587
|
+
onRecentTokenSelect={(token) => {
|
|
588
|
+
const formattedToken = {
|
|
589
|
+
...token,
|
|
590
|
+
decimals: token.decimals,
|
|
591
|
+
contractInfo: {
|
|
592
|
+
decimals: token.decimals,
|
|
593
|
+
contractAddress: token.contractAddress,
|
|
594
|
+
symbol: token.symbol,
|
|
595
|
+
name: token.name,
|
|
596
|
+
},
|
|
597
|
+
} as any
|
|
598
|
+
setOriginToken(formattedToken)
|
|
599
|
+
setOriginChainId(token.chainId)
|
|
600
|
+
setIsOriginTokenDropdownOpen(false)
|
|
601
|
+
onRecentTokenSelect?.(token)
|
|
602
|
+
}}
|
|
603
|
+
/>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
)}
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
|
|
610
|
+
{/* Bottom Info Row for sell */}
|
|
611
|
+
<div className="mt-2 flex justify-between items-center">
|
|
612
|
+
{/* USD Amount */}
|
|
613
|
+
{originToken?.symbol && (
|
|
614
|
+
<div className="text-xs trails-text-muted">
|
|
615
|
+
≈{" "}
|
|
616
|
+
{tradeType === TradeType.EXACT_INPUT
|
|
617
|
+
? amountUsdDisplay || "$0.00"
|
|
618
|
+
: isLoadingQuote
|
|
619
|
+
? "$0.00"
|
|
620
|
+
: prepareSendQuote?.originAmountUsdDisplay || "$0.00"}
|
|
621
|
+
</div>
|
|
622
|
+
)}
|
|
623
|
+
|
|
624
|
+
{/* Origin Token Balance */}
|
|
625
|
+
{originToken && (
|
|
626
|
+
<button
|
|
627
|
+
type="button"
|
|
628
|
+
className="text-xs trails-text-muted cursor-pointer hover:trails-hover-text transition-colors bg-transparent border-none p-0"
|
|
629
|
+
onClick={() => {
|
|
630
|
+
if (balanceFormatted) {
|
|
631
|
+
const balance = parseFloat(balanceFormatted)
|
|
632
|
+
if (!Number.isNaN(balance)) {
|
|
633
|
+
setTradeType(TradeType.EXACT_INPUT)
|
|
634
|
+
setSellAmount(balance.toFixed(6))
|
|
635
|
+
setBuyAmount("")
|
|
636
|
+
setLastInputType("sell")
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}}
|
|
640
|
+
onKeyDown={(e) => {
|
|
641
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
642
|
+
e.preventDefault()
|
|
643
|
+
if (balanceFormatted) {
|
|
644
|
+
const balance = parseFloat(balanceFormatted)
|
|
645
|
+
if (!Number.isNaN(balance)) {
|
|
646
|
+
setTradeType(TradeType.EXACT_INPUT)
|
|
647
|
+
setSellAmount(balance.toFixed(6))
|
|
648
|
+
setBuyAmount("")
|
|
649
|
+
setLastInputType("sell")
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}}
|
|
654
|
+
title="Click to use full balance"
|
|
655
|
+
>
|
|
656
|
+
Balance:{" "}
|
|
657
|
+
{isBalanceVisible
|
|
658
|
+
? `${balanceFormatted || "0.00"} ${originToken.symbol}`
|
|
659
|
+
: "••••••"}
|
|
660
|
+
</button>
|
|
661
|
+
)}
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
|
|
665
|
+
{/* Flip Button - Absolutely Positioned */}
|
|
666
|
+
<div className="relative">
|
|
667
|
+
<button
|
|
668
|
+
type="button"
|
|
669
|
+
onClick={handleFlip}
|
|
670
|
+
className="absolute left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 p-1.5 trails-border-radius-button trails-bg-tertiary hover:trails-hover-bg transition-colors cursor-pointer border-2 border-white dark:border-gray-800"
|
|
671
|
+
>
|
|
672
|
+
<ArrowDown
|
|
673
|
+
className="w-5 h-5 text-gray-900 dark:text-white"
|
|
674
|
+
strokeWidth={2.5}
|
|
675
|
+
/>
|
|
676
|
+
</button>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
{/* Output Section - Amount + Token Selection */}
|
|
680
|
+
<div className="trails-bg-secondary trails-bg-secondary-hover trails-border-radius-container p-3 transition-all duration-200 border border-transparent focus-within:!bg-white dark:focus-within:!bg-gray-800 trails-focus-border-secondary min-h-[120px] flex flex-col">
|
|
681
|
+
{/* Buy Label */}
|
|
682
|
+
<div className="text-sm font-medium trails-text-secondary mb-2 text-left">
|
|
683
|
+
Buy
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
<div className="flex items-center space-x-2 flex-1">
|
|
687
|
+
{/* Output Amount */}
|
|
688
|
+
<div className="flex-1">
|
|
689
|
+
<div className="flex items-center space-x-2">
|
|
690
|
+
<input
|
|
691
|
+
type="text"
|
|
692
|
+
value={
|
|
693
|
+
tradeType === TradeType.EXACT_OUTPUT
|
|
694
|
+
? buyAmount
|
|
695
|
+
: toAmountDisplay || ""
|
|
696
|
+
}
|
|
697
|
+
onChange={(e) => handleBuyAmountChange(e.target.value)}
|
|
698
|
+
onFocus={handleBuyInputFocus}
|
|
699
|
+
placeholder={`0 ${selectedDestToken?.symbol || ""}`}
|
|
700
|
+
className={`w-full bg-transparent font-bold placeholder:trails-text-muted border-none outline-none ${
|
|
701
|
+
!amount
|
|
702
|
+
? "text-gray-400 dark:text-gray-500"
|
|
703
|
+
: "trails-text-primary"
|
|
704
|
+
} ${isLoadingQuote && tradeType === TradeType.EXACT_INPUT ? "animate-pulse" : ""}`}
|
|
705
|
+
style={buyInputStyles}
|
|
706
|
+
readOnly={
|
|
707
|
+
tradeType === TradeType.EXACT_INPUT && isLoadingQuote
|
|
708
|
+
}
|
|
709
|
+
/>
|
|
710
|
+
{isLoadingQuote && tradeType === TradeType.EXACT_INPUT && (
|
|
711
|
+
<div className="animate-spin rounded-full h-4 w-4 border-solid border-b-2 trails-primary" />
|
|
712
|
+
)}
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
|
|
716
|
+
{/* Destination Token Selection */}
|
|
717
|
+
<div className="relative" ref={tokenDropdownRef}>
|
|
718
|
+
<button
|
|
719
|
+
type="button"
|
|
720
|
+
onClick={() => setIsTokenDropdownOpen(!isTokenDropdownOpen)}
|
|
721
|
+
className={`flex items-center space-x-2 trails-border-radius-input px-2.5 py-1.5 border transition-colors cursor-pointer ${
|
|
722
|
+
selectedDestToken
|
|
723
|
+
? "trails-bg-card hover:trails-hover-bg trails-border-primary"
|
|
724
|
+
: "bg-blue-500 hover:bg-blue-600 border-blue-500 text-white"
|
|
725
|
+
}`}
|
|
726
|
+
>
|
|
727
|
+
{selectedDestToken ? (
|
|
728
|
+
<>
|
|
729
|
+
<TokenImage
|
|
730
|
+
symbol={selectedDestToken.symbol}
|
|
731
|
+
imageUrl={selectedDestToken.imageUrl}
|
|
732
|
+
chainId={(selectedDestToken as any)?.chainId}
|
|
733
|
+
size={20}
|
|
734
|
+
/>
|
|
735
|
+
<span className="font-medium trails-text-primary text-sm">
|
|
736
|
+
{selectedDestToken.symbol}
|
|
737
|
+
</span>
|
|
738
|
+
<ChevronDown className="w-3.5 h-3.5 trails-text-muted" />
|
|
739
|
+
</>
|
|
740
|
+
) : (
|
|
741
|
+
<>
|
|
742
|
+
<span className="font-medium text-sm text-white">
|
|
743
|
+
Select Token
|
|
744
|
+
</span>
|
|
745
|
+
<ChevronDown className="w-3.5 h-3.5 text-white" />
|
|
746
|
+
</>
|
|
747
|
+
)}
|
|
748
|
+
</button>
|
|
749
|
+
|
|
750
|
+
{/* Token Selector Modal */}
|
|
751
|
+
{isTokenDropdownOpen && (
|
|
752
|
+
<div className="absolute right-0 bottom-full mb-1 w-80 trails-bg-card rounded-lg shadow-lg border trails-border-primary max-h-80 overflow-hidden z-10 translate-y-32">
|
|
753
|
+
<div className="p-2">
|
|
754
|
+
<TokenSelector
|
|
755
|
+
onTokenSelect={(token: any) => {
|
|
756
|
+
const formattedToken = {
|
|
757
|
+
...token,
|
|
758
|
+
decimals:
|
|
759
|
+
token.contractInfo?.decimals || token.decimals,
|
|
760
|
+
contractInfo: {
|
|
761
|
+
decimals:
|
|
762
|
+
token.contractInfo?.decimals || token.decimals,
|
|
763
|
+
},
|
|
764
|
+
} as any
|
|
765
|
+
setDestinationToken(formattedToken)
|
|
766
|
+
setSelectedDestToken(formattedToken)
|
|
767
|
+
logger.console.log(
|
|
768
|
+
"[trails-sdk] selected destination token",
|
|
769
|
+
token,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
setIsTokenDropdownOpen(false)
|
|
773
|
+
// Track the token selection
|
|
774
|
+
onTrackToken?.(token)
|
|
775
|
+
}}
|
|
776
|
+
onError={onError}
|
|
777
|
+
fundMethod={fundMethod}
|
|
778
|
+
allSupportedTokens={true}
|
|
779
|
+
showContinueButton={false}
|
|
780
|
+
compactMode={false}
|
|
781
|
+
recentTokens={recentTokens}
|
|
782
|
+
onRecentTokenSelect={(token) => {
|
|
783
|
+
const formattedToken = {
|
|
784
|
+
...token,
|
|
785
|
+
decimals: token.decimals,
|
|
786
|
+
contractInfo: {
|
|
787
|
+
decimals: token.decimals,
|
|
788
|
+
},
|
|
789
|
+
} as any
|
|
790
|
+
setDestinationToken(formattedToken)
|
|
791
|
+
setSelectedDestToken(formattedToken)
|
|
792
|
+
setIsTokenDropdownOpen(false)
|
|
793
|
+
onRecentTokenSelect?.(token)
|
|
794
|
+
}}
|
|
795
|
+
/>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
)}
|
|
799
|
+
</div>
|
|
800
|
+
</div>
|
|
801
|
+
|
|
802
|
+
{/* Bottom Info Row */}
|
|
803
|
+
<div className="mt-2 flex justify-between items-center">
|
|
804
|
+
{/* Destination Amount USD from Quote */}
|
|
805
|
+
{(prepareSendQuote?.destinationAmountUsdDisplay ||
|
|
806
|
+
(isLoadingQuote && tradeType === TradeType.EXACT_INPUT)) && (
|
|
807
|
+
<div className="text-xs trails-text-muted">
|
|
808
|
+
≈{" "}
|
|
809
|
+
{isLoadingQuote && tradeType === TradeType.EXACT_INPUT
|
|
810
|
+
? "$0.00"
|
|
811
|
+
: prepareSendQuote?.destinationAmountUsdDisplay}
|
|
812
|
+
</div>
|
|
813
|
+
)}
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
|
|
817
|
+
{/* Fee Options */}
|
|
818
|
+
<FeeOptions
|
|
819
|
+
options={feeOptions}
|
|
820
|
+
selectedOption={selectedFeeToken ?? undefined}
|
|
821
|
+
onSelect={setSelectedFeeToken}
|
|
822
|
+
/>
|
|
823
|
+
|
|
824
|
+
{/* Warning Messages - Show only one at a time */}
|
|
825
|
+
<ErrorDisplay
|
|
826
|
+
errorPrettified={quoteErrorPrettified}
|
|
827
|
+
error={quoteError}
|
|
828
|
+
severity="warning"
|
|
829
|
+
/>
|
|
830
|
+
|
|
831
|
+
{prepareSendQuote?.noSufficientBalance ? (
|
|
832
|
+
<div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
|
|
833
|
+
<div className="flex items-center space-x-2">
|
|
834
|
+
<svg
|
|
835
|
+
className="w-4 h-4 text-amber-500 flex-shrink-0"
|
|
836
|
+
fill="none"
|
|
837
|
+
stroke="currentColor"
|
|
838
|
+
viewBox="0 0 24 24"
|
|
839
|
+
aria-hidden="true"
|
|
840
|
+
>
|
|
841
|
+
<path
|
|
842
|
+
strokeLinecap="round"
|
|
843
|
+
strokeLinejoin="round"
|
|
844
|
+
strokeWidth={2}
|
|
845
|
+
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"
|
|
846
|
+
/>
|
|
847
|
+
</svg>
|
|
848
|
+
<p className="text-sm text-amber-600 dark:text-amber-400">
|
|
849
|
+
Insufficient balance to complete this transaction. Required{" "}
|
|
850
|
+
{prepareSendQuote?.originAmountDisplay} {originToken?.symbol}
|
|
851
|
+
</p>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
) : prepareSendQuote?.minimumNotMet ? (
|
|
855
|
+
<div className="px-2 py-3 rounded-lg bg-amber-500/10 border border-solid border-amber-500/30">
|
|
856
|
+
<div className="flex items-center space-x-2">
|
|
857
|
+
<svg
|
|
858
|
+
className="w-4 h-4 text-amber-500 flex-shrink-0"
|
|
859
|
+
fill="none"
|
|
860
|
+
stroke="currentColor"
|
|
861
|
+
viewBox="0 0 24 24"
|
|
862
|
+
aria-hidden="true"
|
|
863
|
+
>
|
|
864
|
+
<path
|
|
865
|
+
strokeLinecap="round"
|
|
866
|
+
strokeLinejoin="round"
|
|
867
|
+
strokeWidth={2}
|
|
868
|
+
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"
|
|
869
|
+
/>
|
|
870
|
+
</svg>
|
|
871
|
+
<p className="text-sm text-amber-600 dark:text-amber-400">
|
|
872
|
+
Please enter an amount above{" "}
|
|
873
|
+
{formatUsdAmountDisplay(MINIMUM_USD_AMOUNT_FOR_SWAP)} otherwise
|
|
874
|
+
transfer may fail
|
|
875
|
+
</p>
|
|
876
|
+
</div>
|
|
877
|
+
</div>
|
|
878
|
+
) : null}
|
|
879
|
+
|
|
880
|
+
{/* Quote Details */}
|
|
881
|
+
{prepareSendQuote && (
|
|
882
|
+
<div className="space-y-2">
|
|
883
|
+
<QuoteDetails
|
|
884
|
+
quote={prepareSendQuote}
|
|
885
|
+
showContent={true}
|
|
886
|
+
swapMode={true}
|
|
887
|
+
/>
|
|
888
|
+
</div>
|
|
889
|
+
)}
|
|
890
|
+
|
|
891
|
+
<button
|
|
892
|
+
type="submit"
|
|
893
|
+
disabled={
|
|
894
|
+
!amount ||
|
|
895
|
+
isSubmitting ||
|
|
896
|
+
!destinationTokenAddress ||
|
|
897
|
+
!isValidCustomToken ||
|
|
898
|
+
isLoadingQuote ||
|
|
899
|
+
!prepareSendQuote ||
|
|
900
|
+
prepareSendQuote?.noSufficientBalance ||
|
|
901
|
+
isSameTokenWithoutCustomCalldata
|
|
902
|
+
}
|
|
903
|
+
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"
|
|
904
|
+
>
|
|
905
|
+
{isSubmitting ? (
|
|
906
|
+
<div className="flex items-center justify-center">
|
|
907
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2 trails-text-muted" />
|
|
908
|
+
<span>{buttonText}</span>
|
|
909
|
+
</div>
|
|
910
|
+
) : isSameTokenWithoutCustomCalldata ? (
|
|
911
|
+
"Select Different Tokens"
|
|
912
|
+
) : prepareSendQuote?.noSufficientBalance ? (
|
|
913
|
+
"Insufficient Balance"
|
|
914
|
+
) : (
|
|
915
|
+
buttonText
|
|
916
|
+
)}
|
|
917
|
+
</button>
|
|
918
|
+
</form>
|
|
919
|
+
</div>
|
|
920
|
+
)
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
export default ClassicSwap
|