@sodax/dapp-kit 1.3.0-beta → 1.3.1-beta-rc2

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.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useContext, useState, useCallback, useMemo, useRef, useEffect } from 'react';
2
- import { SpokeService, deriveUserWalletAddress, STELLAR_MAINNET_CHAIN_ID, StellarSpokeProvider, StellarSpokeService, HubService, spokeChainConfig, SONIC_MAINNET_CHAIN_ID, SonicSpokeProvider, EvmSpokeProvider, SuiSpokeProvider, IconSpokeProvider, InjectiveSpokeProvider, SolanaSpokeProvider, NearSpokeProvider, isLegacybnUSDToken, Sodax } from '@sodax/sdk';
2
+ import { SpokeService, deriveUserWalletAddress, STELLAR_MAINNET_CHAIN_ID, StellarSpokeProvider, StellarSpokeService, HubService, spokeChainConfig, BitcoinSpokeProvider, SONIC_MAINNET_CHAIN_ID, SonicSpokeProvider, EvmSpokeProvider, SuiSpokeProvider, IconSpokeProvider, InjectiveSpokeProvider, SolanaSpokeProvider, NearSpokeProvider, BitcoinSpokeService, normalizePsbtToBase64, isLegacybnUSDToken, Sodax } from '@sodax/sdk';
3
3
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
4
4
  import { isAddress, parseUnits } from 'viem';
5
5
  import { ICON_MAINNET_CHAIN_ID } from '@sodax/types';
@@ -137,6 +137,21 @@ function useSpokeProvider(spokeChainId, walletProvider) {
137
137
  if (!spokeChainId) return void 0;
138
138
  if (!xChainType) return void 0;
139
139
  if (!rpcConfig) return void 0;
140
+ if (xChainType === "BITCOIN") {
141
+ const bitcoinConfig = spokeChainConfig[spokeChainId];
142
+ const btcRpcOverride = rpcConfig[spokeChainId];
143
+ return new BitcoinSpokeProvider(
144
+ walletProvider,
145
+ bitcoinConfig,
146
+ {
147
+ url: btcRpcOverride?.radfiApiUrl || bitcoinConfig.radfiApiUrl,
148
+ apiKey: bitcoinConfig.radfiApiKey,
149
+ umsUrl: btcRpcOverride?.radfiUmsUrl || bitcoinConfig.radfiUmsUrl
150
+ },
151
+ "TRADING",
152
+ btcRpcOverride?.rpcUrl || bitcoinConfig.rpcUrl
153
+ );
154
+ }
140
155
  if (xChainType === "EVM") {
141
156
  if (spokeChainId === SONIC_MAINNET_CHAIN_ID) {
142
157
  return new SonicSpokeProvider(
@@ -144,9 +159,11 @@ function useSpokeProvider(spokeChainId, walletProvider) {
144
159
  spokeChainConfig[spokeChainId]
145
160
  );
146
161
  }
162
+ const evmRpcUrl = rpcConfig[spokeChainId];
147
163
  return new EvmSpokeProvider(
148
164
  walletProvider,
149
- spokeChainConfig[spokeChainId]
165
+ spokeChainConfig[spokeChainId],
166
+ typeof evmRpcUrl === "string" ? evmRpcUrl : void 0
150
167
  );
151
168
  }
152
169
  if (xChainType === "SUI") {
@@ -197,6 +214,258 @@ function useSpokeProvider(spokeChainId, walletProvider) {
197
214
  }, [spokeChainId, xChainType, walletProvider, rpcConfig]);
198
215
  return spokeProvider;
199
216
  }
217
+
218
+ // src/hooks/bitcoin/radfiConstants.ts
219
+ var ACCESS_TOKEN_TTL = 10 * 60 * 1e3;
220
+ var REFRESH_TOKEN_TTL = 7 * 24 * 60 * 60 * 1e3;
221
+
222
+ // src/hooks/bitcoin/useRadfiAuth.ts
223
+ var SESSION_KEY = (address) => `radfi_session_${address}`;
224
+ function saveRadfiSession(address, session) {
225
+ try {
226
+ localStorage.setItem(SESSION_KEY(address), JSON.stringify(session));
227
+ } catch {
228
+ }
229
+ }
230
+ function loadRadfiSession(address) {
231
+ try {
232
+ const raw = localStorage.getItem(SESSION_KEY(address));
233
+ return raw ? JSON.parse(raw) : null;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+ function clearRadfiSession(address) {
239
+ try {
240
+ localStorage.removeItem(SESSION_KEY(address));
241
+ } catch {
242
+ }
243
+ }
244
+ function isAccessTokenExpired(address) {
245
+ const session = loadRadfiSession(address);
246
+ if (!session) return true;
247
+ return Date.now() >= session.accessTokenExpiry;
248
+ }
249
+ function isRefreshTokenExpired(address) {
250
+ const session = loadRadfiSession(address);
251
+ if (!session) return true;
252
+ return Date.now() >= session.refreshTokenExpiry;
253
+ }
254
+ function useRadfiAuth(spokeProvider) {
255
+ return useMutation({
256
+ mutationFn: async () => {
257
+ if (!spokeProvider) {
258
+ throw new Error("Bitcoin spoke provider not found");
259
+ }
260
+ const walletAddress = await spokeProvider.walletProvider.getWalletAddress();
261
+ const existingSession = loadRadfiSession(walletAddress);
262
+ const cachedPublicKey = existingSession?.publicKey;
263
+ try {
264
+ const { accessToken, refreshToken, tradingAddress, publicKey } = await spokeProvider.authenticateWithWallet(cachedPublicKey);
265
+ const session = {
266
+ accessToken,
267
+ refreshToken,
268
+ tradingAddress,
269
+ publicKey,
270
+ accessTokenExpiry: Date.now() + ACCESS_TOKEN_TTL,
271
+ refreshTokenExpiry: Date.now() + REFRESH_TOKEN_TTL
272
+ };
273
+ saveRadfiSession(walletAddress, session);
274
+ return { accessToken, refreshToken, tradingAddress };
275
+ } catch (err) {
276
+ const isAlreadyRegistered = err instanceof Error && (err.message.includes("duplicatedPubKey") || err.message.includes("4008"));
277
+ if (isAlreadyRegistered) {
278
+ if (existingSession && !isRefreshTokenExpired(walletAddress)) {
279
+ const refreshed = await spokeProvider.radfi.refreshAccessToken(existingSession.refreshToken);
280
+ const session = {
281
+ ...existingSession,
282
+ accessToken: refreshed.accessToken,
283
+ refreshToken: refreshed.refreshToken,
284
+ accessTokenExpiry: Date.now() + ACCESS_TOKEN_TTL,
285
+ refreshTokenExpiry: Date.now() + REFRESH_TOKEN_TTL
286
+ };
287
+ spokeProvider.setRadfiAccessToken(refreshed.accessToken);
288
+ saveRadfiSession(walletAddress, session);
289
+ return { accessToken: refreshed.accessToken, refreshToken: refreshed.refreshToken, tradingAddress: existingSession.tradingAddress };
290
+ }
291
+ throw new Error(
292
+ "This wallet is already registered with Radfi from another session. Please clear your browser storage for this site and try again, or wait for the previous session to expire."
293
+ );
294
+ }
295
+ throw err;
296
+ }
297
+ }
298
+ });
299
+ }
300
+ var POLL_INTERVAL = 3e4;
301
+ function useRadfiSession(spokeProvider) {
302
+ const [walletAddress, setWalletAddress] = useState();
303
+ const [isAuthed, setIsAuthed] = useState(false);
304
+ const [tradingAddress, setTradingAddress] = useState();
305
+ const isRefreshingRef = useRef(false);
306
+ const silentRefresh = useCallback(async (address) => {
307
+ if (!spokeProvider || isRefreshingRef.current) return;
308
+ isRefreshingRef.current = true;
309
+ try {
310
+ const session = loadRadfiSession(address);
311
+ if (!session?.refreshToken) {
312
+ setIsAuthed(false);
313
+ return;
314
+ }
315
+ const { accessToken, refreshToken } = await spokeProvider.radfi.refreshAccessToken(session.refreshToken);
316
+ const updated = {
317
+ ...session,
318
+ accessToken,
319
+ refreshToken,
320
+ accessTokenExpiry: Date.now() + ACCESS_TOKEN_TTL
321
+ // Keep the original refreshTokenExpiry — don't roll it forward on every silent refresh
322
+ };
323
+ saveRadfiSession(address, updated);
324
+ spokeProvider.setRadfiAccessToken(accessToken);
325
+ setIsAuthed(true);
326
+ setTradingAddress(updated.tradingAddress || void 0);
327
+ } catch {
328
+ clearRadfiSession(address);
329
+ spokeProvider.setRadfiAccessToken("");
330
+ setIsAuthed(false);
331
+ setTradingAddress(void 0);
332
+ } finally {
333
+ isRefreshingRef.current = false;
334
+ }
335
+ }, [spokeProvider]);
336
+ useEffect(() => {
337
+ if (!spokeProvider) return;
338
+ const fetchAndRestore = () => {
339
+ spokeProvider.walletProvider.getWalletAddress().then((addr) => {
340
+ setWalletAddress(addr);
341
+ const session = loadRadfiSession(addr);
342
+ if (!session || isRefreshTokenExpired(addr)) return;
343
+ if (!isAccessTokenExpired(addr)) {
344
+ spokeProvider.setRadfiAccessToken(session.accessToken);
345
+ setIsAuthed(true);
346
+ setTradingAddress(session.tradingAddress || void 0);
347
+ } else {
348
+ silentRefresh(addr);
349
+ }
350
+ }).catch(() => {
351
+ });
352
+ };
353
+ fetchAndRestore();
354
+ const id = setInterval(fetchAndRestore, 3e3);
355
+ return () => clearInterval(id);
356
+ }, [spokeProvider, silentRefresh]);
357
+ useEffect(() => {
358
+ if (!walletAddress || !spokeProvider) return;
359
+ const id = setInterval(() => {
360
+ if (isRefreshTokenExpired(walletAddress)) {
361
+ clearRadfiSession(walletAddress);
362
+ spokeProvider.setRadfiAccessToken("");
363
+ setIsAuthed(false);
364
+ setTradingAddress(void 0);
365
+ return;
366
+ }
367
+ if (isAccessTokenExpired(walletAddress)) {
368
+ silentRefresh(walletAddress);
369
+ }
370
+ }, POLL_INTERVAL);
371
+ return () => clearInterval(id);
372
+ }, [walletAddress, spokeProvider, silentRefresh]);
373
+ const { mutateAsync: loginMutate, isPending: isLoginPending } = useRadfiAuth(spokeProvider);
374
+ const login = useCallback(async () => {
375
+ const result = await loginMutate();
376
+ setIsAuthed(true);
377
+ setTradingAddress(result.tradingAddress || void 0);
378
+ }, [loginMutate]);
379
+ return { walletAddress, isAuthed, tradingAddress, login, isLoginPending };
380
+ }
381
+ function useFundTradingWallet(spokeProvider) {
382
+ const queryClient = useQueryClient();
383
+ return useMutation({
384
+ mutationFn: async (amount) => {
385
+ if (!spokeProvider) {
386
+ throw new Error("Bitcoin spoke provider not found");
387
+ }
388
+ return BitcoinSpokeService.fundTradingWallet(amount, spokeProvider);
389
+ },
390
+ onSuccess: () => {
391
+ queryClient.invalidateQueries({ queryKey: ["btc-balance"] });
392
+ queryClient.invalidateQueries({ queryKey: ["xBalances"] });
393
+ }
394
+ });
395
+ }
396
+ function useBitcoinBalance(address, rpcUrl = "https://mempool.space/api") {
397
+ return useQuery({
398
+ queryKey: ["btc-balance", address],
399
+ queryFn: async () => {
400
+ if (!address) return 0n;
401
+ const response = await fetch(`${rpcUrl}/address/${address}/utxo`);
402
+ if (!response.ok) return 0n;
403
+ const utxos = await response.json();
404
+ return BigInt(utxos.reduce((sum, utxo) => sum + utxo.value, 0));
405
+ },
406
+ enabled: !!address
407
+ });
408
+ }
409
+ function useTradingWalletBalance(spokeProvider, tradingAddress) {
410
+ return useQuery({
411
+ queryKey: ["trading-wallet-balance", tradingAddress],
412
+ queryFn: () => {
413
+ if (!spokeProvider || !tradingAddress) {
414
+ throw new Error("spokeProvider and tradingAddress are required");
415
+ }
416
+ return spokeProvider.radfi.getBalance(tradingAddress);
417
+ },
418
+ enabled: !!spokeProvider && !!tradingAddress
419
+ });
420
+ }
421
+ function useExpiredUtxos(spokeProvider, tradingAddress) {
422
+ return useQuery({
423
+ queryKey: ["expired-utxos", tradingAddress],
424
+ queryFn: async () => {
425
+ if (!spokeProvider || !tradingAddress) {
426
+ throw new Error("spokeProvider and tradingAddress are required");
427
+ }
428
+ const result = await spokeProvider.radfi.getExpiredUtxos(tradingAddress);
429
+ return result.data;
430
+ },
431
+ enabled: !!spokeProvider && !!tradingAddress,
432
+ refetchInterval: 6e4
433
+ // refetch every minute
434
+ });
435
+ }
436
+ function useRenewUtxos(spokeProvider) {
437
+ const queryClient = useQueryClient();
438
+ return useMutation({
439
+ mutationFn: async ({ txIdVouts }) => {
440
+ if (!spokeProvider) {
441
+ throw new Error("Bitcoin spoke provider not found");
442
+ }
443
+ const userAddress = await spokeProvider.walletProvider.getWalletAddress();
444
+ const session = loadRadfiSession(userAddress);
445
+ const accessToken = session?.accessToken || spokeProvider.radfiAccessToken;
446
+ if (!accessToken) {
447
+ throw new Error("Radfi authentication required. Please login first.");
448
+ }
449
+ const buildResult = await spokeProvider.radfi.buildRenewUtxoTransaction(
450
+ { userAddress, txIdVouts },
451
+ accessToken
452
+ );
453
+ const signedTx = await spokeProvider.walletProvider.signTransaction(
454
+ buildResult.base64Psbt,
455
+ false
456
+ );
457
+ const signedBase64Tx = normalizePsbtToBase64(signedTx);
458
+ return spokeProvider.radfi.signAndBroadcastRenewUtxo(
459
+ { userAddress, signedBase64Tx },
460
+ accessToken
461
+ );
462
+ },
463
+ onSuccess: () => {
464
+ queryClient.invalidateQueries({ queryKey: ["expired-utxos"] });
465
+ queryClient.invalidateQueries({ queryKey: ["trading-wallet-balance"] });
466
+ }
467
+ });
468
+ }
200
469
  function useBorrow() {
201
470
  const { sodax } = useSodaxContext();
202
471
  return useMutation({
@@ -318,8 +587,22 @@ function useMMAllowance({
318
587
  const { sodax } = useSodaxContext();
319
588
  const defaultQueryOptions = {
320
589
  queryKey: ["mm", "allowance", params?.token, params?.action],
321
- enabled: !!spokeProvider,
322
- refetchInterval: 5e3
590
+ /**
591
+ * IMPORTANT: Skip allowance checks for 'borrow' and 'withdraw' actions.
592
+ *
593
+ * Reason: According to the SDK's MoneyMarketService.isAllowanceValid() implementation,
594
+ * borrow and withdraw actions do NOT require ERC-20 token approval. The SDK's
595
+ * isAllowanceValid() method always returns `true` for these actions without making
596
+ * any on-chain allowance checks.
597
+ *
598
+ * This optimization prevents unnecessary RPC calls and avoids showing confusing states for actions that don't actually need approval.
599
+ *
600
+ * Only 'supply' and 'repay' actions require token approval and should trigger allowance checks.
601
+ */
602
+ enabled: !!spokeProvider && !!params && params.action !== "borrow" && params.action !== "withdraw",
603
+ refetchInterval: 5e3,
604
+ gcTime: 0
605
+ // Don't cache failed queries
323
606
  };
324
607
  queryOptions = {
325
608
  ...defaultQueryOptions,
@@ -331,6 +614,9 @@ function useMMAllowance({
331
614
  queryFn: async () => {
332
615
  if (!spokeProvider) throw new Error("Spoke provider is required");
333
616
  if (!params) throw new Error("Params are required");
617
+ if (params.action === "borrow" || params.action === "withdraw") {
618
+ return true;
619
+ }
334
620
  const allowance = await sodax.moneyMarket.isAllowanceValid(params, spokeProvider);
335
621
  if (!allowance.ok) {
336
622
  throw allowance.error;
@@ -1170,11 +1456,9 @@ function useInstantUnstakeRatio(amount, refetchInterval = 1e4) {
1170
1456
  }
1171
1457
  function useConvertedAssets(amount, refetchInterval = 1e4) {
1172
1458
  const { sodax } = useSodaxContext();
1173
- console.log("useConvertedAssets hook called with:", { amount: amount?.toString(), sodax: !!sodax });
1174
1459
  return useQuery({
1175
1460
  queryKey: ["soda", "convertedAssets", amount?.toString()],
1176
1461
  queryFn: async () => {
1177
- console.log("useConvertedAssets queryFn called with amount:", amount?.toString());
1178
1462
  if (!amount || amount <= 0n) {
1179
1463
  throw new Error("Amount must be greater than 0");
1180
1464
  }
@@ -1495,6 +1779,6 @@ var SodaxProvider = ({ children, testnet = false, config, rpcConfig }) => {
1495
1779
  return /* @__PURE__ */ React.createElement(SodaxContext.Provider, { value: { sodax, testnet, rpcConfig } }, children);
1496
1780
  };
1497
1781
 
1498
- export { MIGRATION_MODE_BNUSD, MIGRATION_MODE_ICX_SODA, SodaxProvider, useAToken, useATokensBalances, useBackendAllMoneyMarketAssets, useBackendAllMoneyMarketBorrowers, useBackendIntentByHash, useBackendIntentByTxHash, useBackendMoneyMarketAsset, useBackendMoneyMarketAssetBorrowers, useBackendMoneyMarketAssetSuppliers, useBackendMoneyMarketPosition, useBackendOrderbook, useBackendUserIntents, useBorrow, useBridge, useBridgeAllowance, useBridgeApprove, useCancelLimitOrder, useCancelSwap, useCancelUnstake, useClaim, useConvertedAssets, useCreateLimitOrder, useDeriveUserWalletAddress, useEstimateGas, useGetBridgeableAmount, useGetBridgeableTokens, useGetUserHubWalletAddress, useHubProvider, useInstantUnstake, useInstantUnstakeAllowance, useInstantUnstakeApprove, useInstantUnstakeRatio, useMMAllowance, useMMApprove, useMigrate, useMigrationAllowance, useMigrationApprove, useQuote, useRepay, useRequestTrustline, useReservesData, useReservesUsdFormat, useSodaxContext, useSpokeProvider, useStake, useStakeAllowance, useStakeApprove, useStakeRatio, useStakingConfig, useStakingInfo, useStatus, useStellarTrustlineCheck, useSupply, useSwap, useSwapAllowance, useSwapApprove, useUnstake, useUnstakeAllowance, useUnstakeApprove, useUnstakingInfo, useUnstakingInfoWithPenalty, useUserFormattedSummary, useUserReservesData, useWithdraw };
1782
+ export { MIGRATION_MODE_BNUSD, MIGRATION_MODE_ICX_SODA, SodaxProvider, clearRadfiSession, isAccessTokenExpired, isRefreshTokenExpired, loadRadfiSession, saveRadfiSession, useAToken, useATokensBalances, useBackendAllMoneyMarketAssets, useBackendAllMoneyMarketBorrowers, useBackendIntentByHash, useBackendIntentByTxHash, useBackendMoneyMarketAsset, useBackendMoneyMarketAssetBorrowers, useBackendMoneyMarketAssetSuppliers, useBackendMoneyMarketPosition, useBackendOrderbook, useBackendUserIntents, useBitcoinBalance, useBorrow, useBridge, useBridgeAllowance, useBridgeApprove, useCancelLimitOrder, useCancelSwap, useCancelUnstake, useClaim, useConvertedAssets, useCreateLimitOrder, useDeriveUserWalletAddress, useEstimateGas, useExpiredUtxos, useFundTradingWallet, useGetBridgeableAmount, useGetBridgeableTokens, useGetUserHubWalletAddress, useHubProvider, useInstantUnstake, useInstantUnstakeAllowance, useInstantUnstakeApprove, useInstantUnstakeRatio, useMMAllowance, useMMApprove, useMigrate, useMigrationAllowance, useMigrationApprove, useQuote, useRadfiAuth, useRadfiSession, useRenewUtxos, useRepay, useRequestTrustline, useReservesData, useReservesUsdFormat, useSodaxContext, useSpokeProvider, useStake, useStakeAllowance, useStakeApprove, useStakeRatio, useStakingConfig, useStakingInfo, useStatus, useStellarTrustlineCheck, useSupply, useSwap, useSwapAllowance, useSwapApprove, useTradingWalletBalance, useUnstake, useUnstakeAllowance, useUnstakeApprove, useUnstakingInfo, useUnstakingInfoWithPenalty, useUserFormattedSummary, useUserReservesData, useWithdraw };
1499
1783
  //# sourceMappingURL=index.mjs.map
1500
1784
  //# sourceMappingURL=index.mjs.map