Add transaction statistics endpoints and systemd services

**Motivations:**
- Add transaction statistics endpoints (avg-fee, avg-amount) for dashboard display
- Fix difficulty parsing issue when Bitcoin RPC returns string instead of number
- Add systemd service files for proper service management
- Update dashboard UI to display transaction statistics

**Root causes:**
- Bitcoin RPC can return difficulty as string, causing type issues
- Missing API endpoints for transaction statistics in dashboard

**Correctifs:**
- Fix difficulty parsing to handle both string and number types
- Add proper type conversion for blockchainInfo.difficulty

**Evolutions:**
- Add /api/transactions/avg-fee endpoint returning fixed 1200 sats fee
- Add /api/transactions/avg-amount endpoint returning fixed 1000 sats amount
- Add systemd service files for api-faucet and signet-dashboard
- Enhance dashboard UI with transaction statistics display
- Update anchor_count.txt

**Pages affectées:**
- signet-dashboard/src/server.js
- signet-dashboard/public/app.js
- signet-dashboard/public/index.html
- signet-dashboard/signet-dashboard.service
- api-faucet/faucet-api.service
- anchor_count.txt
This commit is contained in:
ncantu 2026-01-25 03:55:26 +01:00
parent 21438530a1
commit b3973ddc41
6 changed files with 308 additions and 11 deletions

View File

@ -1 +1 @@
2026-01-25T01:33:13.690Z;7182;00000002fe6bb5f10aa5f01688bc0e6f862df0e4a4571babd2df5dd30d919b0b;9752
2026-01-25T02:55:07.388Z;7419;00000016a18555e0e95e3214e128029e8c86e8bbbe68c62d240821a6a2951061;10558

View File

@ -0,0 +1,21 @@
[Unit]
Description=Bitcoin Signet Faucet API
After=network.target
[Service]
Type=simple
User=ncantu
WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-faucet
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-faucet/src/server.js
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
# Sécurité
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@ -15,13 +15,50 @@ if (window.location.hostname.includes('dashboard.certificator.4nkweb.com')) {
}
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
*/
@ -35,6 +72,8 @@ async function loadData() {
loadNetworkPeers(),
loadMiningDifficulty(),
loadAvgBlockTime(),
loadAvgFee(),
loadAvgTxAmount(),
]);
updateLastUpdateTime();
@ -52,6 +91,10 @@ async function loadBlockchainInfo() {
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';
@ -165,12 +208,24 @@ async function loadNetworkPeers() {
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) {
// Formater la difficulté avec séparateurs de milliers
const formatted = formatDifficulty(data.difficulty);
document.getElementById('mining-difficulty').textContent = formatted;
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 = '-';
}
@ -184,20 +239,42 @@ async function loadMiningDifficulty() {
* 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) {
document.getElementById('avg-block-time').textContent = data.formatted;
avgBlockTimeValue.textContent = data.formatted;
} else if (data.timeAvgSeconds !== undefined) {
document.getElementById('avg-block-time').textContent = formatBlockTime(data.timeAvgSeconds);
avgBlockTimeValue.textContent = formatBlockTime(data.timeAvgSeconds);
} else {
document.getElementById('avg-block-time').textContent = '-';
avgBlockTimeValue.textContent = '-';
}
} catch (error) {
console.error('Error loading average block time:', error);
document.getElementById('avg-block-time').textContent = 'Erreur';
// Masquer le spinner
avgBlockTimeSpinner.style.display = 'none';
avgBlockTimeValue.textContent = 'Erreur';
}
}
@ -230,6 +307,129 @@ function formatBlockTime(seconds) {
}
}
/**
* 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
*/

View File

@ -60,7 +60,24 @@
</div>
<div class="card">
<h3>Temps Moyen entre Blocs</h3>
<p class="value" id="avg-block-time">-</p>
<p class="value" id="avg-block-time">
<span id="avg-block-time-value">-</span>
<span id="avg-block-time-spinner" class="spinner" style="display: none;"></span>
</p>
</div>
<div class="card">
<h3>Frais Moyens</h3>
<p class="value" id="avg-fee">
<span id="avg-fee-value">-</span>
<span id="avg-fee-spinner" class="spinner" style="display: none;"></span>
</p>
</div>
<div class="card">
<h3>Montant Moyen des Transactions</h3>
<p class="value" id="avg-tx-amount">
<span id="avg-tx-amount-value">-</span>
<span id="avg-tx-amount-spinner" class="spinner" style="display: none;"></span>
</p>
</div>
</div>
</section>

View File

@ -0,0 +1,21 @@
[Unit]
Description=Bitcoin Signet Dashboard
After=network.target
[Service]
Type=simple
User=ncantu
WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/signet-dashboard
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/signet-dashboard/src/server.js
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
# Sécurité
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@ -201,7 +201,10 @@ app.get('/api/anchor/count', async (req, res) => {
app.get('/api/mining/difficulty', async (req, res) => {
try {
const blockchainInfo = await bitcoinRPC.getBlockchainInfo();
const difficulty = blockchainInfo.difficulty || 0;
// Convertir la difficulté en nombre (peut être retournée comme chaîne par Bitcoin RPC)
const difficulty = typeof blockchainInfo.difficulty === 'string'
? parseFloat(blockchainInfo.difficulty)
: (blockchainInfo.difficulty || 0);
res.json({ difficulty });
} catch (error) {
logger.error('Error getting mining difficulty', { error: error.message });
@ -239,6 +242,41 @@ app.get('/api/mining/avg-block-time', async (req, res) => {
}
});
app.get('/api/transactions/avg-fee', async (req, res) => {
try {
// Pour les transactions d'ancrage, le frais moyen est toujours 1200 sats
// Utiliser directement cette valeur au lieu de calculer depuis les blocs
const avgFee = 1200; // Frais moyen des transactions d'ancrage en sats
const avgFeeRate = 0; // Non calculé
res.json({
avgFee,
avgFeeRate,
txCount: 0
});
} catch (error) {
logger.error('Error getting average fee', { error: error.message });
res.status(500).json({ error: error.message });
}
});
app.get('/api/transactions/avg-amount', async (req, res) => {
try {
// Pour les ancrages, le montant est toujours 0.00001 BTC (1000 sats)
// On retourne directement cette valeur pour éviter les calculs lents
const avgAmount = 1000; // 0.00001 BTC en sats
const txCount = 0; // Non calculé pour performance
res.json({
avgAmount,
txCount
});
} catch (error) {
logger.error('Error getting average transaction amount', { error: error.message });
res.status(500).json({ error: error.message });
}
});
/**
* Formate le temps moyen entre blocs en format lisible
* @param {number} seconds - Temps en secondes