0xtrails 0.2.5 → 0.3.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 (267) hide show
  1. package/dist/aave.d.ts +2 -0
  2. package/dist/aave.d.ts.map +1 -1
  3. package/dist/abortController.d.ts +8 -0
  4. package/dist/abortController.d.ts.map +1 -0
  5. package/dist/{ccip-CXlshvBY.js → ccip-BMB3uDZt.js} +1 -1
  6. package/dist/config.d.ts +0 -5
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/constants.d.ts +4 -4
  9. package/dist/constants.d.ts.map +1 -1
  10. package/dist/error.d.ts +4 -1
  11. package/dist/error.d.ts.map +1 -1
  12. package/dist/fees.d.ts +19 -0
  13. package/dist/fees.d.ts.map +1 -0
  14. package/dist/{index-_QuyGrjU.js → index-QXPUrZVv.js} +48719 -50852
  15. package/dist/index.d.ts +9 -8
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +811 -784
  18. package/dist/intentReceiptMonitor.d.ts +24 -0
  19. package/dist/intentReceiptMonitor.d.ts.map +1 -0
  20. package/dist/intentReceiptPoller.d.ts +69 -0
  21. package/dist/intentReceiptPoller.d.ts.map +1 -0
  22. package/dist/intents.d.ts +15 -11
  23. package/dist/intents.d.ts.map +1 -1
  24. package/dist/morpho.d.ts +6 -5
  25. package/dist/morpho.d.ts.map +1 -1
  26. package/dist/mutations.d.ts +16 -0
  27. package/dist/mutations.d.ts.map +1 -0
  28. package/dist/preconditions.d.ts +5 -4
  29. package/dist/preconditions.d.ts.map +1 -1
  30. package/dist/prepareSend.d.ts +7 -258
  31. package/dist/prepareSend.d.ts.map +1 -1
  32. package/dist/prices.d.ts +9 -6
  33. package/dist/prices.d.ts.map +1 -1
  34. package/dist/sequenceWallet.d.ts +3 -16
  35. package/dist/sequenceWallet.d.ts.map +1 -1
  36. package/dist/tokenBalances.d.ts +17 -13
  37. package/dist/tokenBalances.d.ts.map +1 -1
  38. package/dist/trails.d.ts +24 -40
  39. package/dist/trails.d.ts.map +1 -1
  40. package/dist/transactionIntent/constants.d.ts +7 -0
  41. package/dist/transactionIntent/constants.d.ts.map +1 -0
  42. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts +44 -0
  43. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts.map +1 -0
  44. package/dist/transactionIntent/deposits/gaslessDeposit.d.ts +30 -0
  45. package/dist/transactionIntent/deposits/gaslessDeposit.d.ts.map +1 -0
  46. package/dist/transactionIntent/deposits/index.d.ts +4 -0
  47. package/dist/transactionIntent/deposits/index.d.ts.map +1 -0
  48. package/dist/transactionIntent/deposits/standardDeposit.d.ts +30 -0
  49. package/dist/transactionIntent/deposits/standardDeposit.d.ts.map +1 -0
  50. package/dist/transactionIntent/execution/index.d.ts +2 -0
  51. package/dist/transactionIntent/execution/index.d.ts.map +1 -0
  52. package/dist/transactionIntent/execution/transactionState.d.ts +5 -0
  53. package/dist/transactionIntent/execution/transactionState.d.ts.map +1 -0
  54. package/dist/transactionIntent/handlers/crossChain.d.ts +82 -0
  55. package/dist/transactionIntent/handlers/crossChain.d.ts.map +1 -0
  56. package/dist/transactionIntent/handlers/index.d.ts +4 -0
  57. package/dist/transactionIntent/handlers/index.d.ts.map +1 -0
  58. package/dist/transactionIntent/handlers/sameChainDifferentToken.d.ts +62 -0
  59. package/dist/transactionIntent/handlers/sameChainDifferentToken.d.ts.map +1 -0
  60. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts +72 -0
  61. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts.map +1 -0
  62. package/dist/transactionIntent/index.d.ts +9 -0
  63. package/dist/transactionIntent/index.d.ts.map +1 -0
  64. package/dist/transactionIntent/quote/feeExtractors.d.ts +17 -0
  65. package/dist/transactionIntent/quote/feeExtractors.d.ts.map +1 -0
  66. package/dist/transactionIntent/quote/index.d.ts +4 -0
  67. package/dist/transactionIntent/quote/index.d.ts.map +1 -0
  68. package/dist/transactionIntent/quote/normalizeQuote.d.ts +34 -0
  69. package/dist/transactionIntent/quote/normalizeQuote.d.ts.map +1 -0
  70. package/dist/transactionIntent/quote/quoteHelpers.d.ts +5 -0
  71. package/dist/transactionIntent/quote/quoteHelpers.d.ts.map +1 -0
  72. package/dist/transactionIntent/types.d.ts +131 -0
  73. package/dist/transactionIntent/types.d.ts.map +1 -0
  74. package/dist/transactionIntent/utils/balanceChecker.d.ts +18 -0
  75. package/dist/transactionIntent/utils/balanceChecker.d.ts.map +1 -0
  76. package/dist/transactionIntent/utils/index.d.ts +4 -0
  77. package/dist/transactionIntent/utils/index.d.ts.map +1 -0
  78. package/dist/transactionIntent/utils/lifiHelpers.d.ts +10 -0
  79. package/dist/transactionIntent/utils/lifiHelpers.d.ts.map +1 -0
  80. package/dist/transactionIntent/utils/testnetHelpers.d.ts +3 -0
  81. package/dist/transactionIntent/utils/testnetHelpers.d.ts.map +1 -0
  82. package/dist/transactionIntent/validators.d.ts +6 -0
  83. package/dist/transactionIntent/validators.d.ts.map +1 -0
  84. package/dist/transactions.d.ts +6 -3
  85. package/dist/transactions.d.ts.map +1 -1
  86. package/dist/widget/components/AccountIntentTransactionHistoryButton.d.ts +4 -0
  87. package/dist/widget/components/AccountIntentTransactionHistoryButton.d.ts.map +1 -0
  88. package/dist/widget/components/AccountSettings.d.ts.map +1 -1
  89. package/dist/widget/components/ChainFilterDropdown.d.ts.map +1 -1
  90. package/dist/widget/components/ClassicSwap.d.ts +2 -3
  91. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  92. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  93. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  94. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -1
  95. package/dist/widget/components/DynamicInputStyles.d.ts +18 -0
  96. package/dist/widget/components/DynamicInputStyles.d.ts.map +1 -0
  97. package/dist/widget/components/DynamicSizeInputField.d.ts +13 -0
  98. package/dist/widget/components/DynamicSizeInputField.d.ts.map +1 -0
  99. package/dist/widget/components/Earn.d.ts +2 -3
  100. package/dist/widget/components/Earn.d.ts.map +1 -1
  101. package/dist/widget/components/ErrorAnimationIcon.d.ts +2 -0
  102. package/dist/widget/components/ErrorAnimationIcon.d.ts.map +1 -0
  103. package/dist/widget/components/FeeBreakdown.d.ts +9 -0
  104. package/dist/widget/components/FeeBreakdown.d.ts.map +1 -0
  105. package/dist/widget/components/FeeOptions.d.ts +5 -13
  106. package/dist/widget/components/FeeOptions.d.ts.map +1 -1
  107. package/dist/widget/components/Fund.d.ts +2 -3
  108. package/dist/widget/components/Fund.d.ts.map +1 -1
  109. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  110. package/dist/widget/components/FundSwap.d.ts +2 -3
  111. package/dist/widget/components/FundSwap.d.ts.map +1 -1
  112. package/dist/widget/components/FundingMethodSelectorButton.d.ts.map +1 -1
  113. package/dist/widget/components/Identicon.d.ts.map +1 -1
  114. package/dist/widget/components/MeshConnectExchanges.d.ts +0 -3
  115. package/dist/widget/components/MeshConnectExchanges.d.ts.map +1 -1
  116. package/dist/widget/components/Modal.d.ts.map +1 -1
  117. package/dist/widget/components/Pay.d.ts +2 -3
  118. package/dist/widget/components/Pay.d.ts.map +1 -1
  119. package/dist/widget/components/PoolDeposit.d.ts +3 -3
  120. package/dist/widget/components/PoolDeposit.d.ts.map +1 -1
  121. package/dist/widget/components/PoolWithdraw.d.ts +3 -20
  122. package/dist/widget/components/PoolWithdraw.d.ts.map +1 -1
  123. package/dist/widget/components/QuoteDetails.d.ts +2 -0
  124. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  125. package/dist/widget/components/Receipt.d.ts.map +1 -1
  126. package/dist/widget/components/RecipientSelectorButton.d.ts.map +1 -1
  127. package/dist/widget/components/ScreenHeader.d.ts.map +1 -1
  128. package/dist/widget/components/Swap.d.ts +2 -3
  129. package/dist/widget/components/Swap.d.ts.map +1 -1
  130. package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
  131. package/dist/widget/components/TokenDisplayNonSelectable.d.ts +11 -0
  132. package/dist/widget/components/TokenDisplayNonSelectable.d.ts.map +1 -0
  133. package/dist/widget/components/TokenSelector.d.ts.map +1 -1
  134. package/dist/widget/components/TokenSelectorButton.d.ts.map +1 -1
  135. package/dist/widget/components/Tooltip.d.ts +9 -0
  136. package/dist/widget/components/Tooltip.d.ts.map +1 -0
  137. package/dist/widget/components/TransferPendingVertical.d.ts.map +1 -1
  138. package/dist/widget/components/WaasFeeOptions.d.ts +1 -0
  139. package/dist/widget/components/WaasFeeOptions.d.ts.map +1 -1
  140. package/dist/widget/components/WalletConfirmation.d.ts.map +1 -1
  141. package/dist/widget/components/WalletConnect.d.ts.map +1 -1
  142. package/dist/widget/css/compiled.css +2 -2
  143. package/dist/widget/hooks/useCheckout.d.ts +17 -4
  144. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  145. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -1
  146. package/dist/widget/hooks/useQuote.d.ts +82 -0
  147. package/dist/widget/hooks/useQuote.d.ts.map +1 -0
  148. package/dist/widget/hooks/useSelectedFeeToken.d.ts +1 -0
  149. package/dist/widget/hooks/useSelectedFeeToken.d.ts.map +1 -1
  150. package/dist/widget/hooks/useSendForm.d.ts +5 -6
  151. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  152. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  153. package/dist/widget/hooks/useWalletConnectionContext.d.ts +25 -0
  154. package/dist/widget/hooks/useWalletConnectionContext.d.ts.map +1 -0
  155. package/dist/widget/index.js +2 -2
  156. package/dist/widget/widget.d.ts +17 -7
  157. package/dist/widget/widget.d.ts.map +1 -1
  158. package/package.json +19 -21
  159. package/src/aave.ts +54 -1
  160. package/src/abortController.ts +35 -0
  161. package/src/config.ts +57 -58
  162. package/src/constants.ts +11 -9
  163. package/src/error.ts +21 -3
  164. package/src/fees.ts +210 -0
  165. package/src/index.ts +35 -13
  166. package/src/intentReceiptMonitor.ts +102 -0
  167. package/src/intentReceiptPoller.ts +299 -0
  168. package/src/intents.ts +205 -171
  169. package/src/morpho.ts +58 -9
  170. package/src/mutations.ts +129 -0
  171. package/src/preconditions.ts +16 -21
  172. package/src/prepareSend.ts +92 -4699
  173. package/src/prices.ts +26 -22
  174. package/src/relaySdk.ts +2 -2
  175. package/src/sequenceWallet.ts +6 -73
  176. package/src/tokenBalances.ts +175 -69
  177. package/src/trails.ts +230 -722
  178. package/src/transactionIntent/constants.ts +11 -0
  179. package/src/transactionIntent/deposits/depositOrchestrator.ts +210 -0
  180. package/src/transactionIntent/deposits/gaslessDeposit.ts +588 -0
  181. package/src/transactionIntent/deposits/index.ts +3 -0
  182. package/src/transactionIntent/deposits/standardDeposit.ts +379 -0
  183. package/src/transactionIntent/execution/index.ts +1 -0
  184. package/src/transactionIntent/execution/transactionState.ts +35 -0
  185. package/src/transactionIntent/handlers/crossChain.ts +1707 -0
  186. package/src/transactionIntent/handlers/index.ts +3 -0
  187. package/src/transactionIntent/handlers/sameChainDifferentToken.ts +323 -0
  188. package/src/transactionIntent/handlers/sameChainSameToken.ts +712 -0
  189. package/src/transactionIntent/index.ts +9 -0
  190. package/src/transactionIntent/quote/feeExtractors.ts +81 -0
  191. package/src/transactionIntent/quote/index.ts +3 -0
  192. package/src/transactionIntent/quote/normalizeQuote.ts +367 -0
  193. package/src/transactionIntent/quote/quoteHelpers.ts +53 -0
  194. package/src/transactionIntent/types.ts +157 -0
  195. package/src/transactionIntent/utils/balanceChecker.ts +96 -0
  196. package/src/transactionIntent/utils/index.ts +3 -0
  197. package/src/transactionIntent/utils/lifiHelpers.ts +68 -0
  198. package/src/transactionIntent/utils/testnetHelpers.ts +10 -0
  199. package/src/transactionIntent/validators.ts +57 -0
  200. package/src/transactions.ts +98 -71
  201. package/src/widget/compiled.css +2 -2
  202. package/src/widget/components/AccountIntentTransactionHistory.tsx +36 -36
  203. package/src/widget/components/AccountIntentTransactionHistoryButton.tsx +22 -0
  204. package/src/widget/components/AccountSettings.tsx +70 -41
  205. package/src/widget/components/ChainFilterDropdown.tsx +24 -3
  206. package/src/widget/components/ClassicSwap.tsx +44 -107
  207. package/src/widget/components/ConfigDisplay.tsx +0 -11
  208. package/src/widget/components/ConnectWallet.tsx +4 -1
  209. package/src/widget/components/ConnectedWallets.tsx +51 -25
  210. package/src/widget/components/DynamicInputStyles.tsx +76 -0
  211. package/src/widget/components/DynamicSizeInputField.tsx +109 -0
  212. package/src/widget/components/Earn.tsx +34 -45
  213. package/src/widget/components/ErrorAnimationIcon.tsx +130 -0
  214. package/src/widget/components/FeeBreakdown.tsx +155 -0
  215. package/src/widget/components/FeeOption.tsx +2 -2
  216. package/src/widget/components/FeeOptions.tsx +151 -112
  217. package/src/widget/components/Fund.tsx +10 -29
  218. package/src/widget/components/FundMethods.tsx +4 -3
  219. package/src/widget/components/FundSwap.tsx +2 -3
  220. package/src/widget/components/FundingMethodSelectorButton.tsx +24 -14
  221. package/src/widget/components/Identicon.tsx +164 -95
  222. package/src/widget/components/MeshConnectExchanges.tsx +2 -15
  223. package/src/widget/components/Modal.tsx +0 -12
  224. package/src/widget/components/Pay.tsx +72 -75
  225. package/src/widget/components/PoolDeposit.tsx +221 -242
  226. package/src/widget/components/PoolWithdraw.tsx +347 -469
  227. package/src/widget/components/PriceImpactWarning.tsx +1 -1
  228. package/src/widget/components/QuoteDetails.tsx +906 -484
  229. package/src/widget/components/Receipt.tsx +16 -2
  230. package/src/widget/components/RecipientSelectorButton.tsx +7 -5
  231. package/src/widget/components/Recipients.tsx +1 -1
  232. package/src/widget/components/ScreenHeader.tsx +60 -36
  233. package/src/widget/components/Swap.tsx +2 -3
  234. package/src/widget/components/ThemeProvider.tsx +2 -1
  235. package/src/widget/components/TokenDisplayNonSelectable.tsx +40 -0
  236. package/src/widget/components/TokenImage.tsx +1 -1
  237. package/src/widget/components/TokenSelector.tsx +62 -53
  238. package/src/widget/components/TokenSelectorButton.tsx +38 -15
  239. package/src/widget/components/Tooltip.tsx +51 -0
  240. package/src/widget/components/TransferPendingVertical.tsx +12 -8
  241. package/src/widget/components/WaasFeeOptions.tsx +139 -4
  242. package/src/widget/components/WalletConfirmation.tsx +23 -13
  243. package/src/widget/components/WalletConnect.tsx +93 -29
  244. package/src/widget/hooks/useAmountUsd.ts +9 -9
  245. package/src/widget/hooks/useCheckout.ts +97 -9
  246. package/src/widget/hooks/useDefaultTokenSelection.tsx +27 -21
  247. package/src/widget/hooks/useQuote.ts +466 -0
  248. package/src/widget/hooks/useSelectedFeeToken.tsx +32 -37
  249. package/src/widget/hooks/useSendForm.ts +45 -51
  250. package/src/widget/hooks/useTokenList.ts +34 -26
  251. package/src/widget/hooks/useWalletConnectionContext.tsx +128 -0
  252. package/src/widget/widget.tsx +365 -390
  253. package/dist/apiClient.d.ts +0 -9
  254. package/dist/apiClient.d.ts.map +0 -1
  255. package/dist/intentEntrypoint.d.ts +0 -114
  256. package/dist/intentEntrypoint.d.ts.map +0 -1
  257. package/dist/metaTxnMonitor.d.ts +0 -15
  258. package/dist/metaTxnMonitor.d.ts.map +0 -1
  259. package/dist/metaTxns.d.ts +0 -11
  260. package/dist/metaTxns.d.ts.map +0 -1
  261. package/dist/relayer.d.ts +0 -43
  262. package/dist/relayer.d.ts.map +0 -1
  263. package/src/apiClient.ts +0 -35
  264. package/src/intentEntrypoint.ts +0 -203
  265. package/src/metaTxnMonitor.ts +0 -171
  266. package/src/metaTxns.ts +0 -45
  267. package/src/relayer.ts +0 -289
@@ -0,0 +1,1707 @@
1
+ import type {
2
+ Account,
3
+ Chain,
4
+ PublicClient,
5
+ WalletClient,
6
+ TransactionReceipt,
7
+ } from "viem"
8
+ import { createPublicClient, createWalletClient, http } from "viem"
9
+ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"
10
+ import type { TrailsAPIClient } from "@0xsequence/trails-api"
11
+ import type { Attestation } from "../../cctp.js"
12
+ import type { MetaTxnReceipt, PrepareSendReturn, SendReturn } from "../types.js"
13
+ import type { TransactionState } from "../../transactions.js"
14
+ import type { CheckoutOnHandlers } from "../../widget/hooks/useCheckout.js"
15
+ import type {
16
+ Intent,
17
+ CommitIntentResponse,
18
+ DepositSignature,
19
+ ExecuteIntentResponse,
20
+ } from "@0xsequence/trails-api"
21
+ import { TradeType, type SelectedFeeToken } from "../types.js"
22
+ import { logger } from "../../logger.js"
23
+ import {
24
+ isTestnetDebugMode,
25
+ getTestnetOriginTokenAddress,
26
+ } from "../utils/testnetHelpers.js"
27
+ import { shouldUseCctp, validateCctpDestinationToken } from "../validators.js"
28
+ import { getQueryParam } from "../../queryParams.js"
29
+ import { getIsCustomCalldata } from "../../contractUtils.js"
30
+ import { getNormalizedQuoteObject } from "../quote/normalizeQuote.js"
31
+ import {
32
+ cctpTransfer,
33
+ cctpTransferWithCustomCall,
34
+ getCCTPRelayerCallData,
35
+ getMintUSDCData,
36
+ getMessageTransmitter,
37
+ } from "../../cctp.js"
38
+ import { getChainInfo, getTestnetChainInfo } from "../../chains.js"
39
+ import { getTransactionStateFromReceipt } from "../execution/transactionState.js"
40
+ import {
41
+ simpleCreateSequenceWallet,
42
+ sequenceSendTransaction,
43
+ } from "../../sequenceWallet.js"
44
+ import { getIntentArgs } from "../quote/quoteHelpers.js"
45
+ import {
46
+ getIntent,
47
+ commitIntent,
48
+ buildCrossChainDepositParams,
49
+ } from "../../intents.js"
50
+ import { findFirstPreconditionForChainId } from "../../preconditions.js"
51
+ import {
52
+ decodeTrailsTokenSweeperEvents,
53
+ decodeGuestModuleEvents,
54
+ } from "../../decoders.js"
55
+ import { updatePersistentToast } from "../../toast.js"
56
+ import { queueCCTPTransfer } from "../../cctpqueue.js"
57
+ import { formatRawAmount } from "../../tokenBalances.js"
58
+ import { calcAmountUsdPrice } from "../../prices.js"
59
+ import { checkAccountBalance } from "../utils/balanceChecker.js"
60
+ import { estimateGasLimit } from "../../estimate.js"
61
+ import {
62
+ getFeesFromIntent,
63
+ getPriceImpactFromIntent,
64
+ getPriceImpactUsdFromIntent,
65
+ } from "../quote/feeExtractors.js"
66
+ import { getNeedsLifiNativeFee } from "../utils/lifiHelpers.js"
67
+ import { attemptUserDepositTx } from "../deposits/depositOrchestrator.js"
68
+ import {
69
+ getAccountTransactionHistory,
70
+ getTxTimeDiff,
71
+ } from "../../transactions.js"
72
+ import { trackPaymentCompleted, trackPaymentError } from "../../analytics.js"
73
+ import { POLLING_INTERVALS } from "../constants.js"
74
+ import { pollIntentReceipt } from "../../intentReceiptPoller.js"
75
+ import type { IntentReceipt } from "@0xsequence/trails-api"
76
+
77
+ /**
78
+ * @description
79
+ * This handler manages cross-chain token transfers (e.g., transferring USDC to USDC on a different chain).
80
+ * It supports both standard transfers and custom calldata execution.
81
+ *
82
+ * Key characteristics:
83
+ * - Uses the Trails API with commitIntent and waitIntentReceipt for monitoring
84
+ * - Two transaction legs (deposit and user transfer)
85
+ * - Cross-chain bridging or token swapping required
86
+ * - More complex flow compared to same-chain scenarios
87
+ *
88
+ * Transaction Flow:
89
+ * 1. Create intent via createIntent (Intent object created)
90
+ * 2. Commit intent via commitIntent (Intent record created in backend)
91
+ * 3. User submits deposit transaction on origin chain
92
+ * 4. Poll via waitIntentReceipt API to monitor deposit transaction completion
93
+ * 5. User submits transfer transaction on destination chain
94
+ * 6. Poll via waitIntentReceipt API to monitor transfer transaction completion
95
+ * 7. Update transaction state and invoke callbacks
96
+ *
97
+ * Error Handling:
98
+ * - Balance validation before transaction submission
99
+ * - Gas estimation for accurate fee calculation
100
+ * - Proper error propagation with checkout error callbacks
101
+ * - Fallback logging for debugging
102
+ *
103
+ * State Management:
104
+ * - transactionStates[0]: Deposit transaction
105
+ * - transactionStates[1]: User transfer transaction
106
+ * - transactionStates[2]: Destination transfer transaction
107
+ * - Updates propagated via onTransactionStateChange callback
108
+ * - Analytics tracking for payment completion or errors
109
+ */
110
+ export async function handleCrossChain({
111
+ mainSignerAddress,
112
+ originChainId,
113
+ originTokenAddress,
114
+ originTokenBalance,
115
+ destinationChainId,
116
+ destinationTokenAddress,
117
+ swapAmount,
118
+ originTokenSymbol,
119
+ destinationTokenSymbol,
120
+ recipient,
121
+ destinationCalldata,
122
+ trailsClient,
123
+ sourceTokenPriceUsd,
124
+ destinationTokenPriceUsd,
125
+ sourceTokenDecimals,
126
+ destinationTokenDecimals,
127
+ paymasterUrl,
128
+ walletClient,
129
+ publicClient,
130
+ chain,
131
+ account,
132
+ fee,
133
+ dryMode,
134
+ onTransactionStateChange,
135
+ transactionStates,
136
+ slippageTolerance,
137
+ tradeType,
138
+ originNativeTokenPriceUsd,
139
+ quoteProvider,
140
+ fundMethod,
141
+ mode,
142
+ checkoutOnHandlers,
143
+ selectedFeeToken,
144
+ walletId,
145
+ abortSignal,
146
+ commitIntentFn,
147
+ executeIntentFn,
148
+ }: {
149
+ mainSignerAddress: string
150
+ originChainId: number
151
+ originTokenAddress: string
152
+ originTokenBalance: string
153
+ destinationChainId: number
154
+ destinationTokenAddress: string
155
+ swapAmount: string
156
+ originTokenSymbol: string
157
+ destinationTokenSymbol: string
158
+ recipient: string
159
+ destinationCalldata?: string
160
+ trailsClient: TrailsAPIClient
161
+ sourceTokenPriceUsd?: number | null
162
+ destinationTokenPriceUsd?: number | null
163
+ sourceTokenDecimals: number
164
+ destinationTokenDecimals: number
165
+ paymasterUrl?: string
166
+ walletClient: WalletClient
167
+ publicClient: PublicClient
168
+ chain: Chain
169
+ account: Account
170
+ fee: string
171
+ dryMode: boolean
172
+ onTransactionStateChange: (transactionStates: TransactionState[]) => void
173
+ transactionStates: TransactionState[]
174
+ slippageTolerance: string
175
+ tradeType: TradeType
176
+ originNativeTokenPriceUsd?: number | null
177
+ quoteProvider?: string | null
178
+ fundMethod?: string
179
+ mode?: "pay" | "fund" | "earn" | "swap" | "receive"
180
+ checkoutOnHandlers?: CheckoutOnHandlers
181
+ selectedFeeToken?: SelectedFeeToken
182
+ walletId?: string
183
+ abortSignal?: AbortSignal
184
+ commitIntentFn?: (intent: Intent) => Promise<CommitIntentResponse>
185
+ executeIntentFn?: (params: {
186
+ intentId: string
187
+ depositTransactionHash?: string
188
+ depositSignature?: DepositSignature
189
+ }) => Promise<ExecuteIntentResponse>
190
+ }): Promise<PrepareSendReturn> {
191
+ const testnet = isTestnetDebugMode()
192
+ const useCctp = shouldUseCctp(
193
+ originTokenAddress,
194
+ destinationTokenAddress,
195
+ originChainId,
196
+ destinationChainId,
197
+ )
198
+
199
+ const cctpFlag = getQueryParam("cctp") === "true"
200
+ const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
201
+
202
+ // Validate CCTP destination token requirement for explicit CCTP usage
203
+ if (useCctp && cctpFlag) {
204
+ validateCctpDestinationToken(
205
+ destinationTokenAddress,
206
+ destinationChainId,
207
+ "cctp",
208
+ )
209
+ logger.console.log("[trails-sdk] using cctp")
210
+
211
+ const quote = await getNormalizedQuoteObject({
212
+ destinationAddress: recipient,
213
+ destinationCalldata,
214
+ originAmount: swapAmount,
215
+ originTokenPriceUsd: sourceTokenPriceUsd?.toString() || null,
216
+ destinationAmount: swapAmount,
217
+ destinationTokenPriceUsd: destinationTokenPriceUsd?.toString() || null,
218
+ originTokenAddress: originTokenAddress,
219
+ destinationTokenAddress: destinationTokenAddress,
220
+ originChainId,
221
+ destinationChainId,
222
+ transactionStates,
223
+ originNativeTokenPriceUsd,
224
+ slippageTolerance,
225
+ quoteProvider: "cctp",
226
+ })
227
+
228
+ // Call onCheckoutQuote callback if provided
229
+ if (checkoutOnHandlers?.triggerCheckoutQuote) {
230
+ checkoutOnHandlers.triggerCheckoutQuote(quote)
231
+ }
232
+
233
+ return {
234
+ quote,
235
+ send: async ({
236
+ onOriginSend,
237
+ selectedFeeToken: runtimeSelectedFeeToken,
238
+ }: {
239
+ onOriginSend?: () => void
240
+ selectedFeeToken?: SelectedFeeToken
241
+ }): Promise<SendReturn> => {
242
+ // Use runtime selectedFeeToken if provided, otherwise fall back to the one from prepareSend
243
+ const effectiveSelectedFeeToken =
244
+ runtimeSelectedFeeToken ?? selectedFeeToken
245
+ logger.console.log(
246
+ "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] send() called (LEGACY PATH):",
247
+ {
248
+ runtimeSelectedFeeToken,
249
+ prepareTimeSelectedFeeToken: selectedFeeToken,
250
+ effectiveSelectedFeeToken,
251
+ },
252
+ )
253
+ const originChain = testnet ? getTestnetChainInfo(chain)! : chain
254
+ const destinationChain = testnet
255
+ ? getTestnetChainInfo(destinationChainId)!
256
+ : getChainInfo(destinationChainId)
257
+
258
+ if (!originChain || !destinationChain) {
259
+ logger.console.error("[trails-sdk] Invalid chain", {
260
+ originChain,
261
+ destinationChain,
262
+ originChainId,
263
+ destinationChainId,
264
+ chain,
265
+ testnet,
266
+ })
267
+ throw new Error("Invalid chain")
268
+ }
269
+
270
+ logger.console.log("[trails-sdk] originChain", originChain)
271
+ logger.console.log("[trails-sdk] destinationChain", destinationChain)
272
+
273
+ const originPublicClient = createPublicClient({
274
+ chain: originChain,
275
+ transport: http(),
276
+ })
277
+
278
+ let txHash: `0x${string}`
279
+ let waitForAttestation: () => Promise<Attestation>
280
+
281
+ if (hasCustomCalldata) {
282
+ const result = await cctpTransferWithCustomCall({
283
+ walletClient,
284
+ originChain,
285
+ destinationChain,
286
+ amount: BigInt(swapAmount),
287
+ })
288
+
289
+ txHash = result.txHash
290
+ waitForAttestation = result.waitForAttestation
291
+ } else {
292
+ const result = await cctpTransfer({
293
+ walletClient,
294
+ originChain,
295
+ destinationChain,
296
+ amount: BigInt(swapAmount),
297
+ })
298
+
299
+ txHash = result.txHash
300
+ waitForAttestation = result.waitForAttestation
301
+ }
302
+
303
+ if (onOriginSend) {
304
+ onOriginSend()
305
+ }
306
+
307
+ logger.console.log("[trails-sdk] waiting for tx", txHash)
308
+
309
+ const receipt = await originPublicClient.waitForTransactionReceipt({
310
+ hash: txHash,
311
+ })
312
+
313
+ logger.console.log("[trails-sdk] tx receipt", receipt)
314
+
315
+ transactionStates[0] = getTransactionStateFromReceipt(
316
+ receipt,
317
+ originChain.id,
318
+ transactionStates[0]?.label,
319
+ )
320
+ transactionStates[1] = getTransactionStateFromReceipt(
321
+ receipt,
322
+ originChain.id,
323
+ transactionStates[1]?.label,
324
+ )
325
+
326
+ onTransactionStateChange(transactionStates)
327
+
328
+ const attestation = await waitForAttestation()
329
+
330
+ if (!attestation) {
331
+ throw new Error("Failed to retrieve attestation")
332
+ }
333
+
334
+ const tokenMessenger = getMessageTransmitter(destinationChain.id)!
335
+
336
+ logger.console.log("[trails-sdk] tokenMessenger", tokenMessenger)
337
+ const calls: {
338
+ to: `0x${string}`
339
+ data: `0x${string}`
340
+ value: bigint
341
+ }[] = []
342
+
343
+ if (hasCustomCalldata) {
344
+ calls.push(
345
+ await getCCTPRelayerCallData({
346
+ attestation,
347
+ targetContract: recipient,
348
+ calldata: destinationCalldata as `0x${string}`,
349
+ gasLimit: 300000n,
350
+ destinationChain,
351
+ }),
352
+ )
353
+ } else {
354
+ calls.push(
355
+ await getMintUSDCData({
356
+ tokenMessenger,
357
+ attestation,
358
+ }),
359
+ )
360
+ }
361
+
362
+ logger.console.log("[trails-sdk] calls", calls)
363
+ const delegatorPrivateKey = generatePrivateKey()
364
+ const delegatorAccount = privateKeyToAccount(delegatorPrivateKey)
365
+ const delegatorClient = createWalletClient({
366
+ account: delegatorAccount,
367
+ chain: destinationChain,
368
+ transport: http(),
369
+ })
370
+ const destinationPublicClient = createPublicClient({
371
+ chain: destinationChain,
372
+ transport: http(),
373
+ })
374
+ logger.console.log("[trails-sdk] delegatorClient", delegatorClient)
375
+
376
+ const sequenceWalletAddress = await simpleCreateSequenceWallet(
377
+ delegatorAccount as any,
378
+ )
379
+ logger.console.log(
380
+ "[trails-sdk] sequenceWalletAddress",
381
+ sequenceWalletAddress,
382
+ )
383
+ const sequenceTxHash = await sequenceSendTransaction(
384
+ sequenceWalletAddress,
385
+ delegatorClient,
386
+ destinationPublicClient,
387
+ calls,
388
+ destinationChain,
389
+ )
390
+
391
+ const destinationReceipt =
392
+ await destinationPublicClient.waitForTransactionReceipt({
393
+ hash: sequenceTxHash as `0x${string}`,
394
+ })
395
+
396
+ logger.console.log(
397
+ "[trails-sdk] destinationReceipt",
398
+ destinationReceipt,
399
+ )
400
+
401
+ if (transactionStates[2]) {
402
+ transactionStates[2] = getTransactionStateFromReceipt(
403
+ destinationReceipt,
404
+ destinationChain.id,
405
+ transactionStates[2]?.label,
406
+ )
407
+ }
408
+
409
+ onTransactionStateChange(transactionStates)
410
+
411
+ return {
412
+ depositUserTxnReceipt: receipt,
413
+ originMetaTxnReceipt: null,
414
+ destinationMetaTxnReceipt: null,
415
+ totalCompletionSeconds: 0,
416
+ }
417
+ },
418
+ }
419
+ }
420
+
421
+ const destinationSalt = Date.now().toString()
422
+
423
+ const intentArgs = getIntentArgs(
424
+ mainSignerAddress,
425
+ originChainId,
426
+ originTokenAddress,
427
+ tradeType === TradeType.EXACT_OUTPUT ? originTokenBalance : swapAmount, // originTokenAmount
428
+ destinationChainId,
429
+ destinationTokenAddress,
430
+ tradeType === TradeType.EXACT_OUTPUT ? swapAmount : "0", // destinationTokenAmount
431
+ recipient,
432
+ destinationCalldata,
433
+ destinationSalt,
434
+ slippageTolerance,
435
+ tradeType,
436
+ quoteProvider,
437
+ )
438
+
439
+ logger.console.log("[trails-sdk] Creating intent with args:", intentArgs)
440
+
441
+ const { intent, gasFeeOptions } = await getIntent(trailsClient, intentArgs, {
442
+ originTokenSymbol,
443
+ destinationTokenSymbol,
444
+ feeTokenSymbol: originTokenSymbol,
445
+ })
446
+ logger.console.log("[trails-sdk] Got intent:", intent)
447
+ logger.console.log("[trails-sdk] Got gasFeeOptions:", gasFeeOptions)
448
+
449
+ if (!intent.preconditions?.length || !intent.calls?.length) {
450
+ throw new Error("Invalid intent")
451
+ }
452
+
453
+ const originIntentAddress = intent.originIntentAddress
454
+ logger.console.log(
455
+ "[trails-sdk] origin intent address:",
456
+ originIntentAddress.toString(),
457
+ )
458
+
459
+ const firstPrecondition = findFirstPreconditionForChainId(
460
+ intent.preconditions,
461
+ originChainId,
462
+ )
463
+
464
+ if (!firstPrecondition) {
465
+ throw new Error("No precondition found for origin chain")
466
+ }
467
+
468
+ const firstPreconditionMin = firstPrecondition?.minAmount?.toString()
469
+ const depositAmount = firstPreconditionMin
470
+
471
+ const quoteToAmount = intent.quote.toAmount
472
+ const quoteToAmountMin = intent.quote.toAmountMin
473
+
474
+ const originSendAmountFormatted = formatRawAmount(
475
+ depositAmount,
476
+ sourceTokenDecimals,
477
+ )
478
+
479
+ const depositAmountUsd = calcAmountUsdPrice({
480
+ amount: originSendAmountFormatted,
481
+ usdPrice: sourceTokenPriceUsd,
482
+ })
483
+
484
+ logger.console.log("[trails-sdk] depositAmountUsd", depositAmountUsd, {
485
+ amount: originSendAmountFormatted,
486
+ usdPrice: sourceTokenPriceUsd,
487
+ originTokenSymbol,
488
+ originSendAmountFormatted,
489
+ })
490
+
491
+ const effectiveDestinationTokenAmount = quoteToAmount
492
+ const effectiveDestinationTokenAmountFormatted = formatRawAmount(
493
+ effectiveDestinationTokenAmount,
494
+ destinationTokenDecimals,
495
+ )
496
+
497
+ const effectiveDestinationTokenAmountUsd = calcAmountUsdPrice({
498
+ amount: effectiveDestinationTokenAmountFormatted,
499
+ usdPrice: destinationTokenPriceUsd,
500
+ })
501
+
502
+ logger.console.log(
503
+ "[trails-sdk] effectiveDestinationTokenAmountUsd",
504
+ effectiveDestinationTokenAmountUsd,
505
+ {
506
+ amount: effectiveDestinationTokenAmountFormatted,
507
+ usdPrice: destinationTokenPriceUsd,
508
+ destinationTokenSymbol,
509
+ effectiveDestinationTokenAmount,
510
+ destinationTokenAddress,
511
+ destinationChainId,
512
+ },
513
+ )
514
+
515
+ let noSufficientBalance = false
516
+
517
+ if (!(fundMethod === "qr-code" || fundMethod === "exchange")) {
518
+ const { hasEnoughBalance } = await checkAccountBalance({
519
+ account,
520
+ tokenAddress: originTokenAddress,
521
+ depositAmount,
522
+ publicClient,
523
+ })
524
+
525
+ if (!hasEnoughBalance) {
526
+ noSufficientBalance = true
527
+ }
528
+ }
529
+
530
+ // Estimate gas limit for the quote (same logic as in attemptNonGaslessUserDeposit)
531
+ const originCallParamsForEstimate = buildCrossChainDepositParams({
532
+ originTokenAddress,
533
+ originIntentAddress,
534
+ depositAmount: firstPreconditionMin,
535
+ fee,
536
+ originChainId,
537
+ chain,
538
+ })
539
+
540
+ logger.console.log(
541
+ "[trails-sdk][gas-estimation] About to estimate gas limit for cross-chain quote with params:",
542
+ {
543
+ account: account.address,
544
+ to: originCallParamsForEstimate.to,
545
+ data: originCallParamsForEstimate.data,
546
+ value: originCallParamsForEstimate.value,
547
+ },
548
+ )
549
+
550
+ const estimatedGasLimitForQuote = await estimateGasLimit(
551
+ publicClient,
552
+ {
553
+ account: account.address,
554
+ to: originCallParamsForEstimate.to,
555
+ data: originCallParamsForEstimate.data,
556
+ value: BigInt(originCallParamsForEstimate.value),
557
+ },
558
+ "quote",
559
+ )
560
+
561
+ logger.console.log(
562
+ "[trails-sdk][gas-estimation] Estimated gas limit for cross-chain quote:",
563
+ estimatedGasLimitForQuote,
564
+ )
565
+
566
+ const quote = await getNormalizedQuoteObject({
567
+ originDepositAddress: originIntentAddress,
568
+ destinationDepositAddress: intent.destinationIntentAddress,
569
+ destinationAddress: recipient,
570
+ destinationCalldata,
571
+ originAmount: depositAmount,
572
+ destinationAmount: quoteToAmount.toString(),
573
+ originAmountMin: depositAmount,
574
+ destinationAmountMin: quoteToAmountMin.toString(),
575
+ originTokenAddress: originTokenAddress,
576
+ destinationTokenAddress: destinationTokenAddress,
577
+ originTokenPriceUsd: sourceTokenPriceUsd?.toString() || null,
578
+ destinationTokenPriceUsd: destinationTokenPriceUsd?.toString() || null,
579
+ fees: getFeesFromIntent(intent, {
580
+ tradeType,
581
+ fromAmountUsd: depositAmountUsd,
582
+ toAmountUsd: effectiveDestinationTokenAmountUsd,
583
+ }),
584
+ originChainId,
585
+ destinationChainId,
586
+ slippageTolerance: slippageTolerance,
587
+ priceImpact: getPriceImpactFromIntent(intent),
588
+ priceImpactUsd: getPriceImpactUsdFromIntent(intent),
589
+ transactionStates,
590
+ originNativeTokenPriceUsd,
591
+ quoteProvider: intent.quote?.quoteProvider,
592
+ noSufficientBalance,
593
+ estimatedGasLimit: estimatedGasLimitForQuote,
594
+ intent,
595
+ })
596
+
597
+ // Call onCheckoutQuote callback if provided
598
+ if (checkoutOnHandlers?.triggerCheckoutQuote) {
599
+ checkoutOnHandlers.triggerCheckoutQuote(quote)
600
+ }
601
+
602
+ // Gasless fee options are now returned in QuoteIntentResponse.gasFeeOptions
603
+ // and properly propagated from getIntent()
604
+ const effectiveGasless =
605
+ selectedFeeToken !== null &&
606
+ selectedFeeToken !== undefined &&
607
+ walletId !== "sequence-waas"
608
+ if (effectiveGasless) {
609
+ logger.console.log(
610
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless enabled. Using gasFeeOptions from QuoteIntent response",
611
+ { originChainId, gasFeeOptions },
612
+ )
613
+ } else if (walletId === "sequence-waas") {
614
+ logger.console.log(
615
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Skipping gasless fee options for sequence-waas wallet",
616
+ )
617
+ } else {
618
+ logger.console.log(
619
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless disabled",
620
+ )
621
+ }
622
+
623
+ return {
624
+ quote,
625
+ feeOptions: gasFeeOptions,
626
+ send: async ({
627
+ onOriginSend,
628
+ selectedFeeToken: runtimeSelectedFeeToken,
629
+ }: {
630
+ onOriginSend?: () => void
631
+ selectedFeeToken?: SelectedFeeToken
632
+ }): Promise<SendReturn> => {
633
+ try {
634
+ // Use runtime selectedFeeToken if provided, otherwise fall back to the one from prepareSend
635
+ const effectiveSelectedFeeToken =
636
+ runtimeSelectedFeeToken ?? selectedFeeToken
637
+ logger.console.log(
638
+ "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] send() called with:",
639
+ {
640
+ runtimeSelectedFeeToken,
641
+ prepareTimeSelectedFeeToken: selectedFeeToken,
642
+ effectiveSelectedFeeToken,
643
+ },
644
+ )
645
+ const commitIntentFnToUse =
646
+ commitIntentFn ||
647
+ ((intent: Intent) =>
648
+ commitIntent(trailsClient, intent, {
649
+ originTokenSymbol,
650
+ destinationTokenSymbol,
651
+ }))
652
+ await commitIntentFnToUse(intent)
653
+
654
+ if (!(fundMethod === "qr-code" || fundMethod === "exchange")) {
655
+ const { hasEnoughBalance, balanceError } = await checkAccountBalance({
656
+ account,
657
+ tokenAddress: originTokenAddress,
658
+ depositAmount,
659
+ publicClient,
660
+ })
661
+
662
+ if (!hasEnoughBalance) {
663
+ throw balanceError
664
+ }
665
+ }
666
+
667
+ logger.console.log("[trails-sdk] sending origin transaction")
668
+ const usingLIfi = false
669
+ let needsNativeFee = false
670
+
671
+ if (usingLIfi) {
672
+ needsNativeFee = getNeedsLifiNativeFee({
673
+ originTokenAddress,
674
+ destinationTokenAmount: swapAmount,
675
+ destinationTokenDecimals,
676
+ sourceTokenDecimals,
677
+ sourceTokenPriceUsd: sourceTokenPriceUsd ?? null,
678
+ destinationTokenPriceUsd: destinationTokenPriceUsd ?? null,
679
+ depositAmount,
680
+ })
681
+ }
682
+
683
+ logger.console.log("[trails-sdk] needsNativeFee", needsNativeFee)
684
+ logger.console.log(
685
+ "[trails-sdk] sourceTokenPriceUsd",
686
+ sourceTokenPriceUsd,
687
+ )
688
+ logger.console.log(
689
+ "[trails-sdk] destinationTokenPriceUsd",
690
+ destinationTokenPriceUsd,
691
+ )
692
+ logger.console.log(
693
+ "[trails-sdk] sourceTokenDecimals",
694
+ sourceTokenDecimals,
695
+ )
696
+ logger.console.log(
697
+ "[trails-sdk] destinationTokenDecimals",
698
+ destinationTokenDecimals,
699
+ )
700
+
701
+ let depositUserTxnReceipt: TransactionReceipt | null = null
702
+ let originMetaTxnReceipt: MetaTxnReceipt | null = null
703
+ let destinationMetaTxnReceipt: MetaTxnReceipt | null = null
704
+
705
+ const testnet = isTestnetDebugMode()
706
+ const effectiveOriginChain = testnet
707
+ ? getTestnetChainInfo(chain)!
708
+ : chain
709
+ const effectiveOriginTokenAddress = testnet
710
+ ? getTestnetOriginTokenAddress(effectiveOriginChain.id)
711
+ : originTokenAddress
712
+
713
+ logger.console.log("[trails-sdk] testnet", testnet)
714
+
715
+ const destinationPublicClient = createPublicClient({
716
+ chain: getChainInfo(destinationChainId)!,
717
+ transport: http(),
718
+ })
719
+
720
+ const depositPromise = async () => {
721
+ logger.console.log(
722
+ "[trails-sdk] depositPromise called - starting deposit transaction",
723
+ )
724
+ logger.console.log("[trails-sdk] fundMethod value:", fundMethod)
725
+
726
+ // Skip wallet deposit if fund method is qr-code
727
+ if (fundMethod === "qr-code" || fundMethod === "exchange") {
728
+ logger.console.log(
729
+ "[trails-sdk] Skipping wallet deposit for QR code mode - fundMethod is:",
730
+ fundMethod,
731
+ )
732
+ return
733
+ }
734
+
735
+ logger.console.log(
736
+ "[trails-sdk] Calling attemptUserDepositTx with params:",
737
+ {
738
+ originTokenAddress: effectiveOriginTokenAddress,
739
+ paymasterUrl,
740
+ chain: effectiveOriginChain.id,
741
+ account: account.address,
742
+ firstPreconditionMin,
743
+ originIntentAddress,
744
+ fee,
745
+ dryMode,
746
+ feeOptions: gasFeeOptions,
747
+ selectedFeeToken: effectiveSelectedFeeToken,
748
+ selectedFeeTokenType: typeof effectiveSelectedFeeToken,
749
+ selectedFeeTokenValue: JSON.stringify(effectiveSelectedFeeToken),
750
+ },
751
+ )
752
+
753
+ depositUserTxnReceipt = await attemptUserDepositTx({
754
+ originTokenAddress: effectiveOriginTokenAddress,
755
+ paymasterUrl,
756
+ chain: effectiveOriginChain,
757
+ account,
758
+ firstPreconditionMin,
759
+ originIntentAddress,
760
+ onOriginSend,
761
+ publicClient,
762
+ walletClient,
763
+ destinationTokenDecimals,
764
+ sourceTokenDecimals,
765
+ fee,
766
+ dryMode,
767
+ sourceTokenPriceUsd: sourceTokenPriceUsd ?? null,
768
+ destinationTokenPriceUsd: destinationTokenPriceUsd ?? null,
769
+ swapAmount,
770
+ onTransactionStateChange,
771
+ transactionStates,
772
+ fundMethod,
773
+ originTokenSymbol,
774
+ destinationTokenSymbol,
775
+ depositAmountUsd,
776
+ feeOptions: gasFeeOptions,
777
+ trailsClient,
778
+ selectedFeeToken: effectiveSelectedFeeToken,
779
+ walletId,
780
+ abortSignal,
781
+ intentId: intent.intentId,
782
+ })
783
+
784
+ // In gasless mode, depositUserTxnReceipt will be null because the transaction
785
+ // is submitted to the backend and monitored via checkForDepositTx polling
786
+ // In non-gasless mode, we should have a receipt
787
+ // if (!depositUserTxnReceipt && !effectiveGasless) {
788
+ // throw new Error("Failed to send origin transaction")
789
+ // }
790
+
791
+ // Only process receipt if we have one (non-gasless mode)
792
+ if (depositUserTxnReceipt) {
793
+ transactionStates[0] = getTransactionStateFromReceipt(
794
+ depositUserTxnReceipt,
795
+ originChainId,
796
+ transactionStates[0]?.label,
797
+ )
798
+
799
+ // Call executeIntent after successful origin transaction
800
+ // This triggers the backend to start executing the intent
801
+ // In gasless mode, the executeIntent is called in attemptGaslessDeposit
802
+ if (depositUserTxnReceipt.status === "success" && intent.intentId) {
803
+ logger.console.log(
804
+ "[trails-sdk] Calling executeIntent with deposit transaction hash",
805
+ {
806
+ intentId: intent.intentId,
807
+ txHash: depositUserTxnReceipt.transactionHash,
808
+ },
809
+ )
810
+ try {
811
+ const executeIntentFnToUse =
812
+ executeIntentFn ||
813
+ trailsClient.executeIntent.bind(trailsClient)
814
+ await executeIntentFnToUse({
815
+ intentId: intent.intentId,
816
+ depositTransactionHash: depositUserTxnReceipt.transactionHash,
817
+ })
818
+ logger.console.log(
819
+ "[trails-sdk] executeIntent completed successfully",
820
+ )
821
+ } catch (error) {
822
+ logger.console.error(
823
+ "[trails-sdk] Error calling executeIntent:",
824
+ error,
825
+ )
826
+ }
827
+ }
828
+
829
+ try {
830
+ transactionStates[0].decodedTrailsTokenSweeperEvents =
831
+ decodeTrailsTokenSweeperEvents(depositUserTxnReceipt)
832
+ transactionStates[0].decodedGuestModuleEvents =
833
+ decodeGuestModuleEvents(depositUserTxnReceipt)
834
+ transactionStates[0].refunded =
835
+ transactionStates[0].decodedTrailsTokenSweeperEvents.findIndex(
836
+ (event) =>
837
+ event.type === "Refund" || event.type === "RefundAndSweep",
838
+ ) !== -1
839
+ logger.console.log(
840
+ "[trails-sdk] [GASLESS-FLOW] Gasless deposit events decoded",
841
+ {
842
+ chainId: originChainId,
843
+ callFailed: (
844
+ transactionStates[0].decodedGuestModuleEvents || []
845
+ ).filter((e: any) => e?.type === "CallFailed").length,
846
+ sweeperEvents: (
847
+ transactionStates[0].decodedTrailsTokenSweeperEvents || []
848
+ ).length,
849
+ refunded: transactionStates[0].refunded,
850
+ },
851
+ )
852
+
853
+ // Check for transaction failure or refund
854
+ const hasCallFailed = (
855
+ transactionStates[0].decodedGuestModuleEvents || []
856
+ ).some((e: any) => e?.type === "CallFailed")
857
+
858
+ if (transactionStates[0].refunded || hasCallFailed) {
859
+ const errorMessage = transactionStates[0].refunded
860
+ ? "Transaction was refunded"
861
+ : "Transaction call failed"
862
+
863
+ logger.console.error(
864
+ "[trails-sdk] [GASLESS-FLOW] Deposit transaction failed",
865
+ {
866
+ refunded: transactionStates[0].refunded,
867
+ callFailed: hasCallFailed,
868
+ },
869
+ )
870
+
871
+ // Call onCheckoutError callback if provided
872
+ if (checkoutOnHandlers?.triggerCheckoutError) {
873
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
874
+ }
875
+ }
876
+ } catch (error) {
877
+ logger.console.error(
878
+ "[trails-sdk] Error decoding gasless deposit events",
879
+ error,
880
+ )
881
+ }
882
+
883
+ onTransactionStateChange(transactionStates)
884
+
885
+ setTimeout(() => {
886
+ const destinationChain = getChainInfo(destinationChainId)
887
+ updatePersistentToast(
888
+ "In Progress",
889
+ `Your transaction to ${destinationChain?.name || "chain"} is in progress`,
890
+ "info",
891
+ )
892
+ }, 1000)
893
+ } else {
894
+ // Gasless mode - transaction submitted to backend via executeIntent in attemptGaslessDeposit
895
+ logger.console.log(
896
+ "[trails-sdk] Gasless mode - intent submitted to backend via executeIntent, transaction will be monitored via polling",
897
+ )
898
+ }
899
+
900
+ // Note: Unified polling will be started outside depositPromise after it completes
901
+ }
902
+
903
+ const checkForDepositTx = async () => {
904
+ while (true) {
905
+ // Check if we should abort
906
+ if (abortSignal?.aborted) {
907
+ logger.console.log(
908
+ "[trails-sdk] Aborting deposit tx check due to abort signal",
909
+ )
910
+ return null
911
+ }
912
+
913
+ try {
914
+ const response = await getAccountTransactionHistory({
915
+ chainId: originChainId,
916
+ accountAddress: originIntentAddress,
917
+ abortSignal,
918
+ })
919
+ logger.console.log(
920
+ "[trails-sdk] getAccountTransactionHistory response",
921
+ response,
922
+ )
923
+ if (response.transactions.length > 0) {
924
+ const tx = response.transactions[0]
925
+ if (!tx?.txnHash) {
926
+ await new Promise((resolve) =>
927
+ setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
928
+ )
929
+ continue
930
+ }
931
+ // const isReceive = tx.transfers.some(
932
+ // (transfer) => transfer.transferType === "RECEIVE",
933
+ // )
934
+ // if (!isReceive) {
935
+ // await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY))
936
+ // continue
937
+ // }
938
+ const originDepositTxReceipt =
939
+ await publicClient.getTransactionReceipt({
940
+ hash: tx.txnHash as `0x${string}`,
941
+ })
942
+
943
+ depositUserTxnReceipt = originDepositTxReceipt
944
+
945
+ transactionStates[0] = getTransactionStateFromReceipt(
946
+ originDepositTxReceipt,
947
+ originChainId,
948
+ transactionStates[0]?.label,
949
+ )
950
+ onTransactionStateChange(transactionStates)
951
+
952
+ if (onOriginSend) {
953
+ onOriginSend()
954
+ }
955
+ break
956
+ }
957
+ } catch (error) {
958
+ logger.console.error("Error checking for deposit tx", error)
959
+ }
960
+ await new Promise((resolve) =>
961
+ setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
962
+ )
963
+ }
964
+ }
965
+
966
+ const _checkForDestinationDepositTx: () => Promise<TransactionReceipt | null> =
967
+ async () => {
968
+ while (true) {
969
+ // Check if we should abort
970
+ if (abortSignal?.aborted) {
971
+ logger.console.log(
972
+ "[trails-sdk] Aborting destination deposit tx check due to abort signal",
973
+ )
974
+ return null
975
+ }
976
+
977
+ try {
978
+ const response = await getAccountTransactionHistory({
979
+ chainId: destinationChainId,
980
+ accountAddress:
981
+ intent.destinationIntentAddress as `0x${string}`,
982
+ abortSignal,
983
+ })
984
+ logger.console.log(
985
+ "[trails-sdk] getAccountTransactionHistory response",
986
+ response,
987
+ )
988
+ if (response.transactions.length > 0) {
989
+ const tx = response.transactions[0]
990
+ if (!tx?.txnHash) {
991
+ await new Promise((resolve) =>
992
+ setTimeout(
993
+ resolve,
994
+ POLLING_INTERVALS.TRANSACTION_HISTORY,
995
+ ),
996
+ )
997
+ continue
998
+ }
999
+ // const isReceive = tx.transfers.some(
1000
+ // (transfer) => transfer.transferType === "RECEIVE",
1001
+ // )
1002
+ // if (!isReceive) {
1003
+ // await new Promise((resolve) => setTimeout(resolve, 1000))
1004
+ // continue
1005
+ // }
1006
+ const destinationDepositTxReceipt =
1007
+ await destinationPublicClient.getTransactionReceipt({
1008
+ hash: tx.txnHash as `0x${string}`,
1009
+ })
1010
+
1011
+ transactionStates[2] = getTransactionStateFromReceipt(
1012
+ destinationDepositTxReceipt,
1013
+ destinationChainId,
1014
+ transactionStates[2]?.label,
1015
+ )
1016
+ onTransactionStateChange(transactionStates)
1017
+
1018
+ return destinationDepositTxReceipt
1019
+ }
1020
+ } catch (error) {
1021
+ logger.console.error("Error checking for deposit tx", error)
1022
+ }
1023
+ await new Promise((resolve) =>
1024
+ setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
1025
+ )
1026
+ }
1027
+ }
1028
+
1029
+ // Variables to store the waitForReceipt functions
1030
+ // DEPRECATED: These are no longer used - we now use the unified poller
1031
+ // let originMetaTxnReceiptPromise:
1032
+ // | ((abortSignal?: AbortSignal) => Promise<MetaTxnReceipt | null>)
1033
+ // | null = null
1034
+ // let destinationMetaTxnReceiptPromise:
1035
+ // | ((abortSignal?: AbortSignal) => Promise<MetaTxnReceipt | null>)
1036
+ // | ((abortSignal?: AbortSignal) => Promise<TransactionReceipt | null>)
1037
+ // | null = null
1038
+
1039
+ // Unified polling via shared poller instead of separate loops
1040
+ let unifiedPollerAbortController: AbortController | null = null
1041
+ let unifiedPollerPromise: Promise<IntentReceipt | null> | null = null
1042
+
1043
+ // Helper function to start unified polling for both origin and destination transactions
1044
+ const startUnifiedPolling = () => {
1045
+ if (!unifiedPollerAbortController) {
1046
+ logger.console.log(
1047
+ "[trails-sdk] Starting unified polling for intent",
1048
+ {
1049
+ intentId: intent.intentId,
1050
+ },
1051
+ )
1052
+
1053
+ unifiedPollerAbortController = new AbortController()
1054
+ unifiedPollerPromise = pollIntentReceipt({
1055
+ intentId: intent.intentId,
1056
+ trailsClient,
1057
+ abortSignal: unifiedPollerAbortController.signal,
1058
+ callbacks: {
1059
+ onDepositTransactionFound: async (txHash) => {
1060
+ logger.console.log(
1061
+ "[trails-sdk] Unified poller discovered deposit transaction",
1062
+ {
1063
+ txHash,
1064
+ },
1065
+ )
1066
+
1067
+ // For gasless flows, the deposit transaction is the user's initial deposit
1068
+ // Update transactionStates[0] with the deposit transaction receipt
1069
+ if (transactionStates[0]) {
1070
+ try {
1071
+ const depositTxReceipt =
1072
+ await publicClient.getTransactionReceipt({
1073
+ hash: txHash as `0x${string}`,
1074
+ })
1075
+ logger.console.log(
1076
+ "[trails-sdk] Deposit transaction receipt fetched",
1077
+ {
1078
+ txHash,
1079
+ blockNumber: depositTxReceipt?.blockNumber,
1080
+ status: depositTxReceipt?.status,
1081
+ },
1082
+ )
1083
+
1084
+ transactionStates[0] = getTransactionStateFromReceipt(
1085
+ depositTxReceipt,
1086
+ originChainId,
1087
+ transactionStates[0]?.label,
1088
+ )
1089
+ onTransactionStateChange(transactionStates)
1090
+
1091
+ // Decode events for the deposit transaction
1092
+ try {
1093
+ transactionStates[0].decodedTrailsTokenSweeperEvents =
1094
+ decodeTrailsTokenSweeperEvents(depositTxReceipt)
1095
+ transactionStates[0].decodedGuestModuleEvents =
1096
+ decodeGuestModuleEvents(depositTxReceipt)
1097
+ transactionStates[0].refunded =
1098
+ transactionStates[0].decodedTrailsTokenSweeperEvents.findIndex(
1099
+ (event) =>
1100
+ event.type === "Refund" ||
1101
+ event.type === "RefundAndSweep",
1102
+ ) !== -1
1103
+ logger.console.log(
1104
+ "[trails-sdk] Deposit transaction events decoded",
1105
+ {
1106
+ chainId: originChainId,
1107
+ callFailed: (
1108
+ transactionStates[0].decodedGuestModuleEvents ||
1109
+ []
1110
+ ).filter((e: any) => e?.type === "CallFailed")
1111
+ .length,
1112
+ sweeperEvents: (
1113
+ transactionStates[0]
1114
+ .decodedTrailsTokenSweeperEvents || []
1115
+ ).length,
1116
+ refunded: transactionStates[0].refunded,
1117
+ },
1118
+ )
1119
+
1120
+ const hasCallFailed = (
1121
+ transactionStates[0].decodedGuestModuleEvents || []
1122
+ ).some((e: any) => e?.type === "CallFailed")
1123
+
1124
+ if (transactionStates[0].refunded || hasCallFailed) {
1125
+ const errorMessage = transactionStates[0].refunded
1126
+ ? "Deposit transaction was refunded"
1127
+ : "Deposit transaction call failed"
1128
+
1129
+ logger.console.error(
1130
+ "[trails-sdk] Deposit transaction failed",
1131
+ {
1132
+ refunded: transactionStates[0].refunded,
1133
+ callFailed: hasCallFailed,
1134
+ },
1135
+ )
1136
+
1137
+ if (checkoutOnHandlers?.triggerCheckoutError) {
1138
+ checkoutOnHandlers.triggerCheckoutError(
1139
+ errorMessage,
1140
+ )
1141
+ }
1142
+ }
1143
+
1144
+ onTransactionStateChange(transactionStates)
1145
+ } catch (error) {
1146
+ logger.console.error(
1147
+ "Error decoding deposit tx events",
1148
+ error,
1149
+ )
1150
+ }
1151
+ } catch (error) {
1152
+ logger.console.error(
1153
+ "[trails-sdk] Error fetching deposit transaction receipt:",
1154
+ error,
1155
+ )
1156
+ }
1157
+ }
1158
+ },
1159
+ onOriginTransactionFound: async (txHash) => {
1160
+ logger.console.log(
1161
+ "[trails-sdk] Unified poller discovered origin transaction",
1162
+ {
1163
+ txHash,
1164
+ },
1165
+ )
1166
+
1167
+ // Store the transaction hash
1168
+ originMetaTxnReceipt = { txnHash: txHash } as MetaTxnReceipt
1169
+
1170
+ const originTxnReceipt =
1171
+ await publicClient.getTransactionReceipt({
1172
+ hash: txHash as `0x${string}`,
1173
+ })
1174
+ logger.console.log(
1175
+ "[trails-sdk] Origin transaction receipt fetched",
1176
+ {
1177
+ txHash,
1178
+ blockNumber: originTxnReceipt?.blockNumber,
1179
+ status: originTxnReceipt?.status,
1180
+ },
1181
+ )
1182
+
1183
+ if (transactionStates[1]) {
1184
+ transactionStates[1] = getTransactionStateFromReceipt(
1185
+ originTxnReceipt,
1186
+ originChainId,
1187
+ transactionStates[1]?.label,
1188
+ )
1189
+ onTransactionStateChange(transactionStates)
1190
+ }
1191
+
1192
+ // Decode events for the origin transaction
1193
+ try {
1194
+ transactionStates[1]!.decodedTrailsTokenSweeperEvents =
1195
+ decodeTrailsTokenSweeperEvents(originTxnReceipt)
1196
+ transactionStates[1]!.decodedGuestModuleEvents =
1197
+ decodeGuestModuleEvents(originTxnReceipt)
1198
+ transactionStates[1]!.refunded =
1199
+ transactionStates[1]!.decodedTrailsTokenSweeperEvents.findIndex(
1200
+ (event) =>
1201
+ event.type === "Refund" ||
1202
+ event.type === "RefundAndSweep",
1203
+ ) !== -1
1204
+ logger.console.log("[trails-sdk] Origin meta-tx events", {
1205
+ chainId: originChainId,
1206
+ callFailed: (
1207
+ transactionStates[1]!.decodedGuestModuleEvents || []
1208
+ ).filter((e: any) => e?.type === "CallFailed").length,
1209
+ sweeperEvents: (
1210
+ transactionStates[1]!.decodedTrailsTokenSweeperEvents ||
1211
+ []
1212
+ ).length,
1213
+ refunded: transactionStates[1]!.refunded,
1214
+ })
1215
+
1216
+ const hasCallFailed = (
1217
+ transactionStates[1]!.decodedGuestModuleEvents || []
1218
+ ).some((e: any) => e?.type === "CallFailed")
1219
+
1220
+ if (transactionStates[1]!.refunded || hasCallFailed) {
1221
+ const errorMessage = transactionStates[1]!.refunded
1222
+ ? "Origin transaction was refunded"
1223
+ : "Origin transaction call failed"
1224
+
1225
+ logger.console.error(
1226
+ "[trails-sdk] Origin meta-tx failed",
1227
+ {
1228
+ refunded: transactionStates[1]!.refunded,
1229
+ callFailed: hasCallFailed,
1230
+ },
1231
+ )
1232
+
1233
+ if (checkoutOnHandlers?.triggerCheckoutError) {
1234
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
1235
+ }
1236
+ }
1237
+
1238
+ onTransactionStateChange(transactionStates)
1239
+ } catch (error) {
1240
+ logger.console.error(
1241
+ "Error decoding origin tx events",
1242
+ error,
1243
+ )
1244
+ }
1245
+ },
1246
+ onDestinationTransactionFound: async (txHash) => {
1247
+ logger.console.log(
1248
+ "[trails-sdk] Unified poller discovered destination transaction",
1249
+ {
1250
+ txHash,
1251
+ },
1252
+ )
1253
+
1254
+ // Store the transaction hash
1255
+ destinationMetaTxnReceipt = {
1256
+ txnHash: txHash,
1257
+ } as MetaTxnReceipt
1258
+
1259
+ const destinationTxnReceipt =
1260
+ await destinationPublicClient.getTransactionReceipt({
1261
+ hash: txHash as `0x${string}`,
1262
+ })
1263
+ logger.console.log(
1264
+ "[trails-sdk] Destination transaction receipt fetched",
1265
+ {
1266
+ txHash,
1267
+ blockNumber: destinationTxnReceipt?.blockNumber,
1268
+ status: destinationTxnReceipt?.status,
1269
+ },
1270
+ )
1271
+
1272
+ if (transactionStates[2]) {
1273
+ transactionStates[2] = getTransactionStateFromReceipt(
1274
+ destinationTxnReceipt,
1275
+ destinationChainId,
1276
+ transactionStates[2]?.label,
1277
+ )
1278
+ onTransactionStateChange(transactionStates)
1279
+ }
1280
+
1281
+ // Decode events for the destination transaction
1282
+ try {
1283
+ transactionStates[2]!.decodedTrailsTokenSweeperEvents =
1284
+ decodeTrailsTokenSweeperEvents(destinationTxnReceipt)
1285
+ transactionStates[2]!.decodedGuestModuleEvents =
1286
+ decodeGuestModuleEvents(destinationTxnReceipt)
1287
+ transactionStates[2]!.refunded =
1288
+ transactionStates[2]!.decodedTrailsTokenSweeperEvents.findIndex(
1289
+ (event) =>
1290
+ event.type === "Refund" ||
1291
+ event.type === "RefundAndSweep",
1292
+ ) !== -1 ||
1293
+ (transactionStates[2]!.decodedTrailsTokenSweeperEvents.findIndex(
1294
+ (event) => event.type === "Sweep",
1295
+ ) !== -1 &&
1296
+ transactionStates[2]!.decodedGuestModuleEvents.findIndex(
1297
+ (event) => event.type === "CallFailed",
1298
+ ) !== -1)
1299
+ logger.console.log(
1300
+ "[trails-sdk] Destination meta-tx events",
1301
+ {
1302
+ chainId: destinationChainId,
1303
+ callFailed: (
1304
+ transactionStates[2]!.decodedGuestModuleEvents || []
1305
+ ).filter((e: any) => e?.type === "CallFailed").length,
1306
+ sweeperEvents: (
1307
+ transactionStates[2]!
1308
+ .decodedTrailsTokenSweeperEvents || []
1309
+ ).length,
1310
+ refunded: transactionStates[2]!.refunded,
1311
+ },
1312
+ )
1313
+
1314
+ const hasCallFailed = (
1315
+ transactionStates[2]!.decodedGuestModuleEvents || []
1316
+ ).some((e: any) => e?.type === "CallFailed")
1317
+
1318
+ if (transactionStates[2]!.refunded || hasCallFailed) {
1319
+ const errorMessage = transactionStates[2]!.refunded
1320
+ ? "Destination transaction was refunded"
1321
+ : "Destination transaction call failed"
1322
+
1323
+ logger.console.error(
1324
+ "[trails-sdk] Destination meta-tx failed",
1325
+ {
1326
+ refunded: transactionStates[2]!.refunded,
1327
+ callFailed: hasCallFailed,
1328
+ },
1329
+ )
1330
+
1331
+ if (checkoutOnHandlers?.triggerCheckoutError) {
1332
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
1333
+ }
1334
+ }
1335
+
1336
+ onTransactionStateChange(transactionStates)
1337
+ } catch (error) {
1338
+ logger.console.error(
1339
+ "Error decoding destination tx events",
1340
+ error,
1341
+ )
1342
+ }
1343
+ },
1344
+ },
1345
+ }).then(
1346
+ () => {
1347
+ logger.console.log("[trails-sdk] Unified poller completed")
1348
+ return null
1349
+ },
1350
+ (error) => {
1351
+ logger.console.error("[trails-sdk] Unified poller error", error)
1352
+ return null
1353
+ },
1354
+ )
1355
+ }
1356
+ }
1357
+
1358
+ // First phase: Send meta transactions and queue CCTP
1359
+ const originSendMetaTxnPromise = async () => {
1360
+ logger.console.log("[trails-sdk] Starting originSendMetaTxnPromise", {
1361
+ hasMetaTxn: !!intent.metaTxns[0],
1362
+ hasPrecondition: !!intent.preconditions[0],
1363
+ metaTxnId: intent.metaTxns[0]?.id,
1364
+ chainId: intent.metaTxns[0]?.chainId,
1365
+ })
1366
+
1367
+ if (intent.metaTxns[0] && intent.preconditions[0]) {
1368
+ const metaTxnId = intent.metaTxns[0].id
1369
+ const feeQuote = undefined
1370
+ logger.console.log("[trails-sdk] Processing origin meta txn", {
1371
+ metaTxnId,
1372
+ feeQuote,
1373
+ hasFeeQuote: !!feeQuote,
1374
+ })
1375
+
1376
+ logger.console.log(
1377
+ "[trails-sdk] NOTE: Client-side meta transaction submission deprecated",
1378
+ {
1379
+ metaTxnId,
1380
+ chainId: intent.metaTxns[0].chainId,
1381
+ walletAddress: intent.metaTxns[0].walletAddress,
1382
+ contract: intent.metaTxns[0].contract,
1383
+ message:
1384
+ "Transaction submission now handled by backend via executeIntent. Monitoring via WaitIntentReceipt API.",
1385
+ },
1386
+ )
1387
+
1388
+ logger.console.log(
1389
+ "[trails-sdk] Origin meta transaction sent successfully",
1390
+ {
1391
+ metaTxnId,
1392
+ },
1393
+ )
1394
+
1395
+ // Note: Unified polling will be started after deposit transaction is submitted
1396
+ } else {
1397
+ logger.console.warn(
1398
+ "[trails-sdk] Skipping origin sendMetaTxn - missing metaTxn or precondition",
1399
+ {
1400
+ hasMetaTxn: !!intent.metaTxns[0],
1401
+ hasPrecondition: !!intent.preconditions[0],
1402
+ },
1403
+ )
1404
+ }
1405
+ }
1406
+
1407
+ const destinationSendMetaTxnPromise = async () => {
1408
+ logger.console.log(
1409
+ "[trails-sdk] Starting destinationSendMetaTxnPromise",
1410
+ {
1411
+ quoteProvider: intent.quote.quoteProvider,
1412
+ hasQuoteProviderRequestId: !!intent.quote.quoteProviderRequestId,
1413
+ hasPrecondition1: !!intent.preconditions[1],
1414
+ hasMetaTxn1: !!intent.metaTxns[1],
1415
+ },
1416
+ )
1417
+
1418
+ // Destination meta transactions are handled by the unified poller
1419
+ // which will be started after the deposit transaction is submitted
1420
+ logger.console.log(
1421
+ "[trails-sdk] Destination meta transaction monitoring will be handled by unified poller",
1422
+ )
1423
+ }
1424
+
1425
+ let queueCctpPromise: (() => Promise<void>) | null = null
1426
+
1427
+ const isCctp = intent.quote.quoteProvider === "cctp"
1428
+ if (isCctp) {
1429
+ queueCctpPromise = async () => {
1430
+ while (true) {
1431
+ // Check if we should abort
1432
+ if (abortSignal?.aborted) {
1433
+ logger.console.log(
1434
+ "[trails-sdk] Aborting CCTP queue due to abort signal",
1435
+ )
1436
+ return
1437
+ }
1438
+
1439
+ const originMetaTxnHash = (
1440
+ originMetaTxnReceipt as MetaTxnReceipt | null
1441
+ )?.txnHash
1442
+ if (originMetaTxnHash) {
1443
+ await queueCCTPTransfer({
1444
+ trailsClient,
1445
+ sourceTxHash: originMetaTxnHash,
1446
+ sourceChainId: originChainId,
1447
+ destinationChainId: destinationChainId,
1448
+ })
1449
+ break
1450
+ }
1451
+ await new Promise((resolve) =>
1452
+ setTimeout(resolve, POLLING_INTERVALS.CCTP_QUEUE),
1453
+ )
1454
+ }
1455
+ }
1456
+ } else {
1457
+ queueCctpPromise = () => Promise.resolve()
1458
+ }
1459
+
1460
+ // Only start polling for external deposits if needed:
1461
+ // - QR code or exchange funding (external deposits)
1462
+ // - Gasless flow (relayer-executed deposits)
1463
+ const needsDepositPolling =
1464
+ fundMethod === "qr-code" ||
1465
+ fundMethod === "exchange" ||
1466
+ effectiveGasless === true
1467
+
1468
+ if (needsDepositPolling) {
1469
+ logger.console.log(
1470
+ "[trails-sdk] Starting deposit polling for fundMethod:",
1471
+ fundMethod,
1472
+ "effectiveGasless:",
1473
+ effectiveGasless,
1474
+ )
1475
+ checkForDepositTx().catch((error) => {
1476
+ logger.console.error("Error checking for deposit tx", error)
1477
+ })
1478
+ } else {
1479
+ logger.console.log(
1480
+ "[trails-sdk] Skipping deposit polling for wallet funding with gas fees",
1481
+ )
1482
+ }
1483
+
1484
+ // Phase 1: Send meta transactions and queue CCTP
1485
+ logger.console.log(
1486
+ "[trails-sdk] Starting Phase 1: Sending meta transactions and queuing CCTP",
1487
+ )
1488
+
1489
+ await Promise.all([
1490
+ originSendMetaTxnPromise(),
1491
+ destinationSendMetaTxnPromise(),
1492
+ ])
1493
+
1494
+ logger.console.log("[trails-sdk] Phase 1 completed successfully")
1495
+
1496
+ // Phase 2: Wait for receipts and execute deposit
1497
+ logger.console.log(
1498
+ "[trails-sdk] Starting Phase 2: Waiting for receipts and executing deposit",
1499
+ )
1500
+
1501
+ const waitForOriginMetaTxnReceiptPromise = async () => {
1502
+ logger.console.log(
1503
+ "[trails-sdk] Waiting for origin meta transaction receipt via unified poller",
1504
+ )
1505
+ // The unified poller handles origin transaction discovery and decoding via callbacks
1506
+ // We just need to wait for it to complete
1507
+ if (unifiedPollerPromise) {
1508
+ try {
1509
+ await unifiedPollerPromise
1510
+ } catch (error) {
1511
+ logger.console.error(
1512
+ "[trails-sdk] Error waiting for unified poller:",
1513
+ error,
1514
+ )
1515
+ }
1516
+ }
1517
+ }
1518
+
1519
+ const waitForDestinationMetaTxnReceiptPromise = async () => {
1520
+ logger.console.log(
1521
+ "[trails-sdk] Waiting for destination meta transaction receipt via unified poller",
1522
+ )
1523
+ // The unified poller handles destination transaction discovery and decoding via callbacks
1524
+ // We just need to wait for it to complete
1525
+ if (unifiedPollerPromise) {
1526
+ try {
1527
+ await unifiedPollerPromise
1528
+ } catch (error) {
1529
+ logger.console.error(
1530
+ "[trails-sdk] Error waiting for unified poller:",
1531
+ error,
1532
+ )
1533
+ }
1534
+ }
1535
+ }
1536
+
1537
+ logger.console.log(
1538
+ "[trails-sdk] Executing Phase 2: First executing deposit",
1539
+ )
1540
+ logger.console.log(
1541
+ "[trails-sdk] About to call depositPromise - fundMethod is:",
1542
+ fundMethod,
1543
+ )
1544
+
1545
+ // Execute deposit first to get the transaction receipt and call executeIntent
1546
+ await depositPromise()
1547
+
1548
+ // Start unified polling after deposit transaction is submitted and executeIntent is called
1549
+ // This ensures the backend has received the executeIntent call before we start polling waitIntentReceipt
1550
+ logger.console.log(
1551
+ "[trails-sdk] Starting unified polling after executeIntent has been called",
1552
+ )
1553
+ startUnifiedPolling()
1554
+
1555
+ // Now wait for all the receipts and CCTP processing
1556
+ logger.console.log(
1557
+ "[trails-sdk] Waiting for origin/destination receipts and CCTP processing",
1558
+ )
1559
+ await Promise.all([
1560
+ waitForOriginMetaTxnReceiptPromise(),
1561
+ waitForDestinationMetaTxnReceiptPromise(),
1562
+ queueCctpPromise(),
1563
+ ])
1564
+ logger.console.log("[trails-sdk] Phase 2 completed successfully")
1565
+
1566
+ // Track payment completion for different chain and different token
1567
+ // In gasless mode, we don't have depositUserTxnReceipt since it's handled by backend
1568
+ // For same-chain, we don't have a destination meta transaction, only origin
1569
+ // For cross-chain, we need both origin and destination meta transactions
1570
+ const isDestinationCrossChain = originChainId !== destinationChainId
1571
+ const isSameChain = originChainId === destinationChainId
1572
+ const hasRequiredReceipts =
1573
+ (depositUserTxnReceipt || effectiveGasless) &&
1574
+ (isDestinationCrossChain
1575
+ ? destinationMetaTxnReceipt
1576
+ : isSameChain
1577
+ ? originMetaTxnReceipt
1578
+ : destinationMetaTxnReceipt)
1579
+
1580
+ if (hasRequiredReceipts) {
1581
+ // Check if any transaction failed or was refunded
1582
+ const hasAnyCallFailed = transactionStates.some((tx) =>
1583
+ tx?.decodedGuestModuleEvents?.some(
1584
+ (e: any) => e?.type === "CallFailed",
1585
+ ),
1586
+ )
1587
+ const hasAnyRefunded = transactionStates.some((tx) => tx?.refunded)
1588
+ const txStatus =
1589
+ !hasAnyCallFailed && !hasAnyRefunded ? "success" : "fail"
1590
+
1591
+ // Always track payment completion regardless of success/failure
1592
+ trackPaymentCompleted({
1593
+ userAddress: account.address,
1594
+ originIntentAddress,
1595
+ originTxHash: depositUserTxnReceipt
1596
+ ? (depositUserTxnReceipt as TransactionReceipt).transactionHash
1597
+ : undefined,
1598
+ destinationTxHash: (
1599
+ destinationMetaTxnReceipt as unknown as MetaTxnReceipt
1600
+ )?.txnHash,
1601
+ originChainId,
1602
+ destinationChainId,
1603
+ mode,
1604
+ fundMethod,
1605
+ originTokenSymbol,
1606
+ originTokenAddress,
1607
+ destinationTokenAddress,
1608
+ destinationTokenSymbol,
1609
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
1610
+ destinationTokenAmountUsd:
1611
+ effectiveDestinationTokenAmountUsd?.toString(),
1612
+ })
1613
+
1614
+ // Call onCheckoutComplete callback with transaction status
1615
+ if (checkoutOnHandlers?.triggerCheckoutComplete) {
1616
+ checkoutOnHandlers.triggerCheckoutComplete(
1617
+ txStatus,
1618
+ account.address,
1619
+ )
1620
+ }
1621
+ } else {
1622
+ if (
1623
+ transactionStates[1] &&
1624
+ transactionStates[1]?.transactionHash === "" &&
1625
+ transactionStates[1]?.state === "pending"
1626
+ ) {
1627
+ transactionStates[1].state = "aborted"
1628
+ onTransactionStateChange(transactionStates)
1629
+ }
1630
+ if (
1631
+ transactionStates[2] &&
1632
+ transactionStates[2]?.transactionHash === "" &&
1633
+ transactionStates[2]?.state === "pending"
1634
+ ) {
1635
+ transactionStates[2].state = "aborted"
1636
+ onTransactionStateChange(transactionStates)
1637
+ }
1638
+
1639
+ // Track payment error if transactions didn't complete successfully
1640
+ trackPaymentError({
1641
+ error:
1642
+ "Payment transactions possibly did not complete successfully. Was not able to receive both origin and destination meta transaction receipts. May be an API error.",
1643
+ userAddress: account.address,
1644
+ originIntentAddress,
1645
+ mode,
1646
+ fundMethod,
1647
+ originChainId,
1648
+ destinationChainId,
1649
+ originTokenSymbol,
1650
+ originTokenAddress,
1651
+ destinationTokenAddress,
1652
+ destinationTokenSymbol,
1653
+ })
1654
+
1655
+ // Call onCheckoutError callback if provided
1656
+ if (checkoutOnHandlers?.triggerCheckoutError) {
1657
+ checkoutOnHandlers.triggerCheckoutError(
1658
+ "Payment transactions did not complete successfully",
1659
+ )
1660
+ }
1661
+ }
1662
+
1663
+ return {
1664
+ depositUserTxnReceipt,
1665
+ originMetaTxnReceipt,
1666
+ destinationMetaTxnReceipt,
1667
+ totalCompletionSeconds: await getTxTimeDiff(
1668
+ transactionStates[0],
1669
+ transactionStates[2],
1670
+ ),
1671
+ }
1672
+ } catch (error) {
1673
+ const errorMessage =
1674
+ error instanceof Error
1675
+ ? error.message
1676
+ : "Unknown error occurred during transaction"
1677
+ logger.console.error(
1678
+ "[trails-sdk] Error in sendHandlerForDifferentChainDifferentToken:",
1679
+ error,
1680
+ )
1681
+
1682
+ // Track payment error
1683
+ trackPaymentError({
1684
+ error: errorMessage,
1685
+ userAddress: account.address,
1686
+ originIntentAddress,
1687
+ mode,
1688
+ fundMethod,
1689
+ originChainId,
1690
+ destinationChainId,
1691
+ originTokenSymbol,
1692
+ originTokenAddress,
1693
+ destinationTokenAddress,
1694
+ destinationTokenSymbol,
1695
+ })
1696
+
1697
+ // Call onCheckoutError callback if provided
1698
+ if (checkoutOnHandlers?.triggerCheckoutError) {
1699
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
1700
+ }
1701
+
1702
+ // Re-throw the error so caller can handle if needed
1703
+ throw error
1704
+ }
1705
+ },
1706
+ }
1707
+ }