0xtrails 0.2.0 → 0.2.2

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 (119) hide show
  1. package/dist/aave.d.ts.map +1 -1
  2. package/dist/analytics.d.ts +1 -0
  3. package/dist/analytics.d.ts.map +1 -1
  4. package/dist/{ccip-D6ToCrWc.js → ccip-ConT1gDe.js} +1 -1
  5. package/dist/chains.d.ts.map +1 -1
  6. package/dist/config.d.ts +1 -2
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/constants.d.ts +2 -2
  9. package/dist/constants.d.ts.map +1 -1
  10. package/dist/gasless.d.ts +19 -7
  11. package/dist/gasless.d.ts.map +1 -1
  12. package/dist/{index-BqgeTLL8.js → index-CMh8uEbV.js} +27716 -26616
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +68 -68
  16. package/dist/indexerClient.d.ts +10 -0
  17. package/dist/indexerClient.d.ts.map +1 -1
  18. package/dist/intentEntrypoint.d.ts +40 -14
  19. package/dist/intentEntrypoint.d.ts.map +1 -1
  20. package/dist/intents.d.ts.map +1 -1
  21. package/dist/prepareSend.d.ts +11 -8
  22. package/dist/prepareSend.d.ts.map +1 -1
  23. package/dist/relayer.d.ts.map +1 -1
  24. package/dist/tokenBalances.d.ts.map +1 -1
  25. package/dist/trails.d.ts.map +1 -1
  26. package/dist/trailsClient.d.ts.map +1 -1
  27. package/dist/trailsRouter.d.ts +22 -0
  28. package/dist/trailsRouter.d.ts.map +1 -0
  29. package/dist/transactions.d.ts +0 -1
  30. package/dist/transactions.d.ts.map +1 -1
  31. package/dist/widget/components/AccountSettings.d.ts.map +1 -1
  32. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  33. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  34. package/dist/widget/components/ConnectWallet.d.ts.map +1 -1
  35. package/dist/widget/components/ConnectedWallets.d.ts.map +1 -1
  36. package/dist/widget/components/Earn.d.ts.map +1 -1
  37. package/dist/widget/components/FeeOption.d.ts +22 -0
  38. package/dist/widget/components/FeeOption.d.ts.map +1 -0
  39. package/dist/widget/components/FeeOptions.d.ts +13 -17
  40. package/dist/widget/components/FeeOptions.d.ts.map +1 -1
  41. package/dist/widget/components/Fund.d.ts.map +1 -1
  42. package/dist/widget/components/FundMethods.d.ts +1 -1
  43. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  44. package/dist/widget/components/FundSendForm.d.ts.map +1 -1
  45. package/dist/widget/components/MeshConnectExchanges.d.ts +5 -2
  46. package/dist/widget/components/MeshConnectExchanges.d.ts.map +1 -1
  47. package/dist/widget/components/MeshConnectFlow.d.ts +2 -0
  48. package/dist/widget/components/MeshConnectFlow.d.ts.map +1 -1
  49. package/dist/widget/components/NativeGasOption.d.ts +12 -0
  50. package/dist/widget/components/NativeGasOption.d.ts.map +1 -0
  51. package/dist/widget/components/Pay.d.ts.map +1 -1
  52. package/dist/widget/components/PaySendForm.d.ts.map +1 -1
  53. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  54. package/dist/widget/components/TokenSelector.d.ts.map +1 -1
  55. package/dist/widget/components/UserPreferences.d.ts.map +1 -1
  56. package/dist/widget/hooks/useBack.d.ts +2 -0
  57. package/dist/widget/hooks/useBack.d.ts.map +1 -1
  58. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  59. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  60. package/dist/widget/hooks/useDefaultTokenSelection.d.ts.map +1 -1
  61. package/dist/widget/hooks/useSelectedFeeToken.d.ts +32 -0
  62. package/dist/widget/hooks/useSelectedFeeToken.d.ts.map +1 -0
  63. package/dist/widget/hooks/useSelectedMeshExchange.d.ts +14 -0
  64. package/dist/widget/hooks/useSelectedMeshExchange.d.ts.map +1 -0
  65. package/dist/widget/hooks/useSendForm.d.ts +8 -13
  66. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  67. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  68. package/dist/widget/index.js +1 -1
  69. package/dist/widget/widget.d.ts.map +1 -1
  70. package/package.json +29 -28
  71. package/src/aave.ts +90 -74
  72. package/src/analytics.ts +6 -0
  73. package/src/chains.ts +10 -0
  74. package/src/config.ts +25 -10
  75. package/src/constants.ts +11 -10
  76. package/src/gasless.ts +162 -109
  77. package/src/index.ts +1 -1
  78. package/src/indexerClient.ts +73 -1
  79. package/src/intentEntrypoint.ts +66 -101
  80. package/src/intents.ts +0 -2
  81. package/src/prepareSend.ts +1425 -887
  82. package/src/relayer.ts +4 -3
  83. package/src/tokenBalances.ts +8 -1
  84. package/src/trails.ts +1 -3
  85. package/src/trailsClient.ts +4 -1
  86. package/src/{balanceInjector.ts → trailsRouter.ts} +14 -14
  87. package/src/transactions.ts +4 -54
  88. package/src/widget/compiled.css +1 -1
  89. package/src/widget/components/AccountSettings.tsx +7 -1
  90. package/src/widget/components/ClassicSwap.tsx +173 -175
  91. package/src/widget/components/ConfigDisplay.tsx +34 -1
  92. package/src/widget/components/ConnectWallet.tsx +168 -11
  93. package/src/widget/components/ConnectedWallets.tsx +184 -102
  94. package/src/widget/components/DebugToast.tsx +3 -3
  95. package/src/widget/components/Earn.tsx +4 -27
  96. package/src/widget/components/FeeOption.tsx +78 -0
  97. package/src/widget/components/FeeOptions.tsx +192 -127
  98. package/src/widget/components/Fund.tsx +18 -27
  99. package/src/widget/components/FundMethods.tsx +3 -3
  100. package/src/widget/components/FundSendForm.tsx +0 -33
  101. package/src/widget/components/MeshConnectExchanges.tsx +32 -3
  102. package/src/widget/components/MeshConnectFlow.tsx +23 -4
  103. package/src/widget/components/NativeGasOption.tsx +99 -0
  104. package/src/widget/components/Pay.tsx +36 -32
  105. package/src/widget/components/PaySendForm.tsx +0 -37
  106. package/src/widget/components/QuoteDetails.tsx +0 -29
  107. package/src/widget/components/TokenSelector.tsx +11 -0
  108. package/src/widget/components/TransferPendingVertical.tsx +1 -1
  109. package/src/widget/components/UserPreferences.tsx +3 -4
  110. package/src/widget/hooks/useBack.tsx +4 -0
  111. package/src/widget/hooks/useCurrentScreen.tsx +1 -0
  112. package/src/widget/hooks/useDefaultTokenSelection.tsx +8 -8
  113. package/src/widget/hooks/useSelectedFeeToken.tsx +299 -0
  114. package/src/widget/hooks/useSelectedMeshExchange.tsx +46 -0
  115. package/src/widget/hooks/useSendForm.ts +78 -23
  116. package/src/widget/hooks/useTokenList.ts +10 -1
  117. package/src/widget/widget.tsx +173 -111
  118. package/dist/balanceInjector.d.ts +0 -22
  119. package/dist/balanceInjector.d.ts.map +0 -1
@@ -51,8 +51,12 @@ import {
51
51
  import { queueCCTPTransfer } from "./cctpqueue.js"
52
52
  import { getChainInfo, getTestnetChainInfo } from "./chains.js"
53
53
  import { attemptSwitchChain } from "./chainSwitch.js"
54
- import { getSequenceEnv, getSlippageTolerance } from "./config.js"
55
- import { intentEntrypoints, MINIMUM_USD_AMOUNT_FOR_SWAP } from "./constants.js"
54
+ import {
55
+ getSequenceEnv,
56
+ getSlippageTolerance,
57
+ type SequenceEnv,
58
+ } from "./config.js"
59
+ import { intentEntrypoints } from "./constants.js"
56
60
  import {
57
61
  decodeGuestModuleEvents,
58
62
  decodeTrailsTokenSweeperEvents,
@@ -65,8 +69,13 @@ import {
65
69
  getNeedsIntentEntrypointApproval,
66
70
  getPermitCalls,
67
71
  getPermitSignature,
72
+ getUserNonce,
68
73
  signIntent,
69
74
  } from "./gasless.js"
75
+ import {
76
+ getIntentEntrypointFeeOptions,
77
+ isIntentEntrypointSupported,
78
+ } from "./intentEntrypoint.js"
70
79
  import { useIndexerGatewayClient } from "./indexerClient.js"
71
80
  import {
72
81
  commitIntentConfig,
@@ -84,10 +93,6 @@ import {
84
93
  } from "./paymasterSend.js"
85
94
  import { findFirstPreconditionForChainId } from "./preconditions.js"
86
95
  import { calcAmountUsdPrice, getTokenPrice } from "./prices.js"
87
- import {
88
- TRAILS_CONTRACT_PLACEHOLDER_AMOUNT,
89
- wrapCalldataWithBalanceInjectorIfNeeded,
90
- } from "./balanceInjector.js"
91
96
  import { getQueryParam } from "./queryParams.js"
92
97
  import type { MetaTxnReceipt, RelayerEnv } from "./relayer.js"
93
98
  import { useRelayers } from "./relayer.js"
@@ -115,6 +120,10 @@ import {
115
120
  useSupportedTokens,
116
121
  type SupportedToken,
117
122
  } from "./tokens.js"
123
+ import {
124
+ TRAILS_ROUTER_PLACEHOLDER_AMOUNT,
125
+ wrapCalldataWithTrailsRouterIfNeeded,
126
+ } from "./trailsRouter.js"
118
127
  import type {
119
128
  TransactionState,
120
129
  TransactionStateStatus,
@@ -168,6 +177,7 @@ export type PrepareSendOptions = {
168
177
  mode?: "pay" | "fund" | "earn" | "swap" | "receive"
169
178
  checkoutOnHandlers?: CheckoutOnHandlers
170
179
  refundAddress?: string
180
+ selectedFeeToken?: any
171
181
  }
172
182
 
173
183
  export type PrepareSendFees = {
@@ -216,7 +226,6 @@ export type PrepareSendQuote = {
216
226
  destinationTokenRate: string
217
227
  quoteProvider: QuoteProviderInfo | null
218
228
  noSufficientBalance: boolean
219
- minimumNotMet: boolean
220
229
  }
221
230
 
222
231
  export type PrepareSendReturn = {
@@ -224,10 +233,10 @@ export type PrepareSendReturn = {
224
233
  feeOptions?: any
225
234
  send: ({
226
235
  onOriginSend,
227
- feeTokenAddress,
236
+ selectedFeeToken,
228
237
  }: {
229
238
  onOriginSend?: () => void
230
- feeTokenAddress?: string | null
239
+ selectedFeeToken?: any
231
240
  }) => Promise<SendReturn>
232
241
  }
233
242
 
@@ -379,6 +388,7 @@ export async function prepareSend(
379
388
  fundMethod,
380
389
  mode,
381
390
  checkoutOnHandlers,
391
+ selectedFeeToken,
382
392
  } = options
383
393
  let { sourceTokenPriceUsd, destinationTokenPriceUsd } = options
384
394
 
@@ -416,14 +426,14 @@ export async function prepareSend(
416
426
  // we need to set custom calldata for the cctp transfer in order to have destination intent adddress execution needed for metatxn tracking
417
427
  effectiveDestinationCalldata = getERC20TransferData({
418
428
  recipient,
419
- amount: BigInt(TRAILS_CONTRACT_PLACEHOLDER_AMOUNT),
429
+ amount: BigInt(TRAILS_ROUTER_PLACEHOLDER_AMOUNT),
420
430
  })
421
431
  effectiveDestinationAddress = destinationTokenAddress
422
432
  hasCustomCalldata = true
423
433
  }
424
434
 
425
435
  if (hasCustomCalldata && tradeType === TradeType.EXACT_INPUT) {
426
- const wrapResult = wrapCalldataWithBalanceInjectorIfNeeded({
436
+ const wrapResult = wrapCalldataWithTrailsRouterIfNeeded({
427
437
  token: destinationTokenAddress,
428
438
  target: effectiveDestinationAddress,
429
439
  calldata: effectiveDestinationCalldata as `0x${string}`,
@@ -435,7 +445,7 @@ export async function prepareSend(
435
445
  })
436
446
 
437
447
  if (wrapResult) {
438
- effectiveDestinationAddress = wrapResult.balanceInjectorAddress
448
+ effectiveDestinationAddress = wrapResult.trailsRouterAddress
439
449
  effectiveDestinationCalldata = wrapResult.encodedCalldata
440
450
  }
441
451
  }
@@ -686,6 +696,7 @@ export async function prepareSend(
686
696
  fundMethod,
687
697
  mode,
688
698
  checkoutOnHandlers,
699
+ selectedFeeToken,
689
700
  })
690
701
  }
691
702
 
@@ -725,6 +736,7 @@ async function sendHandlerForDifferentChainDifferentToken({
725
736
  fundMethod,
726
737
  mode,
727
738
  checkoutOnHandlers,
739
+ selectedFeeToken,
728
740
  }: {
729
741
  mainSignerAddress: string
730
742
  originChainId: number
@@ -762,6 +774,7 @@ async function sendHandlerForDifferentChainDifferentToken({
762
774
  fundMethod?: string
763
775
  mode?: "pay" | "fund" | "earn" | "swap" | "receive"
764
776
  checkoutOnHandlers?: CheckoutOnHandlers
777
+ selectedFeeToken?: any
765
778
  }): Promise<PrepareSendReturn> {
766
779
  const testnet = isTestnetDebugMode()
767
780
  const useCctp = getUseCctp(
@@ -809,10 +822,22 @@ async function sendHandlerForDifferentChainDifferentToken({
809
822
  quote,
810
823
  send: async ({
811
824
  onOriginSend,
825
+ selectedFeeToken: runtimeSelectedFeeToken,
812
826
  }: {
813
827
  onOriginSend?: () => void
814
- feeTokenAddress?: string | null
828
+ selectedFeeToken?: any
815
829
  }): Promise<SendReturn> => {
830
+ // Use runtime selectedFeeToken if provided, otherwise fall back to the one from prepareSend
831
+ const effectiveSelectedFeeToken =
832
+ runtimeSelectedFeeToken ?? selectedFeeToken
833
+ logger.console.log(
834
+ "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] send() called (LEGACY PATH):",
835
+ {
836
+ runtimeSelectedFeeToken,
837
+ prepareTimeSelectedFeeToken: selectedFeeToken,
838
+ effectiveSelectedFeeToken,
839
+ },
840
+ )
816
841
  const originChain = testnet ? getTestnetChainInfo(chain)! : chain
817
842
  const destinationChain = testnet
818
843
  ? getTestnetChainInfo(destinationChainId)!
@@ -1085,7 +1110,6 @@ async function sendHandlerForDifferentChainDifferentToken({
1085
1110
  )
1086
1111
 
1087
1112
  let noSufficientBalance = false
1088
- let minimumNotMet = false
1089
1113
 
1090
1114
  if (!(fundMethod === "qr-code" || fundMethod === "exchange")) {
1091
1115
  const { hasEnoughBalance } = await checkAccountBalance({
@@ -1100,10 +1124,6 @@ async function sendHandlerForDifferentChainDifferentToken({
1100
1124
  }
1101
1125
  }
1102
1126
 
1103
- if (Number(depositAmountUsd) < Number(MINIMUM_USD_AMOUNT_FOR_SWAP)) {
1104
- minimumNotMet = true
1105
- }
1106
-
1107
1127
  const quote = await getNormalizedQuoteObject({
1108
1128
  originDepositAddress: originIntentAddress,
1109
1129
  destinationDepositAddress: intent.payloads.destinationIntentAddress,
@@ -1131,7 +1151,6 @@ async function sendHandlerForDifferentChainDifferentToken({
1131
1151
  originNativeTokenPriceUsd,
1132
1152
  quoteProvider: intent.payloads?.quote?.quoteProvider,
1133
1153
  noSufficientBalance,
1134
- minimumNotMet,
1135
1154
  })
1136
1155
 
1137
1156
  // Call onCheckoutQuote callback if provided
@@ -1139,255 +1158,294 @@ async function sendHandlerForDifferentChainDifferentToken({
1139
1158
  checkoutOnHandlers.triggerCheckoutQuote(quote)
1140
1159
  }
1141
1160
 
1142
- // let feeOptions: any = null
1143
-
1144
- // try {
1145
- // feeOptions = await getFeeOptions(
1146
- // originRelayer,
1147
- // intentAddress!,
1148
- // originChainId,
1149
- // [
1150
- // {
1151
- // to: intentAddress,
1152
- // value: BigInt(0),
1153
- // data: getERC20TransferData({
1154
- // recipient,
1155
- // amount: BigInt(swapAmount),
1156
- // }),
1157
- // gasLimit: BigInt(0),
1158
- // delegateCall: false,
1159
- // onlyFallback: false,
1160
- // behaviorOnError: "revert",
1161
- // },
1162
- // ] as Payload.Call[],
1163
- // )
1164
- // } catch (error) {
1165
- // console.error("[trails-sdk] Error getting fee options", error)
1166
- // }
1167
-
1168
- // logger.console.log("[trails-sdk] prepareSend feeOptions", feeOptions)
1161
+ // Get intent entrypoint fee options if supported, gasless is enabled, and fundMethod is wallet
1162
+ let intentEntrypointFeeOptions: any = null
1163
+ if (gasless && fundMethod === "wallet") {
1164
+ try {
1165
+ logger.console.log(
1166
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless enabled and fundMethod is wallet, checking intent entrypoint support for chain:",
1167
+ originChainId,
1168
+ )
1169
+ if (isIntentEntrypointSupported(originChainId)) {
1170
+ logger.console.log(
1171
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Chain supported, fetching fee options...",
1172
+ )
1173
+ intentEntrypointFeeOptions = await getIntentEntrypointFeeOptions({
1174
+ trailsClient,
1175
+ userAddress: mainSignerAddress as `0x${string}`,
1176
+ tokenAddress: originTokenAddress as `0x${string}`,
1177
+ amount: depositAmount,
1178
+ intentAddress: originIntentAddress as `0x${string}`,
1179
+ chainId: originChainId,
1180
+ })
1181
+ logger.console.log(
1182
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Intent entrypoint fee options:",
1183
+ intentEntrypointFeeOptions,
1184
+ )
1185
+ } else {
1186
+ logger.console.log(
1187
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Chain not supported for intent entrypoint:",
1188
+ originChainId,
1189
+ )
1190
+ }
1191
+ } catch (error) {
1192
+ logger.console.error(
1193
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Error getting intent entrypoint fee options:",
1194
+ error,
1195
+ )
1196
+ }
1197
+ } else {
1198
+ logger.console.log(
1199
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] Gasless disabled or fundMethod is not wallet, skipping intent entrypoint fee options fetch",
1200
+ )
1201
+ }
1169
1202
 
1170
1203
  return {
1171
1204
  quote,
1205
+ feeOptions: intentEntrypointFeeOptions,
1172
1206
  send: async ({
1173
1207
  onOriginSend,
1208
+ selectedFeeToken: runtimeSelectedFeeToken,
1174
1209
  }: {
1175
1210
  onOriginSend?: () => void
1176
- feeTokenAddress?: string | null
1211
+ selectedFeeToken?: any
1177
1212
  }): Promise<SendReturn> => {
1178
- await commitIntentConfig(
1179
- trailsClient,
1180
- mainSignerAddress,
1181
- intent.payloads.calls,
1182
- intent.payloads.preconditions,
1183
- {
1184
- originTokenSymbol,
1185
- destinationTokenSymbol,
1186
- },
1187
- intentArgs as IntentRequestParams,
1188
- )
1213
+ try {
1214
+ // Use runtime selectedFeeToken if provided, otherwise fall back to the one from prepareSend
1215
+ const effectiveSelectedFeeToken =
1216
+ runtimeSelectedFeeToken ?? selectedFeeToken
1217
+ logger.console.log(
1218
+ "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] send() called with:",
1219
+ {
1220
+ runtimeSelectedFeeToken,
1221
+ prepareTimeSelectedFeeToken: selectedFeeToken,
1222
+ effectiveSelectedFeeToken,
1223
+ },
1224
+ )
1225
+ await commitIntentConfig(
1226
+ trailsClient,
1227
+ mainSignerAddress,
1228
+ intent.payloads.calls,
1229
+ intent.payloads.preconditions,
1230
+ {
1231
+ originTokenSymbol,
1232
+ destinationTokenSymbol,
1233
+ },
1234
+ intentArgs as IntentRequestParams,
1235
+ )
1189
1236
 
1190
- if (!(fundMethod === "qr-code" || fundMethod === "exchange")) {
1191
- const { hasEnoughBalance, balanceError } = await checkAccountBalance({
1192
- account,
1193
- tokenAddress: originTokenAddress,
1194
- depositAmount,
1195
- publicClient,
1196
- })
1237
+ if (!(fundMethod === "qr-code" || fundMethod === "exchange")) {
1238
+ const { hasEnoughBalance, balanceError } = await checkAccountBalance({
1239
+ account,
1240
+ tokenAddress: originTokenAddress,
1241
+ depositAmount,
1242
+ publicClient,
1243
+ })
1197
1244
 
1198
- if (!hasEnoughBalance) {
1199
- throw balanceError
1245
+ if (!hasEnoughBalance) {
1246
+ throw balanceError
1247
+ }
1200
1248
  }
1201
- }
1202
1249
 
1203
- logger.console.log("[trails-sdk] sending origin transaction")
1204
- const usingLIfi = false
1205
- let needsNativeFee = false
1250
+ logger.console.log("[trails-sdk] sending origin transaction")
1251
+ const usingLIfi = false
1252
+ let needsNativeFee = false
1206
1253
 
1207
- if (usingLIfi) {
1208
- needsNativeFee = getNeedsLifiNativeFee({
1209
- originTokenAddress,
1210
- destinationTokenAmount: swapAmount,
1211
- destinationTokenDecimals,
1254
+ if (usingLIfi) {
1255
+ needsNativeFee = getNeedsLifiNativeFee({
1256
+ originTokenAddress,
1257
+ destinationTokenAmount: swapAmount,
1258
+ destinationTokenDecimals,
1259
+ sourceTokenDecimals,
1260
+ sourceTokenPriceUsd: sourceTokenPriceUsd ?? null,
1261
+ destinationTokenPriceUsd: destinationTokenPriceUsd ?? null,
1262
+ depositAmount,
1263
+ })
1264
+ }
1265
+
1266
+ logger.console.log("[trails-sdk] needsNativeFee", needsNativeFee)
1267
+ logger.console.log(
1268
+ "[trails-sdk] sourceTokenPriceUsd",
1269
+ sourceTokenPriceUsd,
1270
+ )
1271
+ logger.console.log(
1272
+ "[trails-sdk] destinationTokenPriceUsd",
1273
+ destinationTokenPriceUsd,
1274
+ )
1275
+ logger.console.log(
1276
+ "[trails-sdk] sourceTokenDecimals",
1212
1277
  sourceTokenDecimals,
1213
- sourceTokenPriceUsd: sourceTokenPriceUsd ?? null,
1214
- destinationTokenPriceUsd: destinationTokenPriceUsd ?? null,
1215
- depositAmount,
1216
- })
1217
- }
1278
+ )
1279
+ logger.console.log(
1280
+ "[trails-sdk] destinationTokenDecimals",
1281
+ destinationTokenDecimals,
1282
+ )
1218
1283
 
1219
- logger.console.log("[trails-sdk] needsNativeFee", needsNativeFee)
1220
- logger.console.log(
1221
- "[trails-sdk] sourceTokenPriceUsd",
1222
- sourceTokenPriceUsd,
1223
- )
1224
- logger.console.log(
1225
- "[trails-sdk] destinationTokenPriceUsd",
1226
- destinationTokenPriceUsd,
1227
- )
1228
- logger.console.log(
1229
- "[trails-sdk] sourceTokenDecimals",
1230
- sourceTokenDecimals,
1231
- )
1232
- logger.console.log(
1233
- "[trails-sdk] destinationTokenDecimals",
1234
- destinationTokenDecimals,
1235
- )
1284
+ let originUserTxReceipt: TransactionReceipt | null = null
1285
+ let originMetaTxnReceipt: MetaTxnReceipt | null = null
1286
+ let destinationMetaTxnReceipt: MetaTxnReceipt | null = null
1236
1287
 
1237
- let originUserTxReceipt: TransactionReceipt | null = null
1238
- let originMetaTxnReceipt: MetaTxnReceipt | null = null
1239
- let destinationMetaTxnReceipt: MetaTxnReceipt | null = null
1288
+ const testnet = isTestnetDebugMode()
1289
+ const effectiveOriginChain = testnet
1290
+ ? getTestnetChainInfo(chain)!
1291
+ : chain
1292
+ const effectiveOriginTokenAddress = testnet
1293
+ ? getTestnetOriginTokenAddress(effectiveOriginChain.id)
1294
+ : originTokenAddress
1240
1295
 
1241
- const testnet = isTestnetDebugMode()
1242
- const effectiveOriginChain = testnet ? getTestnetChainInfo(chain)! : chain
1243
- const effectiveOriginTokenAddress = testnet
1244
- ? getTestnetOriginTokenAddress(effectiveOriginChain.id)
1245
- : originTokenAddress
1296
+ logger.console.log("[trails-sdk] testnet", testnet)
1246
1297
 
1247
- logger.console.log("[trails-sdk] testnet", testnet)
1298
+ const destinationPublicClient = createPublicClient({
1299
+ chain: getChainInfo(destinationChainId)!,
1300
+ transport: http(),
1301
+ })
1248
1302
 
1249
- const destinationPublicClient = createPublicClient({
1250
- chain: getChainInfo(destinationChainId)!,
1251
- transport: http(),
1252
- })
1303
+ const depositPromise = async () => {
1304
+ logger.console.log(
1305
+ "[trails-sdk] depositPromise called - starting deposit transaction",
1306
+ )
1307
+ logger.console.log("[trails-sdk] fundMethod value:", fundMethod)
1253
1308
 
1254
- const depositPromise = async () => {
1255
- logger.console.log(
1256
- "[trails-sdk] depositPromise called - starting deposit transaction",
1257
- )
1258
- logger.console.log("[trails-sdk] fundMethod value:", fundMethod)
1309
+ // Skip wallet deposit if fund method is qr-code
1310
+ if (fundMethod === "qr-code" || fundMethod === "exchange") {
1311
+ logger.console.log(
1312
+ "[trails-sdk] Skipping wallet deposit for QR code mode - fundMethod is:",
1313
+ fundMethod,
1314
+ )
1315
+ return
1316
+ }
1259
1317
 
1260
- // Skip wallet deposit if fund method is qr-code
1261
- if (fundMethod === "qr-code" || fundMethod === "exchange") {
1262
1318
  logger.console.log(
1263
- "[trails-sdk] Skipping wallet deposit for QR code mode - fundMethod is:",
1264
- fundMethod,
1319
+ "[trails-sdk] Calling attemptUserDepositTx with params:",
1320
+ {
1321
+ originTokenAddress: effectiveOriginTokenAddress,
1322
+ gasless,
1323
+ paymasterUrl,
1324
+ chain: effectiveOriginChain.id,
1325
+ account: account.address,
1326
+ firstPreconditionMin,
1327
+ originIntentAddress,
1328
+ fee,
1329
+ dryMode,
1330
+ feeOptions: intentEntrypointFeeOptions,
1331
+ selectedFeeToken: effectiveSelectedFeeToken,
1332
+ selectedFeeTokenType: typeof effectiveSelectedFeeToken,
1333
+ selectedFeeTokenValue: JSON.stringify(effectiveSelectedFeeToken),
1334
+ },
1265
1335
  )
1266
- return
1267
- }
1268
1336
 
1269
- logger.console.log(
1270
- "[trails-sdk] Calling attemptUserDepositTx with params:",
1271
- {
1337
+ originUserTxReceipt = await attemptUserDepositTx({
1272
1338
  originTokenAddress: effectiveOriginTokenAddress,
1273
1339
  gasless,
1274
1340
  paymasterUrl,
1275
- chain: effectiveOriginChain.id,
1276
- account: account.address,
1341
+ chain: effectiveOriginChain,
1342
+ account,
1343
+ originRelayer,
1277
1344
  firstPreconditionMin,
1278
1345
  originIntentAddress,
1346
+ onOriginSend,
1347
+ publicClient,
1348
+ walletClient,
1349
+ destinationTokenDecimals,
1350
+ sourceTokenDecimals,
1279
1351
  fee,
1280
1352
  dryMode,
1281
- },
1282
- )
1283
-
1284
- originUserTxReceipt = await attemptUserDepositTx({
1285
- originTokenAddress: effectiveOriginTokenAddress,
1286
- gasless,
1287
- paymasterUrl,
1288
- chain: effectiveOriginChain,
1289
- account,
1290
- originRelayer,
1291
- firstPreconditionMin,
1292
- originIntentAddress,
1293
- onOriginSend,
1294
- publicClient,
1295
- walletClient,
1296
- destinationTokenDecimals,
1297
- sourceTokenDecimals,
1298
- fee,
1299
- dryMode,
1300
- sourceTokenPriceUsd: sourceTokenPriceUsd ?? null,
1301
- destinationTokenPriceUsd: destinationTokenPriceUsd ?? null,
1302
- swapAmount,
1303
- onTransactionStateChange,
1304
- transactionStates,
1305
- fundMethod,
1306
- originTokenSymbol,
1307
- destinationTokenSymbol,
1308
- depositAmountUsd,
1309
- trailsClient,
1310
- })
1311
-
1312
- if (!originUserTxReceipt) {
1313
- throw new Error("Failed to send origin transaction")
1314
- }
1353
+ sourceTokenPriceUsd: sourceTokenPriceUsd ?? null,
1354
+ destinationTokenPriceUsd: destinationTokenPriceUsd ?? null,
1355
+ swapAmount,
1356
+ onTransactionStateChange,
1357
+ transactionStates,
1358
+ fundMethod,
1359
+ originTokenSymbol,
1360
+ destinationTokenSymbol,
1361
+ depositAmountUsd,
1362
+ feeOptions: intentEntrypointFeeOptions,
1363
+ trailsClient,
1364
+ selectedFeeToken: effectiveSelectedFeeToken,
1365
+ })
1315
1366
 
1316
- transactionStates[0] = getTransactionStateFromReceipt(
1317
- originUserTxReceipt,
1318
- originChainId,
1319
- transactionStates[0]?.label,
1320
- )
1321
- onTransactionStateChange(transactionStates)
1367
+ if (!originUserTxReceipt) {
1368
+ throw new Error("Failed to send origin transaction")
1369
+ }
1322
1370
 
1323
- setTimeout(() => {
1324
- const destinationChain = getChainInfo(destinationChainId)
1325
- updatePersistentToast(
1326
- "In Progress",
1327
- `Your transaction to ${destinationChain?.name || "chain"} is in progress`,
1328
- "info",
1371
+ transactionStates[0] = getTransactionStateFromReceipt(
1372
+ originUserTxReceipt,
1373
+ originChainId,
1374
+ transactionStates[0]?.label,
1329
1375
  )
1330
- }, 1000)
1331
- }
1332
1376
 
1333
- const checkForDepositTx = async () => {
1334
- while (true) {
1335
1377
  try {
1336
- const response = await getAccountTransactionHistory({
1337
- chainId: originChainId,
1338
- accountAddress: originIntentAddress,
1339
- })
1378
+ transactionStates[0].decodedTrailsTokenSweeperEvents =
1379
+ decodeTrailsTokenSweeperEvents(originUserTxReceipt)
1380
+ transactionStates[0].decodedGuestModuleEvents =
1381
+ decodeGuestModuleEvents(originUserTxReceipt)
1382
+ transactionStates[0].refunded =
1383
+ transactionStates[0].decodedTrailsTokenSweeperEvents.findIndex(
1384
+ (event) =>
1385
+ event.type === "Refund" || event.type === "RefundAndSweep",
1386
+ ) !== -1
1340
1387
  logger.console.log(
1341
- "[trails-sdk] getAccountTransactionHistory response",
1342
- response,
1388
+ "[trails-sdk] [GASLESS-FLOW] Gasless deposit events decoded",
1389
+ {
1390
+ chainId: originChainId,
1391
+ callFailed: (
1392
+ transactionStates[0].decodedGuestModuleEvents || []
1393
+ ).filter((e: any) => e?.type === "CallFailed").length,
1394
+ sweeperEvents: (
1395
+ transactionStates[0].decodedTrailsTokenSweeperEvents || []
1396
+ ).length,
1397
+ refunded: transactionStates[0].refunded,
1398
+ },
1343
1399
  )
1344
- if (response.transactions.length > 0) {
1345
- const tx = response.transactions[0]
1346
- if (!tx?.txnHash) {
1347
- await new Promise((resolve) => setTimeout(resolve, 1000))
1348
- continue
1349
- }
1350
- // const isReceive = tx.transfers.some(
1351
- // (transfer) => transfer.transferType === "RECEIVE",
1352
- // )
1353
- // if (!isReceive) {
1354
- // await new Promise((resolve) => setTimeout(resolve, 1000))
1355
- // continue
1356
- // }
1357
- const originDepositTxReceipt =
1358
- await publicClient.getTransactionReceipt({
1359
- hash: tx.txnHash as `0x${string}`,
1360
- })
1361
1400
 
1362
- originUserTxReceipt = originDepositTxReceipt
1401
+ // Check for transaction failure or refund
1402
+ const hasCallFailed = (
1403
+ transactionStates[0].decodedGuestModuleEvents || []
1404
+ ).some((e: any) => e?.type === "CallFailed")
1363
1405
 
1364
- transactionStates[0] = getTransactionStateFromReceipt(
1365
- originDepositTxReceipt,
1366
- originChainId,
1367
- transactionStates[0]?.label,
1406
+ if (transactionStates[0].refunded || hasCallFailed) {
1407
+ const errorMessage = transactionStates[0].refunded
1408
+ ? "Transaction was refunded"
1409
+ : "Transaction call failed"
1410
+
1411
+ logger.console.error(
1412
+ "[trails-sdk] [GASLESS-FLOW] Deposit transaction failed",
1413
+ {
1414
+ refunded: transactionStates[0].refunded,
1415
+ callFailed: hasCallFailed,
1416
+ },
1368
1417
  )
1369
- onTransactionStateChange(transactionStates)
1370
1418
 
1371
- if (onOriginSend) {
1372
- onOriginSend()
1419
+ // Call onCheckoutError callback if provided
1420
+ if (checkoutOnHandlers?.triggerCheckoutError) {
1421
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
1373
1422
  }
1374
- break
1375
1423
  }
1376
1424
  } catch (error) {
1377
- logger.console.error("Error checking for deposit tx", error)
1425
+ logger.console.error(
1426
+ "[trails-sdk] Error decoding gasless deposit events",
1427
+ error,
1428
+ )
1378
1429
  }
1379
- await new Promise((resolve) => setTimeout(resolve, 1000))
1430
+
1431
+ onTransactionStateChange(transactionStates)
1432
+
1433
+ setTimeout(() => {
1434
+ const destinationChain = getChainInfo(destinationChainId)
1435
+ updatePersistentToast(
1436
+ "In Progress",
1437
+ `Your transaction to ${destinationChain?.name || "chain"} is in progress`,
1438
+ "info",
1439
+ )
1440
+ }, 1000)
1380
1441
  }
1381
- }
1382
1442
 
1383
- const _checkForDestinationDepositTx: () => Promise<TransactionReceipt | null> =
1384
- async () => {
1443
+ const checkForDepositTx = async () => {
1385
1444
  while (true) {
1386
1445
  try {
1387
1446
  const response = await getAccountTransactionHistory({
1388
- chainId: destinationChainId,
1389
- accountAddress: intent.payloads
1390
- .destinationIntentAddress as `0x${string}`,
1447
+ chainId: originChainId,
1448
+ accountAddress: originIntentAddress,
1391
1449
  })
1392
1450
  logger.console.log(
1393
1451
  "[trails-sdk] getAccountTransactionHistory response",
@@ -1406,19 +1464,24 @@ async function sendHandlerForDifferentChainDifferentToken({
1406
1464
  // await new Promise((resolve) => setTimeout(resolve, 1000))
1407
1465
  // continue
1408
1466
  // }
1409
- const destinationDepositTxReceipt =
1410
- await destinationPublicClient.getTransactionReceipt({
1467
+ const originDepositTxReceipt =
1468
+ await publicClient.getTransactionReceipt({
1411
1469
  hash: tx.txnHash as `0x${string}`,
1412
1470
  })
1413
1471
 
1414
- transactionStates[2] = getTransactionStateFromReceipt(
1415
- destinationDepositTxReceipt,
1416
- destinationChainId,
1417
- transactionStates[2]?.label,
1472
+ originUserTxReceipt = originDepositTxReceipt
1473
+
1474
+ transactionStates[0] = getTransactionStateFromReceipt(
1475
+ originDepositTxReceipt,
1476
+ originChainId,
1477
+ transactionStates[0]?.label,
1418
1478
  )
1419
1479
  onTransactionStateChange(transactionStates)
1420
1480
 
1421
- return destinationDepositTxReceipt
1481
+ if (onOriginSend) {
1482
+ onOriginSend()
1483
+ }
1484
+ break
1422
1485
  }
1423
1486
  } catch (error) {
1424
1487
  logger.console.error("Error checking for deposit tx", error)
@@ -1427,206 +1490,77 @@ async function sendHandlerForDifferentChainDifferentToken({
1427
1490
  }
1428
1491
  }
1429
1492
 
1430
- // Variables to store the waitForReceipt functions
1431
- let originMetaTxnReceiptPromise:
1432
- | (() => Promise<MetaTxnReceipt | null>)
1433
- | null = null
1434
- let destinationMetaTxnReceiptPromise:
1435
- | ((abortSignal?: AbortSignal) => Promise<MetaTxnReceipt | null>)
1436
- | ((abortSignal?: AbortSignal) => Promise<TransactionReceipt | null>)
1437
- | null = null
1438
-
1439
- // First phase: Send meta transactions and queue CCTP
1440
- const originSendMetaTxnPromise = async () => {
1441
- logger.console.log("[trails-sdk] Starting originSendMetaTxnPromise", {
1442
- hasMetaTxn: !!intent.payloads.metaTxns[0],
1443
- hasPrecondition: !!intent.payloads.preconditions[0],
1444
- metaTxnId: intent.payloads.metaTxns[0]?.id,
1445
- chainId: intent.payloads.metaTxns[0]?.chainId,
1446
- })
1447
-
1448
- if (intent.payloads.metaTxns[0] && intent.payloads.preconditions[0]) {
1449
- // Extract fee quote from intent response using metatxnid as key
1450
- const metaTxnId = intent.payloads.metaTxns[0].id
1451
- const feeQuote = intent.payloads.feeQuotes?.[metaTxnId]
1452
- logger.console.log(
1453
- "[trails-sdk] Extracted fee quote for origin meta txn",
1454
- {
1455
- metaTxnId,
1456
- feeQuote,
1457
- hasFeeQuote: !!feeQuote,
1458
- },
1459
- )
1460
-
1461
- logger.console.log(
1462
- "[trails-sdk] Calling sendMetaTxAndWaitForReceipt for origin",
1463
- {
1464
- metaTxnId,
1465
- chainId: intent.payloads.metaTxns[0].chainId,
1466
- walletAddress: intent.payloads.metaTxns[0].walletAddress,
1467
- contract: intent.payloads.metaTxns[0].contract,
1468
- },
1469
- )
1470
-
1471
- const { waitForReceipt } = await sendMetaTxAndWaitForReceipt({
1472
- metaTx: intent.payloads.metaTxns[0] as MetaTxn,
1473
- relayer: originRelayer,
1474
- precondition: intent.payloads
1475
- .preconditions[0] as IntentPrecondition,
1476
- feeQuote: feeQuote,
1477
- })
1478
-
1479
- logger.console.log(
1480
- "[trails-sdk] Origin meta transaction sent successfully",
1481
- {
1482
- metaTxnId,
1483
- },
1484
- )
1485
-
1486
- // Store the waitForReceipt function for later use
1487
- originMetaTxnReceiptPromise = waitForReceipt
1488
- } else {
1489
- logger.console.warn(
1490
- "[trails-sdk] Skipping origin sendMetaTxn - missing metaTxn or precondition",
1491
- {
1492
- hasMetaTxn: !!intent.payloads.metaTxns[0],
1493
- hasPrecondition: !!intent.payloads.preconditions[0],
1494
- },
1495
- )
1496
- }
1497
- }
1498
-
1499
- const destinationSendMetaTxnPromise = async () => {
1500
- logger.console.log(
1501
- "[trails-sdk] Starting destinationSendMetaTxnPromise",
1502
- {
1503
- quoteProvider: intent.payloads.quote.quoteProvider,
1504
- hasQuoteProviderRequestId:
1505
- !!intent.payloads.quote.quoteProviderRequestId,
1506
- hasPrecondition1: !!intent.payloads.preconditions[1],
1507
- hasMetaTxn1: !!intent.payloads.metaTxns[1],
1508
- },
1509
- )
1510
-
1511
- if (
1512
- intent.payloads.quote.quoteProvider === "relay" &&
1513
- intent.payloads.quote.quoteProviderRequestId &&
1514
- !intent.payloads.preconditions[1] &&
1515
- !intent.payloads.metaTxns[1]
1516
- ) {
1517
- logger.console.log(
1518
- "[trails-sdk] Setting up relay destination promise",
1519
- {
1520
- quoteProviderRequestId:
1521
- intent.payloads.quote.quoteProviderRequestId,
1522
- },
1523
- )
1524
- // For relay, we'll wait for the receipt in the wait phase
1525
- // Just store the requestId for later use
1526
- destinationMetaTxnReceiptPromise = async (
1527
- abortSignal?: AbortSignal,
1528
- ) => {
1529
- logger.console.log(
1530
- "[trails-sdk] waitForRelayDestinationTx starting",
1531
- {
1532
- quoteProviderRequestId:
1533
- intent.payloads.quote.quoteProviderRequestId,
1534
- aborted: abortSignal?.aborted,
1535
- },
1536
- )
1537
- try {
1538
- // Check if we should abort before starting
1539
- if (abortSignal?.aborted) {
1540
- logger.console.log(
1541
- "[trails-sdk] Aborting relay destination tx due to abort signal",
1542
- )
1543
- return null
1544
- }
1545
-
1546
- const txHash = await waitForRelayDestinationTx(
1547
- intent.payloads.quote.quoteProviderRequestId,
1548
- )
1549
- logger.console.log(
1550
- "[trails-sdk] waitForRelayDestinationTx completed",
1551
- {
1552
- txHash,
1553
- quoteProviderRequestId:
1554
- intent.payloads.quote.quoteProviderRequestId,
1555
- },
1556
- )
1557
- if (txHash) {
1493
+ const _checkForDestinationDepositTx: () => Promise<TransactionReceipt | null> =
1494
+ async () => {
1495
+ while (true) {
1496
+ try {
1497
+ const response = await getAccountTransactionHistory({
1498
+ chainId: destinationChainId,
1499
+ accountAddress: intent.payloads
1500
+ .destinationIntentAddress as `0x${string}`,
1501
+ })
1558
1502
  logger.console.log(
1559
- "[trails-sdk] Fetching transaction receipt for relay destination",
1560
- {
1561
- txHash,
1562
- chainId: destinationChainId,
1563
- },
1503
+ "[trails-sdk] getAccountTransactionHistory response",
1504
+ response,
1564
1505
  )
1506
+ if (response.transactions.length > 0) {
1507
+ const tx = response.transactions[0]
1508
+ if (!tx?.txnHash) {
1509
+ await new Promise((resolve) => setTimeout(resolve, 1000))
1510
+ continue
1511
+ }
1512
+ // const isReceive = tx.transfers.some(
1513
+ // (transfer) => transfer.transferType === "RECEIVE",
1514
+ // )
1515
+ // if (!isReceive) {
1516
+ // await new Promise((resolve) => setTimeout(resolve, 1000))
1517
+ // continue
1518
+ // }
1519
+ const destinationDepositTxReceipt =
1520
+ await destinationPublicClient.getTransactionReceipt({
1521
+ hash: tx.txnHash as `0x${string}`,
1522
+ })
1565
1523
 
1566
- const destinationTxnReceipt =
1567
- await destinationPublicClient.getTransactionReceipt({
1568
- hash: txHash as `0x${string}`,
1569
- })
1570
- logger.console.log(
1571
- "[trails-sdk] relay destinationTxnReceipt received",
1572
- {
1573
- txHash,
1574
- blockNumber: destinationTxnReceipt?.blockNumber,
1575
- status: destinationTxnReceipt?.status,
1576
- gasUsed: destinationTxnReceipt?.gasUsed,
1577
- },
1578
- )
1579
- if (transactionStates[2]) {
1580
1524
  transactionStates[2] = getTransactionStateFromReceipt(
1581
- destinationTxnReceipt,
1525
+ destinationDepositTxReceipt,
1582
1526
  destinationChainId,
1583
1527
  transactionStates[2]?.label,
1584
1528
  )
1529
+ onTransactionStateChange(transactionStates)
1530
+
1531
+ return destinationDepositTxReceipt
1585
1532
  }
1586
- onTransactionStateChange(transactionStates)
1587
- return destinationTxnReceipt
1588
- } else {
1589
- logger.console.warn(
1590
- "[trails-sdk] No txHash returned from waitForRelayDestinationTx",
1591
- {
1592
- quoteProviderRequestId:
1593
- intent.payloads.quote.quoteProviderRequestId,
1594
- },
1595
- )
1596
- }
1597
- } catch (error: unknown) {
1598
- logger.console.error(
1599
- "[trails-sdk] Error waiting for relay destination tx",
1600
- {
1601
- error: error instanceof Error ? error.message : String(error),
1602
- quoteProviderRequestId:
1603
- intent.payloads.quote.quoteProviderRequestId,
1604
- },
1605
- )
1606
- if (transactionStates?.[2]) {
1607
- transactionStates[2].state = "failed"
1608
- onTransactionStateChange(transactionStates)
1533
+ } catch (error) {
1534
+ logger.console.error("Error checking for deposit tx", error)
1609
1535
  }
1610
- throw error
1536
+ await new Promise((resolve) => setTimeout(resolve, 1000))
1611
1537
  }
1612
- return null
1613
1538
  }
1614
- } else {
1615
- logger.console.log(
1616
- "[trails-sdk] Setting up destination meta transaction promise (non-relay)",
1617
- {
1618
- quoteProvider: intent.payloads.quote.quoteProvider,
1619
- hasMetaTxn1: !!intent.payloads.metaTxns[1],
1620
- hasPrecondition1: !!intent.payloads.preconditions[1],
1621
- },
1622
- )
1623
1539
 
1624
- if (intent.payloads.metaTxns[1] && intent.payloads.preconditions[1]) {
1540
+ // Variables to store the waitForReceipt functions
1541
+ let originMetaTxnReceiptPromise:
1542
+ | (() => Promise<MetaTxnReceipt | null>)
1543
+ | null = null
1544
+ let destinationMetaTxnReceiptPromise:
1545
+ | ((abortSignal?: AbortSignal) => Promise<MetaTxnReceipt | null>)
1546
+ | ((abortSignal?: AbortSignal) => Promise<TransactionReceipt | null>)
1547
+ | null = null
1548
+
1549
+ // First phase: Send meta transactions and queue CCTP
1550
+ const originSendMetaTxnPromise = async () => {
1551
+ logger.console.log("[trails-sdk] Starting originSendMetaTxnPromise", {
1552
+ hasMetaTxn: !!intent.payloads.metaTxns[0],
1553
+ hasPrecondition: !!intent.payloads.preconditions[0],
1554
+ metaTxnId: intent.payloads.metaTxns[0]?.id,
1555
+ chainId: intent.payloads.metaTxns[0]?.chainId,
1556
+ })
1557
+
1558
+ if (intent.payloads.metaTxns[0] && intent.payloads.preconditions[0]) {
1625
1559
  // Extract fee quote from intent response using metatxnid as key
1626
- const metaTxnId = intent.payloads.metaTxns[1].id
1560
+ const metaTxnId = intent.payloads.metaTxns[0].id
1627
1561
  const feeQuote = intent.payloads.feeQuotes?.[metaTxnId]
1628
1562
  logger.console.log(
1629
- "[trails-sdk] Extracted fee quote for destination meta txn",
1563
+ "[trails-sdk] Extracted fee quote for origin meta txn",
1630
1564
  {
1631
1565
  metaTxnId,
1632
1566
  feeQuote,
@@ -1635,324 +1569,597 @@ async function sendHandlerForDifferentChainDifferentToken({
1635
1569
  )
1636
1570
 
1637
1571
  logger.console.log(
1638
- "[trails-sdk] Calling sendMetaTxAndWaitForReceipt for destination",
1572
+ "[trails-sdk] Calling sendMetaTxAndWaitForReceipt for origin",
1639
1573
  {
1640
1574
  metaTxnId,
1641
- chainId: intent.payloads.metaTxns[1].chainId,
1642
- walletAddress: intent.payloads.metaTxns[1].walletAddress,
1643
- contract: intent.payloads.metaTxns[1].contract,
1575
+ chainId: intent.payloads.metaTxns[0].chainId,
1576
+ walletAddress: intent.payloads.metaTxns[0].walletAddress,
1577
+ contract: intent.payloads.metaTxns[0].contract,
1644
1578
  },
1645
1579
  )
1646
1580
 
1647
1581
  const { waitForReceipt } = await sendMetaTxAndWaitForReceipt({
1648
- metaTx: intent.payloads.metaTxns[1] as MetaTxn,
1649
- relayer: destinationRelayer,
1582
+ metaTx: intent.payloads.metaTxns[0] as MetaTxn,
1583
+ relayer: originRelayer,
1650
1584
  precondition: intent.payloads
1651
- .preconditions[1] as IntentPrecondition,
1585
+ .preconditions[0] as IntentPrecondition,
1652
1586
  feeQuote: feeQuote,
1653
1587
  })
1654
1588
 
1655
1589
  logger.console.log(
1656
- "[trails-sdk] Destination meta transaction sent successfully",
1590
+ "[trails-sdk] Origin meta transaction sent successfully",
1657
1591
  {
1658
1592
  metaTxnId,
1659
1593
  },
1660
1594
  )
1661
1595
 
1662
1596
  // Store the waitForReceipt function for later use
1663
- destinationMetaTxnReceiptPromise = waitForReceipt
1597
+ originMetaTxnReceiptPromise = waitForReceipt
1664
1598
  } else {
1665
1599
  logger.console.warn(
1666
- "[trails-sdk] Skipping destination sendMetaTxn - missing metaTxn or precondition",
1600
+ "[trails-sdk] Skipping origin sendMetaTxn - missing metaTxn or precondition",
1667
1601
  {
1668
- hasMetaTxn: !!intent.payloads.metaTxns[1],
1669
- hasPrecondition: !!intent.payloads.preconditions[1],
1602
+ hasMetaTxn: !!intent.payloads.metaTxns[0],
1603
+ hasPrecondition: !!intent.payloads.preconditions[0],
1670
1604
  },
1671
1605
  )
1672
1606
  }
1673
- // } else if (intent.payloads.destinationIntentAddress) {
1674
- // destinationMetaTxnReceiptPromise = checkForDestinationDepositTx
1675
- // }
1676
1607
  }
1677
- }
1678
-
1679
- let queueCctpPromise: (() => Promise<void>) | null = null
1680
1608
 
1681
- const isCctp = intent.payloads.quote.quoteProvider === "cctp"
1682
- if (isCctp) {
1683
- queueCctpPromise = async () => {
1684
- while (true) {
1685
- const originMetaTxnHash = originMetaTxnReceipt?.txnHash
1686
- if (originMetaTxnHash) {
1687
- await queueCCTPTransfer({
1688
- trailsClient,
1689
- sourceTxHash: originMetaTxnHash,
1690
- sourceChainId: originChainId,
1691
- destinationChainId: destinationChainId,
1692
- })
1693
- break
1694
- }
1695
- await new Promise((resolve) => setTimeout(resolve, 1000))
1696
- }
1697
- }
1698
- } else {
1699
- queueCctpPromise = () => Promise.resolve()
1700
- }
1609
+ const destinationSendMetaTxnPromise = async () => {
1610
+ logger.console.log(
1611
+ "[trails-sdk] Starting destinationSendMetaTxnPromise",
1612
+ {
1613
+ quoteProvider: intent.payloads.quote.quoteProvider,
1614
+ hasQuoteProviderRequestId:
1615
+ !!intent.payloads.quote.quoteProviderRequestId,
1616
+ hasPrecondition1: !!intent.payloads.preconditions[1],
1617
+ hasMetaTxn1: !!intent.payloads.metaTxns[1],
1618
+ },
1619
+ )
1701
1620
 
1702
- checkForDepositTx().catch((error) => {
1703
- console.error("Error checking for deposit tx", error)
1704
- })
1621
+ if (
1622
+ intent.payloads.quote.quoteProvider === "relay" &&
1623
+ intent.payloads.quote.quoteProviderRequestId &&
1624
+ !intent.payloads.preconditions[1] &&
1625
+ !intent.payloads.metaTxns[1]
1626
+ ) {
1627
+ logger.console.log(
1628
+ "[trails-sdk] Setting up relay destination promise",
1629
+ {
1630
+ quoteProviderRequestId:
1631
+ intent.payloads.quote.quoteProviderRequestId,
1632
+ },
1633
+ )
1634
+ // For relay, we'll wait for the receipt in the wait phase
1635
+ // Just store the requestId for later use
1636
+ destinationMetaTxnReceiptPromise = async (
1637
+ abortSignal?: AbortSignal,
1638
+ ) => {
1639
+ logger.console.log(
1640
+ "[trails-sdk] waitForRelayDestinationTx starting",
1641
+ {
1642
+ quoteProviderRequestId:
1643
+ intent.payloads.quote.quoteProviderRequestId,
1644
+ aborted: abortSignal?.aborted,
1645
+ },
1646
+ )
1647
+ try {
1648
+ // Check if we should abort before starting
1649
+ if (abortSignal?.aborted) {
1650
+ logger.console.log(
1651
+ "[trails-sdk] Aborting relay destination tx due to abort signal",
1652
+ )
1653
+ return null
1654
+ }
1705
1655
 
1706
- // Phase 1: Send meta transactions and queue CCTP
1707
- logger.console.log(
1708
- "[trails-sdk] Starting Phase 1: Sending meta transactions and queuing CCTP",
1709
- )
1656
+ const txHash = await waitForRelayDestinationTx(
1657
+ intent.payloads.quote.quoteProviderRequestId,
1658
+ )
1659
+ logger.console.log(
1660
+ "[trails-sdk] waitForRelayDestinationTx completed",
1661
+ {
1662
+ txHash,
1663
+ quoteProviderRequestId:
1664
+ intent.payloads.quote.quoteProviderRequestId,
1665
+ },
1666
+ )
1667
+ if (txHash) {
1668
+ logger.console.log(
1669
+ "[trails-sdk] Fetching transaction receipt for relay destination",
1670
+ {
1671
+ txHash,
1672
+ chainId: destinationChainId,
1673
+ },
1674
+ )
1710
1675
 
1711
- await Promise.all([
1712
- originSendMetaTxnPromise(),
1713
- destinationSendMetaTxnPromise(),
1714
- ])
1676
+ const destinationTxnReceipt =
1677
+ await destinationPublicClient.getTransactionReceipt({
1678
+ hash: txHash as `0x${string}`,
1679
+ })
1680
+ logger.console.log(
1681
+ "[trails-sdk] relay destinationTxnReceipt received",
1682
+ {
1683
+ txHash,
1684
+ blockNumber: destinationTxnReceipt?.blockNumber,
1685
+ status: destinationTxnReceipt?.status,
1686
+ gasUsed: destinationTxnReceipt?.gasUsed,
1687
+ },
1688
+ )
1689
+ if (transactionStates[2]) {
1690
+ transactionStates[2] = getTransactionStateFromReceipt(
1691
+ destinationTxnReceipt,
1692
+ destinationChainId,
1693
+ transactionStates[2]?.label,
1694
+ )
1695
+ }
1696
+ onTransactionStateChange(transactionStates)
1697
+ return destinationTxnReceipt
1698
+ } else {
1699
+ logger.console.warn(
1700
+ "[trails-sdk] No txHash returned from waitForRelayDestinationTx",
1701
+ {
1702
+ quoteProviderRequestId:
1703
+ intent.payloads.quote.quoteProviderRequestId,
1704
+ },
1705
+ )
1706
+ }
1707
+ } catch (error: unknown) {
1708
+ logger.console.error(
1709
+ "[trails-sdk] Error waiting for relay destination tx",
1710
+ {
1711
+ error:
1712
+ error instanceof Error ? error.message : String(error),
1713
+ quoteProviderRequestId:
1714
+ intent.payloads.quote.quoteProviderRequestId,
1715
+ },
1716
+ )
1717
+ if (transactionStates?.[2]) {
1718
+ transactionStates[2].state = "failed"
1719
+ onTransactionStateChange(transactionStates)
1720
+ }
1721
+ throw error
1722
+ }
1723
+ return null
1724
+ }
1725
+ } else {
1726
+ logger.console.log(
1727
+ "[trails-sdk] Setting up destination meta transaction promise (non-relay)",
1728
+ {
1729
+ quoteProvider: intent.payloads.quote.quoteProvider,
1730
+ hasMetaTxn1: !!intent.payloads.metaTxns[1],
1731
+ hasPrecondition1: !!intent.payloads.preconditions[1],
1732
+ },
1733
+ )
1715
1734
 
1716
- logger.console.log("[trails-sdk] Phase 1 completed successfully")
1735
+ if (
1736
+ intent.payloads.metaTxns[1] &&
1737
+ intent.payloads.preconditions[1]
1738
+ ) {
1739
+ // Extract fee quote from intent response using metatxnid as key
1740
+ const metaTxnId = intent.payloads.metaTxns[1].id
1741
+ const feeQuote = intent.payloads.feeQuotes?.[metaTxnId]
1742
+ logger.console.log(
1743
+ "[trails-sdk] Extracted fee quote for destination meta txn",
1744
+ {
1745
+ metaTxnId,
1746
+ feeQuote,
1747
+ hasFeeQuote: !!feeQuote,
1748
+ },
1749
+ )
1717
1750
 
1718
- // Phase 2: Wait for receipts and execute deposit
1719
- logger.console.log(
1720
- "[trails-sdk] Starting Phase 2: Waiting for receipts and executing deposit",
1721
- )
1751
+ logger.console.log(
1752
+ "[trails-sdk] Calling sendMetaTxAndWaitForReceipt for destination",
1753
+ {
1754
+ metaTxnId,
1755
+ chainId: intent.payloads.metaTxns[1].chainId,
1756
+ walletAddress: intent.payloads.metaTxns[1].walletAddress,
1757
+ contract: intent.payloads.metaTxns[1].contract,
1758
+ },
1759
+ )
1722
1760
 
1723
- const waitForOriginMetaTxnReceiptPromise = async () => {
1724
- logger.console.log(
1725
- "[trails-sdk] Waiting for origin meta transaction receipt",
1726
- )
1727
- if (originMetaTxnReceiptPromise) {
1728
- try {
1729
- originMetaTxnReceipt = await originMetaTxnReceiptPromise()
1761
+ const { waitForReceipt } = await sendMetaTxAndWaitForReceipt({
1762
+ metaTx: intent.payloads.metaTxns[1] as MetaTxn,
1763
+ relayer: destinationRelayer,
1764
+ precondition: intent.payloads
1765
+ .preconditions[1] as IntentPrecondition,
1766
+ feeQuote: feeQuote,
1767
+ })
1730
1768
 
1731
- if (originMetaTxnReceipt && transactionStates[1]) {
1732
- transactionStates[1] = getTransactionStateFromReceipt(
1733
- originMetaTxnReceipt,
1734
- originChainId,
1735
- transactionStates[1]?.label,
1769
+ logger.console.log(
1770
+ "[trails-sdk] Destination meta transaction sent successfully",
1771
+ {
1772
+ metaTxnId,
1773
+ },
1736
1774
  )
1737
- onTransactionStateChange(transactionStates)
1738
1775
 
1739
- try {
1740
- const receipt = await publicClient.getTransactionReceipt({
1741
- hash: originMetaTxnReceipt.txnHash as `0x${string}`,
1742
- })
1743
- transactionStates[1].decodedTrailsTokenSweeperEvents =
1744
- decodeTrailsTokenSweeperEvents(receipt)
1745
- transactionStates[1].decodedGuestModuleEvents =
1746
- decodeGuestModuleEvents(receipt)
1747
- transactionStates[1].refunded =
1748
- transactionStates[1].decodedTrailsTokenSweeperEvents.findIndex(
1749
- (event) =>
1750
- event.type === "Refund" ||
1751
- event.type === "RefundAndSweep",
1752
- ) !== -1
1753
- logger.console.log("[trails-sdk] Origin meta-tx events", {
1754
- chainId: originChainId,
1755
- callFailed: (
1756
- transactionStates[1].decodedGuestModuleEvents || []
1757
- ).filter((e: any) => e?.type === "CallFailed").length,
1758
- sweeperEvents: (
1759
- transactionStates[1].decodedTrailsTokenSweeperEvents || []
1760
- ).length,
1761
- refunded: transactionStates[1].refunded,
1776
+ // Store the waitForReceipt function for later use
1777
+ destinationMetaTxnReceiptPromise = waitForReceipt
1778
+ } else {
1779
+ logger.console.warn(
1780
+ "[trails-sdk] Skipping destination sendMetaTxn - missing metaTxn or precondition",
1781
+ {
1782
+ hasMetaTxn: !!intent.payloads.metaTxns[1],
1783
+ hasPrecondition: !!intent.payloads.preconditions[1],
1784
+ },
1785
+ )
1786
+ }
1787
+ // } else if (intent.payloads.destinationIntentAddress) {
1788
+ // destinationMetaTxnReceiptPromise = checkForDestinationDepositTx
1789
+ // }
1790
+ }
1791
+ }
1792
+
1793
+ let queueCctpPromise: (() => Promise<void>) | null = null
1794
+
1795
+ const isCctp = intent.payloads.quote.quoteProvider === "cctp"
1796
+ if (isCctp) {
1797
+ queueCctpPromise = async () => {
1798
+ while (true) {
1799
+ const originMetaTxnHash = originMetaTxnReceipt?.txnHash
1800
+ if (originMetaTxnHash) {
1801
+ await queueCCTPTransfer({
1802
+ trailsClient,
1803
+ sourceTxHash: originMetaTxnHash,
1804
+ sourceChainId: originChainId,
1805
+ destinationChainId: destinationChainId,
1762
1806
  })
1763
- onTransactionStateChange(transactionStates)
1764
- } catch (error) {
1765
- logger.console.error("Error decoding origin tx events", error)
1807
+ break
1766
1808
  }
1809
+ await new Promise((resolve) => setTimeout(resolve, 1000))
1767
1810
  }
1768
- } catch (error) {
1769
- logger.console.error(
1770
- "[trails-sdk] Error waiting for origin receipt:",
1771
- error,
1772
- )
1773
1811
  }
1774
1812
  } else {
1775
- logger.console.log(
1776
- "[trails-sdk] No origin meta transaction receipt promise to wait for",
1777
- )
1813
+ queueCctpPromise = () => Promise.resolve()
1778
1814
  }
1779
- }
1780
1815
 
1781
- const waitForDestinationMetaTxnReceiptPromise = async () => {
1816
+ checkForDepositTx().catch((error) => {
1817
+ console.error("Error checking for deposit tx", error)
1818
+ })
1819
+
1820
+ // Phase 1: Send meta transactions and queue CCTP
1782
1821
  logger.console.log(
1783
- "[trails-sdk] Waiting for destination meta transaction receipt",
1822
+ "[trails-sdk] Starting Phase 1: Sending meta transactions and queuing CCTP",
1784
1823
  )
1785
- if (destinationMetaTxnReceiptPromise) {
1786
- try {
1787
- // Create abort controller for cancelling destination polling
1788
- const abortController = new AbortController()
1789
-
1790
- // Race between destination receipt and failure polling
1791
- const destinationReceiptPromise = destinationMetaTxnReceiptPromise
1792
- ? destinationMetaTxnReceiptPromise(abortController.signal)
1793
- : Promise.resolve(null)
1794
-
1795
- const failurePollingPromise = new Promise<null>((resolve) => {
1796
- const pollForFailures = () => {
1797
- const isPreviousTxCallFailed =
1798
- transactionStates?.some((tx) => tx.state === "failed") ||
1799
- transactionStates?.some((tx) =>
1800
- tx?.decodedGuestModuleEvents?.some(
1801
- (event) => event.type === "CallFailed",
1802
- ),
1803
- )
1804
1824
 
1805
- if (isPreviousTxCallFailed) {
1806
- logger.console.log(
1807
- "[trails-sdk] Aborting destination meta transaction due to previous transaction failure",
1808
- )
1809
- abortController.abort()
1810
- resolve(null)
1811
- } else {
1812
- // Continue polling every 1 second
1813
- setTimeout(pollForFailures, 1000)
1814
- }
1815
- }
1816
- pollForFailures()
1817
- })
1825
+ await Promise.all([
1826
+ originSendMetaTxnPromise(),
1827
+ destinationSendMetaTxnPromise(),
1828
+ ])
1829
+
1830
+ logger.console.log("[trails-sdk] Phase 1 completed successfully")
1831
+
1832
+ // Phase 2: Wait for receipts and execute deposit
1833
+ logger.console.log(
1834
+ "[trails-sdk] Starting Phase 2: Waiting for receipts and executing deposit",
1835
+ )
1836
+
1837
+ const waitForOriginMetaTxnReceiptPromise = async () => {
1838
+ logger.console.log(
1839
+ "[trails-sdk] Waiting for origin meta transaction receipt",
1840
+ )
1841
+ if (originMetaTxnReceiptPromise) {
1842
+ try {
1843
+ originMetaTxnReceipt = await originMetaTxnReceiptPromise()
1818
1844
 
1819
- destinationMetaTxnReceipt = (await Promise.race([
1820
- destinationReceiptPromise,
1821
- failurePollingPromise,
1822
- ])) as MetaTxnReceipt
1845
+ if (originMetaTxnReceipt && transactionStates[1]) {
1846
+ transactionStates[1] = getTransactionStateFromReceipt(
1847
+ originMetaTxnReceipt,
1848
+ originChainId,
1849
+ transactionStates[1]?.label,
1850
+ )
1851
+ onTransactionStateChange(transactionStates)
1852
+
1853
+ try {
1854
+ const receipt = await publicClient.getTransactionReceipt({
1855
+ hash: originMetaTxnReceipt.txnHash as `0x${string}`,
1856
+ })
1857
+ transactionStates[1].decodedTrailsTokenSweeperEvents =
1858
+ decodeTrailsTokenSweeperEvents(receipt)
1859
+ transactionStates[1].decodedGuestModuleEvents =
1860
+ decodeGuestModuleEvents(receipt)
1861
+ transactionStates[1].refunded =
1862
+ transactionStates[1].decodedTrailsTokenSweeperEvents.findIndex(
1863
+ (event) =>
1864
+ event.type === "Refund" ||
1865
+ event.type === "RefundAndSweep",
1866
+ ) !== -1
1867
+ logger.console.log("[trails-sdk] Origin meta-tx events", {
1868
+ chainId: originChainId,
1869
+ callFailed: (
1870
+ transactionStates[1].decodedGuestModuleEvents || []
1871
+ ).filter((e: any) => e?.type === "CallFailed").length,
1872
+ sweeperEvents: (
1873
+ transactionStates[1].decodedTrailsTokenSweeperEvents || []
1874
+ ).length,
1875
+ refunded: transactionStates[1].refunded,
1876
+ })
1823
1877
 
1878
+ // Check for transaction failure or refund
1879
+ const hasCallFailed = (
1880
+ transactionStates[1].decodedGuestModuleEvents || []
1881
+ ).some((e: any) => e?.type === "CallFailed")
1882
+
1883
+ if (transactionStates[1].refunded || hasCallFailed) {
1884
+ const errorMessage = transactionStates[1].refunded
1885
+ ? "Origin transaction was refunded"
1886
+ : "Origin transaction call failed"
1887
+
1888
+ logger.console.error("[trails-sdk] Origin meta-tx failed", {
1889
+ refunded: transactionStates[1].refunded,
1890
+ callFailed: hasCallFailed,
1891
+ })
1892
+
1893
+ // Call onCheckoutError callback if provided
1894
+ if (checkoutOnHandlers?.triggerCheckoutError) {
1895
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
1896
+ }
1897
+ }
1898
+
1899
+ onTransactionStateChange(transactionStates)
1900
+ } catch (error) {
1901
+ logger.console.error("Error decoding origin tx events", error)
1902
+ }
1903
+ }
1904
+ } catch (error) {
1905
+ logger.console.error(
1906
+ "[trails-sdk] Error waiting for origin receipt:",
1907
+ error,
1908
+ )
1909
+ }
1910
+ } else {
1824
1911
  logger.console.log(
1825
- "[trails-sdk] destinationMetaTxnReceipt",
1826
- destinationMetaTxnReceipt,
1912
+ "[trails-sdk] No origin meta transaction receipt promise to wait for",
1827
1913
  )
1914
+ }
1915
+ }
1828
1916
 
1829
- if (destinationMetaTxnReceipt && transactionStates[2]) {
1830
- transactionStates[2] = getTransactionStateFromReceipt(
1917
+ const waitForDestinationMetaTxnReceiptPromise = async () => {
1918
+ logger.console.log(
1919
+ "[trails-sdk] Waiting for destination meta transaction receipt",
1920
+ )
1921
+ if (destinationMetaTxnReceiptPromise) {
1922
+ try {
1923
+ // Create abort controller for cancelling destination polling
1924
+ const abortController = new AbortController()
1925
+
1926
+ // Race between destination receipt and failure polling
1927
+ const destinationReceiptPromise = destinationMetaTxnReceiptPromise
1928
+ ? destinationMetaTxnReceiptPromise(abortController.signal)
1929
+ : Promise.resolve(null)
1930
+
1931
+ const failurePollingPromise = new Promise<null>((resolve) => {
1932
+ const pollForFailures = () => {
1933
+ const isPreviousTxCallFailed =
1934
+ transactionStates?.some((tx) => tx.state === "failed") ||
1935
+ transactionStates?.some((tx) =>
1936
+ tx?.decodedGuestModuleEvents?.some(
1937
+ (event) => event.type === "CallFailed",
1938
+ ),
1939
+ )
1940
+
1941
+ if (isPreviousTxCallFailed) {
1942
+ logger.console.log(
1943
+ "[trails-sdk] Aborting destination meta transaction due to previous transaction failure",
1944
+ )
1945
+ abortController.abort()
1946
+ resolve(null)
1947
+ } else {
1948
+ // Continue polling every 1 second
1949
+ setTimeout(pollForFailures, 1000)
1950
+ }
1951
+ }
1952
+ pollForFailures()
1953
+ })
1954
+
1955
+ destinationMetaTxnReceipt = (await Promise.race([
1956
+ destinationReceiptPromise,
1957
+ failurePollingPromise,
1958
+ ])) as MetaTxnReceipt
1959
+
1960
+ logger.console.log(
1961
+ "[trails-sdk] destinationMetaTxnReceipt",
1831
1962
  destinationMetaTxnReceipt,
1832
- destinationChainId,
1833
- transactionStates[2]?.label,
1834
1963
  )
1835
- onTransactionStateChange(transactionStates)
1836
1964
 
1837
- try {
1838
- const receipt =
1839
- await destinationPublicClient.getTransactionReceipt({
1840
- hash: destinationMetaTxnReceipt.txnHash as `0x${string}`,
1841
- })
1842
- transactionStates[2].decodedTrailsTokenSweeperEvents =
1843
- decodeTrailsTokenSweeperEvents(receipt)
1844
- transactionStates[2].decodedGuestModuleEvents =
1845
- decodeGuestModuleEvents(receipt)
1846
- transactionStates[2].refunded =
1847
- transactionStates[2].decodedTrailsTokenSweeperEvents.findIndex(
1848
- (event) =>
1849
- event.type === "Refund" ||
1850
- event.type === "RefundAndSweep",
1851
- ) !== -1 ||
1852
- (transactionStates[2].decodedTrailsTokenSweeperEvents.findIndex(
1853
- (event) => event.type === "Sweep",
1854
- ) !== -1 &&
1855
- transactionStates[2].decodedGuestModuleEvents.findIndex(
1856
- (event) => event.type === "CallFailed",
1857
- ) !== -1)
1858
- logger.console.log("[trails-sdk] Destination meta-tx events", {
1859
- chainId: destinationChainId,
1860
- callFailed: (
1861
- transactionStates[2].decodedGuestModuleEvents || []
1862
- ).filter((e: any) => e?.type === "CallFailed").length,
1863
- sweeperEvents: (
1864
- transactionStates[2].decodedTrailsTokenSweeperEvents || []
1865
- ).length,
1866
- refunded: transactionStates[2].refunded,
1867
- })
1965
+ if (destinationMetaTxnReceipt && transactionStates[2]) {
1966
+ transactionStates[2] = getTransactionStateFromReceipt(
1967
+ destinationMetaTxnReceipt,
1968
+ destinationChainId,
1969
+ transactionStates[2]?.label,
1970
+ )
1868
1971
  onTransactionStateChange(transactionStates)
1869
- } catch (error) {
1870
- console.error("Error decoding destination tx events", error)
1972
+
1973
+ try {
1974
+ const receipt =
1975
+ await destinationPublicClient.getTransactionReceipt({
1976
+ hash: destinationMetaTxnReceipt.txnHash as `0x${string}`,
1977
+ })
1978
+ transactionStates[2].decodedTrailsTokenSweeperEvents =
1979
+ decodeTrailsTokenSweeperEvents(receipt)
1980
+ transactionStates[2].decodedGuestModuleEvents =
1981
+ decodeGuestModuleEvents(receipt)
1982
+ transactionStates[2].refunded =
1983
+ transactionStates[2].decodedTrailsTokenSweeperEvents.findIndex(
1984
+ (event) =>
1985
+ event.type === "Refund" ||
1986
+ event.type === "RefundAndSweep",
1987
+ ) !== -1 ||
1988
+ (transactionStates[2].decodedTrailsTokenSweeperEvents.findIndex(
1989
+ (event) => event.type === "Sweep",
1990
+ ) !== -1 &&
1991
+ transactionStates[2].decodedGuestModuleEvents.findIndex(
1992
+ (event) => event.type === "CallFailed",
1993
+ ) !== -1)
1994
+ logger.console.log(
1995
+ "[trails-sdk] Destination meta-tx events",
1996
+ {
1997
+ chainId: destinationChainId,
1998
+ callFailed: (
1999
+ transactionStates[2].decodedGuestModuleEvents || []
2000
+ ).filter((e: any) => e?.type === "CallFailed").length,
2001
+ sweeperEvents: (
2002
+ transactionStates[2].decodedTrailsTokenSweeperEvents ||
2003
+ []
2004
+ ).length,
2005
+ refunded: transactionStates[2].refunded,
2006
+ },
2007
+ )
2008
+
2009
+ // Check for transaction failure or refund
2010
+ const hasCallFailed = (
2011
+ transactionStates[2].decodedGuestModuleEvents || []
2012
+ ).some((e: any) => e?.type === "CallFailed")
2013
+
2014
+ if (transactionStates[2].refunded || hasCallFailed) {
2015
+ const errorMessage = transactionStates[2].refunded
2016
+ ? "Destination transaction was refunded"
2017
+ : "Destination transaction call failed"
2018
+
2019
+ logger.console.error(
2020
+ "[trails-sdk] Destination meta-tx failed",
2021
+ {
2022
+ refunded: transactionStates[2].refunded,
2023
+ callFailed: hasCallFailed,
2024
+ },
2025
+ )
2026
+
2027
+ // Call onCheckoutError callback if provided
2028
+ if (checkoutOnHandlers?.triggerCheckoutError) {
2029
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
2030
+ }
2031
+ }
2032
+
2033
+ onTransactionStateChange(transactionStates)
2034
+ } catch (error) {
2035
+ console.error("Error decoding destination tx events", error)
2036
+ }
1871
2037
  }
1872
- }
1873
- } catch (error) {
1874
- console.error(
1875
- "[trails-sdk] Error waiting for destination receipt:",
1876
- error,
1877
- )
1878
- // For relay transactions, this might be expected if still waiting
1879
- if (intent.payloads.quote.quoteProvider === "relay") {
1880
- logger.console.log(
1881
- "[trails-sdk] Relay transaction still waiting, this is normal",
2038
+ } catch (error) {
2039
+ console.error(
2040
+ "[trails-sdk] Error waiting for destination receipt:",
2041
+ error,
1882
2042
  )
2043
+ // For relay transactions, this might be expected if still waiting
2044
+ if (intent.payloads.quote.quoteProvider === "relay") {
2045
+ logger.console.log(
2046
+ "[trails-sdk] Relay transaction still waiting, this is normal",
2047
+ )
2048
+ }
1883
2049
  }
2050
+ } else {
2051
+ logger.console.log(
2052
+ "[trails-sdk] No destination meta transaction receipt promise to wait for",
2053
+ )
1884
2054
  }
1885
- } else {
1886
- logger.console.log(
1887
- "[trails-sdk] No destination meta transaction receipt promise to wait for",
1888
- )
1889
2055
  }
1890
- }
1891
2056
 
1892
- logger.console.log(
1893
- "[trails-sdk] Executing Phase 2 Promise.all with deposit",
1894
- )
1895
- logger.console.log(
1896
- "[trails-sdk] About to call depositPromise - fundMethod is:",
1897
- fundMethod,
1898
- )
2057
+ logger.console.log(
2058
+ "[trails-sdk] Executing Phase 2 Promise.all with deposit",
2059
+ )
2060
+ logger.console.log(
2061
+ "[trails-sdk] About to call depositPromise - fundMethod is:",
2062
+ fundMethod,
2063
+ )
1899
2064
 
1900
- await Promise.all([
1901
- depositPromise(),
1902
- waitForOriginMetaTxnReceiptPromise(),
1903
- waitForDestinationMetaTxnReceiptPromise(),
1904
- queueCctpPromise(),
1905
- ])
1906
- logger.console.log("[trails-sdk] Phase 2 completed successfully")
2065
+ await Promise.all([
2066
+ depositPromise(),
2067
+ waitForOriginMetaTxnReceiptPromise(),
2068
+ waitForDestinationMetaTxnReceiptPromise(),
2069
+ queueCctpPromise(),
2070
+ ])
2071
+ logger.console.log("[trails-sdk] Phase 2 completed successfully")
1907
2072
 
1908
- // Track payment completion for different chain and different token
1909
- if (originUserTxReceipt && destinationMetaTxnReceipt) {
1910
- trackPaymentCompleted({
1911
- userAddress: account.address,
1912
- originIntentAddress,
1913
- originTxHash: (originUserTxReceipt as TransactionReceipt)
1914
- .transactionHash,
1915
- destinationTxHash: (destinationMetaTxnReceipt as MetaTxnReceipt)
1916
- ?.txnHash,
1917
- originChainId,
1918
- destinationChainId,
1919
- mode,
1920
- fundMethod,
1921
- originTokenSymbol,
1922
- originTokenAddress,
1923
- destinationTokenAddress,
1924
- destinationTokenSymbol,
1925
- depositTokenAmountUsd: depositAmountUsd?.toString(),
1926
- destinationTokenAmountUsd:
1927
- effectiveDestinationTokenAmountUsd?.toString(),
1928
- })
2073
+ // Track payment completion for different chain and different token
2074
+ if (originUserTxReceipt && destinationMetaTxnReceipt) {
2075
+ trackPaymentCompleted({
2076
+ userAddress: account.address,
2077
+ originIntentAddress,
2078
+ originTxHash: (originUserTxReceipt as TransactionReceipt)
2079
+ .transactionHash,
2080
+ destinationTxHash: (destinationMetaTxnReceipt as MetaTxnReceipt)
2081
+ ?.txnHash,
2082
+ originChainId,
2083
+ destinationChainId,
2084
+ mode,
2085
+ fundMethod,
2086
+ originTokenSymbol,
2087
+ originTokenAddress,
2088
+ destinationTokenAddress,
2089
+ destinationTokenSymbol,
2090
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
2091
+ destinationTokenAmountUsd:
2092
+ effectiveDestinationTokenAmountUsd?.toString(),
2093
+ })
1929
2094
 
1930
- // Call onCheckoutComplete callback if provided
1931
- if (checkoutOnHandlers?.triggerCheckoutComplete) {
1932
- checkoutOnHandlers.triggerCheckoutComplete()
1933
- }
1934
- } else {
1935
- if (
1936
- transactionStates[1] &&
1937
- transactionStates[1]?.transactionHash === "" &&
1938
- transactionStates[1]?.state === "pending"
1939
- ) {
1940
- transactionStates[1].state = "aborted"
1941
- onTransactionStateChange(transactionStates)
2095
+ // Call onCheckoutComplete callback if provided
2096
+ if (checkoutOnHandlers?.triggerCheckoutComplete) {
2097
+ checkoutOnHandlers.triggerCheckoutComplete()
2098
+ }
2099
+ } else {
2100
+ if (
2101
+ transactionStates[1] &&
2102
+ transactionStates[1]?.transactionHash === "" &&
2103
+ transactionStates[1]?.state === "pending"
2104
+ ) {
2105
+ transactionStates[1].state = "aborted"
2106
+ onTransactionStateChange(transactionStates)
2107
+ }
2108
+ if (
2109
+ transactionStates[2] &&
2110
+ transactionStates[2]?.transactionHash === "" &&
2111
+ transactionStates[2]?.state === "pending"
2112
+ ) {
2113
+ transactionStates[2].state = "aborted"
2114
+ onTransactionStateChange(transactionStates)
2115
+ }
2116
+
2117
+ // Track payment error if transactions didn't complete successfully
2118
+ trackPaymentError({
2119
+ error:
2120
+ "Payment transactions possibly did not complete successfully. Was not able to receive both origin and destination meta transaction receipts. May be an API error.",
2121
+ userAddress: account.address,
2122
+ originIntentAddress,
2123
+ mode,
2124
+ fundMethod,
2125
+ originChainId,
2126
+ destinationChainId,
2127
+ originTokenSymbol,
2128
+ originTokenAddress,
2129
+ destinationTokenAddress,
2130
+ destinationTokenSymbol,
2131
+ })
2132
+
2133
+ // Call onCheckoutError callback if provided
2134
+ if (checkoutOnHandlers?.triggerCheckoutError) {
2135
+ checkoutOnHandlers.triggerCheckoutError(
2136
+ "Payment transactions did not complete successfully",
2137
+ )
2138
+ }
1942
2139
  }
1943
- if (
1944
- transactionStates[2] &&
1945
- transactionStates[2]?.transactionHash === "" &&
1946
- transactionStates[2]?.state === "pending"
1947
- ) {
1948
- transactionStates[2].state = "aborted"
1949
- onTransactionStateChange(transactionStates)
2140
+
2141
+ return {
2142
+ originUserTxReceipt,
2143
+ originMetaTxnReceipt,
2144
+ destinationMetaTxnReceipt,
2145
+ totalCompletionSeconds: await getTxTimeDiff(
2146
+ transactionStates[0],
2147
+ transactionStates[2],
2148
+ ),
1950
2149
  }
2150
+ } catch (error) {
2151
+ const errorMessage =
2152
+ error instanceof Error
2153
+ ? error.message
2154
+ : "Unknown error occurred during transaction"
2155
+ logger.console.error(
2156
+ "[trails-sdk] Error in sendHandlerForDifferentChainDifferentToken:",
2157
+ error,
2158
+ )
1951
2159
 
1952
- // Track payment error if transactions didn't complete successfully
2160
+ // Track payment error
1953
2161
  trackPaymentError({
1954
- error:
1955
- "Payment transactions possibly did not complete successfully. Was not able to receive both origin and destination meta transaction receipts. May be an API error.",
2162
+ error: errorMessage,
1956
2163
  userAddress: account.address,
1957
2164
  originIntentAddress,
1958
2165
  mode,
@@ -1967,20 +2174,11 @@ async function sendHandlerForDifferentChainDifferentToken({
1967
2174
 
1968
2175
  // Call onCheckoutError callback if provided
1969
2176
  if (checkoutOnHandlers?.triggerCheckoutError) {
1970
- checkoutOnHandlers.triggerCheckoutError(
1971
- "Payment transactions did not complete successfully",
1972
- )
2177
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
1973
2178
  }
1974
- }
1975
2179
 
1976
- return {
1977
- originUserTxReceipt,
1978
- originMetaTxnReceipt,
1979
- destinationMetaTxnReceipt,
1980
- totalCompletionSeconds: await getTxTimeDiff(
1981
- transactionStates[0],
1982
- transactionStates[2],
1983
- ),
2180
+ // Re-throw the error so caller can handle if needed
2181
+ throw error
1984
2182
  }
1985
2183
  },
1986
2184
  }
@@ -2040,7 +2238,6 @@ async function sendHandlerForSameChainSameToken({
2040
2238
  })
2041
2239
 
2042
2240
  let noSufficientBalance = false
2043
- const minimumNotMet = false
2044
2241
 
2045
2242
  const { hasEnoughBalance } = await checkAccountBalance({
2046
2243
  account,
@@ -2071,7 +2268,6 @@ async function sendHandlerForSameChainSameToken({
2071
2268
  slippageTolerance,
2072
2269
  quoteProvider: "",
2073
2270
  noSufficientBalance,
2074
- minimumNotMet,
2075
2271
  })
2076
2272
 
2077
2273
  // Call onCheckoutQuote callback if provided
@@ -2085,198 +2281,240 @@ async function sendHandlerForSameChainSameToken({
2085
2281
  onOriginSend,
2086
2282
  }: {
2087
2283
  onOriginSend?: () => void
2088
- feeTokenAddress?: string | null
2089
2284
  }): Promise<SendReturn> => {
2090
- const { hasEnoughBalance, balanceError } = await checkAccountBalance({
2091
- account,
2092
- tokenAddress: effectiveOriginTokenAddress,
2093
- depositAmount: swapAmount,
2094
- publicClient: effectivePublicClient,
2095
- })
2285
+ try {
2286
+ const { hasEnoughBalance, balanceError } = await checkAccountBalance({
2287
+ account,
2288
+ tokenAddress: effectiveOriginTokenAddress,
2289
+ depositAmount: swapAmount,
2290
+ publicClient: effectivePublicClient,
2291
+ })
2096
2292
 
2097
- if (!hasEnoughBalance) {
2098
- throw balanceError
2099
- }
2293
+ if (!hasEnoughBalance) {
2294
+ throw balanceError
2295
+ }
2296
+
2297
+ const depositAmountFormatted = Number(
2298
+ formatUnits(BigInt(swapAmount), originTokenDecimals),
2299
+ )
2300
+ const depositAmountUsd = calcAmountUsdPrice({
2301
+ amount: depositAmountFormatted,
2302
+ usdPrice: sourceTokenPriceUsd,
2303
+ })
2304
+
2305
+ const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
2306
+ const originCallParams = {
2307
+ to: hasCustomCalldata
2308
+ ? recipient
2309
+ : effectiveOriginTokenAddress === zeroAddress
2310
+ ? recipient
2311
+ : effectiveOriginTokenAddress,
2312
+ data: hasCustomCalldata
2313
+ ? destinationCalldata
2314
+ : effectiveOriginTokenAddress === zeroAddress
2315
+ ? "0x"
2316
+ : getERC20TransferData({
2317
+ recipient,
2318
+ amount: BigInt(swapAmount),
2319
+ }),
2320
+ value:
2321
+ effectiveOriginTokenAddress === zeroAddress
2322
+ ? BigInt(swapAmount)
2323
+ : "0",
2324
+ chainId: effectiveOriginChainId,
2325
+ chain: effectiveOriginChain,
2326
+ }
2327
+
2328
+ logger.console.log("[trails-sdk] origin call params", originCallParams)
2329
+
2330
+ let originUserTxReceipt: TransactionReceipt | null = null
2331
+ const originMetaTxnReceipt: MetaTxnReceipt | null = null
2332
+ const destinationMetaTxnReceipt: MetaTxnReceipt | null = null
2333
+
2334
+ await attemptSwitchChain({
2335
+ walletClient,
2336
+ desiredChainId: effectiveOriginChainId,
2337
+ })
2338
+ if (!dryMode) {
2339
+ try {
2340
+ onTransactionStateChange([
2341
+ {
2342
+ transactionHash: "",
2343
+ explorerUrl: "",
2344
+ chainId: effectiveOriginChainId,
2345
+ state: "pending",
2346
+ label: "Execute",
2347
+ },
2348
+ ])
2349
+ } catch (error) {
2350
+ logger.console.error(
2351
+ "[trails-sdk] Error calling onTransactionStateChange:",
2352
+ error,
2353
+ )
2354
+ }
2355
+
2356
+ if (hasCustomCalldata) {
2357
+ try {
2358
+ const needsApproval = await getNeedsApproval({
2359
+ publicClient: effectivePublicClient,
2360
+ token: effectiveOriginTokenAddress,
2361
+ account: account.address,
2362
+ spender: recipient,
2363
+ amount: maxUint256,
2364
+ })
2365
+
2366
+ if (needsApproval) {
2367
+ const txHash = await approveERC20({
2368
+ walletClient,
2369
+ tokenAddress: effectiveOriginTokenAddress,
2370
+ spender: recipient,
2371
+ amount: maxUint256,
2372
+ chain: effectiveOriginChain,
2373
+ })
2374
+
2375
+ logger.console.log("waiting for approve", txHash)
2376
+ await effectivePublicClient.waitForTransactionReceipt({
2377
+ hash: txHash,
2378
+ })
2379
+ logger.console.log("approve done")
2380
+ }
2381
+ } catch (error) {
2382
+ logger.console.error("[trails-sdk] Error approving ERC20", error)
2383
+ }
2384
+ }
2100
2385
 
2101
- const depositAmountFormatted = Number(
2102
- formatUnits(BigInt(swapAmount), originTokenDecimals),
2103
- )
2104
- const depositAmountUsd = calcAmountUsdPrice({
2105
- amount: depositAmountFormatted,
2106
- usdPrice: sourceTokenPriceUsd,
2107
- })
2386
+ // Show persistent toast for checkout flow
2387
+ updatePersistentToast(
2388
+ "Payment Started",
2389
+ "Waiting for wallet confirmation...",
2390
+ "info",
2391
+ )
2108
2392
 
2109
- const hasCustomCalldata = getIsCustomCalldata(destinationCalldata)
2110
- const originCallParams = {
2111
- to: hasCustomCalldata
2112
- ? recipient
2113
- : effectiveOriginTokenAddress === zeroAddress
2114
- ? recipient
2115
- : effectiveOriginTokenAddress,
2116
- data: hasCustomCalldata
2117
- ? destinationCalldata
2118
- : effectiveOriginTokenAddress === zeroAddress
2119
- ? "0x"
2120
- : getERC20TransferData({
2121
- recipient,
2122
- amount: BigInt(swapAmount),
2123
- }),
2124
- value:
2125
- effectiveOriginTokenAddress === zeroAddress
2126
- ? BigInt(swapAmount)
2127
- : "0",
2128
- chainId: effectiveOriginChainId,
2129
- chain: effectiveOriginChain,
2130
- }
2393
+ logger.console.log(
2394
+ "[trails-sdk] origin call params",
2395
+ originCallParams,
2396
+ )
2397
+ const txHash = await sendOriginTransaction(
2398
+ account,
2399
+ walletClient,
2400
+ originCallParams as any,
2401
+ {
2402
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
2403
+ },
2404
+ ) // TODO: Add proper type
2131
2405
 
2132
- logger.console.log("[trails-sdk] origin call params", originCallParams)
2406
+ logger.console.log("[trails-sdk] origin tx", txHash)
2133
2407
 
2134
- let originUserTxReceipt: TransactionReceipt | null = null
2135
- const originMetaTxnReceipt: MetaTxnReceipt | null = null
2136
- const destinationMetaTxnReceipt: MetaTxnReceipt | null = null
2408
+ if (onOriginSend) {
2409
+ onOriginSend()
2410
+ }
2137
2411
 
2138
- await attemptSwitchChain({
2139
- walletClient,
2140
- desiredChainId: effectiveOriginChainId,
2141
- })
2142
- if (!dryMode) {
2143
- try {
2144
- onTransactionStateChange([
2412
+ // Wait for transaction receipt
2413
+ const receipt = await effectivePublicClient.waitForTransactionReceipt(
2145
2414
  {
2146
- transactionHash: "",
2147
- explorerUrl: "",
2148
- chainId: effectiveOriginChainId,
2149
- state: "pending",
2150
- label: "Execute",
2415
+ hash: txHash,
2151
2416
  },
2152
- ])
2153
- } catch (error) {
2154
- logger.console.error(
2155
- "[trails-sdk] Error calling onTransactionStateChange:",
2156
- error,
2157
2417
  )
2158
- }
2418
+ logger.console.log("[trails-sdk] receipt", receipt)
2419
+ originUserTxReceipt = receipt
2159
2420
 
2160
- if (hasCustomCalldata) {
2161
- try {
2162
- const needsApproval = await getNeedsApproval({
2163
- publicClient: effectivePublicClient,
2164
- token: effectiveOriginTokenAddress,
2165
- account: account.address,
2166
- spender: recipient,
2167
- amount: maxUint256,
2168
- })
2421
+ trackTransactionConfirmed({
2422
+ transactionHash: txHash,
2423
+ chainId: effectiveOriginChainId,
2424
+ userAddress: account.address,
2425
+ blockNumber: Number(receipt.blockNumber),
2426
+ originTokenAddress,
2427
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
2428
+ })
2169
2429
 
2170
- if (needsApproval) {
2171
- const txHash = await approveERC20({
2172
- walletClient,
2173
- tokenAddress: effectiveOriginTokenAddress,
2174
- spender: recipient,
2175
- amount: maxUint256,
2176
- chain: effectiveOriginChain,
2177
- })
2430
+ // Remove persistent toast and show success
2431
+ const chainInfo = getChainInfo(effectiveOriginChainId)
2432
+ updatePersistentToast(
2433
+ "Transfer Confirmed",
2434
+ `Your transaction on ${chainInfo?.name || "chain"} has been confirmed`,
2435
+ "info",
2436
+ )
2178
2437
 
2179
- logger.console.log("waiting for approve", txHash)
2180
- await effectivePublicClient.waitForTransactionReceipt({
2181
- hash: txHash,
2182
- })
2183
- logger.console.log("approve done")
2184
- }
2438
+ try {
2439
+ onTransactionStateChange([
2440
+ getTransactionStateFromReceipt(
2441
+ originUserTxReceipt,
2442
+ effectiveOriginChainId,
2443
+ transactionStates[0]?.label,
2444
+ ),
2445
+ ])
2185
2446
  } catch (error) {
2186
- logger.console.error("[trails-sdk] Error approving ERC20", error)
2447
+ logger.console.error(
2448
+ "[trails-sdk] Error calling onTransactionStateChange:",
2449
+ error,
2450
+ )
2187
2451
  }
2188
- }
2189
2452
 
2190
- // Show persistent toast for checkout flow
2191
- updatePersistentToast(
2192
- "Payment Started",
2193
- "Waiting for wallet confirmation...",
2194
- "info",
2195
- )
2196
-
2197
- logger.console.log("[trails-sdk] origin call params", originCallParams)
2198
- const txHash = await sendOriginTransaction(
2199
- account,
2200
- walletClient,
2201
- originCallParams as any,
2202
- {
2203
- depositTokenAmountUsd: depositAmountUsd?.toString(),
2204
- },
2205
- ) // TODO: Add proper type
2453
+ // Track payment completion for same-chain same-token transaction
2454
+ if (originUserTxReceipt && originUserTxReceipt.status === "success") {
2455
+ trackPaymentCompleted({
2456
+ userAddress: account.address,
2457
+ originTxHash: originUserTxReceipt.transactionHash,
2458
+ originChainId: effectiveOriginChainId, // Same chain
2459
+ mode,
2460
+ fundMethod,
2461
+ originTokenAddress,
2462
+ depositTokenAmountUsd: depositAmountUsd?.toString(),
2463
+ destinationTokenAmountUsd: depositAmountUsd?.toString(), // same as deposit amount
2464
+ })
2206
2465
 
2207
- logger.console.log("[trails-sdk] origin tx", txHash)
2466
+ // Call onCheckoutComplete callback if provided
2467
+ if (checkoutOnHandlers?.triggerCheckoutComplete) {
2468
+ checkoutOnHandlers.triggerCheckoutComplete()
2469
+ }
2470
+ } else if (originUserTxReceipt) {
2471
+ trackPaymentError({
2472
+ error: "Transaction failed",
2473
+ userAddress: account.address,
2474
+ mode,
2475
+ fundMethod,
2476
+ originTokenAddress,
2477
+ })
2208
2478
 
2209
- if (onOriginSend) {
2210
- onOriginSend()
2479
+ // Call onCheckoutError callback if provided
2480
+ if (checkoutOnHandlers?.triggerCheckoutError) {
2481
+ checkoutOnHandlers.triggerCheckoutError("Transaction failed")
2482
+ }
2483
+ }
2211
2484
  }
2212
2485
 
2213
- // Wait for transaction receipt
2214
- const receipt = await effectivePublicClient.waitForTransactionReceipt({
2215
- hash: txHash,
2216
- })
2217
- logger.console.log("[trails-sdk] receipt", receipt)
2218
- originUserTxReceipt = receipt
2486
+ return {
2487
+ originUserTxReceipt,
2488
+ originMetaTxnReceipt,
2489
+ destinationMetaTxnReceipt,
2490
+ totalCompletionSeconds: 0,
2491
+ }
2492
+ } catch (error) {
2493
+ const errorMessage =
2494
+ error instanceof Error
2495
+ ? error.message
2496
+ : "Unknown error occurred during transaction"
2497
+ logger.console.error(
2498
+ "[trails-sdk] Error in sendHandlerForSameChainSameToken:",
2499
+ error,
2500
+ )
2219
2501
 
2220
- trackTransactionConfirmed({
2221
- transactionHash: txHash,
2222
- chainId: effectiveOriginChainId,
2502
+ // Track payment error
2503
+ trackPaymentError({
2504
+ error: errorMessage,
2223
2505
  userAddress: account.address,
2224
- blockNumber: Number(receipt.blockNumber),
2506
+ mode,
2507
+ fundMethod,
2225
2508
  originTokenAddress,
2226
- depositTokenAmountUsd: depositAmountUsd?.toString(),
2227
2509
  })
2228
2510
 
2229
- // Remove persistent toast and show success
2230
- const chainInfo = getChainInfo(effectiveOriginChainId)
2231
- updatePersistentToast(
2232
- "Transfer Confirmed",
2233
- `Your transaction on ${chainInfo?.name || "chain"} has been confirmed`,
2234
- "info",
2235
- )
2236
-
2237
- try {
2238
- onTransactionStateChange([
2239
- getTransactionStateFromReceipt(
2240
- originUserTxReceipt,
2241
- effectiveOriginChainId,
2242
- transactionStates[0]?.label,
2243
- ),
2244
- ])
2245
- } catch (error) {
2246
- logger.console.error(
2247
- "[trails-sdk] Error calling onTransactionStateChange:",
2248
- error,
2249
- )
2250
- }
2251
-
2252
- // Track payment completion for same-chain same-token transaction
2253
- if (originUserTxReceipt && originUserTxReceipt.status === "success") {
2254
- trackPaymentCompleted({
2255
- userAddress: account.address,
2256
- originTxHash: originUserTxReceipt.transactionHash,
2257
- originChainId: effectiveOriginChainId, // Same chain
2258
- mode,
2259
- fundMethod,
2260
- originTokenAddress,
2261
- depositTokenAmountUsd: depositAmountUsd?.toString(),
2262
- destinationTokenAmountUsd: depositAmountUsd?.toString(), // same as deposit amount
2263
- })
2264
- } else if (originUserTxReceipt) {
2265
- trackPaymentError({
2266
- error: "Transaction failed",
2267
- userAddress: account.address,
2268
- mode,
2269
- fundMethod,
2270
- originTokenAddress,
2271
- })
2511
+ // Call onCheckoutError callback if provided
2512
+ if (checkoutOnHandlers?.triggerCheckoutError) {
2513
+ checkoutOnHandlers.triggerCheckoutError(errorMessage)
2272
2514
  }
2273
- }
2274
2515
 
2275
- return {
2276
- originUserTxReceipt,
2277
- originMetaTxnReceipt,
2278
- destinationMetaTxnReceipt,
2279
- totalCompletionSeconds: 0,
2516
+ // Re-throw the error so caller can handle if needed
2517
+ throw error
2280
2518
  }
2281
2519
  },
2282
2520
  }
@@ -2443,7 +2681,6 @@ async function _sendHandlerForSameChainDifferentToken({
2443
2681
  onOriginSend,
2444
2682
  }: {
2445
2683
  onOriginSend?: () => void
2446
- feeTokenAddress?: string | null
2447
2684
  }): Promise<SendReturn> => {
2448
2685
  const { hasEnoughBalance, balanceError } = await checkAccountBalance({
2449
2686
  account,
@@ -2557,6 +2794,8 @@ async function attemptGaslessDeposit({
2557
2794
  account,
2558
2795
  trailsClient,
2559
2796
  originRelayer,
2797
+ feeOptions,
2798
+ selectedFeeToken,
2560
2799
  }: {
2561
2800
  paymasterUrl?: string
2562
2801
  depositTokenAddress: string
@@ -2569,11 +2808,25 @@ async function attemptGaslessDeposit({
2569
2808
  trailsClient: TrailsAPIClient
2570
2809
  originRelayer: Relayer.Standard.Rpc.RpcRelayer
2571
2810
  feeOptions: any
2572
- feeTokenAddress?: string
2811
+ selectedFeeToken?: any
2573
2812
  }): Promise<TransactionReceipt | null> {
2574
2813
  let originUserTxReceipt: TransactionReceipt | null = null
2575
2814
  const originChainId = chain.id
2576
- logger.console.log("[trails-sdk] originChainId", originChainId)
2815
+
2816
+ logger.console.log(
2817
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] attemptGaslessDeposit called with:",
2818
+ {
2819
+ originChainId,
2820
+ depositTokenAddress,
2821
+ depositTokenAmount,
2822
+ depositRecipient,
2823
+ hasFeeOptions: !!feeOptions,
2824
+ feeOptionsLength: feeOptions?.feeOptions?.length,
2825
+ selectedFeeToken,
2826
+ hasSelectedFeeToken: !!selectedFeeToken,
2827
+ paymasterUrl,
2828
+ },
2829
+ )
2577
2830
 
2578
2831
  const publicClient = createPublicClient({
2579
2832
  chain,
@@ -2581,12 +2834,20 @@ async function attemptGaslessDeposit({
2581
2834
  })
2582
2835
 
2583
2836
  const intentEntrypoint = intentEntrypoints[chain.id]
2584
- logger.console.log("[trails-sdk] intentEntrypoint", intentEntrypoint)
2837
+ logger.console.log("[trails-sdk] [GASLESS-FLOW] Intent entrypoint check:", {
2838
+ chainId: chain.id,
2839
+ chainName: chain.name,
2840
+ intentEntrypoint,
2841
+ hasIntentEntrypoint: !!intentEntrypoint,
2842
+ availableChains: Object.keys(intentEntrypoints).map(Number),
2843
+ })
2585
2844
 
2586
2845
  // If intent entrypoint is not available, fall back to old flow
2587
2846
  if (!intentEntrypoint) {
2588
- logger.console.log(
2589
- "[trails-sdk] No intent entrypoint for chain, using fallback flow",
2847
+ logger.console.warn(
2848
+ `[trails-sdk] ⚠️ No intent entrypoint configured for chain ${chain.id} (${chain.name}). ` +
2849
+ `Gasless deposits with fee options are only supported on chains: ${Object.keys(intentEntrypoints).join(", ")}. ` +
2850
+ `Falling back to old flow (permit2/paymaster).`,
2590
2851
  )
2591
2852
 
2592
2853
  let calls: Array<{
@@ -2596,7 +2857,19 @@ async function attemptGaslessDeposit({
2596
2857
  }> = []
2597
2858
 
2598
2859
  if (paymasterUrl) {
2599
- logger.console.log("[trails-sdk] doing gasless with paymaster")
2860
+ logger.console.log(
2861
+ "[trails-sdk] [GASLESS-FLOW] doing gasless with paymaster",
2862
+ )
2863
+
2864
+ // Switch to correct chain before requesting signatures
2865
+ logger.console.log(
2866
+ "[trails-sdk] [GASLESS-FLOW] Switching chain for paymaster flow",
2867
+ )
2868
+ await attemptSwitchChain({
2869
+ walletClient,
2870
+ desiredChainId: originChainId,
2871
+ })
2872
+
2600
2873
  const delegatorSmartAccount = await getDelegatorSmartAccount({
2601
2874
  publicClient,
2602
2875
  })
@@ -2631,7 +2904,9 @@ async function attemptGaslessDeposit({
2631
2904
  logger.console.log("[trails-sdk] receipt", receipt)
2632
2905
  return receipt
2633
2906
  } else {
2634
- logger.console.log("[trails-sdk] doing gasless with sequence wallet")
2907
+ logger.console.log(
2908
+ "[trails-sdk] [GASLESS-FLOW] doing gasless with sequence wallet",
2909
+ )
2635
2910
  const delegatorPrivateKey = generatePrivateKey()
2636
2911
  const delegatorAccount = privateKeyToAccount(delegatorPrivateKey)
2637
2912
  const delegatorClient = createWalletClient({
@@ -2702,56 +2977,148 @@ async function attemptGaslessDeposit({
2702
2977
  "[trails-sdk] Using Intent Entrypoint API flow with permit2 support for gasless deposit",
2703
2978
  )
2704
2979
 
2980
+ // Switch to correct chain before requesting signatures
2981
+ logger.console.log(
2982
+ "[trails-sdk] [GASLESS-FLOW] Switching to chain before permit/intent signatures",
2983
+ { originChainId },
2984
+ )
2985
+ await attemptSwitchChain({
2986
+ walletClient,
2987
+ desiredChainId: originChainId,
2988
+ })
2989
+
2705
2990
  try {
2706
2991
  const deadline = Math.floor(Date.now() / 1000) + 3600 // 1 hour from now
2992
+ const hasFeeOptions = Boolean(
2993
+ feeOptions && feeOptions.feeOptions?.length > 0,
2994
+ )
2995
+
2996
+ // 1. Check if we need approval - check if we have enough allowance for this transaction (deposit + fee)
2997
+ let requiredAmount = BigInt(depositTokenAmount)
2998
+
2999
+ // Match selectedFeeToken by tokenAddress to get the latest fee amount from feeOptions
3000
+ let selectedFeeOption = null
3001
+ if (selectedFeeToken && hasFeeOptions) {
3002
+ // Find matching fee option by tokenAddress to get latest amount
3003
+ selectedFeeOption = feeOptions.feeOptions.find(
3004
+ (opt: any) =>
3005
+ opt.tokenAddress?.toLowerCase() ===
3006
+ selectedFeeToken.tokenAddress?.toLowerCase(),
3007
+ )
3008
+ logger.console.log(
3009
+ "[trails-sdk] Matched selectedFeeToken to latest fee option:",
3010
+ {
3011
+ selectedFeeToken,
3012
+ matchedOption: selectedFeeOption,
3013
+ },
3014
+ )
3015
+ }
3016
+
3017
+ // Fallback to first fee option if no match or no selectedFeeToken
3018
+ if (!selectedFeeOption && hasFeeOptions) {
3019
+ selectedFeeOption = feeOptions.feeOptions[0]
3020
+ logger.console.log(
3021
+ "[trails-sdk] Using first fee option as fallback:",
3022
+ selectedFeeOption,
3023
+ )
3024
+ }
3025
+
3026
+ if (selectedFeeOption?.amount) {
3027
+ requiredAmount = requiredAmount + BigInt(selectedFeeOption.amount)
3028
+ logger.console.log("[trails-sdk] Including fee in required amount:", {
3029
+ depositAmount: depositTokenAmount,
3030
+ feeAmount: selectedFeeOption.amount,
3031
+ feeTokenAddress: selectedFeeOption.tokenAddress,
3032
+ totalRequired: requiredAmount.toString(),
3033
+ })
3034
+ }
2707
3035
 
2708
- // 1. Check if we need approval for permit2
2709
3036
  const needsApproval = await getNeedsIntentEntrypointApproval({
2710
3037
  client: publicClient,
2711
3038
  token: depositTokenAddress as `0x${string}`,
2712
3039
  account: account.address,
2713
3040
  entrypoint: intentEntrypoint as `0x${string}`,
2714
- amount: BigInt(depositTokenAmount),
3041
+ amount: requiredAmount, // Check if we have enough allowance for this specific transaction
2715
3042
  })
2716
3043
 
2717
- logger.console.log("[trails-sdk] Checking permit2 requirements", {
2718
- userAddress: account.address,
2719
- tokenAddress: depositTokenAddress,
2720
- amount: depositTokenAmount,
2721
- intentAddress: depositRecipient,
2722
- chainID: originChainId,
2723
- deadline,
2724
- needsApproval,
2725
- })
3044
+ logger.console.log(
3045
+ "[trails-sdk] [GASLESS-FLOW] Checking permit requirements",
3046
+ {
3047
+ userAddress: account.address,
3048
+ tokenAddress: depositTokenAddress,
3049
+ depositAmount: depositTokenAmount,
3050
+ requiredAmount: requiredAmount.toString(),
3051
+ intentAddress: depositRecipient,
3052
+ chainID: originChainId,
3053
+ deadline,
3054
+ needsApproval,
3055
+ },
3056
+ )
2726
3057
 
2727
3058
  // 2. Get permit signature if approval needed
2728
3059
  let permitSignature: string | undefined
2729
3060
  let permitDeadline: number | undefined
2730
3061
 
2731
3062
  if (needsApproval) {
2732
- logger.console.log("[trails-sdk] Getting permit signature for approval")
2733
- // NOTE: Currently signing for exact deposit amount because API encodes permitAmount = amount
2734
- // TODO: Update API to accept permitAmount parameter, then change to maxUint256 for unlimited approval
3063
+ logger.console.log(
3064
+ "[trails-sdk] Getting permit signature for infinite approval",
3065
+ )
3066
+
3067
+ // Use infinite approval (maxUint256) so user doesn't need to approve again
3068
+ const permitAmount = maxUint256
3069
+ logger.console.log(
3070
+ "[trails-sdk] Using infinite approval for gasless deposits",
3071
+ {
3072
+ depositAmount: depositTokenAmount,
3073
+ permitAmount: permitAmount.toString(),
3074
+ },
3075
+ )
3076
+
2735
3077
  const permitSig = await getPermitSignature({
2736
3078
  publicClient,
2737
3079
  walletClient,
2738
3080
  signer: account.address,
2739
3081
  spender: intentEntrypoint as `0x${string}`,
2740
3082
  tokenAddress: depositTokenAddress as `0x${string}`,
2741
- amount: BigInt(depositTokenAmount), // Sign for exact amount to match API encoding
3083
+ amount: permitAmount, // Infinite approval
2742
3084
  chain,
2743
3085
  deadline: BigInt(deadline),
2744
3086
  })
2745
3087
  permitSignature = permitSig.signature
2746
3088
  permitDeadline = Number(permitSig.deadline)
2747
3089
  logger.console.log(
2748
- "[trails-sdk] Permit signature obtained for amount:",
2749
- depositTokenAmount,
3090
+ "[trails-sdk] Permit signature obtained for infinite approval",
2750
3091
  )
2751
3092
  }
2752
3093
 
2753
- // 3. Get intent signature
3094
+ // 3. Get current nonce for the user
3095
+ logger.console.log("[trails-sdk] Getting user nonce")
3096
+ const nonce = await getUserNonce({
3097
+ publicClient,
3098
+ userAddress: account.address,
3099
+ intentEntrypoint: intentEntrypoint as `0x${string}`,
3100
+ })
3101
+ logger.console.log("[trails-sdk] User nonce:", nonce.toString())
3102
+
3103
+ // 4. Get intent signature
2754
3104
  logger.console.log("[trails-sdk] Requesting intent signature via EIP-712")
3105
+ // Get fee collector address from fee options response, or use selected fee option's collector
3106
+ const feeCollectorAddress = (selectedFeeOption?.feeCollector ||
3107
+ feeOptions?.feeCollector) as `0x${string}` | undefined
3108
+
3109
+ // Validate that we have a valid fee collector address
3110
+ if (!feeCollectorAddress || feeCollectorAddress === zeroAddress) {
3111
+ throw new Error(
3112
+ "[trails-sdk] Fee collector address not provided by API. Cannot proceed with gasless deposit. " +
3113
+ "Please ensure the API is returning feeCollector in the fee options response.",
3114
+ )
3115
+ }
3116
+
3117
+ logger.console.log(
3118
+ "[trails-sdk] Using fee collector address:",
3119
+ feeCollectorAddress,
3120
+ )
3121
+
2755
3122
  const { signature: intentSignature } = await signIntent({
2756
3123
  client: walletClient,
2757
3124
  intentParams: {
@@ -2762,15 +3129,39 @@ async function attemptGaslessDeposit({
2762
3129
  deadline: BigInt(deadline),
2763
3130
  chainId: originChainId,
2764
3131
  contractAddress: intentEntrypoint as `0x${string}`,
3132
+ nonce,
3133
+ feeAmount: BigInt(selectedFeeOption?.amount || "0"),
3134
+ feeCollector: feeCollectorAddress,
2765
3135
  },
2766
3136
  })
2767
3137
  logger.console.log("[trails-sdk] Intent signature received")
2768
3138
 
2769
- // 4. Call the deposit endpoint with permit2 support
3139
+ // 5. Call the deposit endpoint with permit support and optional fee
3140
+ logger.console.log(
3141
+ "[trails-sdk] Calling getIntentEntrypointDeposit with permit enabled",
3142
+ { usePermit: needsApproval, hasFee: !!feeOptions },
3143
+ )
3144
+
3145
+ // selectedFeeOption was already determined at the start of the try block
3146
+
2770
3147
  logger.console.log(
2771
- "[trails-sdk] Calling getIntentEntrypointDeposit with permit2 enabled",
2772
- { usePermit: needsApproval },
3148
+ "[trails-sdk] Calling getIntentEntrypointDeposit with params:",
3149
+ {
3150
+ userAddress: account.address,
3151
+ tokenAddress: depositTokenAddress,
3152
+ amount: depositTokenAmount,
3153
+ intentAddress: depositRecipient,
3154
+ chainID: originChainId,
3155
+ deadline,
3156
+ usePermit: needsApproval,
3157
+ hasPermitSignature: !!permitSignature,
3158
+ feeAmount: selectedFeeOption?.amount,
3159
+ feeTokenSymbol: selectedFeeOption?.tokenSymbol,
3160
+ selectedFeeToken,
3161
+ usingSelectedFeeToken: !!selectedFeeToken,
3162
+ },
2773
3163
  )
3164
+
2774
3165
  const depositDataResponse = await trailsClient.getIntentEntrypointDeposit({
2775
3166
  params: {
2776
3167
  userAddress: account.address,
@@ -2780,9 +3171,11 @@ async function attemptGaslessDeposit({
2780
3171
  chainID: originChainId,
2781
3172
  deadline,
2782
3173
  intentSignature,
2783
- usePermit: needsApproval, // Use permit2 if approval needed
3174
+ usePermit: needsApproval, // Use permit if approval needed
3175
+ permitAmount: needsApproval ? maxUint256.toString() : undefined, // Pass infinite approval amount
2784
3176
  permitSignature,
2785
3177
  permitDeadline,
3178
+ feeAmount: selectedFeeOption?.amount || undefined,
2786
3179
  },
2787
3180
  })
2788
3181
 
@@ -3192,8 +3585,8 @@ async function attemptUserDepositTx({
3192
3585
  destinationTokenSymbol,
3193
3586
  depositAmountUsd,
3194
3587
  feeOptions,
3195
- feeTokenAddress,
3196
3588
  trailsClient,
3589
+ selectedFeeToken,
3197
3590
  }: {
3198
3591
  originTokenAddress: string
3199
3592
  gasless: boolean
@@ -3220,8 +3613,8 @@ async function attemptUserDepositTx({
3220
3613
  destinationTokenSymbol: string
3221
3614
  depositAmountUsd: number
3222
3615
  feeOptions?: any
3223
- feeTokenAddress?: string | null
3224
3616
  trailsClient: TrailsAPIClient
3617
+ selectedFeeToken?: any
3225
3618
  }): Promise<TransactionReceipt | null> {
3226
3619
  let originUserTxReceipt: TransactionReceipt | null = null
3227
3620
  const originChainId = chain.id
@@ -3240,11 +3633,36 @@ async function attemptUserDepositTx({
3240
3633
  return null
3241
3634
  }
3242
3635
 
3243
- const doGasless =
3244
- getDoGasless(originTokenAddress, gasless, paymasterUrl) ||
3245
- (feeOptions && feeTokenAddress)
3246
- logger.console.log("[trails-sdk] doGasless", doGasless, paymasterUrl)
3247
- if (doGasless) {
3636
+ const doGasless = getDoGasless(
3637
+ originTokenAddress,
3638
+ gasless,
3639
+ feeOptions,
3640
+ selectedFeeToken,
3641
+ )
3642
+ logger.console.log(
3643
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] doGasless check results:",
3644
+ {
3645
+ doGasless,
3646
+ paymasterUrl,
3647
+ hasFeeOptions: !!feeOptions,
3648
+ feeOptionsCount: feeOptions?.feeOptions?.length || 0,
3649
+ selectedFeeToken,
3650
+ selectedFeeTokenSet:
3651
+ selectedFeeToken !== null && selectedFeeToken !== undefined,
3652
+ originTokenAddress,
3653
+ gasless,
3654
+ isNotNative: originTokenAddress !== zeroAddress,
3655
+ },
3656
+ )
3657
+ if (doGasless || paymasterUrl) {
3658
+ logger.console.log(
3659
+ "[trails-sdk] [GASLESS-FLOW] Entering gasless deposit flow",
3660
+ {
3661
+ doGasless,
3662
+ hasPaymasterUrl: !!paymasterUrl,
3663
+ paymasterUrl,
3664
+ },
3665
+ )
3248
3666
  try {
3249
3667
  originUserTxReceipt = await attemptGaslessDeposit({
3250
3668
  paymasterUrl,
@@ -3257,11 +3675,12 @@ async function attemptUserDepositTx({
3257
3675
  account,
3258
3676
  trailsClient,
3259
3677
  originRelayer,
3260
- feeOptions,
3261
- feeTokenAddress: feeTokenAddress || "",
3678
+ feeOptions: feeOptions,
3679
+ selectedFeeToken: selectedFeeToken,
3262
3680
  })
3263
3681
  } catch (error) {
3264
3682
  logger.console.log("[trails-sdk] gassless attempt failed", error)
3683
+ throw error
3265
3684
  }
3266
3685
  }
3267
3686
 
@@ -3298,11 +3717,108 @@ async function attemptUserDepositTx({
3298
3717
  export function getDoGasless(
3299
3718
  originTokenAddress: string,
3300
3719
  gasless: boolean,
3301
- paymasterUrl?: string,
3720
+ feeOptions?: any,
3721
+ selectedFeeToken?: any,
3302
3722
  ): boolean {
3303
- return Boolean(
3304
- originTokenAddress !== zeroAddress && (gasless || paymasterUrl),
3723
+ const hasFeeOptions = Boolean(feeOptions && feeOptions.feeOptions?.length > 0)
3724
+
3725
+ // Important: The UI passes selectedFeeToken in these states:
3726
+ // - null: User explicitly chose "Pay with native gas"
3727
+ // - {object}: User selected a fee token OR it was auto-selected
3728
+ // - undefined: Should not happen (initial state auto-selects if options exist)
3729
+
3730
+ // If selectedFeeToken is null, user explicitly chose native gas
3731
+ if (selectedFeeToken === null) {
3732
+ logger.console.log(
3733
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: User explicitly selected native gas (null)",
3734
+ )
3735
+ return false
3736
+ }
3737
+
3738
+ // If selectedFeeToken is undefined and no fee options, can't do gasless
3739
+ if (!selectedFeeToken && !hasFeeOptions) {
3740
+ logger.console.log(
3741
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: No fee token selected and no fee options available",
3742
+ )
3743
+ return false
3744
+ }
3745
+
3746
+ // If selectedFeeToken is undefined but fee options exist, use first ERC20 option
3747
+ let effectiveFeeToken = selectedFeeToken
3748
+ if (!effectiveFeeToken && hasFeeOptions) {
3749
+ const firstFeeOption = feeOptions.feeOptions[0]
3750
+
3751
+ // Check if first option is native gas
3752
+ const isFirstOptionNative =
3753
+ firstFeeOption?.tokenAddress === zeroAddress ||
3754
+ firstFeeOption?.tokenAddress?.toLowerCase() === zeroAddress ||
3755
+ firstFeeOption?.isNative === true
3756
+
3757
+ if (!isFirstOptionNative && firstFeeOption?.tokenAddress) {
3758
+ // First fee option is ERC20, use it
3759
+ effectiveFeeToken = firstFeeOption
3760
+ logger.console.log(
3761
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: No fee token selected, using first ERC20 fee option",
3762
+ {
3763
+ feeOption: effectiveFeeToken,
3764
+ },
3765
+ )
3766
+ } else {
3767
+ logger.console.log(
3768
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: First fee option is native gas, skipping gasless",
3769
+ )
3770
+ return false
3771
+ }
3772
+ }
3773
+
3774
+ // Check if the effective fee token is native gas
3775
+ const isNativeGasFee =
3776
+ !effectiveFeeToken ||
3777
+ effectiveFeeToken.tokenAddress === zeroAddress ||
3778
+ effectiveFeeToken.tokenAddress?.toLowerCase() === zeroAddress ||
3779
+ effectiveFeeToken.isNative === true
3780
+
3781
+ // Don't use gasless if origin token is native (sending ETH)
3782
+ if (originTokenAddress === zeroAddress) {
3783
+ logger.console.log(
3784
+ "[trails-sdk] [GASLESS-FLOW] getDoGasless: Origin token is native, skipping gasless",
3785
+ )
3786
+ return false
3787
+ }
3788
+
3789
+ // Don't use gasless if fee token is native
3790
+ if (isNativeGasFee) {
3791
+ logger.console.log(
3792
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless: Fee token is native gas, skipping gasless",
3793
+ {
3794
+ effectiveFeeToken,
3795
+ },
3796
+ )
3797
+ return false
3798
+ }
3799
+
3800
+ // Don't use gasless if disabled
3801
+ if (!gasless) {
3802
+ logger.console.log(
3803
+ "[trails-sdk] [GASLESS-FLOW] getDoGasless: Gasless disabled",
3804
+ )
3805
+ return false
3806
+ }
3807
+
3808
+ // All conditions met, use gasless with ERC20 fee token
3809
+ logger.console.log(
3810
+ "[trails-sdk] [GASLESS-FLOW] [FEE-SELECT] getDoGasless decision: Using gasless",
3811
+ {
3812
+ originTokenAddress,
3813
+ gasless,
3814
+ hasFeeOptions,
3815
+ selectedFeeToken,
3816
+ effectiveFeeToken,
3817
+ feeOptionsCount: feeOptions?.feeOptions?.length || 0,
3818
+ },
3305
3819
  )
3820
+
3821
+ return true
3306
3822
  }
3307
3823
 
3308
3824
  function getTransactionStateFromReceipt(
@@ -3659,11 +4175,20 @@ export function useQuote({
3659
4175
  quoteProvider,
3660
4176
  gasless,
3661
4177
  paymasterUrl,
3662
- }: Partial<UseQuoteProps> = {}): UseQuoteReturn {
4178
+ relayerEnv,
4179
+ nodeGatewayEnv,
4180
+ }: Partial<
4181
+ UseQuoteProps & { relayerEnv?: RelayerEnv; nodeGatewayEnv?: SequenceEnv }
4182
+ > = {}): UseQuoteReturn {
4183
+ // Set node gateway environment override for this quote session
4184
+ if (nodeGatewayEnv) {
4185
+ ;(globalThis as any).__testNodeGatewayEnv = nodeGatewayEnv
4186
+ }
4187
+
3663
4188
  const apiClient = useAPIClient()
3664
4189
  const trailsClient = useTrailsClient()
3665
4190
  const { getRelayer } = useRelayers({
3666
- env: getSequenceEnv() as RelayerEnv,
4191
+ env: relayerEnv || (getSequenceEnv() as RelayerEnv),
3667
4192
  })
3668
4193
  const indexerGatewayClient = useIndexerGatewayClient()
3669
4194
 
@@ -3738,10 +4263,26 @@ export function useQuote({
3738
4263
 
3739
4264
  const sourceTokenDecimals = originToken?.decimals
3740
4265
  if (!sourceTokenDecimals) {
4266
+ logger.console.error(
4267
+ "[trails-sdk] [useQuote] Missing source token decimals:",
4268
+ {
4269
+ originToken,
4270
+ fromTokenAddress,
4271
+ fromChainId,
4272
+ },
4273
+ )
3741
4274
  throw new Error("Source token decimals not found")
3742
4275
  }
3743
4276
  const destinationTokenDecimals = destinationToken?.decimals
3744
4277
  if (!destinationTokenDecimals) {
4278
+ logger.console.error(
4279
+ "[trails-sdk] Missing destination token decimals:",
4280
+ {
4281
+ destinationToken,
4282
+ toTokenAddress,
4283
+ toChainId,
4284
+ },
4285
+ )
3745
4286
  throw new Error("Destination token decimals not found")
3746
4287
  }
3747
4288
  const destinationTokenSymbol = destinationToken?.symbol ?? ""
@@ -3991,7 +4532,6 @@ export async function getNormalizedQuoteObject({
3991
4532
  originNativeTokenPriceUsd,
3992
4533
  quoteProvider,
3993
4534
  noSufficientBalance,
3994
- minimumNotMet,
3995
4535
  }: {
3996
4536
  originDepositAddress?: string
3997
4537
  destinationDepositAddress?: string
@@ -4015,7 +4555,6 @@ export async function getNormalizedQuoteObject({
4015
4555
  originNativeTokenPriceUsd?: number | null
4016
4556
  quoteProvider?: string
4017
4557
  noSufficientBalance?: boolean
4018
- minimumNotMet?: boolean
4019
4558
  }): Promise<PrepareSendQuote> {
4020
4559
  if (!destinationChainId) {
4021
4560
  throw new Error("Destination chain id is required")
@@ -4213,7 +4752,6 @@ export async function getNormalizedQuoteObject({
4213
4752
  ),
4214
4753
  quoteProvider: quoteProviderInfo,
4215
4754
  noSufficientBalance: noSufficientBalance || false,
4216
- minimumNotMet: minimumNotMet || false,
4217
4755
  }
4218
4756
  }
4219
4757