@sodax/dapp-kit 1.3.1-beta-rc1 → 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.d.mts +105 -2
- package/dist/index.d.ts +105 -2
- package/dist/index.js +279 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +269 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/hooks/bitcoin/index.ts +7 -0
- package/src/hooks/bitcoin/radfiConstants.ts +2 -0
- package/src/hooks/bitcoin/useBitcoinBalance.ts +27 -0
- package/src/hooks/bitcoin/useExpiredUtxos.ts +25 -0
- package/src/hooks/bitcoin/useFundTradingWallet.ts +39 -0
- package/src/hooks/bitcoin/useRadfiAuth.ts +123 -0
- package/src/hooks/bitcoin/useRadfiSession.ts +133 -0
- package/src/hooks/bitcoin/useRenewUtxos.ts +72 -0
- package/src/hooks/bitcoin/useTradingWallet.ts +15 -0
- package/src/hooks/bitcoin/useTradingWalletBalance.ts +22 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/provider/useSpokeProvider.ts +20 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sodax/dapp-kit",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "1.3.1-beta-
|
|
4
|
+
"version": "1.3.1-beta-rc2",
|
|
5
5
|
"description": "dapp-kit of New World",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"viem": "2.29.2",
|
|
19
|
-
"@sodax/sdk": "1.3.1-beta-
|
|
20
|
-
"@sodax/types": "1.3.1-beta-
|
|
19
|
+
"@sodax/sdk": "1.3.1-beta-rc2",
|
|
20
|
+
"@sodax/types": "1.3.1-beta-rc2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/react": "19.0.8",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook to fetch BTC balance for any Bitcoin address.
|
|
5
|
+
* Sums all UTXOs (confirmed + unconfirmed) from mempool.space API.
|
|
6
|
+
*
|
|
7
|
+
* The UTXO set already excludes spent outputs (even from unconfirmed txs),
|
|
8
|
+
* so the total is always the correct spendable balance.
|
|
9
|
+
*/
|
|
10
|
+
export function useBitcoinBalance(
|
|
11
|
+
address: string | undefined,
|
|
12
|
+
rpcUrl = 'https://mempool.space/api',
|
|
13
|
+
): UseQueryResult<bigint, Error> {
|
|
14
|
+
return useQuery<bigint, Error>({
|
|
15
|
+
queryKey: ['btc-balance', address],
|
|
16
|
+
queryFn: async () => {
|
|
17
|
+
if (!address) return 0n;
|
|
18
|
+
|
|
19
|
+
const response = await fetch(`${rpcUrl}/address/${address}/utxo`);
|
|
20
|
+
if (!response.ok) return 0n;
|
|
21
|
+
|
|
22
|
+
const utxos: Array<{ value: number }> = await response.json();
|
|
23
|
+
return BigInt(utxos.reduce((sum, utxo) => sum + utxo.value, 0));
|
|
24
|
+
},
|
|
25
|
+
enabled: !!address,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
import type { BitcoinSpokeProvider, RadfiUtxo } from '@sodax/sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to fetch expired UTXOs for a trading wallet address.
|
|
6
|
+
* UTXOs that are expired or within 2 weeks of expiry are considered invalid for trading
|
|
7
|
+
* and need to be renewed via the Radfi renew-utxo flow.
|
|
8
|
+
*/
|
|
9
|
+
export function useExpiredUtxos(
|
|
10
|
+
spokeProvider: BitcoinSpokeProvider | undefined,
|
|
11
|
+
tradingAddress: string | undefined,
|
|
12
|
+
): UseQueryResult<RadfiUtxo[], Error> {
|
|
13
|
+
return useQuery<RadfiUtxo[], Error>({
|
|
14
|
+
queryKey: ['expired-utxos', tradingAddress],
|
|
15
|
+
queryFn: async () => {
|
|
16
|
+
if (!spokeProvider || !tradingAddress) {
|
|
17
|
+
throw new Error('spokeProvider and tradingAddress are required');
|
|
18
|
+
}
|
|
19
|
+
const result = await spokeProvider.radfi.getExpiredUtxos(tradingAddress);
|
|
20
|
+
return result.data;
|
|
21
|
+
},
|
|
22
|
+
enabled: !!spokeProvider && !!tradingAddress,
|
|
23
|
+
refetchInterval: 60_000, // refetch every minute
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { BitcoinSpokeService, type BitcoinSpokeProvider } from '@sodax/sdk';
|
|
2
|
+
import { useMutation, useQueryClient, type UseMutationResult } from '@tanstack/react-query';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to fund the Radfi trading wallet by sending BTC from the user's personal wallet.
|
|
6
|
+
*
|
|
7
|
+
* @param {BitcoinSpokeProvider | undefined} spokeProvider - The Bitcoin spoke provider with signing capability
|
|
8
|
+
* @returns {UseMutationResult} Mutation result — input is amount in satoshis, output is transaction ID
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const { mutateAsync: fundWallet, isPending } = useFundTradingWallet(spokeProvider);
|
|
13
|
+
*
|
|
14
|
+
* const handleFund = async () => {
|
|
15
|
+
* const txId = await fundWallet(100_000n); // fund 100,000 satoshis
|
|
16
|
+
* console.log('Funded:', txId);
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useFundTradingWallet(
|
|
21
|
+
spokeProvider: BitcoinSpokeProvider | undefined,
|
|
22
|
+
): UseMutationResult<string, Error, bigint> {
|
|
23
|
+
const queryClient = useQueryClient();
|
|
24
|
+
|
|
25
|
+
return useMutation<string, Error, bigint>({
|
|
26
|
+
mutationFn: async (amount: bigint) => {
|
|
27
|
+
if (!spokeProvider) {
|
|
28
|
+
throw new Error('Bitcoin spoke provider not found');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return BitcoinSpokeService.fundTradingWallet(amount, spokeProvider);
|
|
32
|
+
},
|
|
33
|
+
onSuccess: () => {
|
|
34
|
+
// Invalidate balance queries to reflect the fund transfer
|
|
35
|
+
queryClient.invalidateQueries({ queryKey: ['btc-balance'] });
|
|
36
|
+
queryClient.invalidateQueries({ queryKey: ['xBalances'] });
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { BitcoinSpokeProvider } from '@sodax/sdk';
|
|
2
|
+
import { useMutation, type UseMutationResult } from '@tanstack/react-query';
|
|
3
|
+
import { ACCESS_TOKEN_TTL, REFRESH_TOKEN_TTL } from './radfiConstants';
|
|
4
|
+
|
|
5
|
+
export type RadfiSession = {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken: string;
|
|
8
|
+
tradingAddress: string;
|
|
9
|
+
publicKey: string;
|
|
10
|
+
accessTokenExpiry: number;
|
|
11
|
+
refreshTokenExpiry: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type RadfiAuthResult = {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken: string;
|
|
17
|
+
tradingAddress: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SESSION_KEY = (address: string) => `radfi_session_${address}`;
|
|
21
|
+
|
|
22
|
+
export function saveRadfiSession(address: string, session: RadfiSession): void {
|
|
23
|
+
try {
|
|
24
|
+
// Radfi tokens are only used for API rate-limiting / anti-spam, not for accessing user assets.
|
|
25
|
+
localStorage.setItem(SESSION_KEY(address), JSON.stringify(session));
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadRadfiSession(address: string): RadfiSession | null {
|
|
30
|
+
try {
|
|
31
|
+
const raw = localStorage.getItem(SESSION_KEY(address));
|
|
32
|
+
return raw ? (JSON.parse(raw) as RadfiSession) : null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function clearRadfiSession(address: string): void {
|
|
39
|
+
try {
|
|
40
|
+
localStorage.removeItem(SESSION_KEY(address));
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isAccessTokenExpired(address: string): boolean {
|
|
45
|
+
const session = loadRadfiSession(address);
|
|
46
|
+
if (!session) return true;
|
|
47
|
+
return Date.now() >= session.accessTokenExpiry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isRefreshTokenExpired(address: string): boolean {
|
|
51
|
+
const session = loadRadfiSession(address);
|
|
52
|
+
if (!session) return true;
|
|
53
|
+
return Date.now() >= session.refreshTokenExpiry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Hook to authenticate with Radfi using BIP322 message signing.
|
|
58
|
+
* Saves full session (accessToken, refreshToken, tradingAddress, expiry) to localStorage.
|
|
59
|
+
*/
|
|
60
|
+
export function useRadfiAuth(
|
|
61
|
+
spokeProvider: BitcoinSpokeProvider | undefined,
|
|
62
|
+
): UseMutationResult<RadfiAuthResult, Error, void> {
|
|
63
|
+
return useMutation<RadfiAuthResult, Error, void>({
|
|
64
|
+
mutationFn: async () => {
|
|
65
|
+
if (!spokeProvider) {
|
|
66
|
+
throw new Error('Bitcoin spoke provider not found');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const walletAddress = await spokeProvider.walletProvider.getWalletAddress();
|
|
70
|
+
const existingSession = loadRadfiSession(walletAddress);
|
|
71
|
+
const cachedPublicKey = existingSession?.publicKey;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const { accessToken, refreshToken, tradingAddress, publicKey } = await spokeProvider.authenticateWithWallet(cachedPublicKey);
|
|
75
|
+
|
|
76
|
+
const session: RadfiSession = {
|
|
77
|
+
accessToken,
|
|
78
|
+
refreshToken,
|
|
79
|
+
tradingAddress,
|
|
80
|
+
publicKey,
|
|
81
|
+
accessTokenExpiry: Date.now() + ACCESS_TOKEN_TTL,
|
|
82
|
+
refreshTokenExpiry: Date.now() + REFRESH_TOKEN_TTL,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
saveRadfiSession(walletAddress, session);
|
|
86
|
+
|
|
87
|
+
return { accessToken, refreshToken, tradingAddress };
|
|
88
|
+
} catch (err: unknown) {
|
|
89
|
+
// Error 4008: wallet already registered — authenticate is register+login combined.
|
|
90
|
+
// Try to refresh with existing session if available.
|
|
91
|
+
const isAlreadyRegistered =
|
|
92
|
+
err instanceof Error &&
|
|
93
|
+
(err.message.includes('duplicatedPubKey') || err.message.includes('4008'));
|
|
94
|
+
|
|
95
|
+
if (isAlreadyRegistered) {
|
|
96
|
+
if (existingSession && !isRefreshTokenExpired(walletAddress)) {
|
|
97
|
+
// Try silent refresh
|
|
98
|
+
const refreshed = await spokeProvider.radfi.refreshAccessToken(existingSession.refreshToken);
|
|
99
|
+
const session: RadfiSession = {
|
|
100
|
+
...existingSession,
|
|
101
|
+
accessToken: refreshed.accessToken,
|
|
102
|
+
refreshToken: refreshed.refreshToken,
|
|
103
|
+
accessTokenExpiry: Date.now() + ACCESS_TOKEN_TTL,
|
|
104
|
+
refreshTokenExpiry: Date.now() + REFRESH_TOKEN_TTL,
|
|
105
|
+
};
|
|
106
|
+
spokeProvider.setRadfiAccessToken(refreshed.accessToken);
|
|
107
|
+
saveRadfiSession(walletAddress, session);
|
|
108
|
+
return { accessToken: refreshed.accessToken, refreshToken: refreshed.refreshToken, tradingAddress: existingSession.tradingAddress };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// No valid session to refresh — guide the user
|
|
112
|
+
throw new Error(
|
|
113
|
+
'This wallet is already registered with Radfi from another session. ' +
|
|
114
|
+
'Please clear your browser storage for this site and try again, ' +
|
|
115
|
+
'or wait for the previous session to expire.',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import type { BitcoinSpokeProvider } from '@sodax/sdk';
|
|
3
|
+
import {
|
|
4
|
+
useRadfiAuth,
|
|
5
|
+
loadRadfiSession,
|
|
6
|
+
saveRadfiSession,
|
|
7
|
+
clearRadfiSession,
|
|
8
|
+
isAccessTokenExpired,
|
|
9
|
+
isRefreshTokenExpired,
|
|
10
|
+
type RadfiSession,
|
|
11
|
+
} from './useRadfiAuth';
|
|
12
|
+
|
|
13
|
+
import { ACCESS_TOKEN_TTL } from './radfiConstants';
|
|
14
|
+
const POLL_INTERVAL = 30_000; // 30 s — access tokens expire every 10 min, no need to poll faster
|
|
15
|
+
|
|
16
|
+
export type UseRadfiSessionReturn = {
|
|
17
|
+
walletAddress: string | undefined;
|
|
18
|
+
isAuthed: boolean;
|
|
19
|
+
tradingAddress: string | undefined;
|
|
20
|
+
login: () => Promise<void>;
|
|
21
|
+
isLoginPending: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Manages the full Radfi session lifecycle:
|
|
26
|
+
* - Restores session from localStorage on mount
|
|
27
|
+
* - Polls every 2s: silently refreshes accessToken before expiry, resets auth when refreshToken expires
|
|
28
|
+
* - Exposes login() and isAuthed for UI
|
|
29
|
+
*/
|
|
30
|
+
export function useRadfiSession(
|
|
31
|
+
spokeProvider: BitcoinSpokeProvider | undefined,
|
|
32
|
+
): UseRadfiSessionReturn {
|
|
33
|
+
const [walletAddress, setWalletAddress] = useState<string | undefined>();
|
|
34
|
+
const [isAuthed, setIsAuthed] = useState(false);
|
|
35
|
+
const [tradingAddress, setTradingAddress] = useState<string | undefined>();
|
|
36
|
+
const isRefreshingRef = useRef(false);
|
|
37
|
+
|
|
38
|
+
// ── Silent refresh helper ────────────────────────────────────────────────
|
|
39
|
+
const silentRefresh = useCallback(async (address: string) => {
|
|
40
|
+
if (!spokeProvider || isRefreshingRef.current) return;
|
|
41
|
+
isRefreshingRef.current = true;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const session = loadRadfiSession(address);
|
|
45
|
+
if (!session?.refreshToken) {
|
|
46
|
+
setIsAuthed(false);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { accessToken, refreshToken } = await spokeProvider.radfi.refreshAccessToken(session.refreshToken);
|
|
51
|
+
const updated: RadfiSession = {
|
|
52
|
+
...session,
|
|
53
|
+
accessToken,
|
|
54
|
+
refreshToken,
|
|
55
|
+
accessTokenExpiry: Date.now() + ACCESS_TOKEN_TTL,
|
|
56
|
+
// Keep the original refreshTokenExpiry — don't roll it forward on every silent refresh
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
saveRadfiSession(address, updated);
|
|
60
|
+
spokeProvider.setRadfiAccessToken(accessToken);
|
|
61
|
+
setIsAuthed(true);
|
|
62
|
+
setTradingAddress(updated.tradingAddress || undefined);
|
|
63
|
+
} catch {
|
|
64
|
+
clearRadfiSession(address);
|
|
65
|
+
spokeProvider.setRadfiAccessToken('');
|
|
66
|
+
setIsAuthed(false);
|
|
67
|
+
setTradingAddress(undefined);
|
|
68
|
+
} finally {
|
|
69
|
+
isRefreshingRef.current = false;
|
|
70
|
+
}
|
|
71
|
+
}, [spokeProvider]);
|
|
72
|
+
|
|
73
|
+
// ── Poll wallet address + restore session eagerly ────────────────────────
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!spokeProvider) return;
|
|
76
|
+
|
|
77
|
+
const fetchAndRestore = () => {
|
|
78
|
+
spokeProvider.walletProvider.getWalletAddress()
|
|
79
|
+
.then((addr) => {
|
|
80
|
+
setWalletAddress(addr);
|
|
81
|
+
// Eagerly restore session in the same tick to avoid extra render cycle
|
|
82
|
+
const session = loadRadfiSession(addr);
|
|
83
|
+
if (!session || isRefreshTokenExpired(addr)) return;
|
|
84
|
+
|
|
85
|
+
if (!isAccessTokenExpired(addr)) {
|
|
86
|
+
spokeProvider.setRadfiAccessToken(session.accessToken);
|
|
87
|
+
setIsAuthed(true);
|
|
88
|
+
setTradingAddress(session.tradingAddress || undefined);
|
|
89
|
+
} else {
|
|
90
|
+
// Access token expired but refresh valid — trigger silent refresh
|
|
91
|
+
silentRefresh(addr);
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
.catch(() => {});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
fetchAndRestore();
|
|
98
|
+
const id = setInterval(fetchAndRestore, 3000);
|
|
99
|
+
return () => clearInterval(id);
|
|
100
|
+
}, [spokeProvider, silentRefresh]);
|
|
101
|
+
|
|
102
|
+
// ── Polling: check expiry every 30s ──────────────────────────────────────
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!walletAddress || !spokeProvider) return;
|
|
105
|
+
|
|
106
|
+
const id = setInterval(() => {
|
|
107
|
+
if (isRefreshTokenExpired(walletAddress)) {
|
|
108
|
+
clearRadfiSession(walletAddress);
|
|
109
|
+
spokeProvider.setRadfiAccessToken('');
|
|
110
|
+
setIsAuthed(false);
|
|
111
|
+
setTradingAddress(undefined);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (isAccessTokenExpired(walletAddress)) {
|
|
116
|
+
silentRefresh(walletAddress);
|
|
117
|
+
}
|
|
118
|
+
}, POLL_INTERVAL);
|
|
119
|
+
|
|
120
|
+
return () => clearInterval(id);
|
|
121
|
+
}, [walletAddress, spokeProvider, silentRefresh]);
|
|
122
|
+
|
|
123
|
+
// ── Login ────────────────────────────────────────────────────────────────
|
|
124
|
+
const { mutateAsync: loginMutate, isPending: isLoginPending } = useRadfiAuth(spokeProvider);
|
|
125
|
+
|
|
126
|
+
const login = useCallback(async () => {
|
|
127
|
+
const result = await loginMutate();
|
|
128
|
+
setIsAuthed(true);
|
|
129
|
+
setTradingAddress(result.tradingAddress || undefined);
|
|
130
|
+
}, [loginMutate]);
|
|
131
|
+
|
|
132
|
+
return { walletAddress, isAuthed, tradingAddress, login, isLoginPending };
|
|
133
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { normalizePsbtToBase64, type BitcoinSpokeProvider } from '@sodax/sdk';
|
|
2
|
+
import { useMutation, useQueryClient, type UseMutationResult } from '@tanstack/react-query';
|
|
3
|
+
import { loadRadfiSession } from './useRadfiAuth';
|
|
4
|
+
|
|
5
|
+
type RenewUtxosParams = {
|
|
6
|
+
txIdVouts: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook to renew expired UTXOs in the Radfi trading wallet.
|
|
11
|
+
*
|
|
12
|
+
* Flow:
|
|
13
|
+
* 1. Build renew-utxo transaction via Radfi API (returns unsigned PSBT)
|
|
14
|
+
* 2. User signs the PSBT with their wallet
|
|
15
|
+
* 3. Submit signed PSBT back to Radfi for co-signing and broadcasting
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const { mutateAsync: renewUtxos, isPending } = useRenewUtxos(spokeProvider);
|
|
20
|
+
*
|
|
21
|
+
* const handleRenew = async (expiredUtxos: RadfiUtxo[]) => {
|
|
22
|
+
* const txIdVouts = expiredUtxos.map(u => `${u.txId}:${u.vout}`);
|
|
23
|
+
* const txId = await renewUtxos({ txIdVouts });
|
|
24
|
+
* console.log('Renewed:', txId);
|
|
25
|
+
* };
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function useRenewUtxos(
|
|
29
|
+
spokeProvider: BitcoinSpokeProvider | undefined,
|
|
30
|
+
): UseMutationResult<string, Error, RenewUtxosParams> {
|
|
31
|
+
const queryClient = useQueryClient();
|
|
32
|
+
|
|
33
|
+
return useMutation<string, Error, RenewUtxosParams>({
|
|
34
|
+
mutationFn: async ({ txIdVouts }: RenewUtxosParams) => {
|
|
35
|
+
if (!spokeProvider) {
|
|
36
|
+
throw new Error('Bitcoin spoke provider not found');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const userAddress = await spokeProvider.walletProvider.getWalletAddress();
|
|
40
|
+
const session = loadRadfiSession(userAddress);
|
|
41
|
+
const accessToken = session?.accessToken || spokeProvider.radfiAccessToken;
|
|
42
|
+
|
|
43
|
+
if (!accessToken) {
|
|
44
|
+
throw new Error('Radfi authentication required. Please login first.');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Step 1: Build the renew-utxo transaction
|
|
48
|
+
const buildResult = await spokeProvider.radfi.buildRenewUtxoTransaction(
|
|
49
|
+
{ userAddress, txIdVouts },
|
|
50
|
+
accessToken,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Step 2: Sign the PSBT with user's wallet
|
|
54
|
+
const signedTx = await spokeProvider.walletProvider.signTransaction(
|
|
55
|
+
buildResult.base64Psbt,
|
|
56
|
+
false,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const signedBase64Tx = normalizePsbtToBase64(signedTx);
|
|
60
|
+
|
|
61
|
+
// Step 3: Submit to Radfi for co-signing and broadcasting
|
|
62
|
+
return spokeProvider.radfi.signAndBroadcastRenewUtxo(
|
|
63
|
+
{ userAddress, signedBase64Tx },
|
|
64
|
+
accessToken,
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
onSuccess: () => {
|
|
68
|
+
queryClient.invalidateQueries({ queryKey: ['expired-utxos'] });
|
|
69
|
+
queryClient.invalidateQueries({ queryKey: ['trading-wallet-balance'] });
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { loadRadfiSession } from './useRadfiAuth';
|
|
2
|
+
|
|
3
|
+
type UseTradingWalletReturn = {
|
|
4
|
+
tradingAddress: string | undefined;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns the Radfi trading wallet address from the persisted session.
|
|
9
|
+
* Trading wallet is created automatically during authentication — no API call needed.
|
|
10
|
+
*/
|
|
11
|
+
export function useTradingWallet(walletAddress: string | undefined): UseTradingWalletReturn {
|
|
12
|
+
if (!walletAddress) return { tradingAddress: undefined };
|
|
13
|
+
const session = loadRadfiSession(walletAddress);
|
|
14
|
+
return { tradingAddress: session?.tradingAddress || undefined };
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useQuery, type UseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
import type { BitcoinSpokeProvider, RadfiWalletBalance } from '@sodax/sdk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to fetch trading wallet balance from Radfi API.
|
|
6
|
+
* Returns confirmed + pending satoshi balances.
|
|
7
|
+
*/
|
|
8
|
+
export function useTradingWalletBalance(
|
|
9
|
+
spokeProvider: BitcoinSpokeProvider | undefined,
|
|
10
|
+
tradingAddress: string | undefined,
|
|
11
|
+
): UseQueryResult<RadfiWalletBalance, Error> {
|
|
12
|
+
return useQuery<RadfiWalletBalance, Error>({
|
|
13
|
+
queryKey: ['trading-wallet-balance', tradingAddress],
|
|
14
|
+
queryFn: () => {
|
|
15
|
+
if (!spokeProvider || !tradingAddress) {
|
|
16
|
+
throw new Error('spokeProvider and tradingAddress are required');
|
|
17
|
+
}
|
|
18
|
+
return spokeProvider.radfi.getBalance(tradingAddress);
|
|
19
|
+
},
|
|
20
|
+
enabled: !!spokeProvider && !!tradingAddress,
|
|
21
|
+
});
|
|
22
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useSodaxContext } from '@/index';
|
|
2
2
|
import {
|
|
3
|
+
BitcoinSpokeProvider,
|
|
3
4
|
EvmSpokeProvider,
|
|
4
5
|
spokeChainConfig,
|
|
5
6
|
type SuiSpokeChainConfig,
|
|
@@ -22,6 +23,9 @@ import {
|
|
|
22
23
|
type NearSpokeChainConfig,
|
|
23
24
|
} from '@sodax/sdk';
|
|
24
25
|
import type {
|
|
26
|
+
BitcoinSpokeChainConfig,
|
|
27
|
+
BitcoinRpcConfig,
|
|
28
|
+
IBitcoinWalletProvider,
|
|
25
29
|
IEvmWalletProvider,
|
|
26
30
|
IIconWalletProvider,
|
|
27
31
|
ISuiWalletProvider,
|
|
@@ -60,6 +64,22 @@ export function useSpokeProvider(
|
|
|
60
64
|
if (!xChainType) return undefined;
|
|
61
65
|
if (!rpcConfig) return undefined;
|
|
62
66
|
|
|
67
|
+
if (xChainType === 'BITCOIN') {
|
|
68
|
+
const bitcoinConfig = spokeChainConfig[spokeChainId] as BitcoinSpokeChainConfig;
|
|
69
|
+
const btcRpcOverride = rpcConfig[spokeChainId] as BitcoinRpcConfig | undefined;
|
|
70
|
+
return new BitcoinSpokeProvider(
|
|
71
|
+
walletProvider as IBitcoinWalletProvider,
|
|
72
|
+
bitcoinConfig,
|
|
73
|
+
{
|
|
74
|
+
url: btcRpcOverride?.radfiApiUrl || bitcoinConfig.radfiApiUrl,
|
|
75
|
+
apiKey: bitcoinConfig.radfiApiKey,
|
|
76
|
+
umsUrl: btcRpcOverride?.radfiUmsUrl || bitcoinConfig.radfiUmsUrl,
|
|
77
|
+
},
|
|
78
|
+
'TRADING',
|
|
79
|
+
btcRpcOverride?.rpcUrl || bitcoinConfig.rpcUrl,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
if (xChainType === 'EVM') {
|
|
64
84
|
if (spokeChainId === SONIC_MAINNET_CHAIN_ID) {
|
|
65
85
|
return new SonicSpokeProvider(
|