0xtrails 0.2.4 → 0.2.6

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 (212) hide show
  1. package/dist/aave.d.ts +8 -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-BlV1Mry3.js → ccip-Xjh9d1gb.js} +7 -7
  6. package/dist/config.d.ts +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/constants.d.ts +3 -0
  9. package/dist/constants.d.ts.map +1 -1
  10. package/dist/error.d.ts +1 -0
  11. package/dist/error.d.ts.map +1 -1
  12. package/dist/estimate.d.ts +52 -0
  13. package/dist/estimate.d.ts.map +1 -1
  14. package/dist/fees.d.ts +19 -0
  15. package/dist/fees.d.ts.map +1 -0
  16. package/dist/{index-BNWCIGfQ.js → index-BnhdZ8Ho.js} +76406 -75798
  17. package/dist/index.js +726 -520
  18. package/dist/intents.d.ts +40 -0
  19. package/dist/intents.d.ts.map +1 -1
  20. package/dist/metaTxnMonitor.d.ts +3 -3
  21. package/dist/metaTxnMonitor.d.ts.map +1 -1
  22. package/dist/metaTxns.d.ts +3 -3
  23. package/dist/metaTxns.d.ts.map +1 -1
  24. package/dist/morpho.d.ts +8 -0
  25. package/dist/morpho.d.ts.map +1 -1
  26. package/dist/prepareSend.d.ts +19 -75
  27. package/dist/prepareSend.d.ts.map +1 -1
  28. package/dist/queryParams.d.ts.map +1 -1
  29. package/dist/relayer.d.ts +6 -6
  30. package/dist/relayer.d.ts.map +1 -1
  31. package/dist/sequenceWallet.d.ts +2 -2
  32. package/dist/sequenceWallet.d.ts.map +1 -1
  33. package/dist/tokens.d.ts.map +1 -1
  34. package/dist/transactions.d.ts +4 -2
  35. package/dist/transactions.d.ts.map +1 -1
  36. package/dist/wallets.d.ts.map +1 -1
  37. package/dist/widget/components/AccountActionsDropdown.d.ts.map +1 -1
  38. package/dist/widget/components/AccountIntentTransactionHistoryButton.d.ts +4 -0
  39. package/dist/widget/components/AccountIntentTransactionHistoryButton.d.ts.map +1 -0
  40. package/dist/widget/components/AccountSettings.d.ts.map +1 -1
  41. package/dist/widget/components/ChainFilterDropdown.d.ts.map +1 -1
  42. package/dist/widget/components/ClassicSwap.d.ts +4 -2
  43. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  44. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  45. package/dist/widget/components/ConnectedWallets.d.ts +4 -0
  46. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -1
  47. package/dist/widget/components/DynamicInputStyles.d.ts +18 -0
  48. package/dist/widget/components/DynamicInputStyles.d.ts.map +1 -0
  49. package/dist/widget/components/Earn.d.ts +2 -2
  50. package/dist/widget/components/Earn.d.ts.map +1 -1
  51. package/dist/widget/components/ErrorAnimationIcon.d.ts +2 -0
  52. package/dist/widget/components/ErrorAnimationIcon.d.ts.map +1 -0
  53. package/dist/widget/components/FeeBreakdown.d.ts +9 -0
  54. package/dist/widget/components/FeeBreakdown.d.ts.map +1 -0
  55. package/dist/widget/components/Fund.d.ts +2 -2
  56. package/dist/widget/components/Fund.d.ts.map +1 -1
  57. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  58. package/dist/widget/components/{FundSendForm.d.ts → FundSwap.d.ts} +13 -7
  59. package/dist/widget/components/FundSwap.d.ts.map +1 -0
  60. package/dist/widget/components/FundingMethodSelectorButton.d.ts +4 -0
  61. package/dist/widget/components/FundingMethodSelectorButton.d.ts.map +1 -0
  62. package/dist/widget/components/Identicon.d.ts.map +1 -1
  63. package/dist/widget/components/MeshConnectExchanges.d.ts +0 -3
  64. package/dist/widget/components/MeshConnectExchanges.d.ts.map +1 -1
  65. package/dist/widget/components/Modal.d.ts.map +1 -1
  66. package/dist/widget/components/Pay.d.ts +2 -2
  67. package/dist/widget/components/Pay.d.ts.map +1 -1
  68. package/dist/widget/components/PercentageMaxButtons.d.ts +12 -0
  69. package/dist/widget/components/PercentageMaxButtons.d.ts.map +1 -0
  70. package/dist/widget/components/{PaySendForm.d.ts → PoolDeposit.d.ts} +14 -36
  71. package/dist/widget/components/PoolDeposit.d.ts.map +1 -0
  72. package/dist/widget/components/{SimpleSwap.d.ts → PoolWithdraw.d.ts} +19 -10
  73. package/dist/widget/components/PoolWithdraw.d.ts.map +1 -0
  74. package/dist/widget/components/QuoteDetails.d.ts +1 -0
  75. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  76. package/dist/widget/components/Receipt.d.ts.map +1 -1
  77. package/dist/widget/components/Receive.d.ts.map +1 -1
  78. package/dist/widget/components/RecipientSelectorButton.d.ts +4 -0
  79. package/dist/widget/components/RecipientSelectorButton.d.ts.map +1 -0
  80. package/dist/widget/components/Recipients.d.ts.map +1 -1
  81. package/dist/widget/components/RequiredPropsError.d.ts +8 -0
  82. package/dist/widget/components/RequiredPropsError.d.ts.map +1 -0
  83. package/dist/widget/components/ScreenHeader.d.ts.map +1 -1
  84. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  85. package/dist/widget/components/Swap.d.ts +3 -2
  86. package/dist/widget/components/Swap.d.ts.map +1 -1
  87. package/dist/widget/components/SwapSettings.d.ts.map +1 -1
  88. package/dist/widget/components/ThemeProvider.d.ts.map +1 -1
  89. package/dist/widget/components/TokenDisplayNonSelectable.d.ts +11 -0
  90. package/dist/widget/components/TokenDisplayNonSelectable.d.ts.map +1 -0
  91. package/dist/widget/components/TokenImage.d.ts +1 -0
  92. package/dist/widget/components/TokenImage.d.ts.map +1 -1
  93. package/dist/widget/components/TokenList.d.ts.map +1 -1
  94. package/dist/widget/components/TokenSelector.d.ts.map +1 -1
  95. package/dist/widget/components/TokenSelectorButton.d.ts +16 -0
  96. package/dist/widget/components/TokenSelectorButton.d.ts.map +1 -0
  97. package/dist/widget/components/Tooltip.d.ts +9 -0
  98. package/dist/widget/components/Tooltip.d.ts.map +1 -0
  99. package/dist/widget/components/UserPreferences.d.ts.map +1 -1
  100. package/dist/widget/components/WaasFeeOptions.d.ts +9 -0
  101. package/dist/widget/components/WaasFeeOptions.d.ts.map +1 -0
  102. package/dist/widget/components/WalletConfirmation.d.ts.map +1 -1
  103. package/dist/widget/components/WalletConnect.d.ts.map +1 -1
  104. package/dist/widget/components/WalletList.d.ts.map +1 -1
  105. package/dist/widget/css/compiled.css +2 -0
  106. package/dist/widget/css/index.css +554 -0
  107. package/dist/widget/hooks/useBack.d.ts +1 -0
  108. package/dist/widget/hooks/useBack.d.ts.map +1 -1
  109. package/dist/widget/hooks/useCheckout.d.ts +1 -1
  110. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  111. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  112. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  113. package/dist/widget/hooks/useDefaultTokenSelection.d.ts +3 -3
  114. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -1
  115. package/dist/widget/hooks/usePayMessage.d.ts.map +1 -1
  116. package/dist/widget/hooks/useQuote.d.ts +83 -0
  117. package/dist/widget/hooks/useQuote.d.ts.map +1 -0
  118. package/dist/widget/hooks/useSelectedFundMethod.d.ts +12 -0
  119. package/dist/widget/hooks/useSelectedFundMethod.d.ts.map +1 -0
  120. package/dist/widget/hooks/useSelectedRecipient.d.ts.map +1 -1
  121. package/dist/widget/hooks/useSendForm.d.ts +2 -2
  122. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  123. package/dist/widget/index.js +2 -2
  124. package/dist/widget/widget.d.ts +9 -4
  125. package/dist/widget/widget.d.ts.map +1 -1
  126. package/package.json +18 -12
  127. package/src/aave.ts +32 -0
  128. package/src/abortController.ts +35 -0
  129. package/src/config.ts +12 -4
  130. package/src/constants.ts +5 -0
  131. package/src/error.ts +19 -1
  132. package/src/estimate.ts +416 -5
  133. package/src/fees.ts +199 -0
  134. package/src/intents.ts +161 -11
  135. package/src/metaTxnMonitor.ts +3 -3
  136. package/src/metaTxns.ts +3 -5
  137. package/src/morpho.ts +32 -0
  138. package/src/prepareSend.ts +714 -550
  139. package/src/queryParams.ts +2 -1
  140. package/src/relayer.ts +11 -11
  141. package/src/sequenceWallet.ts +2 -2
  142. package/src/tokens.ts +7 -1
  143. package/src/trails.ts +3 -3
  144. package/src/transactions.ts +62 -18
  145. package/src/wallets.ts +8 -0
  146. package/src/widget/compiled.css +2 -2
  147. package/src/widget/components/AccountActionsDropdown.tsx +3 -13
  148. package/src/widget/components/AccountIntentTransactionHistoryButton.tsx +22 -0
  149. package/src/widget/components/AccountSettings.tsx +48 -54
  150. package/src/widget/components/ChainFilterDropdown.tsx +24 -3
  151. package/src/widget/components/ClassicSwap.tsx +131 -213
  152. package/src/widget/components/ConnectWallet.tsx +8 -38
  153. package/src/widget/components/ConnectedWallets.tsx +132 -77
  154. package/src/widget/components/DynamicInputStyles.tsx +76 -0
  155. package/src/widget/components/Earn.tsx +82 -593
  156. package/src/widget/components/ErrorAnimationIcon.tsx +130 -0
  157. package/src/widget/components/FeeBreakdown.tsx +155 -0
  158. package/src/widget/components/Fund.tsx +41 -108
  159. package/src/widget/components/FundMethods.tsx +82 -159
  160. package/src/widget/components/FundSwap.tsx +52 -0
  161. package/src/widget/components/FundingMethodSelectorButton.tsx +70 -0
  162. package/src/widget/components/Identicon.tsx +164 -95
  163. package/src/widget/components/MeshConnectExchanges.tsx +2 -15
  164. package/src/widget/components/Modal.tsx +0 -8
  165. package/src/widget/components/Pay.tsx +214 -237
  166. package/src/widget/components/PercentageMaxButtons.tsx +77 -0
  167. package/src/widget/components/PoolDeposit.tsx +569 -0
  168. package/src/widget/components/PoolWithdraw.tsx +884 -0
  169. package/src/widget/components/PriceImpactWarning.tsx +1 -1
  170. package/src/widget/components/QuoteDetails.tsx +43 -12
  171. package/src/widget/components/Receipt.tsx +16 -2
  172. package/src/widget/components/Receive.tsx +0 -2
  173. package/src/widget/components/RecipientSelectorButton.tsx +44 -0
  174. package/src/widget/components/Recipients.tsx +63 -157
  175. package/src/widget/components/RequiredPropsError.tsx +33 -0
  176. package/src/widget/components/ScreenHeader.tsx +62 -34
  177. package/src/widget/components/SlippageToleranceSettings.tsx +2 -1
  178. package/src/widget/components/Swap.tsx +4 -45
  179. package/src/widget/components/SwapSettings.tsx +2 -14
  180. package/src/widget/components/ThemeProvider.tsx +2 -1
  181. package/src/widget/components/TokenDisplayNonSelectable.tsx +40 -0
  182. package/src/widget/components/TokenImage.tsx +22 -5
  183. package/src/widget/components/TokenList.tsx +0 -1
  184. package/src/widget/components/TokenSelector.tsx +63 -53
  185. package/src/widget/components/TokenSelectorButton.tsx +98 -0
  186. package/src/widget/components/Tooltip.tsx +51 -0
  187. package/src/widget/components/TransferPendingVertical.tsx +1 -1
  188. package/src/widget/components/UserPreferences.tsx +6 -24
  189. package/src/widget/components/WaasFeeOptions.tsx +450 -0
  190. package/src/widget/components/WalletConfirmation.tsx +76 -14
  191. package/src/widget/components/WalletConnect.tsx +93 -29
  192. package/src/widget/components/WalletList.tsx +4 -2
  193. package/src/widget/hooks/useBack.tsx +2 -0
  194. package/src/widget/hooks/useCheckout.ts +36 -20
  195. package/src/widget/hooks/useCurrentScreen.tsx +1 -0
  196. package/src/widget/hooks/useDefaultTokenSelection.tsx +104 -28
  197. package/src/widget/hooks/usePayMessage.tsx +86 -11
  198. package/src/widget/hooks/useQuote.ts +413 -0
  199. package/src/widget/hooks/useSelectedFundMethod.tsx +41 -0
  200. package/src/widget/hooks/useSelectedRecipient.tsx +10 -0
  201. package/src/widget/hooks/useSendForm.ts +32 -6
  202. package/src/widget/index.css +27 -0
  203. package/src/widget/widget.tsx +326 -283
  204. package/dist/widget/components/FundSendForm.d.ts.map +0 -1
  205. package/dist/widget/components/PaySendForm.d.ts.map +0 -1
  206. package/dist/widget/components/SimpleSwap.d.ts.map +0 -1
  207. package/dist/widget/hooks/useSwapSettings.d.ts +0 -16
  208. package/dist/widget/hooks/useSwapSettings.d.ts.map +0 -1
  209. package/src/widget/components/FundSendForm.tsx +0 -903
  210. package/src/widget/components/PaySendForm.tsx +0 -869
  211. package/src/widget/components/SimpleSwap.tsx +0 -983
  212. package/src/widget/hooks/useSwapSettings.tsx +0 -100
@@ -1,10 +1,10 @@
1
1
  import type {
2
2
  GetIntentCallsPayloadParams,
3
- GetIntentCallsPayloadsReturn,
4
3
  IntentPrecondition,
5
4
  } from "@0xsequence/trails-api"
5
+ import type { GetIntentCallsPayloadsReturn } from "./intents.js"
6
6
  import type { TrailsAPIClient } from "@0xsequence/trails-api"
7
- import { useQuery } from "@tanstack/react-query"
7
+ import type { Relayer } from "@0xsequence/relayer"
8
8
  import type {
9
9
  Account,
10
10
  Chain,
@@ -12,6 +12,8 @@ import type {
12
12
  TransactionReceipt,
13
13
  WalletClient,
14
14
  } from "viem"
15
+ import { abortControllerRegistry } from "./abortController.js"
16
+ import { extractTrailsFeeBreakdown, type TrailsFeeBreakdown } from "./fees.js"
15
17
  import {
16
18
  createPublicClient,
17
19
  createWalletClient,
@@ -34,7 +36,6 @@ import {
34
36
  trackRelayerCallStarted,
35
37
  trackTransactionConfirmed,
36
38
  } from "./analytics.js"
37
- import { useAPIClient } from "./apiClient.js"
38
39
  import type { Attestation } from "./cctp.js"
39
40
  import {
40
41
  approveERC20,
@@ -50,19 +51,20 @@ import {
50
51
  import { queueCCTPTransfer } from "./cctpqueue.js"
51
52
  import { getChainInfo, getTestnetChainInfo } from "./chains.js"
52
53
  import { attemptSwitchChain } from "./chainSwitch.js"
53
- import {
54
- getSequenceEnv,
55
- getSlippageTolerance,
56
- type SequenceEnv,
57
- } from "./config.js"
54
+ import { getSlippageTolerance } from "./config.js"
58
55
  import { TRAILS_INTENT_ENTRYPOINT_ADDRESS } from "./constants.js"
59
56
  import {
60
57
  decodeGuestModuleEvents,
61
58
  decodeTrailsTokenSweeperEvents,
62
59
  } from "./decoders.js"
63
60
  import { getERC20TransferData } from "./encoders.js"
64
- import { getFullErrorMessage, InsufficientBalanceError } from "./error.js"
65
- import { estimateGasCostUsd } from "./estimate.js"
61
+ import { InsufficientBalanceError } from "./error.js"
62
+ import {
63
+ estimateGasCost,
64
+ estimateGasCostUsd,
65
+ estimateGasLimit,
66
+ DEFAULT_MIN_GASLIMIT,
67
+ } from "./estimate.js"
66
68
  import { getExplorerUrl } from "./explorer.js"
67
69
  import {
68
70
  getNeedsIntentEntrypointApproval,
@@ -71,8 +73,9 @@ import {
71
73
  signIntent,
72
74
  } from "./gasless.js"
73
75
  import { getIntentEntrypointFeeOptions } from "./intentEntrypoint.js"
74
- import { useIndexerGatewayClient } from "./indexerClient.js"
75
76
  import {
77
+ buildSameChainTransactionParams,
78
+ buildCrossChainDepositParams,
76
79
  commitIntentConfig,
77
80
  getIntentCallsPayloads as getIntentCallsPayloadsFromIntents,
78
81
  sendOriginTransaction,
@@ -84,8 +87,7 @@ import { relayerSendMetaTx } from "./metaTxns.js"
84
87
  import { findFirstPreconditionForChainId } from "./preconditions.js"
85
88
  import { calcAmountUsdPrice, getTokenPrice } from "./prices.js"
86
89
  import { getQueryParam } from "./queryParams.js"
87
- import type { MetaTxnReceipt, RelayerEnv } from "./relayer.js"
88
- import { useRelayers } from "./relayer.js"
90
+ import type { MetaTxnReceipt } from "./relayer.js"
89
91
  import {
90
92
  executeSimpleRelayTransaction,
91
93
  getRelaySDKQuote,
@@ -103,13 +105,8 @@ import {
103
105
  formatAmountDisplay,
104
106
  formatRawAmount,
105
107
  formatUsdAmountDisplay,
106
- getTokenBalancesWithPrices,
107
108
  } from "./tokenBalances.js"
108
- import {
109
- getTokenInfo,
110
- useSupportedTokens,
111
- type SupportedToken,
112
- } from "./tokens.js"
109
+ import { getTokenInfo, type SupportedToken } from "./tokens.js"
113
110
  import {
114
111
  TRAILS_ROUTER_PLACEHOLDER_AMOUNT,
115
112
  wrapCalldataWithTrailsRouterIfNeeded,
@@ -122,16 +119,27 @@ import { getAccountTransactionHistory, getTxTimeDiff } from "./transactions.js"
122
119
  import { requestWithTimeout } from "./utils.js"
123
120
  import type { CheckoutOnHandlers } from "./widget/hooks/useCheckout.js"
124
121
  import { logger } from "./logger.js"
122
+
123
+ // Polling intervals for different operations
124
+ const POLLING_INTERVALS = {
125
+ // Consistent interval for transaction history polling (3 seconds)
126
+ TRANSACTION_HISTORY: 3000,
127
+ // Consistent interval for meta transaction polling (2 seconds)
128
+ META_TRANSACTION: 2000,
129
+ // Consistent interval for CCTP queue polling (5 seconds)
130
+ CCTP_QUEUE: 5000,
131
+ // Consistent interval for failure polling (2 seconds)
132
+ FAILURE_POLLING: 2000,
133
+ } as const
134
+
125
135
  import { getIsCustomCalldata } from "./contractUtils.js"
126
136
  import type { SequenceAPIClient } from "@0xsequence/api"
127
- import { useTrailsClient } from "./trailsClient.js"
128
137
  import { updatePersistentToast } from "./toast.js"
129
138
  import {
130
139
  getDelegatorSmartAccount,
131
140
  getPaymasterGaslessTransaction,
132
141
  sendPaymasterGaslessTransaction,
133
142
  } from "./paymasterSend.js"
134
- import type { RpcRelayer } from "@0xsequence/relayer"
135
143
 
136
144
  export enum TradeType {
137
145
  EXACT_INPUT = "EXACT_INPUT",
@@ -156,8 +164,8 @@ export type PrepareSendOptions = {
156
164
  dryMode: boolean
157
165
  apiClient: SequenceAPIClient
158
166
  trailsClient: TrailsAPIClient
159
- originRelayer: RpcRelayer.RpcRelayer
160
- destinationRelayer: RpcRelayer.RpcRelayer
167
+ originRelayer: Relayer.RpcRelayer
168
+ destinationRelayer: Relayer.RpcRelayer
161
169
  destinationCalldata?: string
162
170
  onTransactionStateChange: (transactionStates: TransactionState[]) => void
163
171
  sourceTokenPriceUsd?: number | null
@@ -174,6 +182,8 @@ export type PrepareSendOptions = {
174
182
  checkoutOnHandlers?: CheckoutOnHandlers
175
183
  refundAddress?: string
176
184
  selectedFeeToken?: any
185
+ walletId?: string
186
+ abortSignal?: AbortSignal
177
187
  }
178
188
 
179
189
  export type PrepareSendFees = {
@@ -218,10 +228,13 @@ export type PrepareSendQuote = {
218
228
  transactionStates: TransactionState[]
219
229
  gasCostUsd: number
220
230
  gasCostUsdDisplay: string
231
+ gasCost: string
232
+ gasCostFormatted: string
221
233
  originTokenRate: string
222
234
  destinationTokenRate: string
223
235
  quoteProvider: QuoteProviderInfo | null
224
236
  noSufficientBalance: boolean
237
+ trailsFeeBreakdown?: TrailsFeeBreakdown | null
225
238
  }
226
239
 
227
240
  export type PrepareSendReturn = {
@@ -353,6 +366,12 @@ function getIntentArgs(
353
366
  export async function prepareSend(
354
367
  options: PrepareSendOptions,
355
368
  ): Promise<PrepareSendReturn> {
369
+ // Abort all existing operations when a new quote is generated
370
+ logger.console.log(
371
+ "[trails-sdk] New quote generated - aborting all existing operations",
372
+ )
373
+ abortControllerRegistry.abortAll()
374
+
356
375
  const {
357
376
  account,
358
377
  originTokenAddress,
@@ -385,6 +404,8 @@ export async function prepareSend(
385
404
  mode,
386
405
  checkoutOnHandlers,
387
406
  selectedFeeToken,
407
+ walletId,
408
+ abortSignal,
388
409
  } = options
389
410
  let { sourceTokenPriceUsd, destinationTokenPriceUsd } = options
390
411
 
@@ -568,18 +589,20 @@ export async function prepareSend(
568
589
  label: isToSameChain && isToSameToken ? "Execute" : "Transfer",
569
590
  })
570
591
 
571
- // swap (+ bridge tx)
572
- transactionStates.push({
573
- transactionHash: "",
574
- explorerUrl: "",
575
- chainId: originChainId,
576
- state: "pending",
577
- label: isToSameToken
578
- ? "Bridge"
579
- : isToSameChain && !isToSameToken
580
- ? "Swap"
581
- : "Swap & Bridge",
582
- })
592
+ // swap (+ bridge tx) - skip for same-chain-same-token as there's no second transaction
593
+ if (!(isToSameChain && isToSameToken)) {
594
+ transactionStates.push({
595
+ transactionHash: "",
596
+ explorerUrl: "",
597
+ chainId: originChainId,
598
+ state: "pending",
599
+ label: isToSameToken
600
+ ? "Bridge"
601
+ : isToSameChain && !isToSameToken
602
+ ? "Swap"
603
+ : "Swap & Bridge",
604
+ })
605
+ }
583
606
 
584
607
  // additional execute tx on same chain if needed
585
608
  if (isToSameChain && hasCustomCalldata && !isToSameToken) {
@@ -652,6 +675,14 @@ export async function prepareSend(
652
675
  originNativeTokenPriceUsd,
653
676
  slippageTolerance,
654
677
  checkoutOnHandlers,
678
+ gasless,
679
+ paymasterUrl,
680
+ selectedFeeToken,
681
+ trailsClient,
682
+ originRelayer,
683
+ mode,
684
+ fundMethod,
685
+ abortSignal,
655
686
  })
656
687
  }
657
688
 
@@ -693,6 +724,8 @@ export async function prepareSend(
693
724
  mode,
694
725
  checkoutOnHandlers,
695
726
  selectedFeeToken,
727
+ walletId,
728
+ abortSignal,
696
729
  })
697
730
  }
698
731
 
@@ -733,6 +766,8 @@ async function sendHandlerForDifferentChainDifferentToken({
733
766
  mode,
734
767
  checkoutOnHandlers,
735
768
  selectedFeeToken,
769
+ walletId,
770
+ abortSignal,
736
771
  }: {
737
772
  mainSignerAddress: string
738
773
  originChainId: number
@@ -753,8 +788,8 @@ async function sendHandlerForDifferentChainDifferentToken({
753
788
  destinationTokenDecimals: number
754
789
  gasless: boolean
755
790
  paymasterUrl?: string
756
- originRelayer: RpcRelayer.RpcRelayer
757
- destinationRelayer: RpcRelayer.RpcRelayer
791
+ originRelayer: Relayer.RpcRelayer
792
+ destinationRelayer: Relayer.RpcRelayer
758
793
  walletClient: WalletClient
759
794
  publicClient: PublicClient
760
795
  chain: Chain
@@ -771,6 +806,8 @@ async function sendHandlerForDifferentChainDifferentToken({
771
806
  mode?: "pay" | "fund" | "earn" | "swap" | "receive"
772
807
  checkoutOnHandlers?: CheckoutOnHandlers
773
808
  selectedFeeToken?: any
809
+ walletId?: string
810
+ abortSignal?: AbortSignal
774
811
  }): Promise<PrepareSendReturn> {
775
812
  const testnet = isTestnetDebugMode()
776
813
  const useCctp = getUseCctp(
@@ -1120,6 +1157,42 @@ async function sendHandlerForDifferentChainDifferentToken({
1120
1157
  }
1121
1158
  }
1122
1159
 
1160
+ // Estimate gas limit for the quote (same logic as in attemptNonGaslessUserDeposit)
1161
+ const originCallParamsForEstimate = buildCrossChainDepositParams({
1162
+ originTokenAddress,
1163
+ originIntentAddress,
1164
+ depositAmount: firstPreconditionMin,
1165
+ fee,
1166
+ originChainId,
1167
+ chain,
1168
+ })
1169
+
1170
+ logger.console.log(
1171
+ "[trails-sdk][gas-estimation] About to estimate gas limit for cross-chain quote with params:",
1172
+ {
1173
+ account: account.address,
1174
+ to: originCallParamsForEstimate.to,
1175
+ data: originCallParamsForEstimate.data,
1176
+ value: originCallParamsForEstimate.value,
1177
+ },
1178
+ )
1179
+
1180
+ const estimatedGasLimitForQuote = await estimateGasLimit(
1181
+ publicClient,
1182
+ {
1183
+ account: account.address,
1184
+ to: originCallParamsForEstimate.to,
1185
+ data: originCallParamsForEstimate.data,
1186
+ value: BigInt(originCallParamsForEstimate.value),
1187
+ },
1188
+ "quote",
1189
+ )
1190
+
1191
+ logger.console.log(
1192
+ "[trails-sdk][gas-estimation] Estimated gas limit for cross-chain quote:",
1193
+ estimatedGasLimitForQuote,
1194
+ )
1195
+
1123
1196
  const quote = await getNormalizedQuoteObject({
1124
1197
  originDepositAddress: originIntentAddress,
1125
1198
  destinationDepositAddress: intent.payloads.destinationIntentAddress,
@@ -1147,6 +1220,8 @@ async function sendHandlerForDifferentChainDifferentToken({
1147
1220
  originNativeTokenPriceUsd,
1148
1221
  quoteProvider: intent.payloads?.quote?.quoteProvider,
1149
1222
  noSufficientBalance,
1223
+ estimatedGasLimit: estimatedGasLimitForQuote,
1224
+ intent,
1150
1225
  })
1151
1226
 
1152
1227
  // Call onCheckoutQuote callback if provided
@@ -1154,12 +1229,15 @@ async function sendHandlerForDifferentChainDifferentToken({
1154
1229
  checkoutOnHandlers.triggerCheckoutQuote(quote)
1155
1230
  }
1156
1231
 
1157
- // Get intent entrypoint fee options if supported, gasless is enabled, and fundMethod is wallet
1232
+ // Get intent entrypoint fee options if supported and gasless is enabled
1233
+ // Note: We fetch fee options whenever gasless is true, regardless of fundMethod,
1234
+ // because gasless scenarios (including tests) need fee collector information
1235
+ // Skip gasless fee options for sequence-waas wallet
1158
1236
  let intentEntrypointFeeOptions: any = null
1159
- if (gasless && fundMethod === "wallet") {
1237
+ if (gasless && walletId !== "sequence-waas") {
1160
1238
  try {
1161
1239
  logger.console.log(
1162
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless enabled and fundMethod is wallet, checking intent entrypoint support for chain:",
1240
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless enabled, checking intent entrypoint support for chain:",
1163
1241
  originChainId,
1164
1242
  )
1165
1243
 
@@ -1181,9 +1259,13 @@ async function sendHandlerForDifferentChainDifferentToken({
1181
1259
  error,
1182
1260
  )
1183
1261
  }
1262
+ } else if (walletId === "sequence-waas") {
1263
+ logger.console.log(
1264
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Skipping gasless fee options for sequence-waas wallet",
1265
+ )
1184
1266
  } else {
1185
1267
  logger.console.log(
1186
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless disabled or fundMethod is not wallet, skipping intent entrypoint fee options fetch",
1268
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless disabled, skipping intent entrypoint fee options fetch",
1187
1269
  )
1188
1270
  }
1189
1271
 
@@ -1349,6 +1431,8 @@ async function sendHandlerForDifferentChainDifferentToken({
1349
1431
  feeOptions: intentEntrypointFeeOptions,
1350
1432
  trailsClient,
1351
1433
  selectedFeeToken: effectiveSelectedFeeToken,
1434
+ walletId,
1435
+ abortSignal,
1352
1436
  })
1353
1437
 
1354
1438
  if (!originUserTxReceipt) {
@@ -1429,10 +1513,19 @@ async function sendHandlerForDifferentChainDifferentToken({
1429
1513
 
1430
1514
  const checkForDepositTx = async () => {
1431
1515
  while (true) {
1516
+ // Check if we should abort
1517
+ if (abortSignal?.aborted) {
1518
+ logger.console.log(
1519
+ "[trails-sdk] Aborting deposit tx check due to abort signal",
1520
+ )
1521
+ return null
1522
+ }
1523
+
1432
1524
  try {
1433
1525
  const response = await getAccountTransactionHistory({
1434
1526
  chainId: originChainId,
1435
1527
  accountAddress: originIntentAddress,
1528
+ abortSignal,
1436
1529
  })
1437
1530
  logger.console.log(
1438
1531
  "[trails-sdk] getAccountTransactionHistory response",
@@ -1441,14 +1534,16 @@ async function sendHandlerForDifferentChainDifferentToken({
1441
1534
  if (response.transactions.length > 0) {
1442
1535
  const tx = response.transactions[0]
1443
1536
  if (!tx?.txnHash) {
1444
- await new Promise((resolve) => setTimeout(resolve, 1000))
1537
+ await new Promise((resolve) =>
1538
+ setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
1539
+ )
1445
1540
  continue
1446
1541
  }
1447
1542
  // const isReceive = tx.transfers.some(
1448
1543
  // (transfer) => transfer.transferType === "RECEIVE",
1449
1544
  // )
1450
1545
  // if (!isReceive) {
1451
- // await new Promise((resolve) => setTimeout(resolve, 1000))
1546
+ // await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY))
1452
1547
  // continue
1453
1548
  // }
1454
1549
  const originDepositTxReceipt =
@@ -1473,18 +1568,29 @@ async function sendHandlerForDifferentChainDifferentToken({
1473
1568
  } catch (error) {
1474
1569
  logger.console.error("Error checking for deposit tx", error)
1475
1570
  }
1476
- await new Promise((resolve) => setTimeout(resolve, 1000))
1571
+ await new Promise((resolve) =>
1572
+ setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
1573
+ )
1477
1574
  }
1478
1575
  }
1479
1576
 
1480
1577
  const _checkForDestinationDepositTx: () => Promise<TransactionReceipt | null> =
1481
1578
  async () => {
1482
1579
  while (true) {
1580
+ // Check if we should abort
1581
+ if (abortSignal?.aborted) {
1582
+ logger.console.log(
1583
+ "[trails-sdk] Aborting destination deposit tx check due to abort signal",
1584
+ )
1585
+ return null
1586
+ }
1587
+
1483
1588
  try {
1484
1589
  const response = await getAccountTransactionHistory({
1485
1590
  chainId: destinationChainId,
1486
1591
  accountAddress: intent.payloads
1487
1592
  .destinationIntentAddress as `0x${string}`,
1593
+ abortSignal,
1488
1594
  })
1489
1595
  logger.console.log(
1490
1596
  "[trails-sdk] getAccountTransactionHistory response",
@@ -1493,7 +1599,12 @@ async function sendHandlerForDifferentChainDifferentToken({
1493
1599
  if (response.transactions.length > 0) {
1494
1600
  const tx = response.transactions[0]
1495
1601
  if (!tx?.txnHash) {
1496
- await new Promise((resolve) => setTimeout(resolve, 1000))
1602
+ await new Promise((resolve) =>
1603
+ setTimeout(
1604
+ resolve,
1605
+ POLLING_INTERVALS.TRANSACTION_HISTORY,
1606
+ ),
1607
+ )
1497
1608
  continue
1498
1609
  }
1499
1610
  // const isReceive = tx.transfers.some(
@@ -1520,13 +1631,15 @@ async function sendHandlerForDifferentChainDifferentToken({
1520
1631
  } catch (error) {
1521
1632
  logger.console.error("Error checking for deposit tx", error)
1522
1633
  }
1523
- await new Promise((resolve) => setTimeout(resolve, 1000))
1634
+ await new Promise((resolve) =>
1635
+ setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
1636
+ )
1524
1637
  }
1525
1638
  }
1526
1639
 
1527
1640
  // Variables to store the waitForReceipt functions
1528
1641
  let originMetaTxnReceiptPromise:
1529
- | (() => Promise<MetaTxnReceipt | null>)
1642
+ | ((abortSignal?: AbortSignal) => Promise<MetaTxnReceipt | null>)
1530
1643
  | null = null
1531
1644
  let destinationMetaTxnReceiptPromise:
1532
1645
  | ((abortSignal?: AbortSignal) => Promise<MetaTxnReceipt | null>)
@@ -1783,6 +1896,14 @@ async function sendHandlerForDifferentChainDifferentToken({
1783
1896
  if (isCctp) {
1784
1897
  queueCctpPromise = async () => {
1785
1898
  while (true) {
1899
+ // Check if we should abort
1900
+ if (abortSignal?.aborted) {
1901
+ logger.console.log(
1902
+ "[trails-sdk] Aborting CCTP queue due to abort signal",
1903
+ )
1904
+ return
1905
+ }
1906
+
1786
1907
  const originMetaTxnHash = originMetaTxnReceipt?.txnHash
1787
1908
  if (originMetaTxnHash) {
1788
1909
  await queueCCTPTransfer({
@@ -1793,16 +1914,38 @@ async function sendHandlerForDifferentChainDifferentToken({
1793
1914
  })
1794
1915
  break
1795
1916
  }
1796
- await new Promise((resolve) => setTimeout(resolve, 1000))
1917
+ await new Promise((resolve) =>
1918
+ setTimeout(resolve, POLLING_INTERVALS.CCTP_QUEUE),
1919
+ )
1797
1920
  }
1798
1921
  }
1799
1922
  } else {
1800
1923
  queueCctpPromise = () => Promise.resolve()
1801
1924
  }
1802
1925
 
1803
- checkForDepositTx().catch((error) => {
1804
- console.error("Error checking for deposit tx", error)
1805
- })
1926
+ // Only start polling for external deposits if needed:
1927
+ // - QR code or exchange funding (external deposits)
1928
+ // - Gasless flow (relayer-executed deposits)
1929
+ const needsDepositPolling =
1930
+ fundMethod === "qr-code" ||
1931
+ fundMethod === "exchange" ||
1932
+ gasless === true
1933
+
1934
+ if (needsDepositPolling) {
1935
+ logger.console.log(
1936
+ "[trails-sdk] Starting deposit polling for fundMethod:",
1937
+ fundMethod,
1938
+ "gasless:",
1939
+ gasless,
1940
+ )
1941
+ checkForDepositTx().catch((error) => {
1942
+ logger.console.error("Error checking for deposit tx", error)
1943
+ })
1944
+ } else {
1945
+ logger.console.log(
1946
+ "[trails-sdk] Skipping deposit polling for wallet funding with gas fees",
1947
+ )
1948
+ }
1806
1949
 
1807
1950
  // Phase 1: Send meta transactions and queue CCTP
1808
1951
  logger.console.log(
@@ -1827,7 +1970,8 @@ async function sendHandlerForDifferentChainDifferentToken({
1827
1970
  )
1828
1971
  if (originMetaTxnReceiptPromise) {
1829
1972
  try {
1830
- originMetaTxnReceipt = await originMetaTxnReceiptPromise()
1973
+ originMetaTxnReceipt =
1974
+ await originMetaTxnReceiptPromise(abortSignal)
1831
1975
 
1832
1976
  if (originMetaTxnReceipt && transactionStates[1]) {
1833
1977
  transactionStates[1] = getTransactionStateFromReceipt(
@@ -1907,12 +2051,9 @@ async function sendHandlerForDifferentChainDifferentToken({
1907
2051
  )
1908
2052
  if (destinationMetaTxnReceiptPromise) {
1909
2053
  try {
1910
- // Create abort controller for cancelling destination polling
1911
- const abortController = new AbortController()
1912
-
1913
2054
  // Race between destination receipt and failure polling
1914
2055
  const destinationReceiptPromise = destinationMetaTxnReceiptPromise
1915
- ? destinationMetaTxnReceiptPromise(abortController.signal)
2056
+ ? destinationMetaTxnReceiptPromise(abortSignal)
1916
2057
  : Promise.resolve(null)
1917
2058
 
1918
2059
  const failurePollingPromise = new Promise<null>((resolve) => {
@@ -1929,11 +2070,13 @@ async function sendHandlerForDifferentChainDifferentToken({
1929
2070
  logger.console.log(
1930
2071
  "[trails-sdk] Aborting destination meta transaction due to previous transaction failure",
1931
2072
  )
1932
- abortController.abort()
1933
2073
  resolve(null)
1934
2074
  } else {
1935
- // Continue polling every 1 second
1936
- setTimeout(pollForFailures, 1000)
2075
+ // Continue polling with consistent interval
2076
+ setTimeout(
2077
+ pollForFailures,
2078
+ POLLING_INTERVALS.FAILURE_POLLING,
2079
+ )
1937
2080
  }
1938
2081
  }
1939
2082
  pollForFailures()
@@ -2019,11 +2162,14 @@ async function sendHandlerForDifferentChainDifferentToken({
2019
2162
 
2020
2163
  onTransactionStateChange(transactionStates)
2021
2164
  } catch (error) {
2022
- console.error("Error decoding destination tx events", error)
2165
+ logger.console.error(
2166
+ "Error decoding destination tx events",
2167
+ error,
2168
+ )
2023
2169
  }
2024
2170
  }
2025
2171
  } catch (error) {
2026
- console.error(
2172
+ logger.console.error(
2027
2173
  "[trails-sdk] Error waiting for destination receipt:",
2028
2174
  error,
2029
2175
  )
@@ -2059,6 +2205,17 @@ async function sendHandlerForDifferentChainDifferentToken({
2059
2205
 
2060
2206
  // Track payment completion for different chain and different token
2061
2207
  if (originUserTxReceipt && destinationMetaTxnReceipt) {
2208
+ // Check if any transaction failed or was refunded
2209
+ const hasAnyCallFailed = transactionStates.some((tx) =>
2210
+ tx?.decodedGuestModuleEvents?.some(
2211
+ (e: any) => e?.type === "CallFailed",
2212
+ ),
2213
+ )
2214
+ const hasAnyRefunded = transactionStates.some((tx) => tx?.refunded)
2215
+ const txStatus =
2216
+ !hasAnyCallFailed && !hasAnyRefunded ? "success" : "fail"
2217
+
2218
+ // Always track payment completion regardless of success/failure
2062
2219
  trackPaymentCompleted({
2063
2220
  userAddress: account.address,
2064
2221
  originIntentAddress,
@@ -2079,9 +2236,9 @@ async function sendHandlerForDifferentChainDifferentToken({
2079
2236
  effectiveDestinationTokenAmountUsd?.toString(),
2080
2237
  })
2081
2238
 
2082
- // Call onCheckoutComplete callback if provided
2239
+ // Call onCheckoutComplete callback with transaction status
2083
2240
  if (checkoutOnHandlers?.triggerCheckoutComplete) {
2084
- checkoutOnHandlers.triggerCheckoutComplete()
2241
+ checkoutOnHandlers.triggerCheckoutComplete(txStatus)
2085
2242
  }
2086
2243
  } else {
2087
2244
  if (
@@ -2190,6 +2347,12 @@ async function sendHandlerForSameChainSameToken({
2190
2347
  checkoutOnHandlers,
2191
2348
  mode,
2192
2349
  fundMethod,
2350
+ gasless,
2351
+ paymasterUrl,
2352
+ selectedFeeToken,
2353
+ trailsClient,
2354
+ originRelayer,
2355
+ abortSignal,
2193
2356
  }: {
2194
2357
  originTokenAddress: string
2195
2358
  originTokenDecimals: number
@@ -2211,6 +2374,12 @@ async function sendHandlerForSameChainSameToken({
2211
2374
  checkoutOnHandlers?: CheckoutOnHandlers
2212
2375
  mode?: "pay" | "fund" | "earn" | "swap" | "receive"
2213
2376
  fundMethod?: string
2377
+ gasless?: boolean
2378
+ paymasterUrl?: string
2379
+ selectedFeeToken?: any
2380
+ trailsClient: TrailsAPIClient
2381
+ originRelayer: Relayer.RpcRelayer
2382
+ abortSignal?: AbortSignal
2214
2383
  }): Promise<PrepareSendReturn> {
2215
2384
  logger.console.log("[trails-sdk] isToSameToken && isToSameChain")
2216
2385
  const testnet = isTestnetDebugMode()
@@ -2237,6 +2406,44 @@ async function sendHandlerForSameChainSameToken({
2237
2406
  noSufficientBalance = true
2238
2407
  }
2239
2408
 
2409
+ // Build origin call params and estimate gas limit for the quote
2410
+ const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
2411
+ const originCallParamsBase = buildSameChainTransactionParams({
2412
+ hasCustomCalldata,
2413
+ recipient,
2414
+ effectiveOriginTokenAddress,
2415
+ destinationCalldata,
2416
+ swapAmount,
2417
+ effectiveOriginChainId,
2418
+ effectiveOriginChain,
2419
+ })
2420
+
2421
+ logger.console.log(
2422
+ "[trails-sdk][gas-estimation] About to estimate gas limit for quote with params:",
2423
+ {
2424
+ account: account.address,
2425
+ to: originCallParamsBase.to,
2426
+ data: originCallParamsBase.data,
2427
+ value: originCallParamsBase.value,
2428
+ },
2429
+ )
2430
+
2431
+ const estimatedGasLimitForQuote = await estimateGasLimit(
2432
+ effectivePublicClient,
2433
+ {
2434
+ account: account.address,
2435
+ to: originCallParamsBase.to,
2436
+ data: originCallParamsBase.data,
2437
+ value: BigInt(originCallParamsBase.value),
2438
+ },
2439
+ "quote",
2440
+ )
2441
+
2442
+ logger.console.log(
2443
+ "[trails-sdk][gas-estimation] Estimated gas limit for quote:",
2444
+ estimatedGasLimitForQuote,
2445
+ )
2446
+
2240
2447
  const quote = await getNormalizedQuoteObject({
2241
2448
  originDepositAddress: recipient,
2242
2449
  destinationDepositAddress: recipient,
@@ -2255,6 +2462,8 @@ async function sendHandlerForSameChainSameToken({
2255
2462
  slippageTolerance,
2256
2463
  quoteProvider: "",
2257
2464
  noSufficientBalance,
2465
+ estimatedGasLimit: estimatedGasLimitForQuote,
2466
+ intent: undefined,
2258
2467
  })
2259
2468
 
2260
2469
  // Call onCheckoutQuote callback if provided
@@ -2290,26 +2499,33 @@ async function sendHandlerForSameChainSameToken({
2290
2499
  })
2291
2500
 
2292
2501
  const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
2502
+
2503
+ // Build origin call params (reusing the same logic as quote estimation)
2504
+ const originCallParamsBase = buildSameChainTransactionParams({
2505
+ hasCustomCalldata,
2506
+ recipient,
2507
+ effectiveOriginTokenAddress,
2508
+ destinationCalldata,
2509
+ swapAmount,
2510
+ effectiveOriginChainId,
2511
+ effectiveOriginChain,
2512
+ })
2513
+
2514
+ // Estimate gas limit for consistency with actual transaction
2515
+ const gasLimit = await estimateGasLimit(
2516
+ effectivePublicClient,
2517
+ {
2518
+ account: account.address,
2519
+ to: originCallParamsBase.to,
2520
+ data: originCallParamsBase.data,
2521
+ value: BigInt(originCallParamsBase.value),
2522
+ },
2523
+ "send",
2524
+ )
2525
+
2293
2526
  const originCallParams = {
2294
- to: hasCustomCalldata
2295
- ? recipient
2296
- : effectiveOriginTokenAddress === zeroAddress
2297
- ? recipient
2298
- : effectiveOriginTokenAddress,
2299
- data: hasCustomCalldata
2300
- ? destinationCalldata
2301
- : effectiveOriginTokenAddress === zeroAddress
2302
- ? "0x"
2303
- : getERC20TransferData({
2304
- recipient,
2305
- amount: BigInt(swapAmount),
2306
- }),
2307
- value:
2308
- effectiveOriginTokenAddress === zeroAddress
2309
- ? BigInt(swapAmount)
2310
- : "0",
2311
- chainId: effectiveOriginChainId,
2312
- chain: effectiveOriginChain,
2527
+ ...originCallParamsBase,
2528
+ gasLimit,
2313
2529
  }
2314
2530
 
2315
2531
  logger.console.log("[trails-sdk] origin call params", originCallParams)
@@ -2340,105 +2556,208 @@ async function sendHandlerForSameChainSameToken({
2340
2556
  )
2341
2557
  }
2342
2558
 
2343
- if (hasCustomCalldata) {
2559
+ // For gasless transactions with custom calldata, use the gasless deposit flow
2560
+ if (gasless && hasCustomCalldata) {
2561
+ logger.console.log(
2562
+ "[trails-sdk] Using gasless intent entrypoint flow for same-chain transaction with custom calldata",
2563
+ )
2564
+
2565
+ // Fetch fee options for gasless deposit
2566
+ let feeOptions: any = null
2344
2567
  try {
2345
- const needsApproval = await getNeedsApproval({
2346
- publicClient: effectivePublicClient,
2347
- token: effectiveOriginTokenAddress,
2348
- account: account.address,
2349
- spender: recipient,
2350
- amount: maxUint256,
2568
+ feeOptions = await getIntentEntrypointFeeOptions({
2569
+ trailsClient,
2570
+ userAddress: account.address as `0x${string}`,
2571
+ tokenAddress: effectiveOriginTokenAddress as `0x${string}`,
2572
+ amount: swapAmount,
2573
+ intentAddress: recipient as `0x${string}`,
2574
+ chainId: effectiveOriginChainId,
2575
+ })
2576
+ logger.console.log(
2577
+ "[trails-sdk] [GASLESS-FLOW] Fetched intent entrypoint fee options for same-chain transaction:",
2578
+ feeOptions,
2579
+ )
2580
+ } catch (error) {
2581
+ logger.console.error(
2582
+ "[trails-sdk] [GASLESS-FLOW] Error fetching fee options for same-chain gasless transaction:",
2583
+ error,
2584
+ )
2585
+ }
2586
+
2587
+ const receipt = await attemptGaslessDeposit({
2588
+ account,
2589
+ walletClient,
2590
+ chain: effectiveOriginChain,
2591
+ depositTokenAddress: effectiveOriginTokenAddress,
2592
+ depositTokenAmount: swapAmount,
2593
+ depositRecipient: recipient,
2594
+ onOriginSend: () => {}, // No-op callback
2595
+ feeOptions,
2596
+ paymasterUrl,
2597
+ selectedFeeToken,
2598
+ trailsClient,
2599
+ originRelayer,
2600
+ abortSignal,
2601
+ })
2602
+
2603
+ if (receipt) {
2604
+ originUserTxReceipt = receipt
2605
+
2606
+ // Track the confirmed transaction
2607
+ trackTransactionConfirmed({
2608
+ transactionHash: receipt.transactionHash,
2609
+ chainId: effectiveOriginChainId,
2610
+ userAddress: account.address,
2611
+ blockNumber: Number(receipt.blockNumber),
2612
+ originTokenAddress,
2613
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
2351
2614
  })
2352
2615
 
2353
- if (needsApproval) {
2354
- const txHash = await approveERC20({
2355
- walletClient,
2356
- tokenAddress: effectiveOriginTokenAddress,
2616
+ // Toast will be shown after transaction state analysis
2617
+
2618
+ // Update transaction state
2619
+ try {
2620
+ onTransactionStateChange([
2621
+ getTransactionStateFromReceipt(
2622
+ receipt,
2623
+ effectiveOriginChainId,
2624
+ transactionStates[0]?.label,
2625
+ ),
2626
+ ])
2627
+ } catch (error) {
2628
+ logger.console.error(
2629
+ "[trails-sdk] Error calling onTransactionStateChange:",
2630
+ error,
2631
+ )
2632
+ }
2633
+ }
2634
+ } else {
2635
+ // Non-gasless flow: handle approval if needed
2636
+ if (hasCustomCalldata) {
2637
+ try {
2638
+ const needsApproval = await getNeedsApproval({
2639
+ publicClient: effectivePublicClient,
2640
+ token: effectiveOriginTokenAddress,
2641
+ account: account.address,
2357
2642
  spender: recipient,
2358
2643
  amount: maxUint256,
2359
- chain: effectiveOriginChain,
2360
2644
  })
2361
2645
 
2362
- logger.console.log("waiting for approve", txHash)
2363
- await effectivePublicClient.waitForTransactionReceipt({
2364
- hash: txHash,
2365
- })
2366
- logger.console.log("approve done")
2646
+ if (needsApproval) {
2647
+ const txHash = await approveERC20({
2648
+ walletClient,
2649
+ tokenAddress: effectiveOriginTokenAddress,
2650
+ spender: recipient,
2651
+ amount: maxUint256,
2652
+ chain: effectiveOriginChain,
2653
+ })
2654
+
2655
+ logger.console.log("waiting for approve", txHash)
2656
+ await effectivePublicClient.waitForTransactionReceipt({
2657
+ hash: txHash,
2658
+ })
2659
+ logger.console.log("approve done")
2660
+ }
2661
+ } catch (error) {
2662
+ logger.console.error(
2663
+ "[trails-sdk] Error approving ERC20",
2664
+ error,
2665
+ )
2367
2666
  }
2368
- } catch (error) {
2369
- logger.console.error("[trails-sdk] Error approving ERC20", error)
2370
2667
  }
2371
- }
2372
2668
 
2373
- // Show persistent toast for checkout flow
2374
- updatePersistentToast(
2375
- "Payment Started",
2376
- "Waiting for wallet confirmation...",
2377
- "info",
2378
- )
2669
+ // Show persistent toast for checkout flow
2670
+ updatePersistentToast(
2671
+ "Payment Started",
2672
+ "Waiting for wallet confirmation...",
2673
+ "info",
2674
+ )
2379
2675
 
2380
- logger.console.log(
2381
- "[trails-sdk] origin call params",
2382
- originCallParams,
2383
- )
2384
- const txHash = await sendOriginTransaction(
2385
- account,
2386
- walletClient,
2387
- originCallParams as any,
2388
- {
2389
- depositTokenAmountUsd: depositAmountUsd?.toString(),
2390
- },
2391
- ) // TODO: Add proper type
2676
+ logger.console.log(
2677
+ "[trails-sdk] origin call params",
2678
+ originCallParams,
2679
+ )
2680
+ const txHash = await sendOriginTransaction(
2681
+ account,
2682
+ walletClient,
2683
+ originCallParams as any,
2684
+ {
2685
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
2686
+ },
2687
+ ) // TODO: Add proper type
2392
2688
 
2393
- logger.console.log("[trails-sdk] origin tx", txHash)
2689
+ logger.console.log("[trails-sdk] origin tx", txHash)
2394
2690
 
2395
- if (onOriginSend) {
2396
- onOriginSend()
2397
- }
2691
+ if (onOriginSend) {
2692
+ onOriginSend()
2693
+ }
2398
2694
 
2399
- // Wait for transaction receipt
2400
- const receipt = await effectivePublicClient.waitForTransactionReceipt(
2401
- {
2402
- hash: txHash,
2403
- },
2404
- )
2405
- logger.console.log("[trails-sdk] receipt", receipt)
2406
- originUserTxReceipt = receipt
2695
+ // Wait for transaction receipt
2696
+ const receipt =
2697
+ await effectivePublicClient.waitForTransactionReceipt({
2698
+ hash: txHash,
2699
+ })
2700
+ logger.console.log("[trails-sdk] receipt", receipt)
2701
+ originUserTxReceipt = receipt
2407
2702
 
2408
- trackTransactionConfirmed({
2409
- transactionHash: txHash,
2410
- chainId: effectiveOriginChainId,
2411
- userAddress: account.address,
2412
- blockNumber: Number(receipt.blockNumber),
2413
- originTokenAddress,
2414
- depositTokenAmountUsd: depositAmountUsd?.toString(),
2415
- })
2703
+ trackTransactionConfirmed({
2704
+ transactionHash: txHash,
2705
+ chainId: effectiveOriginChainId,
2706
+ userAddress: account.address,
2707
+ blockNumber: Number(receipt.blockNumber),
2708
+ originTokenAddress,
2709
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
2710
+ })
2416
2711
 
2417
- // Remove persistent toast and show success
2418
- const chainInfo = getChainInfo(effectiveOriginChainId)
2419
- updatePersistentToast(
2420
- "Transfer Confirmed",
2421
- `Your transaction on ${chainInfo?.name || "chain"} has been confirmed`,
2422
- "info",
2423
- )
2712
+ // Toast will be shown after transaction state analysis
2424
2713
 
2425
- try {
2426
- onTransactionStateChange([
2427
- getTransactionStateFromReceipt(
2428
- originUserTxReceipt,
2429
- effectiveOriginChainId,
2430
- transactionStates[0]?.label,
2431
- ),
2432
- ])
2433
- } catch (error) {
2434
- logger.console.error(
2435
- "[trails-sdk] Error calling onTransactionStateChange:",
2436
- error,
2437
- )
2714
+ try {
2715
+ onTransactionStateChange([
2716
+ getTransactionStateFromReceipt(
2717
+ originUserTxReceipt,
2718
+ effectiveOriginChainId,
2719
+ transactionStates[0]?.label,
2720
+ ),
2721
+ ])
2722
+ } catch (error) {
2723
+ logger.console.error(
2724
+ "[trails-sdk] Error calling onTransactionStateChange:",
2725
+ error,
2726
+ )
2727
+ }
2728
+ }
2729
+
2730
+ // Show conditional toast based on transaction status
2731
+ const chainInfo = getChainInfo(effectiveOriginChainId)
2732
+ if (originUserTxReceipt) {
2733
+ if (originUserTxReceipt?.status === "success") {
2734
+ updatePersistentToast(
2735
+ "Transfer Confirmed",
2736
+ `Your transaction on ${chainInfo?.name || "chain"} has been confirmed`,
2737
+ "info",
2738
+ )
2739
+ } else {
2740
+ updatePersistentToast(
2741
+ "Transfer Failed",
2742
+ `Your transaction on ${chainInfo?.name || "chain"} failed`,
2743
+ "error",
2744
+ )
2745
+ }
2438
2746
  }
2439
2747
 
2440
2748
  // Track payment completion for same-chain same-token transaction
2441
2749
  if (originUserTxReceipt && originUserTxReceipt.status === "success") {
2750
+ // Check if any transaction failed or was refunded
2751
+ const hasAnyCallFailed = transactionStates.some((tx) =>
2752
+ tx?.decodedGuestModuleEvents?.some(
2753
+ (e: any) => e?.type === "CallFailed",
2754
+ ),
2755
+ )
2756
+ const hasAnyRefunded = transactionStates.some((tx) => tx?.refunded)
2757
+ const txStatus =
2758
+ !hasAnyCallFailed && !hasAnyRefunded ? "success" : "fail"
2759
+
2760
+ // Always track payment completion regardless of success/failure
2442
2761
  trackPaymentCompleted({
2443
2762
  userAddress: account.address,
2444
2763
  originTxHash: originUserTxReceipt.transactionHash,
@@ -2450,9 +2769,9 @@ async function sendHandlerForSameChainSameToken({
2450
2769
  destinationTokenAmountUsd: depositAmountUsd?.toString(), // same as deposit amount
2451
2770
  })
2452
2771
 
2453
- // Call onCheckoutComplete callback if provided
2772
+ // Call onCheckoutComplete callback with transaction status
2454
2773
  if (checkoutOnHandlers?.triggerCheckoutComplete) {
2455
- checkoutOnHandlers.triggerCheckoutComplete()
2774
+ checkoutOnHandlers.triggerCheckoutComplete(txStatus)
2456
2775
  }
2457
2776
  } else if (originUserTxReceipt) {
2458
2777
  trackPaymentError({
@@ -2607,7 +2926,7 @@ async function _sendHandlerForSameChainDifferentToken({
2607
2926
  }
2608
2927
  }
2609
2928
  } catch (error) {
2610
- console.error("[trails-sdk] Error decoding function data:", error)
2929
+ logger.console.error("[trails-sdk] Error decoding function data:", error)
2611
2930
  }
2612
2931
  }
2613
2932
 
@@ -2660,6 +2979,7 @@ async function _sendHandlerForSameChainDifferentToken({
2660
2979
  destinationChainId: originChainId,
2661
2980
  originNativeTokenPriceUsd,
2662
2981
  quoteProvider: "relay",
2982
+ intent: undefined,
2663
2983
  })
2664
2984
 
2665
2985
  return {
@@ -2783,6 +3103,7 @@ async function attemptGaslessDeposit({
2783
3103
  originRelayer,
2784
3104
  feeOptions,
2785
3105
  selectedFeeToken,
3106
+ abortSignal,
2786
3107
  }: {
2787
3108
  paymasterUrl?: string
2788
3109
  depositTokenAddress: string
@@ -2793,9 +3114,10 @@ async function attemptGaslessDeposit({
2793
3114
  chain: Chain
2794
3115
  account: Account
2795
3116
  trailsClient: TrailsAPIClient
2796
- originRelayer: RpcRelayer.RpcRelayer
3117
+ originRelayer: Relayer.RpcRelayer
2797
3118
  feeOptions: any
2798
3119
  selectedFeeToken?: any
3120
+ abortSignal?: AbortSignal
2799
3121
  }): Promise<TransactionReceipt | null> {
2800
3122
  let originUserTxReceipt: TransactionReceipt | null = null
2801
3123
  const originChainId = chain.id
@@ -2900,7 +3222,7 @@ async function attemptGaslessDeposit({
2900
3222
  feeOptions && feeOptions.feeOptions?.length > 0,
2901
3223
  )
2902
3224
 
2903
- // 1. Check if we need approval - check if we have enough allowance for this transaction (deposit + fee)
3225
+ // 1. Check if we need approval - check if we have enough allowance for this transaction (deposit + fee if same token)
2904
3226
  let requiredAmount = BigInt(depositTokenAmount)
2905
3227
 
2906
3228
  // Match selectedFeeToken by tokenAddress to get the latest fee amount from feeOptions
@@ -2930,14 +3252,36 @@ async function attemptGaslessDeposit({
2930
3252
  )
2931
3253
  }
2932
3254
 
2933
- if (selectedFeeOption?.amount) {
2934
- requiredAmount = requiredAmount + BigInt(selectedFeeOption.amount)
2935
- logger.console.log("[trails-sdk] Including fee in required amount:", {
2936
- depositAmount: depositTokenAmount,
2937
- feeAmount: selectedFeeOption.amount,
2938
- feeTokenAddress: selectedFeeOption.tokenAddress,
2939
- totalRequired: requiredAmount.toString(),
2940
- })
3255
+ // Only include fee in required amount if the fee token is the same as the deposit token
3256
+ if (selectedFeeOption?.amount && selectedFeeOption?.tokenAddress) {
3257
+ const feeTokenIsSameAsDepositToken =
3258
+ selectedFeeOption.tokenAddress.toLowerCase() ===
3259
+ depositTokenAddress.toLowerCase()
3260
+
3261
+ if (feeTokenIsSameAsDepositToken) {
3262
+ requiredAmount = requiredAmount + BigInt(selectedFeeOption.amount)
3263
+ logger.console.log(
3264
+ "[trails-sdk] Fee token matches deposit token, including fee in required approval amount:",
3265
+ {
3266
+ depositAmount: depositTokenAmount,
3267
+ feeAmount: selectedFeeOption.amount,
3268
+ feeTokenAddress: selectedFeeOption.tokenAddress,
3269
+ depositTokenAddress,
3270
+ totalRequired: requiredAmount.toString(),
3271
+ },
3272
+ )
3273
+ } else {
3274
+ logger.console.log(
3275
+ "[trails-sdk] Fee token differs from deposit token, separate approval will be needed:",
3276
+ {
3277
+ depositAmount: depositTokenAmount,
3278
+ depositTokenAddress,
3279
+ feeAmount: selectedFeeOption.amount,
3280
+ feeTokenAddress: selectedFeeOption.tokenAddress,
3281
+ depositTokenRequired: requiredAmount.toString(),
3282
+ },
3283
+ )
3284
+ }
2941
3285
  }
2942
3286
 
2943
3287
  const needsApproval = await getNeedsIntentEntrypointApproval({
@@ -2962,13 +3306,14 @@ async function attemptGaslessDeposit({
2962
3306
  },
2963
3307
  )
2964
3308
 
2965
- // 2. Get permit signature if approval needed
3309
+ // 2. Get permit signature if approval needed for deposit token
3310
+ // Note: Fee payment is handled by the backend/relayer, we only need permit for the deposit token
2966
3311
  let permitSignature: string | undefined
2967
3312
  let permitDeadline: number | undefined
2968
3313
 
2969
3314
  if (needsApproval) {
2970
3315
  logger.console.log(
2971
- "[trails-sdk] Getting permit signature for infinite approval",
3316
+ "[trails-sdk] Getting permit signature for deposit token infinite approval",
2972
3317
  )
2973
3318
 
2974
3319
  // Use infinite approval (maxUint256) so user doesn't need to approve again
@@ -2977,6 +3322,7 @@ async function attemptGaslessDeposit({
2977
3322
  "[trails-sdk] Using infinite approval for gasless deposits",
2978
3323
  {
2979
3324
  depositAmount: depositTokenAmount,
3325
+ depositTokenAddress,
2980
3326
  permitAmount: permitAmount.toString(),
2981
3327
  },
2982
3328
  )
@@ -2994,7 +3340,7 @@ async function attemptGaslessDeposit({
2994
3340
  permitSignature = permitSig.signature
2995
3341
  permitDeadline = Number(permitSig.deadline)
2996
3342
  logger.console.log(
2997
- "[trails-sdk] Permit signature obtained for infinite approval",
3343
+ "[trails-sdk] Deposit token permit signature obtained for infinite approval",
2998
3344
  )
2999
3345
  }
3000
3346
 
@@ -3119,6 +3465,14 @@ async function attemptGaslessDeposit({
3119
3465
  logger.console.log("[trails-sdk] Waiting for transaction receipt")
3120
3466
  // eslint-disable-next-line no-constant-condition
3121
3467
  while (true) {
3468
+ // Check if we should abort
3469
+ if (abortSignal?.aborted) {
3470
+ logger.console.log(
3471
+ "[trails-sdk] Aborting gasless deposit polling due to abort signal",
3472
+ )
3473
+ return null
3474
+ }
3475
+
3122
3476
  const receipt: any = await getMetaTxStatus(
3123
3477
  originRelayer,
3124
3478
  depositData.metaTxn.id,
@@ -3141,7 +3495,9 @@ async function attemptGaslessDeposit({
3141
3495
  break
3142
3496
  }
3143
3497
 
3144
- await new Promise((resolve) => setTimeout(resolve, 1000))
3498
+ await new Promise((resolve) =>
3499
+ setTimeout(resolve, POLLING_INTERVALS.META_TRANSACTION),
3500
+ )
3145
3501
  }
3146
3502
  } catch (error) {
3147
3503
  logger.console.error(
@@ -3221,24 +3577,31 @@ export async function attemptNonGaslessUserDeposit({
3221
3577
 
3222
3578
  logger.console.log("[trails-sdk] needsNativeFee", needsNativeFee)
3223
3579
 
3224
- const originCallParams = {
3225
- to:
3226
- originTokenAddress === zeroAddress
3227
- ? originIntentAddress
3228
- : originTokenAddress,
3229
- data:
3230
- originTokenAddress === zeroAddress
3231
- ? "0x"
3232
- : getERC20TransferData({
3233
- recipient: originIntentAddress,
3234
- amount: BigInt(firstPreconditionMin) + BigInt(fee),
3235
- }),
3236
- value:
3237
- originTokenAddress === zeroAddress
3238
- ? BigInt(firstPreconditionMin) + BigInt(fee)
3239
- : "0",
3240
- chainId: originChainId,
3580
+ // Build origin call params
3581
+ const originCallParamsBase = buildCrossChainDepositParams({
3582
+ originTokenAddress,
3583
+ originIntentAddress,
3584
+ depositAmount: firstPreconditionMin,
3585
+ fee,
3586
+ originChainId,
3241
3587
  chain,
3588
+ })
3589
+
3590
+ // Estimate gas limit for consistency with actual transaction
3591
+ const gasLimit = await estimateGasLimit(
3592
+ publicClient,
3593
+ {
3594
+ account: account.address,
3595
+ to: originCallParamsBase.to,
3596
+ data: originCallParamsBase.data,
3597
+ value: BigInt(originCallParamsBase.value),
3598
+ },
3599
+ "deposit",
3600
+ )
3601
+
3602
+ const originCallParams = {
3603
+ ...originCallParamsBase,
3604
+ gasLimit,
3242
3605
  }
3243
3606
 
3244
3607
  await attemptSwitchChain({
@@ -3494,13 +3857,15 @@ async function attemptUserDepositTx({
3494
3857
  feeOptions,
3495
3858
  trailsClient,
3496
3859
  selectedFeeToken,
3860
+ walletId,
3861
+ abortSignal,
3497
3862
  }: {
3498
3863
  originTokenAddress: string
3499
3864
  gasless: boolean
3500
3865
  paymasterUrl?: string
3501
3866
  chain: Chain
3502
3867
  account: Account
3503
- originRelayer: RpcRelayer.RpcRelayer
3868
+ originRelayer: Relayer.RpcRelayer
3504
3869
  firstPreconditionMin: string
3505
3870
  originIntentAddress: string
3506
3871
  onOriginSend?: () => void
@@ -3522,6 +3887,8 @@ async function attemptUserDepositTx({
3522
3887
  feeOptions?: any
3523
3888
  trailsClient: TrailsAPIClient
3524
3889
  selectedFeeToken?: any
3890
+ walletId?: string
3891
+ abortSignal?: AbortSignal
3525
3892
  }): Promise<TransactionReceipt | null> {
3526
3893
  let originUserTxReceipt: TransactionReceipt | null = null
3527
3894
  const originChainId = chain.id
@@ -3545,6 +3912,7 @@ async function attemptUserDepositTx({
3545
3912
  gasless,
3546
3913
  feeOptions,
3547
3914
  selectedFeeToken,
3915
+ walletId,
3548
3916
  )
3549
3917
  logger.console.log(
3550
3918
  "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] doGasless check results:",
@@ -3584,15 +3952,19 @@ async function attemptUserDepositTx({
3584
3952
  originRelayer,
3585
3953
  feeOptions: feeOptions,
3586
3954
  selectedFeeToken: selectedFeeToken,
3955
+ abortSignal,
3587
3956
  })
3588
3957
  } catch (error) {
3589
3958
  logger.console.log("[trails-sdk] gassless attempt failed", error)
3590
- throw error
3959
+ // In strict gasless mode, re-throw error instead of falling back
3960
+ if (gasless) {
3961
+ throw error
3962
+ }
3591
3963
  }
3592
3964
  }
3593
3965
 
3594
3966
  // If gasless attempt failed, try to send a regular transaction
3595
- if (!originUserTxReceipt) {
3967
+ if (!originUserTxReceipt && !gasless) {
3596
3968
  originUserTxReceipt = await attemptNonGaslessUserDeposit({
3597
3969
  originTokenAddress,
3598
3970
  firstPreconditionMin,
@@ -3626,7 +3998,16 @@ export function getDoGasless(
3626
3998
  gasless: boolean,
3627
3999
  feeOptions?: any,
3628
4000
  selectedFeeToken?: any,
4001
+ walletId?: string,
3629
4002
  ): boolean {
4003
+ // Don't use gasless flow for sequence-waas wallet
4004
+ if (walletId === "sequence-waas") {
4005
+ logger.console.log(
4006
+ "[trails-sdk] [GASLESS-FLOW] getDoGasless: Skipping gasless flow for sequence-waas wallet",
4007
+ )
4008
+ return false
4009
+ }
4010
+
3630
4011
  const hasFeeOptions = Boolean(feeOptions && feeOptions.feeOptions?.length > 0)
3631
4012
 
3632
4013
  // Important: The UI passes selectedFeeToken in these states:
@@ -3763,7 +4144,7 @@ async function sendMetaTxAndWaitForReceipt({
3763
4144
  feeQuote,
3764
4145
  }: {
3765
4146
  metaTx: MetaTxn
3766
- relayer: RpcRelayer.RpcRelayer
4147
+ relayer: Relayer.RpcRelayer
3767
4148
  precondition: IntentPrecondition | null
3768
4149
  feeQuote?: string
3769
4150
  }): Promise<{
@@ -3807,38 +4188,84 @@ async function sendMetaTxAndWaitForReceipt({
3807
4188
  waitForReceipt: async (abortSignal?: AbortSignal) => {
3808
4189
  let originMetaTxnReceipt: MetaTxnReceipt | null = null
3809
4190
 
3810
- // eslint-disable-next-line no-constant-condition
3811
- while (true) {
3812
- // Check if we should abort
3813
- if (abortSignal?.aborted) {
4191
+ // Create a unique ID for this polling operation
4192
+ const pollingId = `meta-txn-${metaTx.id}-${metaTx.chainId}-${Date.now()}`
4193
+
4194
+ // Create an abort controller for this specific polling operation
4195
+ const pollingAbortController = new AbortController()
4196
+
4197
+ // Register this polling operation with the global registry
4198
+ abortControllerRegistry.register(pollingId, pollingAbortController)
4199
+
4200
+ try {
4201
+ // eslint-disable-next-line no-constant-condition
4202
+ while (true) {
4203
+ // Check if we should abort (either from external signal or global registry)
4204
+ if (abortSignal?.aborted || pollingAbortController.signal.aborted) {
4205
+ logger.console.log(
4206
+ "[trails-sdk] Aborting meta transaction polling due to abort signal",
4207
+ )
4208
+ return null
4209
+ }
4210
+
3814
4211
  logger.console.log(
3815
- "[trails-sdk] Aborting meta transaction polling due to abort signal",
4212
+ "[trails-sdk] polling status",
4213
+ metaTx.id as `0x${string}`,
4214
+ metaTx.chainId.toString(),
3816
4215
  )
3817
- return null
3818
- }
4216
+ const receipt: any = await getMetaTxStatus(
4217
+ relayer,
4218
+ metaTx.id,
4219
+ Number(metaTx.chainId),
4220
+ )
4221
+ logger.console.log("[trails-sdk] status", receipt)
4222
+ if (receipt?.transactionHash) {
4223
+ originMetaTxnReceipt = receipt.data?.receipt
4224
+ if (!originMetaTxnReceipt) {
4225
+ throw new Error("No meta txn receipt found")
4226
+ }
4227
+ break
4228
+ }
3819
4229
 
3820
- logger.console.log(
3821
- "[trails-sdk] polling status",
3822
- metaTx.id as `0x${string}`,
3823
- metaTx.chainId.toString(),
3824
- )
3825
- const receipt: any = await getMetaTxStatus(
3826
- relayer,
3827
- metaTx.id,
3828
- Number(metaTx.chainId),
3829
- )
3830
- logger.console.log("[trails-sdk] status", receipt)
3831
- if (receipt?.transactionHash) {
3832
- originMetaTxnReceipt = receipt.data?.receipt
3833
- if (!originMetaTxnReceipt) {
3834
- throw new Error("No meta txn receipt found")
4230
+ // Check abort signal before waiting
4231
+ if (abortSignal?.aborted || pollingAbortController.signal.aborted) {
4232
+ logger.console.log(
4233
+ "[trails-sdk] Aborting meta transaction polling before delay",
4234
+ )
4235
+ return null
3835
4236
  }
3836
- break
4237
+
4238
+ // Use consistent delay and check abort signal during the delay
4239
+ await new Promise((resolve) => {
4240
+ const timeoutId = setTimeout(
4241
+ resolve,
4242
+ POLLING_INTERVALS.META_TRANSACTION,
4243
+ )
4244
+
4245
+ // Listen for abort signal during the delay
4246
+ const abortHandler = () => {
4247
+ clearTimeout(timeoutId)
4248
+ resolve(undefined)
4249
+ }
4250
+
4251
+ if (abortSignal) {
4252
+ abortSignal.addEventListener("abort", abortHandler, {
4253
+ once: true,
4254
+ })
4255
+ }
4256
+ pollingAbortController.signal.addEventListener(
4257
+ "abort",
4258
+ abortHandler,
4259
+ { once: true },
4260
+ )
4261
+ })
3837
4262
  }
3838
- await new Promise((resolve) => setTimeout(resolve, 1000))
3839
- }
3840
4263
 
3841
- return originMetaTxnReceipt
4264
+ return originMetaTxnReceipt
4265
+ } finally {
4266
+ // Always unregister when polling completes or is aborted
4267
+ abortControllerRegistry.unregister(pollingId)
4268
+ }
3842
4269
  },
3843
4270
  }
3844
4271
  }
@@ -3922,7 +4349,7 @@ async function checkAccountBalance({
3922
4349
  balanceError,
3923
4350
  }
3924
4351
  } catch (error) {
3925
- console.error("[trails-sdk] Error checking account balance:", error)
4352
+ logger.console.error("[trails-sdk] Error checking account balance:", error)
3926
4353
  return {
3927
4354
  hasEnoughBalance: false,
3928
4355
  balance: BigInt(0),
@@ -3999,329 +4426,18 @@ function getNeedsLifiNativeFee({
3999
4426
  return needsNativeFee
4000
4427
  }
4001
4428
 
4002
- export type UseQuoteProps = {
4003
- walletClient?: any // TODO: fix this, has to do with viem/wagmi versions
4004
- fromTokenAddress?: string | null
4005
- fromChainId?: number | null
4006
- toTokenAddress?: string | null
4007
- toChainId?: number | null
4008
- toCalldata?: string | null
4009
- swapAmount?: string | bigint
4010
- toRecipient?: string | null
4011
- tradeType?: TradeType | null
4012
- slippageTolerance?: string | number | null
4013
- onStatusUpdate?: ((transactionStates: TransactionState[]) => void) | null
4014
- quoteProvider?: string | null
4015
- gasless?: boolean
4016
- paymasterUrl?: string
4017
- }
4018
-
4019
- export type SwapReturn = {
4020
- originTransaction: {
4021
- transactionHash?: string | null
4022
- explorerUrl?: string | null
4023
- receipt: TransactionReceipt | MetaTxnReceipt | null
4024
- }
4025
- destinationTransaction: {
4026
- transactionHash?: string | null
4027
- explorerUrl?: string | null
4028
- receipt: MetaTxnReceipt | null
4029
- }
4030
- totalCompletionSeconds?: number
4031
- }
4032
-
4033
- export type QuoteProviderInfo = {
4034
- id: string
4035
- name: string
4036
- url: string
4037
- }
4038
-
4039
- export type Quote = {
4040
- fromAmount: string
4041
- fromAmountMin: string
4042
- toAmount: string
4043
- toAmountMin: string
4044
- originToken: SupportedToken
4045
- destinationToken: SupportedToken
4046
- originChain: Chain
4047
- destinationChain: Chain
4048
- fees: PrepareSendFees
4049
- slippageTolerance: string
4050
- priceImpact: string
4051
- completionEstimateSeconds: number
4052
- transactionStates?: TransactionState[]
4053
- originTokenRate?: string
4054
- quoteProvider?: QuoteProviderInfo | null
4055
- destinationTokenRate?: string
4056
- fromAmountUsdDisplay?: string
4057
- toAmountUsdDisplay?: string
4058
- gasCostUsd?: number
4059
- gasCostUsdDisplay?: string
4060
- }
4061
-
4062
- export type UseQuoteReturn = {
4063
- quote: Quote | null
4064
- swap: (() => Promise<SwapReturn | null>) | null
4065
- isLoadingQuote: boolean
4066
- quoteError: unknown
4067
- refetchQuote: () => void
4068
- }
4069
-
4070
- export function useQuote({
4071
- walletClient,
4072
- fromTokenAddress,
4073
- fromChainId,
4074
- toTokenAddress,
4075
- toChainId,
4076
- swapAmount,
4077
- tradeType,
4078
- toRecipient,
4079
- toCalldata,
4080
- slippageTolerance,
4081
- onStatusUpdate,
4082
- quoteProvider,
4083
- gasless,
4084
- paymasterUrl,
4085
- relayerEnv,
4086
- nodeGatewayEnv,
4087
- }: Partial<
4088
- UseQuoteProps & { relayerEnv?: RelayerEnv; nodeGatewayEnv?: SequenceEnv }
4089
- > = {}): UseQuoteReturn {
4090
- // Set node gateway environment override for this quote session
4091
- if (nodeGatewayEnv) {
4092
- ;(globalThis as any).__testNodeGatewayEnv = nodeGatewayEnv
4093
- }
4094
-
4095
- const apiClient = useAPIClient()
4096
- const trailsClient = useTrailsClient()
4097
- const { getRelayer } = useRelayers({
4098
- env: relayerEnv || (getSequenceEnv() as RelayerEnv),
4099
- })
4100
- const indexerGatewayClient = useIndexerGatewayClient()
4101
-
4102
- const { supportedTokens } = useSupportedTokens()
4103
-
4104
- const { data, isLoading, error, refetch } = useQuery({
4105
- queryKey: [
4106
- "quote",
4107
- fromTokenAddress,
4108
- fromChainId,
4109
- toTokenAddress,
4110
- toChainId,
4111
- swapAmount?.toString(),
4112
- toRecipient,
4113
- toCalldata,
4114
- tradeType,
4115
- slippageTolerance,
4116
- quoteProvider,
4117
- ],
4118
- queryFn: async () => {
4119
- try {
4120
- if (
4121
- !walletClient ||
4122
- !apiClient ||
4123
- !trailsClient ||
4124
- !fromTokenAddress ||
4125
- !toTokenAddress ||
4126
- !swapAmount ||
4127
- !toRecipient ||
4128
- !fromChainId ||
4129
- !toChainId ||
4130
- !indexerGatewayClient
4131
- ) {
4132
- return null
4133
- }
4134
-
4135
- // Get token balance using async method
4136
- const { balances } = await getTokenBalancesWithPrices({
4137
- account: walletClient.account!.address,
4138
- indexerGatewayClient,
4139
- apiClient,
4140
- })
4141
-
4142
- const originTokenBalance = balances.find(
4143
- (b) =>
4144
- b.chainId === fromChainId &&
4145
- (b.contractAddress?.toLowerCase() ===
4146
- fromTokenAddress.toLowerCase() ||
4147
- (!b.contractAddress && fromTokenAddress === zeroAddress)),
4148
- )
4149
-
4150
- const originTokenBalanceAmount = originTokenBalance?.balance ?? "0"
4151
- const destinationRelayer = getRelayer(toChainId)
4152
- const originRelayer = getRelayer(fromChainId)
4153
-
4154
- // Note: Disable this check for now to allow fetching a quote even when the origin balance is zero
4155
- // if (originTokenBalanceAmount === "0") {
4156
- // return null
4157
- // }
4158
-
4159
- // logger.console.log("supportedTokens", supportedTokens)
4160
-
4161
- const originToken = supportedTokens?.find(
4162
- (token) =>
4163
- token.contractAddress?.toLowerCase() ===
4164
- fromTokenAddress?.toLowerCase() && token.chainId === fromChainId,
4165
- )
4166
- const destinationToken = supportedTokens?.find(
4167
- (token) =>
4168
- token.contractAddress?.toLowerCase() ===
4169
- toTokenAddress?.toLowerCase() && token.chainId === toChainId,
4170
- )
4171
-
4172
- const sourceTokenDecimals = originToken?.decimals
4173
- if (!sourceTokenDecimals) {
4174
- logger.console.error(
4175
- "[trails-sdk] [useQuote] Missing source token decimals:",
4176
- {
4177
- originToken,
4178
- fromTokenAddress,
4179
- fromChainId,
4180
- },
4181
- )
4182
- throw new Error("Source token decimals not found")
4183
- }
4184
- const destinationTokenDecimals = destinationToken?.decimals
4185
- if (!destinationTokenDecimals) {
4186
- logger.console.error(
4187
- "[trails-sdk] Missing destination token decimals:",
4188
- {
4189
- destinationToken,
4190
- toTokenAddress,
4191
- toChainId,
4192
- },
4193
- )
4194
- throw new Error("Destination token decimals not found")
4195
- }
4196
- const destinationTokenSymbol = destinationToken?.symbol ?? ""
4197
- const originTokenSymbol = originToken?.symbol ?? ""
4198
-
4199
- const options = {
4200
- account: walletClient.account!,
4201
- originTokenAddress: fromTokenAddress,
4202
- originChainId: fromChainId,
4203
- originTokenBalance: originTokenBalanceAmount,
4204
- destinationChainId: toChainId,
4205
- recipient: toRecipient,
4206
- destinationTokenAddress: toTokenAddress,
4207
- swapAmount: swapAmount.toString(),
4208
- tradeType: tradeType ?? TradeType.EXACT_OUTPUT,
4209
- originTokenSymbol: originTokenSymbol,
4210
- destinationTokenSymbol: destinationTokenSymbol,
4211
- destinationCalldata: toCalldata as string,
4212
- client: walletClient,
4213
- apiClient,
4214
- trailsClient,
4215
- originRelayer,
4216
- destinationRelayer,
4217
- sourceTokenDecimals,
4218
- destinationTokenDecimals,
4219
- fee: "0",
4220
- dryMode: false,
4221
- onTransactionStateChange: onStatusUpdate ?? (() => {}),
4222
- slippageTolerance: slippageTolerance?.toString(),
4223
- quoteProvider: quoteProvider,
4224
- gasless: gasless ?? false,
4225
- paymasterUrl: paymasterUrl,
4226
- }
4227
-
4228
- logger.console.log("[trails-sdk] options", options)
4229
-
4230
- const { quote: prepareSendQuote, send } = await prepareSend(options)
4231
-
4232
- const quote = {
4233
- fromAmount: prepareSendQuote.originAmount,
4234
- toAmount: prepareSendQuote.destinationAmount,
4235
- fromAmountMin: prepareSendQuote.originAmountMin,
4236
- toAmountMin: prepareSendQuote.destinationAmountMin,
4237
- originToken: prepareSendQuote.originToken,
4238
- destinationToken: prepareSendQuote.destinationToken,
4239
- originChain: prepareSendQuote.originChain,
4240
- destinationChain: prepareSendQuote.destinationChain,
4241
- fees: prepareSendQuote.fees,
4242
- priceImpact: prepareSendQuote.priceImpact,
4243
- completionEstimateSeconds: prepareSendQuote.completionEstimateSeconds,
4244
- slippageTolerance: prepareSendQuote.slippageTolerance,
4245
- transactionStates: prepareSendQuote.transactionStates,
4246
- originTokenRate: prepareSendQuote.originTokenRate,
4247
- destinationTokenRate: prepareSendQuote.destinationTokenRate,
4248
- quoteProvider: prepareSendQuote.quoteProvider,
4249
- fromAmountUsdDisplay:
4250
- prepareSendQuote.originAmountUsdDisplay ?? undefined,
4251
- toAmountUsdDisplay:
4252
- prepareSendQuote.destinationAmountUsdDisplay ?? undefined,
4253
- gasCostUsd: prepareSendQuote.gasCostUsd ?? undefined,
4254
- gasCostUsdDisplay: prepareSendQuote.gasCostUsdDisplay ?? undefined,
4255
- }
4256
-
4257
- const swap = async (): Promise<SwapReturn> => {
4258
- const {
4259
- originUserTxReceipt,
4260
- destinationMetaTxnReceipt,
4261
- totalCompletionSeconds,
4262
- } = await send({})
4263
-
4264
- return {
4265
- originTransaction: {
4266
- transactionHash: originUserTxReceipt?.transactionHash,
4267
- explorerUrl: getExplorerUrl({
4268
- txHash: originUserTxReceipt?.transactionHash as string,
4269
- chainId: fromChainId,
4270
- }),
4271
- receipt: originUserTxReceipt,
4272
- },
4273
- destinationTransaction: {
4274
- transactionHash: destinationMetaTxnReceipt?.txnHash,
4275
- explorerUrl: getExplorerUrl({
4276
- txHash: destinationMetaTxnReceipt?.txnHash as string,
4277
- chainId: toChainId,
4278
- }),
4279
- receipt: destinationMetaTxnReceipt,
4280
- },
4281
- totalCompletionSeconds,
4282
- }
4283
- }
4284
-
4285
- return {
4286
- quote,
4287
- swap,
4288
- }
4289
- } catch (error) {
4290
- logger.console.error(
4291
- "[trails-sdk] [useQuote] Error getting quote:",
4292
- error,
4293
- )
4294
- throw getFullErrorMessage(error)
4295
- }
4296
- },
4297
- // Prevent unnecessary refetching
4298
- enabled: Boolean(
4299
- walletClient &&
4300
- apiClient &&
4301
- fromTokenAddress &&
4302
- toTokenAddress &&
4303
- swapAmount &&
4304
- toRecipient &&
4305
- fromChainId &&
4306
- toChainId &&
4307
- indexerGatewayClient,
4308
- ),
4309
- staleTime: 30 * 1000, // Consider data fresh for 30 seconds
4310
- refetchOnWindowFocus: false, // Don't refetch when window regains focus
4311
- refetchOnMount: false, // Don't refetch on component remount if data exists
4312
- refetchInterval: false, // Disable automatic polling
4313
- retry: 2, // Limit retry attempts
4314
- refetchOnReconnect: true, // Refetch when network reconnects
4315
- })
4429
+ // Re-export useQuote types and hook from the new location
4430
+ export type {
4431
+ UseQuoteProps,
4432
+ Quote,
4433
+ UseQuoteReturn,
4434
+ SwapReturn,
4435
+ QuoteProviderInfo,
4436
+ } from "./widget/hooks/useQuote.js"
4437
+ export { useQuote } from "./widget/hooks/useQuote.js"
4316
4438
 
4317
- return {
4318
- quote: data?.quote || null,
4319
- swap: data?.swap || null,
4320
- isLoadingQuote: isLoading,
4321
- quoteError: error,
4322
- refetchQuote: () => refetch(),
4323
- }
4324
- }
4439
+ // Import QuoteProviderInfo for use in this file
4440
+ import type { QuoteProviderInfo } from "./widget/hooks/useQuote.js"
4325
4441
 
4326
4442
  export function getFeesFromIntent(
4327
4443
  intent: GetIntentCallsPayloadsReturn,
@@ -4447,6 +4563,8 @@ export async function getNormalizedQuoteObject({
4447
4563
  originNativeTokenPriceUsd,
4448
4564
  quoteProvider,
4449
4565
  noSufficientBalance,
4566
+ estimatedGasLimit,
4567
+ intent,
4450
4568
  }: {
4451
4569
  originDepositAddress?: string
4452
4570
  destinationDepositAddress?: string
@@ -4470,6 +4588,8 @@ export async function getNormalizedQuoteObject({
4470
4588
  originNativeTokenPriceUsd?: number | null
4471
4589
  quoteProvider?: string
4472
4590
  noSufficientBalance?: boolean
4591
+ estimatedGasLimit?: bigint
4592
+ intent?: GetIntentCallsPayloadsReturn
4473
4593
  }): Promise<PrepareSendQuote> {
4474
4594
  if (!destinationChainId) {
4475
4595
  throw new Error("Destination chain id is required")
@@ -4497,7 +4617,7 @@ export async function getNormalizedQuoteObject({
4497
4617
  const destinationChain = getChainInfo(destinationChainId)
4498
4618
 
4499
4619
  if (!originToken || !destinationToken || !originChain || !destinationChain) {
4500
- console.error("[trails-sdk] Token or chain not found", {
4620
+ logger.console.error("[trails-sdk] Token or chain not found", {
4501
4621
  originToken,
4502
4622
  destinationToken,
4503
4623
  originChain,
@@ -4571,17 +4691,52 @@ export async function getNormalizedQuoteObject({
4571
4691
 
4572
4692
  let gasCostUsd: number = 0
4573
4693
  let gasCostUsdDisplay: string = "0"
4694
+ let gasCost: string = "0"
4695
+ let gasCostFormatted: string = "0"
4574
4696
  try {
4575
4697
  if (originNativeTokenPriceUsd) {
4698
+ // Use the actual estimated gas limit if provided, otherwise use default
4699
+ // This ensures the quote gas cost matches what will be used in the actual transaction
4700
+ const gasLimitForCost = estimatedGasLimit || DEFAULT_MIN_GASLIMIT
4701
+
4702
+ logger.console.log(
4703
+ "[trails-sdk][gas-estimation] Calculating gas cost for quote with gasLimit:",
4704
+ gasLimitForCost,
4705
+ "(estimated:",
4706
+ estimatedGasLimit,
4707
+ "default:",
4708
+ DEFAULT_MIN_GASLIMIT,
4709
+ ")",
4710
+ )
4711
+
4712
+ const gasCostWei = await estimateGasCost(publicClient, gasLimitForCost)
4713
+ gasCost = gasCostWei.toString()
4714
+ gasCostFormatted = formatUnits(gasCostWei, 18) // Native tokens always use 18 decimals
4715
+
4716
+ // Calculate USD value
4576
4717
  gasCostUsd = await estimateGasCostUsd(
4577
4718
  publicClient,
4578
4719
  originNativeTokenPriceUsd,
4579
- 200_000n,
4720
+ gasLimitForCost,
4580
4721
  )
4581
4722
  gasCostUsdDisplay = formatUsdAmountDisplay(gasCostUsd)
4723
+
4724
+ logger.console.log(
4725
+ "[trails-sdk][gas-estimation] Quote gas cost calculated:",
4726
+ {
4727
+ gasCostWei,
4728
+ gasCost,
4729
+ gasCostFormatted,
4730
+ gasCostUsd,
4731
+ gasCostUsdDisplay,
4732
+ },
4733
+ )
4582
4734
  }
4583
4735
  } catch (error) {
4584
- console.error("[trails-sdk] Error estimating gas cost", error)
4736
+ logger.console.error(
4737
+ "[trails-sdk][gas-estimation] Error estimating gas cost for quote display",
4738
+ error,
4739
+ )
4585
4740
  }
4586
4741
 
4587
4742
  // Calculate exchange rates
@@ -4623,6 +4778,12 @@ export async function getNormalizedQuoteObject({
4623
4778
 
4624
4779
  const priceImpactUsdDisplay = formatUsdAmountDisplay(priceImpactUsd)
4625
4780
 
4781
+ // Extract fee breakdown from intent if available
4782
+ let trailsFeeBreakdown = null
4783
+ if (intent) {
4784
+ trailsFeeBreakdown = await extractTrailsFeeBreakdown(intent)
4785
+ }
4786
+
4626
4787
  return {
4627
4788
  originDepositAddress: originDepositAddress || "",
4628
4789
  destinationDepositAddress: destinationDepositAddress || "",
@@ -4657,6 +4818,8 @@ export async function getNormalizedQuoteObject({
4657
4818
  transactionStates: transactionStates || [],
4658
4819
  gasCostUsd,
4659
4820
  gasCostUsdDisplay,
4821
+ gasCost,
4822
+ gasCostFormatted,
4660
4823
  originTokenRate: exchangeRates.originTokenRate,
4661
4824
  destinationTokenRate: exchangeRates.destinationTokenRate,
4662
4825
  originAmountDisplay: formatAmountDisplay(originAmountFormatted),
@@ -4667,6 +4830,7 @@ export async function getNormalizedQuoteObject({
4667
4830
  ),
4668
4831
  quoteProvider: quoteProviderInfo,
4669
4832
  noSufficientBalance: noSufficientBalance || false,
4833
+ trailsFeeBreakdown,
4670
4834
  }
4671
4835
  }
4672
4836