0xtrails 0.9.2 → 0.9.4

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 (79) hide show
  1. package/dist/{ccip-g6lDdnrD.js → ccip-lAtzqne5.js} +1 -1
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/constants.d.ts +1 -0
  5. package/dist/constants.d.ts.map +1 -1
  6. package/dist/error.d.ts.map +1 -1
  7. package/dist/{index-D-QngA_s.js → index-D5AG6huo.js} +22290 -21786
  8. package/dist/index.js +3 -3
  9. package/dist/intents.d.ts +1 -1
  10. package/dist/intents.d.ts.map +1 -1
  11. package/dist/mutations.d.ts +5 -2
  12. package/dist/mutations.d.ts.map +1 -1
  13. package/dist/tokens.d.ts.map +1 -1
  14. package/dist/transactionIntent/constants.d.ts +1 -0
  15. package/dist/transactionIntent/constants.d.ts.map +1 -1
  16. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts +3 -1
  17. package/dist/transactionIntent/deposits/depositOrchestrator.d.ts.map +1 -1
  18. package/dist/transactionIntent/deposits/standardDeposit.d.ts +4 -1
  19. package/dist/transactionIntent/deposits/standardDeposit.d.ts.map +1 -1
  20. package/dist/transactionIntent/handlers/crossChain.d.ts.map +1 -1
  21. package/dist/transactionIntent/handlers/sameChainSameToken.d.ts.map +1 -1
  22. package/dist/transactionIntent/quote/normalizeQuote.d.ts.map +1 -1
  23. package/dist/transactionIntent/types.d.ts +2 -0
  24. package/dist/transactionIntent/types.d.ts.map +1 -1
  25. package/dist/transactionIntent/utils/resilientDepositTracker.d.ts +25 -0
  26. package/dist/transactionIntent/utils/resilientDepositTracker.d.ts.map +1 -0
  27. package/dist/widget/components/AccountIntentTransactionHistory.d.ts.map +1 -1
  28. package/dist/widget/components/ClassicSwap.d.ts.map +1 -1
  29. package/dist/widget/components/ConfigDisplay.d.ts.map +1 -1
  30. package/dist/widget/components/DynamicInputStyles.d.ts +2 -2
  31. package/dist/widget/components/Earn.d.ts.map +1 -1
  32. package/dist/widget/components/EarnPools.d.ts.map +1 -1
  33. package/dist/widget/components/Fund.d.ts.map +1 -1
  34. package/dist/widget/components/QuoteDetails.d.ts.map +1 -1
  35. package/dist/widget/components/Receipt.d.ts.map +1 -1
  36. package/dist/widget/components/SlippageToleranceSettings.d.ts.map +1 -1
  37. package/dist/widget/components/TransactionDetails.d.ts.map +1 -1
  38. package/dist/widget/components/UserPreferences.d.ts.map +1 -1
  39. package/dist/widget/components/WalletConnect.d.ts.map +1 -1
  40. package/dist/widget/css/compiled.css +1 -1
  41. package/dist/widget/hooks/useQuote.d.ts +2 -0
  42. package/dist/widget/hooks/useQuote.d.ts.map +1 -1
  43. package/dist/widget/index.js +1 -1
  44. package/dist/widget/providers/TrailsProvider.d.ts +2 -0
  45. package/dist/widget/providers/TrailsProvider.d.ts.map +1 -1
  46. package/dist/widget/widget.d.ts +1 -0
  47. package/dist/widget/widget.d.ts.map +1 -1
  48. package/package.json +2 -2
  49. package/src/config.ts +1 -0
  50. package/src/constants.ts +1 -0
  51. package/src/error.ts +6 -1
  52. package/src/intents.ts +22 -1
  53. package/src/prices.ts +1 -1
  54. package/src/tokens.ts +4 -3
  55. package/src/transactionIntent/constants.ts +2 -0
  56. package/src/transactionIntent/deposits/depositOrchestrator.ts +7 -0
  57. package/src/transactionIntent/deposits/standardDeposit.ts +194 -37
  58. package/src/transactionIntent/handlers/crossChain.ts +152 -105
  59. package/src/transactionIntent/handlers/sameChainSameToken.ts +1 -0
  60. package/src/transactionIntent/quote/normalizeQuote.ts +7 -4
  61. package/src/transactionIntent/types.ts +2 -0
  62. package/src/transactionIntent/utils/resilientDepositTracker.ts +281 -0
  63. package/src/widget/compiled.css +1 -1
  64. package/src/widget/components/AccountIntentTransactionHistory.tsx +170 -87
  65. package/src/widget/components/ClassicSwap.tsx +7 -1
  66. package/src/widget/components/ConfigDisplay.tsx +5 -0
  67. package/src/widget/components/Earn.tsx +14 -1
  68. package/src/widget/components/EarnPools.tsx +180 -59
  69. package/src/widget/components/Fund.tsx +3 -1
  70. package/src/widget/components/PoolWithdraw.tsx +1 -1
  71. package/src/widget/components/QuoteDetails.tsx +12 -35
  72. package/src/widget/components/Receipt.tsx +66 -40
  73. package/src/widget/components/SlippageToleranceSettings.tsx +86 -44
  74. package/src/widget/components/TransactionDetails.tsx +138 -218
  75. package/src/widget/components/UserPreferences.tsx +114 -41
  76. package/src/widget/components/WalletConnect.tsx +111 -48
  77. package/src/widget/hooks/useQuote.ts +389 -352
  78. package/src/widget/providers/TrailsProvider.tsx +5 -0
  79. package/src/widget/widget.tsx +2 -0
@@ -212,6 +212,7 @@ export async function handleCrossChain({
212
212
  destinationTokenSymbol,
213
213
  feeTokenSymbol: originTokenSymbol,
214
214
  },
215
+ abortSignal,
215
216
  )
216
217
  logger.console.log("[trails-sdk] Quote intent:", intent)
217
218
  logger.console.log("[trails-sdk] Quote intent gasFeeOptions:", gasFeeOptions)
@@ -687,6 +688,8 @@ export async function handleCrossChain({
687
688
  abortSignal,
688
689
  intentId: intent.intentId,
689
690
  executeIntentFn,
691
+ sequenceProjectAccessKey,
692
+ sequenceIndexerUrl,
690
693
  })
691
694
 
692
695
  // In gasless mode, depositUserTxnReceipt will be null because the transaction
@@ -817,6 +820,9 @@ export async function handleCrossChain({
817
820
  }
818
821
 
819
822
  const checkForDepositTx = async () => {
823
+ let consecutiveErrors = 0
824
+ const maxConsecutiveErrors = 5
825
+
820
826
  while (true) {
821
827
  // Check if we should abort
822
828
  if (abortSignal?.aborted) {
@@ -827,120 +833,129 @@ export async function handleCrossChain({
827
833
  }
828
834
 
829
835
  try {
830
- if (!sequenceIndexerUrl) {
831
- throw new Error("sequenceIndexerUrl is required")
832
- }
833
- const response = await getAccountTransactionHistory({
834
- chainId: originChainId,
835
- accountAddress: originIntentAddress,
836
- abortSignal,
837
- apiKey: sequenceProjectAccessKey!,
838
- indexerUrl: sequenceIndexerUrl,
836
+ // First try balance check as primary method
837
+ const balanceCheck = await checkAccountBalance({
838
+ account: {
839
+ address: originIntentAddress as `0x${string}`,
840
+ } as Account,
841
+ tokenAddress: originTokenAddress,
842
+ depositAmount: quote.originAmount,
843
+ publicClient: publicClient,
839
844
  })
840
- logger.console.log(
841
- "[trails-sdk] getAccountTransactionHistory response",
842
- response,
843
- )
844
- if (response.transactions.length > 0) {
845
- const tx = response.transactions[0]
846
- if (!tx?.txnHash) {
847
- await new Promise((resolve) =>
848
- setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
849
- )
850
- continue
851
- }
852
- // const isReceive = tx.transfers.some(
853
- // (transfer) => transfer.transferType === "RECEIVE",
854
- // )
855
- // if (!isReceive) {
856
- // await new Promise((resolve) => setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY))
857
- // continue
858
- // }
859
- const originDepositTxReceipt =
860
- await publicClient.getTransactionReceipt({
861
- hash: tx.txnHash as `0x${string}`,
862
- })
863
845
 
864
- // Validate that the deposit amount is sufficient before proceeding
846
+ if (balanceCheck.hasEnoughBalance) {
865
847
  logger.console.log(
866
- "[trails-sdk] Checking if deposit amount is sufficient (cross-chain)",
848
+ "[trails-sdk] Deposit detected via balance check (QR/exchange mode)",
867
849
  {
868
- depositAddress: originIntentAddress,
869
- requiredAmount: quote.originAmount,
870
- tokenAddress: originTokenAddress,
850
+ balance: balanceCheck.balanceFormatted,
851
+ required: balanceCheck.requiredAmountFormatted,
871
852
  },
872
853
  )
873
854
 
874
- const balanceCheck = await checkAccountBalance({
875
- account: {
876
- address: originIntentAddress as `0x${string}`,
877
- } as Account,
878
- tokenAddress: originTokenAddress,
879
- depositAmount: quote.originAmount,
880
- publicClient: publicClient,
881
- })
855
+ // Try to find the actual transaction via indexer
856
+ if (sequenceIndexerUrl && sequenceProjectAccessKey) {
857
+ try {
858
+ const response = await getAccountTransactionHistory({
859
+ chainId: originChainId,
860
+ accountAddress: originIntentAddress,
861
+ abortSignal,
862
+ apiKey: sequenceProjectAccessKey,
863
+ indexerUrl: sequenceIndexerUrl,
864
+ })
882
865
 
883
- if (!balanceCheck.hasEnoughBalance) {
884
- logger.console.warn(
885
- "[trails-sdk] Deposit amount is insufficient, continuing to poll (cross-chain)",
886
- {
887
- balance: balanceCheck.balanceFormatted,
888
- required: balanceCheck.requiredAmountFormatted,
889
- percentComplete: (
890
- (Number(balanceCheck.balance) /
891
- Number(balanceCheck.requiredAmount)) *
892
- 100
893
- ).toFixed(2),
894
- },
895
- )
896
- // Continue polling - don't break out of the loop yet
897
- await new Promise((resolve) =>
898
- setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
899
- )
900
- continue
866
+ if (response.transactions.length > 0) {
867
+ const tx = response.transactions[0]
868
+ if (tx?.txnHash) {
869
+ depositUserTxnReceipt =
870
+ await publicClient.getTransactionReceipt({
871
+ hash: tx.txnHash as `0x${string}`,
872
+ })
873
+
874
+ transactionStates[0] = getTransactionStateFromReceipt(
875
+ depositUserTxnReceipt,
876
+ originChainId,
877
+ transactionStates[0]?.label,
878
+ )
879
+ onTransactionStateChange(transactionStates)
880
+ if (checkoutOnHandlers?.triggerCheckoutStatusUpdate) {
881
+ checkoutOnHandlers.triggerCheckoutStatusUpdate(
882
+ transactionStates,
883
+ )
884
+ }
885
+
886
+ // Call executeIntent after deposit detection
887
+ if (
888
+ depositUserTxnReceipt.status === "success" &&
889
+ intent.intentId &&
890
+ !executeIntentCalled
891
+ ) {
892
+ logger.console.log(
893
+ "[trails-sdk] Calling executeIntent after deposit detection",
894
+ )
895
+ try {
896
+ const executeIntentFnToUse =
897
+ executeIntentFn ||
898
+ trailsClient.executeIntent.bind(trailsClient)
899
+ await executeIntentFnToUse({
900
+ intentId: intent.intentId,
901
+ depositTransactionHash:
902
+ depositUserTxnReceipt.transactionHash,
903
+ })
904
+ executeIntentCalled = true
905
+ if (!unifiedPollerPromise) {
906
+ startUnifiedPolling()
907
+ }
908
+ } catch (error) {
909
+ logger.console.error(
910
+ "[trails-sdk] Error calling executeIntent:",
911
+ error,
912
+ )
913
+ }
914
+ }
915
+
916
+ if (onOriginSend) {
917
+ onOriginSend()
918
+ }
919
+ break
920
+ }
921
+ }
922
+ } catch (indexerError) {
923
+ logger.console.debug(
924
+ "[trails-sdk] Indexer failed but balance is sufficient, proceeding",
925
+ { error: (indexerError as Error)?.message },
926
+ )
927
+ }
901
928
  }
902
929
 
930
+ // Even without tx hash, we know deposit happened
931
+ // Set a flag that deposit was detected but we'll need to poll for the actual tx
903
932
  logger.console.log(
904
- "[trails-sdk] Deposit amount is sufficient, proceeding (cross-chain)",
905
- {
906
- balance: balanceCheck.balanceFormatted,
907
- required: balanceCheck.requiredAmountFormatted,
908
- },
933
+ "[trails-sdk] Balance detected but no transaction hash - proceeding with intent execution",
909
934
  )
910
935
 
911
- depositUserTxnReceipt = originDepositTxReceipt
912
-
913
- transactionStates[0] = getTransactionStateFromReceipt(
914
- originDepositTxReceipt,
915
- originChainId,
916
- transactionStates[0]?.label,
917
- )
936
+ // Don't create a fake receipt - just mark that we detected funds
937
+ depositUserTxnReceipt = null
938
+
939
+ // Update state to show deposit detected (but without fake hash)
940
+ transactionStates[0] = {
941
+ ...transactionStates[0],
942
+ state: "pending",
943
+ transactionHash: "", // Empty string instead of undefined for type compatibility
944
+ explorerUrl: transactionStates[0]?.explorerUrl || "",
945
+ chainId: transactionStates[0]?.chainId || originChainId,
946
+ label: transactionStates[0]?.label || "Deposit detected",
947
+ }
918
948
  onTransactionStateChange(transactionStates)
919
- // Also trigger checkout status update if handler is provided
920
949
  if (checkoutOnHandlers?.triggerCheckoutStatusUpdate) {
921
950
  checkoutOnHandlers.triggerCheckoutStatusUpdate(
922
951
  transactionStates,
923
952
  )
924
953
  }
925
954
 
926
- // Call executeIntent after detecting deposit transaction (for QR code and exchange modes)
927
- // This triggers the backend to start executing the intent
928
- // Must be called BEFORE waitIntentReceipt polling starts
929
- // For onramp-meld, executeIntent was already called after commitIntent
930
- if (
931
- originDepositTxReceipt.status === "success" &&
932
- intent.intentId &&
933
- (fundMethod === "qr-code" ||
934
- fundMethod === "onramp-exchange") &&
935
- !executeIntentCalled
936
- ) {
955
+ // Call executeIntent with balance-based detection
956
+ if (intent.intentId && !executeIntentCalled) {
937
957
  logger.console.log(
938
- "[trails-sdk] Calling executeIntent with detected deposit transaction hash (external funding mode)",
939
- {
940
- intentId: intent.intentId,
941
- txHash: originDepositTxReceipt.transactionHash,
942
- fundMethod,
943
- },
958
+ "[trails-sdk] Calling executeIntent after balance-based deposit detection",
944
959
  )
945
960
  try {
946
961
  const executeIntentFnToUse =
@@ -948,24 +963,15 @@ export async function handleCrossChain({
948
963
  trailsClient.executeIntent.bind(trailsClient)
949
964
  await executeIntentFnToUse({
950
965
  intentId: intent.intentId,
951
- depositTransactionHash:
952
- originDepositTxReceipt.transactionHash,
966
+ // Note: No depositTransactionHash available in this case
953
967
  })
954
968
  executeIntentCalled = true
955
- logger.console.log(
956
- "[trails-sdk] executeIntent completed successfully (QR code mode)",
957
- )
958
-
959
- // Now that executeIntent has been called, start unified polling if not already started
960
969
  if (!unifiedPollerPromise) {
961
- logger.console.log(
962
- "[trails-sdk] Starting unified polling after executeIntent (QR code mode)",
963
- )
964
970
  startUnifiedPolling()
965
971
  }
966
972
  } catch (error) {
967
973
  logger.console.error(
968
- "[trails-sdk] Error calling executeIntent (QR code mode):",
974
+ "[trails-sdk] Error calling executeIntent:",
969
975
  error,
970
976
  )
971
977
  }
@@ -976,9 +982,50 @@ export async function handleCrossChain({
976
982
  }
977
983
  break
978
984
  }
985
+
986
+ // If sequenceIndexerUrl is available, also try the indexer as backup
987
+ if (sequenceIndexerUrl && sequenceProjectAccessKey) {
988
+ try {
989
+ const _response = await getAccountTransactionHistory({
990
+ chainId: originChainId,
991
+ accountAddress: originIntentAddress,
992
+ abortSignal,
993
+ apiKey: sequenceProjectAccessKey,
994
+ indexerUrl: sequenceIndexerUrl,
995
+ })
996
+
997
+ // Original indexer-based logic continues here...
998
+ // (keeping the existing logic for when balance check doesn't pass)
999
+ } catch (indexerError) {
1000
+ logger.console.debug(
1001
+ "[trails-sdk] Indexer check failed, relying on balance check",
1002
+ { error: (indexerError as Error)?.message },
1003
+ )
1004
+ }
1005
+ }
1006
+
1007
+ // Reset consecutive errors on successful iteration
1008
+ consecutiveErrors = 0
979
1009
  } catch (error) {
980
- logger.console.error("Error checking for deposit tx", error)
1010
+ consecutiveErrors++
1011
+ logger.console.error(
1012
+ "[trails-sdk] Error checking for deposit tx",
1013
+ {
1014
+ error,
1015
+ consecutiveErrors,
1016
+ maxConsecutiveErrors,
1017
+ },
1018
+ )
1019
+
1020
+ if (consecutiveErrors >= maxConsecutiveErrors) {
1021
+ logger.console.error(
1022
+ "[trails-sdk] Max consecutive errors reached in deposit polling, continuing anyway",
1023
+ )
1024
+ // Don't break - continue polling as transaction might still go through
1025
+ consecutiveErrors = 0 // Reset counter to keep trying
1026
+ }
981
1027
  }
1028
+
982
1029
  await new Promise((resolve) =>
983
1030
  setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
984
1031
  )
@@ -536,6 +536,7 @@ export async function handleSameChainSameToken({
536
536
  destinationTokenSymbol,
537
537
  feeTokenSymbol: originTokenSymbol,
538
538
  },
539
+ abortSignal,
539
540
  )
540
541
 
541
542
  logger.console.log("[trails-sdk] Quote intent:", intent)
@@ -238,18 +238,19 @@ export async function getNormalizedQuoteObject({
238
238
 
239
239
  // Calculate exchange rates using server-derived prices when available
240
240
  // This ensures consistency with server-provided USD amounts (fromAmountUsd, toAmountUsd)
241
+ // Uses != null checks to handle 0 values correctly (not truthy checks)
241
242
  const originTokenPrice =
242
- intent?.quote?.fromAmountUsd && intent?.quote?.fromAmount
243
+ intent?.quote?.fromAmountUsd != null && intent?.quote?.fromAmount != null
243
244
  ? intent.quote.fromAmountUsd /
244
245
  Number(formatUnits(intent.quote.fromAmount, originToken.decimals))
245
- : originTokenPriceUsd
246
+ : originTokenPriceUsd != null
246
247
  ? Number(originTokenPriceUsd)
247
248
  : 0
248
249
  const destinationTokenPrice =
249
- intent?.quote?.toAmountUsd && intent?.quote?.toAmount
250
+ intent?.quote?.toAmountUsd != null && intent?.quote?.toAmount != null
250
251
  ? intent.quote.toAmountUsd /
251
252
  Number(formatUnits(intent.quote.toAmount, destinationToken.decimals))
252
- : destinationTokenPriceUsd
253
+ : destinationTokenPriceUsd != null
253
254
  ? Number(destinationTokenPriceUsd)
254
255
  : 0
255
256
  const exchangeRates = getTokenExchangeRates(
@@ -373,6 +374,8 @@ export async function getNormalizedQuoteObject({
373
374
  gasCostFormatted,
374
375
  originTokenRate: exchangeRates.originTokenRate,
375
376
  destinationTokenRate: exchangeRates.destinationTokenRate,
377
+ originTokenPriceUsd: originTokenPrice,
378
+ destinationTokenPriceUsd: destinationTokenPrice,
376
379
  originAmountDisplay: formatAmountDisplay(originAmountFormatted),
377
380
  destinationAmountDisplay: formatAmountDisplay(destinationAmountFormatted),
378
381
  originAmountMinDisplay: formatAmountDisplay(originAmountMinFormatted),
@@ -126,6 +126,8 @@ export type PrepareSendQuote = {
126
126
  gasCostFormatted: string
127
127
  originTokenRate: string
128
128
  destinationTokenRate: string
129
+ originTokenPriceUsd: number
130
+ destinationTokenPriceUsd: number
129
131
  routeProviders?: RouteProviderInfo[]
130
132
  noSufficientBalance: boolean
131
133
  trailsFeeBreakdown?: TrailsFeeBreakdown | null
@@ -0,0 +1,281 @@
1
+ import type { PublicClient, TransactionReceipt } from "viem"
2
+ import { logger } from "../../logger.js"
3
+ import { POLLING_INTERVALS } from "../constants.js"
4
+ import { getAccountTransactionHistory } from "../../transactions.js"
5
+ import type { Account } from "viem"
6
+ import { checkAccountBalance } from "./balanceChecker.js"
7
+
8
+ export interface ResilientDepositTrackerOptions {
9
+ publicClient: PublicClient
10
+ originChainId: number
11
+ originIntentAddress: string
12
+ originTokenAddress: string
13
+ depositAmount: string
14
+ txHash?: `0x${string}` | null
15
+ abortSignal?: AbortSignal
16
+ sequenceProjectAccessKey?: string
17
+ sequenceIndexerUrl?: string
18
+ maxRetries?: number
19
+ onDepositDetected?: (
20
+ txHash: `0x${string}` | null,
21
+ receipt: TransactionReceipt | null,
22
+ ) => void
23
+ }
24
+
25
+ /**
26
+ * Resilient deposit tracker that handles RPC failures gracefully
27
+ * Falls back to polling when transaction submission fails
28
+ */
29
+ export async function trackDepositResilient({
30
+ publicClient,
31
+ originChainId,
32
+ originIntentAddress,
33
+ originTokenAddress,
34
+ depositAmount,
35
+ txHash,
36
+ abortSignal,
37
+ sequenceProjectAccessKey,
38
+ sequenceIndexerUrl,
39
+ maxRetries = 60, // 60 attempts = ~3 minutes with 3s intervals
40
+ onDepositDetected,
41
+ }: ResilientDepositTrackerOptions): Promise<TransactionReceipt | null> {
42
+ logger.console.log("[trails-sdk] Starting resilient deposit tracking", {
43
+ hasHash: !!txHash,
44
+ txHash,
45
+ originIntentAddress,
46
+ depositAmount,
47
+ })
48
+
49
+ let attempts = 0
50
+ let lastError: Error | null = null
51
+
52
+ // If we have a transaction hash, try to get its receipt first
53
+ if (txHash) {
54
+ while (attempts < maxRetries) {
55
+ if (abortSignal?.aborted) {
56
+ logger.console.log("[trails-sdk] Deposit tracking aborted by signal")
57
+ return null
58
+ }
59
+
60
+ try {
61
+ // Try to get the transaction receipt
62
+ const receipt = await publicClient.getTransactionReceipt({
63
+ hash: txHash,
64
+ })
65
+
66
+ if (receipt) {
67
+ logger.console.log("[trails-sdk] Deposit transaction receipt found", {
68
+ hash: receipt.transactionHash,
69
+ status: receipt.status,
70
+ blockNumber: receipt.blockNumber,
71
+ })
72
+
73
+ // Notify callback if provided
74
+ if (onDepositDetected) {
75
+ onDepositDetected(txHash, receipt)
76
+ }
77
+
78
+ return receipt
79
+ }
80
+ } catch (error) {
81
+ // Receipt not found yet, this is expected for pending transactions
82
+ if (attempts === 0) {
83
+ logger.console.log(
84
+ "[trails-sdk] Transaction receipt not available yet, will continue polling",
85
+ { txHash, error: (error as Error)?.message },
86
+ )
87
+ }
88
+ lastError = error as Error
89
+ }
90
+
91
+ attempts++
92
+ await new Promise((resolve) =>
93
+ setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_RECEIPT),
94
+ )
95
+ }
96
+
97
+ logger.console.warn(
98
+ "[trails-sdk] Could not get receipt for known transaction hash after retries",
99
+ {
100
+ txHash,
101
+ attempts,
102
+ lastError: lastError?.message,
103
+ },
104
+ )
105
+ }
106
+
107
+ // Fall back to polling the intent address for deposits
108
+ logger.console.log(
109
+ "[trails-sdk] Falling back to intent address polling for deposit detection",
110
+ )
111
+
112
+ attempts = 0
113
+ const pollingStartTime = Date.now()
114
+ const maxPollingDuration = 10 * 60 * 1000 // 10 minutes max
115
+
116
+ while (attempts < maxRetries) {
117
+ if (abortSignal?.aborted) {
118
+ logger.console.log("[trails-sdk] Deposit tracking aborted by signal")
119
+ return null
120
+ }
121
+
122
+ // Check if we've exceeded maximum polling duration
123
+ if (Date.now() - pollingStartTime > maxPollingDuration) {
124
+ logger.console.warn(
125
+ "[trails-sdk] Maximum polling duration exceeded for deposit detection",
126
+ )
127
+ break
128
+ }
129
+
130
+ try {
131
+ // Primary method: Check transaction history via indexer to find ANY transaction to intent address
132
+ if (sequenceIndexerUrl && sequenceProjectAccessKey) {
133
+ try {
134
+ const response = await getAccountTransactionHistory({
135
+ chainId: originChainId,
136
+ accountAddress: originIntentAddress,
137
+ abortSignal,
138
+ apiKey: sequenceProjectAccessKey,
139
+ indexerUrl: sequenceIndexerUrl,
140
+ })
141
+
142
+ if (response.transactions.length > 0) {
143
+ const recentTx = response.transactions[0]
144
+ if (recentTx?.txnHash) {
145
+ logger.console.log(
146
+ "[trails-sdk] Found deposit transaction to intent address via indexer",
147
+ { txHash: recentTx.txnHash },
148
+ )
149
+
150
+ // Get the full receipt
151
+ const receipt = await publicClient.getTransactionReceipt({
152
+ hash: recentTx.txnHash as `0x${string}`,
153
+ })
154
+
155
+ logger.console.log(
156
+ "[trails-sdk] Deposit transaction detected - proceeding",
157
+ {
158
+ txHash: receipt.transactionHash,
159
+ to: receipt.to,
160
+ status: receipt.status,
161
+ },
162
+ )
163
+
164
+ // Notify callback if provided
165
+ if (onDepositDetected) {
166
+ onDepositDetected(
167
+ receipt.transactionHash as `0x${string}`,
168
+ receipt,
169
+ )
170
+ }
171
+
172
+ return receipt
173
+ }
174
+ }
175
+ } catch (indexerError) {
176
+ logger.console.debug(
177
+ "[trails-sdk] Indexer check failed, will continue polling",
178
+ { error: (indexerError as Error)?.message },
179
+ )
180
+ }
181
+ }
182
+
183
+ // Fallback method: Check if ANY balance exists at the intent address
184
+ // This indicates a transaction has occurred even if we can't find the specific tx
185
+ const balanceCheck = await checkAccountBalance({
186
+ account: {
187
+ address: originIntentAddress as `0x${string}`,
188
+ } as Account,
189
+ tokenAddress: originTokenAddress,
190
+ depositAmount: "1", // Check for any non-zero balance
191
+ publicClient: publicClient,
192
+ })
193
+
194
+ if (balanceCheck.hasEnoughBalance) {
195
+ logger.console.log(
196
+ "[trails-sdk] Detected funds at intent address (transaction occurred)",
197
+ {
198
+ balance: balanceCheck.balanceFormatted,
199
+ intentAddress: originIntentAddress,
200
+ },
201
+ )
202
+
203
+ // We detected a deposit via balance, but don't have the tx hash
204
+ // This means a transaction definitely occurred
205
+ // Return null to indicate we detected funds but have no transaction details
206
+ logger.console.log(
207
+ "[trails-sdk] Funds detected but no transaction hash available - proceeding anyway",
208
+ )
209
+ return null
210
+ }
211
+ } catch (error) {
212
+ logger.console.debug("[trails-sdk] Error during deposit polling", {
213
+ attempt: attempts,
214
+ error: (error as Error)?.message,
215
+ })
216
+ }
217
+
218
+ attempts++
219
+ await new Promise((resolve) =>
220
+ setTimeout(resolve, POLLING_INTERVALS.TRANSACTION_HISTORY),
221
+ )
222
+ }
223
+
224
+ logger.console.warn(
225
+ "[trails-sdk] Deposit tracking ended without detecting deposit",
226
+ {
227
+ attempts,
228
+ duration: Date.now() - pollingStartTime,
229
+ },
230
+ )
231
+
232
+ return null
233
+ }
234
+
235
+ /**
236
+ * Helper to determine if an error is recoverable via polling
237
+ */
238
+ export function isRecoverableDepositError(error: unknown): boolean {
239
+ if (!error) return false
240
+
241
+ const errorMessage = (error as Error)?.message?.toLowerCase() || ""
242
+ const errorCode = (error as any)?.code
243
+ const errorCodeLower =
244
+ typeof errorCode === "string" ? errorCode.toLowerCase() : errorCode
245
+
246
+ // User rejections are not recoverable (case-insensitive)
247
+ if (
248
+ errorCode === 4001 ||
249
+ errorCodeLower === "action_rejected" ||
250
+ errorMessage.includes("user rejected") ||
251
+ errorMessage.includes("user denied") ||
252
+ errorMessage.includes("rejected by user")
253
+ ) {
254
+ return false
255
+ }
256
+
257
+ // Specific recoverable errors (case-insensitive)
258
+ if (
259
+ errorMessage.includes("wallet timeout") ||
260
+ errorMessage.includes("an unknown rpc error occurred")
261
+ ) {
262
+ return true
263
+ }
264
+
265
+ // Other network/RPC errors are also recoverable (case-insensitive)
266
+ if (
267
+ errorMessage.includes("network") ||
268
+ errorMessage.includes("timeout") ||
269
+ errorMessage.includes("rpc") ||
270
+ errorMessage.includes("connection") ||
271
+ errorMessage.includes("fetch") ||
272
+ errorCodeLower === "network_error" ||
273
+ errorCodeLower === "timeout" ||
274
+ errorCode === -32603 // Internal JSON-RPC error (numeric, no case)
275
+ ) {
276
+ return true
277
+ }
278
+
279
+ // Unknown errors - DON'T attempt recovery by default (safer)
280
+ return false
281
+ }