import { useState, useCallback, useEffect, useRef } from 'react';
import {
deriveAddresses,
getBalance,
getUtxos,
getTxHistory,
estimateFee,
getNetworkInfo,
getBlockCount,
getTxStatus,
listWallets,
loadWallet,
setLabel,
getUtxoTree,
type AddressInfo,
type WalletMeta,
type TxInfo,
type FeeEstimate,
} from '../api';
// Fiat price caching
let btcPrice = 0;
let btcPriceTime = 0;
export async function fetchBtcPrice(): Promise<number> {
const now = Date.now();
if (btcPrice && now - btcPriceTime < 60_000) return btcPrice;
try {
const resp = await fetch('https://api.coindesk.com/v1/bpi/currentprice.json');
const data = await resp.json();
btcPrice = data.bpi.USD.rate_float;
btcPriceTime = now;
return btcPrice;
} catch {
return btcPrice || 0;
}
}
export function btcToFiat(btc: number): number {
return btc * btcPrice;
}
export function useWallet() {
const [currentWallet, setCurrentWallet] = useState<any>(null);
const [walletPassword, setWalletPassword] = useState('');
const [addresses, setAddresses] = useState<AddressInfo[]>([]);
const [balance, setBalance] = useState<{ balance_btc: number; balance_sats: number } | null>(null);
const [utxos, setUtxos] = useState<any[]>([]);
const [txHistory, setTxHistory] = useState<TxInfo[]>([]);
const [feeEstimate, setFeeEstimate] = useState<FeeEstimate | null>(null);
const [networkInfo, setNetworkInfo] = useState<any>(null);
const [blockCount, setBlockCount] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [savedWallets, setSavedWallets] = useState<WalletMeta[]>([]);
const [addrLabels, setAddrLabels] = useState<Record<string, string>>({});
const [utxoTree, setUtxoTree] = useState<any[]>([]);
// Get address strings for API calls
const addrStrings = addresses.map(a => a.address);
const refreshBalance = useCallback(async () => {
if (addrStrings.length === 0) return;
try {
const bal = await getBalance(addrStrings);
setBalance(bal);
} catch (e: any) {
setError(e.message || 'Failed to fetch balance');
}
}, [addrStrings]);
const refreshUtxos = useCallback(async () => {
if (addrStrings.length === 0) return;
try {
const list = await getUtxos(addrStrings);
setUtxos(list);
} catch (e: any) {
setError(e.message || 'Failed to fetch UTXOs');
}
}, [addrStrings]);
const refreshHistory = useCallback(async () => {
if (!currentWallet) return;
try {
const allTxs: TxInfo[] = [];
for (const addr of addrStrings.slice(0, 5)) {
const txs = await getTxHistory(addr, 10);
const txids = new Set(allTxs.map(t => t.txid));
for (const tx of txs) {
if (!txids.has(tx.txid)) {
allTxs.push(tx);
txids.add(tx.txid);
}
}
}
allTxs.sort((a, b) => (b.time || 0) - (a.time || 0));
setTxHistory(allTxs);
} catch (e: any) {
setError(e.message || 'Failed to fetch history');
}
}, [currentWallet, addrStrings]);
const refreshFee = useCallback(async () => {
try {
const fee = await estimateFee(6);
setFeeEstimate(fee);
} catch {
// ignore
}
}, []);
const refreshNetwork = useCallback(async () => {
try {
const net = await getNetworkInfo();
setNetworkInfo(net);
} catch {
// ignore
}
}, []);
const refreshBlockCount = useCallback(async () => {
try {
const bc = await getBlockCount();
setBlockCount(bc);
} catch {
// ignore
}
}, []);
const refreshAll = useCallback(async () => {
if (loading) return;
setLoading(true);
setError('');
try {
await Promise.all([
refreshBalance(),
refreshUtxos(),
refreshHistory(),
refreshFee(),
refreshNetwork(),
refreshBlockCount(),
(async () => {
await fetchBtcPrice();
})(),
]);
} catch (e: any) {
setError(e.message || 'Refresh failed');
} finally {
setLoading(false);
}
}, [loading, refreshBalance, refreshUtxos, refreshHistory, refreshFee, refreshNetwork, refreshBlockCount]);
// Auto-refresh on wallet change
useEffect(() => {
if (currentWallet) {
refreshAll();
// Load saved wallets list
listWallets().then(r => setSavedWallets(r.wallets || [])).catch(() => {});
}
}, [currentWallet?.file_path]);
// Periodic refresh
useEffect(() => {
if (!currentWallet) return;
const interval = setInterval(refreshAll, 60_000);
return () => clearInterval(interval);
}, [currentWallet?.file_path, refreshAll]);
const refreshTxStatus = useCallback(async (txid: string) => {
try {
return await getTxStatus(txid);
} catch {
return { txid, confirmations: 0, status: 'not_found' as const };
}
}, []);
// Switch to a new wallet (from creation flow)
const switchWallet = useCallback((wallet: any, addrs: AddressInfo[]) => {
setCurrentWallet(wallet);
setAddresses(addrs);
}, []);
// Load wallet from disk (sidebar selection)
const loadWalletFromDisk = useCallback(async (path: string) => {
setLoading(true);
setError('');
try {
const w = await loadWallet(path, '');
const addrs = await deriveAddresses(w.wallet, 5);
setCurrentWallet(w.wallet);
setAddresses(addrs);
setAddrLabels({});
} catch (e: any) {
setError(e.message || 'Failed to load wallet');
} finally {
setLoading(false);
}
}, []);
// Derive more addresses
const deriveMore = useCallback(async (num: number = 5): Promise<AddressInfo[]> => {
if (!currentWallet) return [];
setLoading(true);
try {
const newAddrs = await deriveAddresses(currentWallet, num, 'bech32', walletPassword);
setAddresses(prev => [...prev, ...newAddrs]);
return newAddrs;
} catch (e: any) {
setError(e.message || 'Failed to derive addresses');
return [];
} finally {
setLoading(false);
}
}, [currentWallet, walletPassword]);
// Set label
const setLabelFn = useCallback(async (kind: string, identifier: string, label: string) => {
if (!currentWallet) return;
try {
await setLabel(currentWallet, kind, identifier, label);
if (kind === 'addr') {
setAddrLabels(prev => ({ ...prev, [identifier]: label }));
}
} catch (e: any) {
setError(e.message || 'Failed to set label');
}
}, [currentWallet]);
// UTXO tree
const fetchUtxoTree = useCallback(async () => {
try {
const tree = await getUtxoTree(utxos, txHistory);
setUtxoTree(tree);
} catch {
// ignore
}
}, [utxos, txHistory]);
return {
currentWallet,
walletPassword,
setWalletPassword,
addresses,
balance,
utxos,
txHistory,
feeEstimate,
networkInfo,
blockCount,
loading,
error,
savedWallets,
addrLabels,
utxoTree,
refreshAll,
refreshTxStatus,
switchWallet,
loadWalletFromDisk,
deriveMore,
setLabel: setLabelFn,
fetchUtxoTree,
setError,
};
}