diff --git a/signet-dashboard/public/utxo-list.html b/signet-dashboard/public/utxo-list.html index ff71975..148ee1d 100644 --- a/signet-dashboard/public/utxo-list.html +++ b/signet-dashboard/public/utxo-list.html @@ -264,7 +264,7 @@

Total d'UTXO : -

Capacité d'ancrage restante : - ancrages (- UTXOs confirmés)

- +

Montant total : -

Dernière mise à jour : -

@@ -295,6 +295,7 @@ // Charger la liste au chargement de la page document.addEventListener('DOMContentLoaded', () => { loadUtxoList(); + loadSmallUtxosInfo(); }); function getSortState(categoryName) { @@ -520,6 +521,34 @@ return tableHTML; } + async function loadSmallUtxosInfo() { + const button = document.getElementById('consolidate-button'); + if (!button) return; + + try { + const response = await fetch(`${API_BASE_URL}/api/utxo/small-info`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + const count = data.count || 0; + const totalSats = data.totalSats || 0; + + if (count > 0) { + button.textContent = `Consolider la capacité d'ancrage résiduelle (${count.toLocaleString('fr-FR')} UTXOs, ${totalSats.toLocaleString('fr-FR')} ✅)`; + button.disabled = false; + } else { + button.textContent = 'Aucun UTXO à consolider'; + button.disabled = true; + } + } catch (error) { + console.error('Error loading small UTXOs info:', error); + button.textContent = 'Erreur de chargement'; + button.disabled = true; + } + } + async function loadUtxoList() { const contentDiv = document.getElementById('content'); const refreshButton = document.querySelector('.refresh-button'); @@ -527,6 +556,9 @@ refreshButton.disabled = true; contentDiv.innerHTML = '
Chargement des UTXO...
'; + // Charger les infos des petits UTXOs en parallèle + loadSmallUtxosInfo(); + try { const response = await fetch(`${API_BASE_URL}/api/utxo/list`); @@ -572,6 +604,8 @@ contentDiv.innerHTML = `
Erreur lors du chargement de la liste des UTXO : ${error.message}
`; } finally { refreshButton.disabled = false; + // Recharger les infos des petits UTXOs après le chargement de la liste + loadSmallUtxosInfo(); } } diff --git a/signet-dashboard/src/bitcoin-rpc.js b/signet-dashboard/src/bitcoin-rpc.js index ee62bf6..2fa9b88 100644 --- a/signet-dashboard/src/bitcoin-rpc.js +++ b/signet-dashboard/src/bitcoin-rpc.js @@ -744,6 +744,135 @@ class BitcoinRPC { } } + /** + * Obtient les informations sur les UTXOs de moins de 2500 sats disponibles pour consolidation + * @returns {Promise} Nombre et montant total des petits UTXOs + */ + async getSmallUtxosInfo() { + try { + const walletName = process.env.BITCOIN_RPC_WALLET || 'custom_signet'; + const host = process.env.BITCOIN_RPC_HOST || 'localhost'; + const port = process.env.BITCOIN_RPC_PORT || '38332'; + const username = process.env.BITCOIN_RPC_USER || 'bitcoin'; + const password = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin'; + const rpcUrl = `http://${host}:${port}/wallet/${walletName}`; + const auth = Buffer.from(`${username}:${password}`).toString('base64'); + + // Récupérer les UTXOs confirmés + const rpcResponse = await fetch(rpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${auth}`, + }, + body: JSON.stringify({ + jsonrpc: '1.0', + id: 'listunspent', + method: 'listunspent', + params: [1], // Minimum 1 confirmation + }), + }); + + if (!rpcResponse.ok) { + const errorText = await rpcResponse.text(); + logger.error('HTTP error in listunspent for small UTXOs info', { + status: rpcResponse.status, + statusText: rpcResponse.statusText, + response: errorText, + }); + throw new Error(`HTTP error fetching UTXOs: ${rpcResponse.status} ${rpcResponse.statusText}`); + } + + const rpcResult = await rpcResponse.json(); + if (rpcResult.error) { + logger.error('RPC error in listunspent for small UTXOs info', { error: rpcResult.error }); + throw new Error(`RPC error: ${rpcResult.error.message}`); + } + + const unspent = rpcResult.result || []; + + // Récupérer les UTXOs verrouillés depuis l'API d'ancrage + let lockedUtxos = new Set(); + try { + const anchorApiUrl = process.env.ANCHOR_API_URL || 'http://localhost:3010'; + const anchorApiKey = process.env.ANCHOR_API_KEY || ''; + + const headers = { + 'Content-Type': 'application/json', + }; + if (anchorApiKey) { + headers['x-api-key'] = anchorApiKey; + } + + const lockedResponse = await fetch(`${anchorApiUrl}/api/anchor/locked-utxos`, { + method: 'GET', + headers, + }); + + if (lockedResponse.ok) { + const lockedData = await lockedResponse.json(); + for (const locked of lockedData.locked || []) { + lockedUtxos.add(`${locked.txid}:${locked.vout}`); + } + } + } catch (error) { + logger.debug('Error getting locked UTXOs for small UTXOs info', { error: error.message }); + } + + // Filtrer les UTXOs de moins de 2500 sats (0.000025 BTC), confirmés et non verrouillés + const maxAmount = 0.000025; // 2500 sats + const smallUtxos = unspent.filter(utxo => { + const utxoKey = `${utxo.txid}:${utxo.vout}`; + return utxo.amount < maxAmount && + (utxo.confirmations || 0) > 0 && + !lockedUtxos.has(utxoKey); + }); + + // Vérifier si l'UTXO est dépensé onchain et calculer le montant total + let count = 0; + let totalAmount = 0; + for (const utxo of smallUtxos) { + try { + const txOutResponse = await fetch(rpcUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Basic ${auth}`, + }, + body: JSON.stringify({ + jsonrpc: '1.0', + id: 'gettxout', + method: 'gettxout', + params: [utxo.txid, utxo.vout], + }), + }); + + if (txOutResponse.ok) { + const txOutResult = await txOutResponse.json(); + // Si gettxout retourne null, l'UTXO est dépensé + if (txOutResult.result !== null) { + count++; + totalAmount += utxo.amount; + } + } + } catch (error) { + logger.debug('Error checking if UTXO is spent for small UTXOs info', { txid: utxo.txid, vout: utxo.vout, error: error.message }); + } + } + + const totalSats = Math.round(totalAmount * 100000000); + + return { + count, + totalAmount, + totalSats, + }; + } catch (error) { + logger.error('Error getting small UTXOs info', { error: error.message }); + throw error; + } + } + /** * Consolide les UTXOs de moins de 2500 sats en un gros UTXO * @returns {Promise} Transaction créée avec txid diff --git a/signet-dashboard/src/server.js b/signet-dashboard/src/server.js index 60d1e28..394e67e 100644 --- a/signet-dashboard/src/server.js +++ b/signet-dashboard/src/server.js @@ -298,6 +298,23 @@ app.get('/api/utxo/list', async (req, res) => { } }); +// Route pour obtenir les informations sur les petits UTXOs (< 2500 sats) +app.get('/api/utxo/small-info', async (req, res) => { + try { + const info = await bitcoinRPC.getSmallUtxosInfo(); + res.json({ + count: info.count, + totalAmount: info.totalAmount, + totalSats: info.totalSats, + }); + } catch (error) { + logger.error('Error getting small UTXOs info', { error: error.message }); + res.status(500).json({ + error: error.message, + }); + } +}); + // Route pour consolider les UTXOs de moins de 2500 sats app.post('/api/utxo/consolidate', async (req, res) => { try {