@sodax/dapp-kit 1.5.7-beta → 2.0.0-rc.1

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 (202) hide show
  1. package/README.md +300 -422
  2. package/ai-exported/AGENTS.md +134 -0
  3. package/ai-exported/integration/README.md +49 -0
  4. package/ai-exported/integration/ai-rules.md +79 -0
  5. package/ai-exported/integration/architecture.md +274 -0
  6. package/ai-exported/integration/features/README.md +29 -0
  7. package/ai-exported/integration/features/auxiliary-services.md +169 -0
  8. package/ai-exported/integration/features/bitcoin.md +87 -0
  9. package/ai-exported/integration/features/bridge.md +91 -0
  10. package/ai-exported/integration/features/dex.md +152 -0
  11. package/ai-exported/integration/features/migration.md +118 -0
  12. package/ai-exported/integration/features/money-market.md +116 -0
  13. package/ai-exported/integration/features/staking.md +123 -0
  14. package/ai-exported/integration/features/swap.md +101 -0
  15. package/ai-exported/integration/quickstart.md +187 -0
  16. package/ai-exported/integration/recipes/README.md +136 -0
  17. package/ai-exported/integration/recipes/backend-queries.md +157 -0
  18. package/ai-exported/integration/recipes/bitcoin.md +193 -0
  19. package/ai-exported/integration/recipes/bridge.md +174 -0
  20. package/ai-exported/integration/recipes/dex.md +204 -0
  21. package/ai-exported/integration/recipes/invalidations.md +115 -0
  22. package/ai-exported/integration/recipes/migration.md +212 -0
  23. package/ai-exported/integration/recipes/money-market.md +206 -0
  24. package/ai-exported/integration/recipes/mutation-error-handling.md +118 -0
  25. package/ai-exported/integration/recipes/observability.md +93 -0
  26. package/ai-exported/integration/recipes/setup.md +144 -0
  27. package/ai-exported/integration/recipes/staking.md +202 -0
  28. package/ai-exported/integration/recipes/swap.md +272 -0
  29. package/ai-exported/integration/recipes/wallet-connectivity.md +101 -0
  30. package/ai-exported/integration/reference/README.md +12 -0
  31. package/ai-exported/integration/reference/glossary.md +188 -0
  32. package/ai-exported/integration/reference/hooks-index.md +194 -0
  33. package/ai-exported/integration/reference/public-api.md +110 -0
  34. package/ai-exported/integration/reference/querykey-conventions.md +179 -0
  35. package/ai-exported/migration/README.md +60 -0
  36. package/ai-exported/migration/ai-rules.md +81 -0
  37. package/ai-exported/migration/breaking-changes/hook-signatures.md +233 -0
  38. package/ai-exported/migration/breaking-changes/querykey-conventions.md +108 -0
  39. package/ai-exported/migration/breaking-changes/result-handling.md +211 -0
  40. package/ai-exported/migration/breaking-changes/sdk-leakage.md +165 -0
  41. package/ai-exported/migration/checklist.md +89 -0
  42. package/ai-exported/migration/features/README.md +34 -0
  43. package/ai-exported/migration/features/auxiliary-services.md +114 -0
  44. package/ai-exported/migration/features/bitcoin.md +88 -0
  45. package/ai-exported/migration/features/bridge.md +123 -0
  46. package/ai-exported/migration/features/dex.md +101 -0
  47. package/ai-exported/migration/features/migration.md +120 -0
  48. package/ai-exported/migration/features/money-market.md +97 -0
  49. package/ai-exported/migration/features/staking.md +109 -0
  50. package/ai-exported/migration/features/swap.md +118 -0
  51. package/ai-exported/migration/recipes.md +188 -0
  52. package/ai-exported/migration/reference/README.md +15 -0
  53. package/ai-exported/migration/reference/deleted-hooks.md +110 -0
  54. package/ai-exported/migration/reference/error-shape-crosswalk.md +144 -0
  55. package/ai-exported/migration/reference/renamed-hooks.md +66 -0
  56. package/dist/index.cjs +2642 -0
  57. package/dist/index.cjs.map +1 -0
  58. package/dist/index.d.cts +1550 -0
  59. package/dist/index.d.ts +1020 -2051
  60. package/dist/index.mjs +1581 -1531
  61. package/dist/index.mjs.map +1 -1
  62. package/package.json +20 -10
  63. package/src/contexts/index.ts +0 -3
  64. package/src/hooks/_mutationContract.test.ts +99 -0
  65. package/src/hooks/backend/README.md +2 -2
  66. package/src/hooks/backend/index.ts +13 -13
  67. package/src/hooks/backend/unwrapResult.ts +1 -0
  68. package/src/hooks/backend/useBackendAllMoneyMarketAssets.ts +13 -45
  69. package/src/hooks/backend/useBackendAllMoneyMarketBorrowers.ts +29 -59
  70. package/src/hooks/backend/useBackendIntentByHash.ts +21 -47
  71. package/src/hooks/backend/useBackendIntentByTxHash.ts +23 -50
  72. package/src/hooks/backend/useBackendMoneyMarketAsset.ts +21 -54
  73. package/src/hooks/backend/useBackendMoneyMarketAssetBorrowers.ts +30 -57
  74. package/src/hooks/backend/useBackendMoneyMarketAssetSuppliers.ts +31 -58
  75. package/src/hooks/backend/useBackendMoneyMarketPosition.ts +22 -38
  76. package/src/hooks/backend/useBackendOrderbook.ts +27 -49
  77. package/src/hooks/backend/useBackendSubmitSwapTx.ts +30 -36
  78. package/src/hooks/backend/useBackendSubmitSwapTxStatus.ts +38 -58
  79. package/src/hooks/backend/useBackendUserIntents.ts +25 -63
  80. package/src/hooks/bitcoin/index.ts +9 -8
  81. package/src/hooks/bitcoin/useBitcoinBalance.ts +20 -5
  82. package/src/hooks/bitcoin/useExpiredUtxos.ts +26 -16
  83. package/src/hooks/bitcoin/useFundTradingWallet.ts +33 -30
  84. package/src/hooks/bitcoin/useRadfiAuth.ts +43 -40
  85. package/src/hooks/bitcoin/useRadfiSession.ts +53 -59
  86. package/src/hooks/bitcoin/useRadfiWithdraw.ts +35 -53
  87. package/src/hooks/bitcoin/useRenewUtxos.ts +30 -50
  88. package/src/hooks/bitcoin/useTradingWallet.ts +1 -1
  89. package/src/hooks/bitcoin/useTradingWalletBalance.ts +25 -14
  90. package/src/hooks/bridge/index.ts +5 -5
  91. package/src/hooks/bridge/useBridge.ts +29 -55
  92. package/src/hooks/bridge/useBridgeAllowance.ts +38 -38
  93. package/src/hooks/bridge/useBridgeApprove.ts +32 -57
  94. package/src/hooks/bridge/useGetBridgeableAmount.ts +23 -37
  95. package/src/hooks/bridge/useGetBridgeableTokens.ts +27 -50
  96. package/src/hooks/dex/index.ts +16 -16
  97. package/src/hooks/dex/useClaimRewards.ts +35 -54
  98. package/src/hooks/dex/useCreateDecreaseLiquidityParams.ts +7 -20
  99. package/src/hooks/dex/useCreateDepositParams.ts +7 -21
  100. package/src/hooks/dex/useCreateSupplyLiquidityParams.ts +13 -28
  101. package/src/hooks/dex/useCreateWithdrawParams.ts +7 -20
  102. package/src/hooks/dex/useDecreaseLiquidity.ts +40 -66
  103. package/src/hooks/dex/useDexAllowance.ts +29 -75
  104. package/src/hooks/dex/useDexApprove.ts +32 -43
  105. package/src/hooks/dex/useDexDeposit.ts +42 -49
  106. package/src/hooks/dex/useDexWithdraw.ts +32 -43
  107. package/src/hooks/dex/useLiquidityAmounts.ts +13 -82
  108. package/src/hooks/dex/usePoolBalances.ts +50 -72
  109. package/src/hooks/dex/usePoolData.ts +17 -43
  110. package/src/hooks/dex/usePools.ts +11 -38
  111. package/src/hooks/dex/usePositionInfo.ts +27 -62
  112. package/src/hooks/dex/useSupplyLiquidity.ts +80 -75
  113. package/src/hooks/index.ts +12 -10
  114. package/src/hooks/migrate/index.ts +13 -4
  115. package/src/hooks/migrate/useMigrateBaln.ts +42 -0
  116. package/src/hooks/migrate/useMigrateIcxToSoda.ts +44 -0
  117. package/src/hooks/migrate/useMigratebnUSD.ts +47 -0
  118. package/src/hooks/migrate/useMigrationAllowance.ts +76 -0
  119. package/src/hooks/migrate/useMigrationApprove.ts +66 -0
  120. package/src/hooks/migrate/useRevertMigrateSodaToIcx.ts +39 -0
  121. package/src/hooks/mm/index.ts +14 -12
  122. package/src/hooks/mm/useAToken.ts +25 -41
  123. package/src/hooks/mm/useATokensBalances.ts +29 -60
  124. package/src/hooks/mm/useBorrow.ts +38 -56
  125. package/src/hooks/mm/useMMAllowance.ts +37 -73
  126. package/src/hooks/mm/useMMApprove.ts +36 -43
  127. package/src/hooks/mm/useRepay.ts +33 -53
  128. package/src/hooks/mm/useReservesData.ts +12 -38
  129. package/src/hooks/mm/useReservesHumanized.ts +12 -31
  130. package/src/hooks/mm/useReservesList.ts +11 -31
  131. package/src/hooks/mm/useReservesUsdFormat.ts +15 -35
  132. package/src/hooks/mm/useSupply.ts +45 -51
  133. package/src/hooks/mm/useUserFormattedSummary.ts +32 -84
  134. package/src/hooks/mm/useUserReservesData.ts +27 -77
  135. package/src/hooks/mm/useWithdraw.ts +38 -54
  136. package/src/hooks/partner/index.ts +6 -0
  137. package/src/hooks/partner/useApproveToken.ts +42 -0
  138. package/src/hooks/partner/useFeeClaimSwap.ts +38 -0
  139. package/src/hooks/partner/useFetchAssetsBalances.ts +37 -0
  140. package/src/hooks/partner/useGetAutoSwapPreferences.ts +37 -0
  141. package/src/hooks/partner/useIsTokenApproved.ts +39 -0
  142. package/src/hooks/partner/useSetSwapPreference.ts +50 -0
  143. package/src/hooks/provider/index.ts +1 -2
  144. package/src/hooks/provider/useHubProvider.ts +1 -1
  145. package/src/hooks/recovery/index.ts +2 -0
  146. package/src/hooks/recovery/useHubAssetBalances.ts +43 -0
  147. package/src/hooks/recovery/useWithdrawHubAsset.ts +48 -0
  148. package/src/hooks/shared/index.ts +10 -6
  149. package/src/hooks/shared/types.ts +77 -0
  150. package/src/hooks/shared/unwrapResult.ts +19 -0
  151. package/src/hooks/shared/useDeriveUserWalletAddress.ts +22 -40
  152. package/src/hooks/shared/useEstimateGas.ts +18 -15
  153. package/src/hooks/shared/useGetUserHubWalletAddress.ts +25 -26
  154. package/src/hooks/shared/useRequestTrustline.ts +28 -61
  155. package/src/hooks/shared/useSafeMutation.test.ts +43 -0
  156. package/src/hooks/shared/useSafeMutation.ts +68 -0
  157. package/src/hooks/shared/useSodaxContext.ts +1 -1
  158. package/src/hooks/shared/useStellarTrustlineCheck.ts +30 -64
  159. package/src/hooks/shared/useXBalances.test.ts +113 -0
  160. package/src/hooks/shared/useXBalances.ts +61 -0
  161. package/src/hooks/staking/index.ts +18 -18
  162. package/src/hooks/staking/useCancelUnstake.ts +30 -41
  163. package/src/hooks/staking/useClaim.ts +27 -36
  164. package/src/hooks/staking/useConvertedAssets.ts +24 -34
  165. package/src/hooks/staking/useInstantUnstake.ts +33 -40
  166. package/src/hooks/staking/useInstantUnstakeAllowance.ts +37 -45
  167. package/src/hooks/staking/useInstantUnstakeApprove.ts +42 -42
  168. package/src/hooks/staking/useInstantUnstakeRatio.ts +24 -41
  169. package/src/hooks/staking/useStake.ts +32 -37
  170. package/src/hooks/staking/useStakeAllowance.ts +30 -43
  171. package/src/hooks/staking/useStakeApprove.ts +40 -40
  172. package/src/hooks/staking/useStakeRatio.ts +24 -40
  173. package/src/hooks/staking/useStakingConfig.ts +14 -27
  174. package/src/hooks/staking/useStakingInfo.ts +30 -38
  175. package/src/hooks/staking/useUnstake.ts +29 -43
  176. package/src/hooks/staking/useUnstakeAllowance.ts +37 -44
  177. package/src/hooks/staking/useUnstakeApprove.ts +40 -43
  178. package/src/hooks/staking/useUnstakingInfo.ts +29 -41
  179. package/src/hooks/staking/useUnstakingInfoWithPenalty.ts +31 -47
  180. package/src/hooks/swap/index.ts +8 -8
  181. package/src/hooks/swap/useCancelLimitOrder.ts +24 -41
  182. package/src/hooks/swap/useCancelSwap.ts +24 -33
  183. package/src/hooks/swap/useCreateLimitOrder.ts +29 -62
  184. package/src/hooks/swap/useQuote.ts +17 -43
  185. package/src/hooks/swap/useStatus.ts +22 -29
  186. package/src/hooks/swap/useSwap.ts +31 -49
  187. package/src/hooks/swap/useSwapAllowance.ts +38 -35
  188. package/src/hooks/swap/useSwapApprove.ts +48 -57
  189. package/src/index.ts +5 -3
  190. package/src/providers/SodaxProvider.tsx +17 -11
  191. package/src/providers/createSodaxQueryClient.ts +96 -0
  192. package/src/providers/index.ts +2 -1
  193. package/src/utils/dex-utils.ts +27 -5
  194. package/src/utils/index.ts +1 -1
  195. package/dist/index.d.mts +0 -2581
  196. package/dist/index.js +0 -2574
  197. package/dist/index.js.map +0 -1
  198. package/src/hooks/migrate/types.ts +0 -15
  199. package/src/hooks/migrate/useMigrate.tsx +0 -110
  200. package/src/hooks/migrate/useMigrationAllowance.tsx +0 -79
  201. package/src/hooks/migrate/useMigrationApprove.tsx +0 -129
  202. package/src/hooks/provider/useSpokeProvider.ts +0 -172
@@ -1,7 +1,16 @@
1
- import { type SpokeProvider, type SpokeChainId, HubService } from '@sodax/sdk';
1
+ import type { SpokeChainKey } from '@sodax/sdk';
2
2
  import { useQuery, type UseQueryResult } from '@tanstack/react-query';
3
- import { useSodaxContext } from './useSodaxContext';
3
+ import { useSodaxContext } from './useSodaxContext.js';
4
4
  import type { Address } from 'viem';
5
+ import type { ReadHookParams } from './types.js';
6
+
7
+ export type UseGetUserHubWalletAddressParams = ReadHookParams<
8
+ Address,
9
+ {
10
+ spokeChainId?: SpokeChainKey;
11
+ spokeAddress?: string;
12
+ }
13
+ >;
5
14
 
6
15
  /**
7
16
  * Hook for deriving user wallet address for hub abstraction.
@@ -14,41 +23,31 @@ import type { Address } from 'viem';
14
23
  * The query is automatically enabled when both `spokeChainId` and `spokeAddress` are provided.
15
24
  * This is a deterministic operation, so the result is cached and not refetched automatically.
16
25
  *
17
- * @param spokeChainId - Optional spoke chain ID. If not provided, the query will be disabled.
18
- * @param spokeAddress - Optional user wallet address on the spoke chain. If not provided, the query will be disabled.
19
- * @returns A React Query result object containing:
20
- * - data: The derived user wallet address (Address) when available
21
- * - isLoading: Loading state indicator
22
- * - error: Any error that occurred during derivation (Error)
23
- *
24
26
  * @example
25
27
  * ```typescript
26
- * const { data: derivedAddress, isLoading, error } = useDeriveUserWalletAddress(spokeChainId, userAddress);
27
- *
28
- * if (isLoading) return <div>Deriving address...</div>;
29
- * if (error) return <div>Error: {error.message}</div>;
30
- * if (derivedAddress) return <div>Derived Address: {derivedAddress}</div>;
28
+ * const { data: derivedAddress, isLoading, error } = useGetUserHubWalletAddress({
29
+ * params: { spokeChainId, spokeAddress: userAddress },
30
+ * });
31
31
  * ```
32
32
  */
33
- export function useGetUserHubWalletAddress(
34
- spokeChainId?: SpokeChainId | SpokeProvider | undefined,
35
- spokeAddress?: string | undefined,
36
- ): UseQueryResult<Address, Error> {
33
+ export function useGetUserHubWalletAddress({
34
+ params,
35
+ queryOptions,
36
+ }: UseGetUserHubWalletAddressParams = {}): UseQueryResult<Address, Error> {
37
37
  const { sodax } = useSodaxContext();
38
+ const spokeChainId = params?.spokeChainId;
39
+ const spokeAddress = params?.spokeAddress;
38
40
 
39
- return useQuery({
40
- queryKey: ['getUserHubWalletAddress', spokeChainId, spokeAddress],
41
+ return useQuery<Address, Error>({
42
+ queryKey: ['shared', 'userHubWalletAddress', spokeChainId, spokeAddress],
41
43
  queryFn: async (): Promise<Address> => {
42
44
  if (!spokeChainId || !spokeAddress) {
43
45
  throw new Error('Spoke chain id and address are required');
44
46
  }
45
-
46
- // Determine if spokeChainId is a SpokeProvider object or SpokeChainId value
47
- spokeChainId = typeof spokeChainId === 'object' ? spokeChainId.chainConfig.chain.id : spokeChainId;
48
-
49
- return await HubService.getUserHubWalletAddress(spokeAddress, spokeChainId, sodax.hubProvider);
47
+ return await sodax.hubProvider.getUserHubWalletAddress(spokeAddress, spokeChainId);
50
48
  },
51
49
  enabled: !!spokeChainId && !!spokeAddress,
52
- refetchInterval: false, // This is a deterministic operation, no need to refetch
50
+ refetchInterval: false,
51
+ ...queryOptions,
53
52
  });
54
53
  }
@@ -1,86 +1,59 @@
1
- import { type SpokeProvider, StellarSpokeProvider, StellarSpokeService, type TxReturnType } from '@sodax/sdk';
2
1
  import { useQueryClient } from '@tanstack/react-query';
3
2
  import { useCallback, useState } from 'react';
3
+ import type { IStellarWalletProvider, StellarChainKey } from '@sodax/sdk';
4
+ import { useSodaxContext } from './useSodaxContext.js';
4
5
 
5
- /**
6
- * React hook to request a Stellar trustline for a given token and amount.
7
- *
8
- * This hook provides a callback function for requesting a trustline on the Stellar network
9
- * using the provided SpokeProvider. It is intended for use with StellarSpokeProvider
10
- * and will throw if used with a non-Stellar provider. Upon success, it invalidates
11
- * the trustline check query to ensure UI reflects the updated trustline state.
12
- *
13
- * @template T - The type of SpokeProvider, defaults to SpokeProvider.
14
- * @param {string | undefined} token - The Stellar asset code or token address for which to request a trustline.
15
- * @returns {Object} An object containing:
16
- * - `requestTrustline`: Function to trigger the trustline request.
17
- * - `isLoading`: Whether the request is in progress.
18
- * - `isRequested`: Whether a trustline has been successfully requested.
19
- * - `error`: Any error encountered during the request.
20
- * - `data`: The transaction result if successful.
21
- *
22
- * @example
23
- * ```tsx
24
- * import { useRequestTrustline } from '@sodax/dapp-kit';
25
- *
26
- * const { requestTrustline, isLoading, isRequested, error, data } = useRequestTrustline('USDC-G...TOKEN');
27
- *
28
- * // To request a trustline:
29
- * await requestTrustline({
30
- * token: 'USDC-G...TOKEN',
31
- * amount: 10000000n,
32
- * spokeProvider: stellarProvider,
33
- * });
34
- *
35
- * if (isLoading) return <span>Requesting trustline...</span>;
36
- * if (error) return <span>Error: {error.message}</span>;
37
- * if (isRequested) return <span>Trustline requested! Tx: {data?.txHash}</span>;
38
- * ```
39
- */
40
-
41
- export function useRequestTrustline<T extends SpokeProvider = SpokeProvider>(
42
- token: string | undefined,
43
- ): {
6
+ export function useRequestTrustline(token: string | undefined): {
44
7
  requestTrustline: (params: {
45
8
  token: string;
46
9
  amount: bigint;
47
- spokeProvider: T;
48
- }) => Promise<TxReturnType<StellarSpokeProvider, false>>;
10
+ srcChainKey: StellarChainKey;
11
+ walletProvider: IStellarWalletProvider;
12
+ }) => Promise<string>;
49
13
  isLoading: boolean;
50
14
  isRequested: boolean;
51
15
  error: Error | null;
52
- data: TxReturnType<StellarSpokeProvider, false> | null;
16
+ data: string | null;
53
17
  } {
18
+ const { sodax } = useSodaxContext();
54
19
  const queryClient = useQueryClient();
55
20
  const [isLoading, setIsLoading] = useState<boolean>(false);
56
21
  const [isRequested, setIsRequested] = useState<boolean>(false);
57
22
  const [error, setError] = useState<Error | null>(null);
58
- const [data, setData] = useState<TxReturnType<StellarSpokeProvider, false> | null>(null);
23
+ const [data, setData] = useState<string | null>(null);
59
24
 
60
25
  const requestTrustline = useCallback(
61
26
  async ({
62
27
  token,
63
28
  amount,
64
- spokeProvider,
29
+ srcChainKey,
30
+ walletProvider,
65
31
  }: {
66
32
  token: string;
67
33
  amount: bigint;
68
- spokeProvider: T;
69
- }): Promise<TxReturnType<StellarSpokeProvider, false>> => {
70
- if (!spokeProvider || !token || !amount || !(spokeProvider instanceof StellarSpokeProvider)) {
71
- const error = new Error('Spoke provider, token or amount not found');
34
+ srcChainKey: StellarChainKey;
35
+ walletProvider: IStellarWalletProvider;
36
+ }): Promise<string> => {
37
+ if (!token || !amount) {
38
+ const error = new Error('Token and amount are required');
72
39
  setError(error);
73
40
  throw error;
74
41
  }
75
-
76
42
  setIsLoading(true);
77
43
  setError(null);
78
-
79
44
  try {
80
- const result = await StellarSpokeService.requestTrustline(token, amount, spokeProvider);
45
+ const srcAddress = await walletProvider.getWalletAddress();
46
+ const result = await sodax.spoke.stellar.requestTrustline<false>({
47
+ raw: false,
48
+ srcChainKey,
49
+ srcAddress,
50
+ token,
51
+ amount,
52
+ walletProvider,
53
+ });
81
54
  setData(result);
82
55
  setIsRequested(true);
83
- queryClient.invalidateQueries({ queryKey: ['stellar-trustline-check', token] });
56
+ queryClient.invalidateQueries({ queryKey: ['shared', 'stellarTrustlineCheck', token] });
84
57
  return result;
85
58
  } catch (err) {
86
59
  const error = err instanceof Error ? err : new Error('Unknown error occurred');
@@ -90,14 +63,8 @@ export function useRequestTrustline<T extends SpokeProvider = SpokeProvider>(
90
63
  setIsLoading(false);
91
64
  }
92
65
  },
93
- [queryClient],
66
+ [queryClient, sodax],
94
67
  );
95
68
 
96
- return {
97
- requestTrustline,
98
- isLoading,
99
- isRequested,
100
- error,
101
- data,
102
- };
69
+ return { requestTrustline, isLoading, isRequested, error, data };
103
70
  }
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { toResult } from './useSafeMutation.js';
3
+
4
+ describe('toResult', () => {
5
+ it('packs a resolved promise into { ok: true, value }', async () => {
6
+ const r = await toResult(Promise.resolve(42));
7
+ expect(r).toEqual({ ok: true, value: 42 });
8
+ });
9
+
10
+ it('packs a rejected promise into { ok: false, error } — never rejects', async () => {
11
+ const err = new Error('user rejected');
12
+ const r = await toResult(Promise.reject(err));
13
+ expect(r).toEqual({ ok: false, error: err });
14
+ });
15
+
16
+ it('preserves non-Error throwables', async () => {
17
+ const r = await toResult(Promise.reject('boom'));
18
+ expect(r.ok).toBe(false);
19
+ if (!r.ok) expect(r.error).toBe('boom');
20
+ });
21
+
22
+ it('forwards the resolved value verbatim (no copy/transform)', async () => {
23
+ const value = { spokeTxHash: '0xaaa', hubTxHash: '0xbbb' };
24
+ const r = await toResult(Promise.resolve(value));
25
+ if (r.ok) expect(r.value).toBe(value); // same reference
26
+ });
27
+
28
+ it('does not catch synchronous throws above the await — caller must produce a Promise', async () => {
29
+ // Documents the contract: toResult only neutralizes Promise rejections, not sync throws
30
+ // before the Promise is constructed. Hooks always pass `mutateAsync(vars)` which is async.
31
+ const wrapped = (): Promise<Result<number>> =>
32
+ toResult(
33
+ (async () => {
34
+ throw new Error('async throw');
35
+ })(),
36
+ );
37
+ const r = await wrapped();
38
+ expect(r.ok).toBe(false);
39
+ });
40
+ });
41
+
42
+ // Local type alias to keep the file self-contained — mirrors @sodax/sdk's Result<T>.
43
+ type Result<T> = { ok: true; value: T } | { ok: false; error: unknown };
@@ -0,0 +1,68 @@
1
+ // packages/dapp-kit/src/hooks/shared/useSafeMutation.ts
2
+ import type { Result } from '@sodax/sdk';
3
+ import {
4
+ useMutation,
5
+ type MutateOptions,
6
+ type UseMutationOptions,
7
+ type UseMutationResult,
8
+ } from '@tanstack/react-query';
9
+ import { useCallback } from 'react';
10
+
11
+ /**
12
+ * Return shape of every dapp-kit mutation hook. Extends `UseMutationResult` with one extra
13
+ * method, `mutateAsyncSafe`, that never rejects — it returns the SDK's `Result<T>` shape so
14
+ * callers can branch on `.ok` without `try/catch`.
15
+ *
16
+ * The underlying `mutationFn` still throws on SDK failure, so React Query's native error model
17
+ * (`isError`, `error`, `onError`, `retry`, `throwOnError`, devtools) keeps working as documented.
18
+ */
19
+ export type SafeUseMutationResult<TData, TError, TVars, TContext = unknown> = UseMutationResult<
20
+ TData,
21
+ TError,
22
+ TVars,
23
+ TContext
24
+ > & {
25
+ /**
26
+ * Like `mutateAsync` but never rejects. Returns `Result<TData>` so callers can branch
27
+ * on `.ok` without `try/catch`.
28
+ *
29
+ * Use this for imperative flows where rejection-style errors are awkward — e.g. sequential
30
+ * `if (!hasAllowance) await approve(...); await action(...)` chains where the user-reject
31
+ * case is the modal failure mode, not an exceptional one.
32
+ */
33
+ mutateAsyncSafe: (
34
+ vars: TVars,
35
+ options?: MutateOptions<TData, TError, TVars, TContext>,
36
+ ) => Promise<Result<TData>>;
37
+ };
38
+
39
+ /**
40
+ * Wraps a `Promise<T>` (typically `mutateAsync(vars)`) into a `Promise<Result<T>>` that never
41
+ * rejects. Pure, side-effect-free — extracted for unit testing.
42
+ */
43
+ export async function toResult<T>(promise: Promise<T>): Promise<Result<T>> {
44
+ try {
45
+ const value = await promise;
46
+ return { ok: true, value };
47
+ } catch (error) {
48
+ return { ok: false, error };
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Drop-in replacement for `useMutation` that augments the result with `mutateAsyncSafe`. Used
54
+ * by every dapp-kit mutation hook so consumers can pick rejection-style (`mutateAsync`) or
55
+ * Result-style (`mutateAsyncSafe`) ergonomics without the hook author having to think about it.
56
+ */
57
+ export function useSafeMutation<TData, TError, TVars, TContext = unknown>(
58
+ options: UseMutationOptions<TData, TError, TVars, TContext>,
59
+ ): SafeUseMutationResult<TData, TError, TVars, TContext> {
60
+ const mutation = useMutation<TData, TError, TVars, TContext>(options);
61
+ const { mutateAsync } = mutation;
62
+ const mutateAsyncSafe = useCallback(
63
+ (vars: TVars, opts?: MutateOptions<TData, TError, TVars, TContext>): Promise<Result<TData>> =>
64
+ toResult(mutateAsync(vars, opts)),
65
+ [mutateAsync],
66
+ );
67
+ return { ...mutation, mutateAsyncSafe };
68
+ }
@@ -1,4 +1,4 @@
1
- import { SodaxContext, type SodaxContextType } from '@/contexts';
1
+ import { SodaxContext, type SodaxContextType } from '@/contexts/index.js';
2
2
  import { useContext } from 'react';
3
3
 
4
4
  /**
@@ -1,71 +1,37 @@
1
- import { STELLAR_MAINNET_CHAIN_ID, StellarSpokeProvider, StellarSpokeService } from '@sodax/sdk';
2
- import type { SpokeChainId, SpokeProvider } from '@sodax/sdk';
1
+ import { ChainKeys, type IStellarWalletProvider, type SpokeChainKey } from '@sodax/sdk';
3
2
  import { useQuery, type UseQueryResult } from '@tanstack/react-query';
3
+ import { useSodaxContext } from './useSodaxContext.js';
4
+ import type { ReadHookParams } from './types.js';
4
5
 
5
- /**
6
- * React hook to check if a Stellar account has a sufficient trustline for a given token and amount.
7
- *
8
- * This hook queries the Stellar network (via the provided SpokeProvider) to determine
9
- * whether the account has established a trustline for the specified token and if the trustline
10
- * is sufficient for the intended amount. It is useful for gating UI actions that require
11
- * a trustline to be present and funded.
12
- *
13
- * @template T - The type of SpokeProvider, defaults to SpokeProvider.
14
- * @param {string | undefined} token - The Stellar asset code or token address to check the trustline for.
15
- * @param {bigint | undefined} amount - The minimum amount required for the trustline.
16
- * @param {T | undefined} spokeProvider - The provider instance for interacting with the Stellar network.
17
- * @param {SpokeChainId | undefined} chainId - The chain ID to determine if the check should be performed (only on Stellar mainnet).
18
- * @returns {UseQueryResult<boolean, Error>} A React Query result object containing:
19
- * - `data`: `true` if the trustline exists and is sufficient, `false` otherwise.
20
- * - `error`: Any error encountered during the check.
21
- * - `isLoading`: Whether the query is in progress.
22
- * - Other React Query state.
23
- *
24
- * @example
25
- * ```tsx
26
- * import { useStellarTrustlineCheck } from '@sodax/dapp-kit';
27
- *
28
- * const { data: hasTrustline, isLoading, error } = useStellarTrustlineCheck(
29
- * 'USDC-G...TOKEN',
30
- * 10000000n,
31
- * stellarProvider,
32
- * 'stellar'
33
- * );
34
- *
35
- * if (isLoading) return <span>Checking trustline...</span>;
36
- * if (error) return <span>Error: {error.message}</span>;
37
- * if (!hasTrustline) return <span>Trustline not established or insufficient.</span>;
38
- * return <span>Trustline is sufficient!</span>;
39
- * ```
40
- */
6
+ export type UseStellarTrustlineCheckParams = ReadHookParams<
7
+ boolean,
8
+ {
9
+ token: string | undefined;
10
+ amount: bigint | undefined;
11
+ chainId: SpokeChainKey | undefined;
12
+ walletProvider: IStellarWalletProvider | undefined;
13
+ }
14
+ >;
41
15
 
42
- export function useStellarTrustlineCheck<T extends SpokeProvider = SpokeProvider>(
43
- token: string | undefined,
44
- amount: bigint | undefined,
45
- spokeProvider: T | undefined,
46
- chainId: SpokeChainId | undefined,
47
- ): UseQueryResult<boolean, Error> {
48
- return useQuery({
49
- queryKey: ['stellar-trustline-check', token],
50
- queryFn: async () => {
51
- if (chainId !== STELLAR_MAINNET_CHAIN_ID) {
52
- return true;
53
- }
54
- if (!spokeProvider || !token || !amount || !(spokeProvider instanceof StellarSpokeProvider)) {
55
- console.error(
56
- 'Spoke provider, token or amount not found. Details: spokeProvider:',
57
- spokeProvider,
58
- 'token:',
59
- token,
60
- 'amount:',
61
- amount,
62
- );
63
- return false;
64
- }
65
- const response = await StellarSpokeService.hasSufficientTrustline(token, amount, spokeProvider);
16
+ export function useStellarTrustlineCheck({
17
+ params,
18
+ queryOptions,
19
+ }: UseStellarTrustlineCheckParams = {}): UseQueryResult<boolean, Error> {
20
+ const { sodax } = useSodaxContext();
21
+ const token = params?.token;
22
+ const amount = params?.amount;
23
+ const chainId = params?.chainId;
24
+ const walletProvider = params?.walletProvider;
66
25
 
67
- return response;
26
+ return useQuery<boolean, Error>({
27
+ queryKey: ['shared', 'stellarTrustlineCheck', token],
28
+ queryFn: async () => {
29
+ if (chainId !== ChainKeys.STELLAR_MAINNET) return true;
30
+ if (!walletProvider || !token || !amount) return false;
31
+ const walletAddress = await walletProvider.getWalletAddress();
32
+ return sodax.spoke.stellar.hasSufficientTrustline(token, amount, walletAddress);
68
33
  },
69
- enabled: !!spokeProvider && !!token && !!amount,
34
+ enabled: !!walletProvider && !!token && !!amount,
35
+ ...queryOptions,
70
36
  });
71
37
  }
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { IXServiceBase, XToken } from '@sodax/sdk';
3
+ import { getXBalancesQueryOptions } from './useXBalances.js';
4
+
5
+ const makeToken = (symbol: string, address: string): XToken =>
6
+ ({
7
+ symbol,
8
+ name: symbol,
9
+ decimals: 18,
10
+ address,
11
+ chainKey: 'sonic',
12
+ hubAsset: '0x0000000000000000000000000000000000000000',
13
+ vault: '0x0000000000000000000000000000000000000000',
14
+ }) as XToken;
15
+
16
+ const makeXService = (balances: Record<string, bigint>): IXServiceBase => ({
17
+ xChainType: 'EVM',
18
+ getBalance: vi.fn(),
19
+ getBalances: vi.fn().mockResolvedValue(balances),
20
+ });
21
+
22
+ describe('getXBalancesQueryOptions', () => {
23
+ const tokenA = makeToken('AAA', '0xaaa');
24
+ const tokenB = makeToken('BBB', '0xbbb');
25
+
26
+ it('builds a queryKey pairing each token symbol with its address', () => {
27
+ const opts = getXBalancesQueryOptions({
28
+ xService: makeXService({}),
29
+ xChainId: 'sonic',
30
+ xTokens: [tokenA, tokenB],
31
+ address: '0xuser',
32
+ });
33
+
34
+ expect(opts.queryKey).toEqual([
35
+ 'shared',
36
+ 'xBalances',
37
+ 'sonic',
38
+ [
39
+ ['AAA', '0xaaa'],
40
+ ['BBB', '0xbbb'],
41
+ ],
42
+ '0xuser',
43
+ ]);
44
+ });
45
+
46
+ it('is disabled when xService is missing', () => {
47
+ const opts = getXBalancesQueryOptions({
48
+ xService: undefined,
49
+ xChainId: 'sonic',
50
+ xTokens: [tokenA],
51
+ address: '0xuser',
52
+ });
53
+ expect(opts.enabled).toBe(false);
54
+ });
55
+
56
+ it('is disabled when address is missing', () => {
57
+ const opts = getXBalancesQueryOptions({
58
+ xService: makeXService({}),
59
+ xChainId: 'sonic',
60
+ xTokens: [tokenA],
61
+ address: undefined,
62
+ });
63
+ expect(opts.enabled).toBe(false);
64
+ });
65
+
66
+ it('is disabled when xTokens is empty', () => {
67
+ const opts = getXBalancesQueryOptions({
68
+ xService: makeXService({}),
69
+ xChainId: 'sonic',
70
+ xTokens: [],
71
+ address: '0xuser',
72
+ });
73
+ expect(opts.enabled).toBe(false);
74
+ });
75
+
76
+ it('is enabled when all inputs are present', () => {
77
+ const opts = getXBalancesQueryOptions({
78
+ xService: makeXService({}),
79
+ xChainId: 'sonic',
80
+ xTokens: [tokenA],
81
+ address: '0xuser',
82
+ });
83
+ expect(opts.enabled).toBe(true);
84
+ });
85
+
86
+ it('queryFn delegates to xService.getBalances with address and tokens', async () => {
87
+ const expected = { '0xaaa': 42n };
88
+ const xService = makeXService(expected);
89
+
90
+ const opts = getXBalancesQueryOptions({
91
+ xService,
92
+ xChainId: 'sonic',
93
+ xTokens: [tokenA],
94
+ address: '0xuser',
95
+ });
96
+
97
+ const result = await opts.queryFn();
98
+ expect(result).toEqual(expected);
99
+ expect(xService.getBalances).toHaveBeenCalledWith('0xuser', [tokenA]);
100
+ });
101
+
102
+ it('queryFn returns {} when xService is undefined (defensive, enabled should prevent this)', async () => {
103
+ const opts = getXBalancesQueryOptions({
104
+ xService: undefined,
105
+ xChainId: 'sonic',
106
+ xTokens: [tokenA],
107
+ address: '0xuser',
108
+ });
109
+
110
+ const result = await opts.queryFn();
111
+ expect(result).toEqual({});
112
+ });
113
+ });
@@ -0,0 +1,61 @@
1
+ import { type UseQueryResult, useQuery } from '@tanstack/react-query';
2
+ import type { SpokeChainKey, IXServiceBase, XToken } from '@sodax/sdk';
3
+ import type { ReadHookParams } from './types.js';
4
+
5
+ /**
6
+ * Domain inputs for {@link useXBalances}. `xChainId` is optional so the hook can be mounted
7
+ * before a chain is selected; `enabled` gates execution on every required field being present.
8
+ */
9
+ export interface XBalancesInputs {
10
+ xService: IXServiceBase | undefined;
11
+ xChainId: SpokeChainKey | undefined;
12
+ xTokens: readonly XToken[];
13
+ address: string | undefined;
14
+ }
15
+
16
+ export type UseXBalancesParams = ReadHookParams<Record<string, bigint>, XBalancesInputs>;
17
+
18
+ const REFETCH_INTERVAL_MS = 5_000;
19
+
20
+ /**
21
+ * Pure builder for {@link useXBalances} query options. Exported for unit
22
+ * tests and for advanced callers that compose their own `useQuery` wrapper.
23
+ */
24
+ export function getXBalancesQueryOptions({ xService, xChainId, xTokens, address }: XBalancesInputs) {
25
+ return {
26
+ // Pair symbol + address: readable in devtools, unique on-chain (symbol alone
27
+ // can collide — e.g. scam tokens copying legitimate ticker).
28
+ queryKey: ['shared', 'xBalances', xChainId, xTokens.map(x => [x.symbol, x.address] as const), address] as const,
29
+ queryFn: async (): Promise<Record<string, bigint>> => {
30
+ if (!xService) return {};
31
+ return xService.getBalances(address, xTokens);
32
+ },
33
+ enabled: !!xService && !!address && xTokens.length > 0,
34
+ refetchInterval: REFETCH_INTERVAL_MS,
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Fetch token balances for multiple tokens on a specific chain. Returns an
40
+ * object mapping each token's address to its balance in smallest unit.
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const xService = useXService({ xChainType: getXChainType(xChainId) });
45
+ * const { data: balances } = useXBalances({ params: { xService, xChainId, xTokens, address } });
46
+ * ```
47
+ */
48
+ export function useXBalances({
49
+ params,
50
+ queryOptions,
51
+ }: UseXBalancesParams = {}): UseQueryResult<Record<string, bigint>> {
52
+ return useQuery({
53
+ ...getXBalancesQueryOptions({
54
+ xService: params?.xService,
55
+ xChainId: params?.xChainId,
56
+ xTokens: params?.xTokens ?? [],
57
+ address: params?.address,
58
+ }),
59
+ ...queryOptions,
60
+ });
61
+ }
@@ -1,19 +1,19 @@
1
1
  // packages/dapp-kit/src/hooks/staking/index.ts
2
- export { useStake } from './useStake';
3
- export { useStakeApprove } from './useStakeApprove';
4
- export { useStakeAllowance } from './useStakeAllowance';
5
- export { useUnstake } from './useUnstake';
6
- export { useClaim } from './useClaim';
7
- export { useCancelUnstake } from './useCancelUnstake';
8
- export { useStakingInfo } from './useStakingInfo';
9
- export { useUnstakingInfoWithPenalty } from './useUnstakingInfoWithPenalty';
10
- export { useStakingConfig } from './useStakingConfig';
11
- export { useStakeRatio } from './useStakeRatio';
12
- export { useInstantUnstakeRatio } from './useInstantUnstakeRatio';
13
- export { useConvertedAssets } from './useConvertedAssets';
14
- export { useInstantUnstake } from './useInstantUnstake';
15
- export { useUnstakeAllowance } from './useUnstakeAllowance';
16
- export { useUnstakeApprove } from './useUnstakeApprove';
17
- export { useUnstakingInfo } from './useUnstakingInfo';
18
- export { useInstantUnstakeApprove } from './useInstantUnstakeApprove';
19
- export { useInstantUnstakeAllowance } from './useInstantUnstakeAllowance';
2
+ export { useStake } from './useStake.js';
3
+ export { useStakeApprove } from './useStakeApprove.js';
4
+ export { useStakeAllowance } from './useStakeAllowance.js';
5
+ export { useUnstake } from './useUnstake.js';
6
+ export { useClaim } from './useClaim.js';
7
+ export { useCancelUnstake } from './useCancelUnstake.js';
8
+ export { useStakingInfo } from './useStakingInfo.js';
9
+ export { useUnstakingInfoWithPenalty } from './useUnstakingInfoWithPenalty.js';
10
+ export { useStakingConfig } from './useStakingConfig.js';
11
+ export { useStakeRatio } from './useStakeRatio.js';
12
+ export { useInstantUnstakeRatio } from './useInstantUnstakeRatio.js';
13
+ export { useConvertedAssets } from './useConvertedAssets.js';
14
+ export { useInstantUnstake } from './useInstantUnstake.js';
15
+ export { useUnstakeAllowance } from './useUnstakeAllowance.js';
16
+ export { useUnstakeApprove } from './useUnstakeApprove.js';
17
+ export { useUnstakingInfo } from './useUnstakingInfo.js';
18
+ export { useInstantUnstakeApprove } from './useInstantUnstakeApprove.js';
19
+ export { useInstantUnstakeAllowance } from './useInstantUnstakeAllowance.js';