0xtrails 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,11 +16,18 @@ import type { WalletClient, Chain } from "viem"
16
16
  import { splitSignature } from "./gasless.js"
17
17
  import { useMemo, useCallback } from "react"
18
18
  import { useGetIntent } from "./widget/hooks/useGetIntent.js"
19
- import { useTokenBalances } from "./tokenBalances.js"
19
+ import {
20
+ useTokenBalances,
21
+ getTokenBalanceUsd,
22
+ type Price,
23
+ } from "./tokenBalances.js"
20
24
  import { getERC20TransferData } from "./encoders.js"
21
25
  import { zeroAddress } from "viem"
26
+ import { useTokenPrices } from "./prices.js"
22
27
  import type { Payload as WalletPayload } from "@0xsequence/wallet-primitives"
23
28
  import { attemptSwitchChain } from "./chainSwitch.js"
29
+ import { decodeGuestModuleEvents } from "./decoders.js"
30
+ import type { TransactionReceipt } from "viem"
24
31
 
25
32
  // Constants for execute function
26
33
  type ChainId = number
@@ -561,13 +568,13 @@ export function determineRefundCall(
561
568
  }
562
569
  }
563
570
 
564
- export interface UseTrailsRefundParams {
571
+ export interface UseIntentRecoverParams {
565
572
  intentId: string | undefined
566
573
  walletClient: WalletClient | undefined
567
574
  refundToAddress?: `0x${string}`
568
575
  }
569
576
 
570
- export interface UseTrailsRefundReturn {
577
+ export interface UseIntentRecoverReturn {
571
578
  intent: any | null
572
579
  isLoadingIntent: boolean
573
580
  isLoadingBalances: boolean
@@ -580,7 +587,7 @@ export interface UseTrailsRefundReturn {
580
587
  payload: Payload.Calls
581
588
  refundCall: Payload.Call
582
589
  }>
583
- getRefundTx: (params: {
590
+ getRecoverTx: (params: {
584
591
  signedHash: string
585
592
  payload: Payload.Calls
586
593
  }) => Promise<{
@@ -589,27 +596,43 @@ export interface UseTrailsRefundReturn {
589
596
  chainId: number
590
597
  chain: Chain
591
598
  }>
599
+ recover: () => Promise<{
600
+ txHash: `0x${string}`
601
+ receipt: TransactionReceipt
602
+ chainId: number
603
+ }>
604
+ getRecoverStatus: (params: {
605
+ txHash: `0x${string}`
606
+ chainId: number
607
+ }) => Promise<{
608
+ status: "success" | "fail" | "pending"
609
+ }>
592
610
  }
593
611
 
594
612
  /**
595
- * Hook for building and signing refund transactions
613
+ * Hook for building and signing recover transactions
596
614
  * @example
597
615
  * ```tsx
598
- * const { signPayload, getRefundTx } = useTrailsRefund({
616
+ * const { signPayload, getRecoverTx, recover } = useIntentRecover({
599
617
  * intentId: "0x...",
600
618
  * walletClient: walletClient,
601
619
  * })
602
620
  *
603
- * const signedHash = await signRefund()
604
- * const refundTx = await getRefundTx({ signedHash })
605
- * await walletClient.sendTransaction(refundTx)
621
+ * // Option 1: Manual flow
622
+ * const signedHash = await signPayload()
623
+ * const recoverTx = await getRecoverTx({ signedHash })
624
+ * await walletClient.sendTransaction(recoverTx)
625
+ *
626
+ * // Option 2: Automatic flow (recommended)
627
+ * const { txHash, receipt, chainId } = await recover()
628
+ * const status = await getRecoverStatus({ txHash, chainId })
606
629
  * ```
607
630
  */
608
- export function useTrailsRefund({
631
+ export function useIntentRecover({
609
632
  intentId,
610
633
  walletClient,
611
634
  refundToAddress,
612
- }: UseTrailsRefundParams): UseTrailsRefundReturn {
635
+ }: UseIntentRecoverParams): UseIntentRecoverReturn {
613
636
  const {
614
637
  intent: intentData,
615
638
  isLoading: isLoadingIntent,
@@ -620,44 +643,333 @@ export function useTrailsRefund({
620
643
  enabled: !!intentId,
621
644
  })
622
645
 
623
- // Get token balances for the intent address
624
- const intentAddressForBalances = useMemo<Address.Address | null>(() => {
646
+ // Get token balances for both origin and destination intent addresses
647
+ const originIntentAddressForBalances = useMemo<Address.Address | null>(() => {
625
648
  if (!intentData?.originIntentAddress) return null
626
649
  return Address.from(intentData.originIntentAddress)
627
650
  }, [intentData?.originIntentAddress])
628
651
 
652
+ const destinationIntentAddressForBalances =
653
+ useMemo<Address.Address | null>(() => {
654
+ if (!intentData?.destinationIntentAddress) return null
655
+ return Address.from(intentData.destinationIntentAddress)
656
+ }, [intentData?.destinationIntentAddress])
657
+
629
658
  const {
630
- tokenBalancesData,
631
- isLoadingBalances,
632
- balanceError: balancesError,
633
- } = useTokenBalances(intentAddressForBalances)
659
+ tokenBalancesData: originTokenBalancesData,
660
+ isLoadingBalances: isLoadingOriginBalances,
661
+ balanceError: originBalanceError,
662
+ } = useTokenBalances(originIntentAddressForBalances)
634
663
 
635
- // Check if intent address has any balance (native or ERC20)
636
- const hasIntentBalance = useMemo<boolean>(() => {
637
- if (!tokenBalancesData) return false
638
-
639
- // Check native token balances
640
- if (tokenBalancesData.nativeBalances) {
641
- for (const nativeBalance of tokenBalancesData.nativeBalances) {
642
- const balance = BigInt(nativeBalance.balance || "0")
643
- if (balance > 0n) {
644
- return true
664
+ const {
665
+ tokenBalancesData: destinationTokenBalancesData,
666
+ isLoadingBalances: isLoadingDestinationBalances,
667
+ balanceError: destinationBalanceError,
668
+ } = useTokenBalances(destinationIntentAddressForBalances)
669
+
670
+ // Get token prices for origin balances
671
+ const originTokensForPricing = useMemo(() => {
672
+ if (!originTokenBalancesData) return []
673
+ const tokens: Array<{
674
+ tokenSymbol: string
675
+ tokenAddress: string
676
+ chainId: number
677
+ }> = []
678
+ if (originTokenBalancesData.nativeBalances) {
679
+ for (const nativeBalance of originTokenBalancesData.nativeBalances) {
680
+ if (nativeBalance.chainId) {
681
+ tokens.push({
682
+ tokenSymbol: (nativeBalance as any).symbol || "ETH",
683
+ tokenAddress: zeroAddress,
684
+ chainId: nativeBalance.chainId,
685
+ })
645
686
  }
646
687
  }
647
688
  }
689
+ if (originTokenBalancesData.balances) {
690
+ for (const tokenBalance of originTokenBalancesData.balances) {
691
+ if (tokenBalance.contractInfo?.chainId) {
692
+ tokens.push({
693
+ tokenSymbol: tokenBalance.contractInfo?.symbol || "",
694
+ tokenAddress: tokenBalance.contractAddress || zeroAddress,
695
+ chainId: tokenBalance.contractInfo.chainId,
696
+ })
697
+ }
698
+ }
699
+ }
700
+ return tokens
701
+ }, [originTokenBalancesData])
702
+
703
+ const { tokenPrices: originTokenPrices } = useTokenPrices(
704
+ originTokensForPricing as any,
705
+ )
648
706
 
649
- // Check ERC20 token balances
650
- if (tokenBalancesData.balances) {
651
- for (const tokenBalance of tokenBalancesData.balances) {
652
- const balance = BigInt(tokenBalance.balance || "0")
653
- if (balance > 0n) {
654
- return true
707
+ // Get token prices for destination balances
708
+ const destinationTokensForPricing = useMemo(() => {
709
+ if (!destinationTokenBalancesData) return []
710
+ const tokens: Array<{
711
+ tokenSymbol: string
712
+ tokenAddress: string
713
+ chainId: number
714
+ }> = []
715
+ if (destinationTokenBalancesData.nativeBalances) {
716
+ for (const nativeBalance of destinationTokenBalancesData.nativeBalances) {
717
+ if (nativeBalance.chainId) {
718
+ tokens.push({
719
+ tokenSymbol: (nativeBalance as any).symbol || "ETH",
720
+ tokenAddress: zeroAddress,
721
+ chainId: nativeBalance.chainId,
722
+ })
655
723
  }
656
724
  }
657
725
  }
726
+ if (destinationTokenBalancesData.balances) {
727
+ for (const tokenBalance of destinationTokenBalancesData.balances) {
728
+ if (tokenBalance.contractInfo?.chainId) {
729
+ tokens.push({
730
+ tokenSymbol: tokenBalance.contractInfo?.symbol || "",
731
+ tokenAddress: tokenBalance.contractAddress || zeroAddress,
732
+ chainId: tokenBalance.contractInfo.chainId,
733
+ })
734
+ }
735
+ }
736
+ }
737
+ return tokens
738
+ }, [destinationTokenBalancesData])
739
+
740
+ const { tokenPrices: destinationTokenPrices } = useTokenPrices(
741
+ destinationTokensForPricing as any,
742
+ )
743
+
744
+ // Calculate total USD balance for an address
745
+ const calculateTotalUsdBalance = useCallback(
746
+ (
747
+ balancesData:
748
+ | {
749
+ nativeBalances?: Array<{
750
+ balance?: string
751
+ chainId?: number
752
+ symbol?: string
753
+ }>
754
+ balances?: Array<{
755
+ balance?: string
756
+ contractAddress?: string
757
+ contractInfo?: {
758
+ decimals?: number
759
+ chainId?: number
760
+ symbol?: string
761
+ }
762
+ }>
763
+ }
764
+ | undefined,
765
+ tokenPrices:
766
+ | Array<{
767
+ token: { chainId: number; tokenAddress: string }
768
+ priceUsd?: number
769
+ }>
770
+ | undefined,
771
+ ): number => {
772
+ if (!balancesData || !tokenPrices) return 0
773
+
774
+ let totalUsd = 0
775
+
776
+ // Sum native token USD values
777
+ if (balancesData.nativeBalances) {
778
+ for (const nativeBalance of balancesData.nativeBalances) {
779
+ const priceData = tokenPrices.find(
780
+ (p) =>
781
+ p.token.tokenAddress === zeroAddress &&
782
+ p.token.chainId === nativeBalance.chainId &&
783
+ p.priceUsd !== undefined,
784
+ )
785
+ if (priceData?.priceUsd && nativeBalance.balance) {
786
+ const price: Price = {
787
+ value: priceData.priceUsd,
788
+ currency: "USD",
789
+ }
790
+ const usdValue = getTokenBalanceUsd(
791
+ {
792
+ balance: nativeBalance.balance,
793
+ chainId: nativeBalance.chainId,
794
+ } as any,
795
+ price,
796
+ )
797
+ totalUsd += usdValue
798
+ }
799
+ }
800
+ }
801
+
802
+ // Sum ERC20 token USD values
803
+ if (balancesData.balances) {
804
+ for (const tokenBalance of balancesData.balances) {
805
+ const priceData = tokenPrices.find(
806
+ (p) =>
807
+ p.token.tokenAddress?.toLowerCase() ===
808
+ (tokenBalance.contractAddress || zeroAddress).toLowerCase() &&
809
+ p.token.chainId === tokenBalance.contractInfo?.chainId &&
810
+ p.priceUsd !== undefined,
811
+ )
812
+ if (priceData?.priceUsd && tokenBalance.balance) {
813
+ const price: Price = {
814
+ value: priceData.priceUsd,
815
+ currency: "USD",
816
+ }
817
+ const usdValue = getTokenBalanceUsd(
818
+ {
819
+ balance: tokenBalance.balance,
820
+ contractAddress: tokenBalance.contractAddress,
821
+ contractInfo: tokenBalance.contractInfo,
822
+ } as any,
823
+ price,
824
+ )
825
+ totalUsd += usdValue
826
+ }
827
+ }
828
+ }
829
+
830
+ return totalUsd
831
+ },
832
+ [],
833
+ )
834
+
835
+ // Determine which intent address has the bigger USD balance
836
+ const selectedIntentAddress = useMemo<{
837
+ address: Address.Address
838
+ chainId: number
839
+ intentAddress: Address.Address
840
+ tokenBalancesData: typeof originTokenBalancesData
841
+ } | null>(() => {
842
+ if (!intentData) return null
843
+
844
+ const originTotalUsd = calculateTotalUsdBalance(
845
+ originTokenBalancesData,
846
+ originTokenPrices,
847
+ )
848
+ const destinationTotalUsd = calculateTotalUsdBalance(
849
+ destinationTokenBalancesData,
850
+ destinationTokenPrices,
851
+ )
852
+
853
+ logger.console.log("[recover] Comparing USD balances:", {
854
+ originAddress: intentData.originIntentAddress,
855
+ originTotalUsd: `$${originTotalUsd.toFixed(2)}`,
856
+ destinationAddress: intentData.destinationIntentAddress,
857
+ destinationTotalUsd: `$${destinationTotalUsd.toFixed(2)}`,
858
+ })
859
+
860
+ // If destination has a bigger USD balance, use it
861
+ if (
862
+ destinationIntentAddressForBalances &&
863
+ destinationTotalUsd > originTotalUsd
864
+ ) {
865
+ const chainId = intentData.destinationCalls?.chainId
866
+ if (!chainId) {
867
+ logger.console.warn(
868
+ "[recover] Destination has bigger USD balance but no chainId, using origin",
869
+ )
870
+ // Fallback to origin if no destination chainId
871
+ if (!intentData.originIntentAddress || !originIntentAddressForBalances)
872
+ return null
873
+ return {
874
+ address: originIntentAddressForBalances,
875
+ chainId: Number(intentData.originCalls?.chainId || 0),
876
+ intentAddress: Address.from(intentData.originIntentAddress),
877
+ tokenBalancesData: originTokenBalancesData,
878
+ }
879
+ }
880
+ if (!intentData.destinationIntentAddress) {
881
+ logger.console.warn(
882
+ "[recover] Destination has bigger USD balance but no destinationIntentAddress, using origin",
883
+ )
884
+ // Fallback to origin if no destinationIntentAddress
885
+ if (!intentData.originIntentAddress || !originIntentAddressForBalances)
886
+ return null
887
+ return {
888
+ address: originIntentAddressForBalances,
889
+ chainId: Number(intentData.originCalls?.chainId || 0),
890
+ intentAddress: Address.from(intentData.originIntentAddress),
891
+ tokenBalancesData: originTokenBalancesData,
892
+ }
893
+ }
894
+ return {
895
+ address: destinationIntentAddressForBalances,
896
+ chainId: Number(chainId),
897
+ intentAddress: Address.from(intentData.destinationIntentAddress),
898
+ tokenBalancesData: destinationTokenBalancesData,
899
+ }
900
+ }
901
+
902
+ // Otherwise use origin (or if destination doesn't exist)
903
+ if (!intentData.originIntentAddress || !originIntentAddressForBalances)
904
+ return null
905
+ return {
906
+ address: originIntentAddressForBalances,
907
+ chainId: Number(intentData.originCalls?.chainId || 0),
908
+ intentAddress: Address.from(intentData.originIntentAddress),
909
+ tokenBalancesData: originTokenBalancesData,
910
+ }
911
+ }, [
912
+ intentData,
913
+ originTokenBalancesData,
914
+ destinationTokenBalancesData,
915
+ originIntentAddressForBalances,
916
+ destinationIntentAddressForBalances,
917
+ originTokenPrices,
918
+ destinationTokenPrices,
919
+ calculateTotalUsdBalance,
920
+ ])
921
+
922
+ const isLoadingBalances =
923
+ isLoadingOriginBalances || isLoadingDestinationBalances
924
+ const balancesError = originBalanceError || destinationBalanceError
925
+
926
+ // Check if ANY intent address (origin or destination) has any balance (native or ERC20)
927
+ // This ensures we show the recover button if there are balances in either address
928
+ const hasIntentBalance = useMemo<boolean>(() => {
929
+ // Helper function to check if balances data has any balance
930
+ const checkBalances = (
931
+ balancesData:
932
+ | {
933
+ nativeBalances?: Array<{ balance?: string; chainId?: number }>
934
+ balances?: Array<{
935
+ balance?: string
936
+ contractAddress?: string
937
+ contractInfo?: { decimals?: number; chainId?: number }
938
+ }>
939
+ }
940
+ | undefined,
941
+ ): boolean => {
942
+ if (!balancesData) return false
943
+
944
+ // Check native token balances
945
+ if (balancesData.nativeBalances) {
946
+ for (const nativeBalance of balancesData.nativeBalances) {
947
+ const balance = BigInt(nativeBalance.balance || "0")
948
+ if (balance > 0n) {
949
+ return true
950
+ }
951
+ }
952
+ }
953
+
954
+ // Check ERC20 token balances
955
+ if (balancesData.balances) {
956
+ for (const tokenBalance of balancesData.balances) {
957
+ const balance = BigInt(tokenBalance.balance || "0")
958
+ if (balance > 0n) {
959
+ return true
960
+ }
961
+ }
962
+ }
963
+
964
+ return false
965
+ }
966
+
967
+ // Check both origin and destination balances
968
+ const originHasBalance = checkBalances(originTokenBalancesData)
969
+ const destinationHasBalance = checkBalances(destinationTokenBalancesData)
658
970
 
659
- return false
660
- }, [tokenBalancesData])
971
+ return originHasBalance || destinationHasBalance
972
+ }, [originTokenBalancesData, destinationTokenBalancesData])
661
973
 
662
974
  // Get refund address (use walletClient account or provided address)
663
975
  // Don't throw during render - validate when methods are called
@@ -724,18 +1036,32 @@ export function useTrailsRefund({
724
1036
  })
725
1037
  }
726
1038
 
727
- // Determine refund call from token balances
1039
+ // Determine which intent address to recover from (biggest balance)
1040
+ if (!selectedIntentAddress) {
1041
+ throw new Error("No intent address selected for recovery")
1042
+ }
1043
+
1044
+ logger.console.log("[recover] Selected intent address for recovery:", {
1045
+ address: selectedIntentAddress.address,
1046
+ chainId: selectedIntentAddress.chainId,
1047
+ intentAddress: selectedIntentAddress.intentAddress,
1048
+ })
1049
+
1050
+ // Determine refund call from token balances of selected address
728
1051
  const refundCall = determineRefundCall(
729
- tokenBalancesData,
1052
+ selectedIntentAddress.tokenBalancesData,
730
1053
  effectiveRefundAddress,
731
1054
  )
732
1055
 
733
- // Get chain ID from origin calls (this is the chain where the refund should execute)
734
- const chainId = intentData.originCalls?.chainId
1056
+ // Use chain ID from selected intent address
1057
+ const chainId = selectedIntentAddress.chainId
735
1058
  if (!chainId) {
736
- throw new Error("Chain ID not found in origin calls")
1059
+ throw new Error("Chain ID not found for selected intent address")
737
1060
  }
738
- logger.console.log("[refund] Using chain ID from origin calls:", chainId)
1061
+ logger.console.log(
1062
+ "[recover] Using chain ID from selected intent:",
1063
+ chainId,
1064
+ )
739
1065
 
740
1066
  // Switch to the correct chain before signing
741
1067
  if (walletClient) {
@@ -756,11 +1082,11 @@ export function useTrailsRefund({
756
1082
  }
757
1083
  }
758
1084
 
759
- // Get intent address (needed for buildRefundTransactionWithSignature)
760
- if (!intentData.originIntentAddress) {
761
- throw new Error("Intent data missing originIntentAddress")
1085
+ // Use the selected intent address (biggest balance)
1086
+ if (!selectedIntentAddress) {
1087
+ throw new Error("No intent address selected for recovery")
762
1088
  }
763
- const intentAddress = Address.from(intentData.originIntentAddress)
1089
+ const intentAddress = selectedIntentAddress.intentAddress
764
1090
 
765
1091
  // Create payload with random space and calculate hash for signing
766
1092
  const payload = createRefundPayload(
@@ -783,12 +1109,12 @@ export function useTrailsRefund({
783
1109
  payload,
784
1110
  refundCall,
785
1111
  }
786
- }, [intentData, walletClient, tokenBalancesData, effectiveRefundAddress])
1112
+ }, [intentData, walletClient, selectedIntentAddress, effectiveRefundAddress])
787
1113
 
788
1114
  // Export as signPayload for the hook return
789
1115
  const signPayload = signPayloadCallback
790
1116
 
791
- const getRefundTx = useCallback(
1117
+ const getRecoverTx = useCallback(
792
1118
  async (params: {
793
1119
  signedHash: string
794
1120
  payload: Payload.Calls
@@ -847,24 +1173,29 @@ export function useTrailsRefund({
847
1173
  })
848
1174
  }
849
1175
 
1176
+ // Use the selected intent address (biggest balance)
1177
+ if (!selectedIntentAddress) {
1178
+ throw new Error("No intent address selected for recovery")
1179
+ }
1180
+
850
1181
  // Use the refund call from the payload (passed from signPayload)
851
1182
  const refundCall = params.payload.calls[0]
852
1183
  if (!refundCall) {
853
1184
  throw new Error("Refund call not found in payload")
854
1185
  }
855
1186
 
856
- // Get chain ID from origin calls (this is the chain where the refund should execute)
857
- const chainId = intentData.originCalls?.chainId
1187
+ // Use chain ID from selected intent address
1188
+ const chainId = selectedIntentAddress.chainId
858
1189
  if (!chainId) {
859
- throw new Error("Chain ID not found in origin calls")
1190
+ throw new Error("Chain ID not found for selected intent address")
860
1191
  }
861
- logger.console.log("[refund] Using chain ID from origin calls:", chainId)
1192
+ logger.console.log(
1193
+ "[recover] Using chain ID from selected intent:",
1194
+ chainId,
1195
+ )
862
1196
 
863
- // Get intent address
864
- if (!intentData.originIntentAddress) {
865
- throw new Error("Intent data missing originIntentAddress")
866
- }
867
- const intentAddress = Address.from(intentData.originIntentAddress)
1197
+ // Use the selected intent address
1198
+ const intentAddress = selectedIntentAddress.intentAddress
868
1199
 
869
1200
  // Build transaction with signature using the payload passed from signPayload
870
1201
  const mainSigner = walletClient.account.address as Address.Address
@@ -897,7 +1228,180 @@ export function useTrailsRefund({
897
1228
  chain: chainInfo as Chain,
898
1229
  }
899
1230
  },
900
- [intentData, walletClient, effectiveRefundAddress],
1231
+ [intentData, walletClient, selectedIntentAddress, effectiveRefundAddress],
1232
+ )
1233
+
1234
+ const recover = useCallback(async (): Promise<{
1235
+ txHash: `0x${string}`
1236
+ receipt: TransactionReceipt
1237
+ chainId: number
1238
+ }> => {
1239
+ if (!intentData || !walletClient || !walletClient.account) {
1240
+ throw new Error("Intent data and wallet client with account required")
1241
+ }
1242
+
1243
+ // Validate refund address
1244
+ if (!effectiveRefundAddress) {
1245
+ throw new Error(
1246
+ "Refund address is required. Provide refundToAddress or connect a wallet with a valid address",
1247
+ )
1248
+ }
1249
+ if (Address.isEqual(effectiveRefundAddress, zeroAddress)) {
1250
+ throw new Error("Refund address cannot be zero address")
1251
+ }
1252
+
1253
+ logger.console.log("[recover] Starting recover process")
1254
+
1255
+ // Step 1: Sign the payload
1256
+ const { signature, payload } = await signPayload()
1257
+ logger.console.log("[recover] Payload signed:", signature)
1258
+
1259
+ // Step 2: Get the recover transaction
1260
+ const recoverTx = await getRecoverTx({
1261
+ signedHash: signature,
1262
+ payload,
1263
+ })
1264
+ logger.console.log("[recover] Recover transaction built:", recoverTx)
1265
+
1266
+ // Step 3: Switch to the correct chain if needed
1267
+ await attemptSwitchChain({
1268
+ walletClient,
1269
+ desiredChainId: recoverTx.chainId,
1270
+ })
1271
+ logger.console.log("[recover] Switched to chain:", recoverTx.chainId)
1272
+
1273
+ // Step 4: Send the transaction
1274
+ if (!walletClient.account) {
1275
+ throw new Error(
1276
+ "Wallet client account is required for sending transaction",
1277
+ )
1278
+ }
1279
+ const txHash = await walletClient.sendTransaction({
1280
+ account: walletClient.account,
1281
+ to: recoverTx.to,
1282
+ data: recoverTx.data,
1283
+ chain: recoverTx.chain,
1284
+ })
1285
+ logger.console.log("[recover] Transaction sent:", txHash)
1286
+
1287
+ // Step 5: Wait for receipt
1288
+ const publicClient = getChainRpcClient(recoverTx.chainId)
1289
+ const receipt = await publicClient.waitForTransactionReceipt({
1290
+ hash: txHash,
1291
+ })
1292
+ logger.console.log("[recover] Transaction receipt received:", receipt)
1293
+
1294
+ return {
1295
+ txHash,
1296
+ receipt,
1297
+ chainId: recoverTx.chainId,
1298
+ }
1299
+ }, [
1300
+ intentData,
1301
+ walletClient,
1302
+ effectiveRefundAddress,
1303
+ signPayload,
1304
+ getRecoverTx,
1305
+ ])
1306
+
1307
+ const getRecoverStatus = useCallback(
1308
+ async (params: {
1309
+ txHash: `0x${string}`
1310
+ chainId: number
1311
+ }): Promise<{
1312
+ status: "success" | "fail" | "pending"
1313
+ }> => {
1314
+ const { txHash, chainId } = params
1315
+
1316
+ logger.console.log("[recover] Getting recover status:", {
1317
+ txHash,
1318
+ chainId,
1319
+ })
1320
+
1321
+ const publicClient = getChainRpcClient(chainId)
1322
+
1323
+ // First, check if the transaction exists (might be pending)
1324
+ try {
1325
+ const transaction = await publicClient.getTransaction({
1326
+ hash: txHash,
1327
+ })
1328
+
1329
+ // If transaction exists but we can't get receipt, it's pending
1330
+ if (transaction) {
1331
+ try {
1332
+ const receipt = await publicClient.getTransactionReceipt({
1333
+ hash: txHash,
1334
+ })
1335
+
1336
+ // Transaction has been mined, check status
1337
+ // Check if transaction itself failed
1338
+ if (receipt.status === "reverted") {
1339
+ logger.console.log("[recover] Transaction reverted")
1340
+ return { status: "fail" }
1341
+ }
1342
+
1343
+ // Decode events to check for CallFailed events
1344
+ const guestModuleEvents = decodeGuestModuleEvents(receipt)
1345
+ const hasCallFailed = guestModuleEvents.some(
1346
+ (event) => event.type === "CallFailed",
1347
+ )
1348
+
1349
+ if (hasCallFailed) {
1350
+ logger.console.log(
1351
+ "[recover] Found CallFailed events in transaction",
1352
+ )
1353
+ return { status: "fail" }
1354
+ }
1355
+
1356
+ logger.console.log(
1357
+ "[recover] Transaction succeeded with no failures",
1358
+ )
1359
+ return { status: "success" }
1360
+ } catch {
1361
+ // Receipt not available yet, transaction is pending
1362
+ logger.console.log(
1363
+ "[recover] Transaction receipt not available, pending",
1364
+ )
1365
+ return { status: "pending" }
1366
+ }
1367
+ }
1368
+ } catch (error) {
1369
+ // Transaction might not exist yet or there's an error
1370
+ logger.console.warn("[recover] Error getting transaction:", error)
1371
+ // If we can't find the transaction, it might be pending or invalid
1372
+ // Try to get receipt anyway as a fallback
1373
+ try {
1374
+ const receipt = await publicClient.getTransactionReceipt({
1375
+ hash: txHash,
1376
+ })
1377
+
1378
+ if (receipt.status === "reverted") {
1379
+ return { status: "fail" }
1380
+ }
1381
+
1382
+ const guestModuleEvents = decodeGuestModuleEvents(receipt)
1383
+ const hasCallFailed = guestModuleEvents.some(
1384
+ (event) => event.type === "CallFailed",
1385
+ )
1386
+
1387
+ if (hasCallFailed) {
1388
+ return { status: "fail" }
1389
+ }
1390
+
1391
+ return { status: "success" }
1392
+ } catch {
1393
+ // If we can't get receipt either, assume pending
1394
+ logger.console.log(
1395
+ "[recover] Cannot determine status, assuming pending",
1396
+ )
1397
+ return { status: "pending" }
1398
+ }
1399
+ }
1400
+
1401
+ // Fallback: if we can't determine, assume pending
1402
+ return { status: "pending" }
1403
+ },
1404
+ [],
901
1405
  )
902
1406
 
903
1407
  return {
@@ -909,6 +1413,8 @@ export function useTrailsRefund({
909
1413
  hasIntentBalance,
910
1414
  refetchIntent,
911
1415
  signPayload,
912
- getRefundTx,
1416
+ getRecoverTx,
1417
+ recover,
1418
+ getRecoverStatus,
913
1419
  }
914
1420
  }