0xtrails 0.4.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/dist/analytics.d.ts +8 -3
  2. package/dist/analytics.d.ts.map +1 -1
  3. package/dist/{ccip-C9CA3tIi.js → ccip-Dw5AN7oU.js} +27 -27
  4. package/dist/cctp.d.ts +0 -149
  5. package/dist/cctp.d.ts.map +1 -1
  6. package/dist/chains.d.ts +31 -4
  7. package/dist/chains.d.ts.map +1 -1
  8. package/dist/config.d.ts +28 -52
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/constants.d.ts +1 -7
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/contractUtils.d.ts.map +1 -1
  13. package/dist/estimate.d.ts.map +1 -1
  14. package/dist/fees.d.ts.map +1 -1
  15. package/dist/gasless.d.ts +12 -0
  16. package/dist/gasless.d.ts.map +1 -1
  17. package/dist/{index-CNB57v0n.js → index-BtVUTbEZ.js} +32255 -40547
  18. package/dist/index.d.ts +13 -11
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +320 -944
  21. package/dist/indexerClient.d.ts +9 -6
  22. package/dist/indexerClient.d.ts.map +1 -1
  23. package/dist/intents.d.ts +0 -18
  24. package/dist/intents.d.ts.map +1 -1
  25. package/dist/mutations.d.ts.map +1 -1
  26. package/dist/paymasterSend.d.ts.map +1 -1
  27. package/dist/prepareSend.d.ts +1 -1
  28. package/dist/prepareSend.d.ts.map +1 -1
  29. package/dist/sendUserOp.d.ts +0 -18
  30. package/dist/sendUserOp.d.ts.map +1 -1
  31. package/dist/sequenceWallet.d.ts +1 -1
  32. package/dist/sequenceWallet.d.ts.map +1 -1
  33. package/dist/tokenBalances.d.ts.map +1 -1
  34. package/dist/tokens.d.ts +10 -8
  35. package/dist/tokens.d.ts.map +1 -1
  36. package/dist/trailsClient.d.ts +3 -3
  37. package/dist/trailsClient.d.ts.map +1 -1
  38. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts +4 -5
  39. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts.map +1 -1
  40. package/dist/transactionIntent/deposits/gaslessDeposit.d.ts +4 -5
  41. package/dist/transactionIntent/deposits/gaslessDeposit.d.ts.map +1 -1
  42. package/dist/transactionIntent/deposits/standardDeposit.d.ts +2 -2
  43. package/dist/transactionIntent/deposits/standardDeposit.d.ts.map +1 -1
  44. package/dist/transactionIntent/execution/transactionState.d.ts +2 -2
  45. package/dist/transactionIntent/execution/transactionState.d.ts.map +1 -1
  46. package/dist/transactionIntent/handlers/crossChain.d.ts +6 -4
  47. package/dist/transactionIntent/handlers/crossChain.d.ts.map +1 -1
  48. package/dist/transactionIntent/handlers/index.d.ts +0 -1
  49. package/dist/transactionIntent/handlers/index.d.ts.map +1 -1
  50. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts +4 -34
  51. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts.map +1 -1
  52. package/dist/transactionIntent/quote/normalizeQuote.d.ts.map +1 -1
  53. package/dist/transactionIntent/quote/quoteHelpers.d.ts +2 -1
  54. package/dist/transactionIntent/quote/quoteHelpers.d.ts.map +1 -1
  55. package/dist/transactionIntent/types.d.ts +7 -19
  56. package/dist/transactionIntent/types.d.ts.map +1 -1
  57. package/dist/transactionIntent/utils/index.d.ts +0 -1
  58. package/dist/transactionIntent/utils/index.d.ts.map +1 -1
  59. package/dist/transactions.d.ts +6 -18
  60. package/dist/transactions.d.ts.map +1 -1
  61. package/dist/utils.d.ts +8 -2
  62. package/dist/utils.d.ts.map +1 -1
  63. package/dist/walletUtils.d.ts +21 -0
  64. package/dist/walletUtils.d.ts.map +1 -0
  65. package/dist/wallets.d.ts +33 -240
  66. package/dist/wallets.d.ts.map +1 -1
  67. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  68. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  69. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  70. package/dist/widget/components/FeeOption.d.ts +8 -13
  71. package/dist/widget/components/FeeOption.d.ts.map +1 -1
  72. package/dist/widget/components/FeeOptions.d.ts +11 -5
  73. package/dist/widget/components/FeeOptions.d.ts.map +1 -1
  74. package/dist/widget/components/NativeGasOption.d.ts.map +1 -1
  75. package/dist/widget/components/Pay.d.ts.map +1 -1
  76. package/dist/widget/components/PoolDeposit.d.ts.map +1 -1
  77. package/dist/widget/components/QRCodeDeposit.d.ts +5 -0
  78. package/dist/widget/components/QRCodeDeposit.d.ts.map +1 -1
  79. package/dist/widget/components/QRCodeWalletSelect.d.ts +13 -0
  80. package/dist/widget/components/QRCodeWalletSelect.d.ts.map +1 -0
  81. package/dist/widget/components/QrCode.d.ts.map +1 -1
  82. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  83. package/dist/widget/components/Receipt.d.ts.map +1 -1
  84. package/dist/widget/components/ScreenHeader.d.ts +1 -1
  85. package/dist/widget/components/ScreenHeader.d.ts.map +1 -1
  86. package/dist/widget/components/SlippageToleranceSettings.d.ts +1 -0
  87. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  88. package/dist/widget/components/Toast.d.ts.map +1 -1
  89. package/dist/widget/components/TokenImage.d.ts.map +1 -1
  90. package/dist/widget/components/WalletConnect.d.ts.map +1 -1
  91. package/dist/widget/css/compiled.css +1 -1
  92. package/dist/widget/hooks/useCheckout.d.ts +15 -1
  93. package/dist/widget/hooks/useCheckout.d.ts.map +1 -1
  94. package/dist/widget/hooks/useCurrentScreen.d.ts +1 -1
  95. package/dist/widget/hooks/useCurrentScreen.d.ts.map +1 -1
  96. package/dist/widget/hooks/useDebugScreens.d.ts +1 -1
  97. package/dist/widget/hooks/useDebugScreens.d.ts.map +1 -1
  98. package/dist/widget/hooks/useIntentTransactionHistory.d.ts.map +1 -1
  99. package/dist/widget/hooks/useIsConnectedWalletSmartContract.d.ts +7 -0
  100. package/dist/widget/hooks/useIsConnectedWalletSmartContract.d.ts.map +1 -0
  101. package/dist/widget/hooks/useIsSequenceWallet.d.ts +6 -0
  102. package/dist/widget/hooks/useIsSequenceWallet.d.ts.map +1 -0
  103. package/dist/widget/hooks/useQuote.d.ts +6 -11
  104. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  105. package/dist/widget/hooks/useRecentTokens.d.ts.map +1 -1
  106. package/dist/widget/hooks/useSelectedFeeOption.d.ts +30 -0
  107. package/dist/widget/hooks/useSelectedFeeOption.d.ts.map +1 -0
  108. package/dist/widget/hooks/useSendForm.d.ts +6 -15
  109. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  110. package/dist/widget/hooks/useTokenList.d.ts.map +1 -1
  111. package/dist/widget/index.d.ts +1 -0
  112. package/dist/widget/index.d.ts.map +1 -1
  113. package/dist/widget/index.js +4 -2
  114. package/dist/widget/providers/TrailsProvider.d.ts +29 -0
  115. package/dist/widget/providers/TrailsProvider.d.ts.map +1 -0
  116. package/dist/widget/widget.d.ts +14 -3
  117. package/dist/widget/widget.d.ts.map +1 -1
  118. package/package.json +9 -8
  119. package/src/analytics.ts +53 -21
  120. package/src/cctp.ts +0 -1016
  121. package/src/chains.ts +155 -68
  122. package/src/config.ts +51 -207
  123. package/src/constants.ts +1 -13
  124. package/src/contractUtils.ts +6 -6
  125. package/src/estimate.ts +3 -6
  126. package/src/fees.ts +5 -10
  127. package/src/gasless.ts +45 -0
  128. package/src/index.ts +18 -40
  129. package/src/indexerClient.ts +39 -48
  130. package/src/intents.ts +51 -226
  131. package/src/mutations.ts +3 -2
  132. package/src/paymasterSend.ts +2 -5
  133. package/src/prepareSend.ts +21 -14
  134. package/src/sendUserOp.ts +3 -64
  135. package/src/sequenceWallet.ts +1 -2
  136. package/src/tokenBalances.ts +2 -1
  137. package/src/tokens.ts +62 -133
  138. package/src/trailsClient.ts +17 -12
  139. package/src/transactionIntent/deposits/depositOrchestrator.ts +14 -15
  140. package/src/transactionIntent/deposits/gaslessDeposit.ts +70 -100
  141. package/src/transactionIntent/deposits/standardDeposit.ts +22 -28
  142. package/src/transactionIntent/execution/transactionState.ts +2 -2
  143. package/src/transactionIntent/handlers/crossChain.ts +179 -381
  144. package/src/transactionIntent/handlers/index.ts +0 -1
  145. package/src/transactionIntent/handlers/sameChainSameToken.ts +228 -94
  146. package/src/transactionIntent/quote/normalizeQuote.ts +4 -6
  147. package/src/transactionIntent/quote/quoteHelpers.ts +35 -3
  148. package/src/transactionIntent/types.ts +7 -27
  149. package/src/transactionIntent/utils/index.ts +0 -1
  150. package/src/transactions.ts +19 -118
  151. package/src/umd.tsx +1 -3
  152. package/src/utils.ts +28 -8
  153. package/src/walletUtils.ts +42 -0
  154. package/src/wallets.ts +365 -208
  155. package/src/widget/compiled.css +1 -1
  156. package/src/widget/components/AccountIntentTransactionHistory.tsx +78 -4
  157. package/src/widget/components/AccountSettings.tsx +17 -17
  158. package/src/widget/components/ChainList.tsx +3 -3
  159. package/src/widget/components/ClassicSwap.tsx +19 -10
  160. package/src/widget/components/ConfigDisplay.tsx +19 -59
  161. package/src/widget/components/FeeOption.tsx +63 -20
  162. package/src/widget/components/FeeOptions.tsx +54 -123
  163. package/src/widget/components/NativeGasOption.tsx +3 -1
  164. package/src/widget/components/Pay.tsx +18 -11
  165. package/src/widget/components/PoolDeposit.tsx +23 -10
  166. package/src/widget/components/QRCodeDeposit.tsx +50 -30
  167. package/src/widget/components/QRCodeWalletSelect.tsx +77 -0
  168. package/src/widget/components/QrCode.tsx +188 -233
  169. package/src/widget/components/QuoteDetails.tsx +48 -2
  170. package/src/widget/components/Receipt.tsx +5 -2
  171. package/src/widget/components/ScreenHeader.tsx +10 -8
  172. package/src/widget/components/SlippageToleranceSettings.tsx +63 -14
  173. package/src/widget/components/Toast.tsx +10 -0
  174. package/src/widget/components/TokenImage.tsx +56 -13
  175. package/src/widget/components/WalletConnect.tsx +37 -99
  176. package/src/widget/hooks/useCheckout.ts +71 -0
  177. package/src/widget/hooks/useCurrentScreen.tsx +1 -0
  178. package/src/widget/hooks/useDebugScreens.ts +5 -0
  179. package/src/widget/hooks/useIntentTransactionHistory.ts +790 -412
  180. package/src/widget/hooks/useIsConnectedWalletSmartContract.ts +43 -0
  181. package/src/widget/hooks/useIsSequenceWallet.ts +17 -0
  182. package/src/widget/hooks/useQuote.ts +27 -34
  183. package/src/widget/hooks/useRecentTokens.ts +2 -1
  184. package/src/widget/hooks/useSelectedFeeOption.tsx +257 -0
  185. package/src/widget/hooks/useSendForm.ts +175 -46
  186. package/src/widget/hooks/useTokenList.ts +15 -2
  187. package/src/widget/index.tsx +8 -0
  188. package/src/widget/providers/TrailsProvider.tsx +123 -0
  189. package/src/widget/widget.tsx +165 -143
  190. package/dist/cctpqueue.d.ts +0 -18
  191. package/dist/cctpqueue.d.ts.map +0 -1
  192. package/dist/preconditions.d.ts +0 -12
  193. package/dist/preconditions.d.ts.map +0 -1
  194. package/dist/trails.d.ts +0 -110
  195. package/dist/trails.d.ts.map +0 -1
  196. package/dist/transactionIntent/handlers/sameChainDifferentToken.d.ts +0 -62
  197. package/dist/transactionIntent/handlers/sameChainDifferentToken.d.ts.map +0 -1
  198. package/dist/transactionIntent/utils/lifiHelpers.d.ts +0 -10
  199. package/dist/transactionIntent/utils/lifiHelpers.d.ts.map +0 -1
  200. package/dist/widget/hooks/useSelectedFeeToken.d.ts +0 -33
  201. package/dist/widget/hooks/useSelectedFeeToken.d.ts.map +0 -1
  202. package/src/cctpqueue.ts +0 -66
  203. package/src/preconditions.ts +0 -47
  204. package/src/trails.ts +0 -1303
  205. package/src/transactionIntent/handlers/sameChainDifferentToken.ts +0 -323
  206. package/src/transactionIntent/utils/lifiHelpers.ts +0 -68
  207. package/src/widget/hooks/useSelectedFeeToken.tsx +0 -288
@@ -1,4 +1,5 @@
1
- import type { TokenPrice } from "@0xsequence/trails-api"
1
+ import type { TokenPrice, FeeOption } from "@0xsequence/trails-api"
2
+ import { JsonEncode } from "@0xsequence/trails-api"
2
3
  import type React from "react"
3
4
  import { useCallback, useEffect, useMemo, useState, useRef } from "react"
4
5
  import {
@@ -17,7 +18,6 @@ import {
17
18
  TradeType,
18
19
  type PrepareSendReturn,
19
20
  type PrepareSendQuote,
20
- type SelectedFeeToken,
21
21
  } from "../../prepareSend.js"
22
22
  import type { TransactionState } from "../../transactions.js"
23
23
  import { getTokenPrice, useTokenPrices, normalizeNumber } from "../../prices.js"
@@ -40,8 +40,12 @@ import { etherlink } from "viem/chains"
40
40
  import { logger } from "../../logger.js"
41
41
  import { getIsContract } from "../../contractUtils.js"
42
42
  import { useTrailsClient } from "../../trailsClient.js"
43
+ import { useTrails } from "../providers/TrailsProvider.js"
43
44
  import { useTokenList } from "./useTokenList.js"
44
- import { useSelectedFeeToken } from "./useSelectedFeeToken.js"
45
+ import {
46
+ useSelectedFeeOption,
47
+ type ProcessedFeeOption,
48
+ } from "./useSelectedFeeOption.js"
45
49
 
46
50
  export interface Token {
47
51
  id: number
@@ -117,17 +121,6 @@ export type UseSendProps = {
117
121
  refetchTrigger?: number
118
122
  }
119
123
 
120
- export type FeeOption = {
121
- tokenAddress: string
122
- tokenSymbol: string
123
- tokenDecimals: number
124
- amount: string
125
- amountUSD: number
126
- notEnoughBalance?: boolean
127
- tokenImageUrl?: string
128
- chainId?: number
129
- }
130
-
131
124
  export type UseSendReturn = {
132
125
  amount: string
133
126
  amountRaw: string
@@ -152,8 +145,8 @@ export type UseSendReturn = {
152
145
  setRecipientInput: (recipientInput: string) => void
153
146
  setSelectedDestinationChain: (chain: ChainInfo) => void
154
147
  setSelectedDestToken: (token: TokenInfo) => void
155
- setSelectedFeeToken: (token: FeeOption) => void
156
- feeOptions: FeeOption[]
148
+ setSelectedFeeOption: (token: FeeOption | null) => void
149
+ processedFeeOptions: ProcessedFeeOption[]
157
150
  supportedTokens: SupportedToken[]
158
151
  supportedChains: ChainInfo[]
159
152
  ensAddress: string | null
@@ -163,7 +156,7 @@ export type UseSendReturn = {
163
156
  destTokenPrices: TokenPrice[] | null
164
157
  sourceTokenPrices: TokenPrice[] | null
165
158
  selectedToken?: Token
166
- selectedFeeToken: SelectedFeeToken | null
159
+ selectedFeeOption: FeeOption | null
167
160
  setIsChainDropdownOpen: (isOpen: boolean) => void
168
161
  setIsTokenDropdownOpen: (isOpen: boolean) => void
169
162
  toAmountFormatted: string
@@ -433,6 +426,7 @@ export function useSendForm({
433
426
  }, [selectedDestToken, defaultDestToken])
434
427
 
435
428
  const trailsClient = useTrailsClient()
429
+ const { trailsApiKey, sequenceIndexerUrl } = useTrails()
436
430
 
437
431
  // Get user's token balances for balance checking
438
432
  const { filteredTokensFormatted } = useTokenList({
@@ -545,6 +539,10 @@ export function useSendForm({
545
539
  const [isLoadingQuote, setIsLoadingQuote] = useState(false)
546
540
  const [prepareSendResult, setPrepareSendResult] =
547
541
  useState<PrepareSendReturn | null>(null)
542
+ const latestResolvedIntentIdRef = useRef<string | null>(null)
543
+ const quoteInputsFingerprintRef = useRef<string | null>(null)
544
+ const preparedQuoteFingerprintRef = useRef<string | null>(null)
545
+
548
546
  // Create a stable callback for transaction state changes
549
547
  const handleTransactionStateChange = useCallback(
550
548
  (transactionStates: TransactionState[]) => {
@@ -583,11 +581,11 @@ export function useSendForm({
583
581
 
584
582
  // Use the fee token selection hook
585
583
  const {
586
- selectedFeeToken,
587
- setSelectedFeeToken,
588
- processedFeeOptions: feeOptions,
589
- setRawFeeOptions,
590
- } = useSelectedFeeToken()
584
+ selectedFeeOption,
585
+ setSelectedFeeOption,
586
+ processedFeeOptions,
587
+ setFeeOptions,
588
+ } = useSelectedFeeOption()
591
589
 
592
590
  const [isRecipientContract, setIsRecipientContract] = useState(false)
593
591
  const [isSenderContractOnOrigin, setIsSenderContractOnOrigin] =
@@ -684,6 +682,34 @@ export function useSendForm({
684
682
  tradeType,
685
683
  ])
686
684
 
685
+ const buildQuoteInputsFingerprint = useCallback(
686
+ () =>
687
+ JSON.stringify({
688
+ originTokenAddress: selectedToken?.contractAddress?.toLowerCase() ?? "",
689
+ originChainId: selectedToken?.chainId ?? "",
690
+ destinationTokenSymbol: selectedDestToken?.symbol ?? "",
691
+ destinationChainId: selectedDestinationChain?.id ?? "",
692
+ destinationTokenAddress: destinationTokenAddress?.toLowerCase() ?? "",
693
+ amount: amount || "",
694
+ amountRaw: amountRaw || "",
695
+ recipient: recipient?.toLowerCase() ?? "",
696
+ tradeType,
697
+ toCalldata: toCalldata ?? "",
698
+ }),
699
+ [
700
+ amount,
701
+ amountRaw,
702
+ destinationTokenAddress,
703
+ recipient,
704
+ selectedDestinationChain?.id,
705
+ selectedDestToken?.symbol,
706
+ selectedToken?.chainId,
707
+ selectedToken?.contractAddress,
708
+ toCalldata,
709
+ tradeType,
710
+ ],
711
+ )
712
+
687
713
  // Get quote automatically when inputs change
688
714
  const getQuote = useCallback(async () => {
689
715
  // Only get quote if all required inputs are present
@@ -713,9 +739,16 @@ export function useSendForm({
713
739
  )
714
740
  setQuoteError(null)
715
741
  setPrepareSendResult(null)
742
+ preparedQuoteFingerprintRef.current = null
716
743
  return
717
744
  }
718
745
 
746
+ latestResolvedIntentIdRef.current = null
747
+ let requestFingerprint = quoteInputsFingerprintRef.current
748
+ if (!requestFingerprint) {
749
+ requestFingerprint = buildQuoteInputsFingerprint()
750
+ quoteInputsFingerprintRef.current = requestFingerprint
751
+ }
719
752
  try {
720
753
  setIsLoadingQuote(true)
721
754
  setError(null)
@@ -733,6 +766,8 @@ export function useSendForm({
733
766
  tradeType,
734
767
  })
735
768
  setPrepareSendResult(null)
769
+ preparedQuoteFingerprintRef.current = null
770
+ latestResolvedIntentIdRef.current = null
736
771
  setIsLoadingQuote(false)
737
772
  return
738
773
  }
@@ -847,30 +882,66 @@ export function useSendForm({
847
882
  mode,
848
883
  fundMethod,
849
884
  checkoutOnHandlers,
850
- selectedFeeToken: selectedFeeToken ?? undefined,
885
+ selectedFeeOption: selectedFeeOption ?? null,
851
886
  walletId,
887
+ sequenceIndexerUrl,
888
+ sequenceProjectAccessKey: trailsApiKey,
852
889
  }
853
890
 
854
891
  logger.console.log(
855
- "[trails-sdk] [FEE-SELECT] getQuote using selectedFeeToken:",
892
+ "[trails-sdk] [FEE-SELECT] getQuote using selectedFeeOption:",
856
893
  {
857
- selectedFeeToken,
894
+ selectedFeeOption,
858
895
  },
859
896
  )
860
897
 
861
898
  const result = await prepareSend(options)
862
899
 
900
+ if (requestFingerprint !== quoteInputsFingerprintRef.current) {
901
+ logger.console.log(
902
+ "[trails-sdk] Ignoring stale quote result (fingerprint mismatch)",
903
+ {
904
+ requestFingerprint,
905
+ currentFingerprint: quoteInputsFingerprintRef.current,
906
+ },
907
+ )
908
+ return
909
+ }
910
+
863
911
  logger.console.log("[trails-sdk] prepareSend quote:", result.quote)
864
912
 
913
+ // Track the intentId returned by the most recent quote so we never execute a stale CommitIntent
914
+ // even when the quote inputs (fingerprint) are identical between retries.
915
+ const quoteIntentId = result.quote.intentId ?? null
916
+
917
+ logger.console.log("[trails-sdk] Resolved quote intentId:", quoteIntentId)
918
+
919
+ latestResolvedIntentIdRef.current = quoteIntentId
865
920
  setPrepareSendResult(result)
921
+ preparedQuoteFingerprintRef.current = requestFingerprint
866
922
  setIsLoadingQuote(false)
867
923
  } catch (error) {
924
+ if (requestFingerprint !== quoteInputsFingerprintRef.current) {
925
+ logger.console.log(
926
+ "[trails-sdk] Ignoring quote error (fingerprint mismatch)",
927
+ {
928
+ requestFingerprint,
929
+ currentFingerprint: quoteInputsFingerprintRef.current,
930
+ },
931
+ )
932
+ return
933
+ }
934
+
868
935
  logger.console.error("[trails-sdk] Error getting quote:", error)
869
- setQuoteError(getFullErrorMessage(error))
936
+ const errorMessage = getFullErrorMessage(error)
937
+ setQuoteError(errorMessage)
870
938
  setPrepareSendResult(null)
939
+ preparedQuoteFingerprintRef.current = null
940
+ latestResolvedIntentIdRef.current = null
871
941
  setIsLoadingQuote(false)
872
942
  }
873
943
  }, [
944
+ trailsApiKey,
874
945
  tradeType,
875
946
  isDryMode,
876
947
  account,
@@ -879,6 +950,7 @@ export function useSendForm({
879
950
  selectedDestToken?.decimals,
880
951
  recipient,
881
952
  destinationTokenAddress,
953
+ sequenceIndexerUrl,
882
954
  selectedDestToken?.symbol,
883
955
  selectedDestinationChain?.id,
884
956
  selectedToken?.contractAddress,
@@ -900,8 +972,9 @@ export function useSendForm({
900
972
  amountRaw,
901
973
  checkoutOnHandlers,
902
974
  mode,
903
- selectedFeeToken,
975
+ selectedFeeOption,
904
976
  walletId,
977
+ buildQuoteInputsFingerprint,
905
978
  ])
906
979
 
907
980
  // Auto-fetch quotes when inputs change (debounced)
@@ -913,12 +986,29 @@ export function useSendForm({
913
986
  !destinationTokenAddress ||
914
987
  !isValidRecipient ||
915
988
  !selectedDestToken?.symbol ||
916
- !selectedDestinationChain?.id
989
+ !selectedDestinationChain?.id ||
990
+ !selectedToken
917
991
  ) {
918
992
  setPrepareSendResult(null)
993
+ latestResolvedIntentIdRef.current = null
994
+ quoteInputsFingerprintRef.current = null
995
+ preparedQuoteFingerprintRef.current = null
996
+ setIsLoadingQuote(false)
919
997
  return
920
998
  }
921
999
 
1000
+ const nextQuoteInputsFingerprint = buildQuoteInputsFingerprint()
1001
+
1002
+ // Fingerprint all quote-driving inputs so we can invalidate debounced quotes immediately when
1003
+ // the user switches tokens; this fixes the stale quote so that it stays loading until the new quote is ready.
1004
+ if (quoteInputsFingerprintRef.current !== nextQuoteInputsFingerprint) {
1005
+ quoteInputsFingerprintRef.current = nextQuoteInputsFingerprint
1006
+ latestResolvedIntentIdRef.current = null
1007
+ setPrepareSendResult(null)
1008
+ preparedQuoteFingerprintRef.current = null
1009
+ setIsLoadingQuote(true)
1010
+ }
1011
+
922
1012
  const timeoutId = setTimeout(() => {
923
1013
  getQuote()
924
1014
  }, 500) // Debounce by 500ms
@@ -932,12 +1022,15 @@ export function useSendForm({
932
1022
  selectedDestinationChain?.id,
933
1023
  toCalldata,
934
1024
  refetchTrigger,
1025
+ amountRaw,
1026
+ tradeType,
935
1027
  selectedToken?.contractAddress,
936
1028
  selectedToken?.chainId,
937
1029
  selectedToken?.balance,
938
1030
  selectedToken?.tokenPriceUsd,
939
1031
  recipient, // Add recipient to trigger quote re-fetch when it changes
940
- // selectedFeeToken is passed to send() at execution time, not needed here
1032
+ // selectedFeeOption is passed to send() at execution time, not needed here
1033
+ buildQuoteInputsFingerprint,
941
1034
  ])
942
1035
 
943
1036
  // Calculate destination amount from quote if available
@@ -967,12 +1060,12 @@ export function useSendForm({
967
1060
  },
968
1061
  )
969
1062
 
970
- setRawFeeOptions(
1063
+ setFeeOptions(
971
1064
  apiFeeOptions,
972
1065
  selectedToken?.chainId,
973
1066
  filteredTokensFormattedRef.current,
974
1067
  )
975
- }, [prepareSendResult, selectedToken?.chainId, setRawFeeOptions])
1068
+ }, [prepareSendResult, selectedToken?.chainId, setFeeOptions])
976
1069
 
977
1070
  const processSend = useCallback(async () => {
978
1071
  try {
@@ -981,6 +1074,41 @@ export function useSendForm({
981
1074
  return
982
1075
  }
983
1076
 
1077
+ const currentQuoteInputsFingerprint = buildQuoteInputsFingerprint()
1078
+ if (
1079
+ !preparedQuoteFingerprintRef.current ||
1080
+ preparedQuoteFingerprintRef.current !== currentQuoteInputsFingerprint
1081
+ ) {
1082
+ logger.console.warn(
1083
+ "[trails-sdk] Blocking send because quote fingerprint is stale",
1084
+ {
1085
+ prepared: preparedQuoteFingerprintRef.current,
1086
+ current: currentQuoteInputsFingerprint,
1087
+ },
1088
+ )
1089
+ setPrepareSendResult(null)
1090
+ preparedQuoteFingerprintRef.current = null
1091
+ latestResolvedIntentIdRef.current = null
1092
+ setIsLoadingQuote(true)
1093
+ return
1094
+ }
1095
+
1096
+ const quoteIntentId = prepareSendResult.quote.intentId ?? null
1097
+ if (
1098
+ quoteIntentId &&
1099
+ latestResolvedIntentIdRef.current !== quoteIntentId
1100
+ ) {
1101
+ logger.console.error(
1102
+ "[trails-sdk] Quote intentId has changed, waiting for latest quote",
1103
+ {
1104
+ current: latestResolvedIntentIdRef.current,
1105
+ prepareSend: quoteIntentId,
1106
+ },
1107
+ )
1108
+ setError("Quote is updating. Please wait for the latest quote.")
1109
+ return
1110
+ }
1111
+
984
1112
  setError(null)
985
1113
  setIsSubmitting(true)
986
1114
 
@@ -1059,28 +1187,28 @@ export function useSendForm({
1059
1187
  "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] handleSend called, about to call send()",
1060
1188
  )
1061
1189
  logger.console.log(
1062
- "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] selectedFeeToken value at send() call time:",
1190
+ "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] selectedFeeOption value at send() call time:",
1063
1191
  {
1064
- selectedFeeToken,
1065
- isNull: selectedFeeToken === null,
1066
- isUndefined: selectedFeeToken === undefined,
1067
- type: typeof selectedFeeToken,
1068
- stringified: JSON.stringify(selectedFeeToken),
1192
+ selectedFeeOption,
1193
+ isNull: selectedFeeOption === null,
1194
+ isUndefined: selectedFeeOption === undefined,
1195
+ type: typeof selectedFeeOption,
1196
+ stringified: JsonEncode(selectedFeeOption),
1069
1197
  },
1070
1198
  )
1071
1199
  // Wait for full send to complete
1072
1200
  const {
1073
1201
  depositUserTxnReceipt,
1074
- originMetaTxnReceipt,
1075
- destinationMetaTxnReceipt,
1202
+ originIntentTransaction,
1203
+ destinationIntentTransaction,
1076
1204
  } = await send({
1077
1205
  onOriginSend,
1078
- selectedFeeToken: selectedFeeToken ?? undefined, // Pass current value at execution time
1206
+ selectedFeeOption: selectedFeeOption ?? null, // Pass current value at execution time
1079
1207
  })
1080
1208
  logger.console.log("[trails-sdk] send() completed, receipts:", {
1081
1209
  depositUserTxnReceipt,
1082
- originMetaTxnReceipt,
1083
- destinationMetaTxnReceipt,
1210
+ originIntentTransaction,
1211
+ destinationIntentTransaction,
1084
1212
  })
1085
1213
 
1086
1214
  // Move to receipt screen
@@ -1136,7 +1264,8 @@ export function useSendForm({
1136
1264
  onError,
1137
1265
  fundMethod,
1138
1266
  onNavigateToMeshConnect,
1139
- selectedFeeToken, // Include so handleSend captures latest value
1267
+ selectedFeeOption, // Include so handleSend captures latest value
1268
+ buildQuoteInputsFingerprint,
1140
1269
  ])
1141
1270
 
1142
1271
  const handleSubmit = async (e: React.FormEvent) => {
@@ -1325,8 +1454,8 @@ export function useSendForm({
1325
1454
  setRecipientInput,
1326
1455
  setSelectedDestinationChain,
1327
1456
  setSelectedDestToken,
1328
- setSelectedFeeToken,
1329
- feeOptions,
1457
+ setSelectedFeeOption,
1458
+ processedFeeOptions,
1330
1459
  supportedTokens,
1331
1460
  supportedChains,
1332
1461
  ensAddress: ensAddress ?? null,
@@ -1336,7 +1465,7 @@ export function useSendForm({
1336
1465
  destTokenPrices: destTokenPrices ?? null,
1337
1466
  sourceTokenPrices: sourceTokenPrices ?? null,
1338
1467
  selectedToken,
1339
- selectedFeeToken: selectedFeeToken ?? null,
1468
+ selectedFeeOption: selectedFeeOption ?? null,
1340
1469
  setIsChainDropdownOpen,
1341
1470
  setIsTokenDropdownOpen,
1342
1471
  toAmountFormatted: quotedDestinationAmount,
@@ -15,7 +15,11 @@ import {
15
15
  useHasSufficientBalanceUsd,
16
16
  useTokenBalances,
17
17
  } from "../../tokenBalances.js"
18
- import { getFormatttedTokenName, useSupportedTokens } from "../../tokens.js"
18
+ import {
19
+ getFormatttedTokenName,
20
+ useSupportedTokens,
21
+ useGetTokenImageUrl,
22
+ } from "../../tokens.js"
19
23
  import { useIndexerGatewayClient } from "../../indexerClient.js"
20
24
  import { useTrailsClient } from "../../trailsClient.js"
21
25
  import { useTokenPrices } from "../../prices.js"
@@ -137,6 +141,7 @@ export function useTokenList({
137
141
  const { address } = useAccount()
138
142
  const indexerGatewayClient = useIndexerGatewayClient()
139
143
  const trailsClient = useTrailsClient()
144
+ const { getTokenImageUrl } = useGetTokenImageUrl()
140
145
  const {
141
146
  sortedTokens: allSortedTokens,
142
147
  isLoadingSortedTokens,
@@ -536,7 +541,14 @@ export function useTokenList({
536
541
  if (tokenSymbol === "WETH") {
537
542
  imageContractAddress = zeroAddress
538
543
  }
539
- const imageUrl = `https://assets.sequence.info/images/tokens/small/${token.chainId}/${imageContractAddress}.webp`
544
+ const fallbackImageUrl =
545
+ (token as TokenBalanceWithPrice).contractInfo?.logoURI || ""
546
+ const imageUrl = getTokenImageUrl({
547
+ chainId: token.chainId,
548
+ contractAddress: imageContractAddress,
549
+ symbol: tokenSymbol,
550
+ fallbackImageUrl: fallbackImageUrl || undefined,
551
+ })
540
552
  const currentTokenName =
541
553
  (token as TokenBalanceWithPrice).contractInfo?.name || ""
542
554
  const tokenName = getFormatttedTokenName(
@@ -771,6 +783,7 @@ export function useTokenList({
771
783
  supportedTokens,
772
784
  supportedTokenPrices,
773
785
  searchQuery,
786
+ getTokenImageUrl,
774
787
  ])
775
788
 
776
789
  const showInsufficientBalance = useMemo(() => {
@@ -5,3 +5,11 @@ export {
5
5
  createModalController,
6
6
  TrailsWidget,
7
7
  } from "./widget.js"
8
+
9
+ // Export config provider and hooks
10
+ export {
11
+ TrailsProvider,
12
+ useTrails,
13
+ type TrailsConfig,
14
+ type SequenceEnv,
15
+ } from "./providers/TrailsProvider.js"
@@ -0,0 +1,123 @@
1
+ import { SequenceHooksProvider } from "@0xsequence/hooks"
2
+ import { createContext, useContext, useMemo, useEffect } from "react"
3
+ import type { ComponentProps } from "react"
4
+ import {
5
+ DEFAULT_SLIPPAGE_TOLERANCE,
6
+ DEFAULT_WALLETCONNECT_PROJECT_ID,
7
+ PROD_SEQUENCE_INDEXER_URL,
8
+ PROD_SEQUENCE_NODE_GATEWAY_URL,
9
+ PROD_SEQUENCE_METADATA_URL,
10
+ PROD_TRAILS_API_URL,
11
+ } from "../../constants.js"
12
+ import {
13
+ declareCommitHash,
14
+ setGlobalDebug,
15
+ updateGlobalConfig,
16
+ } from "../../config.js"
17
+ import { logger } from "../../logger.js"
18
+ import { getQueryParam } from "../../queryParams.js"
19
+
20
+ export type SequenceEnv = "prod" | "dev" | "local" | "cors-anywhere"
21
+
22
+ export interface TrailsConfig {
23
+ trailsApiKey: string
24
+ trailsApiUrl: string
25
+ sequenceIndexerUrl: string
26
+ sequenceNodeGatewayUrl: string
27
+ sequenceMetadataUrl: string
28
+ walletConnectProjectId: string
29
+ slippageTolerance: string | number
30
+ debug: boolean
31
+ commitHash: string | undefined
32
+ }
33
+
34
+ export interface TrailsProviderProps {
35
+ children: React.ReactNode
36
+ config: {
37
+ trailsApiKey: string
38
+ trailsApiUrl?: string
39
+ sequenceIndexerUrl?: string
40
+ sequenceNodeGatewayUrl?: string
41
+ sequenceMetadataUrl?: string
42
+ walletConnectProjectId?: string
43
+ slippageTolerance?: string | number
44
+ debug?: boolean
45
+ }
46
+ }
47
+
48
+ export const TrailsContext = createContext<TrailsConfig | null>(null)
49
+
50
+ export const TrailsProvider = ({ children, config }: TrailsProviderProps) => {
51
+ // Populate the trails config with default values if not provided
52
+ const trailsConfig: TrailsConfig = useMemo(() => {
53
+ return {
54
+ trailsApiKey: config.trailsApiKey,
55
+ trailsApiUrl: config.trailsApiUrl || PROD_TRAILS_API_URL,
56
+ sequenceIndexerUrl:
57
+ config.sequenceIndexerUrl || PROD_SEQUENCE_INDEXER_URL,
58
+ sequenceNodeGatewayUrl:
59
+ config.sequenceNodeGatewayUrl || PROD_SEQUENCE_NODE_GATEWAY_URL,
60
+ sequenceMetadataUrl:
61
+ config.sequenceMetadataUrl || PROD_SEQUENCE_METADATA_URL,
62
+ walletConnectProjectId:
63
+ config.walletConnectProjectId || DEFAULT_WALLETCONNECT_PROJECT_ID,
64
+ slippageTolerance: config.slippageTolerance ?? DEFAULT_SLIPPAGE_TOLERANCE,
65
+ debug: (config.debug ?? false) || getQueryParam("debug") === "true",
66
+ commitHash: declareCommitHash(),
67
+ } satisfies TrailsConfig
68
+ }, [
69
+ config.trailsApiKey,
70
+ config.trailsApiUrl,
71
+ config.sequenceIndexerUrl,
72
+ config.sequenceMetadataUrl,
73
+ config.sequenceNodeGatewayUrl,
74
+ config.walletConnectProjectId,
75
+ config.slippageTolerance,
76
+ config.debug,
77
+ ])
78
+
79
+ // Set global debug mode for backward compatibility with getDebug()
80
+ useEffect(() => {
81
+ setGlobalDebug(trailsConfig.debug ?? false)
82
+ }, [trailsConfig.debug])
83
+
84
+ // Update global config when trails config changes (temporary solution)
85
+ useEffect(() => {
86
+ updateGlobalConfig({
87
+ trailsApiKey: trailsConfig.trailsApiKey,
88
+ sequenceNodeGatewayUrl: trailsConfig.sequenceNodeGatewayUrl,
89
+ })
90
+ }, [trailsConfig.trailsApiKey, trailsConfig.sequenceNodeGatewayUrl])
91
+
92
+ // Create a sequence hooks config based on the trails config
93
+ const sequenceHooksConfig = useMemo(() => {
94
+ return {
95
+ projectAccessKey: trailsConfig.trailsApiKey || "",
96
+ env: {
97
+ apiUrl: trailsConfig.trailsApiUrl,
98
+ indexerUrl: trailsConfig.sequenceIndexerUrl,
99
+ indexerGatewayUrl: trailsConfig.sequenceIndexerUrl,
100
+ },
101
+ } satisfies ComponentProps<typeof SequenceHooksProvider>["config"]
102
+ }, [trailsConfig])
103
+
104
+ logger.console.log("sequenceHooksConfig", sequenceHooksConfig)
105
+
106
+ return (
107
+ <TrailsContext.Provider value={trailsConfig}>
108
+ <SequenceHooksProvider config={sequenceHooksConfig}>
109
+ {children}
110
+ </SequenceHooksProvider>
111
+ </TrailsContext.Provider>
112
+ )
113
+ }
114
+
115
+ export const useTrails = (): Readonly<TrailsConfig> => {
116
+ const context = useContext(TrailsContext)
117
+
118
+ if (!context) {
119
+ throw new Error("useTrails must be used within a TrailsProvider")
120
+ }
121
+
122
+ return context
123
+ }