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)
-
Consolider les UTXOs < 2500 sats
+
Chargement...
Montant total : -
Dernière mise à jour : -
Actualiser
@@ -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 {