0xtrails 0.8.2 → 0.8.3

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 (68) hide show
  1. package/dist/aave.d.ts.map +1 -1
  2. package/dist/{ccip-ru_Yzdas.js → ccip-Bs-QcZXm.js} +13 -13
  3. package/dist/constants.d.ts +2 -0
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/fees.d.ts +11 -17
  6. package/dist/fees.d.ts.map +1 -1
  7. package/dist/{index-Si7cO9V7.js → index-C_EsqqSn.js} +20320 -20063
  8. package/dist/index.js +425 -847
  9. package/dist/intents.d.ts +1 -2
  10. package/dist/intents.d.ts.map +1 -1
  11. package/dist/prepareSend.d.ts.map +1 -1
  12. package/dist/recover.d.ts +8 -9
  13. package/dist/recover.d.ts.map +1 -1
  14. package/dist/tokenBalances.d.ts +51 -0
  15. package/dist/tokenBalances.d.ts.map +1 -1
  16. package/dist/trailsRouter.d.ts +15 -0
  17. package/dist/trailsRouter.d.ts.map +1 -1
  18. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts +1 -3
  19. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts.map +1 -1
  20. package/dist/transactionIntent/deposits/standardDeposit.d.ts +1 -3
  21. package/dist/transactionIntent/deposits/standardDeposit.d.ts.map +1 -1
  22. package/dist/transactionIntent/handlers/crossChain.d.ts +2 -4
  23. package/dist/transactionIntent/handlers/crossChain.d.ts.map +1 -1
  24. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts +5 -4
  25. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts.map +1 -1
  26. package/dist/transactionIntent/quote/normalizeQuote.d.ts +1 -1
  27. package/dist/transactionIntent/quote/normalizeQuote.d.ts.map +1 -1
  28. package/dist/transactionIntent/quote/quoteHelpers.d.ts +1 -1
  29. package/dist/transactionIntent/quote/quoteHelpers.d.ts.map +1 -1
  30. package/dist/transactionIntent/types.d.ts +11 -18
  31. package/dist/transactionIntent/types.d.ts.map +1 -1
  32. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  33. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  34. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  35. package/dist/widget/components/SlippageToleranceSettings.d.ts +2 -1
  36. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  37. package/dist/widget/css/compiled.css +1 -1
  38. package/dist/widget/hooks/useQuote.d.ts +94 -35
  39. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  40. package/dist/widget/hooks/useSendForm.d.ts +2 -2
  41. package/dist/widget/hooks/useSendForm.d.ts.map +1 -1
  42. package/dist/widget/hooks/useTrailsSendTransaction.d.ts.map +1 -1
  43. package/dist/widget/index.js +1 -1
  44. package/package.json +2 -2
  45. package/src/aave.ts +4 -0
  46. package/src/constants.ts +4 -0
  47. package/src/fees.ts +47 -72
  48. package/src/intents.ts +1 -3
  49. package/src/morpho.ts +1 -1
  50. package/src/prepareSend.ts +42 -6
  51. package/src/recover.ts +116 -172
  52. package/src/tokenBalances.ts +301 -1
  53. package/src/trailsRouter.ts +77 -0
  54. package/src/transactionIntent/deposits/depositOrchestrator.ts +0 -6
  55. package/src/transactionIntent/deposits/standardDeposit.ts +167 -184
  56. package/src/transactionIntent/handlers/crossChain.ts +8 -11
  57. package/src/transactionIntent/handlers/sameChainSameToken.ts +619 -608
  58. package/src/transactionIntent/quote/normalizeQuote.ts +32 -46
  59. package/src/transactionIntent/quote/quoteHelpers.ts +4 -2
  60. package/src/transactionIntent/types.ts +11 -18
  61. package/src/widget/compiled.css +1 -1
  62. package/src/widget/components/AccountIntentTransactionHistory.tsx +50 -18
  63. package/src/widget/components/ClassicSwap.tsx +25 -30
  64. package/src/widget/components/QuoteDetails.tsx +18 -27
  65. package/src/widget/components/SlippageToleranceSettings.tsx +55 -25
  66. package/src/widget/hooks/useQuote.ts +317 -79
  67. package/src/widget/hooks/useSendForm.ts +123 -764
  68. package/src/widget/hooks/useTrailsSendTransaction.ts +0 -2
@@ -8,24 +8,13 @@ import {
8
8
  isAddress,
9
9
  parseUnits,
10
10
  type WalletClient,
11
- zeroAddress,
12
11
  } from "viem"
13
12
  import { useAccount } from "wagmi"
14
- import {
15
- getChainInfo,
16
- useSupportedChains,
17
- useChainRpcClient,
18
- } from "../../chains.js"
19
- import { getFullErrorMessage, getPrettifiedErrorMessage } from "../../error.js"
20
- import {
21
- prepareSend,
22
- TradeType,
23
- type PrepareSendReturn,
24
- type PrepareSendQuote,
25
- } from "../../prepareSend.js"
13
+ import { getChainInfo, useSupportedChains } from "../../chains.js"
14
+ import { getFullErrorMessage } from "../../error.js"
15
+ import { TradeType, type PrepareSendQuote } from "../../prepareSend.js"
26
16
  import type { TransactionState } from "../../transactions.js"
27
- import { getTokenPrice, useTokenPrices, normalizeNumber } from "../../prices.js"
28
- import { useQueryParams } from "../../queryParams.js"
17
+ import { useTokenPrices, normalizeNumber } from "../../prices.js"
29
18
  import {
30
19
  formatRawAmount,
31
20
  formatUsdAmountDisplay,
@@ -42,9 +31,7 @@ import {
42
31
  } from "../../tokens.js"
43
32
  import type { CheckoutOnHandlers } from "./useCheckout.js"
44
33
  import { useResolveEnsAddress } from "../../ens.js"
45
- import { etherlink } from "viem/chains"
46
34
  import { logger } from "../../logger.js"
47
- import { getIsContract } from "../../contractUtils.js"
48
35
  import { useTrailsClient } from "../../trailsClient.js"
49
36
  import { useTrails } from "../providers/TrailsProvider.js"
50
37
  import { useTokenList } from "./useTokenList.js"
@@ -54,6 +41,7 @@ import {
54
41
  } from "./useSelectedFeeOption.js"
55
42
  import { useWidgetProps } from "./useWidgetProps.js"
56
43
  import { useCustomTokens } from "../../customTokens.js"
44
+ import { useQuote } from "./useQuote.js"
57
45
 
58
46
  type ChainInfo = {
59
47
  id: number
@@ -164,7 +152,7 @@ export function useSendForm({
164
152
  toChainId, // Custom specified destination chain id
165
153
  toToken, // Custom specified destination token address or symbol
166
154
  toCalldata, // Custom specified destination calldata
167
- refundAddress, // Custom specified refund address
155
+ refundAddress: _refundAddress, // Custom specified refund address (unused - useQuote handles this internally)
168
156
  walletClient,
169
157
  onTransactionStateChange,
170
158
  onError,
@@ -182,39 +170,15 @@ export function useSendForm({
182
170
  mode,
183
171
  onNavigateToOnramp,
184
172
  checkoutOnHandlers,
185
- refetchTrigger = 0,
173
+ refetchTrigger: _refetchTrigger = 0, // Unused - useQuote handles quote refreshing via React Query
186
174
  }: UseSendProps): UseSendReturn {
187
- // Get the active wallet ID from wagmi
188
- const { connector } = useAccount()
189
- const walletId = connector?.id
175
+ // Get the active wallet ID from wagmi (currently unused - useQuote handles this internally)
176
+ const { connector: _connector } = useAccount()
190
177
 
191
178
  // Read isSmartWallet from widget props
192
179
  const { isSmartWallet } = useWidgetProps()
193
180
 
194
- // Auto-set swapProvider and bridgeProvider to "lifi" if either from or to chain is etherlink
195
- const effectiveSwapProvider = useMemo(() => {
196
- if (!swapProvider || swapProvider === "auto") {
197
- if (
198
- selectedToken?.chainId === etherlink.id ||
199
- toChainId === etherlink.id
200
- ) {
201
- return "lifi"
202
- }
203
- }
204
- return swapProvider
205
- }, [swapProvider, selectedToken?.chainId, toChainId])
206
-
207
- const effectiveBridgeProvider = useMemo(() => {
208
- if (!bridgeProvider || bridgeProvider === "auto") {
209
- if (
210
- selectedToken?.chainId === etherlink.id ||
211
- toChainId === etherlink.id
212
- ) {
213
- return "lifi"
214
- }
215
- }
216
- return bridgeProvider
217
- }, [bridgeProvider, selectedToken?.chainId, toChainId])
181
+ // Note: swapProvider and bridgeProvider auto-detection for etherlink is now handled in useQuote
218
182
 
219
183
  const [amount, setAmount] = useState(
220
184
  tradeType === TradeType.EXACT_INPUT ? "" : (toAmount ?? ""),
@@ -222,10 +186,6 @@ export function useSendForm({
222
186
  const [recipientInput, setRecipientInput] = useState(toRecipient ?? "")
223
187
  const [recipient, setRecipient] = useState(toRecipient ?? "")
224
188
  const [error, setError] = useState<string | null>(null)
225
- const [quoteError, setQuoteError] = useState<string | null>(null)
226
- const [quoteErrorPrettified, setQuoteErrorPrettified] = useState<
227
- string | null
228
- >(null)
229
189
  const { supportedChains } = useSupportedChains()
230
190
  const { ensAddress } = useResolveEnsAddress({
231
191
  textInput: recipientInput,
@@ -282,115 +242,7 @@ export function useSendForm({
282
242
  refetchOnReconnect: false,
283
243
  })
284
244
 
285
- // Get RPC clients for known chainIds using hooks (reads from TrailsProvider context)
286
- const destinationChainPublicClient = useChainRpcClient(
287
- selectedDestinationChain?.id,
288
- )
289
- const originChainPublicClient = useChainRpcClient(selectedToken?.chainId)
290
-
291
- // Check if recipient is a contract address
292
- useEffect(() => {
293
- const checkRecipientContract = async () => {
294
- if (
295
- recipient &&
296
- isAddress(recipient) &&
297
- selectedDestinationChain?.id &&
298
- destinationChainPublicClient
299
- ) {
300
- try {
301
- const isContract = await getIsContract(
302
- recipient as `0x${string}`,
303
- selectedDestinationChain.id,
304
- destinationChainPublicClient,
305
- )
306
- logger.console.log("[trails-sdk] isRecipientContract:", isContract)
307
- setIsRecipientContract(isContract)
308
- } catch (error) {
309
- logger.console.error(
310
- "[trails-sdk] Error checking if recipient is contract:",
311
- error,
312
- )
313
- setIsRecipientContract(false)
314
- }
315
- } else {
316
- setIsRecipientContract(false)
317
- }
318
- }
319
-
320
- checkRecipientContract()
321
- }, [recipient, selectedDestinationChain?.id, destinationChainPublicClient])
322
-
323
- // Check if sender is a contract address on origin chain
324
- useEffect(() => {
325
- const checkSenderContractOnOrigin = async () => {
326
- if (
327
- account?.address &&
328
- selectedToken?.chainId &&
329
- originChainPublicClient
330
- ) {
331
- try {
332
- const isContract = await getIsContract(
333
- account.address as `0x${string}`,
334
- selectedToken.chainId,
335
- originChainPublicClient,
336
- )
337
- logger.console.log(
338
- "[trails-sdk] isSenderContractOnOrigin:",
339
- isContract,
340
- )
341
- setIsSenderContractOnOrigin(isContract)
342
- } catch (error) {
343
- logger.console.error(
344
- "[trails-sdk] Error checking if sender is contract on origin:",
345
- error,
346
- )
347
- setIsSenderContractOnOrigin(false)
348
- }
349
- } else {
350
- setIsSenderContractOnOrigin(false)
351
- }
352
- }
353
-
354
- checkSenderContractOnOrigin()
355
- }, [account?.address, selectedToken?.chainId, originChainPublicClient])
356
-
357
- // Check if sender is a contract address on destination chain
358
- useEffect(() => {
359
- const checkSenderContractOnDestination = async () => {
360
- if (
361
- account?.address &&
362
- selectedDestinationChain?.id &&
363
- destinationChainPublicClient
364
- ) {
365
- try {
366
- const isContract = await getIsContract(
367
- account.address as `0x${string}`,
368
- selectedDestinationChain.id,
369
- destinationChainPublicClient,
370
- )
371
- logger.console.log(
372
- "[trails-sdk] isSenderContractOnDestination:",
373
- isContract,
374
- )
375
- setIsSenderContractOnDestination(isContract)
376
- } catch (error) {
377
- logger.console.error(
378
- "[trails-sdk] Error checking if sender is contract on destination:",
379
- error,
380
- )
381
- setIsSenderContractOnDestination(false)
382
- }
383
- } else {
384
- setIsSenderContractOnDestination(false)
385
- }
386
- }
387
-
388
- checkSenderContractOnDestination()
389
- }, [
390
- account?.address,
391
- selectedDestinationChain?.id,
392
- destinationChainPublicClient,
393
- ])
245
+ // Note: Contract detection is now handled in useQuote
394
246
 
395
247
  const isCustomToken = useMemo(() => toToken?.startsWith("0x"), [toToken])
396
248
 
@@ -538,8 +390,12 @@ export function useSendForm({
538
390
  }
539
391
  }, [selectedDestToken, defaultDestToken])
540
392
 
541
- const trailsClient = useTrailsClient()
542
- const { trailsApiKey, sequenceIndexerUrl } = useTrails()
393
+ // These are handled internally by useQuote via TrailsProvider context
394
+ const _trailsClient = useTrailsClient()
395
+ const {
396
+ trailsApiKey: _trailsApiKey,
397
+ sequenceIndexerUrl: _sequenceIndexerUrl,
398
+ } = useTrails()
543
399
 
544
400
  // Get user's token balances for balance checking
545
401
  const { filteredTokensFormatted } = useTokenList({
@@ -645,12 +501,6 @@ export function useSendForm({
645
501
  const [isSubmitting, setIsSubmitting] = useState(false)
646
502
  const [isWaitingForWalletConfirm, setIsWaitingForWalletConfirm] =
647
503
  useState(false)
648
- const [isLoadingQuote, setIsLoadingQuote] = useState(false)
649
- const [prepareSendResult, setPrepareSendResult] =
650
- useState<PrepareSendReturn | null>(null)
651
- const latestResolvedIntentIdRef = useRef<string | null>(null)
652
- const quoteInputsFingerprintRef = useRef<string | null>(null)
653
- const preparedQuoteFingerprintRef = useRef<string | null>(null)
654
504
 
655
505
  // Create a stable callback for transaction state changes
656
506
  const handleTransactionStateChange = useCallback(
@@ -696,14 +546,8 @@ export function useSendForm({
696
546
  setFeeOptions,
697
547
  } = useSelectedFeeOption()
698
548
 
699
- const [isRecipientContract, setIsRecipientContract] = useState(false)
700
- const [isSenderContractOnOrigin, setIsSenderContractOnOrigin] =
701
- useState(false)
702
- const [isSenderContractOnDestination, setIsSenderContractOnDestination] =
703
- useState(false)
704
-
705
- const { hasParam } = useQueryParams()
706
- const isDryMode = hasParam("dryMode", "true")
549
+ // Note: Contract detection (isRecipientContract, isSenderContractOnOrigin, isSenderContractOnDestination)
550
+ // is now handled in useQuote and returned from there
707
551
 
708
552
  const destinationTokenAddressFromTokenSymbol = useTokenAddress({
709
553
  chainId: selectedDestinationChain?.id,
@@ -765,24 +609,31 @@ export function useSendForm({
765
609
  return "0"
766
610
  }
767
611
 
768
- if (tradeType === TradeType.EXACT_INPUT && !selectedToken?.decimals) {
769
- logger.console.warn(
770
- "[trails-sdk] Missing source token decimals for quote",
771
- {
772
- selectedToken,
773
- tradeType,
774
- },
775
- )
776
- return "0"
777
- } else if (!selectedDestToken?.decimals) {
778
- logger.console.warn(
779
- "[trails-sdk] Missing destination token decimals for quote",
780
- {
781
- selectedDestToken,
782
- tradeType,
783
- },
784
- )
785
- return "0"
612
+ // For EXACT_INPUT: we only need source token decimals (user enters source amount)
613
+ // For EXACT_OUTPUT: we only need destination token decimals (user enters destination amount)
614
+ if (tradeType === TradeType.EXACT_INPUT) {
615
+ if (!selectedToken?.decimals) {
616
+ logger.console.warn(
617
+ "[trails-sdk] Missing source token decimals for quote",
618
+ {
619
+ selectedToken,
620
+ tradeType,
621
+ },
622
+ )
623
+ return "0"
624
+ }
625
+ } else {
626
+ // EXACT_OUTPUT mode
627
+ if (!selectedDestToken?.decimals) {
628
+ logger.console.warn(
629
+ "[trails-sdk] Missing destination token decimals for quote",
630
+ {
631
+ selectedDestToken,
632
+ tradeType,
633
+ },
634
+ )
635
+ return "0"
636
+ }
786
637
  }
787
638
 
788
639
  // For EXACT_INPUT: use source token decimals (user enters source amount)
@@ -816,522 +667,76 @@ export function useSendForm({
816
667
  tradeType,
817
668
  ])
818
669
 
819
- const buildQuoteInputsFingerprint = useCallback(
820
- () =>
821
- JSON.stringify({
822
- originTokenAddress: selectedToken?.contractAddress?.toLowerCase() ?? "",
823
- originChainId: selectedToken?.chainId ?? "",
824
- destinationTokenSymbol: selectedDestToken?.symbol ?? "",
825
- destinationChainId: selectedDestinationChain?.id ?? "",
826
- destinationTokenAddress: destinationTokenAddress?.toLowerCase() ?? "",
827
- amount: amount || "",
828
- amountRaw: amountRaw || "",
829
- recipient: recipient?.toLowerCase() ?? "",
830
- tradeType,
831
- toCalldata: toCalldata ?? "",
832
- }),
833
- [
834
- amount,
835
- amountRaw,
836
- destinationTokenAddress,
837
- recipient,
838
- selectedDestinationChain?.id,
839
- selectedDestToken?.symbol,
840
- selectedToken?.chainId,
841
- selectedToken?.contractAddress,
842
- toCalldata,
843
- tradeType,
844
- ],
845
- )
670
+ // Determine paymaster URL for the origin chain
671
+ const paymasterUrl = useMemo(() => {
672
+ return paymasterUrls?.find(
673
+ (p) => p.chainId.toString() === (selectedToken?.chainId || 0).toString(),
674
+ )?.url
675
+ }, [paymasterUrls, selectedToken?.chainId])
846
676
 
847
- // Get quote automatically when inputs change
848
- const getQuote = useCallback(async () => {
849
- // Debug log: comprehensive state check
850
- const debugState = {
851
- account: account ? { address: account.address } : null,
852
- amount,
853
- amountRaw,
854
- destinationTokenAddress,
855
- destinationTokenAddressFromTokenSymbol,
856
- isValidRecipient,
857
- selectedDestToken: selectedDestToken
858
- ? {
859
- symbol: selectedDestToken.symbol,
860
- name: selectedDestToken.name,
861
- decimals: selectedDestToken.decimals,
862
- }
863
- : null,
864
- selectedDestinationChain: selectedDestinationChain
865
- ? {
866
- id: selectedDestinationChain.id,
867
- name: selectedDestinationChain.name,
868
- }
869
- : null,
870
- selectedToken: selectedToken
871
- ? {
872
- symbol: selectedToken.symbol,
873
- contractAddress: selectedToken.contractAddress,
874
- chainId: selectedToken.chainId || 0,
875
- }
876
- : null,
877
- recipient,
878
- conditions: {
879
- hasAccount: !!account,
880
- hasAmount: !!amount,
881
- hasAmountRaw: !!amountRaw,
882
- amountNotZero: amount !== "0",
883
- amountRawNotZero: amountRaw !== "0",
884
- hasDestinationTokenAddress: !!destinationTokenAddress,
885
- hasDestinationTokenAddressFromSymbol:
886
- !!destinationTokenAddressFromTokenSymbol,
887
- isValidRecipientValue: isValidRecipient,
888
- hasSelectedDestToken: !!selectedDestToken,
889
- hasSelectedDestinationChain: !!selectedDestinationChain,
890
- hasSelectedToken: !!selectedToken,
891
- },
892
- allConditionsMet:
893
- !!account &&
894
- !!amount &&
895
- !!destinationTokenAddress &&
896
- isValidRecipient &&
897
- !!selectedDestToken &&
898
- !!selectedDestinationChain &&
899
- amount !== "0" &&
900
- !!amountRaw &&
901
- amountRaw !== "0" &&
902
- !!selectedToken,
903
- }
904
-
905
- logger.console.log("[trails-sdk] [DEBUG] getQuote state check", debugState)
906
-
907
- // Only get quote if all required inputs are present
908
- // Skip quote if origin and destination tokens are the same
909
- const isSameToken =
910
- selectedToken?.chainId === selectedDestinationChain?.id &&
911
- selectedToken?.contractAddress?.toLowerCase() ===
912
- destinationTokenAddress?.toLowerCase()
913
- const isSenderSameAsRecipient =
914
- account?.address?.toLowerCase() === recipient?.toLowerCase()
915
-
916
- if (
917
- !account ||
918
- !amount ||
919
- !destinationTokenAddress ||
920
- !isValidRecipient ||
921
- !selectedDestToken ||
922
- !selectedDestinationChain ||
923
- amount === "0" ||
924
- !amountRaw ||
925
- amountRaw === "0" ||
926
- !selectedToken ||
927
- (isSameToken &&
928
- isSenderSameAsRecipient &&
929
- (mode === "swap" || mode === "fund"))
930
- ) {
931
- logger.console.log(
932
- "[trails-sdk] Skipping quote because of missing inputs or same token",
933
- {
934
- amount,
935
- destinationTokenAddress,
936
- isValidRecipient,
937
- selectedDestToken,
938
- selectedDestinationChain,
939
- amountRaw,
940
- selectedToken,
941
- isSameToken,
942
- debugState,
943
- },
944
- )
945
- setQuoteError(null)
946
- setPrepareSendResult(null)
947
- preparedQuoteFingerprintRef.current = null
948
- return
949
- }
950
-
951
- latestResolvedIntentIdRef.current = null
952
- let requestFingerprint = quoteInputsFingerprintRef.current
953
- if (!requestFingerprint) {
954
- requestFingerprint = buildQuoteInputsFingerprint()
955
- quoteInputsFingerprintRef.current = requestFingerprint
956
- }
957
- try {
958
- setIsLoadingQuote(true)
959
- setError(null)
960
- setQuoteError(null)
961
-
962
- const sourceTokenDecimals = selectedToken.decimals || 18
963
- const destinationTokenDecimals = selectedDestToken.decimals || 18
964
-
965
- if (!sourceTokenDecimals || !destinationTokenDecimals) {
966
- logger.console.warn("[trails-sdk] Missing token decimals for quote", {
967
- sourceTokenDecimals,
968
- destinationTokenDecimals,
969
- selectedToken,
970
- selectedDestToken,
971
- tradeType,
972
- })
973
- setPrepareSendResult(null)
974
- preparedQuoteFingerprintRef.current = null
975
- latestResolvedIntentIdRef.current = null
976
- setIsLoadingQuote(false)
977
- return
978
- }
979
-
980
- let sourceTokenPriceUsd = selectedToken.priceUsd ?? null
981
- let destinationTokenPriceUsd = destTokenPrices?.[0]?.priceUsd ?? null
982
-
983
- if (!sourceTokenPriceUsd) {
984
- try {
985
- const price = await getTokenPrice(trailsClient, {
986
- tokenAddress: selectedToken.contractAddress,
987
- tokenSymbol: selectedToken.symbol,
988
- chainId: selectedToken.chainId || 0,
989
- })
990
- sourceTokenPriceUsd = price?.priceUsd ?? null
991
- } catch (error) {
992
- logger.console.error(
993
- "[trails-sdk] Error getting source token price:",
994
- error,
995
- )
996
- }
997
- }
998
-
999
- if (!destinationTokenPriceUsd) {
1000
- try {
1001
- const price = await getTokenPrice(trailsClient, {
1002
- tokenSymbol: selectedDestToken.symbol,
1003
- tokenAddress: destinationTokenAddress ?? "",
1004
- chainId: selectedDestinationChain.id,
1005
- })
1006
- destinationTokenPriceUsd = price?.priceUsd ?? null
1007
- } catch (error) {
1008
- logger.console.error(
1009
- "[trails-sdk] Error getting destination token price:",
1010
- error,
1011
- )
1012
- }
1013
- }
1014
-
1015
- if (
1016
- !destinationTokenPriceUsd &&
1017
- selectedToken.symbol === selectedDestToken.symbol
1018
- ) {
1019
- destinationTokenPriceUsd = sourceTokenPriceUsd
1020
- }
1021
- if (
1022
- !sourceTokenPriceUsd &&
1023
- selectedToken.symbol === selectedDestToken.symbol
1024
- ) {
1025
- sourceTokenPriceUsd = destinationTokenPriceUsd
1026
- }
1027
-
1028
- if (!sourceTokenPriceUsd || !destinationTokenPriceUsd) {
1029
- logger.console.warn("[trails-sdk] Missing token prices for quote", {
1030
- sourceTokenPriceUsd,
1031
- destinationTokenPriceUsd,
1032
- })
1033
- }
1034
-
1035
- let nativeTokenPriceUsd = 0
1036
- if (
1037
- selectedToken.contractAddress === zeroAddress &&
1038
- sourceTokenPriceUsd
1039
- ) {
1040
- nativeTokenPriceUsd = sourceTokenPriceUsd
1041
- } else {
1042
- const originChain = getChainInfo(selectedToken.chainId || 0)
1043
- const nativeTokenSymbol = originChain?.nativeCurrency?.symbol ?? ""
1044
- const nativePrice = await getTokenPrice(trailsClient, {
1045
- tokenSymbol: nativeTokenSymbol,
1046
- tokenAddress: zeroAddress,
1047
- chainId: selectedToken.chainId || 0,
1048
- })
1049
- nativeTokenPriceUsd = nativePrice?.priceUsd ?? 0
1050
- }
1051
-
1052
- const options = {
1053
- account,
1054
- originTokenAddress: selectedToken.contractAddress,
1055
- originChainId: selectedToken.chainId || 0,
1056
- originTokenBalance:
1057
- fundMethod === "qr-code" || fundMethod === "exchange"
1058
- ? parseUnits("100", selectedToken.decimals ?? 18).toString() // needs to be an amount that is greater than the minimum amount for the swap
1059
- : selectedToken.balance || "0",
1060
- destinationChainId: selectedDestinationChain.id,
1061
- recipient,
1062
- destinationTokenAddress,
1063
- swapAmount: amountRaw,
1064
- tradeType,
1065
- originTokenSymbol: selectedToken.symbol,
1066
- destinationTokenSymbol: selectedDestToken.symbol,
1067
- fee: "0",
1068
- client: walletClient,
1069
- trailsClient,
1070
- destinationCalldata: toCalldata,
1071
- refundAddress,
1072
- dryMode: isDryMode,
1073
- onTransactionStateChange: handleTransactionStateChange,
1074
- sourceTokenPriceUsd,
1075
- destinationTokenPriceUsd,
1076
- sourceTokenDecimals,
1077
- destinationTokenDecimals,
1078
- paymasterUrl:
1079
- paymasterUrls?.find(
1080
- (p) =>
1081
- p.chainId.toString() === (selectedToken.chainId || 0).toString(),
1082
- )?.url ?? undefined,
1083
- originNativeTokenPriceUsd: nativeTokenPriceUsd,
1084
- swapProvider: effectiveSwapProvider as RouteProvider | null | undefined,
1085
- bridgeProvider: effectiveBridgeProvider as
1086
- | RouteProvider
1087
- | null
1088
- | undefined,
1089
- mode,
1090
- fundMethod,
1091
- checkoutOnHandlers,
1092
- selectedFeeOption: selectedFeeOption ?? null,
1093
- walletId,
1094
- sequenceIndexerUrl,
1095
- sequenceProjectAccessKey: trailsApiKey,
1096
- originPublicClient: originChainPublicClient ?? undefined,
1097
- destinationPublicClient: destinationChainPublicClient ?? undefined,
1098
- isSmartWallet,
1099
- trailsApiKey,
1100
- trailsApiUrl: trailsConfig.trailsApiUrl,
1101
- }
1102
-
1103
- logger.console.log(
1104
- "[trails-sdk] [FEE-SELECT] getQuote using selectedFeeOption:",
1105
- {
1106
- selectedFeeOption,
1107
- },
1108
- )
1109
-
1110
- const result = await prepareSend(options)
1111
-
1112
- if (requestFingerprint !== quoteInputsFingerprintRef.current) {
1113
- logger.console.log(
1114
- "[trails-sdk] Ignoring stale quote result (fingerprint mismatch)",
1115
- {
1116
- requestFingerprint,
1117
- currentFingerprint: quoteInputsFingerprintRef.current,
1118
- },
1119
- )
1120
- return
1121
- }
1122
-
1123
- logger.console.log("[trails-sdk] prepareSend quote:", result.quote)
1124
-
1125
- // Track the intentId returned by the most recent quote so we never execute a stale CommitIntent
1126
- // even when the quote inputs (fingerprint) are identical between retries.
1127
- const quoteIntentId = result.quote.intentId ?? null
1128
-
1129
- logger.console.log("[trails-sdk] Resolved quote intentId:", quoteIntentId)
1130
-
1131
- latestResolvedIntentIdRef.current = quoteIntentId
1132
- setPrepareSendResult(result)
1133
- preparedQuoteFingerprintRef.current = requestFingerprint
1134
- setIsLoadingQuote(false)
1135
- } catch (error) {
1136
- if (requestFingerprint !== quoteInputsFingerprintRef.current) {
1137
- logger.console.log(
1138
- "[trails-sdk] Ignoring quote error (fingerprint mismatch)",
1139
- {
1140
- requestFingerprint,
1141
- currentFingerprint: quoteInputsFingerprintRef.current,
1142
- },
1143
- )
1144
- return
1145
- }
1146
-
1147
- logger.console.error("[trails-sdk] Error getting quote:", error)
1148
- const errorMessage = getFullErrorMessage(error)
1149
- setQuoteError(errorMessage)
1150
- setPrepareSendResult(null)
1151
- preparedQuoteFingerprintRef.current = null
1152
- latestResolvedIntentIdRef.current = null
1153
- setIsLoadingQuote(false)
1154
- }
1155
- }, [
1156
- trailsApiKey,
1157
- tradeType,
1158
- isDryMode,
1159
- account,
677
+ // Use the centralized useQuote hook for quote fetching
678
+ const {
679
+ quote: quoteResult,
680
+ send: sendFn,
681
+ isLoadingQuote: quoteIsLoading,
682
+ quoteError: rawQuoteError,
683
+ quoteErrorPrettified: rawQuoteErrorPrettified,
684
+ feeOptions: quoteFeeOptions,
685
+ // Contract detection from useQuote
686
+ isRecipientContract: quoteIsRecipientContract,
687
+ isSenderContractOnOrigin: quoteIsSenderContractOnOrigin,
688
+ isSenderContractOnDestination: quoteIsSenderContractOnDestination,
689
+ } = useQuote({
1160
690
  walletClient,
1161
- trailsClient,
1162
- selectedDestToken?.decimals,
1163
- recipient,
1164
- destinationTokenAddress,
1165
- destinationTokenAddressFromTokenSymbol,
1166
- sequenceIndexerUrl,
1167
- selectedDestToken?.symbol,
1168
- selectedDestinationChain?.id,
1169
- selectedToken?.contractAddress,
1170
- selectedToken?.chainId,
1171
- selectedToken?.balance,
1172
- selectedToken?.priceUsd,
691
+ fromTokenAddress: selectedToken?.contractAddress ?? null,
692
+ fromChainId: selectedToken?.chainId ?? null,
693
+ toTokenAddress: destinationTokenAddress ?? null,
694
+ toChainId: selectedDestinationChain?.id ?? null,
695
+ swapAmount: amountRaw !== "0" ? amountRaw : undefined,
696
+ tradeType,
697
+ toAddress: isValidRecipient ? recipient : null,
1173
698
  toCalldata,
1174
- refundAddress,
1175
- paymasterUrls,
1176
- handleTransactionStateChange,
1177
- isValidRecipient,
1178
- destTokenPrices?.[0]?.priceUsd,
1179
- amount,
1180
- selectedDestToken,
1181
- selectedDestinationChain,
1182
- selectedToken,
1183
- effectiveSwapProvider,
1184
- effectiveBridgeProvider,
1185
- fundMethod,
1186
- amountRaw,
699
+ onStatusUpdate: handleTransactionStateChange,
700
+ swapProvider: swapProvider as RouteProvider | null | undefined,
701
+ bridgeProvider: bridgeProvider as RouteProvider | null | undefined,
1187
702
  checkoutOnHandlers,
1188
- mode,
1189
- selectedFeeOption,
1190
- walletId,
1191
- buildQuoteInputsFingerprint,
1192
- originChainPublicClient,
1193
- destinationChainPublicClient,
703
+ paymasterUrl,
704
+ selectedFeeOption: selectedFeeOption ?? null,
1194
705
  isSmartWallet,
1195
- trailsConfig.trailsApiUrl,
1196
- ])
706
+ fundMethod,
707
+ })
1197
708
 
1198
- // Auto-fetch quotes when inputs change (debounced)
1199
- // biome-ignore lint/correctness/useExhaustiveDependencies: getQuote is intentionally excluded to prevent infinite loop
1200
- useEffect(() => {
1201
- // Only trigger if we have the essential inputs
1202
- // Block empty string or exactly "0"
1203
- // Skip quote if origin and destination tokens are the same
1204
- const isSameToken =
1205
- selectedToken?.chainId === selectedDestinationChain?.id &&
1206
- selectedToken?.contractAddress?.toLowerCase() ===
1207
- destinationTokenAddress?.toLowerCase()
1208
-
1209
- const isSenderSameAsRecipient =
1210
- account?.address?.toLowerCase() === recipient?.toLowerCase()
1211
-
1212
- const conditions = {
1213
- hasAmount: !!amount,
1214
- amountNotZero: amount !== "0",
1215
- hasDestinationTokenAddress: !!destinationTokenAddress,
1216
- isValidRecipient,
1217
- hasSelectedDestTokenSymbol: !!selectedDestToken?.symbol,
1218
- hasSelectedDestinationChain: !!selectedDestinationChain?.id,
1219
- hasSelectedToken: !!selectedToken,
1220
- isSameToken,
1221
- isSenderSameAsRecipient,
1222
- mode,
1223
- }
709
+ // Map useQuote outputs for backward compatibility with existing code
710
+ // This allows gradual migration without breaking existing functionality
711
+ const isLoadingQuote = quoteIsLoading
712
+ const quoteError = rawQuoteError ? getFullErrorMessage(rawQuoteError) : null
713
+ const quoteErrorPrettified = rawQuoteErrorPrettified || null
1224
714
 
1225
- logger.console.log(
1226
- "[trails-sdk] [CUSTOM-TOKEN] Quote trigger conditions check:",
1227
- {
1228
- ...conditions,
1229
- selectedDestToken: selectedDestToken
1230
- ? {
1231
- symbol: selectedDestToken.symbol,
1232
- decimals: selectedDestToken.decimals,
1233
- contractAddress: selectedDestToken.contractAddress,
1234
- }
1235
- : null,
1236
- destinationTokenAddress,
1237
- amount,
1238
- recipient,
1239
- },
1240
- )
715
+ // Create a compatibility layer for prepareSendResult
716
+ // This maps the useQuote output to the shape expected by existing code
717
+ const prepareSendResult = useMemo(() => {
718
+ if (!quoteResult || !sendFn) return null
1241
719
 
1242
- if (
1243
- !amount ||
1244
- amount === "0" ||
1245
- !destinationTokenAddress ||
1246
- !isValidRecipient ||
1247
- !selectedDestToken?.symbol ||
1248
- !selectedDestinationChain?.id ||
1249
- !selectedToken ||
1250
- (isSameToken &&
1251
- isSenderSameAsRecipient &&
1252
- (mode === "swap" || mode === "fund"))
1253
- ) {
1254
- logger.console.log(
1255
- "[trails-sdk] [CUSTOM-TOKEN] Quote blocked - missing conditions:",
1256
- {
1257
- blockedReasons: {
1258
- noAmount: !amount || amount === "0",
1259
- noDestinationTokenAddress: !destinationTokenAddress,
1260
- invalidRecipient: !isValidRecipient,
1261
- noSelectedDestTokenSymbol: !selectedDestToken?.symbol,
1262
- noSelectedDestinationChain: !selectedDestinationChain?.id,
1263
- noSelectedToken: !selectedToken,
1264
- sameTokenAndRecipient:
1265
- isSameToken &&
1266
- isSenderSameAsRecipient &&
1267
- (mode === "swap" || mode === "fund"),
1268
- },
1269
- conditions,
1270
- },
1271
- )
1272
- setPrepareSendResult(null)
1273
- latestResolvedIntentIdRef.current = null
1274
- quoteInputsFingerprintRef.current = null
1275
- preparedQuoteFingerprintRef.current = null
1276
- setIsLoadingQuote(false)
1277
- return
1278
- }
720
+ // Quote type now has all PrepareSendQuote fields with proper defaults
721
+ const quote = { ...quoteResult } as PrepareSendQuote
1279
722
 
1280
- const nextQuoteInputsFingerprint = buildQuoteInputsFingerprint()
1281
-
1282
- logger.console.log("[trails-sdk] [CUSTOM-TOKEN] Quote fingerprint check:", {
1283
- currentFingerprint: quoteInputsFingerprintRef.current,
1284
- nextFingerprint: nextQuoteInputsFingerprint,
1285
- willTrigger:
1286
- quoteInputsFingerprintRef.current !== nextQuoteInputsFingerprint,
1287
- })
1288
-
1289
- // Fingerprint all quote-driving inputs so we can invalidate debounced quotes immediately when
1290
- // the user switches tokens; this fixes the stale quote so that it stays loading until the new quote is ready.
1291
- if (quoteInputsFingerprintRef.current !== nextQuoteInputsFingerprint) {
1292
- logger.console.log(
1293
- "[trails-sdk] [CUSTOM-TOKEN] Quote fingerprint changed, triggering quote fetch",
1294
- )
1295
- quoteInputsFingerprintRef.current = nextQuoteInputsFingerprint
1296
- latestResolvedIntentIdRef.current = null
1297
- setPrepareSendResult(null)
1298
- preparedQuoteFingerprintRef.current = null
1299
- setIsLoadingQuote(true)
723
+ return {
724
+ quote,
725
+ feeOptions: { feeOptions: quoteFeeOptions },
726
+ // Note: send function is handled separately via sendFn
1300
727
  }
1301
-
1302
- const timeoutId = setTimeout(() => {
1303
- logger.console.log(
1304
- "[trails-sdk] [CUSTOM-TOKEN] Debounce timeout completed, calling getQuote",
1305
- )
1306
- getQuote()
1307
- }, 500) // Debounce by 500ms
1308
-
1309
- return () => clearTimeout(timeoutId)
1310
- }, [
1311
- amount,
1312
- destinationTokenAddress,
1313
- isValidRecipient,
1314
- selectedDestToken?.symbol,
1315
- selectedDestinationChain?.id,
1316
- toCalldata,
1317
- refetchTrigger,
1318
- amountRaw,
1319
- tradeType,
1320
- selectedToken?.contractAddress,
1321
- selectedToken?.chainId,
1322
- selectedToken?.balance,
1323
- selectedToken?.priceUsd,
1324
- recipient, // Add recipient to trigger quote re-fetch when it changes
1325
- // selectedFeeOption is passed to send() at execution time, not needed here
1326
- buildQuoteInputsFingerprint,
1327
- ])
728
+ }, [quoteResult, sendFn, quoteFeeOptions])
1328
729
 
1329
730
  // Calculate destination amount from quote if available
1330
731
  const quotedDestinationAmount = useMemo(() => {
1331
- if (prepareSendResult) {
1332
- return prepareSendResult.quote.destinationAmountFormatted
732
+ const currentDestAmount =
733
+ prepareSendResult?.quote?.destinationAmountFormatted
734
+
735
+ if (currentDestAmount && currentDestAmount !== "0") {
736
+ return currentDestAmount
1333
737
  }
1334
738
 
739
+ // Fall back to toAmount prop (for EXACT_OUTPUT mode)
1335
740
  return toAmountFormatted
1336
741
  }, [prepareSendResult, toAmountFormatted])
1337
742
 
@@ -1370,58 +775,24 @@ export function useSendForm({
1370
775
  logger.console.log("[trails-sdk] processSend called", {
1371
776
  fundMethod,
1372
777
  hasOnNavigateToOnramp: !!onNavigateToOnramp,
1373
- hasPrepareSendResult: !!prepareSendResult,
778
+ hasQuote: !!prepareSendResult,
779
+ hasSend: !!sendFn,
1374
780
  })
1375
781
  try {
1376
- if (!prepareSendResult) {
782
+ if (!prepareSendResult || !sendFn) {
1377
783
  setError("No quote available. Please wait for quote to load.")
1378
784
  return
1379
785
  }
1380
786
 
1381
- const currentQuoteInputsFingerprint = buildQuoteInputsFingerprint()
1382
- if (
1383
- !preparedQuoteFingerprintRef.current ||
1384
- preparedQuoteFingerprintRef.current !== currentQuoteInputsFingerprint
1385
- ) {
1386
- logger.console.warn(
1387
- "[trails-sdk] Blocking send because quote fingerprint is stale",
1388
- {
1389
- prepared: preparedQuoteFingerprintRef.current,
1390
- current: currentQuoteInputsFingerprint,
1391
- },
1392
- )
1393
- setPrepareSendResult(null)
1394
- preparedQuoteFingerprintRef.current = null
1395
- latestResolvedIntentIdRef.current = null
1396
- setIsLoadingQuote(true)
1397
- return
1398
- }
1399
-
1400
- const quoteIntentId = prepareSendResult.quote.intentId ?? null
1401
- if (
1402
- quoteIntentId &&
1403
- latestResolvedIntentIdRef.current !== quoteIntentId
1404
- ) {
1405
- logger.console.error(
1406
- "[trails-sdk] Quote intentId has changed, waiting for latest quote",
1407
- {
1408
- current: latestResolvedIntentIdRef.current,
1409
- prepareSend: quoteIntentId,
1410
- },
1411
- )
1412
- setError("Quote is updating. Please wait for the latest quote.")
1413
- return
1414
- }
787
+ // React Query handles quote staleness via its caching mechanism,
788
+ // so we no longer need fingerprint checks
1415
789
 
1416
790
  setError(null)
1417
791
  setIsSubmitting(true)
1418
792
 
1419
- const { quote, send } = prepareSendResult
793
+ const quote = prepareSendResult.quote
1420
794
 
1421
- logger.console.log(
1422
- "[trails-sdk] Using prepared send result quote:",
1423
- quote,
1424
- )
795
+ logger.console.log("[trails-sdk] Using quote from useQuote:", quote)
1425
796
 
1426
797
  function onOriginSend() {
1427
798
  logger.console.log("[trails-sdk] onOriginSend called")
@@ -1435,10 +806,10 @@ export function useSendForm({
1435
806
 
1436
807
  async function handleSend() {
1437
808
  logger.console.log(
1438
- "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] handleSend called, about to call send()",
809
+ "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] handleSend called, about to call sendFn()",
1439
810
  )
1440
811
  logger.console.log(
1441
- "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] selectedFeeOption value at send() call time:",
812
+ "[trails-sdk] [FEE-SELECT] [GASLESS-FLOW] selectedFeeOption value at swap() call time:",
1442
813
  {
1443
814
  selectedFeeOption,
1444
815
  isNull: selectedFeeOption === null,
@@ -1447,24 +818,20 @@ export function useSendForm({
1447
818
  stringified: JsonEncode(selectedFeeOption),
1448
819
  },
1449
820
  )
1450
- // Wait for full send to complete
1451
- const {
1452
- depositUserTxnReceipt,
1453
- originIntentTransaction,
1454
- destinationIntentTransaction,
1455
- } = await send({
1456
- onOriginSend,
1457
- selectedFeeOption: selectedFeeOption ?? null, // Pass current value at execution time
1458
- })
1459
- logger.console.log("[trails-sdk] send() completed, receipts:", {
1460
- depositUserTxnReceipt,
1461
- originIntentTransaction,
1462
- destinationIntentTransaction,
821
+
822
+ // Execute the swap via useQuote's send function
823
+ // sendFn is guaranteed to be non-null here due to the check at the top of processSend
824
+ // Pass the current selectedFeeOption so gasless flow uses the correct fee token
825
+ // Pass onOriginSend as callback to be called when wallet signature is confirmed
826
+ const sendResult = await sendFn!({
827
+ selectedFeeOption: selectedFeeOption ?? null,
828
+ onOriginSend, // Called when wallet signature is confirmed, before tx completes
1463
829
  })
830
+ logger.console.log("[trails-sdk] sendFn() completed:", sendResult)
1464
831
 
1465
832
  // Move to receipt screen
1466
833
  onComplete({
1467
- transactionStates: quote.transactionStates,
834
+ transactionStates: quote.transactionStates ?? [],
1468
835
  })
1469
836
  }
1470
837
 
@@ -1484,7 +851,7 @@ export function useSendForm({
1484
851
 
1485
852
  const toTokenSymbol = quote?.originToken?.symbol // Onramp will deposit origin token
1486
853
  const toTokenAmount = normalizeNumber(
1487
- quote.originAmountFormatted,
854
+ quote.originAmountFormatted ?? "0",
1488
855
  ).toString() // Onramp will deposit origin token amount
1489
856
  const toChainId = quote?.originChain?.id // Onramp will deposit to origin chain
1490
857
  const toRecipientAddress = quote.originDepositAddress // Onramp will deposit to origin address
@@ -1577,6 +944,7 @@ export function useSendForm({
1577
944
  setIsWaitingForWalletConfirm(false)
1578
945
  }, [
1579
946
  prepareSendResult,
947
+ sendFn,
1580
948
  amount,
1581
949
  onSend,
1582
950
  onConfirm,
@@ -1588,7 +956,6 @@ export function useSendForm({
1588
956
  fundMethod,
1589
957
  onNavigateToOnramp,
1590
958
  selectedFeeOption, // Include so handleSend captures latest value
1591
- buildQuoteInputsFingerprint,
1592
959
  ])
1593
960
 
1594
961
  const handleSubmit = async (e: React.FormEvent) => {
@@ -1709,14 +1076,6 @@ export function useSendForm({
1709
1076
  fundMethod,
1710
1077
  ])
1711
1078
 
1712
- useEffect(() => {
1713
- if (quoteError) {
1714
- setQuoteErrorPrettified(getPrettifiedErrorMessage(quoteError))
1715
- } else {
1716
- setQuoteErrorPrettified(null)
1717
- }
1718
- }, [quoteError])
1719
-
1720
1079
  return {
1721
1080
  amount,
1722
1081
  amountRaw,
@@ -1762,8 +1121,8 @@ export function useSendForm({
1762
1121
  prepareSendQuote: prepareSendResult?.quote ?? null,
1763
1122
  quoteError,
1764
1123
  quoteErrorPrettified,
1765
- isRecipientContract,
1766
- isSenderContractOnOrigin,
1767
- isSenderContractOnDestination,
1124
+ isRecipientContract: quoteIsRecipientContract ?? false,
1125
+ isSenderContractOnOrigin: quoteIsSenderContractOnOrigin ?? false,
1126
+ isSenderContractOnDestination: quoteIsSenderContractOnDestination ?? false,
1768
1127
  }
1769
1128
  }