0xtrails 0.2.6 → 0.4.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 (218) hide show
  1. package/dist/aave.d.ts +2 -0
  2. package/dist/aave.d.ts.map +1 -1
  3. package/dist/{ccip-Xjh9d1gb.js → ccip-BpQGQiWq.js} +7 -7
  4. package/dist/config.d.ts +0 -5
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/constants.d.ts +2 -4
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/error.d.ts +4 -1
  9. package/dist/error.d.ts.map +1 -1
  10. package/dist/fees.d.ts +2 -2
  11. package/dist/fees.d.ts.map +1 -1
  12. package/dist/{index-BnhdZ8Ho.js → index-DsJM5F-V.js} +46084 -48697
  13. package/dist/index.d.ts +9 -8
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +741 -923
  16. package/dist/intentReceiptMonitor.d.ts +24 -0
  17. package/dist/intentReceiptMonitor.d.ts.map +1 -0
  18. package/dist/intentReceiptPoller.d.ts +69 -0
  19. package/dist/intentReceiptPoller.d.ts.map +1 -0
  20. package/dist/intents.d.ts +15 -11
  21. package/dist/intents.d.ts.map +1 -1
  22. package/dist/morpho.d.ts +6 -5
  23. package/dist/morpho.d.ts.map +1 -1
  24. package/dist/mutations.d.ts +16 -0
  25. package/dist/mutations.d.ts.map +1 -0
  26. package/dist/preconditions.d.ts +5 -4
  27. package/dist/preconditions.d.ts.map +1 -1
  28. package/dist/prepareSend.d.ts +5 -190
  29. package/dist/prepareSend.d.ts.map +1 -1
  30. package/dist/prices.d.ts +9 -6
  31. package/dist/prices.d.ts.map +1 -1
  32. package/dist/sequenceWallet.d.ts +3 -16
  33. package/dist/sequenceWallet.d.ts.map +1 -1
  34. package/dist/tokenBalances.d.ts +17 -13
  35. package/dist/tokenBalances.d.ts.map +1 -1
  36. package/dist/trails.d.ts +24 -40
  37. package/dist/trails.d.ts.map +1 -1
  38. package/dist/trailsClient.d.ts +5 -6
  39. package/dist/trailsClient.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 +2 -1
  85. package/dist/transactions.d.ts.map +1 -1
  86. package/dist/widget/components/AccountSettings.d.ts.map +1 -1
  87. package/dist/widget/components/ClassicSwap.d.ts +0 -1
  88. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  89. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  90. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -1
  91. package/dist/widget/components/DynamicSizeInputField.d.ts +13 -0
  92. package/dist/widget/components/DynamicSizeInputField.d.ts.map +1 -0
  93. package/dist/widget/components/Earn.d.ts +0 -1
  94. package/dist/widget/components/Earn.d.ts.map +1 -1
  95. package/dist/widget/components/FeeOptions.d.ts +5 -13
  96. package/dist/widget/components/FeeOptions.d.ts.map +1 -1
  97. package/dist/widget/components/Fund.d.ts +0 -1
  98. package/dist/widget/components/Fund.d.ts.map +1 -1
  99. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  100. package/dist/widget/components/FundSwap.d.ts +0 -1
  101. package/dist/widget/components/FundSwap.d.ts.map +1 -1
  102. package/dist/widget/components/Pay.d.ts +0 -1
  103. package/dist/widget/components/Pay.d.ts.map +1 -1
  104. package/dist/widget/components/PoolDeposit.d.ts +0 -1
  105. package/dist/widget/components/PoolDeposit.d.ts.map +1 -1
  106. package/dist/widget/components/PoolWithdraw.d.ts +0 -18
  107. package/dist/widget/components/PoolWithdraw.d.ts.map +1 -1
  108. package/dist/widget/components/QuoteDetails.d.ts +1 -0
  109. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  110. package/dist/widget/components/Swap.d.ts +0 -1
  111. package/dist/widget/components/Swap.d.ts.map +1 -1
  112. package/dist/widget/components/TransferPendingVertical.d.ts.map +1 -1
  113. package/dist/widget/components/WaasFeeOptions.d.ts.map +1 -1
  114. package/dist/widget/css/compiled.css +2 -2
  115. package/dist/widget/hooks/useCheckout.d.ts +17 -4
  116. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  117. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -1
  118. package/dist/widget/hooks/useQuote.d.ts +3 -4
  119. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  120. package/dist/widget/hooks/useSelectedFeeToken.d.ts +1 -0
  121. package/dist/widget/hooks/useSelectedFeeToken.d.ts.map +1 -1
  122. package/dist/widget/hooks/useSendForm.d.ts +3 -4
  123. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  124. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  125. package/dist/widget/hooks/useWalletConnectionContext.d.ts +25 -0
  126. package/dist/widget/hooks/useWalletConnectionContext.d.ts.map +1 -0
  127. package/dist/widget/index.js +1 -1
  128. package/dist/widget/widget.d.ts +12 -7
  129. package/dist/widget/widget.d.ts.map +1 -1
  130. package/package.json +21 -23
  131. package/src/aave.ts +54 -1
  132. package/src/config.ts +57 -58
  133. package/src/constants.ts +8 -9
  134. package/src/error.ts +21 -3
  135. package/src/fees.ts +53 -42
  136. package/src/index.ts +35 -13
  137. package/src/intentReceiptMonitor.ts +102 -0
  138. package/src/intentReceiptPoller.ts +299 -0
  139. package/src/intents.ts +206 -172
  140. package/src/morpho.ts +58 -9
  141. package/src/mutations.ts +129 -0
  142. package/src/preconditions.ts +16 -21
  143. package/src/prepareSend.ts +80 -4514
  144. package/src/prices.ts +26 -22
  145. package/src/relaySdk.ts +2 -2
  146. package/src/sequenceWallet.ts +6 -73
  147. package/src/tokenBalances.ts +175 -69
  148. package/src/trails.ts +230 -722
  149. package/src/trailsClient.ts +10 -23
  150. package/src/transactionIntent/constants.ts +11 -0
  151. package/src/transactionIntent/deposits/depositOrchestrator.ts +210 -0
  152. package/src/transactionIntent/deposits/gaslessDeposit.ts +588 -0
  153. package/src/transactionIntent/deposits/index.ts +3 -0
  154. package/src/transactionIntent/deposits/standardDeposit.ts +379 -0
  155. package/src/transactionIntent/execution/index.ts +1 -0
  156. package/src/transactionIntent/execution/transactionState.ts +35 -0
  157. package/src/transactionIntent/handlers/crossChain.ts +1707 -0
  158. package/src/transactionIntent/handlers/index.ts +3 -0
  159. package/src/transactionIntent/handlers/sameChainDifferentToken.ts +323 -0
  160. package/src/transactionIntent/handlers/sameChainSameToken.ts +712 -0
  161. package/src/transactionIntent/index.ts +9 -0
  162. package/src/transactionIntent/quote/feeExtractors.ts +81 -0
  163. package/src/transactionIntent/quote/index.ts +3 -0
  164. package/src/transactionIntent/quote/normalizeQuote.ts +367 -0
  165. package/src/transactionIntent/quote/quoteHelpers.ts +53 -0
  166. package/src/transactionIntent/types.ts +157 -0
  167. package/src/transactionIntent/utils/balanceChecker.ts +96 -0
  168. package/src/transactionIntent/utils/index.ts +3 -0
  169. package/src/transactionIntent/utils/lifiHelpers.ts +68 -0
  170. package/src/transactionIntent/utils/testnetHelpers.ts +10 -0
  171. package/src/transactionIntent/validators.ts +57 -0
  172. package/src/transactions.ts +36 -53
  173. package/src/widget/compiled.css +2 -2
  174. package/src/widget/components/AccountIntentTransactionHistory.tsx +36 -36
  175. package/src/widget/components/AccountSettings.tsx +23 -6
  176. package/src/widget/components/ClassicSwap.tsx +28 -53
  177. package/src/widget/components/ConfigDisplay.tsx +0 -11
  178. package/src/widget/components/ConnectedWallets.tsx +30 -4
  179. package/src/widget/components/DynamicSizeInputField.tsx +109 -0
  180. package/src/widget/components/Earn.tsx +0 -16
  181. package/src/widget/components/FeeBreakdown.tsx +3 -3
  182. package/src/widget/components/FeeOption.tsx +2 -2
  183. package/src/widget/components/FeeOptions.tsx +151 -112
  184. package/src/widget/components/Fund.tsx +0 -3
  185. package/src/widget/components/FundMethods.tsx +4 -3
  186. package/src/widget/components/FundSwap.tsx +0 -1
  187. package/src/widget/components/Pay.tsx +11 -16
  188. package/src/widget/components/PoolDeposit.tsx +35 -32
  189. package/src/widget/components/PoolWithdraw.tsx +153 -256
  190. package/src/widget/components/QuoteDetails.tsx +899 -494
  191. package/src/widget/components/Swap.tsx +0 -1
  192. package/src/widget/components/TransferPendingVertical.tsx +12 -8
  193. package/src/widget/components/WaasFeeOptions.tsx +23 -7
  194. package/src/widget/components/WalletConfirmation.tsx +1 -1
  195. package/src/widget/hooks/useAmountUsd.ts +9 -9
  196. package/src/widget/hooks/useCheckout.ts +97 -9
  197. package/src/widget/hooks/useDefaultTokenSelection.tsx +27 -21
  198. package/src/widget/hooks/useQuote.ts +86 -33
  199. package/src/widget/hooks/useSelectedFeeToken.tsx +32 -37
  200. package/src/widget/hooks/useSendForm.ts +37 -47
  201. package/src/widget/hooks/useTokenList.ts +34 -26
  202. package/src/widget/hooks/useWalletConnectionContext.tsx +128 -0
  203. package/src/widget/widget.tsx +197 -207
  204. package/dist/apiClient.d.ts +0 -9
  205. package/dist/apiClient.d.ts.map +0 -1
  206. package/dist/intentEntrypoint.d.ts +0 -114
  207. package/dist/intentEntrypoint.d.ts.map +0 -1
  208. package/dist/metaTxnMonitor.d.ts +0 -15
  209. package/dist/metaTxnMonitor.d.ts.map +0 -1
  210. package/dist/metaTxns.d.ts +0 -11
  211. package/dist/metaTxns.d.ts.map +0 -1
  212. package/dist/relayer.d.ts +0 -43
  213. package/dist/relayer.d.ts.map +0 -1
  214. package/src/apiClient.ts +0 -35
  215. package/src/intentEntrypoint.ts +0 -203
  216. package/src/metaTxnMonitor.ts +0 -171
  217. package/src/metaTxns.ts +0 -45
  218. package/src/relayer.ts +0 -289
@@ -1,367 +1,47 @@
1
- import type {
2
- GetIntentCallsPayloadParams,
3
- IntentPrecondition,
4
- } from "@0xsequence/trails-api"
5
- import type { GetIntentCallsPayloadsReturn } from "./intents.js"
6
- import type { TrailsAPIClient } from "@0xsequence/trails-api"
7
- import type { Relayer } from "@0xsequence/relayer"
8
- import type {
9
- Account,
10
- Chain,
11
- PublicClient,
12
- TransactionReceipt,
13
- WalletClient,
14
- } from "viem"
15
1
  import { abortControllerRegistry } from "./abortController.js"
16
- import { extractTrailsFeeBreakdown, type TrailsFeeBreakdown } from "./fees.js"
17
- import {
18
- createPublicClient,
19
- createWalletClient,
20
- decodeFunctionData,
21
- erc20Abi,
22
- formatUnits,
23
- http,
24
- maxUint256,
25
- parseUnits,
26
- zeroAddress,
27
- } from "viem"
28
- import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"
29
- import { mainnet } from "viem/chains"
30
- import {
31
- trackPaymentCompleted,
32
- trackPaymentError,
33
- trackPaymentStarted,
34
- trackRelayerCallCompleted,
35
- trackRelayerCallError,
36
- trackRelayerCallStarted,
37
- trackTransactionConfirmed,
38
- } from "./analytics.js"
39
- import type { Attestation } from "./cctp.js"
40
- import {
41
- approveERC20,
42
- cctpTransfer,
43
- cctpTransferWithCustomCall,
44
- getCCTPRelayerCallData,
45
- getIsUsdcAddress,
46
- getMessageTransmitter,
47
- getMintUSDCData,
48
- getNeedsApproval,
49
- getUSDCTokenAddress,
50
- } from "./cctp.js"
51
- import { queueCCTPTransfer } from "./cctpqueue.js"
52
- import { getChainInfo, getTestnetChainInfo } from "./chains.js"
53
- import { attemptSwitchChain } from "./chainSwitch.js"
2
+ import { createPublicClient, http, zeroAddress } from "viem"
3
+ import { trackPaymentError, trackPaymentStarted } from "./analytics.js"
54
4
  import { getSlippageTolerance } from "./config.js"
55
- import { TRAILS_INTENT_ENTRYPOINT_ADDRESS } from "./constants.js"
56
- import {
57
- decodeGuestModuleEvents,
58
- decodeTrailsTokenSweeperEvents,
59
- } from "./decoders.js"
60
5
  import { getERC20TransferData } from "./encoders.js"
61
- import { InsufficientBalanceError } from "./error.js"
62
- import {
63
- estimateGasCost,
64
- estimateGasCostUsd,
65
- estimateGasLimit,
66
- DEFAULT_MIN_GASLIMIT,
67
- } from "./estimate.js"
68
- import { getExplorerUrl } from "./explorer.js"
69
- import {
70
- getNeedsIntentEntrypointApproval,
71
- getPermitSignature,
72
- getUserNonce,
73
- signIntent,
74
- } from "./gasless.js"
75
- import { getIntentEntrypointFeeOptions } from "./intentEntrypoint.js"
76
- import {
77
- buildSameChainTransactionParams,
78
- buildCrossChainDepositParams,
79
- commitIntentConfig,
80
- getIntentCallsPayloads as getIntentCallsPayloadsFromIntents,
81
- sendOriginTransaction,
82
- } from "./intents.js"
83
- import type { IntentRequestParams } from "./intents.js"
84
- import type { MetaTxn } from "./metaTxnMonitor.js"
85
- import { getMetaTxStatus } from "./metaTxnMonitor.js"
86
- import { relayerSendMetaTx } from "./metaTxns.js"
87
- import { findFirstPreconditionForChainId } from "./preconditions.js"
88
- import { calcAmountUsdPrice, getTokenPrice } from "./prices.js"
89
- import { getQueryParam } from "./queryParams.js"
90
- import type { MetaTxnReceipt } from "./relayer.js"
91
- import {
92
- executeSimpleRelayTransaction,
93
- getRelaySDKQuote,
94
- getTxHashFromRelayResult,
95
- waitForRelayDestinationTx,
96
- type RelayQuote,
97
- type RelayTradeType,
98
- } from "./relaySdk.js"
99
- import {
100
- sequenceSendTransaction,
101
- simpleCreateSequenceWallet,
102
- } from "./sequenceWallet.js"
103
- import {
104
- formatAmount,
105
- formatAmountDisplay,
106
- formatRawAmount,
107
- formatUsdAmountDisplay,
108
- } from "./tokenBalances.js"
109
- import { getTokenInfo, type SupportedToken } from "./tokens.js"
6
+ import { getTokenPrice } from "./prices.js"
110
7
  import {
111
8
  TRAILS_ROUTER_PLACEHOLDER_AMOUNT,
112
9
  wrapCalldataWithTrailsRouterIfNeeded,
113
10
  } from "./trailsRouter.js"
114
- import type {
115
- TransactionState,
116
- TransactionStateStatus,
117
- } from "./transactions.js"
118
- import { getAccountTransactionHistory, getTxTimeDiff } from "./transactions.js"
119
- import { requestWithTimeout } from "./utils.js"
120
- import type { CheckoutOnHandlers } from "./widget/hooks/useCheckout.js"
11
+ import type { TransactionState } from "./transactions.js"
121
12
  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
-
135
13
  import { getIsCustomCalldata } from "./contractUtils.js"
136
- import type { SequenceAPIClient } from "@0xsequence/api"
137
- import { updatePersistentToast } from "./toast.js"
138
- import {
139
- getDelegatorSmartAccount,
140
- getPaymasterGaslessTransaction,
141
- sendPaymasterGaslessTransaction,
142
- } from "./paymasterSend.js"
143
-
144
- export enum TradeType {
145
- EXACT_INPUT = "EXACT_INPUT",
146
- EXACT_OUTPUT = "EXACT_OUTPUT",
147
- }
148
-
149
- export type PrepareSendOptions = {
150
- account: Account
151
- originTokenAddress: string
152
- originChainId: number
153
- originTokenBalance: string
154
- destinationChainId: number
155
- recipient: string
156
- destinationTokenAddress: string
157
- swapAmount: string
158
- tradeType?: TradeType
159
- originTokenSymbol: string
160
- destinationTokenSymbol: string
161
- sequenceProjectAccessKey?: string
162
- fee: string
163
- client?: WalletClient
164
- dryMode: boolean
165
- apiClient: SequenceAPIClient
166
- trailsClient: TrailsAPIClient
167
- originRelayer: Relayer.RpcRelayer
168
- destinationRelayer: Relayer.RpcRelayer
169
- destinationCalldata?: string
170
- onTransactionStateChange: (transactionStates: TransactionState[]) => void
171
- sourceTokenPriceUsd?: number | null
172
- destinationTokenPriceUsd?: number | null
173
- sourceTokenDecimals: number
174
- destinationTokenDecimals: number
175
- paymasterUrl?: string
176
- gasless?: boolean
177
- slippageTolerance?: string
178
- originNativeTokenPriceUsd?: number | null
179
- quoteProvider?: string | null
180
- fundMethod?: string
181
- mode?: "pay" | "fund" | "earn" | "swap" | "receive"
182
- checkoutOnHandlers?: CheckoutOnHandlers
183
- refundAddress?: string
184
- selectedFeeToken?: any
185
- walletId?: string
186
- abortSignal?: AbortSignal
187
- }
188
-
189
- export type PrepareSendFees = {
190
- feeTokenAddress: string | null
191
- totalFeeAmount: string | null
192
- totalFeeAmountUsd: string | null
193
- totalFeeAmountUsdDisplay: string | null
194
- }
195
-
196
- export type PrepareSendQuote = {
197
- originDepositAddress: string
198
- destinationDepositAddress: string
199
- destinationAddress: string
200
- destinationCalldata: string
201
- originChain: Chain
202
- destinationChain: Chain
203
- originAmount: string
204
- originAmountDisplay: string
205
- originAmountMin: string
206
- originAmountMinDisplay: string
207
- originAmountMinUsdFormatted: string
208
- originAmountMinUsdDisplay: string
209
- destinationAmount: string
210
- destinationAmountDisplay: string
211
- destinationAmountMin: string
212
- destinationAmountMinDisplay: string
213
- destinationAmountMinUsdFormatted: string
214
- destinationAmountMinUsdDisplay: string
215
- originAmountFormatted: string
216
- originAmountUsdFormatted: string
217
- originAmountUsdDisplay: string
218
- destinationAmountFormatted: string
219
- destinationAmountUsdFormatted: string
220
- destinationAmountUsdDisplay: string
221
- originToken: SupportedToken
222
- destinationToken: SupportedToken
223
- fees: PrepareSendFees
224
- slippageTolerance: string
225
- priceImpact: string
226
- priceImpactUsdDisplay: string
227
- completionEstimateSeconds: number
228
- transactionStates: TransactionState[]
229
- gasCostUsd: number
230
- gasCostUsdDisplay: string
231
- gasCost: string
232
- gasCostFormatted: string
233
- originTokenRate: string
234
- destinationTokenRate: string
235
- quoteProvider: QuoteProviderInfo | null
236
- noSufficientBalance: boolean
237
- trailsFeeBreakdown?: TrailsFeeBreakdown | null
238
- }
239
-
240
- export type PrepareSendReturn = {
241
- quote: PrepareSendQuote
242
- feeOptions?: any
243
- send: ({
244
- onOriginSend,
245
- selectedFeeToken,
246
- }: {
247
- onOriginSend?: () => void
248
- selectedFeeToken?: any
249
- }) => Promise<SendReturn>
250
- }
251
-
252
- export type SendReturn = {
253
- originUserTxReceipt: TransactionReceipt | null
254
- originMetaTxnReceipt: MetaTxnReceipt | null
255
- destinationMetaTxnReceipt: MetaTxnReceipt | null
256
- totalCompletionSeconds?: number
257
- }
258
-
259
- export function getIsToSameChain(
260
- originChainId: number,
261
- destinationChainId: number,
262
- ): boolean {
263
- return originChainId?.toString() === destinationChainId?.toString()
264
- }
265
-
266
- export function getIsToSameToken(
267
- originTokenAddress: string,
268
- destinationTokenAddress: string,
269
- ): boolean {
270
- return (
271
- originTokenAddress?.toLowerCase() === destinationTokenAddress?.toLowerCase()
272
- )
273
- }
274
-
275
- export function getIsToSameChainAndToken(
276
- originChainId: number,
277
- originTokenAddress: string,
278
- destinationChainId: number,
279
- destinationTokenAddress: string,
280
- ): boolean {
281
- return (
282
- getIsToSameChain(originChainId, destinationChainId) &&
283
- getIsToSameToken(originTokenAddress, destinationTokenAddress)
284
- )
285
- }
286
-
287
- export function getUseCctp(
288
- originTokenAddress: string,
289
- destinationTokenAddress: string,
290
- originChainId: number,
291
- destinationChainId: number,
292
- ) {
293
- return (
294
- getIsUsdcAddress(originTokenAddress, originChainId) &&
295
- getIsUsdcAddress(destinationTokenAddress, destinationChainId)
296
- )
297
- }
298
-
299
- export function validateCctpDestinationToken(
300
- destinationTokenAddress: string,
301
- destinationChainId: number,
302
- quoteProvider?: string | null,
303
- ): void {
304
- // If CCTP provider is selected, destination token must be USDC
305
- if (
306
- quoteProvider === "cctp" &&
307
- !getIsUsdcAddress(destinationTokenAddress, destinationChainId)
308
- ) {
309
- throw new Error(
310
- `CCTP provider requires destination token to be USDC. Current destination token: ${destinationTokenAddress} on chain ${destinationChainId}. Please ensure the destination token is USDC as listed in the Circle CCTP documentation.`,
311
- )
312
- }
313
- }
314
-
315
- function isTestnetDebugMode(): boolean {
316
- return getQueryParam("testnet") === "true"
317
- }
318
-
319
- function getTestnetOriginTokenAddress(testnetChainId: number): string {
320
- return getUSDCTokenAddress(testnetChainId)!
321
- }
322
-
323
- function getIntentArgs(
324
- mainSignerAddress: string,
325
- originChainId: number,
326
- originTokenAddress: string,
327
- originTokenAmount: string,
328
- destinationChainId: number,
329
- destinationTokenAddress: string,
330
- destinationTokenAmount: string,
331
- destinationTokenSymbol: string,
332
- recipient: string,
333
- destinationCalldata: string | undefined,
334
- destinationSalt: string = Date.now().toString(),
335
- slippageTolerance: string, // 0.03 = 3%
336
- tradeType: TradeType,
337
- provider?: string | null,
338
- ): GetIntentCallsPayloadParams {
339
- const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
340
-
341
- if (!provider || provider === "auto") {
342
- provider = undefined
343
- }
344
-
345
- const intentArgs = {
346
- userAddress: mainSignerAddress,
347
- originChainId,
348
- originTokenAddress,
349
- originTokenAmount: originTokenAmount, // max amount for exact_output
350
- destinationChainId,
351
- destinationToAddress: recipient,
352
- destinationTokenAddress: destinationTokenAddress,
353
- destinationTokenAmount: destinationTokenAmount,
354
- destinationTokenSymbol: destinationTokenSymbol,
355
- destinationCallData: hasCustomCalldata ? destinationCalldata : "0x",
356
- destinationCallValue: "0",
357
- destinationSalt,
358
- slippageTolerance: Number(slippageTolerance),
359
- tradeType,
360
- provider,
361
- }
14
+ import { getChainInfo } from "./chains.js"
362
15
 
363
- return intentArgs
364
- }
16
+ // Import from transactionIntent module
17
+ import {
18
+ TradeType,
19
+ type PrepareSendOptions,
20
+ type PrepareSendReturn,
21
+ handleSameChainSameToken,
22
+ handleCrossChain,
23
+ isSameChain,
24
+ isSameToken,
25
+ validateCctpDestinationToken,
26
+ } from "./transactionIntent/index.js"
27
+
28
+ // Re-export types for backward compatibility
29
+ export { TradeType } from "./transactionIntent/index.js"
30
+ export type {
31
+ PrepareSendOptions,
32
+ PrepareSendFees,
33
+ PrepareSendQuote,
34
+ PrepareSendReturn,
35
+ SendReturn,
36
+ SelectedFeeToken,
37
+ } from "./transactionIntent/index.js"
38
+
39
+ // Re-export validators for backward compatibility
40
+ export {
41
+ isSameChain,
42
+ isSameToken,
43
+ validateCctpDestinationToken,
44
+ } from "./transactionIntent/index.js"
365
45
 
366
46
  export async function prepareSend(
367
47
  options: PrepareSendOptions,
@@ -387,16 +67,12 @@ export async function prepareSend(
387
67
  fee,
388
68
  client: walletClient,
389
69
  dryMode = false,
390
- apiClient,
391
70
  trailsClient,
392
- originRelayer,
393
- destinationRelayer,
394
71
  destinationCalldata,
395
72
  onTransactionStateChange,
396
73
  sourceTokenDecimals,
397
74
  destinationTokenDecimals,
398
75
  paymasterUrl,
399
- gasless = false,
400
76
  slippageTolerance = getSlippageTolerance(),
401
77
  originNativeTokenPriceUsd,
402
78
  quoteProvider,
@@ -406,6 +82,8 @@ export async function prepareSend(
406
82
  selectedFeeToken,
407
83
  walletId,
408
84
  abortSignal,
85
+ commitIntentFn,
86
+ executeIntentFn,
409
87
  } = options
410
88
  let { sourceTokenPriceUsd, destinationTokenPriceUsd } = options
411
89
 
@@ -474,12 +152,12 @@ export async function prepareSend(
474
152
  originTokenAddress,
475
153
  originChainId,
476
154
  })
477
- const price = await getTokenPrice(apiClient, {
478
- tokenId: originTokenAddress,
479
- contractAddress: originTokenAddress,
155
+ const price = await getTokenPrice(trailsClient, {
156
+ tokenSymbol: originTokenAddress,
157
+ tokenAddress: originTokenAddress,
480
158
  chainId: originChainId,
481
159
  })
482
- sourceTokenPriceUsd = price?.price?.value ?? 0
160
+ sourceTokenPriceUsd = price?.priceUsd ?? 0
483
161
  logger.console.log(
484
162
  "[trails-sdk] source token price:",
485
163
  sourceTokenPriceUsd,
@@ -498,12 +176,12 @@ export async function prepareSend(
498
176
  destinationTokenAddress,
499
177
  destinationChainId,
500
178
  })
501
- const price = await getTokenPrice(apiClient, {
502
- tokenId: destinationTokenAddress,
503
- contractAddress: destinationTokenAddress,
179
+ const price = await getTokenPrice(trailsClient, {
180
+ tokenSymbol: destinationTokenAddress,
181
+ tokenAddress: destinationTokenAddress,
504
182
  chainId: destinationChainId,
505
183
  })
506
- destinationTokenPriceUsd = price?.price?.value ?? 0
184
+ destinationTokenPriceUsd = price?.priceUsd ?? 0
507
185
  logger.console.log(
508
186
  "[trails-sdk] destination token price:",
509
187
  destinationTokenPriceUsd,
@@ -566,11 +244,8 @@ export async function prepareSend(
566
244
  })
567
245
  throw new Error(`Chain ${originChainId} not found`)
568
246
  }
569
- const isToSameChain = getIsToSameChain(originChainId, destinationChainId)
570
- const isToSameToken = getIsToSameToken(
571
- originTokenAddress,
572
- destinationTokenAddress,
573
- )
247
+ const isToSameChain = isSameChain(originChainId, destinationChainId)
248
+ const isToSameToken = isSameToken(originTokenAddress, destinationTokenAddress)
574
249
 
575
250
  logger.console.log("[trails-sdk] isToSameChain", isToSameChain)
576
251
  logger.console.log("[trails-sdk] isToSameToken", isToSameToken)
@@ -656,15 +331,17 @@ export async function prepareSend(
656
331
  // }
657
332
 
658
333
  if (isToSameToken && isToSameChain) {
659
- return await sendHandlerForSameChainSameToken({
334
+ return await handleSameChainSameToken({
335
+ mainSignerAddress,
660
336
  originTokenAddress,
661
337
  originTokenDecimals: sourceTokenDecimals,
338
+ originTokenSymbol,
339
+ destinationTokenSymbol,
662
340
  swapAmount,
663
341
  destinationCalldata: effectiveDestinationCalldata,
664
342
  recipient: effectiveDestinationAddress,
665
343
  originChainId,
666
344
  walletClient,
667
- publicClient,
668
345
  onTransactionStateChange,
669
346
  dryMode,
670
347
  account,
@@ -673,20 +350,21 @@ export async function prepareSend(
673
350
  sourceTokenPriceUsd,
674
351
  destinationTokenPriceUsd,
675
352
  originNativeTokenPriceUsd,
676
- slippageTolerance,
353
+ slippageTolerance: slippageTolerance,
677
354
  checkoutOnHandlers,
678
- gasless,
679
355
  paymasterUrl,
680
356
  selectedFeeToken,
357
+ walletId,
681
358
  trailsClient,
682
- originRelayer,
683
359
  mode,
684
360
  fundMethod,
685
361
  abortSignal,
362
+ commitIntentFn,
363
+ executeIntentFn,
686
364
  })
687
365
  }
688
366
 
689
- return await sendHandlerForDifferentChainDifferentToken({
367
+ return await handleCrossChain({
690
368
  mainSignerAddress,
691
369
  originChainId,
692
370
  originTokenAddress,
@@ -698,16 +376,12 @@ export async function prepareSend(
698
376
  destinationTokenSymbol,
699
377
  recipient: effectiveDestinationAddress,
700
378
  destinationCalldata: effectiveDestinationCalldata,
701
- apiClient,
702
379
  trailsClient,
703
380
  sourceTokenPriceUsd,
704
381
  destinationTokenPriceUsd,
705
382
  sourceTokenDecimals,
706
383
  destinationTokenDecimals,
707
- gasless,
708
384
  paymasterUrl,
709
- originRelayer,
710
- destinationRelayer,
711
385
  walletClient,
712
386
  publicClient,
713
387
  chain,
@@ -726,4140 +400,32 @@ export async function prepareSend(
726
400
  selectedFeeToken,
727
401
  walletId,
728
402
  abortSignal,
403
+ commitIntentFn,
404
+ executeIntentFn,
729
405
  })
730
406
  }
731
407
 
732
- async function sendHandlerForDifferentChainDifferentToken({
733
- mainSignerAddress,
734
- originChainId,
735
- originTokenAddress,
736
- originTokenBalance,
737
- destinationChainId,
738
- destinationTokenAddress,
739
- swapAmount,
740
- originTokenSymbol,
741
- destinationTokenSymbol,
742
- recipient,
743
- destinationCalldata,
744
- trailsClient,
745
- sourceTokenPriceUsd,
746
- destinationTokenPriceUsd,
747
- sourceTokenDecimals,
748
- destinationTokenDecimals,
749
- gasless,
750
- paymasterUrl,
751
- originRelayer,
752
- destinationRelayer,
753
- walletClient,
754
- publicClient,
755
- chain,
756
- account,
757
- fee,
758
- dryMode,
759
- onTransactionStateChange,
760
- transactionStates,
761
- slippageTolerance,
762
- tradeType,
763
- originNativeTokenPriceUsd,
764
- quoteProvider,
765
- fundMethod,
766
- mode,
767
- checkoutOnHandlers,
768
- selectedFeeToken,
769
- walletId,
770
- abortSignal,
771
- }: {
772
- mainSignerAddress: string
773
- originChainId: number
774
- originTokenAddress: string
775
- originTokenBalance: string
776
- destinationChainId: number
777
- destinationTokenAddress: string
778
- swapAmount: string
779
- originTokenSymbol: string
780
- destinationTokenSymbol: string
781
- recipient: string
782
- destinationCalldata?: string
783
- apiClient: SequenceAPIClient
784
- trailsClient: TrailsAPIClient
785
- sourceTokenPriceUsd?: number | null
786
- destinationTokenPriceUsd?: number | null
787
- sourceTokenDecimals: number
788
- destinationTokenDecimals: number
789
- gasless: boolean
790
- paymasterUrl?: string
791
- originRelayer: Relayer.RpcRelayer
792
- destinationRelayer: Relayer.RpcRelayer
793
- walletClient: WalletClient
794
- publicClient: PublicClient
795
- chain: Chain
796
- account: Account
797
- fee: string
798
- dryMode: boolean
799
- onTransactionStateChange: (transactionStates: TransactionState[]) => void
800
- transactionStates: TransactionState[]
801
- slippageTolerance: string
802
- tradeType: TradeType
803
- originNativeTokenPriceUsd?: number | null
804
- quoteProvider?: string | null
805
- fundMethod?: string
806
- mode?: "pay" | "fund" | "earn" | "swap" | "receive"
807
- checkoutOnHandlers?: CheckoutOnHandlers
808
- selectedFeeToken?: any
809
- walletId?: string
810
- abortSignal?: AbortSignal
811
- }): Promise<PrepareSendReturn> {
812
- const testnet = isTestnetDebugMode()
813
- const useCctp = getUseCctp(
814
- originTokenAddress,
815
- destinationTokenAddress,
816
- originChainId,
817
- destinationChainId,
818
- )
819
-
820
- const cctpFlag = getQueryParam("cctp") === "true"
821
- const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
822
-
823
- // Validate CCTP destination token requirement for explicit CCTP usage
824
- if (useCctp && cctpFlag) {
825
- validateCctpDestinationToken(
826
- destinationTokenAddress,
827
- destinationChainId,
828
- "cctp",
829
- )
830
- logger.console.log("[trails-sdk] using cctp")
831
-
832
- const quote = await getNormalizedQuoteObject({
833
- destinationAddress: recipient,
834
- destinationCalldata,
835
- originAmount: swapAmount,
836
- originTokenPriceUsd: sourceTokenPriceUsd?.toString() || null,
837
- destinationAmount: swapAmount,
838
- destinationTokenPriceUsd: destinationTokenPriceUsd?.toString() || null,
839
- originTokenAddress: originTokenAddress,
840
- destinationTokenAddress: destinationTokenAddress,
841
- originChainId,
842
- destinationChainId,
843
- transactionStates,
844
- originNativeTokenPriceUsd,
845
- slippageTolerance,
846
- quoteProvider: "cctp",
847
- })
848
-
849
- // Call onCheckoutQuote callback if provided
850
- if (checkoutOnHandlers?.triggerCheckoutQuote) {
851
- checkoutOnHandlers.triggerCheckoutQuote(quote)
852
- }
853
-
854
- return {
855
- quote,
856
- send: async ({
857
- onOriginSend,
858
- selectedFeeToken: runtimeSelectedFeeToken,
859
- }: {
860
- onOriginSend?: () => void
861
- selectedFeeToken?: any
862
- }): Promise<SendReturn> => {
863
- // Use runtime selectedFeeToken if provided, otherwise fall back to the one from prepareSend
864
- const effectiveSelectedFeeToken =
865
- runtimeSelectedFeeToken ?? selectedFeeToken
866
- logger.console.log(
867
- "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] send() called (LEGACY PATH):",
868
- {
869
- runtimeSelectedFeeToken,
870
- prepareTimeSelectedFeeToken: selectedFeeToken,
871
- effectiveSelectedFeeToken,
872
- },
873
- )
874
- const originChain = testnet ? getTestnetChainInfo(chain)! : chain
875
- const destinationChain = testnet
876
- ? getTestnetChainInfo(destinationChainId)!
877
- : getChainInfo(destinationChainId)
878
-
879
- if (!originChain || !destinationChain) {
880
- logger.console.error("[trails-sdk] Invalid chain", {
881
- originChain,
882
- destinationChain,
883
- originChainId,
884
- destinationChainId,
885
- chain,
886
- testnet,
887
- })
888
- throw new Error("Invalid chain")
889
- }
890
-
891
- logger.console.log("[trails-sdk] originChain", originChain)
892
- logger.console.log("[trails-sdk] destinationChain", destinationChain)
893
-
894
- const originPublicClient = createPublicClient({
895
- chain: originChain,
896
- transport: http(),
897
- })
898
-
899
- let txHash: `0x${string}`
900
- let waitForAttestation: () => Promise<Attestation>
901
-
902
- if (hasCustomCalldata) {
903
- const result = await cctpTransferWithCustomCall({
904
- walletClient,
905
- originChain,
906
- destinationChain,
907
- amount: BigInt(swapAmount),
908
- })
909
-
910
- txHash = result.txHash
911
- waitForAttestation = result.waitForAttestation
912
- } else {
913
- const result = await cctpTransfer({
914
- walletClient,
915
- originChain,
916
- destinationChain,
917
- amount: BigInt(swapAmount),
918
- })
919
-
920
- txHash = result.txHash
921
- waitForAttestation = result.waitForAttestation
922
- }
923
-
924
- if (onOriginSend) {
925
- onOriginSend()
926
- }
927
-
928
- logger.console.log("[trails-sdk] waiting for tx", txHash)
929
-
930
- const receipt = await originPublicClient.waitForTransactionReceipt({
931
- hash: txHash,
932
- })
933
-
934
- logger.console.log("[trails-sdk] tx receipt", receipt)
935
-
936
- transactionStates[0] = getTransactionStateFromReceipt(
937
- receipt,
938
- originChain.id,
939
- transactionStates[0]?.label,
940
- )
941
- transactionStates[1] = getTransactionStateFromReceipt(
942
- receipt,
943
- originChain.id,
944
- transactionStates[1]?.label,
945
- )
946
-
947
- onTransactionStateChange(transactionStates)
948
-
949
- const attestation = await waitForAttestation()
950
-
951
- if (!attestation) {
952
- throw new Error("Failed to retrieve attestation")
953
- }
954
-
955
- const tokenMessenger = getMessageTransmitter(destinationChain.id)!
956
-
957
- logger.console.log("[trails-sdk] tokenMessenger", tokenMessenger)
958
- const calls: {
959
- to: `0x${string}`
960
- data: `0x${string}`
961
- value: bigint
962
- }[] = []
963
-
964
- if (hasCustomCalldata) {
965
- calls.push(
966
- await getCCTPRelayerCallData({
967
- attestation,
968
- targetContract: recipient,
969
- calldata: destinationCalldata as `0x${string}`,
970
- gasLimit: 300000n,
971
- destinationChain,
972
- }),
973
- )
974
- } else {
975
- calls.push(
976
- await getMintUSDCData({
977
- tokenMessenger,
978
- attestation,
979
- }),
980
- )
981
- }
982
-
983
- logger.console.log("[trails-sdk] calls", calls)
984
- const delegatorPrivateKey = generatePrivateKey()
985
- const delegatorAccount = privateKeyToAccount(delegatorPrivateKey)
986
- const delegatorClient = createWalletClient({
987
- account: delegatorAccount,
988
- chain: destinationChain,
989
- transport: http(),
990
- })
991
- const destinationPublicClient = createPublicClient({
992
- chain: destinationChain,
993
- transport: http(),
994
- })
995
- logger.console.log("[trails-sdk] delegatorClient", delegatorClient)
996
-
997
- const sequenceWalletAddress = await simpleCreateSequenceWallet(
998
- delegatorAccount as any,
999
- )
1000
- logger.console.log(
1001
- "[trails-sdk] sequenceWalletAddress",
1002
- sequenceWalletAddress,
1003
- )
1004
- const sequenceTxHash = await sequenceSendTransaction(
1005
- sequenceWalletAddress,
1006
- delegatorClient,
1007
- destinationPublicClient,
1008
- calls,
1009
- destinationChain,
1010
- )
1011
-
1012
- const destinationReceipt =
1013
- await destinationPublicClient.waitForTransactionReceipt({
1014
- hash: sequenceTxHash as `0x${string}`,
1015
- })
1016
-
1017
- logger.console.log(
1018
- "[trails-sdk] destinationReceipt",
1019
- destinationReceipt,
1020
- )
1021
-
1022
- if (transactionStates[2]) {
1023
- transactionStates[2] = getTransactionStateFromReceipt(
1024
- destinationReceipt,
1025
- destinationChain.id,
1026
- transactionStates[2]?.label,
1027
- )
1028
- }
1029
-
1030
- onTransactionStateChange(transactionStates)
1031
-
1032
- return {
1033
- originUserTxReceipt: receipt,
1034
- originMetaTxnReceipt: null,
1035
- destinationMetaTxnReceipt: null,
1036
- totalCompletionSeconds: 0,
1037
- }
1038
- },
1039
- }
1040
- }
1041
-
1042
- const destinationSalt = Date.now().toString()
1043
-
1044
- const intentArgs = getIntentArgs(
1045
- mainSignerAddress,
1046
- originChainId,
1047
- originTokenAddress,
1048
- tradeType === TradeType.EXACT_OUTPUT ? originTokenBalance : swapAmount, // originTokenAmount
1049
- destinationChainId,
1050
- destinationTokenAddress,
1051
- tradeType === TradeType.EXACT_OUTPUT ? swapAmount : "0", // destinationTokenAmount
1052
- destinationTokenSymbol,
1053
- recipient,
1054
- destinationCalldata,
1055
- destinationSalt,
1056
- slippageTolerance,
1057
- tradeType,
1058
- quoteProvider,
1059
- )
1060
-
1061
- logger.console.log("[trails-sdk] Creating intent with args:", intentArgs)
1062
-
1063
- const intent = await getIntentCallsPayloadsFromIntents(
1064
- trailsClient,
1065
- {
1066
- params: intentArgs,
1067
- },
1068
- {
1069
- originTokenSymbol,
1070
- destinationTokenSymbol,
1071
- feeTokenSymbol: originTokenSymbol,
1072
- },
1073
- )
1074
- logger.console.log("[trails-sdk] Got intent:", intent)
1075
-
1076
- if (
1077
- !intent.payloads.preconditions?.length ||
1078
- !intent.payloads.calls?.length
1079
- ) {
1080
- throw new Error("Invalid intent")
1081
- }
1082
-
1083
- const originIntentAddress = intent.payloads.originIntentAddress
1084
- logger.console.log(
1085
- "[trails-sdk] origin intent address:",
1086
- originIntentAddress.toString(),
1087
- )
1088
-
1089
- const firstPrecondition = findFirstPreconditionForChainId(
1090
- intent.payloads.preconditions,
1091
- originChainId,
1092
- )
1093
-
1094
- if (!firstPrecondition) {
1095
- throw new Error("No precondition found for origin chain")
1096
- }
1097
-
1098
- const firstPreconditionMin = firstPrecondition?.data?.min?.toString()
1099
- const depositAmount = firstPreconditionMin
1100
-
1101
- const quoteToAmount = intent.payloads.quote.toAmount
1102
- const quoteToAmountMin = intent.payloads.quote.toAmountMin
1103
-
1104
- const originSendAmountFormatted = formatRawAmount(
1105
- depositAmount,
1106
- sourceTokenDecimals,
1107
- )
1108
-
1109
- const depositAmountUsd = calcAmountUsdPrice({
1110
- amount: originSendAmountFormatted,
1111
- usdPrice: sourceTokenPriceUsd,
1112
- })
1113
-
1114
- logger.console.log("[trails-sdk] depositAmountUsd", depositAmountUsd, {
1115
- amount: originSendAmountFormatted,
1116
- usdPrice: sourceTokenPriceUsd,
1117
- originTokenSymbol,
1118
- originSendAmountFormatted,
1119
- })
1120
-
1121
- const effectiveDestinationTokenAmount = quoteToAmount
1122
- const effectiveDestinationTokenAmountFormatted = formatRawAmount(
1123
- effectiveDestinationTokenAmount,
1124
- destinationTokenDecimals,
1125
- )
1126
-
1127
- const effectiveDestinationTokenAmountUsd = calcAmountUsdPrice({
1128
- amount: effectiveDestinationTokenAmountFormatted,
1129
- usdPrice: destinationTokenPriceUsd,
1130
- })
1131
-
1132
- logger.console.log(
1133
- "[trails-sdk] effectiveDestinationTokenAmountUsd",
1134
- effectiveDestinationTokenAmountUsd,
1135
- {
1136
- amount: effectiveDestinationTokenAmountFormatted,
1137
- usdPrice: destinationTokenPriceUsd,
1138
- destinationTokenSymbol,
1139
- effectiveDestinationTokenAmount,
1140
- destinationTokenAddress,
1141
- destinationChainId,
1142
- },
1143
- )
1144
-
1145
- let noSufficientBalance = false
1146
-
1147
- if (!(fundMethod === "qr-code" || fundMethod === "exchange")) {
1148
- const { hasEnoughBalance } = await checkAccountBalance({
1149
- account,
1150
- tokenAddress: originTokenAddress,
1151
- depositAmount,
1152
- publicClient,
1153
- })
1154
-
1155
- if (!hasEnoughBalance) {
1156
- noSufficientBalance = true
1157
- }
1158
- }
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
-
1196
- const quote = await getNormalizedQuoteObject({
1197
- originDepositAddress: originIntentAddress,
1198
- destinationDepositAddress: intent.payloads.destinationIntentAddress,
1199
- destinationAddress: recipient,
1200
- destinationCalldata,
1201
- originAmount: depositAmount,
1202
- destinationAmount: quoteToAmount,
1203
- originAmountMin: depositAmount,
1204
- destinationAmountMin: quoteToAmountMin,
1205
- originTokenAddress: originTokenAddress,
1206
- destinationTokenAddress: destinationTokenAddress,
1207
- originTokenPriceUsd: sourceTokenPriceUsd?.toString() || null,
1208
- destinationTokenPriceUsd: destinationTokenPriceUsd?.toString() || null,
1209
- fees: getFeesFromIntent(intent, {
1210
- tradeType,
1211
- fromAmountUsd: depositAmountUsd,
1212
- toAmountUsd: effectiveDestinationTokenAmountUsd,
1213
- }),
1214
- originChainId,
1215
- destinationChainId,
1216
- slippageTolerance: slippageTolerance,
1217
- priceImpact: getPriceImpactFromIntent(intent),
1218
- priceImpactUsd: getPriceImpactUsdFromIntent(intent),
1219
- transactionStates,
1220
- originNativeTokenPriceUsd,
1221
- quoteProvider: intent.payloads?.quote?.quoteProvider,
1222
- noSufficientBalance,
1223
- estimatedGasLimit: estimatedGasLimitForQuote,
1224
- intent,
1225
- })
1226
-
1227
- // Call onCheckoutQuote callback if provided
1228
- if (checkoutOnHandlers?.triggerCheckoutQuote) {
1229
- checkoutOnHandlers.triggerCheckoutQuote(quote)
1230
- }
1231
-
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
1236
- let intentEntrypointFeeOptions: any = null
1237
- if (gasless && walletId !== "sequence-waas") {
1238
- try {
1239
- logger.console.log(
1240
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless enabled, checking intent entrypoint support for chain:",
1241
- originChainId,
1242
- )
1243
-
1244
- intentEntrypointFeeOptions = await getIntentEntrypointFeeOptions({
1245
- trailsClient,
1246
- userAddress: mainSignerAddress as `0x${string}`,
1247
- tokenAddress: originTokenAddress as `0x${string}`,
1248
- amount: depositAmount,
1249
- intentAddress: originIntentAddress as `0x${string}`,
1250
- chainId: originChainId,
1251
- })
1252
- logger.console.log(
1253
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Intent entrypoint fee options:",
1254
- intentEntrypointFeeOptions,
1255
- )
1256
- } catch (error) {
1257
- logger.console.error(
1258
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Error getting intent entrypoint fee options:",
1259
- error,
1260
- )
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
- )
1266
- } else {
1267
- logger.console.log(
1268
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless disabled, skipping intent entrypoint fee options fetch",
1269
- )
1270
- }
1271
-
1272
- return {
1273
- quote,
1274
- feeOptions: intentEntrypointFeeOptions,
1275
- send: async ({
1276
- onOriginSend,
1277
- selectedFeeToken: runtimeSelectedFeeToken,
1278
- }: {
1279
- onOriginSend?: () => void
1280
- selectedFeeToken?: any
1281
- }): Promise<SendReturn> => {
1282
- try {
1283
- // Use runtime selectedFeeToken if provided, otherwise fall back to the one from prepareSend
1284
- const effectiveSelectedFeeToken =
1285
- runtimeSelectedFeeToken ?? selectedFeeToken
1286
- logger.console.log(
1287
- "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] send() called with:",
1288
- {
1289
- runtimeSelectedFeeToken,
1290
- prepareTimeSelectedFeeToken: selectedFeeToken,
1291
- effectiveSelectedFeeToken,
1292
- },
1293
- )
1294
- await commitIntentConfig(
1295
- trailsClient,
1296
- mainSignerAddress,
1297
- intent.payloads.calls,
1298
- intent.payloads.preconditions,
1299
- {
1300
- originTokenSymbol,
1301
- destinationTokenSymbol,
1302
- },
1303
- intentArgs as IntentRequestParams,
1304
- )
1305
-
1306
- if (!(fundMethod === "qr-code" || fundMethod === "exchange")) {
1307
- const { hasEnoughBalance, balanceError } = await checkAccountBalance({
1308
- account,
1309
- tokenAddress: originTokenAddress,
1310
- depositAmount,
1311
- publicClient,
1312
- })
1313
-
1314
- if (!hasEnoughBalance) {
1315
- throw balanceError
1316
- }
1317
- }
1318
-
1319
- logger.console.log("[trails-sdk] sending origin transaction")
1320
- const usingLIfi = false
1321
- let needsNativeFee = false
1322
-
1323
- if (usingLIfi) {
1324
- needsNativeFee = getNeedsLifiNativeFee({
1325
- originTokenAddress,
1326
- destinationTokenAmount: swapAmount,
1327
- destinationTokenDecimals,
1328
- sourceTokenDecimals,
1329
- sourceTokenPriceUsd: sourceTokenPriceUsd ?? null,
1330
- destinationTokenPriceUsd: destinationTokenPriceUsd ?? null,
1331
- depositAmount,
1332
- })
1333
- }
1334
-
1335
- logger.console.log("[trails-sdk] needsNativeFee", needsNativeFee)
1336
- logger.console.log(
1337
- "[trails-sdk] sourceTokenPriceUsd",
1338
- sourceTokenPriceUsd,
1339
- )
1340
- logger.console.log(
1341
- "[trails-sdk] destinationTokenPriceUsd",
1342
- destinationTokenPriceUsd,
1343
- )
1344
- logger.console.log(
1345
- "[trails-sdk] sourceTokenDecimals",
1346
- sourceTokenDecimals,
1347
- )
1348
- logger.console.log(
1349
- "[trails-sdk] destinationTokenDecimals",
1350
- destinationTokenDecimals,
1351
- )
1352
-
1353
- let originUserTxReceipt: TransactionReceipt | null = null
1354
- let originMetaTxnReceipt: MetaTxnReceipt | null = null
1355
- let destinationMetaTxnReceipt: MetaTxnReceipt | null = null
1356
-
1357
- const testnet = isTestnetDebugMode()
1358
- const effectiveOriginChain = testnet
1359
- ? getTestnetChainInfo(chain)!
1360
- : chain
1361
- const effectiveOriginTokenAddress = testnet
1362
- ? getTestnetOriginTokenAddress(effectiveOriginChain.id)
1363
- : originTokenAddress
1364
-
1365
- logger.console.log("[trails-sdk] testnet", testnet)
1366
-
1367
- const destinationPublicClient = createPublicClient({
1368
- chain: getChainInfo(destinationChainId)!,
1369
- transport: http(),
1370
- })
1371
-
1372
- const depositPromise = async () => {
1373
- logger.console.log(
1374
- "[trails-sdk] depositPromise called - starting deposit transaction",
1375
- )
1376
- logger.console.log("[trails-sdk] fundMethod value:", fundMethod)
1377
-
1378
- // Skip wallet deposit if fund method is qr-code
1379
- if (fundMethod === "qr-code" || fundMethod === "exchange") {
1380
- logger.console.log(
1381
- "[trails-sdk] Skipping wallet deposit for QR code mode - fundMethod is:",
1382
- fundMethod,
1383
- )
1384
- return
1385
- }
1386
-
1387
- logger.console.log(
1388
- "[trails-sdk] Calling attemptUserDepositTx with params:",
1389
- {
1390
- originTokenAddress: effectiveOriginTokenAddress,
1391
- gasless,
1392
- paymasterUrl,
1393
- chain: effectiveOriginChain.id,
1394
- account: account.address,
1395
- firstPreconditionMin,
1396
- originIntentAddress,
1397
- fee,
1398
- dryMode,
1399
- feeOptions: intentEntrypointFeeOptions,
1400
- selectedFeeToken: effectiveSelectedFeeToken,
1401
- selectedFeeTokenType: typeof effectiveSelectedFeeToken,
1402
- selectedFeeTokenValue: JSON.stringify(effectiveSelectedFeeToken),
1403
- },
1404
- )
1405
-
1406
- originUserTxReceipt = await attemptUserDepositTx({
1407
- originTokenAddress: effectiveOriginTokenAddress,
1408
- gasless,
1409
- paymasterUrl,
1410
- chain: effectiveOriginChain,
1411
- account,
1412
- originRelayer,
1413
- firstPreconditionMin,
1414
- originIntentAddress,
1415
- onOriginSend,
1416
- publicClient,
1417
- walletClient,
1418
- destinationTokenDecimals,
1419
- sourceTokenDecimals,
1420
- fee,
1421
- dryMode,
1422
- sourceTokenPriceUsd: sourceTokenPriceUsd ?? null,
1423
- destinationTokenPriceUsd: destinationTokenPriceUsd ?? null,
1424
- swapAmount,
1425
- onTransactionStateChange,
1426
- transactionStates,
1427
- fundMethod,
1428
- originTokenSymbol,
1429
- destinationTokenSymbol,
1430
- depositAmountUsd,
1431
- feeOptions: intentEntrypointFeeOptions,
1432
- trailsClient,
1433
- selectedFeeToken: effectiveSelectedFeeToken,
1434
- walletId,
1435
- abortSignal,
1436
- })
1437
-
1438
- if (!originUserTxReceipt) {
1439
- throw new Error("Failed to send origin transaction")
1440
- }
1441
-
1442
- transactionStates[0] = getTransactionStateFromReceipt(
1443
- originUserTxReceipt,
1444
- originChainId,
1445
- transactionStates[0]?.label,
1446
- )
1447
-
1448
- try {
1449
- transactionStates[0].decodedTrailsTokenSweeperEvents =
1450
- decodeTrailsTokenSweeperEvents(originUserTxReceipt)
1451
- transactionStates[0].decodedGuestModuleEvents =
1452
- decodeGuestModuleEvents(originUserTxReceipt)
1453
- transactionStates[0].refunded =
1454
- transactionStates[0].decodedTrailsTokenSweeperEvents.findIndex(
1455
- (event) =>
1456
- event.type === "Refund" || event.type === "RefundAndSweep",
1457
- ) !== -1
1458
- logger.console.log(
1459
- "[trails-sdk] [GASLESS-FLOW] Gasless deposit events decoded",
1460
- {
1461
- chainId: originChainId,
1462
- callFailed: (
1463
- transactionStates[0].decodedGuestModuleEvents || []
1464
- ).filter((e: any) => e?.type === "CallFailed").length,
1465
- sweeperEvents: (
1466
- transactionStates[0].decodedTrailsTokenSweeperEvents || []
1467
- ).length,
1468
- refunded: transactionStates[0].refunded,
1469
- },
1470
- )
1471
-
1472
- // Check for transaction failure or refund
1473
- const hasCallFailed = (
1474
- transactionStates[0].decodedGuestModuleEvents || []
1475
- ).some((e: any) => e?.type === "CallFailed")
1476
-
1477
- if (transactionStates[0].refunded || hasCallFailed) {
1478
- const errorMessage = transactionStates[0].refunded
1479
- ? "Transaction was refunded"
1480
- : "Transaction call failed"
1481
-
1482
- logger.console.error(
1483
- "[trails-sdk] [GASLESS-FLOW] Deposit transaction failed",
1484
- {
1485
- refunded: transactionStates[0].refunded,
1486
- callFailed: hasCallFailed,
1487
- },
1488
- )
1489
-
1490
- // Call onCheckoutError callback if provided
1491
- if (checkoutOnHandlers?.triggerCheckoutError) {
1492
- checkoutOnHandlers.triggerCheckoutError(errorMessage)
1493
- }
1494
- }
1495
- } catch (error) {
1496
- logger.console.error(
1497
- "[trails-sdk] Error decoding gasless deposit events",
1498
- error,
1499
- )
1500
- }
1501
-
1502
- onTransactionStateChange(transactionStates)
1503
-
1504
- setTimeout(() => {
1505
- const destinationChain = getChainInfo(destinationChainId)
1506
- updatePersistentToast(
1507
- "In Progress",
1508
- `Your transaction to ${destinationChain?.name || "chain"} is in progress`,
1509
- "info",
1510
- )
1511
- }, 1000)
1512
- }
1513
-
1514
- const checkForDepositTx = async () => {
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
-
1524
- try {
1525
- const response = await getAccountTransactionHistory({
1526
- chainId: originChainId,
1527
- accountAddress: originIntentAddress,
1528
- abortSignal,
1529
- })
1530
- logger.console.log(
1531
- "[trails-sdk] getAccountTransactionHistory response",
1532
- response,
1533
- )
1534
- if (response.transactions.length > 0) {
1535
- const tx = response.transactions[0]
1536
- if (!tx?.txnHash) {
1537
- await new Promise((resolve) =>
1538
- setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
1539
- )
1540
- continue
1541
- }
1542
- // const isReceive = tx.transfers.some(
1543
- // (transfer) => transfer.transferType === "RECEIVE",
1544
- // )
1545
- // if (!isReceive) {
1546
- // await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY))
1547
- // continue
1548
- // }
1549
- const originDepositTxReceipt =
1550
- await publicClient.getTransactionReceipt({
1551
- hash: tx.txnHash as `0x${string}`,
1552
- })
1553
-
1554
- originUserTxReceipt = originDepositTxReceipt
1555
-
1556
- transactionStates[0] = getTransactionStateFromReceipt(
1557
- originDepositTxReceipt,
1558
- originChainId,
1559
- transactionStates[0]?.label,
1560
- )
1561
- onTransactionStateChange(transactionStates)
1562
-
1563
- if (onOriginSend) {
1564
- onOriginSend()
1565
- }
1566
- break
1567
- }
1568
- } catch (error) {
1569
- logger.console.error("Error checking for deposit tx", error)
1570
- }
1571
- await new Promise((resolve) =>
1572
- setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
1573
- )
1574
- }
1575
- }
1576
-
1577
- const _checkForDestinationDepositTx: () => Promise<TransactionReceipt | null> =
1578
- async () => {
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
-
1588
- try {
1589
- const response = await getAccountTransactionHistory({
1590
- chainId: destinationChainId,
1591
- accountAddress: intent.payloads
1592
- .destinationIntentAddress as `0x${string}`,
1593
- abortSignal,
1594
- })
1595
- logger.console.log(
1596
- "[trails-sdk] getAccountTransactionHistory response",
1597
- response,
1598
- )
1599
- if (response.transactions.length > 0) {
1600
- const tx = response.transactions[0]
1601
- if (!tx?.txnHash) {
1602
- await new Promise((resolve) =>
1603
- setTimeout(
1604
- resolve,
1605
- POLLING_INTERVALS.TRANSACTION_HISTORY,
1606
- ),
1607
- )
1608
- continue
1609
- }
1610
- // const isReceive = tx.transfers.some(
1611
- // (transfer) => transfer.transferType === "RECEIVE",
1612
- // )
1613
- // if (!isReceive) {
1614
- // await new Promise((resolve) => setTimeout(resolve, 1000))
1615
- // continue
1616
- // }
1617
- const destinationDepositTxReceipt =
1618
- await destinationPublicClient.getTransactionReceipt({
1619
- hash: tx.txnHash as `0x${string}`,
1620
- })
1621
-
1622
- transactionStates[2] = getTransactionStateFromReceipt(
1623
- destinationDepositTxReceipt,
1624
- destinationChainId,
1625
- transactionStates[2]?.label,
1626
- )
1627
- onTransactionStateChange(transactionStates)
1628
-
1629
- return destinationDepositTxReceipt
1630
- }
1631
- } catch (error) {
1632
- logger.console.error("Error checking for deposit tx", error)
1633
- }
1634
- await new Promise((resolve) =>
1635
- setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
1636
- )
1637
- }
1638
- }
1639
-
1640
- // Variables to store the waitForReceipt functions
1641
- let originMetaTxnReceiptPromise:
1642
- | ((abortSignal?: AbortSignal) => Promise<MetaTxnReceipt | null>)
1643
- | null = null
1644
- let destinationMetaTxnReceiptPromise:
1645
- | ((abortSignal?: AbortSignal) => Promise<MetaTxnReceipt | null>)
1646
- | ((abortSignal?: AbortSignal) => Promise<TransactionReceipt | null>)
1647
- | null = null
1648
-
1649
- // First phase: Send meta transactions and queue CCTP
1650
- const originSendMetaTxnPromise = async () => {
1651
- logger.console.log("[trails-sdk] Starting originSendMetaTxnPromise", {
1652
- hasMetaTxn: !!intent.payloads.metaTxns[0],
1653
- hasPrecondition: !!intent.payloads.preconditions[0],
1654
- metaTxnId: intent.payloads.metaTxns[0]?.id,
1655
- chainId: intent.payloads.metaTxns[0]?.chainId,
1656
- })
1657
-
1658
- if (intent.payloads.metaTxns[0] && intent.payloads.preconditions[0]) {
1659
- // Extract fee quote from intent response using metatxnid as key
1660
- const metaTxnId = intent.payloads.metaTxns[0].id
1661
- const feeQuote = intent.payloads.feeQuotes?.[metaTxnId]
1662
- logger.console.log(
1663
- "[trails-sdk] Extracted fee quote for origin meta txn",
1664
- {
1665
- metaTxnId,
1666
- feeQuote,
1667
- hasFeeQuote: !!feeQuote,
1668
- },
1669
- )
1670
-
1671
- logger.console.log(
1672
- "[trails-sdk] Calling sendMetaTxAndWaitForReceipt for origin",
1673
- {
1674
- metaTxnId,
1675
- chainId: intent.payloads.metaTxns[0].chainId,
1676
- walletAddress: intent.payloads.metaTxns[0].walletAddress,
1677
- contract: intent.payloads.metaTxns[0].contract,
1678
- },
1679
- )
1680
-
1681
- const { waitForReceipt } = await sendMetaTxAndWaitForReceipt({
1682
- metaTx: intent.payloads.metaTxns[0] as MetaTxn,
1683
- relayer: originRelayer,
1684
- precondition: intent.payloads
1685
- .preconditions[0] as IntentPrecondition,
1686
- feeQuote: feeQuote,
1687
- })
1688
-
1689
- logger.console.log(
1690
- "[trails-sdk] Origin meta transaction sent successfully",
1691
- {
1692
- metaTxnId,
1693
- },
1694
- )
408
+ // Re-export useQuote types and hook from the new location
409
+ export type {
410
+ UseQuoteProps,
411
+ Quote,
412
+ UseQuoteReturn,
413
+ SwapReturn,
414
+ QuoteProviderInfo,
415
+ } from "./widget/hooks/useQuote.js"
416
+ export { useQuote } from "./widget/hooks/useQuote.js"
1695
417
 
1696
- // Store the waitForReceipt function for later use
1697
- originMetaTxnReceiptPromise = waitForReceipt
1698
- } else {
1699
- logger.console.warn(
1700
- "[trails-sdk] Skipping origin sendMetaTxn - missing metaTxn or precondition",
1701
- {
1702
- hasMetaTxn: !!intent.payloads.metaTxns[0],
1703
- hasPrecondition: !!intent.payloads.preconditions[0],
1704
- },
1705
- )
1706
- }
1707
- }
1708
-
1709
- const destinationSendMetaTxnPromise = async () => {
1710
- logger.console.log(
1711
- "[trails-sdk] Starting destinationSendMetaTxnPromise",
1712
- {
1713
- quoteProvider: intent.payloads.quote.quoteProvider,
1714
- hasQuoteProviderRequestId:
1715
- !!intent.payloads.quote.quoteProviderRequestId,
1716
- hasPrecondition1: !!intent.payloads.preconditions[1],
1717
- hasMetaTxn1: !!intent.payloads.metaTxns[1],
1718
- },
1719
- )
1720
-
1721
- if (
1722
- intent.payloads.quote.quoteProvider === "relay" &&
1723
- intent.payloads.quote.quoteProviderRequestId &&
1724
- !intent.payloads.preconditions[1] &&
1725
- !intent.payloads.metaTxns[1]
1726
- ) {
1727
- logger.console.log(
1728
- "[trails-sdk] Setting up relay destination promise",
1729
- {
1730
- quoteProviderRequestId:
1731
- intent.payloads.quote.quoteProviderRequestId,
1732
- },
1733
- )
1734
- // For relay, we'll wait for the receipt in the wait phase
1735
- // Just store the requestId for later use
1736
- destinationMetaTxnReceiptPromise = async (
1737
- abortSignal?: AbortSignal,
1738
- ) => {
1739
- logger.console.log(
1740
- "[trails-sdk] waitForRelayDestinationTx starting",
1741
- {
1742
- quoteProviderRequestId:
1743
- intent.payloads.quote.quoteProviderRequestId,
1744
- aborted: abortSignal?.aborted,
1745
- },
1746
- )
1747
- try {
1748
- // Check if we should abort before starting
1749
- if (abortSignal?.aborted) {
1750
- logger.console.log(
1751
- "[trails-sdk] Aborting relay destination tx due to abort signal",
1752
- )
1753
- return null
1754
- }
1755
-
1756
- const txHash = await waitForRelayDestinationTx(
1757
- intent.payloads.quote.quoteProviderRequestId,
1758
- )
1759
- logger.console.log(
1760
- "[trails-sdk] waitForRelayDestinationTx completed",
1761
- {
1762
- txHash,
1763
- quoteProviderRequestId:
1764
- intent.payloads.quote.quoteProviderRequestId,
1765
- },
1766
- )
1767
- if (txHash) {
1768
- logger.console.log(
1769
- "[trails-sdk] Fetching transaction receipt for relay destination",
1770
- {
1771
- txHash,
1772
- chainId: destinationChainId,
1773
- },
1774
- )
1775
-
1776
- const destinationTxnReceipt =
1777
- await destinationPublicClient.getTransactionReceipt({
1778
- hash: txHash as `0x${string}`,
1779
- })
1780
- logger.console.log(
1781
- "[trails-sdk] relay destinationTxnReceipt received",
1782
- {
1783
- txHash,
1784
- blockNumber: destinationTxnReceipt?.blockNumber,
1785
- status: destinationTxnReceipt?.status,
1786
- gasUsed: destinationTxnReceipt?.gasUsed,
1787
- },
1788
- )
1789
- if (transactionStates[2]) {
1790
- transactionStates[2] = getTransactionStateFromReceipt(
1791
- destinationTxnReceipt,
1792
- destinationChainId,
1793
- transactionStates[2]?.label,
1794
- )
1795
- }
1796
- onTransactionStateChange(transactionStates)
1797
- return destinationTxnReceipt
1798
- } else {
1799
- logger.console.warn(
1800
- "[trails-sdk] No txHash returned from waitForRelayDestinationTx",
1801
- {
1802
- quoteProviderRequestId:
1803
- intent.payloads.quote.quoteProviderRequestId,
1804
- },
1805
- )
1806
- }
1807
- } catch (error: unknown) {
1808
- logger.console.error(
1809
- "[trails-sdk] Error waiting for relay destination tx",
1810
- {
1811
- error:
1812
- error instanceof Error ? error.message : String(error),
1813
- quoteProviderRequestId:
1814
- intent.payloads.quote.quoteProviderRequestId,
1815
- },
1816
- )
1817
- if (transactionStates?.[2]) {
1818
- transactionStates[2].state = "failed"
1819
- onTransactionStateChange(transactionStates)
1820
- }
1821
- throw error
1822
- }
1823
- return null
1824
- }
1825
- } else {
1826
- logger.console.log(
1827
- "[trails-sdk] Setting up destination meta transaction promise (non-relay)",
1828
- {
1829
- quoteProvider: intent.payloads.quote.quoteProvider,
1830
- hasMetaTxn1: !!intent.payloads.metaTxns[1],
1831
- hasPrecondition1: !!intent.payloads.preconditions[1],
1832
- },
1833
- )
1834
-
1835
- if (
1836
- intent.payloads.metaTxns[1] &&
1837
- intent.payloads.preconditions[1]
1838
- ) {
1839
- // Extract fee quote from intent response using metatxnid as key
1840
- const metaTxnId = intent.payloads.metaTxns[1].id
1841
- const feeQuote = intent.payloads.feeQuotes?.[metaTxnId]
1842
- logger.console.log(
1843
- "[trails-sdk] Extracted fee quote for destination meta txn",
1844
- {
1845
- metaTxnId,
1846
- feeQuote,
1847
- hasFeeQuote: !!feeQuote,
1848
- },
1849
- )
1850
-
1851
- logger.console.log(
1852
- "[trails-sdk] Calling sendMetaTxAndWaitForReceipt for destination",
1853
- {
1854
- metaTxnId,
1855
- chainId: intent.payloads.metaTxns[1].chainId,
1856
- walletAddress: intent.payloads.metaTxns[1].walletAddress,
1857
- contract: intent.payloads.metaTxns[1].contract,
1858
- },
1859
- )
1860
-
1861
- const { waitForReceipt } = await sendMetaTxAndWaitForReceipt({
1862
- metaTx: intent.payloads.metaTxns[1] as MetaTxn,
1863
- relayer: destinationRelayer,
1864
- precondition: intent.payloads
1865
- .preconditions[1] as IntentPrecondition,
1866
- feeQuote: feeQuote,
1867
- })
1868
-
1869
- logger.console.log(
1870
- "[trails-sdk] Destination meta transaction sent successfully",
1871
- {
1872
- metaTxnId,
1873
- },
1874
- )
1875
-
1876
- // Store the waitForReceipt function for later use
1877
- destinationMetaTxnReceiptPromise = waitForReceipt
1878
- } else {
1879
- logger.console.warn(
1880
- "[trails-sdk] Skipping destination sendMetaTxn - missing metaTxn or precondition",
1881
- {
1882
- hasMetaTxn: !!intent.payloads.metaTxns[1],
1883
- hasPrecondition: !!intent.payloads.preconditions[1],
1884
- },
1885
- )
1886
- }
1887
- // } else if (intent.payloads.destinationIntentAddress) {
1888
- // destinationMetaTxnReceiptPromise = checkForDestinationDepositTx
1889
- // }
1890
- }
1891
- }
1892
-
1893
- let queueCctpPromise: (() => Promise<void>) | null = null
1894
-
1895
- const isCctp = intent.payloads.quote.quoteProvider === "cctp"
1896
- if (isCctp) {
1897
- queueCctpPromise = async () => {
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
-
1907
- const originMetaTxnHash = originMetaTxnReceipt?.txnHash
1908
- if (originMetaTxnHash) {
1909
- await queueCCTPTransfer({
1910
- trailsClient,
1911
- sourceTxHash: originMetaTxnHash,
1912
- sourceChainId: originChainId,
1913
- destinationChainId: destinationChainId,
1914
- })
1915
- break
1916
- }
1917
- await new Promise((resolve) =>
1918
- setTimeout(resolve, POLLING_INTERVALS.CCTP_QUEUE),
1919
- )
1920
- }
1921
- }
1922
- } else {
1923
- queueCctpPromise = () => Promise.resolve()
1924
- }
1925
-
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
- }
1949
-
1950
- // Phase 1: Send meta transactions and queue CCTP
1951
- logger.console.log(
1952
- "[trails-sdk] Starting Phase 1: Sending meta transactions and queuing CCTP",
1953
- )
1954
-
1955
- await Promise.all([
1956
- originSendMetaTxnPromise(),
1957
- destinationSendMetaTxnPromise(),
1958
- ])
1959
-
1960
- logger.console.log("[trails-sdk] Phase 1 completed successfully")
1961
-
1962
- // Phase 2: Wait for receipts and execute deposit
1963
- logger.console.log(
1964
- "[trails-sdk] Starting Phase 2: Waiting for receipts and executing deposit",
1965
- )
1966
-
1967
- const waitForOriginMetaTxnReceiptPromise = async () => {
1968
- logger.console.log(
1969
- "[trails-sdk] Waiting for origin meta transaction receipt",
1970
- )
1971
- if (originMetaTxnReceiptPromise) {
1972
- try {
1973
- originMetaTxnReceipt =
1974
- await originMetaTxnReceiptPromise(abortSignal)
1975
-
1976
- if (originMetaTxnReceipt && transactionStates[1]) {
1977
- transactionStates[1] = getTransactionStateFromReceipt(
1978
- originMetaTxnReceipt,
1979
- originChainId,
1980
- transactionStates[1]?.label,
1981
- )
1982
- onTransactionStateChange(transactionStates)
1983
-
1984
- try {
1985
- const receipt = await publicClient.getTransactionReceipt({
1986
- hash: originMetaTxnReceipt.txnHash as `0x${string}`,
1987
- })
1988
- transactionStates[1].decodedTrailsTokenSweeperEvents =
1989
- decodeTrailsTokenSweeperEvents(receipt)
1990
- transactionStates[1].decodedGuestModuleEvents =
1991
- decodeGuestModuleEvents(receipt)
1992
- transactionStates[1].refunded =
1993
- transactionStates[1].decodedTrailsTokenSweeperEvents.findIndex(
1994
- (event) =>
1995
- event.type === "Refund" ||
1996
- event.type === "RefundAndSweep",
1997
- ) !== -1
1998
- logger.console.log("[trails-sdk] Origin meta-tx events", {
1999
- chainId: originChainId,
2000
- callFailed: (
2001
- transactionStates[1].decodedGuestModuleEvents || []
2002
- ).filter((e: any) => e?.type === "CallFailed").length,
2003
- sweeperEvents: (
2004
- transactionStates[1].decodedTrailsTokenSweeperEvents || []
2005
- ).length,
2006
- refunded: transactionStates[1].refunded,
2007
- })
2008
-
2009
- // Check for transaction failure or refund
2010
- const hasCallFailed = (
2011
- transactionStates[1].decodedGuestModuleEvents || []
2012
- ).some((e: any) => e?.type === "CallFailed")
2013
-
2014
- if (transactionStates[1].refunded || hasCallFailed) {
2015
- const errorMessage = transactionStates[1].refunded
2016
- ? "Origin transaction was refunded"
2017
- : "Origin transaction call failed"
2018
-
2019
- logger.console.error("[trails-sdk] Origin meta-tx failed", {
2020
- refunded: transactionStates[1].refunded,
2021
- callFailed: hasCallFailed,
2022
- })
2023
-
2024
- // Call onCheckoutError callback if provided
2025
- if (checkoutOnHandlers?.triggerCheckoutError) {
2026
- checkoutOnHandlers.triggerCheckoutError(errorMessage)
2027
- }
2028
- }
2029
-
2030
- onTransactionStateChange(transactionStates)
2031
- } catch (error) {
2032
- logger.console.error("Error decoding origin tx events", error)
2033
- }
2034
- }
2035
- } catch (error) {
2036
- logger.console.error(
2037
- "[trails-sdk] Error waiting for origin receipt:",
2038
- error,
2039
- )
2040
- }
2041
- } else {
2042
- logger.console.log(
2043
- "[trails-sdk] No origin meta transaction receipt promise to wait for",
2044
- )
2045
- }
2046
- }
2047
-
2048
- const waitForDestinationMetaTxnReceiptPromise = async () => {
2049
- logger.console.log(
2050
- "[trails-sdk] Waiting for destination meta transaction receipt",
2051
- )
2052
- if (destinationMetaTxnReceiptPromise) {
2053
- try {
2054
- // Race between destination receipt and failure polling
2055
- const destinationReceiptPromise = destinationMetaTxnReceiptPromise
2056
- ? destinationMetaTxnReceiptPromise(abortSignal)
2057
- : Promise.resolve(null)
2058
-
2059
- const failurePollingPromise = new Promise<null>((resolve) => {
2060
- const pollForFailures = () => {
2061
- const isPreviousTxCallFailed =
2062
- transactionStates?.some((tx) => tx.state === "failed") ||
2063
- transactionStates?.some((tx) =>
2064
- tx?.decodedGuestModuleEvents?.some(
2065
- (event) => event.type === "CallFailed",
2066
- ),
2067
- )
2068
-
2069
- if (isPreviousTxCallFailed) {
2070
- logger.console.log(
2071
- "[trails-sdk] Aborting destination meta transaction due to previous transaction failure",
2072
- )
2073
- resolve(null)
2074
- } else {
2075
- // Continue polling with consistent interval
2076
- setTimeout(
2077
- pollForFailures,
2078
- POLLING_INTERVALS.FAILURE_POLLING,
2079
- )
2080
- }
2081
- }
2082
- pollForFailures()
2083
- })
2084
-
2085
- destinationMetaTxnReceipt = (await Promise.race([
2086
- destinationReceiptPromise,
2087
- failurePollingPromise,
2088
- ])) as MetaTxnReceipt
2089
-
2090
- logger.console.log(
2091
- "[trails-sdk] destinationMetaTxnReceipt",
2092
- destinationMetaTxnReceipt,
2093
- )
2094
-
2095
- if (destinationMetaTxnReceipt && transactionStates[2]) {
2096
- transactionStates[2] = getTransactionStateFromReceipt(
2097
- destinationMetaTxnReceipt,
2098
- destinationChainId,
2099
- transactionStates[2]?.label,
2100
- )
2101
- onTransactionStateChange(transactionStates)
2102
-
2103
- try {
2104
- const receipt =
2105
- await destinationPublicClient.getTransactionReceipt({
2106
- hash: destinationMetaTxnReceipt.txnHash as `0x${string}`,
2107
- })
2108
- transactionStates[2].decodedTrailsTokenSweeperEvents =
2109
- decodeTrailsTokenSweeperEvents(receipt)
2110
- transactionStates[2].decodedGuestModuleEvents =
2111
- decodeGuestModuleEvents(receipt)
2112
- transactionStates[2].refunded =
2113
- transactionStates[2].decodedTrailsTokenSweeperEvents.findIndex(
2114
- (event) =>
2115
- event.type === "Refund" ||
2116
- event.type === "RefundAndSweep",
2117
- ) !== -1 ||
2118
- (transactionStates[2].decodedTrailsTokenSweeperEvents.findIndex(
2119
- (event) => event.type === "Sweep",
2120
- ) !== -1 &&
2121
- transactionStates[2].decodedGuestModuleEvents.findIndex(
2122
- (event) => event.type === "CallFailed",
2123
- ) !== -1)
2124
- logger.console.log(
2125
- "[trails-sdk] Destination meta-tx events",
2126
- {
2127
- chainId: destinationChainId,
2128
- callFailed: (
2129
- transactionStates[2].decodedGuestModuleEvents || []
2130
- ).filter((e: any) => e?.type === "CallFailed").length,
2131
- sweeperEvents: (
2132
- transactionStates[2].decodedTrailsTokenSweeperEvents ||
2133
- []
2134
- ).length,
2135
- refunded: transactionStates[2].refunded,
2136
- },
2137
- )
2138
-
2139
- // Check for transaction failure or refund
2140
- const hasCallFailed = (
2141
- transactionStates[2].decodedGuestModuleEvents || []
2142
- ).some((e: any) => e?.type === "CallFailed")
2143
-
2144
- if (transactionStates[2].refunded || hasCallFailed) {
2145
- const errorMessage = transactionStates[2].refunded
2146
- ? "Destination transaction was refunded"
2147
- : "Destination transaction call failed"
2148
-
2149
- logger.console.error(
2150
- "[trails-sdk] Destination meta-tx failed",
2151
- {
2152
- refunded: transactionStates[2].refunded,
2153
- callFailed: hasCallFailed,
2154
- },
2155
- )
2156
-
2157
- // Call onCheckoutError callback if provided
2158
- if (checkoutOnHandlers?.triggerCheckoutError) {
2159
- checkoutOnHandlers.triggerCheckoutError(errorMessage)
2160
- }
2161
- }
2162
-
2163
- onTransactionStateChange(transactionStates)
2164
- } catch (error) {
2165
- logger.console.error(
2166
- "Error decoding destination tx events",
2167
- error,
2168
- )
2169
- }
2170
- }
2171
- } catch (error) {
2172
- logger.console.error(
2173
- "[trails-sdk] Error waiting for destination receipt:",
2174
- error,
2175
- )
2176
- // For relay transactions, this might be expected if still waiting
2177
- if (intent.payloads.quote.quoteProvider === "relay") {
2178
- logger.console.log(
2179
- "[trails-sdk] Relay transaction still waiting, this is normal",
2180
- )
2181
- }
2182
- }
2183
- } else {
2184
- logger.console.log(
2185
- "[trails-sdk] No destination meta transaction receipt promise to wait for",
2186
- )
2187
- }
2188
- }
2189
-
2190
- logger.console.log(
2191
- "[trails-sdk] Executing Phase 2 Promise.all with deposit",
2192
- )
2193
- logger.console.log(
2194
- "[trails-sdk] About to call depositPromise - fundMethod is:",
2195
- fundMethod,
2196
- )
2197
-
2198
- await Promise.all([
2199
- depositPromise(),
2200
- waitForOriginMetaTxnReceiptPromise(),
2201
- waitForDestinationMetaTxnReceiptPromise(),
2202
- queueCctpPromise(),
2203
- ])
2204
- logger.console.log("[trails-sdk] Phase 2 completed successfully")
2205
-
2206
- // Track payment completion for different chain and different token
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
2219
- trackPaymentCompleted({
2220
- userAddress: account.address,
2221
- originIntentAddress,
2222
- originTxHash: (originUserTxReceipt as TransactionReceipt)
2223
- .transactionHash,
2224
- destinationTxHash: (destinationMetaTxnReceipt as MetaTxnReceipt)
2225
- ?.txnHash,
2226
- originChainId,
2227
- destinationChainId,
2228
- mode,
2229
- fundMethod,
2230
- originTokenSymbol,
2231
- originTokenAddress,
2232
- destinationTokenAddress,
2233
- destinationTokenSymbol,
2234
- depositTokenAmountUsd: depositAmountUsd?.toString(),
2235
- destinationTokenAmountUsd:
2236
- effectiveDestinationTokenAmountUsd?.toString(),
2237
- })
2238
-
2239
- // Call onCheckoutComplete callback with transaction status
2240
- if (checkoutOnHandlers?.triggerCheckoutComplete) {
2241
- checkoutOnHandlers.triggerCheckoutComplete(txStatus)
2242
- }
2243
- } else {
2244
- if (
2245
- transactionStates[1] &&
2246
- transactionStates[1]?.transactionHash === "" &&
2247
- transactionStates[1]?.state === "pending"
2248
- ) {
2249
- transactionStates[1].state = "aborted"
2250
- onTransactionStateChange(transactionStates)
2251
- }
2252
- if (
2253
- transactionStates[2] &&
2254
- transactionStates[2]?.transactionHash === "" &&
2255
- transactionStates[2]?.state === "pending"
2256
- ) {
2257
- transactionStates[2].state = "aborted"
2258
- onTransactionStateChange(transactionStates)
2259
- }
2260
-
2261
- // Track payment error if transactions didn't complete successfully
2262
- trackPaymentError({
2263
- error:
2264
- "Payment transactions possibly did not complete successfully. Was not able to receive both origin and destination meta transaction receipts. May be an API error.",
2265
- userAddress: account.address,
2266
- originIntentAddress,
2267
- mode,
2268
- fundMethod,
2269
- originChainId,
2270
- destinationChainId,
2271
- originTokenSymbol,
2272
- originTokenAddress,
2273
- destinationTokenAddress,
2274
- destinationTokenSymbol,
2275
- })
2276
-
2277
- // Call onCheckoutError callback if provided
2278
- if (checkoutOnHandlers?.triggerCheckoutError) {
2279
- checkoutOnHandlers.triggerCheckoutError(
2280
- "Payment transactions did not complete successfully",
2281
- )
2282
- }
2283
- }
2284
-
2285
- return {
2286
- originUserTxReceipt,
2287
- originMetaTxnReceipt,
2288
- destinationMetaTxnReceipt,
2289
- totalCompletionSeconds: await getTxTimeDiff(
2290
- transactionStates[0],
2291
- transactionStates[2],
2292
- ),
2293
- }
2294
- } catch (error) {
2295
- const errorMessage =
2296
- error instanceof Error
2297
- ? error.message
2298
- : "Unknown error occurred during transaction"
2299
- logger.console.error(
2300
- "[trails-sdk] Error in sendHandlerForDifferentChainDifferentToken:",
2301
- error,
2302
- )
2303
-
2304
- // Track payment error
2305
- trackPaymentError({
2306
- error: errorMessage,
2307
- userAddress: account.address,
2308
- originIntentAddress,
2309
- mode,
2310
- fundMethod,
2311
- originChainId,
2312
- destinationChainId,
2313
- originTokenSymbol,
2314
- originTokenAddress,
2315
- destinationTokenAddress,
2316
- destinationTokenSymbol,
2317
- })
2318
-
2319
- // Call onCheckoutError callback if provided
2320
- if (checkoutOnHandlers?.triggerCheckoutError) {
2321
- checkoutOnHandlers.triggerCheckoutError(errorMessage)
2322
- }
2323
-
2324
- // Re-throw the error so caller can handle if needed
2325
- throw error
2326
- }
2327
- },
2328
- }
2329
- }
2330
-
2331
- async function sendHandlerForSameChainSameToken({
2332
- originTokenAddress,
2333
- originTokenDecimals,
2334
- swapAmount,
2335
- destinationCalldata,
2336
- recipient,
2337
- walletClient,
2338
- onTransactionStateChange,
2339
- dryMode,
2340
- account,
2341
- chain,
2342
- transactionStates,
2343
- sourceTokenPriceUsd,
2344
- destinationTokenPriceUsd,
2345
- originNativeTokenPriceUsd,
2346
- slippageTolerance,
2347
- checkoutOnHandlers,
2348
- mode,
2349
- fundMethod,
2350
- gasless,
2351
- paymasterUrl,
2352
- selectedFeeToken,
2353
- trailsClient,
2354
- originRelayer,
2355
- abortSignal,
2356
- }: {
2357
- originTokenAddress: string
2358
- originTokenDecimals: number
2359
- swapAmount: string
2360
- destinationCalldata?: string
2361
- recipient: string
2362
- originChainId: number
2363
- walletClient: WalletClient
2364
- publicClient: PublicClient
2365
- onTransactionStateChange: (transactionStates: TransactionState[]) => void
2366
- dryMode: boolean
2367
- account: Account
2368
- chain: Chain
2369
- transactionStates: TransactionState[]
2370
- sourceTokenPriceUsd?: number | null
2371
- destinationTokenPriceUsd?: number | null
2372
- originNativeTokenPriceUsd?: number | null
2373
- slippageTolerance?: string
2374
- checkoutOnHandlers?: CheckoutOnHandlers
2375
- mode?: "pay" | "fund" | "earn" | "swap" | "receive"
2376
- fundMethod?: string
2377
- gasless?: boolean
2378
- paymasterUrl?: string
2379
- selectedFeeToken?: any
2380
- trailsClient: TrailsAPIClient
2381
- originRelayer: Relayer.RpcRelayer
2382
- abortSignal?: AbortSignal
2383
- }): Promise<PrepareSendReturn> {
2384
- logger.console.log("[trails-sdk] isToSameToken && isToSameChain")
2385
- const testnet = isTestnetDebugMode()
2386
- const effectiveOriginChain = testnet ? getTestnetChainInfo(chain)! : chain
2387
- const effectiveOriginChainId = effectiveOriginChain.id
2388
- const effectiveOriginTokenAddress = testnet
2389
- ? getTestnetOriginTokenAddress(effectiveOriginChainId)
2390
- : originTokenAddress
2391
- const effectivePublicClient = createPublicClient({
2392
- chain: effectiveOriginChain,
2393
- transport: http(),
2394
- })
2395
-
2396
- let noSufficientBalance = false
2397
-
2398
- const { hasEnoughBalance } = await checkAccountBalance({
2399
- account,
2400
- tokenAddress: originTokenAddress,
2401
- depositAmount: swapAmount,
2402
- publicClient: effectivePublicClient,
2403
- })
2404
-
2405
- if (!hasEnoughBalance) {
2406
- noSufficientBalance = true
2407
- }
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
-
2447
- const quote = await getNormalizedQuoteObject({
2448
- originDepositAddress: recipient,
2449
- destinationDepositAddress: recipient,
2450
- destinationAddress: recipient,
2451
- destinationCalldata,
2452
- originAmount: swapAmount, // fromAmount is same as toAmount for same chain same token
2453
- destinationAmount: swapAmount,
2454
- originTokenPriceUsd: sourceTokenPriceUsd?.toString() || null,
2455
- destinationTokenPriceUsd: destinationTokenPriceUsd?.toString() || null,
2456
- originTokenAddress: effectiveOriginTokenAddress,
2457
- destinationTokenAddress: effectiveOriginTokenAddress,
2458
- transactionStates,
2459
- originChainId: effectiveOriginChainId,
2460
- destinationChainId: effectiveOriginChainId,
2461
- originNativeTokenPriceUsd,
2462
- slippageTolerance,
2463
- quoteProvider: "",
2464
- noSufficientBalance,
2465
- estimatedGasLimit: estimatedGasLimitForQuote,
2466
- intent: undefined,
2467
- })
2468
-
2469
- // Call onCheckoutQuote callback if provided
2470
- if (checkoutOnHandlers?.triggerCheckoutQuote) {
2471
- checkoutOnHandlers.triggerCheckoutQuote(quote)
2472
- }
2473
-
2474
- return {
2475
- quote,
2476
- send: async ({
2477
- onOriginSend,
2478
- }: {
2479
- onOriginSend?: () => void
2480
- }): Promise<SendReturn> => {
2481
- try {
2482
- const { hasEnoughBalance, balanceError } = await checkAccountBalance({
2483
- account,
2484
- tokenAddress: effectiveOriginTokenAddress,
2485
- depositAmount: swapAmount,
2486
- publicClient: effectivePublicClient,
2487
- })
2488
-
2489
- if (!hasEnoughBalance) {
2490
- throw balanceError
2491
- }
2492
-
2493
- const depositAmountFormatted = Number(
2494
- formatUnits(BigInt(swapAmount), originTokenDecimals),
2495
- )
2496
- const depositAmountUsd = calcAmountUsdPrice({
2497
- amount: depositAmountFormatted,
2498
- usdPrice: sourceTokenPriceUsd,
2499
- })
2500
-
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
-
2526
- const originCallParams = {
2527
- ...originCallParamsBase,
2528
- gasLimit,
2529
- }
2530
-
2531
- logger.console.log("[trails-sdk] origin call params", originCallParams)
2532
-
2533
- let originUserTxReceipt: TransactionReceipt | null = null
2534
- const originMetaTxnReceipt: MetaTxnReceipt | null = null
2535
- const destinationMetaTxnReceipt: MetaTxnReceipt | null = null
2536
-
2537
- await attemptSwitchChain({
2538
- walletClient,
2539
- desiredChainId: effectiveOriginChainId,
2540
- })
2541
- if (!dryMode) {
2542
- try {
2543
- onTransactionStateChange([
2544
- {
2545
- transactionHash: "",
2546
- explorerUrl: "",
2547
- chainId: effectiveOriginChainId,
2548
- state: "pending",
2549
- label: "Execute",
2550
- },
2551
- ])
2552
- } catch (error) {
2553
- logger.console.error(
2554
- "[trails-sdk] Error calling onTransactionStateChange:",
2555
- error,
2556
- )
2557
- }
2558
-
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
2567
- try {
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(),
2614
- })
2615
-
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,
2642
- spender: recipient,
2643
- amount: maxUint256,
2644
- })
2645
-
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
- )
2666
- }
2667
- }
2668
-
2669
- // Show persistent toast for checkout flow
2670
- updatePersistentToast(
2671
- "Payment Started",
2672
- "Waiting for wallet confirmation...",
2673
- "info",
2674
- )
2675
-
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
2688
-
2689
- logger.console.log("[trails-sdk] origin tx", txHash)
2690
-
2691
- if (onOriginSend) {
2692
- onOriginSend()
2693
- }
2694
-
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
2702
-
2703
- trackTransactionConfirmed({
2704
- transactionHash: txHash,
2705
- chainId: effectiveOriginChainId,
2706
- userAddress: account.address,
2707
- blockNumber: Number(receipt.blockNumber),
2708
- originTokenAddress,
2709
- depositTokenAmountUsd: depositAmountUsd?.toString(),
2710
- })
2711
-
2712
- // Toast will be shown after transaction state analysis
2713
-
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
- }
2746
- }
2747
-
2748
- // Track payment completion for same-chain same-token transaction
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
2761
- trackPaymentCompleted({
2762
- userAddress: account.address,
2763
- originTxHash: originUserTxReceipt.transactionHash,
2764
- originChainId: effectiveOriginChainId, // Same chain
2765
- mode,
2766
- fundMethod,
2767
- originTokenAddress,
2768
- depositTokenAmountUsd: depositAmountUsd?.toString(),
2769
- destinationTokenAmountUsd: depositAmountUsd?.toString(), // same as deposit amount
2770
- })
2771
-
2772
- // Call onCheckoutComplete callback with transaction status
2773
- if (checkoutOnHandlers?.triggerCheckoutComplete) {
2774
- checkoutOnHandlers.triggerCheckoutComplete(txStatus)
2775
- }
2776
- } else if (originUserTxReceipt) {
2777
- trackPaymentError({
2778
- error: "Transaction failed",
2779
- userAddress: account.address,
2780
- mode,
2781
- fundMethod,
2782
- originTokenAddress,
2783
- })
2784
-
2785
- // Call onCheckoutError callback if provided
2786
- if (checkoutOnHandlers?.triggerCheckoutError) {
2787
- checkoutOnHandlers.triggerCheckoutError("Transaction failed")
2788
- }
2789
- }
2790
- }
2791
-
2792
- return {
2793
- originUserTxReceipt,
2794
- originMetaTxnReceipt,
2795
- destinationMetaTxnReceipt,
2796
- totalCompletionSeconds: 0,
2797
- }
2798
- } catch (error) {
2799
- const errorMessage =
2800
- error instanceof Error
2801
- ? error.message
2802
- : "Unknown error occurred during transaction"
2803
- logger.console.error(
2804
- "[trails-sdk] Error in sendHandlerForSameChainSameToken:",
2805
- error,
2806
- )
2807
-
2808
- // Track payment error
2809
- trackPaymentError({
2810
- error: errorMessage,
2811
- userAddress: account.address,
2812
- mode,
2813
- fundMethod,
2814
- originTokenAddress,
2815
- })
2816
-
2817
- // Call onCheckoutError callback if provided
2818
- if (checkoutOnHandlers?.triggerCheckoutError) {
2819
- checkoutOnHandlers.triggerCheckoutError(errorMessage)
2820
- }
2821
-
2822
- // Re-throw the error so caller can handle if needed
2823
- throw error
2824
- }
2825
- },
2826
- }
2827
- }
2828
-
2829
- // This handler uses relay sdk directly
2830
- async function _sendHandlerForSameChainDifferentToken({
2831
- originTokenAddress,
2832
- originTokenDecimals,
2833
- swapAmount,
2834
- destinationTokenAddress,
2835
- destinationTokenDecimals,
2836
- destinationCalldata,
2837
- recipient,
2838
- originChainId,
2839
- walletClient,
2840
- publicClient,
2841
- account,
2842
- tradeType = TradeType.EXACT_OUTPUT,
2843
- slippageTolerance,
2844
- onTransactionStateChange,
2845
- transactionStates,
2846
- sourceTokenPriceUsd,
2847
- destinationTokenPriceUsd,
2848
- originNativeTokenPriceUsd,
2849
- mode,
2850
- fundMethod,
2851
- originTokenSymbol,
2852
- destinationTokenSymbol,
2853
- }: {
2854
- originTokenAddress: string
2855
- originTokenDecimals: number
2856
- swapAmount: string
2857
- destinationTokenAddress: string
2858
- destinationTokenDecimals: number
2859
- destinationCalldata?: string
2860
- recipient: string
2861
- originChainId: number
2862
- walletClient: WalletClient
2863
- publicClient: PublicClient
2864
- account: Account
2865
- tradeType?: TradeType
2866
- slippageTolerance?: string
2867
- onTransactionStateChange: (transactionStates: TransactionState[]) => void
2868
- transactionStates: TransactionState[]
2869
- sourceTokenPriceUsd?: number | null
2870
- destinationTokenPriceUsd?: number | null
2871
- originNativeTokenPriceUsd?: number | null
2872
- mode?: "pay" | "fund" | "earn" | "swap" | "receive"
2873
- fundMethod?: string
2874
- originTokenSymbol: string
2875
- destinationTokenSymbol: string
2876
- }): Promise<PrepareSendReturn> {
2877
- const destinationTxs: { to: string; value: string; data: string }[] = []
2878
- const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
2879
- if (hasCustomCalldata && tradeType === TradeType.EXACT_OUTPUT) {
2880
- destinationTxs.push({
2881
- to: recipient,
2882
- value: destinationTokenAddress === zeroAddress ? swapAmount : "0",
2883
- data: destinationCalldata as `0x${string}`,
2884
- })
2885
- } else if (hasCustomCalldata && tradeType === TradeType.EXACT_INPUT) {
2886
- destinationTxs.push({
2887
- to: recipient,
2888
- value: "0",
2889
- data: destinationCalldata as `0x${string}`,
2890
- })
2891
- }
2892
-
2893
- const quote = await getRelaySDKQuote({
2894
- wallet: walletClient,
2895
- chainId: originChainId,
2896
- amount: swapAmount,
2897
- currency: originTokenAddress,
2898
- toCurrency: destinationTokenAddress,
2899
- txs: destinationTxs,
2900
- tradeType: tradeType as unknown as RelayTradeType,
2901
- slippageTolerance: slippageTolerance,
2902
- recipient: hasCustomCalldata ? recipient : undefined,
2903
- })
2904
-
2905
- logger.console.log("[trails-sdk] relaysdk quote", quote)
2906
- let depositAmount = "0"
2907
- let destinationTokenAmount = "0"
2908
-
2909
- if (tradeType === TradeType.EXACT_INPUT) {
2910
- depositAmount = swapAmount
2911
- destinationTokenAmount = quote?.details?.currencyOut?.amount?.toString()
2912
- } else {
2913
- try {
2914
- destinationTokenAmount = swapAmount
2915
- depositAmount = quote.steps?.[0]?.items?.[0]?.data?.value
2916
- if (originTokenAddress !== zeroAddress) {
2917
- const decoded = decodeFunctionData({
2918
- abi: erc20Abi,
2919
- data: quote.steps?.[0]?.items?.[0]?.data?.data,
2920
- })
2921
- if (decoded.functionName === "approve") {
2922
- depositAmount = decoded.args[1].toString()
2923
- }
2924
- if (decoded.functionName === "transfer") {
2925
- depositAmount = decoded.args[1].toString()
2926
- }
2927
- }
2928
- } catch (error) {
2929
- logger.console.error("[trails-sdk] Error decoding function data:", error)
2930
- }
2931
- }
2932
-
2933
- const depositOriginAddress =
2934
- quote?.steps?.[quote?.steps?.length - 1]?.items?.[
2935
- quote?.steps?.[quote?.steps?.length - 1]?.items?.length - 1
2936
- ]?.data?.to
2937
-
2938
- if (
2939
- quote?.details?.currencyIn?.amountFormatted &&
2940
- quote?.details?.currencyIn?.amountUsd
2941
- ) {
2942
- const quoteProviderSourceTokenPriceUsd =
2943
- Number(quote.details.currencyIn.amountUsd) /
2944
- Number(quote.details.currencyIn.amountFormatted)
2945
- if (quoteProviderSourceTokenPriceUsd) {
2946
- sourceTokenPriceUsd = quoteProviderSourceTokenPriceUsd
2947
- }
2948
- }
2949
-
2950
- if (
2951
- quote?.details?.currencyOut?.amountFormatted &&
2952
- quote?.details?.currencyOut?.amountUsd
2953
- ) {
2954
- const quoteProviderDestinationTokenPriceUsd =
2955
- Number(quote.details.currencyOut.amountUsd) /
2956
- Number(quote.details.currencyOut.amountFormatted)
2957
- if (quoteProviderDestinationTokenPriceUsd) {
2958
- destinationTokenPriceUsd = quoteProviderDestinationTokenPriceUsd
2959
- }
2960
- }
2961
-
2962
- const normalizedQuote = await getNormalizedQuoteObject({
2963
- originDepositAddress: depositOriginAddress,
2964
- destinationDepositAddress: recipient,
2965
- destinationAddress: recipient,
2966
- destinationCalldata,
2967
- originAmount: depositAmount,
2968
- destinationAmount: destinationTokenAmount,
2969
- originTokenAddress: originTokenAddress,
2970
- destinationTokenAddress: destinationTokenAddress,
2971
- originTokenPriceUsd: sourceTokenPriceUsd?.toString() || null,
2972
- destinationTokenPriceUsd: destinationTokenPriceUsd?.toString() || null,
2973
- fees: getFeesFromRelaySdkQuote(quote),
2974
- slippageTolerance: getSlippageToleranceFromRelaySdkQuote(quote),
2975
- priceImpact: getPriceImpactFromRelaySdkQuote(quote),
2976
- priceImpactUsd: getPriceImpactUsdFromRelaySdkQuote(quote),
2977
- transactionStates,
2978
- originChainId,
2979
- destinationChainId: originChainId,
2980
- originNativeTokenPriceUsd,
2981
- quoteProvider: "relay",
2982
- intent: undefined,
2983
- })
2984
-
2985
- return {
2986
- quote: normalizedQuote,
2987
- send: async ({
2988
- onOriginSend,
2989
- }: {
2990
- onOriginSend?: () => void
2991
- }): Promise<SendReturn> => {
2992
- const { hasEnoughBalance, balanceError } = await checkAccountBalance({
2993
- account,
2994
- tokenAddress: originTokenAddress,
2995
- depositAmount,
2996
- publicClient,
2997
- })
2998
-
2999
- if (!hasEnoughBalance) {
3000
- throw balanceError
3001
- }
3002
-
3003
- await attemptSwitchChain({
3004
- walletClient,
3005
- desiredChainId: originChainId,
3006
- })
3007
-
3008
- const result = await executeSimpleRelayTransaction(quote, walletClient)
3009
- logger.console.log("[trails-sdk] relaysdk result", result)
3010
-
3011
- const txHash = getTxHashFromRelayResult(result)
3012
-
3013
- if (onOriginSend) {
3014
- onOriginSend()
3015
- }
3016
-
3017
- const originUserTxReceipt = await publicClient.waitForTransactionReceipt({
3018
- hash: txHash as `0x${string}`,
3019
- })
3020
-
3021
- transactionStates[0] = getTransactionStateFromReceipt(
3022
- originUserTxReceipt,
3023
- originChainId,
3024
- transactionStates[0]?.label,
3025
- )
3026
-
3027
- try {
3028
- onTransactionStateChange(transactionStates)
3029
- } catch (error) {
3030
- logger.console.error(
3031
- "[trails-sdk] Error calling onTransactionStateChange:",
3032
- error,
3033
- )
3034
- }
3035
-
3036
- const depositAmountFormatted = Number(
3037
- formatUnits(BigInt(depositAmount), originTokenDecimals),
3038
- )
3039
-
3040
- const depositAmountUsd = calcAmountUsdPrice({
3041
- amount: depositAmountFormatted,
3042
- usdPrice: sourceTokenPriceUsd,
3043
- })
3044
-
3045
- const destinationTokenAmountFormatted = Number(
3046
- formatUnits(BigInt(destinationTokenAmount), destinationTokenDecimals),
3047
- )
3048
-
3049
- const destinationTokenAmountUsd = calcAmountUsdPrice({
3050
- amount: Number(destinationTokenAmountFormatted),
3051
- usdPrice: destinationTokenPriceUsd,
3052
- })
3053
-
3054
- // Track payment completion for same-chain different-token transaction
3055
- if (originUserTxReceipt && originUserTxReceipt.status === "success") {
3056
- trackPaymentCompleted({
3057
- userAddress: account.address,
3058
- originTxHash: originUserTxReceipt.transactionHash,
3059
- originChainId,
3060
- destinationChainId: originChainId, // Same chain
3061
- mode,
3062
- fundMethod,
3063
- originTokenAddress,
3064
- originTokenSymbol,
3065
- destinationTokenAddress,
3066
- destinationTokenSymbol,
3067
- depositTokenAmountUsd: depositAmountUsd?.toString(),
3068
- destinationTokenAmountUsd: destinationTokenAmountUsd?.toString(),
3069
- })
3070
- } else if (originUserTxReceipt) {
3071
- trackPaymentError({
3072
- error: "Relay transaction failed",
3073
- userAddress: account.address,
3074
- mode,
3075
- fundMethod,
3076
- originTokenAddress,
3077
- originTokenSymbol,
3078
- destinationTokenAddress,
3079
- destinationTokenSymbol,
3080
- })
3081
- }
3082
-
3083
- return {
3084
- originUserTxReceipt: originUserTxReceipt,
3085
- originMetaTxnReceipt: null,
3086
- destinationMetaTxnReceipt: null,
3087
- totalCompletionSeconds: 0,
3088
- }
3089
- },
3090
- }
3091
- }
3092
-
3093
- async function attemptGaslessDeposit({
3094
- paymasterUrl,
3095
- depositTokenAddress,
3096
- depositTokenAmount,
3097
- depositRecipient,
3098
- onOriginSend,
3099
- walletClient,
3100
- chain,
3101
- account,
3102
- trailsClient,
3103
- originRelayer,
3104
- feeOptions,
3105
- selectedFeeToken,
3106
- abortSignal,
3107
- }: {
3108
- paymasterUrl?: string
3109
- depositTokenAddress: string
3110
- depositTokenAmount: string
3111
- depositRecipient: string
3112
- onOriginSend?: () => void
3113
- walletClient: WalletClient
3114
- chain: Chain
3115
- account: Account
3116
- trailsClient: TrailsAPIClient
3117
- originRelayer: Relayer.RpcRelayer
3118
- feeOptions: any
3119
- selectedFeeToken?: any
3120
- abortSignal?: AbortSignal
3121
- }): Promise<TransactionReceipt | null> {
3122
- let originUserTxReceipt: TransactionReceipt | null = null
3123
- const originChainId = chain.id
3124
-
3125
- logger.console.log(
3126
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] attemptGaslessDeposit called with:",
3127
- {
3128
- originChainId,
3129
- depositTokenAddress,
3130
- depositTokenAmount,
3131
- depositRecipient,
3132
- hasFeeOptions: !!feeOptions,
3133
- feeOptionsLength: feeOptions?.feeOptions?.length,
3134
- selectedFeeToken,
3135
- hasSelectedFeeToken: !!selectedFeeToken,
3136
- paymasterUrl,
3137
- },
3138
- )
3139
-
3140
- const publicClient = createPublicClient({
3141
- chain,
3142
- transport: http(),
3143
- })
3144
-
3145
- logger.console.log("[trails-sdk] [GASLESS-FLOW] Intent entrypoint check:", {
3146
- chainId: chain.id,
3147
- chainName: chain.name,
3148
- intentEntrypoint: TRAILS_INTENT_ENTRYPOINT_ADDRESS,
3149
- })
3150
-
3151
- // NEW FLOW: Use Intent Entrypoint API with permit2 support
3152
- logger.console.log(
3153
- "[trails-sdk] Using Intent Entrypoint API flow with permit2 support for gasless deposit",
3154
- )
3155
-
3156
- // Switch to correct chain before requesting signatures
3157
- logger.console.log(
3158
- "[trails-sdk] [GASLESS-FLOW] Switching to chain before permit/intent signatures",
3159
- { originChainId },
3160
- )
3161
- await attemptSwitchChain({
3162
- walletClient,
3163
- desiredChainId: originChainId,
3164
- })
3165
-
3166
- try {
3167
- if (paymasterUrl) {
3168
- logger.console.log(
3169
- "[trails-sdk] [GASLESS-FLOW] doing gasless with paymaster",
3170
- )
3171
-
3172
- // Switch to correct chain before requesting signatures
3173
- logger.console.log(
3174
- "[trails-sdk] [GASLESS-FLOW] Switching chain for paymaster flow",
3175
- )
3176
- await attemptSwitchChain({
3177
- walletClient,
3178
- desiredChainId: originChainId,
3179
- })
3180
-
3181
- const delegatorSmartAccount = await getDelegatorSmartAccount({
3182
- publicClient,
3183
- })
3184
-
3185
- const calls: Array<{
3186
- to: string
3187
- data: string
3188
- value: string
3189
- }> = await getPaymasterGaslessTransaction({
3190
- walletClient,
3191
- chain,
3192
- tokenAddress: depositTokenAddress as `0x${string}`,
3193
- amount: BigInt(depositTokenAmount),
3194
- recipient: depositRecipient as `0x${string}`,
3195
- delegatorSmartAccount,
3196
- })
3197
-
3198
- logger.console.log("[trails-sdk] calls", calls)
3199
-
3200
- const txHash = await sendPaymasterGaslessTransaction({
3201
- walletClient,
3202
- publicClient,
3203
- chain,
3204
- paymasterUrl,
3205
- delegatorSmartAccount,
3206
- calls,
3207
- })
3208
-
3209
- if (onOriginSend) {
3210
- onOriginSend()
3211
- }
3212
-
3213
- const receipt = await publicClient.waitForTransactionReceipt({
3214
- hash: txHash as `0x${string}`,
3215
- })
3216
- logger.console.log("[trails-sdk] receipt", receipt)
3217
- return receipt
3218
- }
3219
-
3220
- const deadline = Math.floor(Date.now() / 1000) + 3600 // 1 hour from now
3221
- const hasFeeOptions = Boolean(
3222
- feeOptions && feeOptions.feeOptions?.length > 0,
3223
- )
3224
-
3225
- // 1. Check if we need approval - check if we have enough allowance for this transaction (deposit + fee if same token)
3226
- let requiredAmount = BigInt(depositTokenAmount)
3227
-
3228
- // Match selectedFeeToken by tokenAddress to get the latest fee amount from feeOptions
3229
- let selectedFeeOption = null
3230
- if (selectedFeeToken && hasFeeOptions) {
3231
- // Find matching fee option by tokenAddress to get latest amount
3232
- selectedFeeOption = feeOptions.feeOptions.find(
3233
- (opt: any) =>
3234
- opt.tokenAddress?.toLowerCase() ===
3235
- selectedFeeToken.tokenAddress?.toLowerCase(),
3236
- )
3237
- logger.console.log(
3238
- "[trails-sdk] Matched selectedFeeToken to latest fee option:",
3239
- {
3240
- selectedFeeToken,
3241
- matchedOption: selectedFeeOption,
3242
- },
3243
- )
3244
- }
3245
-
3246
- // Fallback to first fee option if no match or no selectedFeeToken
3247
- if (!selectedFeeOption && hasFeeOptions) {
3248
- selectedFeeOption = feeOptions.feeOptions[0]
3249
- logger.console.log(
3250
- "[trails-sdk] Using first fee option as fallback:",
3251
- selectedFeeOption,
3252
- )
3253
- }
3254
-
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
- }
3285
- }
3286
-
3287
- const needsApproval = await getNeedsIntentEntrypointApproval({
3288
- client: publicClient,
3289
- token: depositTokenAddress as `0x${string}`,
3290
- account: account.address,
3291
- entrypoint: TRAILS_INTENT_ENTRYPOINT_ADDRESS,
3292
- amount: requiredAmount, // Check if we have enough allowance for this specific transaction
3293
- })
3294
-
3295
- logger.console.log(
3296
- "[trails-sdk] [GASLESS-FLOW] Checking permit requirements",
3297
- {
3298
- userAddress: account.address,
3299
- tokenAddress: depositTokenAddress,
3300
- depositAmount: depositTokenAmount,
3301
- requiredAmount: requiredAmount.toString(),
3302
- intentAddress: depositRecipient,
3303
- chainID: originChainId,
3304
- deadline,
3305
- needsApproval,
3306
- },
3307
- )
3308
-
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
3311
- let permitSignature: string | undefined
3312
- let permitDeadline: number | undefined
3313
-
3314
- if (needsApproval) {
3315
- logger.console.log(
3316
- "[trails-sdk] Getting permit signature for deposit token infinite approval",
3317
- )
3318
-
3319
- // Use infinite approval (maxUint256) so user doesn't need to approve again
3320
- const permitAmount = maxUint256
3321
- logger.console.log(
3322
- "[trails-sdk] Using infinite approval for gasless deposits",
3323
- {
3324
- depositAmount: depositTokenAmount,
3325
- depositTokenAddress,
3326
- permitAmount: permitAmount.toString(),
3327
- },
3328
- )
3329
-
3330
- const permitSig = await getPermitSignature({
3331
- publicClient,
3332
- walletClient,
3333
- signer: account.address,
3334
- spender: TRAILS_INTENT_ENTRYPOINT_ADDRESS,
3335
- tokenAddress: depositTokenAddress as `0x${string}`,
3336
- amount: permitAmount, // Infinite approval
3337
- chain,
3338
- deadline: BigInt(deadline),
3339
- })
3340
- permitSignature = permitSig.signature
3341
- permitDeadline = Number(permitSig.deadline)
3342
- logger.console.log(
3343
- "[trails-sdk] Deposit token permit signature obtained for infinite approval",
3344
- )
3345
- }
3346
-
3347
- // 3. Get current nonce for the user
3348
- logger.console.log("[trails-sdk] Getting user nonce")
3349
- const nonce = await getUserNonce({
3350
- publicClient,
3351
- userAddress: account.address,
3352
- intentEntrypoint: TRAILS_INTENT_ENTRYPOINT_ADDRESS,
3353
- })
3354
- logger.console.log("[trails-sdk] User nonce:", nonce.toString())
3355
-
3356
- // 4. Get intent signature
3357
- logger.console.log("[trails-sdk] Requesting intent signature via EIP-712")
3358
- // Get fee collector address from fee options response, or use selected fee option's collector
3359
- const feeCollectorAddress = (selectedFeeOption?.feeCollector ||
3360
- feeOptions?.feeCollector) as `0x${string}` | undefined
3361
-
3362
- // Validate that we have a valid fee collector address
3363
- if (!feeCollectorAddress || feeCollectorAddress === zeroAddress) {
3364
- throw new Error(
3365
- "[trails-sdk] Fee collector address not provided by API. Cannot proceed with gasless deposit. " +
3366
- "Please ensure the API is returning feeCollector in the fee options response.",
3367
- )
3368
- }
3369
-
3370
- logger.console.log(
3371
- "[trails-sdk] Using fee collector address:",
3372
- feeCollectorAddress,
3373
- )
3374
-
3375
- const { signature: intentSignature } = await signIntent({
3376
- client: walletClient,
3377
- intentParams: {
3378
- user: account.address,
3379
- token: depositTokenAddress as `0x${string}`,
3380
- amount: BigInt(depositTokenAmount),
3381
- intentAddress: depositRecipient as `0x${string}`,
3382
- deadline: BigInt(deadline),
3383
- chainId: originChainId,
3384
- contractAddress: TRAILS_INTENT_ENTRYPOINT_ADDRESS,
3385
- nonce,
3386
- feeAmount: BigInt(selectedFeeOption?.amount || "0"),
3387
- feeCollector: feeCollectorAddress,
3388
- },
3389
- })
3390
- logger.console.log("[trails-sdk] Intent signature received")
3391
-
3392
- // 5. Call the deposit endpoint with permit support and optional fee
3393
- logger.console.log(
3394
- "[trails-sdk] Calling getIntentEntrypointDeposit with permit enabled",
3395
- { usePermit: needsApproval, hasFee: !!feeOptions },
3396
- )
3397
-
3398
- // selectedFeeOption was already determined at the start of the try block
3399
-
3400
- logger.console.log(
3401
- "[trails-sdk] Calling getIntentEntrypointDeposit with params:",
3402
- {
3403
- userAddress: account.address,
3404
- tokenAddress: depositTokenAddress,
3405
- amount: depositTokenAmount,
3406
- intentAddress: depositRecipient,
3407
- chainID: originChainId,
3408
- deadline,
3409
- usePermit: needsApproval,
3410
- hasPermitSignature: !!permitSignature,
3411
- feeAmount: selectedFeeOption?.amount,
3412
- feeTokenSymbol: selectedFeeOption?.tokenSymbol,
3413
- selectedFeeToken,
3414
- usingSelectedFeeToken: !!selectedFeeToken,
3415
- },
3416
- )
3417
-
3418
- const depositDataResponse = await trailsClient.getIntentEntrypointDeposit({
3419
- params: {
3420
- userAddress: account.address,
3421
- tokenAddress: depositTokenAddress,
3422
- amount: depositTokenAmount,
3423
- intentAddress: depositRecipient,
3424
- chainID: originChainId,
3425
- deadline,
3426
- intentSignature,
3427
- usePermit: needsApproval, // Use permit if approval needed
3428
- permitAmount: needsApproval ? maxUint256.toString() : undefined, // Pass infinite approval amount
3429
- permitSignature,
3430
- permitDeadline,
3431
- feeAmount: selectedFeeOption?.amount || undefined,
3432
- },
3433
- })
3434
-
3435
- const depositData = depositDataResponse.result
3436
- logger.console.log("[trails-sdk] Deposit data received:", {
3437
- depositWalletAddress: depositData.depositWalletAddress,
3438
- entrypointAddress: depositData.entrypointAddress,
3439
- metaTxnId: depositData.metaTxn?.id,
3440
- usedPermit2: needsApproval,
3441
- })
3442
-
3443
- // 5. Send meta transaction via relayer
3444
- logger.console.log("[trails-sdk] Sending meta transaction to relayer")
3445
- const feeQuoteObj = depositData.feeQuote
3446
- ? ({
3447
- _tag: "FeeQuote",
3448
- _quote: { toJSON: () => depositData.feeQuote },
3449
- } as any)
3450
- : undefined
3451
-
3452
- const opHash = await relayerSendMetaTx(
3453
- originRelayer,
3454
- depositData.metaTxn as any,
3455
- [], // No preconditions for gasless deposit
3456
- feeQuoteObj,
3457
- )
3458
- logger.console.log("[trails-sdk] Meta transaction sent, opHash:", opHash)
3459
-
3460
- if (onOriginSend) {
3461
- onOriginSend()
3462
- }
3463
-
3464
- // 6. Wait for transaction receipt
3465
- logger.console.log("[trails-sdk] Waiting for transaction receipt")
3466
- // eslint-disable-next-line no-constant-condition
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
-
3476
- const receipt: any = await getMetaTxStatus(
3477
- originRelayer,
3478
- depositData.metaTxn.id,
3479
- Number(depositData.metaTxn.chainId),
3480
- )
3481
- logger.console.log("[trails-sdk] Meta transaction status:", receipt)
3482
-
3483
- if (receipt?.transactionHash) {
3484
- const metaTxnReceipt = receipt.data?.receipt
3485
- if (!metaTxnReceipt) {
3486
- throw new Error("No meta txn receipt found")
3487
- }
3488
-
3489
- // Get the full transaction receipt
3490
- const txReceipt = await publicClient.getTransactionReceipt({
3491
- hash: receipt.transactionHash as `0x${string}`,
3492
- })
3493
- logger.console.log("[trails-sdk] Transaction receipt:", txReceipt)
3494
- originUserTxReceipt = txReceipt
3495
- break
3496
- }
3497
-
3498
- await new Promise((resolve) =>
3499
- setTimeout(resolve, POLLING_INTERVALS.META_TRANSACTION),
3500
- )
3501
- }
3502
- } catch (error) {
3503
- logger.console.error(
3504
- "[trails-sdk] Error in Intent Entrypoint gasless deposit with permit2:",
3505
- error,
3506
- )
3507
- throw error
3508
- }
3509
-
3510
- return originUserTxReceipt
3511
- }
3512
-
3513
- export async function attemptNonGaslessUserDeposit({
3514
- originTokenAddress,
3515
- firstPreconditionMin,
3516
- onOriginSend,
3517
- publicClient,
3518
- walletClient,
3519
- originChainId,
3520
- chain,
3521
- account,
3522
- fee,
3523
- dryMode,
3524
- sourceTokenPriceUsd,
3525
- destinationTokenPriceUsd,
3526
- swapAmount,
3527
- destinationTokenDecimals,
3528
- sourceTokenDecimals,
3529
- originIntentAddress,
3530
- onTransactionStateChange,
3531
- transactionStates,
3532
- originTokenSymbol,
3533
- destinationTokenSymbol,
3534
- depositAmountUsd,
3535
- }: {
3536
- originTokenAddress: string
3537
- firstPreconditionMin: string
3538
- onOriginSend?: () => void
3539
- publicClient: PublicClient
3540
- walletClient: WalletClient
3541
- originChainId: number
3542
- chain: Chain
3543
- account: Account
3544
- fee: string
3545
- dryMode: boolean
3546
- sourceTokenPriceUsd?: number | null
3547
- destinationTokenPriceUsd?: number | null
3548
- swapAmount: string
3549
- destinationTokenDecimals: number
3550
- sourceTokenDecimals: number
3551
- originIntentAddress: string
3552
- onTransactionStateChange: (transactionStates: TransactionState[]) => void
3553
- transactionStates: TransactionState[]
3554
- originTokenSymbol: string
3555
- destinationTokenSymbol: string
3556
- depositAmountUsd: number
3557
- }): Promise<TransactionReceipt | null> {
3558
- let originUserTxReceipt: TransactionReceipt | null = null
3559
- const usingLIfi = false
3560
- let needsNativeFee = false
3561
-
3562
- if (usingLIfi) {
3563
- needsNativeFee = await getNeedsLifiNativeFee({
3564
- originTokenAddress,
3565
- destinationTokenAmount: swapAmount,
3566
- destinationTokenDecimals,
3567
- sourceTokenDecimals,
3568
- sourceTokenPriceUsd: sourceTokenPriceUsd ?? null,
3569
- destinationTokenPriceUsd: destinationTokenPriceUsd ?? null,
3570
- depositAmount: firstPreconditionMin,
3571
- })
3572
- }
3573
- let nativeFee = parseUnits("0.00005", 18).toString()
3574
- if (originChainId === 137) {
3575
- nativeFee = parseUnits("1.5", 18).toString()
3576
- }
3577
-
3578
- logger.console.log("[trails-sdk] needsNativeFee", needsNativeFee)
3579
-
3580
- // Build origin call params
3581
- const originCallParamsBase = buildCrossChainDepositParams({
3582
- originTokenAddress,
3583
- originIntentAddress,
3584
- depositAmount: firstPreconditionMin,
3585
- fee,
3586
- originChainId,
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,
3605
- }
3606
-
3607
- await attemptSwitchChain({
3608
- walletClient,
3609
- desiredChainId: originChainId,
3610
- })
3611
-
3612
- let useSendCalls = false
3613
- const moreThan1Tx = needsNativeFee
3614
-
3615
- if (moreThan1Tx) {
3616
- try {
3617
- // the reason for the timeout is some users experience this call to hang indefinitely on metamask on all chains.
3618
- // not sure why this is happening, but it happens.
3619
- const capabilities = await requestWithTimeout<Record<string, any>>(
3620
- walletClient,
3621
- [
3622
- {
3623
- method: "wallet_getCapabilities",
3624
- params: [account.address],
3625
- },
3626
- ],
3627
- 10000,
3628
- )
3629
-
3630
- logger.console.log("[trails-sdk] capabilities", capabilities)
3631
-
3632
- // Check if the chain supports atomic transactions
3633
- const chainHex = `0x${originChainId.toString(16)}` as const
3634
- const chainCapabilities = capabilities[chainHex]
3635
- useSendCalls = chainCapabilities?.atomic?.status === "supported"
3636
- } catch (error) {
3637
- logger.console.error("[trails-sdk] Error getting capabilities", error)
3638
- }
3639
- }
3640
-
3641
- if (dryMode) {
3642
- logger.console.log("[trails-sdk] dry mode, skipping send calls")
3643
- }
3644
-
3645
- if (useSendCalls) {
3646
- logger.console.log("[trails-sdk] using sendCalls")
3647
- } else {
3648
- logger.console.log("[trails-sdk] using sendTransaction")
3649
- }
3650
-
3651
- if (useSendCalls) {
3652
- if (!dryMode) {
3653
- const calls: Array<{
3654
- to: `0x${string}`
3655
- data: `0x${string}`
3656
- value?: `0x${string}`
3657
- }> = []
3658
- if (needsNativeFee) {
3659
- calls.push({
3660
- to: originIntentAddress as `0x${string}`,
3661
- data: "0x00",
3662
- value: `0x${BigInt(nativeFee).toString(16)}` as `0x${string}`,
3663
- })
3664
- }
3665
-
3666
- // Add the origin call
3667
- calls.push({
3668
- to: originCallParams.to as `0x${string}`,
3669
- data: originCallParams.data as `0x${string}`,
3670
- value: originCallParams.value
3671
- ? `0x${BigInt(originCallParams.value).toString(16)}`
3672
- : "0x0",
3673
- })
3674
-
3675
- // Update persistent toast before wallet interaction
3676
- updatePersistentToast(
3677
- "Waiting for Confirmation",
3678
- "Please confirm the transaction in your wallet...",
3679
- "info",
3680
- )
3681
-
3682
- // Send the batched call via EIP-7702
3683
- const result = (await walletClient.request({
3684
- method: "wallet_sendCalls",
3685
- params: [
3686
- {
3687
- version: "2.0.0",
3688
- chainId: `0x${originChainId.toString(16)}`,
3689
- atomicRequired: true,
3690
- calls,
3691
- },
3692
- ],
3693
- })) as { requestId: `0x${string}` }
3694
-
3695
- logger.console.log("[trails-sdk] sendCalls result", result)
3696
- const requestId = result.requestId || (result as any).id
3697
-
3698
- // Poll to check if the tx has been submitted
3699
- let txHash: `0x${string}` | undefined
3700
- while (!txHash) {
3701
- const status = (await walletClient.request({
3702
- method: "wallet_getCallsStatus",
3703
- params: [requestId],
3704
- })) as {
3705
- status: "pending" | "submitted" | "failed"
3706
- transactionHash?: `0x${string}`
3707
- error?: string
3708
- }
3709
-
3710
- logger.console.log("[trails-sdk] getCallsStatus result", status)
3711
- const receipt = (status as any)?.receipts?.[0]
3712
-
3713
- if ((status as any).status === 200 && receipt?.transactionHash) {
3714
- txHash = receipt.transactionHash
3715
- break
3716
- } else if ((status as any).status === 500) {
3717
- throw new Error(`Transaction failed: ${status.error}`)
3718
- }
3719
-
3720
- // wait a bit before polling again
3721
- await new Promise((r) => setTimeout(r, 2000))
3722
- }
3723
-
3724
- if (onOriginSend) {
3725
- onOriginSend()
3726
- }
3727
-
3728
- // Update persistent toast after transaction sent
3729
- const chainInfo = getChainInfo(originChainId)
3730
- updatePersistentToast(
3731
- "Transaction Submitted",
3732
- `Waiting for confirmation on ${chainInfo?.name || "chain"}...`,
3733
- "info",
3734
- )
3735
-
3736
- const receipt = await publicClient.waitForTransactionReceipt({
3737
- hash: txHash as `0x${string}`,
3738
- })
3739
- logger.console.log("[trails-sdk] receipt", receipt)
3740
- originUserTxReceipt = receipt
3741
- }
3742
- } else {
3743
- if (!dryMode) {
3744
- if (needsNativeFee) {
3745
- const txHashNativeFee = await sendOriginTransaction(
3746
- account,
3747
- walletClient,
3748
- {
3749
- to: originIntentAddress,
3750
- data: "0x00",
3751
- value: nativeFee,
3752
- chainId: originChainId,
3753
- chain,
3754
- } as any,
3755
- {
3756
- depositTokenAmountUsd: depositAmountUsd?.toString(),
3757
- },
3758
- ) // TODO: Add proper type
3759
- logger.console.log("[trails-sdk] origin tx native fee", txHashNativeFee)
3760
- // Wait for transaction receipt
3761
- const feeReceipt = await publicClient.waitForTransactionReceipt({
3762
- hash: txHashNativeFee,
3763
- })
3764
- logger.console.log("[trails-sdk] nativeFeeReceipt", feeReceipt)
3765
- }
3766
-
3767
- // Show persistent toast for checkout flow
3768
- updatePersistentToast(
3769
- "Payment Started",
3770
- "Waiting for wallet confirmation...",
3771
- "info",
3772
- )
3773
-
3774
- const txHash = await sendOriginTransaction(
3775
- account,
3776
- walletClient,
3777
- originCallParams as any,
3778
- {
3779
- depositTokenAmountUsd: depositAmountUsd?.toString(),
3780
- },
3781
- ) // TODO: Add proper type
3782
- logger.console.log("[trails-sdk] origin tx", txHash)
3783
-
3784
- if (onOriginSend) {
3785
- onOriginSend()
3786
- }
3787
-
3788
- if (transactionStates[0]) {
3789
- transactionStates[0].state = "pending"
3790
-
3791
- try {
3792
- onTransactionStateChange(transactionStates)
3793
- } catch (error) {
3794
- logger.console.error(
3795
- "[trails-sdk] Error calling onTransactionStateChange:",
3796
- error,
3797
- )
3798
- }
3799
- }
3800
-
3801
- // Wait for transaction receipt
3802
- const receipt = await publicClient.waitForTransactionReceipt({
3803
- hash: txHash,
3804
- })
3805
-
3806
- const chainInfo = getChainInfo(originChainId)
3807
- updatePersistentToast(
3808
- "Transfer Confirmed",
3809
- `Your transaction on ${chainInfo?.name || "chain"} has been confirmed`,
3810
- "info",
3811
- )
3812
-
3813
- trackTransactionConfirmed({
3814
- transactionHash: txHash,
3815
- chainId: originChainId,
3816
- userAddress: account.address,
3817
- blockNumber: Number(receipt.blockNumber),
3818
- originIntentAddress,
3819
- originTokenSymbol,
3820
- destinationTokenSymbol,
3821
- depositTokenAmountUsd: depositAmountUsd?.toString(),
3822
- })
3823
-
3824
- logger.console.log("[trails-sdk] receipt", receipt)
3825
- originUserTxReceipt = receipt
3826
- }
3827
- }
3828
-
3829
- return originUserTxReceipt
3830
- }
3831
-
3832
- async function attemptUserDepositTx({
3833
- originTokenAddress,
3834
- gasless,
3835
- paymasterUrl,
3836
- chain,
3837
- account,
3838
- originRelayer,
3839
- firstPreconditionMin,
3840
- originIntentAddress,
3841
- onOriginSend,
3842
- publicClient,
3843
- walletClient,
3844
- destinationTokenDecimals,
3845
- sourceTokenDecimals,
3846
- fee,
3847
- dryMode,
3848
- sourceTokenPriceUsd,
3849
- destinationTokenPriceUsd,
3850
- swapAmount,
3851
- onTransactionStateChange,
3852
- transactionStates,
3853
- fundMethod,
3854
- originTokenSymbol,
3855
- destinationTokenSymbol,
3856
- depositAmountUsd,
3857
- feeOptions,
3858
- trailsClient,
3859
- selectedFeeToken,
3860
- walletId,
3861
- abortSignal,
3862
- }: {
3863
- originTokenAddress: string
3864
- gasless: boolean
3865
- paymasterUrl?: string
3866
- chain: Chain
3867
- account: Account
3868
- originRelayer: Relayer.RpcRelayer
3869
- firstPreconditionMin: string
3870
- originIntentAddress: string
3871
- onOriginSend?: () => void
3872
- publicClient: PublicClient
3873
- walletClient: WalletClient
3874
- destinationTokenDecimals: number
3875
- sourceTokenDecimals: number
3876
- swapAmount: string
3877
- dryMode: boolean
3878
- sourceTokenPriceUsd: number | null
3879
- destinationTokenPriceUsd: number | null
3880
- fee: string
3881
- onTransactionStateChange: (transactionStates: TransactionState[]) => void
3882
- transactionStates: TransactionState[]
3883
- fundMethod?: string
3884
- originTokenSymbol: string
3885
- destinationTokenSymbol: string
3886
- depositAmountUsd: number
3887
- feeOptions?: any
3888
- trailsClient: TrailsAPIClient
3889
- selectedFeeToken?: any
3890
- walletId?: string
3891
- abortSignal?: AbortSignal
3892
- }): Promise<TransactionReceipt | null> {
3893
- let originUserTxReceipt: TransactionReceipt | null = null
3894
- const originChainId = chain.id
3895
-
3896
- logger.console.log(
3897
- "[trails-sdk] attemptUserDepositTx called with fundMethod:",
3898
- fundMethod,
3899
- )
3900
-
3901
- // Skip wallet deposit if fund method is qr-code
3902
- if (fundMethod === "qr-code" || fundMethod === "exchange") {
3903
- logger.console.log(
3904
- "[trails-sdk] Skipping wallet deposit for QR code mode - fundMethod is:",
3905
- fundMethod,
3906
- )
3907
- return null
3908
- }
3909
-
3910
- const doGasless = getDoGasless(
3911
- originTokenAddress,
3912
- gasless,
3913
- feeOptions,
3914
- selectedFeeToken,
3915
- walletId,
3916
- )
3917
- logger.console.log(
3918
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] doGasless check results:",
3919
- {
3920
- doGasless,
3921
- paymasterUrl,
3922
- hasFeeOptions: !!feeOptions,
3923
- feeOptionsCount: feeOptions?.feeOptions?.length || 0,
3924
- selectedFeeToken,
3925
- selectedFeeTokenSet:
3926
- selectedFeeToken !== null && selectedFeeToken !== undefined,
3927
- originTokenAddress,
3928
- gasless,
3929
- isNotNative: originTokenAddress !== zeroAddress,
3930
- },
3931
- )
3932
- if (doGasless || paymasterUrl) {
3933
- logger.console.log(
3934
- "[trails-sdk] [GASLESS-FLOW] Entering gasless deposit flow",
3935
- {
3936
- doGasless,
3937
- hasPaymasterUrl: !!paymasterUrl,
3938
- paymasterUrl,
3939
- },
3940
- )
3941
- try {
3942
- originUserTxReceipt = await attemptGaslessDeposit({
3943
- paymasterUrl,
3944
- depositTokenAddress: originTokenAddress,
3945
- depositTokenAmount: firstPreconditionMin,
3946
- depositRecipient: originIntentAddress,
3947
- onOriginSend,
3948
- walletClient,
3949
- chain,
3950
- account,
3951
- trailsClient,
3952
- originRelayer,
3953
- feeOptions: feeOptions,
3954
- selectedFeeToken: selectedFeeToken,
3955
- abortSignal,
3956
- })
3957
- } catch (error) {
3958
- logger.console.log("[trails-sdk] gassless attempt failed", error)
3959
- // In strict gasless mode, re-throw error instead of falling back
3960
- if (gasless) {
3961
- throw error
3962
- }
3963
- }
3964
- }
3965
-
3966
- // If gasless attempt failed, try to send a regular transaction
3967
- if (!originUserTxReceipt && !gasless) {
3968
- originUserTxReceipt = await attemptNonGaslessUserDeposit({
3969
- originTokenAddress,
3970
- firstPreconditionMin,
3971
- originIntentAddress,
3972
- onOriginSend,
3973
- publicClient,
3974
- walletClient,
3975
- originChainId,
3976
- chain,
3977
- account,
3978
- fee,
3979
- dryMode,
3980
- sourceTokenPriceUsd,
3981
- destinationTokenPriceUsd,
3982
- swapAmount,
3983
- destinationTokenDecimals,
3984
- sourceTokenDecimals,
3985
- onTransactionStateChange,
3986
- transactionStates,
3987
- originTokenSymbol,
3988
- destinationTokenSymbol,
3989
- depositAmountUsd,
3990
- })
3991
- }
3992
-
3993
- return originUserTxReceipt
3994
- }
3995
-
3996
- export function getDoGasless(
3997
- originTokenAddress: string,
3998
- gasless: boolean,
3999
- feeOptions?: any,
4000
- selectedFeeToken?: any,
4001
- walletId?: string,
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
-
4011
- const hasFeeOptions = Boolean(feeOptions && feeOptions.feeOptions?.length > 0)
4012
-
4013
- // Important: The UI passes selectedFeeToken in these states:
4014
- // - null: User explicitly chose "Pay with native gas"
4015
- // - {object}: User selected a fee token OR it was auto-selected
4016
- // - undefined: Should not happen (initial state auto-selects if options exist)
4017
-
4018
- // If selectedFeeToken is null, user explicitly chose native gas
4019
- if (selectedFeeToken === null) {
4020
- logger.console.log(
4021
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: User explicitly selected native gas (null)",
4022
- )
4023
- return false
4024
- }
4025
-
4026
- // If selectedFeeToken is undefined and no fee options, can't do gasless
4027
- if (!selectedFeeToken && !hasFeeOptions) {
4028
- logger.console.log(
4029
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: No fee token selected and no fee options available",
4030
- )
4031
- return false
4032
- }
4033
-
4034
- // If selectedFeeToken is undefined but fee options exist, use first ERC20 option
4035
- let effectiveFeeToken = selectedFeeToken
4036
- if (!effectiveFeeToken && hasFeeOptions) {
4037
- const firstFeeOption = feeOptions.feeOptions[0]
4038
-
4039
- // Check if first option is native gas
4040
- const isFirstOptionNative =
4041
- firstFeeOption?.tokenAddress === zeroAddress ||
4042
- firstFeeOption?.tokenAddress?.toLowerCase() === zeroAddress ||
4043
- firstFeeOption?.isNative === true
4044
-
4045
- if (!isFirstOptionNative && firstFeeOption?.tokenAddress) {
4046
- // First fee option is ERC20, use it
4047
- effectiveFeeToken = firstFeeOption
4048
- logger.console.log(
4049
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: No fee token selected, using first ERC20 fee option",
4050
- {
4051
- feeOption: effectiveFeeToken,
4052
- },
4053
- )
4054
- } else {
4055
- logger.console.log(
4056
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: First fee option is native gas, skipping gasless",
4057
- )
4058
- return false
4059
- }
4060
- }
4061
-
4062
- // Check if the effective fee token is native gas
4063
- const isNativeGasFee =
4064
- !effectiveFeeToken ||
4065
- effectiveFeeToken.tokenAddress === zeroAddress ||
4066
- effectiveFeeToken.tokenAddress?.toLowerCase() === zeroAddress ||
4067
- effectiveFeeToken.isNative === true
4068
-
4069
- // Don't use gasless if origin token is native (sending ETH)
4070
- if (originTokenAddress === zeroAddress) {
4071
- logger.console.log(
4072
- "[trails-sdk] [GASLESS-FLOW] getDoGasless: Origin token is native, skipping gasless",
4073
- )
4074
- return false
4075
- }
4076
-
4077
- // Don't use gasless if fee token is native
4078
- if (isNativeGasFee) {
4079
- logger.console.log(
4080
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: Fee token is native gas, skipping gasless",
4081
- {
4082
- effectiveFeeToken,
4083
- },
4084
- )
4085
- return false
4086
- }
4087
-
4088
- // Don't use gasless if disabled
4089
- if (!gasless) {
4090
- logger.console.log(
4091
- "[trails-sdk] [GASLESS-FLOW] getDoGasless: Gasless disabled",
4092
- )
4093
- return false
4094
- }
4095
-
4096
- // All conditions met, use gasless with ERC20 fee token
4097
- logger.console.log(
4098
- "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless decision: Using gasless",
4099
- {
4100
- originTokenAddress,
4101
- gasless,
4102
- hasFeeOptions,
4103
- selectedFeeToken,
4104
- effectiveFeeToken,
4105
- feeOptionsCount: feeOptions?.feeOptions?.length || 0,
4106
- },
4107
- )
4108
-
4109
- return true
4110
- }
4111
-
4112
- function getTransactionStateFromReceipt(
4113
- receipt: TransactionReceipt | MetaTxnReceipt,
4114
- chainId: number,
4115
- label: string = "Transaction",
4116
- ): TransactionState {
4117
- let txHash: string = ""
4118
- let state: TransactionStateStatus = "pending"
4119
- let blockNumber: number = 0
4120
- if ("transactionHash" in receipt) {
4121
- txHash = receipt.transactionHash
4122
- state = receipt.status === "success" ? "confirmed" : "failed"
4123
- blockNumber = Number(receipt.blockNumber)
4124
- } else if ("txnHash" in receipt) {
4125
- txHash = receipt.txnHash
4126
- state = receipt.status === "SUCCEEDED" ? "confirmed" : "failed"
4127
- blockNumber = Number(receipt.blockNumber)
4128
- }
4129
-
4130
- return {
4131
- transactionHash: txHash,
4132
- explorerUrl: getExplorerUrl({ txHash, chainId }),
4133
- chainId,
4134
- blockNumber,
4135
- state,
4136
- label,
4137
- }
4138
- }
4139
-
4140
- async function sendMetaTxAndWaitForReceipt({
4141
- metaTx,
4142
- relayer,
4143
- precondition,
4144
- feeQuote,
4145
- }: {
4146
- metaTx: MetaTxn
4147
- relayer: Relayer.RpcRelayer
4148
- precondition: IntentPrecondition | null
4149
- feeQuote?: string
4150
- }): Promise<{
4151
- waitForReceipt: (abortSignal?: AbortSignal) => Promise<MetaTxnReceipt | null>
4152
- }> {
4153
- try {
4154
- logger.console.log("[trails-sdk] metaTx", metaTx)
4155
- trackRelayerCallStarted({
4156
- walletAddress: metaTx.walletAddress as `0x${string}`,
4157
- contractAddress: metaTx.contract as `0x${string}`,
4158
- chainId: Number(metaTx.chainId),
4159
- })
4160
- const feeQuoteObj = feeQuote
4161
- ? ({ _tag: "FeeQuote", _quote: { toJSON: () => feeQuote } } as any)
4162
- : undefined
4163
- logger.console.log("[trails-sdk] feeQuote", feeQuoteObj)
4164
- const opHash = await relayerSendMetaTx(
4165
- relayer,
4166
- metaTx,
4167
- precondition ? [precondition] : ([] as IntentPrecondition[]),
4168
- feeQuoteObj,
4169
- )
4170
- logger.console.log("[trails-sdk] opHash", opHash)
4171
-
4172
- trackRelayerCallCompleted({
4173
- walletAddress: metaTx.walletAddress as `0x${string}`,
4174
- contractAddress: metaTx.contract as `0x${string}`,
4175
- chainId: Number(metaTx.chainId),
4176
- })
4177
- } catch (error) {
4178
- trackRelayerCallError({
4179
- walletAddress: metaTx.walletAddress as `0x${string}`,
4180
- contractAddress: metaTx.contract as `0x${string}`,
4181
- chainId: Number(metaTx.chainId),
4182
- error: error instanceof Error ? error.message : "Unknown error",
4183
- })
4184
- throw error
4185
- }
4186
-
4187
- return {
4188
- waitForReceipt: async (abortSignal?: AbortSignal) => {
4189
- let originMetaTxnReceipt: MetaTxnReceipt | null = null
4190
-
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
-
4211
- logger.console.log(
4212
- "[trails-sdk] polling status",
4213
- metaTx.id as `0x${string}`,
4214
- metaTx.chainId.toString(),
4215
- )
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
- }
4229
-
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
4236
- }
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
- })
4262
- }
4263
-
4264
- return originMetaTxnReceipt
4265
- } finally {
4266
- // Always unregister when polling completes or is aborted
4267
- abortControllerRegistry.unregister(pollingId)
4268
- }
4269
- },
4270
- }
4271
- }
4272
-
4273
- /**
4274
- * Check if the account has enough balance for the deposit amount
4275
- */
4276
- async function checkAccountBalance({
4277
- account,
4278
- tokenAddress,
4279
- depositAmount,
4280
- publicClient,
4281
- }: {
4282
- account: Account
4283
- tokenAddress: string
4284
- depositAmount: string
4285
- publicClient: PublicClient
4286
- }): Promise<{
4287
- hasEnoughBalance: boolean
4288
- balance: bigint
4289
- requiredAmount: bigint
4290
- balanceFormatted: string
4291
- requiredAmountFormatted: string
4292
- balanceError: Error | null
4293
- }> {
4294
- try {
4295
- let balance: bigint
4296
-
4297
- if (tokenAddress === zeroAddress) {
4298
- // Native token balance
4299
- balance = await publicClient.getBalance({ address: account.address })
4300
- } else {
4301
- // ERC20 token balance
4302
- balance = await publicClient.readContract({
4303
- address: tokenAddress as `0x${string}`,
4304
- abi: erc20Abi,
4305
- functionName: "balanceOf",
4306
- args: [account.address],
4307
- })
4308
- }
4309
-
4310
- const requiredAmount = BigInt(depositAmount)
4311
-
4312
- logger.console.log("[trails-sdk] balance", balance)
4313
- logger.console.log("[trails-sdk] requiredAmount", requiredAmount)
4314
- const hasEnoughBalance = balance >= requiredAmount
4315
- logger.console.log("[trails-sdk] hasEnoughBalance", hasEnoughBalance)
4316
-
4317
- let balanceFormatted = ""
4318
- let requiredAmountFormatted = ""
4319
- if (tokenAddress === zeroAddress) {
4320
- balanceFormatted = formatUnits(balance, 18)
4321
- requiredAmountFormatted = formatUnits(requiredAmount, 18)
4322
- } else {
4323
- // ERC20 token balance
4324
- const decimals = await publicClient.readContract({
4325
- address: tokenAddress as `0x${string}`,
4326
- abi: erc20Abi,
4327
- functionName: "decimals",
4328
- })
4329
- if (!decimals) {
4330
- throw new Error("Decimals not found")
4331
- }
4332
- balanceFormatted = formatUnits(balance, decimals)
4333
- requiredAmountFormatted = formatUnits(requiredAmount, decimals)
4334
- }
4335
-
4336
- let balanceError = null
4337
- if (!hasEnoughBalance) {
4338
- balanceError = new InsufficientBalanceError(
4339
- `Insufficient balance: Need ${formatAmount(requiredAmountFormatted)} ${tokenAddress} for ${account.address} on ${publicClient.chain?.name} but only have ${formatAmount(balanceFormatted)} ${tokenAddress}`,
4340
- )
4341
- }
4342
-
4343
- return {
4344
- hasEnoughBalance,
4345
- balance,
4346
- balanceFormatted,
4347
- requiredAmount,
4348
- requiredAmountFormatted,
4349
- balanceError,
4350
- }
4351
- } catch (error) {
4352
- logger.console.error("[trails-sdk] Error checking account balance:", error)
4353
- return {
4354
- hasEnoughBalance: false,
4355
- balance: BigInt(0),
4356
- balanceFormatted: "0",
4357
- requiredAmount: BigInt(0),
4358
- requiredAmountFormatted: "0",
4359
- balanceError: error instanceof Error ? error : null,
4360
- }
4361
- }
4362
- }
4363
-
4364
- // ETH fee required by some bridges for low token amounts
4365
- // TODO: update backend API to return the native fee requirement, if any
4366
- function getNeedsLifiNativeFee({
4367
- originTokenAddress,
4368
- destinationTokenAmount,
4369
- destinationTokenDecimals,
4370
- sourceTokenDecimals,
4371
- sourceTokenPriceUsd,
4372
- destinationTokenPriceUsd,
4373
- depositAmount,
4374
- }: {
4375
- originTokenAddress: string
4376
- destinationTokenAmount: string
4377
- destinationTokenDecimals: number
4378
- sourceTokenDecimals: number
4379
- sourceTokenPriceUsd: number | null
4380
- destinationTokenPriceUsd: number | null
4381
- depositAmount: string
4382
- }): boolean {
4383
- let needsNativeFee = false
4384
- if (
4385
- originTokenAddress !== zeroAddress &&
4386
- sourceTokenPriceUsd &&
4387
- destinationTokenPriceUsd &&
4388
- depositAmount &&
4389
- destinationTokenDecimals !== undefined &&
4390
- sourceTokenDecimals !== undefined
4391
- ) {
4392
- // Convert from wei to token units using formatUnits
4393
- const destinationAmount = Number(
4394
- formatUnits(BigInt(destinationTokenAmount), destinationTokenDecimals),
4395
- )
4396
- const depositAmountFormatted = Number(
4397
- formatUnits(BigInt(depositAmount), sourceTokenDecimals),
4398
- )
4399
- logger.console.log("[trails-sdk] destinationAmount", destinationAmount)
4400
- logger.console.log(
4401
- "[trails-sdk] depositAmountFormatted",
4402
- depositAmountFormatted,
4403
- )
4404
- const destinationAmountUsd = calcAmountUsdPrice({
4405
- amount: destinationAmount,
4406
- usdPrice: destinationTokenPriceUsd,
4407
- })
4408
- const depositAmountUsd = calcAmountUsdPrice({
4409
- amount: depositAmountFormatted,
4410
- usdPrice: sourceTokenPriceUsd,
4411
- })
4412
- const diff = depositAmountUsd - destinationAmountUsd
4413
- logger.console.log(
4414
- "[trails-sdk] destinationAmountUsd",
4415
- destinationAmountUsd,
4416
- "[trails-sdk] depositAmountUsd",
4417
- depositAmountUsd,
4418
- "[trails-sdk] diff",
4419
- diff,
4420
- )
4421
- if (diff >= 0 && diff <= 0.02) {
4422
- needsNativeFee = true
4423
- }
4424
- }
4425
-
4426
- return needsNativeFee
4427
- }
4428
-
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"
4438
-
4439
- // Import QuoteProviderInfo for use in this file
4440
- import type { QuoteProviderInfo } from "./widget/hooks/useQuote.js"
4441
-
4442
- export function getFeesFromIntent(
4443
- intent: GetIntentCallsPayloadsReturn,
4444
- {
4445
- tradeType,
4446
- fromAmountUsd,
4447
- toAmountUsd,
4448
- }: { tradeType: TradeType; fromAmountUsd: number; toAmountUsd: number },
4449
- ): PrepareSendFees {
4450
- const totalFeeAmountUsd = intent.payloads?.trailsFee?.totalFeeUSD ?? 0
4451
- const totalFeeAmountUsdDisplay = formatUsdAmountDisplay(totalFeeAmountUsd)
4452
-
4453
- logger.console.log("[trails-sdk] getFeesFromIntent", {
4454
- tradeType,
4455
- fromAmountUsd,
4456
- toAmountUsd,
4457
- totalFeeAmountUsd,
4458
- totalFeeAmountUsdDisplay,
4459
- })
4460
-
4461
- return {
4462
- feeTokenAddress: intent.payloads.trailsFee?.feeToken ?? zeroAddress,
4463
- totalFeeAmount: intent.payloads.trailsFee?.totalFeeAmount ?? "0",
4464
- totalFeeAmountUsd: totalFeeAmountUsd.toString(),
4465
- totalFeeAmountUsdDisplay,
4466
- }
4467
- }
4468
-
4469
- export function getSlippageToleranceFromIntent(
4470
- intent: GetIntentCallsPayloadsReturn,
4471
- ): string {
4472
- return intent.payloads.quote?.maxSlippage?.toString() ?? "0"
4473
- }
4474
-
4475
- export function getPriceImpactFromIntent(
4476
- intent: GetIntentCallsPayloadsReturn,
4477
- ): string {
4478
- return intent.payloads?.quote?.priceImpact?.toString() ?? "0"
4479
- }
4480
-
4481
- export function getPriceImpactUsdFromIntent(
4482
- intent: GetIntentCallsPayloadsReturn,
4483
- ): string {
4484
- // Temporary type assertion until API types are regenerated
4485
- return (intent.payloads?.quote as any)?.priceImpactUsd?.toString() ?? "0"
4486
- }
4487
-
4488
- export function getFeesFromRelaySdkQuote(quote: RelayQuote): PrepareSendFees {
4489
- const totalFeeAmountUsd = quote?.fees?.relayer?.amount ?? 0
4490
- const totalFeeAmountUsdDisplay = formatUsdAmountDisplay(totalFeeAmountUsd)
4491
- return {
4492
- feeTokenAddress: quote?.fees?.relayer?.currency?.address ?? zeroAddress,
4493
- totalFeeAmount: quote?.fees?.relayer?.amount ?? "0",
4494
- totalFeeAmountUsd: totalFeeAmountUsd.toString(),
4495
- totalFeeAmountUsdDisplay,
4496
- }
4497
- }
4498
-
4499
- export function getSlippageToleranceFromRelaySdkQuote(
4500
- quote: RelayQuote,
4501
- ): string {
4502
- return (
4503
- Number(quote?.details?.slippageTolerance?.origin?.percent ?? "0") / 100
4504
- ).toString()
4505
- }
4506
-
4507
- export function getPriceImpactFromRelaySdkQuote(quote: RelayQuote): string {
4508
- return (quote?.details?.swapImpact?.percent ?? 0).toString()
4509
- }
4510
-
4511
- export function getPriceImpactUsdFromRelaySdkQuote(quote: RelayQuote): string {
4512
- return (quote?.details?.swapImpact?.usd ?? 0).toString()
4513
- }
4514
-
4515
- export function getZeroFees(): PrepareSendFees {
4516
- return {
4517
- feeTokenAddress: zeroAddress,
4518
- totalFeeAmount: "0",
4519
- totalFeeAmountUsd: "0",
4520
- totalFeeAmountUsdDisplay: formatUsdAmountDisplay(0),
4521
- }
4522
- }
4523
-
4524
- // TODO: make this dyanamic
4525
- export function getCompletionEstimateSeconds({
4526
- originChainId,
4527
- destinationChainId,
4528
- }: {
4529
- originChainId: number
4530
- destinationChainId: number
4531
- }): number {
4532
- if (originChainId === mainnet.id && destinationChainId === mainnet.id) {
4533
- return 60
4534
- }
4535
-
4536
- if (originChainId === mainnet.id || destinationChainId === mainnet.id) {
4537
- return 45
4538
- }
4539
-
4540
- return 15
4541
- }
4542
-
4543
- export async function getNormalizedQuoteObject({
4544
- originDepositAddress,
4545
- destinationDepositAddress,
4546
- destinationAddress,
4547
- destinationCalldata,
4548
- originChainId,
4549
- destinationChainId,
4550
- originAmount,
4551
- originAmountMin,
4552
- destinationAmount,
4553
- destinationAmountMin,
4554
- originTokenAddress,
4555
- destinationTokenAddress,
4556
- originTokenPriceUsd,
4557
- destinationTokenPriceUsd,
4558
- transactionStates,
4559
- fees,
4560
- slippageTolerance,
4561
- priceImpact,
4562
- priceImpactUsd,
4563
- originNativeTokenPriceUsd,
4564
- quoteProvider,
4565
- noSufficientBalance,
4566
- estimatedGasLimit,
4567
- intent,
4568
- }: {
4569
- originDepositAddress?: string
4570
- destinationDepositAddress?: string
4571
- destinationAddress?: string
4572
- destinationCalldata?: string
4573
- originChainId: number
4574
- destinationChainId?: number
4575
- originAmount: string
4576
- originAmountMin?: string
4577
- destinationAmount: string
4578
- destinationAmountMin?: string
4579
- originTokenAddress: string
4580
- destinationTokenAddress?: string
4581
- originTokenPriceUsd?: string | null
4582
- destinationTokenPriceUsd?: string | null
4583
- transactionStates?: TransactionState[]
4584
- fees?: PrepareSendFees
4585
- slippageTolerance?: string
4586
- priceImpact?: string
4587
- priceImpactUsd?: string
4588
- originNativeTokenPriceUsd?: number | null
4589
- quoteProvider?: string
4590
- noSufficientBalance?: boolean
4591
- estimatedGasLimit?: bigint
4592
- intent?: GetIntentCallsPayloadsReturn
4593
- }): Promise<PrepareSendQuote> {
4594
- if (!destinationChainId) {
4595
- throw new Error("Destination chain id is required")
4596
- }
4597
-
4598
- if (!destinationTokenAddress && originChainId === destinationChainId) {
4599
- destinationTokenAddress = originTokenAddress
4600
- }
4601
-
4602
- if (!destinationTokenAddress && originChainId === destinationChainId) {
4603
- destinationTokenAddress = originTokenAddress
4604
- }
4605
-
4606
- if (!destinationTokenAddress) {
4607
- throw new Error("Destination token address is required")
4608
- }
4609
-
4610
- const originToken = await getTokenInfo(originChainId, originTokenAddress)
4611
- const destinationToken = await getTokenInfo(
4612
- destinationChainId,
4613
- destinationTokenAddress,
4614
- )
4615
-
4616
- const originChain = getChainInfo(originChainId)
4617
- const destinationChain = getChainInfo(destinationChainId)
4618
-
4619
- if (!originToken || !destinationToken || !originChain || !destinationChain) {
4620
- logger.console.error("[trails-sdk] Token or chain not found", {
4621
- originToken,
4622
- destinationToken,
4623
- originChain,
4624
- destinationChain,
4625
- })
4626
- throw new Error("Token or chain not found")
4627
- }
4628
-
4629
- const originAmountMinFormatted = formatRawAmount(
4630
- originAmountMin || "0",
4631
- originToken.decimals,
4632
- )
4633
- const originAmountMinUsdFormatted = formatAmount(
4634
- calcAmountUsdPrice({
4635
- amount: originAmountMinFormatted,
4636
- usdPrice: originTokenPriceUsd,
4637
- }),
4638
- )
4639
- const originAmountMinUsdDisplay = formatUsdAmountDisplay(
4640
- originAmountMinUsdFormatted,
4641
- )
4642
-
4643
- const destinationAmountMinFormatted = formatRawAmount(
4644
- destinationAmountMin || "0",
4645
- destinationToken.decimals,
4646
- )
4647
- const destinationAmountMinUsdFormatted = formatAmount(
4648
- calcAmountUsdPrice({
4649
- amount: destinationAmountMinFormatted,
4650
- usdPrice: destinationTokenPriceUsd,
4651
- }),
4652
- )
4653
- const destinationAmountMinUsdDisplay = formatUsdAmountDisplay(
4654
- destinationAmountMinUsdFormatted,
4655
- )
4656
-
4657
- const originAmountFormatted = formatRawAmount(
4658
- originAmount,
4659
- originToken.decimals,
4660
- )
4661
- const originAmountUsdFormatted = formatAmount(
4662
- calcAmountUsdPrice({
4663
- amount: originAmountFormatted,
4664
- usdPrice: originTokenPriceUsd,
4665
- }),
4666
- )
4667
- const originAmountUsdDisplay = formatUsdAmountDisplay(
4668
- originAmountUsdFormatted,
4669
- )
4670
-
4671
- const destinationAmountFormatted = formatRawAmount(
4672
- destinationAmount,
4673
- destinationToken.decimals,
4674
- )
4675
- const destinationAmountUsdFormatted = formatAmount(
4676
- calcAmountUsdPrice({
4677
- amount: destinationAmountFormatted,
4678
- usdPrice: destinationTokenPriceUsd,
4679
- }),
4680
- )
4681
- const destinationAmountUsdDisplay = formatUsdAmountDisplay(
4682
- destinationAmountUsdFormatted,
4683
- )
4684
-
4685
- const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
4686
-
4687
- const publicClient = createPublicClient({
4688
- chain: originChain,
4689
- transport: http(),
4690
- })
4691
-
4692
- let gasCostUsd: number = 0
4693
- let gasCostUsdDisplay: string = "0"
4694
- let gasCost: string = "0"
4695
- let gasCostFormatted: string = "0"
4696
- try {
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
4717
- gasCostUsd = await estimateGasCostUsd(
4718
- publicClient,
4719
- originNativeTokenPriceUsd,
4720
- gasLimitForCost,
4721
- )
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
- )
4734
- }
4735
- } catch (error) {
4736
- logger.console.error(
4737
- "[trails-sdk][gas-estimation] Error estimating gas cost for quote display",
4738
- error,
4739
- )
4740
- }
4741
-
4742
- // Calculate exchange rates
4743
- const originTokenPrice = originTokenPriceUsd ? Number(originTokenPriceUsd) : 0
4744
- const destinationTokenPrice = destinationTokenPriceUsd
4745
- ? Number(destinationTokenPriceUsd)
4746
- : 0
4747
- const exchangeRates = getTokenExchangeRates(
4748
- originTokenPrice,
4749
- destinationTokenPrice,
4750
- originToken.symbol,
4751
- destinationToken.symbol,
4752
- )
4753
-
4754
- let quoteProviderUrl = ""
4755
- if (quoteProvider === "cctp") {
4756
- quoteProviderUrl = "https://www.circle.com/"
4757
- } else if (quoteProvider === "relay") {
4758
- quoteProviderUrl = "https://relay.link/"
4759
- } else if (quoteProvider === "lifi") {
4760
- quoteProviderUrl = "https://li.fi/"
4761
- }
4762
-
4763
- const quoteProviderNames = {
4764
- cctp: "Circle CCTP",
4765
- relay: "Relay",
4766
- lifi: "LiFi",
4767
- }
4768
-
4769
- const quoteProviderName =
4770
- quoteProviderNames[quoteProvider as keyof typeof quoteProviderNames] ||
4771
- quoteProvider ||
4772
- ""
4773
- const quoteProviderInfo: QuoteProviderInfo = {
4774
- id: quoteProvider || "",
4775
- name: quoteProviderName || "",
4776
- url: quoteProviderUrl || "",
4777
- }
4778
-
4779
- const priceImpactUsdDisplay = formatUsdAmountDisplay(priceImpactUsd)
4780
-
4781
- // Extract fee breakdown from intent if available
4782
- let trailsFeeBreakdown = null
4783
- if (intent) {
4784
- trailsFeeBreakdown = await extractTrailsFeeBreakdown(intent)
4785
- }
4786
-
4787
- return {
4788
- originDepositAddress: originDepositAddress || "",
4789
- destinationDepositAddress: destinationDepositAddress || "",
4790
- destinationAddress: destinationAddress || "",
4791
- destinationCalldata: hasCustomCalldata ? destinationCalldata || "" : "",
4792
- originAmount: originAmount || "0",
4793
- originAmountMin: originAmountMin || originAmount || "0",
4794
- originAmountMinUsdFormatted: originAmountMinUsdFormatted || "0",
4795
- originAmountMinUsdDisplay: originAmountMinUsdDisplay || "0",
4796
- destinationAmount: destinationAmount || "0",
4797
- destinationAmountMin: destinationAmountMin || destinationAmount || "0",
4798
- destinationAmountMinUsdFormatted: destinationAmountMinUsdFormatted || "0",
4799
- destinationAmountMinUsdDisplay: destinationAmountMinUsdDisplay || "0",
4800
- originAmountFormatted,
4801
- originAmountUsdFormatted,
4802
- originAmountUsdDisplay,
4803
- destinationAmountFormatted,
4804
- destinationAmountUsdFormatted,
4805
- destinationAmountUsdDisplay,
4806
- originToken,
4807
- destinationToken,
4808
- fees: fees || getZeroFees(),
4809
- slippageTolerance: slippageTolerance || "0",
4810
- priceImpact: priceImpact || "0",
4811
- priceImpactUsdDisplay: priceImpactUsdDisplay || "0",
4812
- originChain,
4813
- destinationChain,
4814
- completionEstimateSeconds: getCompletionEstimateSeconds({
4815
- originChainId,
4816
- destinationChainId,
4817
- }),
4818
- transactionStates: transactionStates || [],
4819
- gasCostUsd,
4820
- gasCostUsdDisplay,
4821
- gasCost,
4822
- gasCostFormatted,
4823
- originTokenRate: exchangeRates.originTokenRate,
4824
- destinationTokenRate: exchangeRates.destinationTokenRate,
4825
- originAmountDisplay: formatAmountDisplay(originAmountFormatted),
4826
- destinationAmountDisplay: formatAmountDisplay(destinationAmountFormatted),
4827
- originAmountMinDisplay: formatAmountDisplay(originAmountMinFormatted),
4828
- destinationAmountMinDisplay: formatAmountDisplay(
4829
- destinationAmountMinFormatted,
4830
- ),
4831
- quoteProvider: quoteProviderInfo,
4832
- noSufficientBalance: noSufficientBalance || false,
4833
- trailsFeeBreakdown,
4834
- }
4835
- }
4836
-
4837
- function getTokenExchangeRates(
4838
- originTokenPrice: number,
4839
- destinationTokenPrice: number,
4840
- originTokenSymbol: string,
4841
- destinationTokenSymbol: string,
4842
- ): { originTokenRate: string; destinationTokenRate: string } {
4843
- if (originTokenPrice === 0 || destinationTokenPrice === 0) {
4844
- return {
4845
- originTokenRate: "0",
4846
- destinationTokenRate: "0",
4847
- }
4848
- }
4849
-
4850
- const originToDestination = originTokenPrice / destinationTokenPrice
4851
- const destinationToOrigin = destinationTokenPrice / originTokenPrice
4852
-
4853
- // Format the rates with appropriate precision
4854
- const originTokenRate = formatAmountDisplay(originToDestination, {
4855
- maxFractionDigits: destinationTokenSymbol === "USDC" ? 2 : 7,
4856
- })
4857
- const destinationTokenRate = formatAmountDisplay(destinationToOrigin, {
4858
- maxFractionDigits: originTokenSymbol === "USDC" ? 2 : 7,
4859
- })
4860
-
4861
- return {
4862
- originTokenRate: `1 ${originTokenSymbol} = ${originTokenRate} ${destinationTokenSymbol}`,
4863
- destinationTokenRate: `1 ${destinationTokenSymbol} = ${destinationTokenRate} ${originTokenSymbol}`,
4864
- }
4865
- }
418
+ // Re-export quote and fee utilities from transactionIntent for backward compatibility
419
+ export {
420
+ getFeesFromIntent,
421
+ getSlippageToleranceFromIntent,
422
+ getPriceImpactFromIntent,
423
+ getPriceImpactUsdFromIntent,
424
+ getFeesFromRelaySdkQuote,
425
+ getSlippageToleranceFromRelaySdkQuote,
426
+ getPriceImpactFromRelaySdkQuote,
427
+ getPriceImpactUsdFromRelaySdkQuote,
428
+ getZeroFees,
429
+ getCompletionEstimateSeconds,
430
+ getNormalizedQuoteObject,
431
+ } from "./transactionIntent/index.js"