Update consolidate button with dynamic info and implement API call

**Motivations:**
- Afficher le nombre d'UTXOs et le montant total concernés par la consolidation
- Rendre le bouton de consolidation plus informatif

**Root causes:**
- Le bouton affichait un texte statique sans information sur ce qui sera consolidé
- Pas de visibilité sur le nombre d'UTXOs et le montant concernés

**Correctifs:**
- Création de la méthode getSmallUtxosInfo() pour obtenir les infos sans consolider
- Ajout de la route GET /api/utxo/small-info pour exposer ces informations

**Evolutions:**
- Bouton de consolidation avec texte dynamique : "Consolider la capacité d'ancrage résiduelle (X UTXOs, Y )"
- Fonction loadSmallUtxosInfo() qui charge les infos depuis l'API
- Bouton désactivé avec message "Aucun UTXO à consolider" si aucun UTXO disponible
- Chargement automatique des infos au chargement de la page et après refresh
- Gestion d'erreur avec message "Erreur de chargement" si l'API échoue

**Pages affectées:**
- signet-dashboard/src/bitcoin-rpc.js: Méthode getSmallUtxosInfo() pour obtenir les infos des petits UTXOs
- signet-dashboard/src/server.js: Route GET /api/utxo/small-info
- signet-dashboard/public/utxo-list.html: Fonction loadSmallUtxosInfo() et mise à jour du bouton avec texte dynamique
This commit is contained in:
ncantu 2026-01-25 23:39:33 +01:00
parent 15edb1bac1
commit 3e3bfc72d3
3 changed files with 181 additions and 1 deletions

View File

@ -264,7 +264,7 @@
<div class="info-section"> <div class="info-section">
<p><strong>Total d'UTXO :</strong> <span id="utxo-count">-</span></p> <p><strong>Total d'UTXO :</strong> <span id="utxo-count">-</span></p>
<p><strong>Capacité d'ancrage restante :</strong> <span id="available-for-anchor">-</span> ancrages (<span id="confirmed-available-for-anchor">-</span> UTXOs confirmés)</p> <p><strong>Capacité d'ancrage restante :</strong> <span id="available-for-anchor">-</span> ancrages (<span id="confirmed-available-for-anchor">-</span> UTXOs confirmés)</p>
<button class="consolidate-button" onclick="consolidateSmallUtxos()" style="margin-left: 10px; background: #ffc107; color: #000; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 1em; margin-top: 10px;">Consolider les UTXOs < 2500 sats</button> <button class="consolidate-button" id="consolidate-button" onclick="consolidateSmallUtxos()" style="margin-left: 10px; background: #ffc107; color: #000; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; font-size: 1em; margin-top: 10px;">Chargement...</button>
<p><strong>Montant total :</strong> <span id="total-amount" class="total-amount">-</span></p> <p><strong>Montant total :</strong> <span id="total-amount" class="total-amount">-</span></p>
<p><strong>Dernière mise à jour :</strong> <span id="last-update">-</span></p> <p><strong>Dernière mise à jour :</strong> <span id="last-update">-</span></p>
<button class="refresh-button" onclick="loadUtxoList()">Actualiser</button> <button class="refresh-button" onclick="loadUtxoList()">Actualiser</button>
@ -295,6 +295,7 @@
// Charger la liste au chargement de la page // Charger la liste au chargement de la page
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadUtxoList(); loadUtxoList();
loadSmallUtxosInfo();
}); });
function getSortState(categoryName) { function getSortState(categoryName) {
@ -520,6 +521,34 @@
return tableHTML; 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() { async function loadUtxoList() {
const contentDiv = document.getElementById('content'); const contentDiv = document.getElementById('content');
const refreshButton = document.querySelector('.refresh-button'); const refreshButton = document.querySelector('.refresh-button');
@ -527,6 +556,9 @@
refreshButton.disabled = true; refreshButton.disabled = true;
contentDiv.innerHTML = '<div class="loading">Chargement des UTXO...</div>'; contentDiv.innerHTML = '<div class="loading">Chargement des UTXO...</div>';
// Charger les infos des petits UTXOs en parallèle
loadSmallUtxosInfo();
try { try {
const response = await fetch(`${API_BASE_URL}/api/utxo/list`); const response = await fetch(`${API_BASE_URL}/api/utxo/list`);
@ -572,6 +604,8 @@
contentDiv.innerHTML = `<div class="error">Erreur lors du chargement de la liste des UTXO : ${error.message}</div>`; contentDiv.innerHTML = `<div class="error">Erreur lors du chargement de la liste des UTXO : ${error.message}</div>`;
} finally { } finally {
refreshButton.disabled = false; refreshButton.disabled = false;
// Recharger les infos des petits UTXOs après le chargement de la liste
loadSmallUtxosInfo();
} }
} }

View File

@ -744,6 +744,135 @@ class BitcoinRPC {
} }
} }
/**
* Obtient les informations sur les UTXOs de moins de 2500 sats disponibles pour consolidation
* @returns {Promise<Object>} 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 * Consolide les UTXOs de moins de 2500 sats en un gros UTXO
* @returns {Promise<Object>} Transaction créée avec txid * @returns {Promise<Object>} Transaction créée avec txid

View File

@ -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 // Route pour consolider les UTXOs de moins de 2500 sats
app.post('/api/utxo/consolidate', async (req, res) => { app.post('/api/utxo/consolidate', async (req, res) => {
try { try {