/** * 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; let lastBlockHeight = null; let blockPollingInterval = null; // Initialisation document.addEventListener('DOMContentLoaded', () => { loadData(); setInterval(loadData, 30000); // Rafraîchir toutes les 30 secondes // Démarrer le polling pour détecter les nouveaux blocs startBlockPolling(); }); /** * Démarrer le polling pour détecter les nouveaux blocs */ function startBlockPolling() { // Vérifier la hauteur du bloc toutes les 5 secondes blockPollingInterval = setInterval(async () => { try { const response = await fetch(`${API_BASE_URL}/api/blockchain/info`); if (!response.ok) return; const data = await response.json(); const currentHeight = data.blocks; // Si la hauteur a changé, actualiser les métriques if (lastBlockHeight !== null && currentHeight > lastBlockHeight) { console.log(`Nouveau bloc détecté: ${lastBlockHeight} -> ${currentHeight}`); // Actualiser uniquement les 4 métriques spécifiques await Promise.all([ loadAvgFee(), loadAvgTxAmount(), loadMiningDifficulty(), loadAvgBlockTime(), ]); } lastBlockHeight = currentHeight; } catch (error) { console.error('Error checking block height:', error); } }, 5000); // Vérifier toutes les 5 secondes } /** * Charge toutes les données de supervision */ async function loadData() { try { await Promise.all([ loadBlockchainInfo(), loadLatestBlock(), loadWalletBalance(), loadAnchorCount(), loadNetworkPeers(), loadMiningDifficulty(), loadAvgBlockTime(), loadAvgFee(), loadAvgTxAmount(), ]); 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; // Initialiser lastBlockHeight si ce n'est pas déjà fait if (lastBlockHeight === null && data.blocks !== undefined) { lastBlockHeight = data.blocks; } } 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'; } } /** * Charge la difficulté de minage */ async function loadMiningDifficulty() { try { const response = await fetch(`${API_BASE_URL}/api/mining/difficulty`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.difficulty !== undefined && data.difficulty !== null) { // Convertir en nombre si nécessaire (sécurité supplémentaire) const difficulty = typeof data.difficulty === 'string' ? parseFloat(data.difficulty) : Number(data.difficulty); if (!isNaN(difficulty)) { // Formater la difficulté avec séparateurs de milliers const formatted = formatDifficulty(difficulty); document.getElementById('mining-difficulty').textContent = formatted; } else { document.getElementById('mining-difficulty').textContent = '-'; } } else { document.getElementById('mining-difficulty').textContent = '-'; } } catch (error) { console.error('Error loading mining difficulty:', error); document.getElementById('mining-difficulty').textContent = 'Erreur'; } } /** * Charge le temps moyen entre blocs */ async function loadAvgBlockTime() { const avgBlockTimeValue = document.getElementById('avg-block-time-value'); const avgBlockTimeSpinner = document.getElementById('avg-block-time-spinner'); if (!avgBlockTimeValue || !avgBlockTimeSpinner) { console.error('Elements avg-block-time-value or avg-block-time-spinner not found in DOM'); return; } // Afficher le spinner avgBlockTimeSpinner.style.display = 'inline'; avgBlockTimeValue.textContent = '...'; try { const response = await fetch(`${API_BASE_URL}/api/mining/avg-block-time`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Masquer le spinner et mettre à jour la valeur avgBlockTimeSpinner.style.display = 'none'; if (data.formatted) { avgBlockTimeValue.textContent = data.formatted; } else if (data.timeAvgSeconds !== undefined) { avgBlockTimeValue.textContent = formatBlockTime(data.timeAvgSeconds); } else { avgBlockTimeValue.textContent = '-'; } } catch (error) { console.error('Error loading average block time:', error); // Masquer le spinner avgBlockTimeSpinner.style.display = 'none'; avgBlockTimeValue.textContent = 'Erreur'; } } /** * Formate la difficulté avec séparateurs de milliers */ function formatDifficulty(difficulty) { if (difficulty === 0) return '0'; if (difficulty < 1) return difficulty.toFixed(4); if (difficulty < 1000) return difficulty.toFixed(2); if (difficulty < 1000000) return (difficulty / 1000).toFixed(2) + ' K'; if (difficulty < 1000000000) return (difficulty / 1000000).toFixed(2) + ' M'; return (difficulty / 1000000000).toFixed(2) + ' G'; } /** * Formate le temps moyen entre blocs en format lisible */ function formatBlockTime(seconds) { if (seconds < 60) { return `${seconds}s`; } else if (seconds < 3600) { const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m`; } else { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`; } } /** * Formate un montant en sats avec l'emoji ✅ */ function formatSats(sats) { if (sats === 0) return '0 ✅'; if (sats < 1000) return sats.toLocaleString('fr-FR') + ' ✅'; if (sats < 1000000) return (sats / 1000).toFixed(2) + ' K ✅'; if (sats < 1000000000) return (sats / 1000000).toFixed(2) + ' M ✅'; return (sats / 1000000000).toFixed(2) + ' G ✅'; } /** * Formate un montant en sats avec l'emoji ✅ (sans préfixes K/M/G) */ function formatSatsSimple(sats) { if (sats === 0) return '0 ✅'; return sats.toLocaleString('fr-FR') + ' ✅'; } /** * Charge les frais moyens */ async function loadAvgFee() { const avgFeeValue = document.getElementById('avg-fee-value'); const avgFeeSpinner = document.getElementById('avg-fee-spinner'); if (!avgFeeValue || !avgFeeSpinner) { console.error('Elements avg-fee-value or avg-fee-spinner not found in DOM'); return; } // Afficher le spinner avgFeeSpinner.style.display = 'inline'; avgFeeValue.textContent = '...'; try { const response = await fetch(`${API_BASE_URL}/api/transactions/avg-fee`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Masquer le spinner et mettre à jour la valeur avgFeeSpinner.style.display = 'none'; if (data.avgFee !== undefined && data.avgFee !== null) { const avgFee = Number(data.avgFee); if (!isNaN(avgFee) && avgFee >= 0) { const formatted = formatSatsSimple(avgFee); avgFeeValue.textContent = formatted; } else { avgFeeValue.textContent = '-'; } } else { avgFeeValue.textContent = '-'; } } catch (error) { console.error('Error loading average fee', error); // Masquer le spinner avgFeeSpinner.style.display = 'none'; avgFeeValue.textContent = 'Erreur'; } } /** * Charge le montant moyen des transactions */ async function loadAvgTxAmount() { const avgTxAmountValue = document.getElementById('avg-tx-amount-value'); const avgTxAmountSpinner = document.getElementById('avg-tx-amount-spinner'); if (!avgTxAmountValue || !avgTxAmountSpinner) { console.error('Elements avg-tx-amount-value or avg-tx-amount-spinner not found in DOM'); return; } // Afficher le spinner avgTxAmountSpinner.style.display = 'inline'; avgTxAmountValue.textContent = '...'; try { const response = await fetch(`${API_BASE_URL}/api/transactions/avg-amount`, { signal: AbortSignal.timeout(60000) // Timeout de 60 secondes }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.error) { throw new Error(data.error); } // Masquer le spinner et mettre à jour la valeur avgTxAmountSpinner.style.display = 'none'; if (data.avgAmount !== undefined && data.avgAmount !== null) { // Convertir en nombre si nécessaire const avgAmount = typeof data.avgAmount === 'string' ? parseFloat(data.avgAmount) : Number(data.avgAmount); if (!isNaN(avgAmount) && avgAmount >= 0) { const formatted = formatSatsSimple(avgAmount); avgTxAmountValue.textContent = formatted; } else { avgTxAmountValue.textContent = '-'; } } else { avgTxAmountValue.textContent = '-'; } } catch (error) { console.error('Error loading average transaction amount', error); // Masquer le spinner avgTxAmountSpinner.style.display = 'none'; avgTxAmountValue.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 = ` Fichier sélectionné : ${file.name}
Taille : ${formatFileSize(file.size)}
Type : ${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 !
TXID : ${info.transaction_id || 'N/A'}
Hauteur du bloc : ${info.block_height !== null && info.block_height !== undefined ? info.block_height : 'Non confirmé'}
Confirmations : ${info.confirmations || 0}
Statut : ${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 !
TXID : ${data.txid}
Statut : ${data.status}
Confirmations : ${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 !
TXID : ${data.txid}
Montant : ${data.amount || '50000'} sats
Statut : ${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; }