0xtrails 0.12.0 → 0.12.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 (205) hide show
  1. package/dist/analytics.d.ts +65 -50
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/{ccip-DtfgR432.js → ccip-62W6LwH2.js} +28 -28
  4. package/dist/chains.d.ts.map +1 -1
  5. package/dist/error.d.ts +2 -0
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/estimate.d.ts.map +1 -1
  8. package/dist/fees.d.ts.map +1 -1
  9. package/dist/{index-CHiCSmCD.js → index-C0QTNYIA.js} +43750 -41806
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +199 -171
  13. package/dist/localeUtils.d.ts.map +1 -1
  14. package/dist/meld/components/MeldCountriesList.d.ts +0 -2
  15. package/dist/meld/components/MeldCountriesList.d.ts.map +1 -1
  16. package/dist/meld/components/MeldFundMethods.d.ts.map +1 -1
  17. package/dist/meld/components/MeldTokensList.d.ts.map +1 -1
  18. package/dist/meld/utils/meld.d.ts +2 -52
  19. package/dist/meld/utils/meld.d.ts.map +1 -1
  20. package/dist/poolUtils.d.ts.map +1 -1
  21. package/dist/prepareSend.d.ts.map +1 -1
  22. package/dist/prices.d.ts +1 -2
  23. package/dist/prices.d.ts.map +1 -1
  24. package/dist/query/balance.fetchers.d.ts +2 -2
  25. package/dist/query/balance.fetchers.d.ts.map +1 -1
  26. package/dist/query/fiat.fetchers.d.ts +11 -0
  27. package/dist/query/fiat.fetchers.d.ts.map +1 -0
  28. package/dist/query/fiat.hooks.d.ts +18 -0
  29. package/dist/query/fiat.hooks.d.ts.map +1 -0
  30. package/dist/query/fiat.queries.d.ts +24 -0
  31. package/dist/query/fiat.queries.d.ts.map +1 -0
  32. package/dist/query/meld.fetchers.d.ts +19 -0
  33. package/dist/query/meld.fetchers.d.ts.map +1 -0
  34. package/dist/query/meld.hooks.d.ts +4 -0
  35. package/dist/query/meld.hooks.d.ts.map +1 -0
  36. package/dist/query/meld.queries.d.ts +61 -0
  37. package/dist/query/meld.queries.d.ts.map +1 -0
  38. package/dist/recover.d.ts.map +1 -1
  39. package/dist/tokens.d.ts.map +1 -1
  40. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts.map +1 -1
  41. package/dist/transactionIntent/deposits/gaslessDeposit.d.ts.map +1 -1
  42. package/dist/transactionIntent/deposits/standardDeposit.d.ts +7 -1
  43. package/dist/transactionIntent/deposits/standardDeposit.d.ts.map +1 -1
  44. package/dist/transactionIntent/handlers/intentHandler.d.ts +2 -0
  45. package/dist/transactionIntent/handlers/intentHandler.d.ts.map +1 -1
  46. package/dist/transactionIntent/quote/normalizeQuote.d.ts +2 -2
  47. package/dist/transactionIntent/quote/normalizeQuote.d.ts.map +1 -1
  48. package/dist/transactionIntent/quote/quoteHelpers.d.ts +1 -1
  49. package/dist/transactionIntent/quote/quoteHelpers.d.ts.map +1 -1
  50. package/dist/transactionIntent/types.d.ts +2 -0
  51. package/dist/transactionIntent/types.d.ts.map +1 -1
  52. package/dist/transactionIntent/utils/balanceChecker.d.ts +3 -1
  53. package/dist/transactionIntent/utils/balanceChecker.d.ts.map +1 -1
  54. package/dist/transactions.d.ts +2 -9
  55. package/dist/transactions.d.ts.map +1 -1
  56. package/dist/umd/trails.min.js +206 -152
  57. package/dist/utils/fiat.d.ts +8 -0
  58. package/dist/utils/fiat.d.ts.map +1 -0
  59. package/dist/utils/format.d.ts.map +1 -1
  60. package/dist/utils/passthrough.d.ts +5 -2
  61. package/dist/utils/passthrough.d.ts.map +1 -1
  62. package/dist/utils/validation.d.ts +33 -0
  63. package/dist/utils/validation.d.ts.map +1 -1
  64. package/dist/utils.d.ts.map +1 -1
  65. package/dist/walletUtils.d.ts +1 -1
  66. package/dist/walletUtils.d.ts.map +1 -1
  67. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  68. package/dist/widget/components/DepositTracker.d.ts.map +1 -1
  69. package/dist/widget/components/Earn.d.ts +2 -0
  70. package/dist/widget/components/Earn.d.ts.map +1 -1
  71. package/dist/widget/components/FeeOption.d.ts.map +1 -1
  72. package/dist/widget/components/Fund.d.ts.map +1 -1
  73. package/dist/widget/components/FundMethods.d.ts.map +1 -1
  74. package/dist/widget/components/HookModalContent.d.ts.map +1 -1
  75. package/dist/widget/components/MeldForm.d.ts.map +1 -1
  76. package/dist/widget/components/MeldHistory.d.ts.map +1 -1
  77. package/dist/widget/components/MeldStepsFlow.d.ts.map +1 -1
  78. package/dist/widget/components/OFTProgressBar.d.ts +2 -0
  79. package/dist/widget/components/OFTProgressBar.d.ts.map +1 -1
  80. package/dist/widget/components/OnRampProviderSelector.d.ts.map +1 -1
  81. package/dist/widget/components/OnrampHistoryRow.d.ts.map +1 -1
  82. package/dist/widget/components/Pay.d.ts.map +1 -1
  83. package/dist/widget/components/PercentageMaxButtons.d.ts.map +1 -1
  84. package/dist/widget/components/PoolDeposit.d.ts +2 -0
  85. package/dist/widget/components/PoolDeposit.d.ts.map +1 -1
  86. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  87. package/dist/widget/components/Receipt.d.ts.map +1 -1
  88. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  89. package/dist/widget/components/Swap.d.ts +2 -0
  90. package/dist/widget/components/Swap.d.ts.map +1 -1
  91. package/dist/widget/components/TransferPendingVertical.d.ts.map +1 -1
  92. package/dist/widget/components/Withdraw.d.ts.map +1 -1
  93. package/dist/widget/hooks/useAmountUsd.d.ts.map +1 -1
  94. package/dist/widget/hooks/useCustomTokenSearch.d.ts.map +1 -1
  95. package/dist/widget/hooks/useDisplayCurrencyPreference.d.ts.map +1 -1
  96. package/dist/widget/hooks/useFiatOnRampCurrencies.d.ts +3 -21
  97. package/dist/widget/hooks/useFiatOnRampCurrencies.d.ts.map +1 -1
  98. package/dist/widget/hooks/useMeldTransactionHistory.d.ts.map +1 -1
  99. package/dist/widget/hooks/useOnRampCountryDefaults.d.ts +0 -18
  100. package/dist/widget/hooks/useOnRampCountryDefaults.d.ts.map +1 -1
  101. package/dist/widget/hooks/useOnRampPaymentMethods.d.ts +2 -18
  102. package/dist/widget/hooks/useOnRampPaymentMethods.d.ts.map +1 -1
  103. package/dist/widget/hooks/useOnRampQuote.d.ts.map +1 -1
  104. package/dist/widget/hooks/useQuote.d.ts +5 -1
  105. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  106. package/dist/widget/hooks/useSendForm.d.ts +3 -1
  107. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  108. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  109. package/dist/widget/hooks/useTokenWithFreshBalance.d.ts +3 -2
  110. package/dist/widget/hooks/useTokenWithFreshBalance.d.ts.map +1 -1
  111. package/dist/widget/index.js +1 -1
  112. package/dist/widget/types/commonProps.d.ts +2 -0
  113. package/dist/widget/types/commonProps.d.ts.map +1 -1
  114. package/dist/widget/utils/transactionFailure.d.ts +20 -0
  115. package/dist/widget/utils/transactionFailure.d.ts.map +1 -0
  116. package/dist/widget/widget.d.ts +44 -3
  117. package/dist/widget/widget.d.ts.map +1 -1
  118. package/dist/widget/workers/intentExecutionWorker.d.ts.map +1 -1
  119. package/package.json +22 -22
  120. package/src/analytics.ts +115 -79
  121. package/src/chains.ts +0 -1
  122. package/src/error.ts +11 -0
  123. package/src/estimate.ts +12 -7
  124. package/src/fees.ts +0 -1
  125. package/src/index.ts +11 -0
  126. package/src/localeUtils.ts +3 -1
  127. package/src/meld/components/MeldCountriesList.tsx +30 -15
  128. package/src/meld/components/MeldFundMethods.tsx +8 -4
  129. package/src/meld/components/MeldTokensList.tsx +90 -2
  130. package/src/meld/utils/meld.ts +3 -400
  131. package/src/poolUtils.ts +5 -19
  132. package/src/prepareSend.ts +32 -5
  133. package/src/prices.ts +7 -33
  134. package/src/query/balance.fetchers.ts +128 -168
  135. package/src/query/fiat.fetchers.ts +33 -0
  136. package/src/query/fiat.hooks.ts +71 -0
  137. package/src/query/fiat.queries.ts +67 -0
  138. package/src/query/meld.fetchers.ts +97 -0
  139. package/src/query/meld.hooks.ts +18 -0
  140. package/src/query/meld.queries.ts +184 -0
  141. package/src/recover.ts +6 -1
  142. package/src/tokens.ts +31 -6
  143. package/src/transactionIntent/deposits/depositOrchestrator.ts +2 -0
  144. package/src/transactionIntent/deposits/gaslessDeposit.ts +9 -2
  145. package/src/transactionIntent/deposits/standardDeposit.ts +35 -14
  146. package/src/transactionIntent/handlers/intentHandler.ts +134 -138
  147. package/src/transactionIntent/quote/normalizeQuote.ts +31 -22
  148. package/src/transactionIntent/quote/quoteHelpers.ts +24 -7
  149. package/src/transactionIntent/types.ts +2 -0
  150. package/src/transactionIntent/utils/balanceChecker.ts +10 -4
  151. package/src/transactions.ts +22 -13
  152. package/src/umd.tsx +1 -1
  153. package/src/utils/fiat.ts +32 -0
  154. package/src/utils/format.ts +1 -3
  155. package/src/utils/passthrough.ts +19 -3
  156. package/src/utils/validation.ts +88 -0
  157. package/src/utils.ts +2 -1
  158. package/src/walletUtils.ts +2 -2
  159. package/src/widget/components/AccountIntentTransactionHistory.tsx +2 -2
  160. package/src/widget/components/ClassicSwap.tsx +10 -4
  161. package/src/widget/components/DepositTracker.tsx +2 -5
  162. package/src/widget/components/Earn.tsx +6 -0
  163. package/src/widget/components/FeeOption.tsx +15 -8
  164. package/src/widget/components/Fund.tsx +16 -11
  165. package/src/widget/components/FundMethods.tsx +255 -192
  166. package/src/widget/components/HookModalContent.tsx +4 -0
  167. package/src/widget/components/MeldForm.tsx +44 -42
  168. package/src/widget/components/MeldHistory.tsx +4 -3
  169. package/src/widget/components/MeldStepsFlow.tsx +33 -71
  170. package/src/widget/components/OFTProgressBar.tsx +32 -12
  171. package/src/widget/components/OnRampProviderSelector.tsx +2 -1
  172. package/src/widget/components/OnrampHistoryRow.tsx +2 -1
  173. package/src/widget/components/Pay.tsx +8 -2
  174. package/src/widget/components/PercentageMaxButtons.tsx +5 -3
  175. package/src/widget/components/PoolDeposit.tsx +6 -0
  176. package/src/widget/components/PoolWithdraw.tsx +1 -1
  177. package/src/widget/components/QuoteDetails.tsx +5 -4
  178. package/src/widget/components/Receipt.tsx +4 -3
  179. package/src/widget/components/SlippageToleranceSettings.tsx +3 -2
  180. package/src/widget/components/Swap.tsx +2 -0
  181. package/src/widget/components/TransferPendingVertical.tsx +21 -28
  182. package/src/widget/components/UserPreferences.tsx +1 -1
  183. package/src/widget/components/Withdraw.tsx +20 -14
  184. package/src/widget/hooks/useAmountUsd.ts +3 -15
  185. package/src/widget/hooks/useCustomTokenSearch.tsx +2 -6
  186. package/src/widget/hooks/useDisplayCurrencyPreference.tsx +1 -2
  187. package/src/widget/hooks/useFiatOnRampCurrencies.ts +11 -76
  188. package/src/widget/hooks/useMeldTransactionHistory.ts +24 -89
  189. package/src/widget/hooks/useOnRampCountryDefaults.ts +3 -49
  190. package/src/widget/hooks/useOnRampPaymentMethods.ts +21 -100
  191. package/src/widget/hooks/useOnRampQuote.ts +2 -5
  192. package/src/widget/hooks/useQuote.ts +10 -12
  193. package/src/widget/hooks/useSendForm.ts +6 -0
  194. package/src/widget/hooks/useTokenList.ts +3 -6
  195. package/src/widget/hooks/useTokenWithFreshBalance.ts +141 -11
  196. package/src/widget/types/commonProps.ts +2 -0
  197. package/src/widget/utils/transactionFailure.ts +52 -0
  198. package/src/widget/widget.tsx +137 -59
  199. package/src/widget/workers/intentExecutionWorker.ts +3 -1
  200. package/dist/widget/hooks/useExchangeRate.d.ts +0 -31
  201. package/dist/widget/hooks/useExchangeRate.d.ts.map +0 -1
  202. package/dist/widget/hooks/useFiatCurrencyList.d.ts +0 -3
  203. package/dist/widget/hooks/useFiatCurrencyList.d.ts.map +0 -1
  204. package/src/widget/hooks/useExchangeRate.ts +0 -257
  205. package/src/widget/hooks/useFiatCurrencyList.ts +0 -66
@@ -45,10 +45,6 @@ import { formatRawAmount, rawAmountToNumber } from "../../utils/format.js"
45
45
  import { invalidateTokenBalancesCache } from "../../query/balance.queries.js"
46
46
  import { calcAmountUsdPrice } from "../../prices.js"
47
47
  import { checkAccountBalance } from "../utils/balanceChecker.js"
48
- import {
49
- fetchCachedGasLimit,
50
- createGasEstimationParams,
51
- } from "../../useGasEstimation.js"
52
48
  import { queryClient as defaultQueryClient } from "../../query/client.js"
53
49
  import {
54
50
  getFeesFromIntent,
@@ -63,7 +59,7 @@ import {
63
59
  trackPaymentError,
64
60
  trackTransactionConfirmed,
65
61
  } from "../../analytics.js"
66
- import { getFullErrorMessage } from "../../error.js"
62
+ import { getFullErrorMessage, getIsAlreadyExecutingError } from "../../error.js"
67
63
  import { getIsCustomCalldata } from "../../contractUtils.js"
68
64
  import { isNativeToken } from "../../utils/address.js"
69
65
  import { ensureErc20Approval } from "../utils/erc20Approval.js"
@@ -109,6 +105,8 @@ export interface IntentHandlerParams {
109
105
  tradeType?: TradeType
110
106
  swapProvider?: RouteProvider | null
111
107
  bridgeProvider?: RouteProvider | null
108
+ swapProviderFallback?: boolean
109
+ bridgeProviderFallback?: boolean
112
110
  fundMethod?: FundMethod
113
111
  mode?: "pay" | "fund" | "earn" | "swap" | "withdraw"
114
112
  checkoutOnHandlers?: Partial<CheckoutOnHandlers>
@@ -132,10 +130,33 @@ export interface IntentHandlerParams {
132
130
  refundAddress?: string
133
131
  }
134
132
 
133
+ function isFeeSelectionPassthroughEligible(params: {
134
+ selectedFeeOption: FeeOption | null | undefined
135
+ gasFeeOptions: GasFeeOptions | undefined
136
+ }): boolean {
137
+ const { selectedFeeOption, gasFeeOptions } = params
138
+
139
+ // Undefined means no explicit user selection yet: stay conservative and use intent flow.
140
+ if (selectedFeeOption === undefined) {
141
+ return false
142
+ }
143
+
144
+ // Null means "native fee selected". This is only passthrough-safe if the native fee option
145
+ // advertised by backend is explicitly marked passthrough-eligible.
146
+ if (selectedFeeOption === null) {
147
+ const nativeFeeOption = gasFeeOptions?.feeOptions?.find((option) =>
148
+ isNativeToken(option.tokenAddress),
149
+ )
150
+ return nativeFeeOption?.isPassthroughEligible === true
151
+ }
152
+
153
+ return selectedFeeOption.isPassthroughEligible === true
154
+ }
155
+
135
156
  /**
136
157
  * Execute a passthrough transaction (direct transfer, no intents).
137
158
  *
138
- * Used for same-chain same-token transfers with native gas payment.
159
+ * Used when backend marks quote passthrough-eligible and client-side fee gating allows it.
139
160
  * Handles ERC20 approval if needed for custom calldata scenarios (S4c).
140
161
  * See SPEC.md for flow decision details.
141
162
  */
@@ -556,18 +577,7 @@ async function executeIntent(
556
577
  })
557
578
  } catch (executeError: unknown) {
558
579
  // If intent is already executing, that's fine - it means the system picked it up
559
- const errorMessage =
560
- executeError instanceof Error ? executeError.message : ""
561
- const errorCause =
562
- typeof executeError === "object" &&
563
- executeError !== null &&
564
- "cause" in executeError
565
- ? String((executeError as { cause?: unknown }).cause)
566
- : ""
567
- if (
568
- errorMessage.includes("EXECUTING") ||
569
- errorCause.includes("EXECUTING")
570
- ) {
580
+ if (getIsAlreadyExecutingError(executeError)) {
571
581
  logger.console.log(
572
582
  "[trails-sdk] Intent already executing, continuing...",
573
583
  )
@@ -655,24 +665,28 @@ async function executeIntent(
655
665
  intentId: intent.intentId,
656
666
  depositTransactionHash: depositUserTxnReceipt.transactionHash,
657
667
  })
658
- } catch (error) {
659
- logger.console.error(
660
- "[trails-sdk] Error calling executeIntent:",
661
- error,
662
- )
668
+ } catch (executeError: unknown) {
669
+ // If intent is already executing (e.g. from early executeIntent call), that's fine
670
+ if (getIsAlreadyExecutingError(executeError)) {
671
+ logger.console.log(
672
+ "[trails-sdk] Intent already executing after deposit, continuing...",
673
+ )
674
+ } else {
675
+ logger.console.error(
676
+ "[trails-sdk] Error calling executeIntent:",
677
+ executeError,
678
+ )
679
+ }
663
680
  }
664
681
  }
665
682
  }
666
683
  }
667
-
668
684
  // Start polling for intent completion to update transaction states
669
685
  let finalReceipt: Awaited<ReturnType<typeof pollIntentReceipt>> = null
670
686
  if (intent.intentId) {
671
- // Determine max wait time based on route provider
672
- // OFT routes (LayerZero OFT and Stargate) can take 5-10+ minutes to complete
673
- const isOFTRoute = intent.quote?.routeProviders?.some(
674
- (p) => p === "LZ_OFT" || p === "LZ_STARGATE",
675
- )
687
+ // Determine max wait time based on route provider.
688
+ // OFT routes (LayerZero OFT) can take 5-10+ minutes to complete.
689
+ const isOFTRoute = intent.quote?.routeProviders?.some((p) => p === "LZ_OFT")
676
690
  const maxWaitTime = isOFTRoute ? HOUR_MS : 10 * 60 * 1000 // 1 hour for OFT, 10 min for others
677
691
 
678
692
  logger.console.log("[trails-sdk] Intent polling configuration:", {
@@ -1082,11 +1096,10 @@ export async function handleIntent(
1082
1096
  params: IntentHandlerParams,
1083
1097
  ): Promise<PrepareSendReturn> {
1084
1098
  const {
1085
- queryClient: paramQueryClient,
1086
1099
  mainSignerAddress,
1087
1100
  originChainId,
1088
1101
  originTokenAddress,
1089
- // originTokenBalance not destructured — balance is checked via checkAccountBalance
1102
+ originTokenBalance,
1090
1103
  originTokenDecimals,
1091
1104
  originTokenSymbol,
1092
1105
  destinationChainId,
@@ -1107,6 +1120,8 @@ export async function handleIntent(
1107
1120
  tradeType = TradeType.EXACT_INPUT,
1108
1121
  swapProvider: initialSwapProvider,
1109
1122
  bridgeProvider: initialBridgeProvider,
1123
+ swapProviderFallback,
1124
+ bridgeProviderFallback,
1110
1125
  fundMethod: fundMethodFromParams,
1111
1126
  mode,
1112
1127
  checkoutOnHandlers,
@@ -1123,7 +1138,6 @@ export async function handleIntent(
1123
1138
 
1124
1139
  logger.console.log("[trails-sdk] handleIntent called")
1125
1140
 
1126
- const effectiveQueryClient = paramQueryClient ?? defaultQueryClient
1127
1141
  const effectiveFundMethod = fundMethodFromParams ?? "wallet"
1128
1142
  const effectiveOriginChain = chain
1129
1143
  const effectiveOriginTokenAddress = originTokenAddress
@@ -1165,6 +1179,8 @@ export async function handleIntent(
1165
1179
  tradeType,
1166
1180
  swapProvider,
1167
1181
  bridgeProvider,
1182
+ swapProviderFallback,
1183
+ bridgeProviderFallback,
1168
1184
  undefined,
1169
1185
  walletId,
1170
1186
  isSmartWallet,
@@ -1212,30 +1228,6 @@ export async function handleIntent(
1212
1228
  throw new Error("Invalid intent")
1213
1229
  }
1214
1230
 
1215
- // Estimate gas using depositTransaction from API
1216
- // Use passthrough depositTransaction if available, otherwise use intent depositTransaction
1217
- const depositTxForGasEstimate =
1218
- passthrough?.passthroughTransaction ?? intent.depositTransaction
1219
- let estimatedGasLimit: bigint | undefined
1220
- if (depositTxForGasEstimate) {
1221
- try {
1222
- estimatedGasLimit = await fetchCachedGasLimit(
1223
- effectiveQueryClient,
1224
- effectivePublicClient,
1225
- createGasEstimationParams(
1226
- account.address,
1227
- depositTxForGasEstimate.to,
1228
- depositTxForGasEstimate.data ?? "0x",
1229
- BigInt(depositTxForGasEstimate.value ?? "0"),
1230
- ),
1231
- "intent-quote",
1232
- )
1233
- } catch (error) {
1234
- logger.console.warn("[trails-sdk] Gas estimation failed:", error)
1235
- estimatedGasLimit = undefined
1236
- }
1237
- }
1238
-
1239
1231
  // Determine if passthrough should be used
1240
1232
  // SPEC: Only same-chain same-token with wallet fund method may use passthrough (C9b/C10/C11b/C12/D9b/D10/D11b/D12).
1241
1233
  // SPEC: Cross-chain (A/B), different-token (C1-C8/D1-D8), non-wallet (C13/C14/D13/D14), and gasless (G1-G20) must always use intents.
@@ -1243,21 +1235,22 @@ export async function handleIntent(
1243
1235
  // Backend sets passthrough.eligible, but we enforce structural invariants client-side as a safety net.
1244
1236
  // For ERC20 passthrough with custom calldata, the SDK handles approval if insufficient (see executePassthrough).
1245
1237
  const usePassthrough =
1246
- isSameChainSameToken &&
1247
1238
  effectiveFundMethod === "wallet" &&
1248
1239
  passthrough?.eligible === true &&
1249
- // NOTE: `selectedFeeOption` being `undefined` (unset) intentionally fails this check,
1250
- // conservatively falling back to the intent flow. Only an explicit `null` (native gas selected)
1251
- // or an explicit fee option selection with `isPassthroughEligible === true` enables passthrough.
1252
- (selectedFeeOption === null ||
1253
- selectedFeeOption?.isPassthroughEligible === true)
1240
+ !!passthrough?.passthroughTransaction &&
1241
+ isFeeSelectionPassthroughEligible({
1242
+ selectedFeeOption,
1243
+ gasFeeOptions,
1244
+ })
1254
1245
 
1255
1246
  logger.console.log("[trails-sdk] Passthrough decision:", {
1256
1247
  usePassthrough,
1257
1248
  isSameChainSameToken,
1258
1249
  fundMethod: effectiveFundMethod,
1259
1250
  passthroughEligible: passthrough?.eligible,
1251
+ hasPassthroughTransaction: !!passthrough?.passthroughTransaction,
1260
1252
  feeOptionPassthroughEligible: selectedFeeOption?.isPassthroughEligible,
1253
+ selectedFeeOption,
1261
1254
  })
1262
1255
 
1263
1256
  // Use passthrough-specific deposit transaction and states when doing passthrough
@@ -1270,27 +1263,6 @@ export async function handleIntent(
1270
1263
  const quoteToAmount = intent.quote.toAmount.toString()
1271
1264
  const quoteToAmountMin = intent.quote.toAmountMin.toString() || quoteToAmount
1272
1265
 
1273
- // Now do the balance check with the actual deposit amount from the intent API
1274
- // This is the correct amount needed in origin token units
1275
- if (effectiveFundMethod === "wallet") {
1276
- const { hasEnoughBalance } = await checkAccountBalance({
1277
- account,
1278
- tokenAddress: originTokenAddress,
1279
- depositAmount: depositAmount,
1280
- publicClient: effectivePublicClient,
1281
- })
1282
- noSufficientBalance = !hasEnoughBalance
1283
- logger.console.log(
1284
- "[trails-sdk] Balance check with actual deposit amount:",
1285
- {
1286
- depositAmount,
1287
- originTokenAddress,
1288
- hasEnoughBalance,
1289
- noSufficientBalance,
1290
- },
1291
- )
1292
- }
1293
-
1294
1266
  // Compute transaction states for both passthrough and intent flows
1295
1267
  // This allows the UI to switch between them based on fee option selection
1296
1268
  // SPEC: Passthrough-capable scenarios (C9b/C10/C11b/C12/D9b/D10/D11b/D12) expose passthrough transaction states; other scenarios use intent-flow states.
@@ -1311,51 +1283,78 @@ export async function handleIntent(
1311
1283
  ? passthroughTransactionStates
1312
1284
  : intentTransactionStates
1313
1285
 
1314
- // Build the normalized quote object
1315
- const quote = await getNormalizedQuoteObject({
1316
- originDepositAddress: usePassthrough
1317
- ? recipient
1318
- : intent.originIntentAddress,
1319
- destinationDepositAddress: usePassthrough
1320
- ? recipient
1321
- : intent.destinationIntentAddress,
1322
- destinationAddress: recipient,
1323
- trailsApiKey: trailsApiKey || "",
1324
- trailsApiUrl: trailsApiUrl || "",
1325
- sequenceMetadataUrl: sequenceMetadataUrl || "",
1326
- destinationCalldata,
1327
- originAmount: depositAmount,
1328
- destinationAmount: quoteToAmount,
1329
- originAmountMin: depositAmount,
1330
- destinationAmountMin: quoteToAmountMin,
1331
- originTokenAddress,
1332
- destinationTokenAddress,
1333
- originTokenPriceUsd: sourceTokenPriceUsd?.toString() || null,
1334
- destinationTokenPriceUsd: destinationTokenPriceUsd?.toString() || null,
1335
- fees: isSameChainSameToken
1336
- ? undefined
1337
- : getFeesFromIntent(intent, {
1338
- tradeType,
1339
- fromAmountUsd: calcAmountUsdPrice({
1340
- amount: rawAmountToNumber(depositAmount, originTokenDecimals),
1341
- usdPrice: sourceTokenPriceUsd,
1342
- }),
1343
- toAmountUsd: calcAmountUsdPrice({
1344
- amount: rawAmountToNumber(quoteToAmount, destinationTokenDecimals),
1345
- usdPrice: destinationTokenPriceUsd,
1286
+ // Run balance check and quote normalization in parallel to reduce latency
1287
+ const [balanceResult, quote] = await Promise.all([
1288
+ effectiveFundMethod === "wallet"
1289
+ ? checkAccountBalance({
1290
+ account,
1291
+ tokenAddress: originTokenAddress,
1292
+ depositAmount: depositAmount,
1293
+ publicClient: effectivePublicClient,
1294
+ knownBalance: originTokenBalance,
1295
+ decimals: originTokenDecimals,
1296
+ })
1297
+ : Promise.resolve({ hasEnoughBalance: true }),
1298
+ getNormalizedQuoteObject({
1299
+ originDepositAddress: usePassthrough
1300
+ ? recipient
1301
+ : intent.originIntentAddress,
1302
+ destinationDepositAddress: usePassthrough
1303
+ ? recipient
1304
+ : intent.destinationIntentAddress,
1305
+ destinationAddress: recipient,
1306
+ trailsApiKey: trailsApiKey || "",
1307
+ trailsApiUrl: trailsApiUrl || "",
1308
+ sequenceMetadataUrl: sequenceMetadataUrl || "",
1309
+ destinationCalldata,
1310
+ originAmount: depositAmount,
1311
+ destinationAmount: quoteToAmount,
1312
+ originAmountMin: depositAmount,
1313
+ destinationAmountMin: quoteToAmountMin,
1314
+ originTokenAddress,
1315
+ destinationTokenAddress,
1316
+ originTokenPriceUsd: sourceTokenPriceUsd?.toString() || null,
1317
+ destinationTokenPriceUsd: destinationTokenPriceUsd?.toString() || null,
1318
+ fees: isSameChainSameToken
1319
+ ? undefined
1320
+ : getFeesFromIntent(intent, {
1321
+ tradeType,
1322
+ fromAmountUsd: calcAmountUsdPrice({
1323
+ amount: rawAmountToNumber(depositAmount, originTokenDecimals),
1324
+ usdPrice: sourceTokenPriceUsd,
1325
+ }),
1326
+ toAmountUsd: calcAmountUsdPrice({
1327
+ amount: rawAmountToNumber(
1328
+ quoteToAmount,
1329
+ destinationTokenDecimals,
1330
+ ),
1331
+ usdPrice: destinationTokenPriceUsd,
1332
+ }),
1346
1333
  }),
1347
- }),
1348
- originChainId,
1349
- destinationChainId,
1350
- slippageTolerance,
1351
- priceImpact: getPriceImpactFromIntent(intent),
1352
- priceImpactUsd: getPriceImpactUsdFromIntent(intent),
1353
- transactionStates,
1354
- originNativeTokenPriceUsd,
1355
- noSufficientBalance,
1356
- estimatedGasLimit,
1357
- intent: usePassthrough ? undefined : intent,
1358
- })
1334
+ originChainId,
1335
+ destinationChainId,
1336
+ slippageTolerance,
1337
+ priceImpact: getPriceImpactFromIntent(intent),
1338
+ priceImpactUsd: getPriceImpactUsdFromIntent(intent),
1339
+ transactionStates,
1340
+ originNativeTokenPriceUsd,
1341
+ intent: usePassthrough ? undefined : intent,
1342
+ }),
1343
+ ])
1344
+
1345
+ noSufficientBalance = !balanceResult.hasEnoughBalance
1346
+ quote.noSufficientBalance = noSufficientBalance
1347
+ if (effectiveFundMethod === "wallet") {
1348
+ logger.console.log(
1349
+ "[trails-sdk] Balance check with actual deposit amount:",
1350
+ {
1351
+ depositAmount,
1352
+ originTokenAddress,
1353
+ hasEnoughBalance: balanceResult.hasEnoughBalance,
1354
+ noSufficientBalance,
1355
+ },
1356
+ )
1357
+ }
1359
1358
 
1360
1359
  // Add passthrough-specific fields to the quote
1361
1360
  // This allows the UI to switch between passthrough and intent states based on fee option
@@ -1379,25 +1378,22 @@ export async function handleIntent(
1379
1378
  const { skipCommit, commitOnly, skipIntentMonitoring } = options
1380
1379
 
1381
1380
  try {
1382
- // Recalculate passthrough decision at send time based on fee option
1383
- // This allows the user to change fee option after getting the quote
1384
- // SPEC: All passthrough-capable scenarios (C9b/C10/C11b/C12/D9b/D10/D11b/D12) can switch to intent flow when a gasless fee option is selected.
1381
+ // Recalculate passthrough decision at send time based on fee option.
1382
+ // This allows switching between passthrough and intent flow after quote.
1385
1383
  const sendFeeOption = options.selectedFeeOption
1386
- // SPEC: Only same-chain same-token + wallet may passthrough (C9b/C10/C11b/C12/D9b/D10/D11b/D12).
1387
- // When selectedFeeOption is null, it means native token is selected (which is passthrough eligible)
1388
1384
  const sendUsePassthrough =
1389
- isSameChainSameToken &&
1390
1385
  effectiveFundMethod === "wallet" &&
1391
1386
  passthrough?.eligible === true &&
1392
- // NOTE: `sendFeeOption` being `undefined` (unset) intentionally fails this check,
1393
- // conservatively falling back to the intent flow. Only an explicit `null` (native gas selected)
1394
- // or a fee option with `isPassthroughEligible === true` enables passthrough.
1395
- (sendFeeOption === null ||
1396
- sendFeeOption?.isPassthroughEligible === true)
1387
+ !!passthrough?.passthroughTransaction &&
1388
+ isFeeSelectionPassthroughEligible({
1389
+ selectedFeeOption: sendFeeOption,
1390
+ gasFeeOptions,
1391
+ })
1397
1392
 
1398
1393
  logger.console.log("[trails-sdk] Send passthrough decision:", {
1399
1394
  sendUsePassthrough,
1400
1395
  passthroughEligible: passthrough?.eligible,
1396
+ hasPassthroughTransaction: !!passthrough?.passthroughTransaction,
1401
1397
  sendFeeOptionPassthroughEligible:
1402
1398
  sendFeeOption?.isPassthroughEligible,
1403
1399
  sendFeeOption,
@@ -22,25 +22,22 @@ import { getZeroFees } from "./feeExtractors.js"
22
22
  import { formatDuration } from "../../utils/time.js"
23
23
  import { IntentProtocolVersion } from "@0xtrails/api"
24
24
 
25
- // TODO: make this dyanamic
26
- function isOFTRoute(routeProviders?: string[]): boolean {
27
- return (
28
- routeProviders?.some((p) => p === "LZ_OFT" || p === "LZ_STARGATE") ?? false
29
- )
30
- }
31
-
32
25
  export function getCompletionEstimateSeconds({
33
26
  originChainId,
34
27
  destinationChainId,
35
- routeProviders,
28
+ esimatedDurationSeconds,
36
29
  }: {
37
30
  originChainId: number
38
31
  destinationChainId: number
39
- routeProviders?: string[]
32
+ esimatedDurationSeconds?: number | null
40
33
  }): number {
41
- // OFT routes can take significantly longer than standard routes.
42
- if (isOFTRoute(routeProviders)) {
43
- return 20 * 60
34
+ // Prefer provider-estimated route duration from QuoteIntent API when available.
35
+ if (
36
+ esimatedDurationSeconds != null &&
37
+ Number.isFinite(esimatedDurationSeconds) &&
38
+ esimatedDurationSeconds >= 0
39
+ ) {
40
+ return Math.round(esimatedDurationSeconds)
44
41
  }
45
42
 
46
43
  if (originChainId === mainnet.id && destinationChainId === mainnet.id) {
@@ -57,25 +54,35 @@ export function getCompletionEstimateSeconds({
57
54
  function getCompletionEstimateDisplay({
58
55
  originChainId,
59
56
  destinationChainId,
60
- routeProviders,
57
+ esimatedDurationSeconds,
61
58
  }: {
62
59
  originChainId: number
63
60
  destinationChainId: number
64
- routeProviders?: string[]
61
+ esimatedDurationSeconds?: number | null
65
62
  }): string {
66
- if (isOFTRoute(routeProviders)) {
67
- return "5-20 min"
68
- }
69
-
70
63
  return formatDuration(
71
64
  getCompletionEstimateSeconds({
72
65
  originChainId,
73
66
  destinationChainId,
74
- routeProviders,
67
+ esimatedDurationSeconds,
75
68
  }),
76
69
  )
77
70
  }
78
71
 
72
+ function getEsimatedDurationSeconds(intent?: Intent): number | undefined {
73
+ if (!intent?.quote) return undefined
74
+
75
+ const quote = intent.quote as { estimatedDuration?: unknown }
76
+ if (
77
+ typeof quote.estimatedDuration === "number" &&
78
+ Number.isFinite(quote.estimatedDuration)
79
+ ) {
80
+ return quote.estimatedDuration
81
+ }
82
+
83
+ return undefined
84
+ }
85
+
79
86
  export async function getNormalizedQuoteObject({
80
87
  originDepositAddress,
81
88
  destinationDepositAddress,
@@ -253,7 +260,7 @@ export async function getNormalizedQuoteObject({
253
260
  let gasCost: string = "0"
254
261
  let gasCostFormatted: string = "0"
255
262
  try {
256
- if (originNativeTokenPriceUsd) {
263
+ if (originNativeTokenPriceUsd && (estimatedGasLimit || providedFeeData)) {
257
264
  // Use the actual estimated gas limit if provided, otherwise use default
258
265
  // This ensures the quote gas cost matches what will be used in the actual transaction
259
266
  const gasLimitForCost = estimatedGasLimit || DEFAULT_MIN_GASLIMIT
@@ -414,10 +421,12 @@ export async function getNormalizedQuoteObject({
414
421
  },
415
422
  )
416
423
 
424
+ const esimatedDurationSeconds = getEsimatedDurationSeconds(intent)
425
+
417
426
  const completionEstimateSeconds = getCompletionEstimateSeconds({
418
427
  originChainId,
419
428
  destinationChainId,
420
- routeProviders: intent?.quote?.routeProviders,
429
+ esimatedDurationSeconds,
421
430
  })
422
431
 
423
432
  return {
@@ -465,7 +474,7 @@ export async function getNormalizedQuoteObject({
465
474
  completionEstimateDisplay: getCompletionEstimateDisplay({
466
475
  originChainId,
467
476
  destinationChainId,
468
- routeProviders: intent?.quote?.routeProviders,
477
+ esimatedDurationSeconds,
469
478
  }),
470
479
  transactionStates: transactionStates || [],
471
480
  gasCostUsd,
@@ -9,6 +9,13 @@ import {
9
9
  import { TradeType } from "../types.js"
10
10
  import { logger } from "../../logger.js"
11
11
 
12
+ type QuoteIntentRequestOptionsWithFallback = NonNullable<
13
+ QuoteIntentRequest["options"]
14
+ > & {
15
+ swapProviderFallback?: boolean
16
+ bridgeProviderFallback?: boolean
17
+ }
18
+
12
19
  export async function getIntentArgs(
13
20
  mainSignerAddress: string,
14
21
  originChainId: number,
@@ -24,6 +31,8 @@ export async function getIntentArgs(
24
31
  tradeType: TradeType,
25
32
  swapProvider?: RouteProvider | null,
26
33
  bridgeProvider?: RouteProvider | null,
34
+ swapProviderFallback?: boolean,
35
+ bridgeProviderFallback?: boolean,
27
36
  connector?: Connector | undefined,
28
37
  walletId?: string | undefined,
29
38
  isSmartWallet?: boolean,
@@ -73,6 +82,20 @@ export async function getIntentArgs(
73
82
  )
74
83
  }
75
84
 
85
+ const options: QuoteIntentRequestOptionsWithFallback = {
86
+ // When slippageTolerance is null (AUTO mode), don't send it so backend calculates optimal slippage
87
+ slippageTolerance:
88
+ slippageTolerance !== null ? Number(slippageTolerance) : undefined,
89
+ swapProvider: swapProvider || undefined,
90
+ bridgeProvider: bridgeProvider || undefined,
91
+ }
92
+ if (swapProviderFallback !== undefined) {
93
+ options.swapProviderFallback = swapProviderFallback
94
+ }
95
+ if (bridgeProviderFallback !== undefined) {
96
+ options.bridgeProviderFallback = bridgeProviderFallback
97
+ }
98
+
76
99
  const intentArgs: QuoteIntentRequest = {
77
100
  ownerAddress: mainSignerAddress,
78
101
  originChainId,
@@ -86,13 +109,7 @@ export async function getIntentArgs(
86
109
  destinationCallData: hasCustomCalldata ? destinationCalldata : "0x",
87
110
  destinationCallValue: BigInt("0"),
88
111
  onlyNativeGasFee,
89
- options: {
90
- // When slippageTolerance is null (AUTO mode), don't send it so backend calculates optimal slippage
91
- slippageTolerance:
92
- slippageTolerance !== null ? Number(slippageTolerance) : undefined,
93
- swapProvider: swapProvider || undefined,
94
- bridgeProvider: bridgeProvider || undefined,
95
- },
112
+ options,
96
113
  tradeType,
97
114
  }
98
115
 
@@ -80,6 +80,8 @@ export type PrepareSendOptions = {
80
80
  originNativeTokenPriceUsd?: number | null
81
81
  swapProvider?: RouteProvider | null
82
82
  bridgeProvider?: RouteProvider | null
83
+ swapProviderFallback?: boolean
84
+ bridgeProviderFallback?: boolean
83
85
  fundMethod?: FundMethod
84
86
  mode?: "pay" | "fund" | "earn" | "swap" | "withdraw"
85
87
  originPublicClient?: PublicClient
@@ -13,11 +13,15 @@ export async function checkAccountBalance({
13
13
  tokenAddress,
14
14
  depositAmount,
15
15
  publicClient,
16
+ knownBalance,
17
+ decimals: knownDecimals,
16
18
  }: {
17
19
  account: Account
18
20
  tokenAddress: string
19
21
  depositAmount: string
20
22
  publicClient: PublicClient
23
+ knownBalance?: string
24
+ decimals?: number
21
25
  }): Promise<{
22
26
  hasEnoughBalance: boolean
23
27
  balance: bigint
@@ -29,11 +33,11 @@ export async function checkAccountBalance({
29
33
  try {
30
34
  let balance: bigint
31
35
 
32
- if (isNativeToken(tokenAddress)) {
33
- // Native token balance
36
+ if (knownBalance !== undefined) {
37
+ balance = BigInt(knownBalance)
38
+ } else if (isNativeToken(tokenAddress)) {
34
39
  balance = await publicClient.getBalance({ address: account.address })
35
40
  } else {
36
- // ERC20 token balance
37
41
  balance = await publicClient.readContract({
38
42
  address: tokenAddress as `0x${string}`,
39
43
  abi: erc20Abi,
@@ -54,8 +58,10 @@ export async function checkAccountBalance({
54
58
  if (isNativeToken(tokenAddress)) {
55
59
  balanceFormatted = formatUnits(balance, 18)
56
60
  requiredAmountFormatted = formatUnits(requiredAmount, 18)
61
+ } else if (knownDecimals !== undefined) {
62
+ balanceFormatted = formatUnits(balance, knownDecimals)
63
+ requiredAmountFormatted = formatUnits(requiredAmount, knownDecimals)
57
64
  } else {
58
- // ERC20 token balance
59
65
  const decimals = await publicClient.readContract({
60
66
  address: tokenAddress as `0x${string}`,
61
67
  abi: erc20Abi,