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:
parent
15edb1bac1
commit
3e3bfc72d3
@ -264,7 +264,7 @@
|
||||
<div class="info-section">
|
||||
<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>
|
||||
<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>Dernière mise à jour :</strong> <span id="last-update">-</span></p>
|
||||
<button class="refresh-button" onclick="loadUtxoList()">Actualiser</button>
|
||||
@ -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 = '<div class="loading">Chargement des UTXO...</div>';
|
||||
|
||||
// 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 = `<div class="error">Erreur lors du chargement de la liste des UTXO : ${error.message}</div>`;
|
||||
} finally {
|
||||
refreshButton.disabled = false;
|
||||
// Recharger les infos des petits UTXOs après le chargement de la liste
|
||||
loadSmallUtxosInfo();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
* @returns {Promise<Object>} Transaction créée avec txid
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user