**Motivations:** - Resolve insufficient UTXO amount errors when wallet has many small UTXOs - Prevent race conditions when multiple anchor requests arrive simultaneously - Improve signet dashboard functionality and documentation **Root causes:** - API tried to find a single UTXO large enough instead of combining multiple UTXOs - No mutex mechanism to prevent concurrent transactions from using the same UTXOs - UTXOs in mempool still appear as available in listunspent before block confirmation **Correctifs:** - Implement coin selection algorithm to combine multiple UTXOs when needed - Add mutex-based locking mechanism to serialize UTXO access - Filter locked UTXOs during selection to prevent double spending - Properly handle change output when combining multiple UTXOs - Lock UTXOs during transaction creation and unlock after mempool broadcast **Evolutions:** - Enhance signet dashboard with improved Bitcoin RPC integration - Update mempool documentation - Add comprehensive fix documentation in fixKnowledge/ **Pages affectées:** - api-anchorage/src/bitcoin-rpc.js - signet-dashboard/src/bitcoin-rpc.js - signet-dashboard/src/server.js - signet-dashboard/public/app.js - signet-dashboard/public/index.html - signet-dashboard/public/styles.css - signet-dashboard/start.sh - docs/MEMPOOL.md - fixKnowledge/api-anchorage-insufficient-utxo.md (new) - fixKnowledge/api-anchorage-utxo-race-condition.md (new) - anchor_count.txt (new) - mempool (submodule update)
434 lines
14 KiB
JavaScript
434 lines
14 KiB
JavaScript
/**
|
|
* Application JavaScript pour le Dashboard Bitcoin Signet
|
|
*/
|
|
|
|
const API_BASE_URL = window.location.origin;
|
|
// Utiliser le même origin pour le faucet (sera configuré via le proxy)
|
|
// Si on est sur dashboard.certificator.4nkweb.com, utiliser faucet.certificator.4nkweb.com
|
|
let FAUCET_API_URL;
|
|
if (window.location.hostname.includes('dashboard.certificator.4nkweb.com')) {
|
|
FAUCET_API_URL = window.location.origin.replace('dashboard.certificator.4nkweb.com', 'faucet.certificator.4nkweb.com');
|
|
} else if (window.location.hostname.includes('localhost') || window.location.hostname === '127.0.0.1') {
|
|
FAUCET_API_URL = 'http://localhost:3021';
|
|
} else {
|
|
FAUCET_API_URL = window.location.origin;
|
|
}
|
|
|
|
let selectedFile = null;
|
|
|
|
// Initialisation
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadData();
|
|
setInterval(loadData, 30000); // Rafraîchir toutes les 30 secondes
|
|
});
|
|
|
|
/**
|
|
* Charge toutes les données de supervision
|
|
*/
|
|
async function loadData() {
|
|
try {
|
|
await Promise.all([
|
|
loadBlockchainInfo(),
|
|
loadLatestBlock(),
|
|
loadWalletBalance(),
|
|
loadAnchorCount(),
|
|
loadNetworkPeers(),
|
|
]);
|
|
|
|
updateLastUpdateTime();
|
|
} catch (error) {
|
|
console.error('Error loading data:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge les informations de la blockchain
|
|
*/
|
|
async function loadBlockchainInfo() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/blockchain/info`);
|
|
const data = await response.json();
|
|
|
|
document.getElementById('block-height').textContent = data.blocks || 0;
|
|
} catch (error) {
|
|
console.error('Error loading blockchain info:', error);
|
|
document.getElementById('block-height').textContent = 'Erreur';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge les informations du dernier bloc
|
|
*/
|
|
async function loadLatestBlock() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/blockchain/latest-block`);
|
|
const data = await response.json();
|
|
|
|
if (data.time) {
|
|
const date = new Date(data.time * 1000);
|
|
document.getElementById('last-block-time').textContent = date.toLocaleString('fr-FR');
|
|
} else {
|
|
document.getElementById('last-block-time').textContent = 'Aucun bloc';
|
|
}
|
|
|
|
document.getElementById('last-block-tx-count').textContent = data.tx_count || 0;
|
|
} catch (error) {
|
|
console.error('Error loading latest block:', error);
|
|
document.getElementById('last-block-time').textContent = 'Erreur';
|
|
document.getElementById('last-block-tx-count').textContent = 'Erreur';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge le solde du wallet
|
|
*/
|
|
async function loadWalletBalance() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/wallet/balance`);
|
|
const data = await response.json();
|
|
|
|
document.getElementById('balance-mature').textContent = formatBTC(data.mature || 0);
|
|
document.getElementById('balance-immature').textContent = formatBTC(data.immature || 0);
|
|
} catch (error) {
|
|
console.error('Error loading wallet balance:', error);
|
|
document.getElementById('balance-mature').textContent = 'Erreur';
|
|
document.getElementById('balance-immature').textContent = 'Erreur';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge le nombre d'ancrages
|
|
*/
|
|
async function loadAnchorCount() {
|
|
const anchorCountValue = document.getElementById('anchor-count-value');
|
|
const anchorCountSpinner = document.getElementById('anchor-count-spinner');
|
|
|
|
if (!anchorCountValue || !anchorCountSpinner) {
|
|
console.error('Elements anchor-count-value or anchor-count-spinner not found in DOM');
|
|
return;
|
|
}
|
|
|
|
// Afficher le spinner
|
|
anchorCountSpinner.style.display = 'inline';
|
|
anchorCountValue.textContent = '...';
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/anchor/count`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const count = data.count !== undefined ? data.count : 0;
|
|
|
|
// Masquer le spinner et mettre à jour la valeur
|
|
anchorCountSpinner.style.display = 'none';
|
|
if (count >= 0) {
|
|
anchorCountValue.textContent = count.toLocaleString();
|
|
} else {
|
|
anchorCountValue.textContent = '0';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading anchor count:', error);
|
|
// Masquer le spinner
|
|
anchorCountSpinner.style.display = 'none';
|
|
// Ne pas réinitialiser à "Erreur" si on a déjà une valeur affichée
|
|
// Garder la dernière valeur valide ou afficher "0" si c'est la première erreur
|
|
const currentValue = anchorCountValue.textContent;
|
|
if (currentValue === '-' || currentValue === 'Erreur' || currentValue === '...') {
|
|
anchorCountValue.textContent = '0';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge les informations sur les pairs
|
|
*/
|
|
async function loadNetworkPeers() {
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/network/peers`);
|
|
const data = await response.json();
|
|
|
|
document.getElementById('peer-count').textContent = data.connections || 0;
|
|
} catch (error) {
|
|
console.error('Error loading network peers:', error);
|
|
document.getElementById('peer-count').textContent = 'Erreur';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formate un montant en BTC
|
|
*/
|
|
function formatBTC(btc) {
|
|
if (btc === 0) return '0 🛡';
|
|
if (btc < 0.000001) return `${(btc * 100000000).toFixed(0)} sats`;
|
|
// Arrondir sans décimales pour les balances Mature et Immature
|
|
return `${Math.round(btc)} 🛡`;
|
|
}
|
|
|
|
/**
|
|
* Met à jour l'heure de dernière mise à jour
|
|
*/
|
|
function updateLastUpdateTime() {
|
|
const now = new Date();
|
|
document.getElementById('last-update').textContent = now.toLocaleString('fr-FR');
|
|
}
|
|
|
|
/**
|
|
* Change d'onglet
|
|
*/
|
|
function switchTab(tab, buttonElement) {
|
|
// Désactiver tous les onglets
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
document.querySelectorAll('.tab-button').forEach(button => {
|
|
button.classList.remove('active');
|
|
});
|
|
|
|
// Activer l'onglet sélectionné
|
|
document.getElementById(`${tab}-tab`).classList.add('active');
|
|
// Activer le bouton correspondant
|
|
if (buttonElement) {
|
|
buttonElement.classList.add('active');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gère la sélection de fichier
|
|
*/
|
|
function handleFileSelect(event) {
|
|
const file = event.target.files[0];
|
|
if (file) {
|
|
selectedFile = file;
|
|
const fileInfo = document.getElementById('file-info');
|
|
fileInfo.innerHTML = `
|
|
<strong>Fichier sélectionné :</strong> ${file.name}<br>
|
|
<strong>Taille :</strong> ${formatFileSize(file.size)}<br>
|
|
<strong>Type :</strong> ${file.type || 'Non spécifié'}
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formate la taille d'un fichier
|
|
*/
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
/**
|
|
* Génère le hash depuis le texte
|
|
*/
|
|
async function generateHashFromText() {
|
|
const text = document.getElementById('anchor-text').value;
|
|
|
|
if (!text.trim()) {
|
|
showResult('anchor-result', 'error', 'Veuillez entrer un texte à ancrer.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/hash/generate`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ text }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.hash) {
|
|
document.getElementById('anchor-hash').value = data.hash;
|
|
showResult('anchor-result', 'success', `Hash généré avec succès : ${data.hash}`);
|
|
} else {
|
|
showResult('anchor-result', 'error', 'Erreur lors de la génération du hash.');
|
|
}
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Génère le hash depuis le fichier
|
|
*/
|
|
async function generateHashFromFile() {
|
|
if (!selectedFile) {
|
|
showResult('anchor-result', 'error', 'Veuillez sélectionner un fichier.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const reader = new FileReader();
|
|
reader.onload = async (e) => {
|
|
const fileContent = e.target.result;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}/api/hash/generate`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ fileContent }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.hash) {
|
|
document.getElementById('anchor-hash').value = data.hash;
|
|
showResult('anchor-result', 'success', `Hash généré avec succès : ${data.hash}`);
|
|
} else {
|
|
showResult('anchor-result', 'error', 'Erreur lors de la génération du hash.');
|
|
}
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
};
|
|
|
|
reader.readAsText(selectedFile);
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur lors de la lecture du fichier : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vérifie le hash
|
|
*/
|
|
async function verifyHash() {
|
|
const hash = document.getElementById('anchor-hash').value.trim();
|
|
|
|
if (!hash || !/^[0-9a-fA-F]{64}$/.test(hash)) {
|
|
showResult('anchor-result', 'error', 'Veuillez entrer un hash valide (64 caractères hexadécimaux).');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showResult('anchor-result', 'info', 'Vérification du hash en cours...');
|
|
|
|
const response = await fetch(`${API_BASE_URL}/api/anchor/verify`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ hash }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.anchor_info) {
|
|
const info = data.anchor_info;
|
|
showResult('anchor-result', 'success',
|
|
`Hash vérifié avec succès !<br>
|
|
<strong>TXID :</strong> ${info.transaction_id || 'N/A'}<br>
|
|
<strong>Hauteur du bloc :</strong> ${info.block_height !== null && info.block_height !== undefined ? info.block_height : 'Non confirmé'}<br>
|
|
<strong>Confirmations :</strong> ${info.confirmations || 0}<br>
|
|
<strong>Statut :</strong> ${info.confirmations > 0 ? 'Confirmé' : 'En attente'}`);
|
|
} else {
|
|
showResult('anchor-result', 'error', data.message || data.error || 'Hash non trouvé sur la blockchain.');
|
|
}
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ancre le document
|
|
*/
|
|
async function anchorDocument() {
|
|
const hash = document.getElementById('anchor-hash').value;
|
|
|
|
if (!hash || !/^[0-9a-fA-F]{64}$/.test(hash)) {
|
|
showResult('anchor-result', 'error', 'Veuillez générer un hash valide (64 caractères hexadécimaux).');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showResult('anchor-result', 'info', 'Ancrage en cours...');
|
|
|
|
const response = await fetch(`${API_BASE_URL}/api/anchor/test`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ hash }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.txid) {
|
|
showResult('anchor-result', 'success',
|
|
`Document ancré avec succès !<br>
|
|
<strong>TXID :</strong> ${data.txid}<br>
|
|
<strong>Statut :</strong> ${data.status}<br>
|
|
<strong>Confirmations :</strong> ${data.confirmations || 0}`);
|
|
|
|
// Recharger le nombre d'ancrages après un court délai
|
|
setTimeout(loadAnchorCount, 2000);
|
|
} else {
|
|
showResult('anchor-result', 'error', data.message || data.error || 'Erreur lors de l\'ancrage.');
|
|
}
|
|
} catch (error) {
|
|
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Demande des sats via le faucet
|
|
*/
|
|
async function requestFaucet() {
|
|
const address = document.getElementById('faucet-address').value.trim();
|
|
|
|
if (!address) {
|
|
showResult('faucet-result', 'error', 'Veuillez entrer une adresse Bitcoin.');
|
|
return;
|
|
}
|
|
|
|
// Validation basique de l'adresse
|
|
const addressPattern = /^(tb1|bcrt1|2|3)[a-zA-HJ-NP-Z0-9]{25,62}$/;
|
|
if (!addressPattern.test(address)) {
|
|
showResult('faucet-result', 'error', 'Adresse Bitcoin invalide.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showResult('faucet-result', 'info', 'Demande en cours...');
|
|
|
|
const response = await fetch(`${FAUCET_API_URL}/api/faucet/request`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ address }),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok && data.txid) {
|
|
showResult('faucet-result', 'success',
|
|
`50 000 sats envoyés avec succès !<br>
|
|
<strong>TXID :</strong> ${data.txid}<br>
|
|
<strong>Montant :</strong> ${data.amount || '50000'} sats<br>
|
|
<strong>Statut :</strong> ${data.status || 'En attente de confirmation'}`);
|
|
} else {
|
|
showResult('faucet-result', 'error', data.message || data.error || 'Erreur lors de la demande.');
|
|
}
|
|
} catch (error) {
|
|
showResult('faucet-result', 'error', `Erreur : ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Affiche un résultat
|
|
*/
|
|
function showResult(elementId, type, message) {
|
|
const element = document.getElementById(elementId);
|
|
element.className = `result ${type}`;
|
|
element.innerHTML = message;
|
|
}
|