0xtrails 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/aave.d.ts.map +1 -1
  2. package/dist/{ccip-ru_Yzdas.js → ccip-BKavX04a.js} +13 -13
  3. package/dist/constants.d.ts +2 -0
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/fees.d.ts +11 -17
  6. package/dist/fees.d.ts.map +1 -1
  7. package/dist/gasless.d.ts.map +1 -1
  8. package/dist/{index-Si7cO9V7.js → index-D5kULpIU.js} +20430 -20133
  9. package/dist/index.js +425 -847
  10. package/dist/intents.d.ts +1 -2
  11. package/dist/intents.d.ts.map +1 -1
  12. package/dist/prepareSend.d.ts.map +1 -1
  13. package/dist/recover.d.ts +8 -9
  14. package/dist/recover.d.ts.map +1 -1
  15. package/dist/tokenBalances.d.ts +51 -0
  16. package/dist/tokenBalances.d.ts.map +1 -1
  17. package/dist/trailsRouter.d.ts +15 -0
  18. package/dist/trailsRouter.d.ts.map +1 -1
  19. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts +1 -3
  20. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts.map +1 -1
  21. package/dist/transactionIntent/deposits/standardDeposit.d.ts +1 -3
  22. package/dist/transactionIntent/deposits/standardDeposit.d.ts.map +1 -1
  23. package/dist/transactionIntent/handlers/crossChain.d.ts +2 -4
  24. package/dist/transactionIntent/handlers/crossChain.d.ts.map +1 -1
  25. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts +5 -4
  26. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts.map +1 -1
  27. package/dist/transactionIntent/quote/normalizeQuote.d.ts +1 -1
  28. package/dist/transactionIntent/quote/normalizeQuote.d.ts.map +1 -1
  29. package/dist/transactionIntent/quote/quoteHelpers.d.ts +1 -1
  30. package/dist/transactionIntent/quote/quoteHelpers.d.ts.map +1 -1
  31. package/dist/transactionIntent/types.d.ts +11 -18
  32. package/dist/transactionIntent/types.d.ts.map +1 -1
  33. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  34. package/dist/widget/components/ClassicSwap.d.ts +1 -0
  35. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  36. package/dist/widget/components/DynamicSizeInputField.d.ts.map +1 -1
  37. package/dist/widget/components/Fund.d.ts.map +1 -1
  38. package/dist/widget/components/FundSwap.d.ts +1 -0
  39. package/dist/widget/components/FundSwap.d.ts.map +1 -1
  40. package/dist/widget/components/Pay.d.ts.map +1 -1
  41. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  42. package/dist/widget/components/SlippageToleranceSettings.d.ts +2 -1
  43. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  44. package/dist/widget/components/Swap.d.ts +1 -0
  45. package/dist/widget/components/Swap.d.ts.map +1 -1
  46. package/dist/widget/components/WidgetProviders.d.ts.map +1 -1
  47. package/dist/widget/css/compiled.css +1 -1
  48. package/dist/widget/hooks/useDefaultDestinationToken.d.ts +20 -0
  49. package/dist/widget/hooks/useDefaultDestinationToken.d.ts.map +1 -0
  50. package/dist/widget/hooks/{useDefaultTokenSelection.d.ts → useDefaultOriginToken.d.ts} +4 -16
  51. package/dist/widget/hooks/useDefaultOriginToken.d.ts.map +1 -0
  52. package/dist/widget/hooks/useQuote.d.ts +94 -35
  53. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  54. package/dist/widget/hooks/useSendForm.d.ts +2 -2
  55. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  56. package/dist/widget/hooks/useTrailsSendTransaction.d.ts.map +1 -1
  57. package/dist/widget/index.js +1 -1
  58. package/dist/widget/widget.d.ts.map +1 -1
  59. package/package.json +2 -2
  60. package/src/aave.ts +4 -0
  61. package/src/constants.ts +4 -0
  62. package/src/fees.ts +47 -72
  63. package/src/gasless.ts +62 -32
  64. package/src/intents.ts +1 -3
  65. package/src/morpho.ts +1 -1
  66. package/src/prepareSend.ts +42 -6
  67. package/src/recover.ts +116 -172
  68. package/src/tokenBalances.ts +301 -1
  69. package/src/trailsRouter.ts +77 -0
  70. package/src/transactionIntent/deposits/depositOrchestrator.ts +0 -6
  71. package/src/transactionIntent/deposits/standardDeposit.ts +167 -184
  72. package/src/transactionIntent/handlers/crossChain.ts +8 -11
  73. package/src/transactionIntent/handlers/sameChainSameToken.ts +619 -608
  74. package/src/transactionIntent/quote/normalizeQuote.ts +32 -46
  75. package/src/transactionIntent/quote/quoteHelpers.ts +4 -2
  76. package/src/transactionIntent/types.ts +11 -18
  77. package/src/widget/compiled.css +1 -1
  78. package/src/widget/components/AccountIntentTransactionHistory.tsx +50 -18
  79. package/src/widget/components/ClassicSwap.tsx +158 -63
  80. package/src/widget/components/DynamicSizeInputField.tsx +2 -0
  81. package/src/widget/components/Fund.tsx +12 -11
  82. package/src/widget/components/FundSwap.tsx +1 -0
  83. package/src/widget/components/Pay.tsx +15 -14
  84. package/src/widget/components/QuoteDetails.tsx +18 -27
  85. package/src/widget/components/SlippageToleranceSettings.tsx +55 -25
  86. package/src/widget/components/Swap.tsx +1 -0
  87. package/src/widget/components/WidgetProviders.tsx +1 -6
  88. package/src/widget/hooks/useDefaultDestinationToken.tsx +173 -0
  89. package/src/widget/hooks/{useDefaultTokenSelection.tsx → useDefaultOriginToken.tsx} +58 -191
  90. package/src/widget/hooks/useQuote.ts +317 -79
  91. package/src/widget/hooks/useSendForm.ts +123 -764
  92. package/src/widget/hooks/useTrailsSendTransaction.ts +0 -2
  93. package/src/widget/widget.tsx +2 -0
  94. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +0 -1
@@ -280,7 +280,7 @@ export const QuoteDetails: React.FC<QuoteDetailsProps> = ({
280
280
  />
281
281
  </RowLabel>
282
282
  <RowValue>
283
- <span title={`$${quote.destGasUsd ?? 0}`}>
283
+ <span title={`$${quote.destinationGasUsd ?? 0}`}>
284
284
  {
285
285
  quote.trailsFeeBreakdown.destinationRelayFee
286
286
  .usdValue
@@ -301,37 +301,28 @@ export const QuoteDetails: React.FC<QuoteDetailsProps> = ({
301
301
  if (!quote?.trailsFeeBreakdown) return null
302
302
 
303
303
  // Check if provider fee is non-zero
304
- const hasProviderFee = (quote.providerUsd ?? 0) > 0
304
+ const hasProviderFee = (quote.providerFeeUsd ?? 0) > 0
305
305
 
306
306
  // Check if trails fee is non-zero
307
- const hasTrailsFee = (quote.trailsUsd ?? 0) > 0
307
+ const hasTrailsFee = (quote.trailsFeeUsd ?? 0) > 0
308
308
 
309
309
  if (!hasProviderFee && !hasTrailsFee) return null
310
310
 
311
311
  return (
312
312
  <RowGroup>
313
- {/* All Provider Fees = provider fee + swap fee */}
314
- {((quote.providerUsd ?? 0) > 0 ||
315
- (quote.swapFeesUsd ?? 0) > 0) &&
316
- quote.allProviderFeesUsdDisplay &&
317
- (() => {
318
- const allProviderFees =
319
- (quote.providerUsd ?? 0) + (quote.swapFeesUsd ?? 0)
320
- if (allProviderFees <= 0) return null
321
-
322
- return (
323
- <Row>
324
- <RowLabel tooltip="Total provider fees including bridge fees and swap fees">
325
- All provider fees
326
- </RowLabel>
327
- <RowValue>
328
- <span title={`$${quote.allProviderFeesUsd ?? 0}`}>
329
- {quote.allProviderFeesUsdDisplay}
330
- </span>
331
- </RowValue>
332
- </Row>
333
- )
334
- })()}
313
+ {/* All Provider Fees (swap spread is shown in priceImpact) */}
314
+ {(quote.totalProviderFeesUsd ?? 0) > 0 && (
315
+ <Row>
316
+ <RowLabel tooltip="Total provider fees for bridge and routing">
317
+ All provider fees
318
+ </RowLabel>
319
+ <RowValue>
320
+ <span title={`$${quote.totalProviderFeesUsd ?? 0}`}>
321
+ {quote.totalProviderFeesUsdDisplay}
322
+ </span>
323
+ </RowValue>
324
+ </Row>
325
+ )}
335
326
 
336
327
  {hasProviderFee &&
337
328
  quote.trailsFeeBreakdown.providerFee &&
@@ -394,7 +385,7 @@ export const QuoteDetails: React.FC<QuoteDetailsProps> = ({
394
385
  )}
395
386
  </RowLabel>
396
387
  <RowValue>
397
- <span title={`$${quote.providerUsd ?? 0}`}>
388
+ <span title={`$${quote.providerFeeUsd ?? 0}`}>
398
389
  {providerFee.usdValue}
399
390
  </span>
400
391
  </RowValue>
@@ -497,7 +488,7 @@ export const QuoteDetails: React.FC<QuoteDetailsProps> = ({
497
488
  <TriangleAlert className="size-3" />
498
489
  </span>
499
490
  )}
500
- {quote.priceImpact}%
491
+ {Number(quote.priceImpact).toFixed(2)}%
501
492
  </RowValue>
502
493
  </Row>
503
494
  {quote.priceImpactUsdDisplay && (
@@ -1,11 +1,12 @@
1
1
  import type React from "react"
2
2
  import { useState, useEffect } from "react"
3
3
  import { HelpCircle } from "lucide-react"
4
- import { useTrails } from "../providers/TrailsProvider.js"
4
+ import { SLIPPAGE_AUTO } from "../../constants.js"
5
5
 
6
6
  // Convert decimal format to percentage for display
7
7
  // Format: "0.05" (5%) -> "5"
8
8
  const decimalToPercentage = (decimal: string): string => {
9
+ if (decimal === SLIPPAGE_AUTO) return SLIPPAGE_AUTO
9
10
  const num = parseFloat(decimal)
10
11
  return (num * 100).toString()
11
12
  }
@@ -13,6 +14,7 @@ const decimalToPercentage = (decimal: string): string => {
13
14
  // Convert percentage to decimal format for storage
14
15
  // Format: "5" (5%) -> "0.05"
15
16
  const percentageToDecimal = (percentage: string): string => {
17
+ if (percentage === SLIPPAGE_AUTO) return SLIPPAGE_AUTO
16
18
  const num = parseFloat(percentage)
17
19
  return (num / 100).toString()
18
20
  }
@@ -20,11 +22,23 @@ const percentageToDecimal = (percentage: string): string => {
20
22
  // Local storage key for user's slippage preference
21
23
  const SLIPPAGE_STORAGE_KEY = "trails-slippage-tolerance"
22
24
 
25
+ // Helper function to check if slippage is in AUTO mode
26
+ export const isSlippageAuto = (value: string | null | undefined): boolean => {
27
+ return value === SLIPPAGE_AUTO || value === null || value === undefined
28
+ }
29
+
23
30
  // Helper function to get current slippage tolerance (for use in SDK functions)
24
31
  // Returns the user's stored preference or falls back to the provided default
25
- export const getSlippageToleranceValue = (defaultValue: string): string => {
32
+ // Returns null if AUTO mode is selected (backend will calculate optimal slippage)
33
+ export const getSlippageToleranceValue = (
34
+ defaultValue: string,
35
+ ): string | null => {
26
36
  try {
27
37
  const stored = localStorage.getItem(SLIPPAGE_STORAGE_KEY)
38
+ if (stored === SLIPPAGE_AUTO || stored === null) {
39
+ // AUTO mode - return null so backend calculates optimal slippage
40
+ return null
41
+ }
28
42
  return stored || defaultValue
29
43
  } catch (_error) {
30
44
  return defaultValue
@@ -38,7 +52,6 @@ interface SlippageToleranceSettingsProps {
38
52
  export const SlippageToleranceSettings: React.FC<
39
53
  SlippageToleranceSettingsProps
40
54
  > = ({ className = "" }) => {
41
- const config = useTrails()
42
55
  const [displayValue, setDisplayValue] = useState("")
43
56
  const [showTooltip, setShowTooltip] = useState(false)
44
57
 
@@ -49,14 +62,14 @@ export const SlippageToleranceSettings: React.FC<
49
62
  if (stored) {
50
63
  setDisplayValue(decimalToPercentage(stored))
51
64
  } else {
52
- // Default to config value
53
- setDisplayValue(decimalToPercentage(String(config.slippageTolerance)))
65
+ // Default to AUTO mode
66
+ setDisplayValue(SLIPPAGE_AUTO)
54
67
  }
55
68
  } catch (_error) {
56
- // Fallback to config if localStorage fails
57
- setDisplayValue(decimalToPercentage(String(config.slippageTolerance)))
69
+ // Fallback to AUTO if localStorage fails
70
+ setDisplayValue(SLIPPAGE_AUTO)
58
71
  }
59
- }, [config.slippageTolerance])
72
+ }, [])
60
73
 
61
74
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
62
75
  const value = e.target.value
@@ -73,17 +86,13 @@ export const SlippageToleranceSettings: React.FC<
73
86
  }
74
87
 
75
88
  const handleInputBlur = () => {
76
- if (displayValue === "") {
77
- // Reset to stored value or config default
89
+ if (displayValue === "" || displayValue === SLIPPAGE_AUTO) {
90
+ // Reset to AUTO mode
91
+ setDisplayValue(SLIPPAGE_AUTO)
78
92
  try {
79
- const stored = localStorage.getItem(SLIPPAGE_STORAGE_KEY)
80
- if (stored) {
81
- setDisplayValue(decimalToPercentage(stored))
82
- } else {
83
- setDisplayValue(decimalToPercentage(String(config.slippageTolerance)))
84
- }
93
+ localStorage.setItem(SLIPPAGE_STORAGE_KEY, SLIPPAGE_AUTO)
85
94
  } catch (_error) {
86
- setDisplayValue(decimalToPercentage(String(config.slippageTolerance)))
95
+ // Ignore localStorage errors
87
96
  }
88
97
  return
89
98
  }
@@ -91,17 +100,17 @@ export const SlippageToleranceSettings: React.FC<
91
100
  const percentage = parseFloat(displayValue)
92
101
 
93
102
  // Validate range (0.01% to 50%)
94
- if (percentage < 0.01 || percentage > 50) {
95
- // Reset to stored value or config default
103
+ if (Number.isNaN(percentage) || percentage < 0.01 || percentage > 50) {
104
+ // Reset to stored value or AUTO default
96
105
  try {
97
106
  const stored = localStorage.getItem(SLIPPAGE_STORAGE_KEY)
98
- if (stored) {
107
+ if (stored && stored !== SLIPPAGE_AUTO) {
99
108
  setDisplayValue(decimalToPercentage(stored))
100
109
  } else {
101
- setDisplayValue(decimalToPercentage(String(config.slippageTolerance)))
110
+ setDisplayValue(SLIPPAGE_AUTO)
102
111
  }
103
112
  } catch (_error) {
104
- setDisplayValue(decimalToPercentage(String(config.slippageTolerance)))
113
+ setDisplayValue(SLIPPAGE_AUTO)
105
114
  }
106
115
  return
107
116
  }
@@ -124,6 +133,15 @@ export const SlippageToleranceSettings: React.FC<
124
133
  }
125
134
  }
126
135
 
136
+ const handleAutoClick = () => {
137
+ setDisplayValue(SLIPPAGE_AUTO)
138
+ try {
139
+ localStorage.setItem(SLIPPAGE_STORAGE_KEY, SLIPPAGE_AUTO)
140
+ } catch (error) {
141
+ console.warn("Failed to save slippage tolerance to localStorage:", error)
142
+ }
143
+ }
144
+
127
145
  const handlePresetClick = (percentage: number) => {
128
146
  const percentageStr = percentage.toString()
129
147
  setDisplayValue(percentageStr)
@@ -152,8 +170,9 @@ export const SlippageToleranceSettings: React.FC<
152
170
  </button>
153
171
  {showTooltip && (
154
172
  <div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 w-64 p-2 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded-md shadow-lg z-10">
155
- Higher slippage tolerance allows for larger price movements but
156
- may result in less favorable rates. Maximum 50%.
173
+ Auto mode calculates optimal slippage based on transaction size
174
+ and routing. Higher slippage allows for larger price movements but
175
+ may result in less favorable rates.
157
176
  <div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
158
177
  </div>
159
178
  )}
@@ -162,7 +181,18 @@ export const SlippageToleranceSettings: React.FC<
162
181
 
163
182
  {/* Preset buttons */}
164
183
  <div className="flex items-center space-x-2">
165
- {[0.1, 0.5, 1, 3, 5].map((preset) => (
184
+ <button
185
+ type="button"
186
+ onClick={handleAutoClick}
187
+ className={`px-2 py-1 text-xs font-medium trails-border-radius-container border border-solid transition-colors cursor-pointer ${
188
+ displayValue === SLIPPAGE_AUTO
189
+ ? "bg-blue-500 text-white border-blue-500"
190
+ : "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"
191
+ }`}
192
+ >
193
+ Auto
194
+ </button>
195
+ {[0.5, 1, 3, 5].map((preset) => (
166
196
  <button
167
197
  key={preset}
168
198
  type="button"
@@ -44,6 +44,7 @@ interface SwapProps {
44
44
  onRecentTokenSelect?: (token: Token) => void
45
45
  onTrackToken?: (token: any) => void
46
46
  isSequenceWallet?: boolean
47
+ exactInputOnly?: boolean
47
48
  }
48
49
 
49
50
  export const Swap: React.FC<SwapProps> = (props) => {
@@ -15,7 +15,6 @@ import { BalanceVisibleProvider } from "../hooks/useBalanceVisible.js"
15
15
  import { ThemeProvider as ThemePreferenceProvider } from "../hooks/useTheme.js"
16
16
  import { SelectedFundMethodProvider } from "../hooks/useSelectedFundMethod.js"
17
17
  import { EarnPoolProvider } from "../hooks/useEarnPool.js"
18
- import { DefaultTokenSelectionProvider } from "../hooks/useDefaultTokenSelection.js"
19
18
 
20
19
  // Default props for hook modal (minimal required config)
21
20
  const DEFAULT_HOOK_MODAL_PROPS: TrailsWidgetProps = {
@@ -52,11 +51,7 @@ export function WidgetProviders({
52
51
  <BalanceVisibleProvider>
53
52
  <ThemePreferenceProvider>
54
53
  <SelectedFundMethodProvider>
55
- <EarnPoolProvider>
56
- <DefaultTokenSelectionProvider>
57
- {children}
58
- </DefaultTokenSelectionProvider>
59
- </EarnPoolProvider>
54
+ <EarnPoolProvider>{children}</EarnPoolProvider>
60
55
  </SelectedFundMethodProvider>
61
56
  </ThemePreferenceProvider>
62
57
  </BalanceVisibleProvider>
@@ -0,0 +1,173 @@
1
+ import { useMemo, useRef } from "react"
2
+ import { useAccount } from "wagmi"
3
+ import { base, arbitrum } from "viem/chains"
4
+ import type { Address } from "ox"
5
+ import { useSupportedTokens } from "../../tokens.js"
6
+ import type { Token } from "../../tokens.js"
7
+ import { useTokenBalances } from "../../tokenBalances.js"
8
+ import { logger } from "../../logger.js"
9
+
10
+ /**
11
+ * Hook for intelligent default destination token selection.
12
+ *
13
+ * This hook calculates the best destination token based on:
14
+ * - Origin token's chain (to minimize bridging costs)
15
+ * - Token availability on destination chain
16
+ * - User's existing balances for accurate balance display
17
+ *
18
+ * Only calculates destination when toToken/toChainId are NOT specified.
19
+ *
20
+ * @param originToken - The selected origin token
21
+ * @returns defaultDestinationToken - Best destination token or null
22
+ * @returns isLoading - Whether token data is still loading
23
+ */
24
+ export function useDefaultDestinationToken(originToken: Token | null) {
25
+ const { address } = useAccount()
26
+
27
+ const { supportedTokens, isLoadingTokens } = useSupportedTokens()
28
+
29
+ const { sortedTokens, isLoadingSortedTokens } = useTokenBalances(
30
+ address as Address.Address,
31
+ )
32
+
33
+ const isLoading = isLoadingTokens || isLoadingSortedTokens
34
+
35
+ // Use a ref to prevent unnecessary recalculations on balance-only updates
36
+ const stableDestTokenRef = useRef<Token | null>(null)
37
+ const lastOriginTokenKey = useRef<string>("")
38
+
39
+ const defaultDestinationToken = useMemo(() => {
40
+ // Don't compute destination if no origin token or still loading
41
+ if (!originToken || !supportedTokens?.length || !sortedTokens?.length) {
42
+ stableDestTokenRef.current = null
43
+ return null
44
+ }
45
+
46
+ // Create a key based on origin token identity (not balance)
47
+ const originTokenKey = `${originToken.symbol}-${originToken.chainId}-${originToken.contractAddress || "native"}`
48
+
49
+ // Only recalculate if origin token has actually changed
50
+ const shouldRecalculate =
51
+ originTokenKey !== lastOriginTokenKey.current ||
52
+ !stableDestTokenRef.current
53
+
54
+ if (!shouldRecalculate && stableDestTokenRef.current) {
55
+ return stableDestTokenRef.current
56
+ }
57
+
58
+ lastOriginTokenKey.current = originTokenKey
59
+
60
+ // Determine destination chain: Base if origin is not Base, Arbitrum if origin is Base
61
+ const defaultDestChainId =
62
+ originToken.chainId === base.id ? arbitrum.id : base.id
63
+
64
+ // Find USDC on destination chain first (preferred stable coin)
65
+ const usdcOnDestChain = supportedTokens.find(
66
+ (token: Token) =>
67
+ token.chainId === defaultDestChainId && token.symbol === "USDC",
68
+ )
69
+
70
+ let destToken: Token | null = null
71
+
72
+ if (usdcOnDestChain) {
73
+ destToken = usdcOnDestChain
74
+ logger.console.log(
75
+ "[trails-sdk] Selected USDC as destination token on chain:",
76
+ defaultDestChainId,
77
+ )
78
+ } else {
79
+ // Fallback: Find matching token on destination chain
80
+ const destChainTokens = supportedTokens.filter(
81
+ (token: Token) => token.chainId === defaultDestChainId,
82
+ )
83
+
84
+ // Try to find same symbol on destination chain first
85
+ const sameSymbolOnDestChain = destChainTokens.find((token: Token) => {
86
+ return token.symbol.toUpperCase() === originToken.symbol.toUpperCase()
87
+ })
88
+
89
+ if (sameSymbolOnDestChain) {
90
+ destToken = sameSymbolOnDestChain
91
+ logger.console.log(
92
+ "[trails-sdk] Selected same symbol token on destination chain:",
93
+ sameSymbolOnDestChain.symbol,
94
+ )
95
+ } else {
96
+ // Fallback: ETH on destination chain > first available token on destination chain
97
+ destToken =
98
+ destChainTokens.find((token: Token) => token.symbol === "ETH") ||
99
+ destChainTokens[0] ||
100
+ null
101
+
102
+ if (destToken) {
103
+ logger.console.log(
104
+ "[trails-sdk] Selected fallback token on destination chain:",
105
+ destToken.symbol,
106
+ )
107
+ }
108
+ }
109
+ }
110
+
111
+ if (!destToken) {
112
+ logger.console.log(
113
+ "[trails-sdk] No suitable destination token found on chain:",
114
+ defaultDestChainId,
115
+ )
116
+ return null
117
+ }
118
+
119
+ const decimals = destToken.decimals
120
+ if (!decimals) {
121
+ logger.console.warn(
122
+ "[trails-sdk] Missing decimals for destination token, skipping:",
123
+ destToken,
124
+ )
125
+ return null
126
+ }
127
+
128
+ // Try to find this token in user's balances to get the actual balance
129
+ const matchingUserToken = sortedTokens.find((t) => {
130
+ if (t.chainId !== destToken.chainId) return false
131
+
132
+ const isDestNative = destToken.isNativeToken ?? false
133
+ const isUserNative = t.isNativeToken ?? false
134
+
135
+ if (isDestNative && isUserNative) return true
136
+ if (isDestNative || isUserNative) return false
137
+
138
+ return (
139
+ t.contractAddress?.toLowerCase() ===
140
+ destToken.contractAddress?.toLowerCase()
141
+ )
142
+ })
143
+
144
+ const finalDestinationToken = {
145
+ ...destToken,
146
+ balance: matchingUserToken?.balance ?? "0",
147
+ balanceUsdFormatted: matchingUserToken?.balanceUsdFormatted ?? "0",
148
+ priceUsd: matchingUserToken?.priceUsd ?? 0,
149
+ decimals,
150
+ }
151
+
152
+ logger.console.log("[trails-sdk] Default destination token selection:", {
153
+ destination: {
154
+ symbol: finalDestinationToken.symbol,
155
+ chainId: finalDestinationToken.chainId,
156
+ hasUserBalance: !!matchingUserToken,
157
+ userBalanceUsd: matchingUserToken?.balanceUsdFormatted ?? "0",
158
+ },
159
+ originChain: originToken.chainId,
160
+ destinationChain: defaultDestChainId,
161
+ })
162
+
163
+ // Store the calculated result in ref for stability
164
+ stableDestTokenRef.current = finalDestinationToken
165
+
166
+ return finalDestinationToken
167
+ }, [originToken, supportedTokens, sortedTokens])
168
+
169
+ return {
170
+ defaultDestinationToken,
171
+ isLoading,
172
+ }
173
+ }