0xtrails 0.1.13 → 0.2.1

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 (256) hide show
  1. package/dist/aave.d.ts.map +1 -1
  2. package/dist/analytics.d.ts +12 -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/{ccip-D3gTQONK.js → ccip-BbfANth7.js} +12 -12
  7. package/dist/cctp.d.ts.map +1 -1
  8. package/dist/cctpqueue.d.ts +3 -3
  9. package/dist/cctpqueue.d.ts.map +1 -1
  10. package/dist/chains.d.ts.map +1 -1
  11. package/dist/config.d.ts +18 -5
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/constants.d.ts +6 -5
  14. package/dist/constants.d.ts.map +1 -1
  15. package/dist/contractUtils.d.ts +2 -0
  16. package/dist/contractUtils.d.ts.map +1 -1
  17. package/dist/customChains.d.ts +24 -0
  18. package/dist/customChains.d.ts.map +1 -0
  19. package/dist/gasless.d.ts +19 -7
  20. package/dist/gasless.d.ts.map +1 -1
  21. package/dist/{index-CnUM7lKf.js → index-WpIVoh3X.js} +36741 -31761
  22. package/dist/index.d.ts +5 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +405 -394
  25. package/dist/indexerClient.d.ts +10 -0
  26. package/dist/indexerClient.d.ts.map +1 -1
  27. package/dist/intentEntrypoint.d.ts +122 -0
  28. package/dist/intentEntrypoint.d.ts.map +1 -0
  29. package/dist/intents.d.ts +5 -3
  30. package/dist/intents.d.ts.map +1 -1
  31. package/dist/metaTxnMonitor.d.ts.map +1 -1
  32. package/dist/morpho.d.ts.map +1 -1
  33. package/dist/pools.d.ts +3 -1
  34. package/dist/pools.d.ts.map +1 -1
  35. package/dist/prepareSend.d.ts +18 -9
  36. package/dist/prepareSend.d.ts.map +1 -1
  37. package/dist/prices.d.ts +1 -1
  38. package/dist/prices.d.ts.map +1 -1
  39. package/dist/relaySdk.d.ts.map +1 -1
  40. package/dist/relayer.d.ts.map +1 -1
  41. package/dist/toast.d.ts +9 -0
  42. package/dist/toast.d.ts.map +1 -0
  43. package/dist/tokenBalances.d.ts +6 -2
  44. package/dist/tokenBalances.d.ts.map +1 -1
  45. package/dist/tokens.d.ts.map +1 -1
  46. package/dist/trails.d.ts +6 -5
  47. package/dist/trails.d.ts.map +1 -1
  48. package/dist/trailsClient.d.ts +12 -0
  49. package/dist/trailsClient.d.ts.map +1 -0
  50. package/dist/trailsRouter.d.ts +22 -0
  51. package/dist/trailsRouter.d.ts.map +1 -0
  52. package/dist/transactions.d.ts +8 -1
  53. package/dist/transactions.d.ts.map +1 -1
  54. package/dist/wallets.d.ts.map +1 -1
  55. package/dist/widget/components/AccountActionsDropdown.d.ts.map +1 -1
  56. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  57. package/dist/widget/components/AccountSettings.d.ts +7 -0
  58. package/dist/widget/components/AccountSettings.d.ts.map +1 -0
  59. package/dist/widget/components/ChainList.d.ts +0 -1
  60. package/dist/widget/components/ChainList.d.ts.map +1 -1
  61. package/dist/widget/components/ClassicSwap.d.ts +46 -0
  62. package/dist/widget/components/ClassicSwap.d.ts.map +1 -0
  63. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  64. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  65. package/dist/widget/components/ConnectedWallets.d.ts +9 -0
  66. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -0
  67. package/dist/widget/components/DebugMenu.d.ts.map +1 -1
  68. package/dist/widget/components/DebugScreensList.d.ts.map +1 -1
  69. package/dist/widget/components/DebugToast.d.ts +3 -0
  70. package/dist/widget/components/DebugToast.d.ts.map +1 -0
  71. package/dist/widget/components/Earn.d.ts.map +1 -1
  72. package/dist/widget/components/EarnPools.d.ts.map +1 -1
  73. package/dist/widget/components/FeeOption.d.ts +22 -0
  74. package/dist/widget/components/FeeOption.d.ts.map +1 -0
  75. package/dist/widget/components/FeeOptions.d.ts +13 -17
  76. package/dist/widget/components/FeeOptions.d.ts.map +1 -1
  77. package/dist/widget/components/Fund.d.ts +44 -0
  78. package/dist/widget/components/Fund.d.ts.map +1 -0
  79. package/dist/widget/components/FundMethods.d.ts +1 -1
  80. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  81. package/dist/widget/components/FundSendForm.d.ts.map +1 -1
  82. package/dist/widget/components/Identicon.d.ts +9 -0
  83. package/dist/widget/components/Identicon.d.ts.map +1 -0
  84. package/dist/widget/components/MeshConnectExchanges.d.ts +5 -2
  85. package/dist/widget/components/MeshConnectExchanges.d.ts.map +1 -1
  86. package/dist/widget/components/MeshConnectFlow.d.ts +2 -0
  87. package/dist/widget/components/MeshConnectFlow.d.ts.map +1 -1
  88. package/dist/widget/components/NativeGasOption.d.ts +12 -0
  89. package/dist/widget/components/NativeGasOption.d.ts.map +1 -0
  90. package/dist/widget/components/Pay.d.ts +46 -0
  91. package/dist/widget/components/Pay.d.ts.map +1 -0
  92. package/dist/widget/components/PaySendForm.d.ts.map +1 -1
  93. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  94. package/dist/widget/components/Receive.d.ts.map +1 -1
  95. package/dist/widget/components/RecentTokens.d.ts.map +1 -1
  96. package/dist/widget/components/Recipients.d.ts +9 -0
  97. package/dist/widget/components/Recipients.d.ts.map +1 -0
  98. package/dist/widget/components/RefundWarning.d.ts +9 -0
  99. package/dist/widget/components/RefundWarning.d.ts.map +1 -0
  100. package/dist/widget/components/SimpleSwap.d.ts.map +1 -1
  101. package/dist/widget/components/Swap.d.ts.map +1 -1
  102. package/dist/widget/components/SwapSettings.d.ts +1 -5
  103. package/dist/widget/components/SwapSettings.d.ts.map +1 -1
  104. package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
  105. package/dist/widget/components/ThemeSyncer.d.ts +6 -0
  106. package/dist/widget/components/ThemeSyncer.d.ts.map +1 -0
  107. package/dist/widget/components/Toast.d.ts +24 -0
  108. package/dist/widget/components/Toast.d.ts.map +1 -0
  109. package/dist/widget/components/TokenList.d.ts.map +1 -1
  110. package/dist/widget/components/TokenSelector.d.ts.map +1 -1
  111. package/dist/widget/components/TransactionDetails.d.ts.map +1 -1
  112. package/dist/widget/components/TruncatedAddress.d.ts +2 -0
  113. package/dist/widget/components/TruncatedAddress.d.ts.map +1 -1
  114. package/dist/widget/components/UserPreferences.d.ts +7 -0
  115. package/dist/widget/components/UserPreferences.d.ts.map +1 -0
  116. package/dist/widget/hooks/useBack.d.ts +2 -0
  117. package/dist/widget/hooks/useBack.d.ts.map +1 -1
  118. package/dist/widget/hooks/useBalanceVisible.d.ts +1 -0
  119. package/dist/widget/hooks/useBalanceVisible.d.ts.map +1 -1
  120. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  121. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  122. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  123. package/dist/widget/hooks/useDebugScreens.d.ts +1 -1
  124. package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -1
  125. package/dist/widget/hooks/useDefaultTokenSelection.d.ts +54 -0
  126. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -0
  127. package/dist/widget/hooks/useIntentTransactionHistory.d.ts.map +1 -1
  128. package/dist/widget/hooks/usePayMessage.d.ts +34 -0
  129. package/dist/widget/hooks/usePayMessage.d.ts.map +1 -0
  130. package/dist/widget/hooks/useRecipients.d.ts +17 -0
  131. package/dist/widget/hooks/useRecipients.d.ts.map +1 -0
  132. package/dist/widget/hooks/useSelectedFeeToken.d.ts +32 -0
  133. package/dist/widget/hooks/useSelectedFeeToken.d.ts.map +1 -0
  134. package/dist/widget/hooks/useSelectedMeshExchange.d.ts +14 -0
  135. package/dist/widget/hooks/useSelectedMeshExchange.d.ts.map +1 -0
  136. package/dist/widget/hooks/useSelectedRecipient.d.ts +12 -0
  137. package/dist/widget/hooks/useSelectedRecipient.d.ts.map +1 -0
  138. package/dist/widget/hooks/useSendForm.d.ts +10 -13
  139. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  140. package/dist/widget/hooks/useSwapAmount.d.ts +13 -0
  141. package/dist/widget/hooks/useSwapAmount.d.ts.map +1 -0
  142. package/dist/widget/hooks/useSwapSettings.d.ts +16 -0
  143. package/dist/widget/hooks/useSwapSettings.d.ts.map +1 -0
  144. package/dist/widget/hooks/useTargetAmount.d.ts +5 -0
  145. package/dist/widget/hooks/useTargetAmount.d.ts.map +1 -0
  146. package/dist/widget/hooks/useTheme.d.ts +14 -0
  147. package/dist/widget/hooks/useTheme.d.ts.map +1 -0
  148. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  149. package/dist/widget/index.js +2 -2
  150. package/dist/widget/widget.d.ts +9 -0
  151. package/dist/widget/widget.d.ts.map +1 -1
  152. package/package.json +38 -36
  153. package/src/aave.ts +6 -1
  154. package/src/analytics.ts +109 -53
  155. package/src/apiClient.ts +1 -1
  156. package/src/cctp.ts +6 -2
  157. package/src/cctpqueue.ts +7 -7
  158. package/src/chains.ts +18 -0
  159. package/src/config.ts +63 -17
  160. package/src/constants.ts +20 -16
  161. package/src/contractUtils.ts +33 -2
  162. package/src/customChains.ts +24 -0
  163. package/src/gasless.ts +162 -109
  164. package/src/index.ts +11 -1
  165. package/src/indexerClient.ts +73 -1
  166. package/src/intentEntrypoint.ts +218 -0
  167. package/src/intents.ts +85 -54
  168. package/src/metaTxnMonitor.ts +1 -0
  169. package/src/morpho.ts +13 -2
  170. package/src/pools.ts +68 -86
  171. package/src/prepareSend.ts +1719 -967
  172. package/src/prices.ts +51 -7
  173. package/src/relaySdk.ts +6 -4
  174. package/src/relayer.ts +6 -3
  175. package/src/toast.ts +110 -0
  176. package/src/tokenBalances.ts +112 -20
  177. package/src/tokens.ts +70 -7
  178. package/src/trails.ts +81 -80
  179. package/src/trailsClient.ts +48 -0
  180. package/src/{proxyCaller.ts → trailsRouter.ts} +25 -20
  181. package/src/transactions.ts +30 -88
  182. package/src/umd.tsx +1 -1
  183. package/src/wallets.ts +2 -1
  184. package/src/widget/assets/sequence-logo.svg +15 -0
  185. package/src/widget/compiled.css +2 -2
  186. package/src/widget/components/AccountActionsDropdown.tsx +18 -159
  187. package/src/widget/components/AccountIntentTransactionHistory.tsx +346 -63
  188. package/src/widget/components/AccountSettings.tsx +102 -0
  189. package/src/widget/components/ChainFilterDropdown.tsx +1 -1
  190. package/src/widget/components/ChainList.tsx +10 -20
  191. package/src/widget/components/ClassicSwap.tsx +921 -0
  192. package/src/widget/components/ConfigDisplay.tsx +41 -5
  193. package/src/widget/components/ConnectWallet.tsx +168 -11
  194. package/src/widget/components/ConnectedWallets.tsx +342 -0
  195. package/src/widget/components/DebugMenu.tsx +2 -0
  196. package/src/widget/components/DebugScreensList.tsx +3 -0
  197. package/src/widget/components/DebugToast.tsx +63 -0
  198. package/src/widget/components/Earn.tsx +112 -143
  199. package/src/widget/components/EarnPools.tsx +2 -4
  200. package/src/widget/components/EarnPoolsFilters.tsx +6 -6
  201. package/src/widget/components/FeeOption.tsx +78 -0
  202. package/src/widget/components/FeeOptions.tsx +192 -127
  203. package/src/widget/components/Fund.tsx +1236 -0
  204. package/src/widget/components/FundMethods.tsx +4 -4
  205. package/src/widget/components/FundSendForm.tsx +1 -34
  206. package/src/widget/components/Identicon.tsx +158 -0
  207. package/src/widget/components/MeshConnectExchanges.tsx +32 -3
  208. package/src/widget/components/MeshConnectFlow.tsx +23 -4
  209. package/src/widget/components/NativeGasOption.tsx +99 -0
  210. package/src/widget/components/Pay.tsx +1092 -0
  211. package/src/widget/components/PaySendForm.tsx +1 -38
  212. package/src/widget/components/QuoteDetails.tsx +1 -30
  213. package/src/widget/components/Receipt.tsx +1 -1
  214. package/src/widget/components/Receive.tsx +4 -2
  215. package/src/widget/components/RecentTokens.tsx +2 -1
  216. package/src/widget/components/Recipients.tsx +448 -0
  217. package/src/widget/components/RefundWarning.tsx +61 -0
  218. package/src/widget/components/ScreenHeader.tsx +1 -1
  219. package/src/widget/components/SimpleSwap.tsx +74 -58
  220. package/src/widget/components/Swap.tsx +35 -853
  221. package/src/widget/components/SwapSettings.tsx +5 -11
  222. package/src/widget/components/ThemeProvider.tsx +32 -0
  223. package/src/widget/components/ThemeSyncer.tsx +47 -0
  224. package/src/widget/components/Toast.tsx +315 -0
  225. package/src/widget/components/TokenList.tsx +2 -34
  226. package/src/widget/components/TokenSelector.tsx +14 -3
  227. package/src/widget/components/TransactionDetails.tsx +153 -13
  228. package/src/widget/components/TransferPendingVertical.tsx +1 -1
  229. package/src/widget/components/TruncatedAddress.tsx +5 -1
  230. package/src/widget/components/UserPreferences.tsx +155 -0
  231. package/src/widget/components/WalletList.tsx +1 -1
  232. package/src/widget/hooks/useBack.tsx +4 -0
  233. package/src/widget/hooks/useBalanceVisible.tsx +40 -2
  234. package/src/widget/hooks/useCheckout.ts +13 -0
  235. package/src/widget/hooks/useCurrentScreen.tsx +4 -0
  236. package/src/widget/hooks/useDebugScreens.ts +12 -2
  237. package/src/widget/hooks/useDefaultTokenSelection.tsx +471 -0
  238. package/src/widget/hooks/useIntentTransactionHistory.ts +212 -0
  239. package/src/widget/hooks/usePayMessage.tsx +370 -0
  240. package/src/widget/hooks/useRecipients.ts +168 -0
  241. package/src/widget/hooks/useSelectedFeeToken.tsx +299 -0
  242. package/src/widget/hooks/useSelectedMeshExchange.tsx +46 -0
  243. package/src/widget/hooks/useSelectedRecipient.tsx +48 -0
  244. package/src/widget/hooks/useSendForm.ts +257 -49
  245. package/src/widget/hooks/useSwapAmount.tsx +50 -0
  246. package/src/widget/hooks/useSwapSettings.tsx +100 -0
  247. package/src/widget/hooks/useTargetAmount.ts +23 -0
  248. package/src/widget/hooks/useTheme.tsx +80 -0
  249. package/src/widget/hooks/useTokenList.ts +20 -11
  250. package/src/widget/index.css +45 -21
  251. package/src/widget/widget.tsx +294 -136
  252. package/dist/address.d.ts +0 -2
  253. package/dist/address.d.ts.map +0 -1
  254. package/dist/proxyCaller.d.ts +0 -21
  255. package/dist/proxyCaller.d.ts.map +0 -1
  256. package/src/address.ts +0 -6
@@ -0,0 +1,471 @@
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
+
16
+ const MINIMUM_24H_VOLUME_USD = 1_000_000 // $1M
17
+
18
+ /**
19
+ * Hook for intelligent default token selection in 0xtrails widget.
20
+ *
21
+ * When toToken and toChainId are specified (user wants specific destination):
22
+ * 1. Same-chain, same-token with balance >= targetAmountUsd (no swap, no bridge) - FASTEST
23
+ * 2. Same-chain, different-token with balance >= targetAmountUsd (swap only) - FAST
24
+ * - Ordered by: 24h volume > $1M first, then highest balance USD
25
+ * 3. Different-chain, same-token with balance >= targetAmountUsd (bridge only) - SLOWER
26
+ * - Ordered by: 24h volume > $1M first, then highest balance USD
27
+ * 4. Different-chain, different-token with balance >= targetAmountUsd (swap + bridge) - SLOWEST
28
+ * - Ordered by: 24h volume > $1M first, then highest balance USD
29
+ * 5. Fallback: Any token with balance > 0 if none can cover target amount
30
+ *
31
+ * When toToken/toChainId NOT specified (general send/bridge):
32
+ * - Select highest balance token with 24h volume > $1M
33
+ * - Fallback to any token with balance > 0
34
+ *
35
+ * Destination Token Logic (only when toToken/toChainId not provided):
36
+ * - Matches origin token symbol on different chain when possible
37
+ * - Default destination chain: Base (unless origin is Base, then Arbitrum)
38
+ * - Fallback priority: USDC > ETH > first available token
39
+ *
40
+ * @returns defaultOriginToken - Best origin token that can cover targetAmountUsd (if specified)
41
+ * @returns defaultDestinationToken - Matching token on different chain (null if toToken/toChainId set)
42
+ * @returns isLoading - Whether token data is still loading
43
+ */
44
+ export interface DefaultToken {
45
+ id: number
46
+ name: string
47
+ symbol: string
48
+ balance: string
49
+ imageUrl: string
50
+ chainId: number
51
+ contractAddress: string
52
+ balanceUsdFormatted: string
53
+ tokenPriceUsd: number
54
+ decimals: number // Top-level decimals for compatibility
55
+ contractInfo?: {
56
+ decimals: number
57
+ symbol: string
58
+ name: string
59
+ }
60
+ }
61
+
62
+ export interface UseDefaultTokenSelectionReturn {
63
+ defaultOriginToken: DefaultToken | null
64
+ defaultDestinationToken: DefaultToken | null
65
+ isLoading: boolean
66
+ }
67
+
68
+ function getToken24hVolume(token: TokenBalanceExtended): number {
69
+ const price = token.price as any
70
+ if (!price) return 0
71
+
72
+ return Number(
73
+ price.price24hVol?.value ||
74
+ price.volume24h ||
75
+ price.volume_24h ||
76
+ price.vol24h ||
77
+ price.dailyVolume ||
78
+ 0,
79
+ )
80
+ }
81
+
82
+ function tokenMatches(
83
+ token: TokenBalanceExtended,
84
+ targetSymbolOrAddress: string,
85
+ targetChainId?: number,
86
+ ): boolean {
87
+ const isNative =
88
+ !("contractAddress" in token) || token.contractAddress === zeroAddress
89
+
90
+ // Check chain match first
91
+ const chainMatches = !targetChainId || token.chainId === targetChainId
92
+ if (!chainMatches) return false
93
+
94
+ // Check if target is an address (starts with 0x and is longer than 5 chars)
95
+ const isTargetAddress =
96
+ targetSymbolOrAddress.startsWith("0x") && targetSymbolOrAddress.length > 5
97
+
98
+ if (isNative) {
99
+ const chainInfo = getChainInfo(token.chainId)
100
+ const nativeSymbol = chainInfo?.nativeCurrency.symbol || "ETH"
101
+
102
+ // For native tokens, check if target is zero address or matches symbol
103
+ if (isTargetAddress) {
104
+ return targetSymbolOrAddress.toLowerCase() === zeroAddress.toLowerCase()
105
+ }
106
+
107
+ return nativeSymbol.toUpperCase() === targetSymbolOrAddress.toUpperCase()
108
+ }
109
+
110
+ // For non-native tokens, check both address and symbol
111
+ if (isTargetAddress) {
112
+ return (
113
+ token.contractAddress.toLowerCase() ===
114
+ targetSymbolOrAddress.toLowerCase()
115
+ )
116
+ }
117
+
118
+ return (
119
+ token.contractInfo?.symbol.toUpperCase() ===
120
+ targetSymbolOrAddress.toUpperCase()
121
+ )
122
+ }
123
+
124
+ function useDefaultTokenSelectionInternal(): UseDefaultTokenSelectionReturn {
125
+ const { toToken, toChainId } = useWidgetProps()
126
+ const { targetAmountUsd } = useTargetAmount()
127
+ const { address } = useAccount()
128
+ const indexerGatewayClient = useIndexerGatewayClient()
129
+
130
+ const { sortedTokens, isLoadingSortedTokens } = useTokenBalances(
131
+ address as Address.Address,
132
+ indexerGatewayClient,
133
+ )
134
+
135
+ const { supportedTokens, isLoadingTokens: isLoadingSupportedTokens } =
136
+ useSupportedTokens()
137
+
138
+ const isLoading = isLoadingSortedTokens || isLoadingSupportedTokens
139
+
140
+ const { defaultOriginToken, defaultDestinationToken } = useMemo(() => {
141
+ if (!sortedTokens?.length || !supportedTokens?.length) {
142
+ return {
143
+ defaultOriginToken: null,
144
+ defaultDestinationToken: null,
145
+ }
146
+ }
147
+
148
+ // Determine if we should compute destination token
149
+ // Only compute destination defaults if toToken and toChainId are NOT set
150
+ const shouldComputeDestination = !toToken && !toChainId
151
+
152
+ // Helper to convert TokenBalanceExtended to DefaultToken
153
+ const toDefaultToken = (
154
+ token: TokenBalanceExtended,
155
+ ): DefaultToken | null => {
156
+ const isNative =
157
+ !("contractAddress" in token) || token.contractAddress === zeroAddress
158
+ const chainInfo = getChainInfo(token.chainId)
159
+
160
+ if (isNative) {
161
+ // Native tokens - use token properties as-is
162
+ const tokenWithInfo = token as any
163
+ const imageUrl = tokenWithInfo.logoURI || tokenWithInfo.imageUrl || ""
164
+ const decimals = 18
165
+
166
+ return {
167
+ id: token.chainId,
168
+ name: chainInfo?.nativeCurrency.name || "Native Token",
169
+ symbol: chainInfo?.nativeCurrency.symbol || "ETH",
170
+ balance: token.balance,
171
+ imageUrl,
172
+ chainId: token.chainId,
173
+ contractAddress: zeroAddress,
174
+ balanceUsdFormatted: token.balanceUsdFormatted ?? "0",
175
+ tokenPriceUsd: token.price?.value ?? 0,
176
+ decimals, // Top-level decimals
177
+ contractInfo: {
178
+ decimals,
179
+ symbol: chainInfo?.nativeCurrency.symbol || "ETH",
180
+ name: chainInfo?.nativeCurrency.name || "Native Token",
181
+ },
182
+ }
183
+ } else {
184
+ // Non-native tokens - use contractInfo properties
185
+ const tokenWithInfo = token as any
186
+ const imageUrl =
187
+ tokenWithInfo.contractInfo?.logoURI || tokenWithInfo.imageUrl || ""
188
+ const decimals = token.contractInfo?.decimals
189
+ if (!decimals) {
190
+ throw new Error("Decimals not found")
191
+ }
192
+
193
+ return {
194
+ id: token.chainId,
195
+ name: token.contractInfo?.name || "Unknown Token",
196
+ symbol: token.contractInfo?.symbol || "???",
197
+ balance: token.balance,
198
+ imageUrl,
199
+ chainId: token.chainId,
200
+ contractAddress: token.contractAddress,
201
+ balanceUsdFormatted: token.balanceUsdFormatted ?? "0",
202
+ tokenPriceUsd: token.price?.value ?? 0,
203
+ decimals, // Top-level decimals
204
+ contractInfo: {
205
+ decimals,
206
+ symbol: token.contractInfo?.symbol ?? "???",
207
+ name: token.contractInfo?.name ?? "Unknown Token",
208
+ },
209
+ }
210
+ }
211
+ }
212
+
213
+ // Filter tokens with balance > 0
214
+ const tokensWithBalance = sortedTokens.filter((token) => {
215
+ const balanceUsd = token.balanceUsd ?? 0
216
+ return balanceUsd > 0
217
+ })
218
+
219
+ if (tokensWithBalance.length === 0) {
220
+ logger.console.log(
221
+ "[trails-sdk] No tokens with balance found for default selection",
222
+ )
223
+ return {
224
+ defaultOriginToken: null,
225
+ defaultDestinationToken: null,
226
+ }
227
+ }
228
+
229
+ // Helper to sort tokens by balance USD and volume
230
+ const sortTokensByBalanceAndVolume = (tokens: TokenBalanceExtended[]) => {
231
+ return [...tokens].sort((a, b) => {
232
+ const aVolume = getToken24hVolume(a)
233
+ const bVolume = getToken24hVolume(b)
234
+ const aHighVolume = aVolume >= MINIMUM_24H_VOLUME_USD
235
+ const bHighVolume = bVolume >= MINIMUM_24H_VOLUME_USD
236
+
237
+ // Prioritize high volume tokens first
238
+ if (aHighVolume && !bHighVolume) return -1
239
+ if (!aHighVolume && bHighVolume) return 1
240
+
241
+ // Then sort by balance USD (highest first)
242
+ const aBalanceUsd = a.balanceUsd ?? 0
243
+ const bBalanceUsd = b.balanceUsd ?? 0
244
+ return bBalanceUsd - aBalanceUsd
245
+ })
246
+ }
247
+
248
+ // Find the best origin token using intelligent selection
249
+ let bestOriginToken: TokenBalanceExtended | null = null
250
+
251
+ // When toToken and toChainId are specified (user wants specific destination)
252
+ if (toToken && toChainId) {
253
+ const targetChainId = Number(toChainId)
254
+
255
+ // Use targetAmountUsd if available, otherwise use a minimal threshold when toToken is set
256
+ const effectiveTargetAmount = targetAmountUsd ?? 0.01
257
+
258
+ // Filter tokens that can cover the target amount
259
+ const tokensWithSufficientBalance = tokensWithBalance.filter(
260
+ (token) => (token.balanceUsd ?? 0) >= effectiveTargetAmount,
261
+ )
262
+
263
+ // Priority 1: Same-chain, same-token (no swap, no bridge) - FASTEST
264
+ const sameChainSameToken = tokensWithSufficientBalance.find((token) =>
265
+ tokenMatches(token, toToken, targetChainId),
266
+ )
267
+
268
+ if (sameChainSameToken) {
269
+ bestOriginToken = sameChainSameToken
270
+ logger.console.log(
271
+ "[trails-sdk] Selected same-chain, same-token (fastest)",
272
+ )
273
+ }
274
+
275
+ // Priority 2: Same-chain, different-token (swap only, no bridge) - FAST
276
+ if (!bestOriginToken) {
277
+ const sameChainTokens = tokensWithSufficientBalance.filter(
278
+ (token) => token.chainId === targetChainId,
279
+ )
280
+ const sortedSameChain = sortTokensByBalanceAndVolume(sameChainTokens)
281
+ bestOriginToken = sortedSameChain[0] ?? null
282
+ if (bestOriginToken) {
283
+ logger.console.log(
284
+ "[trails-sdk] Selected same-chain, different-token (fast)",
285
+ )
286
+ }
287
+ }
288
+
289
+ // Priority 3: Different-chain, same-token (bridge only, no swap) - SLOWER
290
+ if (!bestOriginToken) {
291
+ const sameTokenDiffChain = tokensWithSufficientBalance.filter(
292
+ (token) =>
293
+ tokenMatches(token, toToken) && token.chainId !== targetChainId,
294
+ )
295
+ const sortedSameToken = sortTokensByBalanceAndVolume(sameTokenDiffChain)
296
+ bestOriginToken = sortedSameToken[0] ?? null
297
+ if (bestOriginToken) {
298
+ logger.console.log(
299
+ "[trails-sdk] Selected different-chain, same-token (slower)",
300
+ )
301
+ }
302
+ }
303
+
304
+ // Priority 4: Different-chain, different-token (swap + bridge) - SLOWEST
305
+ if (!bestOriginToken) {
306
+ const diffChainDiffToken = tokensWithSufficientBalance.filter(
307
+ (token) => token.chainId !== targetChainId,
308
+ )
309
+ const sortedDiffChainDiffToken =
310
+ sortTokensByBalanceAndVolume(diffChainDiffToken)
311
+ bestOriginToken = sortedDiffChainDiffToken[0] ?? null
312
+ if (bestOriginToken) {
313
+ logger.console.log(
314
+ "[trails-sdk] Selected different-chain, different-token (slowest)",
315
+ )
316
+ }
317
+ }
318
+
319
+ // Fallback: If no token can cover target amount, use any token (sorted by balance/volume)
320
+ if (!bestOriginToken) {
321
+ const allTokensSorted = sortTokensByBalanceAndVolume(tokensWithBalance)
322
+ bestOriginToken = allTokensSorted[0] ?? null
323
+ logger.console.log(
324
+ "[trails-sdk] Fallback: No token can cover target amount, using highest balance token",
325
+ )
326
+ }
327
+ } else {
328
+ // When no specific destination is set, select highest value token with good liquidity
329
+ const sortedByBalanceAndVolume =
330
+ sortTokensByBalanceAndVolume(tokensWithBalance)
331
+ bestOriginToken = sortedByBalanceAndVolume[0] ?? null
332
+ logger.console.log(
333
+ "[trails-sdk] No destination specified, selected highest value token with best liquidity",
334
+ )
335
+ }
336
+
337
+ if (!bestOriginToken) {
338
+ return {
339
+ defaultOriginToken: null,
340
+ defaultDestinationToken: null,
341
+ }
342
+ }
343
+
344
+ const originToken = toDefaultToken(bestOriginToken)
345
+
346
+ if (!originToken) {
347
+ return {
348
+ defaultOriginToken: null,
349
+ defaultDestinationToken: null,
350
+ }
351
+ }
352
+
353
+ // Compute destination token only when toToken and toChainId are not provided
354
+ let destinationToken: DefaultToken | null = null
355
+
356
+ if (shouldComputeDestination) {
357
+ // Determine default destination chain: Base if origin is not Base, Arbitrum if origin is Base
358
+ const defaultDestChainId =
359
+ originToken.chainId === base.id ? arbitrum.id : base.id
360
+
361
+ // Find matching token on destination chain (prefer same symbol)
362
+ const destChainTokens = supportedTokens.filter(
363
+ (token: any) => token.chainId === defaultDestChainId,
364
+ )
365
+
366
+ let destToken: any = null
367
+
368
+ // Try to find same symbol on dest chain
369
+ const sameSymbolToken = destChainTokens.find((token: any) => {
370
+ return token.symbol.toUpperCase() === originToken.symbol.toUpperCase()
371
+ })
372
+
373
+ if (sameSymbolToken) {
374
+ destToken = sameSymbolToken
375
+ } else {
376
+ // Fallback: USDC > ETH > first available token
377
+ destToken =
378
+ destChainTokens.find((token: any) => token.symbol === "USDC") ||
379
+ destChainTokens.find((token: any) => token.symbol === "ETH") ||
380
+ destChainTokens[0]
381
+ }
382
+
383
+ const decimals = destToken?.decimals
384
+ if (!decimals) {
385
+ throw new Error("Decimals not found")
386
+ }
387
+
388
+ destinationToken = destToken
389
+ ? {
390
+ id: destToken.chainId,
391
+ name: destToken.name,
392
+ symbol: destToken.symbol,
393
+ balance: "0", // Destination token doesn't have balance yet
394
+ imageUrl: destToken.imageUrl || "",
395
+ chainId: destToken.chainId,
396
+ contractAddress: destToken.contractAddress || zeroAddress,
397
+ balanceUsdFormatted: "0",
398
+ tokenPriceUsd: 0,
399
+ decimals, // Top-level decimals
400
+ contractInfo: {
401
+ decimals,
402
+ symbol: destToken.symbol,
403
+ name: destToken.name,
404
+ },
405
+ }
406
+ : null
407
+ }
408
+
409
+ logger.console.log("[trails-sdk] Default token selection:", {
410
+ origin: originToken
411
+ ? {
412
+ symbol: originToken.symbol,
413
+ chainId: originToken.chainId,
414
+ balanceUsd: originToken.balanceUsdFormatted,
415
+ volume24h: getToken24hVolume(bestOriginToken),
416
+ }
417
+ : null,
418
+ destination: destinationToken
419
+ ? {
420
+ symbol: destinationToken.symbol,
421
+ chainId: destinationToken.chainId,
422
+ }
423
+ : null,
424
+ shouldComputeDestination,
425
+ toToken,
426
+ toChainId,
427
+ targetAmountUsd,
428
+ effectiveTargetAmount:
429
+ toToken && toChainId ? (targetAmountUsd ?? 0.01) : null,
430
+ })
431
+
432
+ return {
433
+ defaultOriginToken: originToken,
434
+ defaultDestinationToken: destinationToken,
435
+ }
436
+ }, [toToken, toChainId, sortedTokens, supportedTokens, targetAmountUsd])
437
+
438
+ return {
439
+ defaultOriginToken,
440
+ defaultDestinationToken,
441
+ isLoading,
442
+ }
443
+ }
444
+
445
+ // Context for sharing default token selection across components
446
+ const DefaultTokenSelectionContext =
447
+ createContext<UseDefaultTokenSelectionReturn | null>(null)
448
+
449
+ export function DefaultTokenSelectionProvider({
450
+ children,
451
+ }: {
452
+ children: ReactNode
453
+ }) {
454
+ const value = useDefaultTokenSelectionInternal()
455
+
456
+ return (
457
+ <DefaultTokenSelectionContext.Provider value={value}>
458
+ {children}
459
+ </DefaultTokenSelectionContext.Provider>
460
+ )
461
+ }
462
+
463
+ export function useDefaultTokenSelection(): UseDefaultTokenSelectionReturn {
464
+ const context = useContext(DefaultTokenSelectionContext)
465
+ if (!context) {
466
+ throw new Error(
467
+ "useDefaultTokenSelection must be used within a DefaultTokenSelectionProvider",
468
+ )
469
+ }
470
+ return context
471
+ }
@@ -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
  )