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,475 @@
1
+ import { createContext, useContext, useMemo } from "react"
2
+ import type { ReactNode } from "react"
3
+ import { useAccount } from "wagmi"
4
+ import type { Address } from "ox"
5
+ import { base, arbitrum } from "viem/chains"
6
+ import { zeroAddress } from "viem"
7
+ import { useWidgetProps } from "./useWidgetProps.js"
8
+ import { useTargetAmount } from "./useTargetAmount.js"
9
+ import { useTokenBalances } from "../../tokenBalances.js"
10
+ import type { TokenBalanceExtended } from "../../tokenBalances.js"
11
+ import { useIndexerGatewayClient } from "../../indexerClient.js"
12
+ import { useSupportedTokens } from "../../tokens.js"
13
+ import { getChainInfo } from "../../chains.js"
14
+ import { logger } from "../../logger.js"
15
+ import { MINIMUM_USD_AMOUNT_FOR_SWAP } from "../../constants.js"
16
+
17
+ const MINIMUM_24H_VOLUME_USD = 1_000_000 // $1M
18
+
19
+ /**
20
+ * Hook for intelligent default token selection in 0xtrails widget.
21
+ *
22
+ * When toToken and toChainId are specified (user wants specific destination):
23
+ * 1. Same-chain, same-token with balance >= targetAmountUsd (no swap, no bridge) - FASTEST
24
+ * 2. Same-chain, different-token with balance >= targetAmountUsd (swap only) - FAST
25
+ * - Ordered by: 24h volume > $1M first, then highest balance USD
26
+ * 3. Different-chain, same-token with balance >= targetAmountUsd (bridge only) - SLOWER
27
+ * - Ordered by: 24h volume > $1M first, then highest balance USD
28
+ * 4. Different-chain, different-token with balance >= targetAmountUsd (swap + bridge) - SLOWEST
29
+ * - Ordered by: 24h volume > $1M first, then highest balance USD
30
+ * 5. Fallback: Any token with balance > 0 if none can cover target amount
31
+ *
32
+ * When toToken/toChainId NOT specified (general send/bridge):
33
+ * - Select highest balance token with 24h volume > $1M
34
+ * - Fallback to any token with balance > 0
35
+ *
36
+ * Destination Token Logic (only when toToken/toChainId not provided):
37
+ * - Matches origin token symbol on different chain when possible
38
+ * - Default destination chain: Base (unless origin is Base, then Arbitrum)
39
+ * - Fallback priority: USDC > ETH > first available token
40
+ *
41
+ * @returns defaultOriginToken - Best origin token that can cover targetAmountUsd (if specified)
42
+ * @returns defaultDestinationToken - Matching token on different chain (null if toToken/toChainId set)
43
+ * @returns isLoading - Whether token data is still loading
44
+ */
45
+ export interface DefaultToken {
46
+ id: number
47
+ name: string
48
+ symbol: string
49
+ balance: string
50
+ imageUrl: string
51
+ chainId: number
52
+ contractAddress: string
53
+ balanceUsdFormatted: string
54
+ tokenPriceUsd: number
55
+ decimals: number // Top-level decimals for compatibility
56
+ contractInfo?: {
57
+ decimals: number
58
+ symbol: string
59
+ name: string
60
+ }
61
+ }
62
+
63
+ export interface UseDefaultTokenSelectionReturn {
64
+ defaultOriginToken: DefaultToken | null
65
+ defaultDestinationToken: DefaultToken | null
66
+ isLoading: boolean
67
+ }
68
+
69
+ function getToken24hVolume(token: TokenBalanceExtended): number {
70
+ const price = token.price as any
71
+ if (!price) return 0
72
+
73
+ return Number(
74
+ price.price24hVol?.value ||
75
+ price.volume24h ||
76
+ price.volume_24h ||
77
+ price.vol24h ||
78
+ price.dailyVolume ||
79
+ 0,
80
+ )
81
+ }
82
+
83
+ function tokenMatches(
84
+ token: TokenBalanceExtended,
85
+ targetSymbolOrAddress: string,
86
+ targetChainId?: number,
87
+ ): boolean {
88
+ const isNative =
89
+ !("contractAddress" in token) || token.contractAddress === zeroAddress
90
+
91
+ // Check chain match first
92
+ const chainMatches = !targetChainId || token.chainId === targetChainId
93
+ if (!chainMatches) return false
94
+
95
+ // Check if target is an address (starts with 0x and is longer than 5 chars)
96
+ const isTargetAddress =
97
+ targetSymbolOrAddress.startsWith("0x") && targetSymbolOrAddress.length > 5
98
+
99
+ if (isNative) {
100
+ const chainInfo = getChainInfo(token.chainId)
101
+ const nativeSymbol = chainInfo?.nativeCurrency.symbol || "ETH"
102
+
103
+ // For native tokens, check if target is zero address or matches symbol
104
+ if (isTargetAddress) {
105
+ return targetSymbolOrAddress.toLowerCase() === zeroAddress.toLowerCase()
106
+ }
107
+
108
+ return nativeSymbol.toUpperCase() === targetSymbolOrAddress.toUpperCase()
109
+ }
110
+
111
+ // For non-native tokens, check both address and symbol
112
+ if (isTargetAddress) {
113
+ return (
114
+ token.contractAddress.toLowerCase() ===
115
+ targetSymbolOrAddress.toLowerCase()
116
+ )
117
+ }
118
+
119
+ return (
120
+ token.contractInfo?.symbol.toUpperCase() ===
121
+ targetSymbolOrAddress.toUpperCase()
122
+ )
123
+ }
124
+
125
+ function useDefaultTokenSelectionInternal(): UseDefaultTokenSelectionReturn {
126
+ const { toToken, toChainId } = useWidgetProps()
127
+ const { targetAmountUsd } = useTargetAmount()
128
+ const { address } = useAccount()
129
+ const indexerGatewayClient = useIndexerGatewayClient()
130
+
131
+ const { sortedTokens, isLoadingSortedTokens } = useTokenBalances(
132
+ address as Address.Address,
133
+ indexerGatewayClient,
134
+ )
135
+
136
+ const { supportedTokens, isLoadingTokens: isLoadingSupportedTokens } =
137
+ useSupportedTokens()
138
+
139
+ const isLoading = isLoadingSortedTokens || isLoadingSupportedTokens
140
+
141
+ const { defaultOriginToken, defaultDestinationToken } = useMemo(() => {
142
+ if (!sortedTokens?.length || !supportedTokens?.length) {
143
+ return {
144
+ defaultOriginToken: null,
145
+ defaultDestinationToken: null,
146
+ }
147
+ }
148
+
149
+ // Determine if we should compute destination token
150
+ // Only compute destination defaults if toToken and toChainId are NOT set
151
+ const shouldComputeDestination = !toToken && !toChainId
152
+
153
+ // Helper to convert TokenBalanceExtended to DefaultToken
154
+ const toDefaultToken = (
155
+ token: TokenBalanceExtended,
156
+ ): DefaultToken | null => {
157
+ const isNative =
158
+ !("contractAddress" in token) || token.contractAddress === zeroAddress
159
+ const chainInfo = getChainInfo(token.chainId)
160
+
161
+ if (isNative) {
162
+ // Native tokens - use token properties as-is
163
+ const tokenWithInfo = token as any
164
+ const imageUrl = tokenWithInfo.logoURI || tokenWithInfo.imageUrl || ""
165
+ const decimals = 18
166
+
167
+ return {
168
+ id: token.chainId,
169
+ name: chainInfo?.nativeCurrency.name || "Native Token",
170
+ symbol: chainInfo?.nativeCurrency.symbol || "ETH",
171
+ balance: token.balance,
172
+ imageUrl,
173
+ chainId: token.chainId,
174
+ contractAddress: zeroAddress,
175
+ balanceUsdFormatted: token.balanceUsdFormatted ?? "0",
176
+ tokenPriceUsd: token.price?.value ?? 0,
177
+ decimals, // Top-level decimals
178
+ contractInfo: {
179
+ decimals,
180
+ symbol: chainInfo?.nativeCurrency.symbol || "ETH",
181
+ name: chainInfo?.nativeCurrency.name || "Native Token",
182
+ },
183
+ }
184
+ } else {
185
+ // Non-native tokens - use contractInfo properties
186
+ const tokenWithInfo = token as any
187
+ const imageUrl =
188
+ tokenWithInfo.contractInfo?.logoURI || tokenWithInfo.imageUrl || ""
189
+ const decimals = token.contractInfo?.decimals
190
+ if (!decimals) {
191
+ throw new Error("Decimals not found")
192
+ }
193
+
194
+ return {
195
+ id: token.chainId,
196
+ name: token.contractInfo?.name || "Unknown Token",
197
+ symbol: token.contractInfo?.symbol || "???",
198
+ balance: token.balance,
199
+ imageUrl,
200
+ chainId: token.chainId,
201
+ contractAddress: token.contractAddress,
202
+ balanceUsdFormatted: token.balanceUsdFormatted ?? "0",
203
+ tokenPriceUsd: token.price?.value ?? 0,
204
+ decimals, // Top-level decimals
205
+ contractInfo: {
206
+ decimals,
207
+ symbol: token.contractInfo?.symbol ?? "???",
208
+ name: token.contractInfo?.name ?? "Unknown Token",
209
+ },
210
+ }
211
+ }
212
+ }
213
+
214
+ // Filter tokens with balance > 0
215
+ const tokensWithBalance = sortedTokens.filter((token) => {
216
+ const balanceUsd = token.balanceUsd ?? 0
217
+ return balanceUsd > 0
218
+ })
219
+
220
+ if (tokensWithBalance.length === 0) {
221
+ logger.console.log(
222
+ "[trails-sdk] No tokens with balance found for default selection",
223
+ )
224
+ return {
225
+ defaultOriginToken: null,
226
+ defaultDestinationToken: null,
227
+ }
228
+ }
229
+
230
+ // Helper to sort tokens by balance USD and volume
231
+ const sortTokensByBalanceAndVolume = (tokens: TokenBalanceExtended[]) => {
232
+ return [...tokens].sort((a, b) => {
233
+ const aVolume = getToken24hVolume(a)
234
+ const bVolume = getToken24hVolume(b)
235
+ const aHighVolume = aVolume >= MINIMUM_24H_VOLUME_USD
236
+ const bHighVolume = bVolume >= MINIMUM_24H_VOLUME_USD
237
+
238
+ // Prioritize high volume tokens first
239
+ if (aHighVolume && !bHighVolume) return -1
240
+ if (!aHighVolume && bHighVolume) return 1
241
+
242
+ // Then sort by balance USD (highest first)
243
+ const aBalanceUsd = a.balanceUsd ?? 0
244
+ const bBalanceUsd = b.balanceUsd ?? 0
245
+ return bBalanceUsd - aBalanceUsd
246
+ })
247
+ }
248
+
249
+ // Find the best origin token using intelligent selection
250
+ let bestOriginToken: TokenBalanceExtended | null = null
251
+
252
+ // When toToken and toChainId are specified (user wants specific destination)
253
+ if (toToken && toChainId) {
254
+ const targetChainId = Number(toChainId)
255
+
256
+ // Use targetAmountUsd if available, otherwise use minimum amount for swap when toToken is set
257
+ const effectiveTargetAmount =
258
+ targetAmountUsd ?? Number(MINIMUM_USD_AMOUNT_FOR_SWAP)
259
+
260
+ // Filter tokens that can cover the target amount
261
+ const tokensWithSufficientBalance = tokensWithBalance.filter(
262
+ (token) => (token.balanceUsd ?? 0) >= effectiveTargetAmount,
263
+ )
264
+
265
+ // Priority 1: Same-chain, same-token (no swap, no bridge) - FASTEST
266
+ const sameChainSameToken = tokensWithSufficientBalance.find((token) =>
267
+ tokenMatches(token, toToken, targetChainId),
268
+ )
269
+
270
+ if (sameChainSameToken) {
271
+ bestOriginToken = sameChainSameToken
272
+ logger.console.log(
273
+ "[trails-sdk] Selected same-chain, same-token (fastest)",
274
+ )
275
+ }
276
+
277
+ // Priority 2: Same-chain, different-token (swap only, no bridge) - FAST
278
+ if (!bestOriginToken) {
279
+ const sameChainTokens = tokensWithSufficientBalance.filter(
280
+ (token) => token.chainId === targetChainId,
281
+ )
282
+ const sortedSameChain = sortTokensByBalanceAndVolume(sameChainTokens)
283
+ bestOriginToken = sortedSameChain[0] ?? null
284
+ if (bestOriginToken) {
285
+ logger.console.log(
286
+ "[trails-sdk] Selected same-chain, different-token (fast)",
287
+ )
288
+ }
289
+ }
290
+
291
+ // Priority 3: Different-chain, same-token (bridge only, no swap) - SLOWER
292
+ if (!bestOriginToken) {
293
+ const sameTokenDiffChain = tokensWithSufficientBalance.filter(
294
+ (token) =>
295
+ tokenMatches(token, toToken) && token.chainId !== targetChainId,
296
+ )
297
+ const sortedSameToken = sortTokensByBalanceAndVolume(sameTokenDiffChain)
298
+ bestOriginToken = sortedSameToken[0] ?? null
299
+ if (bestOriginToken) {
300
+ logger.console.log(
301
+ "[trails-sdk] Selected different-chain, same-token (slower)",
302
+ )
303
+ }
304
+ }
305
+
306
+ // Priority 4: Different-chain, different-token (swap + bridge) - SLOWEST
307
+ if (!bestOriginToken) {
308
+ const diffChainDiffToken = tokensWithSufficientBalance.filter(
309
+ (token) => token.chainId !== targetChainId,
310
+ )
311
+ const sortedDiffChainDiffToken =
312
+ sortTokensByBalanceAndVolume(diffChainDiffToken)
313
+ bestOriginToken = sortedDiffChainDiffToken[0] ?? null
314
+ if (bestOriginToken) {
315
+ logger.console.log(
316
+ "[trails-sdk] Selected different-chain, different-token (slowest)",
317
+ )
318
+ }
319
+ }
320
+
321
+ // Fallback: If no token can cover target amount, use any token (sorted by balance/volume)
322
+ if (!bestOriginToken) {
323
+ const allTokensSorted = sortTokensByBalanceAndVolume(tokensWithBalance)
324
+ bestOriginToken = allTokensSorted[0] ?? null
325
+ logger.console.log(
326
+ "[trails-sdk] Fallback: No token can cover target amount, using highest balance token",
327
+ )
328
+ }
329
+ } else {
330
+ // When no specific destination is set, select highest value token with good liquidity
331
+ const sortedByBalanceAndVolume =
332
+ sortTokensByBalanceAndVolume(tokensWithBalance)
333
+ bestOriginToken = sortedByBalanceAndVolume[0] ?? null
334
+ logger.console.log(
335
+ "[trails-sdk] No destination specified, selected highest value token with best liquidity",
336
+ )
337
+ }
338
+
339
+ if (!bestOriginToken) {
340
+ return {
341
+ defaultOriginToken: null,
342
+ defaultDestinationToken: null,
343
+ }
344
+ }
345
+
346
+ const originToken = toDefaultToken(bestOriginToken)
347
+
348
+ if (!originToken) {
349
+ return {
350
+ defaultOriginToken: null,
351
+ defaultDestinationToken: null,
352
+ }
353
+ }
354
+
355
+ // Compute destination token only when toToken and toChainId are not provided
356
+ let destinationToken: DefaultToken | null = null
357
+
358
+ if (shouldComputeDestination) {
359
+ // Determine default destination chain: Base if origin is not Base, Arbitrum if origin is Base
360
+ const defaultDestChainId =
361
+ originToken.chainId === base.id ? arbitrum.id : base.id
362
+
363
+ // Find matching token on destination chain (prefer same symbol)
364
+ const destChainTokens = supportedTokens.filter(
365
+ (token: any) => token.chainId === defaultDestChainId,
366
+ )
367
+
368
+ let destToken: any = null
369
+
370
+ // Try to find same symbol on dest chain
371
+ const sameSymbolToken = destChainTokens.find((token: any) => {
372
+ return token.symbol.toUpperCase() === originToken.symbol.toUpperCase()
373
+ })
374
+
375
+ if (sameSymbolToken) {
376
+ destToken = sameSymbolToken
377
+ } else {
378
+ // Fallback: USDC > ETH > first available token
379
+ destToken =
380
+ destChainTokens.find((token: any) => token.symbol === "USDC") ||
381
+ destChainTokens.find((token: any) => token.symbol === "ETH") ||
382
+ destChainTokens[0]
383
+ }
384
+
385
+ const decimals = destToken?.decimals
386
+ if (!decimals) {
387
+ throw new Error("Decimals not found")
388
+ }
389
+
390
+ destinationToken = destToken
391
+ ? {
392
+ id: destToken.chainId,
393
+ name: destToken.name,
394
+ symbol: destToken.symbol,
395
+ balance: "0", // Destination token doesn't have balance yet
396
+ imageUrl: destToken.imageUrl || "",
397
+ chainId: destToken.chainId,
398
+ contractAddress: destToken.contractAddress || zeroAddress,
399
+ balanceUsdFormatted: "0",
400
+ tokenPriceUsd: 0,
401
+ decimals, // Top-level decimals
402
+ contractInfo: {
403
+ decimals,
404
+ symbol: destToken.symbol,
405
+ name: destToken.name,
406
+ },
407
+ }
408
+ : null
409
+ }
410
+
411
+ logger.console.log("[trails-sdk] Default token selection:", {
412
+ origin: originToken
413
+ ? {
414
+ symbol: originToken.symbol,
415
+ chainId: originToken.chainId,
416
+ balanceUsd: originToken.balanceUsdFormatted,
417
+ volume24h: getToken24hVolume(bestOriginToken),
418
+ }
419
+ : null,
420
+ destination: destinationToken
421
+ ? {
422
+ symbol: destinationToken.symbol,
423
+ chainId: destinationToken.chainId,
424
+ }
425
+ : null,
426
+ shouldComputeDestination,
427
+ toToken,
428
+ toChainId,
429
+ targetAmountUsd,
430
+ effectiveTargetAmount:
431
+ toToken && toChainId
432
+ ? (targetAmountUsd ?? Number(MINIMUM_USD_AMOUNT_FOR_SWAP))
433
+ : null,
434
+ })
435
+
436
+ return {
437
+ defaultOriginToken: originToken,
438
+ defaultDestinationToken: destinationToken,
439
+ }
440
+ }, [toToken, toChainId, sortedTokens, supportedTokens, targetAmountUsd])
441
+
442
+ return {
443
+ defaultOriginToken,
444
+ defaultDestinationToken,
445
+ isLoading,
446
+ }
447
+ }
448
+
449
+ // Context for sharing default token selection across components
450
+ const DefaultTokenSelectionContext =
451
+ createContext<UseDefaultTokenSelectionReturn | null>(null)
452
+
453
+ export function DefaultTokenSelectionProvider({
454
+ children,
455
+ }: {
456
+ children: ReactNode
457
+ }) {
458
+ const value = useDefaultTokenSelectionInternal()
459
+
460
+ return (
461
+ <DefaultTokenSelectionContext.Provider value={value}>
462
+ {children}
463
+ </DefaultTokenSelectionContext.Provider>
464
+ )
465
+ }
466
+
467
+ export function useDefaultTokenSelection(): UseDefaultTokenSelectionReturn {
468
+ const context = useContext(DefaultTokenSelectionContext)
469
+ if (!context) {
470
+ throw new Error(
471
+ "useDefaultTokenSelection must be used within a DefaultTokenSelectionProvider",
472
+ )
473
+ }
474
+ return context
475
+ }
@@ -1,10 +1,12 @@
1
1
  import { useState, useEffect, useCallback } from "react"
2
2
  import {
3
3
  getIntentTransactionHistory,
4
+ getAccountTransactionHistory,
4
5
  type IntentTransaction,
5
6
  type IntentTransactionHistoryResponse,
6
7
  } from "../../transactions.js"
7
8
  import { getTokenInfo, getSupportedTokens } from "../../tokens.js"
9
+ import { getExplorerUrl } from "../../explorer.js"
8
10
  import { logger } from "../../logger.js"
9
11
 
10
12
  export type UseIntentTransactionHistoryParams = {
@@ -241,10 +243,220 @@ export function useIntentTransactionHistory({
241
243
  }
242
244
  }
243
245
 
246
+ // Check if origin and destination are the same
247
+ const isSameIntentAddress =
248
+ transaction.originIntentAddress?.toLowerCase() ===
249
+ transaction.destinationIntentAddress?.toLowerCase() &&
250
+ transaction.originChainId === transaction.destinationChainId
251
+
252
+ // Fetch intent wallet transaction hashes
253
+ let originIntentTxHash: string | undefined
254
+ let destinationIntentTxHash: string | undefined
255
+ let originIntentTxExplorerUrl: string | undefined
256
+ let destinationIntentTxExplorerUrl: string | undefined
257
+ let originIntentDepositTxHash: string | undefined
258
+ let destinationIntentDepositTxHash: string | undefined
259
+ let originIntentDepositTxExplorerUrl: string | undefined
260
+ let destinationIntentDepositTxExplorerUrl: string | undefined
261
+
262
+ // Get origin intent transaction hashes (both deposit and action)
263
+ if (transaction.originIntentAddress && transaction.originChainId) {
264
+ try {
265
+ const originHistory = await getAccountTransactionHistory({
266
+ chainId: transaction.originChainId,
267
+ accountAddress: transaction.originIntentAddress,
268
+ pageSize: 10, // Fetch more to find both deposit and action txs
269
+ page: 0,
270
+ includeMetadata: true, // Need metadata to check transfer direction
271
+ })
272
+ if (
273
+ originHistory?.transactions &&
274
+ originHistory.transactions.length > 0
275
+ ) {
276
+ const intentAddress =
277
+ transaction.originIntentAddress.toLowerCase()
278
+
279
+ // Find first deposit tx (transfer TO intent address)
280
+ const depositTx = originHistory.transactions.find((tx) =>
281
+ tx.transfers?.some(
282
+ (transfer) => transfer.to.toLowerCase() === intentAddress,
283
+ ),
284
+ )
285
+ if (depositTx) {
286
+ originIntentDepositTxHash = depositTx.txnHash
287
+ originIntentDepositTxExplorerUrl = getExplorerUrl({
288
+ txHash: depositTx.txnHash,
289
+ chainId: transaction.originChainId,
290
+ })
291
+ }
292
+
293
+ // Find first action tx (transfer FROM intent address)
294
+ const actionTx = originHistory.transactions.find((tx) =>
295
+ tx.transfers?.some(
296
+ (transfer) =>
297
+ transfer.from.toLowerCase() === intentAddress,
298
+ ),
299
+ )
300
+ if (actionTx) {
301
+ originIntentTxHash = actionTx.txnHash
302
+ originIntentTxExplorerUrl = getExplorerUrl({
303
+ txHash: actionTx.txnHash,
304
+ chainId: transaction.originChainId,
305
+ })
306
+ }
307
+ }
308
+ } catch (error) {
309
+ logger.console.warn(
310
+ "[trails-sdk] Failed to get origin intent transaction hashes for:",
311
+ transaction.originIntentAddress,
312
+ "on chain",
313
+ transaction.originChainId,
314
+ error,
315
+ )
316
+ }
317
+ }
318
+
319
+ // Get destination intent transaction hashes (only if different from origin)
320
+ if (
321
+ !isSameIntentAddress &&
322
+ transaction.destinationIntentAddress &&
323
+ transaction.destinationChainId
324
+ ) {
325
+ try {
326
+ const destinationHistory = await getAccountTransactionHistory({
327
+ chainId: transaction.destinationChainId,
328
+ accountAddress: transaction.destinationIntentAddress,
329
+ pageSize: 10, // Fetch more to find both deposit and action txs
330
+ page: 0,
331
+ includeMetadata: true, // Need metadata to check transfer direction
332
+ })
333
+ if (
334
+ destinationHistory?.transactions &&
335
+ destinationHistory.transactions.length > 0
336
+ ) {
337
+ const intentAddress =
338
+ transaction.destinationIntentAddress.toLowerCase()
339
+
340
+ // Find first deposit tx (transfer TO intent address)
341
+ const depositTx = destinationHistory.transactions.find((tx) =>
342
+ tx.transfers?.some(
343
+ (transfer) => transfer.to.toLowerCase() === intentAddress,
344
+ ),
345
+ )
346
+ if (depositTx) {
347
+ destinationIntentDepositTxHash = depositTx.txnHash
348
+ destinationIntentDepositTxExplorerUrl = getExplorerUrl({
349
+ txHash: depositTx.txnHash,
350
+ chainId: transaction.destinationChainId,
351
+ })
352
+ }
353
+
354
+ // Find first action tx (transfer FROM intent address)
355
+ const actionTx = destinationHistory.transactions.find((tx) =>
356
+ tx.transfers?.some(
357
+ (transfer) =>
358
+ transfer.from.toLowerCase() === intentAddress,
359
+ ),
360
+ )
361
+ if (actionTx) {
362
+ destinationIntentTxHash = actionTx.txnHash
363
+ destinationIntentTxExplorerUrl = getExplorerUrl({
364
+ txHash: actionTx.txnHash,
365
+ chainId: transaction.destinationChainId,
366
+ })
367
+ }
368
+ }
369
+ } catch (error) {
370
+ logger.console.warn(
371
+ "[trails-sdk] Failed to get destination intent transaction hashes for:",
372
+ transaction.destinationIntentAddress,
373
+ "on chain",
374
+ transaction.destinationChainId,
375
+ error,
376
+ )
377
+ }
378
+ }
379
+
380
+ // Calculate effective execution status
381
+ const getEffectiveStatus = (): string => {
382
+ const {
383
+ executionStatus,
384
+ originChainId,
385
+ destinationChainId,
386
+ originIntentAddress,
387
+ destinationIntentAddress,
388
+ createdAt,
389
+ } = transaction
390
+
391
+ // Check if this is a same-chain transaction (same address and chain)
392
+ const isSameChain =
393
+ originIntentAddress?.toLowerCase() ===
394
+ destinationIntentAddress?.toLowerCase() &&
395
+ originChainId === destinationChainId
396
+
397
+ // Check if there's no destination (same-chain or no destination intent)
398
+ const hasNoDestination =
399
+ !destinationIntentAddress || !destinationChainId || isSameChain
400
+
401
+ // Check if it's been over 5 minutes since creation
402
+ const isOver5Minutes = createdAt
403
+ ? Date.now() - new Date(createdAt).getTime() > 5 * 60 * 1000
404
+ : false
405
+
406
+ // Rule 1: If there's a destination intent tx, it's completed
407
+ if (destinationIntentTxHash) {
408
+ return "completed"
409
+ }
410
+
411
+ // Rule 2: Only origin tx hash exists and no real destination (same-chain case)
412
+ if (originIntentTxHash && originChainId && hasNoDestination) {
413
+ return "completed"
414
+ }
415
+
416
+ // Rule 3: If there's a deposit but no action yet, status is pending
417
+ if (originIntentDepositTxHash && !originIntentTxHash) {
418
+ return "pending"
419
+ }
420
+
421
+ // Rule 4: Cross-chain with origin tx but no destination tx
422
+ // If over 5 minutes, consider it failed; otherwise pending
423
+ if (
424
+ originIntentTxHash &&
425
+ !destinationIntentTxHash &&
426
+ originChainId &&
427
+ destinationChainId &&
428
+ !isSameChain &&
429
+ destinationIntentAddress
430
+ ) {
431
+ return isOver5Minutes ? "failed" : "pending"
432
+ }
433
+
434
+ // Otherwise, use the original execution status
435
+ return executionStatus || "unknown"
436
+ }
437
+
438
+ const effectiveExecutionStatus = getEffectiveStatus()
439
+
440
+ // If same intent address, omit destination address/chain to avoid duplication
244
441
  return {
245
442
  ...transaction,
443
+ ...(isSameIntentAddress
444
+ ? {
445
+ destinationIntentAddress: "",
446
+ destinationChainId: undefined,
447
+ }
448
+ : {}),
246
449
  originToken,
247
450
  destinationToken,
451
+ originIntentTxHash,
452
+ destinationIntentTxHash,
453
+ originIntentTxExplorerUrl,
454
+ destinationIntentTxExplorerUrl,
455
+ originIntentDepositTxHash,
456
+ destinationIntentDepositTxHash,
457
+ originIntentDepositTxExplorerUrl,
458
+ destinationIntentDepositTxExplorerUrl,
459
+ executionStatus: effectiveExecutionStatus,
248
460
  }
249
461
  }),
250
462
  )