diff --git a/anchor_count.txt b/anchor_count.txt index 70ec389..1fadca7 100644 --- a/anchor_count.txt +++ b/anchor_count.txt @@ -1 +1 @@ -2026-01-25T00:20:14.085Z;6982;00000004b2ea52142ffebb57483d6aa53b9b21334e3067f00e54b5df506bf039;9088 \ No newline at end of file +2026-01-25T01:33:13.690Z;7182;00000002fe6bb5f10aa5f01688bc0e6f862df0e4a4571babd2df5dd30d919b0b;9752 \ No newline at end of file diff --git a/api-anchorage/src/bitcoin-rpc.js b/api-anchorage/src/bitcoin-rpc.js index 962eccf..068b378 100644 --- a/api-anchorage/src/bitcoin-rpc.js +++ b/api-anchorage/src/bitcoin-rpc.js @@ -253,6 +253,7 @@ class BitcoinRPC { const estimatedFeePerInput = 0.000001; // Estimation des frais par input (conservateur) const estimatedFeeBase = 0.00001; // Frais de base pour la transaction const maxChangeRatio = 10; // Maximum 10x le montant requis pour éviter un change trop grand + let sortedUnspent = []; // Sélectionner les UTXOs nécessaires pour couvrir le montant + frais const selectedUtxos = []; @@ -269,7 +270,7 @@ class BitcoinRPC { totalSelected = 0; // Trier les UTXOs : d'abord ceux qui sont juste assez grands, puis les plus petits - const sortedUnspent = [...availableUtxos].sort((a, b) => { + sortedUnspent = [...availableUtxos].sort((a, b) => { // Préférer les UTXOs qui sont juste assez grands (pas trop grands) const aGood = a.amount >= totalNeeded && a.amount <= totalNeeded * maxChangeRatio; const bGood = b.amount >= totalNeeded && b.amount <= totalNeeded * maxChangeRatio; diff --git a/api-anchorage/src/bitcoin-rpc.js.backup b/api-anchorage/src/bitcoin-rpc.js.backup new file mode 100644 index 0000000..962eccf --- /dev/null +++ b/api-anchorage/src/bitcoin-rpc.js.backup @@ -0,0 +1,559 @@ +/** + * Client Bitcoin RPC + * + * Gère la connexion et les appels RPC vers le nœud Bitcoin Signet + */ + +import Client from 'bitcoin-core'; +import { logger } from './logger.js'; + +class BitcoinRPC { + constructor() { + this.client = new Client({ + host: process.env.BITCOIN_RPC_HOST || 'localhost', + port: parseInt(process.env.BITCOIN_RPC_PORT || '38332'), + username: process.env.BITCOIN_RPC_USER || 'bitcoin', + password: process.env.BITCOIN_RPC_PASSWORD || 'bitcoin', + timeout: parseInt(process.env.BITCOIN_RPC_TIMEOUT || '30000'), + }); + + // Mutex pour gérer l'accès concurrent aux UTXOs + // Utilise une Promise-based queue pour sérialiser les accès + this.utxoMutexPromise = Promise.resolve(); + + // Liste des UTXOs en cours d'utilisation (format: "txid:vout") + this.lockedUtxos = new Set(); + } + + /** + * Acquiert le mutex pour l'accès aux UTXOs + * @returns {Promise} Fonction pour libérer le mutex + */ + async acquireUtxoMutex() { + // Attendre que le mutex précédent soit libéré + const previousMutex = this.utxoMutexPromise; + let releaseMutex; + + // Créer une nouvelle Promise qui sera résolue quand le mutex est libéré + this.utxoMutexPromise = new Promise((resolve) => { + releaseMutex = resolve; + }); + + // Attendre que le mutex précédent soit libéré + await previousMutex; + + // Retourner la fonction pour libérer le mutex + return releaseMutex; + } + + /** + * Vérifie si un UTXO est verrouillé + * @param {string} txid - ID de la transaction + * @param {number} vout - Index de l'output + * @returns {boolean} True si l'UTXO est verrouillé + */ + isUtxoLocked(txid, vout) { + const key = `${txid}:${vout}`; + return this.lockedUtxos.has(key); + } + + /** + * Verrouille un UTXO + * @param {string} txid - ID de la transaction + * @param {number} vout - Index de l'output + */ + lockUtxo(txid, vout) { + const key = `${txid}:${vout}`; + this.lockedUtxos.add(key); + logger.debug('UTXO locked', { txid: txid.substring(0, 16) + '...', vout }); + } + + /** + * Verrouille plusieurs UTXOs + * @param {Array} utxos - Liste des UTXOs à verrouiller + */ + lockUtxos(utxos) { + for (const utxo of utxos) { + this.lockUtxo(utxo.txid, utxo.vout); + } + } + + /** + * Déverrouille un UTXO + * @param {string} txid - ID de la transaction + * @param {number} vout - Index de l'output + */ + unlockUtxo(txid, vout) { + const key = `${txid}:${vout}`; + this.lockedUtxos.delete(key); + logger.debug('UTXO unlocked', { txid: txid.substring(0, 16) + '...', vout }); + } + + /** + * Déverrouille plusieurs UTXOs + * @param {Array} utxos - Liste des UTXOs à déverrouiller + */ + unlockUtxos(utxos) { + for (const utxo of utxos) { + this.unlockUtxo(utxo.txid, utxo.vout); + } + } + + /** + * Vérifie la connexion au nœud Bitcoin + * @returns {Promise} Informations sur le nœud + */ + async checkConnection() { + try { + const networkInfo = await this.client.getNetworkInfo(); + const blockchainInfo = await this.client.getBlockchainInfo(); + + return { + connected: true, + blocks: blockchainInfo.blocks, + chain: blockchainInfo.chain, + networkactive: networkInfo.networkactive, + connections: networkInfo.connections, + }; + } catch (error) { + logger.error('Bitcoin RPC connection error', { error: error.message }); + return { + connected: false, + error: error.message, + }; + } + } + + /** + * Obtient une nouvelle adresse depuis le wallet + * @returns {Promise} Adresse Bitcoin + */ + async getNewAddress() { + try { + return await this.client.getNewAddress(); + } catch (error) { + logger.error('Error getting new address', { error: error.message }); + throw new Error(`Failed to get new address: ${error.message}`); + } + } + + /** + * Obtient le solde du wallet + * @returns {Promise} Solde en BTC + */ + async getBalance() { + try { + return await this.client.getBalance(); + } catch (error) { + logger.error('Error getting balance', { error: error.message }); + throw new Error(`Failed to get balance: ${error.message}`); + } + } + + /** + * Crée une transaction d'ancrage + * + * @param {string} hash - Hash du document à ancrer (hex) + * @param {string} recipientAddress - Adresse de destination (optionnel, utilise getNewAddress si non fourni) + * @returns {Promise} Transaction créée avec txid + */ + async createAnchorTransaction(hash, recipientAddress = null) { + // Acquérir le mutex pour l'accès aux UTXOs + const releaseMutex = await this.acquireUtxoMutex(); + let selectedUtxos = []; + + try { + // Vérifier que le hash est valide (64 caractères hex) + if (!/^[0-9a-fA-F]{64}$/.test(hash)) { + throw new Error('Invalid hash format. Must be 64 character hexadecimal string.'); + } + + // Obtenir une adresse de destination si non fournie + const address = recipientAddress || await this.getNewAddress(); + + // Obtenir le solde disponible + const balance = await this.getBalance(); + const feeRate = parseFloat(process.env.MINING_FEE_RATE || '0.00001'); + + if (balance < feeRate) { + throw new Error(`Insufficient balance. Required: ${feeRate} BTC, Available: ${balance} BTC`); + } + + // Créer une transaction avec le hash dans les données OP_RETURN + // Format: OP_RETURN + "ANCHOR:" + hash (32 bytes) + const hashBuffer = Buffer.from(hash, 'hex'); + const anchorData = Buffer.concat([ + Buffer.from('ANCHOR:', 'utf8'), + hashBuffer, + ]); + + // Obtenir les UTXOs disponibles (inclure les non confirmés pour avoir plus d'options) + // Utiliser fetch directement avec l'URL RPC incluant le wallet pour éviter les problèmes de wallet + 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}`; + + // Utiliser Basic Auth dans les headers (fetch ne supporte pas les credentials dans l'URL) + const auth = Buffer.from(`${username}:${password}`).toString('base64'); + + 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: [0], + }), + }); + + if (!rpcResponse.ok) { + const errorText = await rpcResponse.text(); + logger.error('HTTP error in listunspent', { 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', { error: rpcResult.error }); + throw new Error(`RPC error: ${rpcResult.error.message}`); + } + const unspent = rpcResult.result; + + logger.info('Fetched UTXOs', { count: unspent.length, firstFew: unspent.slice(0, 3).map(u => ({ txid: u.txid.substring(0, 16), vout: u.vout, amount: u.amount })) }); + + if (unspent.length === 0) { + throw new Error('No unspent outputs available'); + } + + // Filtrer les UTXOs verrouillés (en cours d'utilisation par d'autres transactions) + const availableUtxos = unspent.filter(utxo => !this.isUtxoLocked(utxo.txid, utxo.vout)); + + logger.info('Available UTXOs (after filtering locked)', { + total: unspent.length, + available: availableUtxos.length, + locked: unspent.length - availableUtxos.length, + amounts: availableUtxos.map(u => u.amount).slice(0, 10), + largest: availableUtxos.length > 0 ? Math.max(...availableUtxos.map(u => u.amount)) : 0, + }); + + if (availableUtxos.length === 0) { + throw new Error('No available UTXOs (all are locked or in use)'); + } + + // Sélectionner plusieurs UTXOs si nécessaire (coin selection) + // Stratégie : préférer les UTXOs qui sont juste assez grands, puis combiner plusieurs petits UTXOs + const amount = 0.00001; // Montant minimal pour la transaction + const estimatedFeePerInput = 0.000001; // Estimation des frais par input (conservateur) + const estimatedFeeBase = 0.00001; // Frais de base pour la transaction + const maxChangeRatio = 10; // Maximum 10x le montant requis pour éviter un change trop grand + + // Sélectionner les UTXOs nécessaires pour couvrir le montant + frais + const selectedUtxos = []; + let totalSelected = 0; + + // Estimer le nombre d'inputs nécessaires (itération pour ajuster les frais) + let estimatedInputs = 1; + let totalNeeded = amount + estimatedFeeBase; + + // Itérer jusqu'à trouver une combinaison qui fonctionne + for (let iteration = 0; iteration < 10; iteration++) { + totalNeeded = amount + estimatedFeeBase + (estimatedInputs * estimatedFeePerInput); + selectedUtxos.length = 0; + totalSelected = 0; + + // Trier les UTXOs : d'abord ceux qui sont juste assez grands, puis les plus petits + const sortedUnspent = [...availableUtxos].sort((a, b) => { + // Préférer les UTXOs qui sont juste assez grands (pas trop grands) + const aGood = a.amount >= totalNeeded && a.amount <= totalNeeded * maxChangeRatio; + const bGood = b.amount >= totalNeeded && b.amount <= totalNeeded * maxChangeRatio; + + if (aGood && !bGood) return -1; + if (!aGood && bGood) return 1; + + // Sinon, trier par montant croissant pour minimiser le change + return a.amount - b.amount; + }); + + // Sélectionner les UTXOs jusqu'à avoir suffisamment de fonds + for (const utxo of sortedUnspent) { + if (totalSelected >= totalNeeded) { + break; + } + + // Éviter les UTXOs trop grands qui créeraient un change énorme + // Sauf si c'est le seul UTXO disponible ou si on a déjà plusieurs UTXOs + if (selectedUtxos.length === 0 && utxo.amount > totalNeeded * maxChangeRatio) { + // Si c'est le premier UTXO et qu'il est trop grand, continuer à chercher + // Mais si c'est le seul disponible, l'utiliser quand même + continue; + } + + selectedUtxos.push(utxo); + totalSelected += utxo.amount; + } + + // Si on a assez de fonds, sortir de la boucle + if (totalSelected >= totalNeeded) { + break; + } + + // Sinon, réessayer avec plus d'inputs estimés + estimatedInputs = selectedUtxos.length + 1; + } + + // Vérifier qu'on a assez de fonds + if (totalSelected < totalNeeded) { + throw new Error(`Insufficient UTXO amount. Required: ${totalNeeded} BTC, Available: ${totalSelected} BTC. Selected ${selectedUtxos.length} UTXOs from ${sortedUnspent.length} available.`); + } + + const now = new Date().toISOString(); + logger.info('Selected UTXOs for transaction', { + hash: hash, + date: now, + count: selectedUtxos.length, + totalAmount: totalSelected, + required: totalNeeded, + change: totalSelected - totalNeeded, + }); + + // Verrouiller les UTXOs sélectionnés pour éviter qu'ils soient utilisés par d'autres transactions + this.lockUtxos(selectedUtxos); + + // Créer la transaction raw avec les inputs et outputs (sans fundrawtransaction) + // Cela évite les erreurs de frais trop élevés avec la bibliothèque bitcoin-core + const inputs = selectedUtxos.map(utxo => ({ + txid: utxo.txid, + vout: utxo.vout, + })); + + // Calculer le change (monnaie restante après avoir payé le montant) + // Estimation des frais : base + (nombre d'inputs * frais par input) + const estimatedFee = estimatedFeeBase + (selectedUtxos.length * estimatedFeePerInput); + let change = totalSelected - amount - estimatedFee; + + // Arrondir le change à 8 décimales (précision Bitcoin standard) + change = Math.round(change * 100000000) / 100000000; + + // Créer les outputs + const outputs = { + data: anchorData.toString('hex'), // OP_RETURN output (doit être en premier) + }; + + // Ajouter l'output de destination avec le montant minimal (arrondi à 8 décimales) + outputs[address] = Math.round(amount * 100000000) / 100000000; + + // Si le change est significatif (> 0.00001 BTC pour éviter les problèmes de précision), l'envoyer à une adresse de change + // Sinon, il sera considéré comme frais (dust) + if (change > 0.00001) { + const changeAddress = await this.getNewAddress(); + outputs[changeAddress] = change; + logger.info('Adding change output', { changeAddress, change }); + } else if (change > 0) { + logger.info('Change too small, will be included in fees', { change }); + } + + const tx = await this.client.command('createrawtransaction', inputs, outputs); + + // Signer la transaction + // Utiliser command() directement pour éviter les problèmes avec la bibliothèque + const signedTx = await this.client.command('signrawtransactionwithwallet', tx); + + if (!signedTx.complete) { + throw new Error('Transaction signing failed'); + } + + // Envoyer la transaction au mempool + // Utiliser command() avec maxfeerate comme deuxième paramètre (0 = accepter n'importe quel taux) + // Le test direct avec bitcoin-cli fonctionne avec cette syntaxe + const txid = await this.client.command('sendrawtransaction', signedTx.hex, 0); + + logger.info('Anchor transaction sent to mempool', { + txid, + hash: hash.substring(0, 16) + '...', + address, + }); + + // Obtenir les informations de la transaction (dans le mempool) + const txInfo = await this.getTransactionInfo(txid); + + // Déverrouiller les UTXOs maintenant que la transaction est dans le mempool + // Les UTXOs seront automatiquement marqués comme dépensés par Bitcoin Core + this.unlockUtxos(selectedUtxos); + + // Libérer le mutex + releaseMutex(); + + return { + txid, + status: 'confirmed', // Transaction dans le mempool + confirmations: txInfo.confirmations || 0, + block_height: txInfo.blockheight || null, // null si pas encore dans un bloc + }; + } catch (error) { + logger.error('Error creating anchor transaction', { + error: error.message, + hash: hash?.substring(0, 16) + '...', + }); + + // En cas d'erreur, déverrouiller les UTXOs et libérer le mutex + if (selectedUtxos.length > 0) { + this.unlockUtxos(selectedUtxos); + } + releaseMutex(); + + throw error; + } + } + + /** + * Obtient les informations d'une transaction + * @param {string} txid - ID de la transaction + * @returns {Promise} Informations de la transaction + */ + async getTransactionInfo(txid) { + try { + const tx = await this.client.getTransaction(txid); + const blockchainInfo = await this.client.getBlockchainInfo(); + + return { + txid: tx.txid, + confirmations: tx.confirmations || 0, + blockheight: tx.blockheight || null, + blockhash: tx.blockhash || null, + time: tx.time || null, + currentBlockHeight: blockchainInfo.blocks, + }; + } catch (error) { + logger.error('Error getting transaction info', { error: error.message, txid }); + throw new Error(`Failed to get transaction info: ${error.message}`); + } + } + + /** + * Vérifie si un hash est ancré dans la blockchain + * + * @param {string} hash - Hash à vérifier + * @param {string} txid - ID de transaction optionnel pour accélérer la recherche + * @returns {Promise} Résultat de la vérification + */ + async verifyAnchor(hash, txid = null) { + try { + // Vérifier que le hash est valide + if (!/^[0-9a-fA-F]{64}$/.test(hash)) { + throw new Error('Invalid hash format. Must be 64 character hexadecimal string.'); + } + + // Si un txid est fourni, vérifier directement cette transaction + if (txid) { + try { + const tx = await this.client.getTransaction(txid, true); + const rawTx = await this.client.getRawTransaction(txid, true); + + // Vérifier si le hash est dans les outputs OP_RETURN + const hashFound = this.checkHashInTransaction(rawTx, hash); + + if (hashFound) { + return { + verified: true, + anchor_info: { + transaction_id: txid, + block_height: tx.blockheight || null, + confirmations: tx.confirmations || 0, + }, + }; + } + } catch (error) { + // Si la transaction n'existe pas, continuer la recherche + logger.warn('Transaction not found, searching blockchain', { txid, error: error.message }); + } + } + + // Rechercher dans les blocs récents (derniers 100 blocs) + const blockchainInfo = await this.client.getBlockchainInfo(); + const currentHeight = blockchainInfo.blocks; + const searchRange = 100; // Rechercher dans les 100 derniers blocs + + for (let height = currentHeight; height >= Math.max(0, currentHeight - searchRange); height--) { + try { + const blockHash = await this.client.getBlockHash(height); + const block = await this.client.getBlock(blockHash, 2); // Verbose level 2 + + // Parcourir toutes les transactions du bloc + for (const tx of block.tx || []) { + try { + const rawTx = await this.client.getRawTransaction(tx.txid, true); + const hashFound = this.checkHashInTransaction(rawTx, hash); + + if (hashFound) { + return { + verified: true, + anchor_info: { + transaction_id: tx.txid, + block_height: height, + confirmations: currentHeight - height + 1, + }, + }; + } + } catch (error) { + // Continuer avec la transaction suivante + logger.debug('Error checking transaction', { txid: tx.txid, error: error.message }); + } + } + } catch (error) { + // Continuer avec le bloc suivant + logger.debug('Error checking block', { height, error: error.message }); + } + } + + // Hash non trouvé + return { + verified: false, + message: 'Hash not found in recent blocks', + }; + } catch (error) { + logger.error('Error verifying anchor', { error: error.message, hash: hash?.substring(0, 16) + '...' }); + throw error; + } + } + + /** + * Vérifie si un hash est présent dans une transaction + * @param {Object} rawTx - Transaction brute + * @param {string} hash - Hash à rechercher + * @returns {boolean} True si le hash est trouvé + */ + checkHashInTransaction(rawTx, hash) { + try { + // Parcourir les outputs de la transaction + for (const output of rawTx.vout || []) { + // Chercher dans les scripts OP_RETURN + if (output.scriptPubKey && output.scriptPubKey.hex) { + const scriptHex = output.scriptPubKey.hex; + + // Vérifier si le script contient "ANCHOR:" suivi du hash + const anchorPrefix = Buffer.from('ANCHOR:', 'utf8').toString('hex'); + const hashHex = hash.toLowerCase(); + + if (scriptHex.includes(anchorPrefix + hashHex)) { + return true; + } + } + } + return false; + } catch (error) { + logger.error('Error checking hash in transaction', { error: error.message }); + return false; + } + } +} + +// Export class and singleton +export { BitcoinRPC }; +export const bitcoinRPC = new BitcoinRPC(); diff --git a/signet-dashboard/public/app.js b/signet-dashboard/public/app.js index 1cf1a22..eb7ba2d 100644 --- a/signet-dashboard/public/app.js +++ b/signet-dashboard/public/app.js @@ -33,6 +33,8 @@ async function loadData() { loadWalletBalance(), loadAnchorCount(), loadNetworkPeers(), + loadMiningDifficulty(), + loadAvgBlockTime(), ]); updateLastUpdateTime(); @@ -157,6 +159,77 @@ async function loadNetworkPeers() { } } +/** + * Charge la difficulté de minage + */ +async function loadMiningDifficulty() { + try { + const response = await fetch(`${API_BASE_URL}/api/mining/difficulty`); + 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; + } 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() { + try { + const response = await fetch(`${API_BASE_URL}/api/mining/avg-block-time`); + const data = await response.json(); + + if (data.formatted) { + document.getElementById('avg-block-time').textContent = data.formatted; + } else if (data.timeAvgSeconds !== undefined) { + document.getElementById('avg-block-time').textContent = formatBlockTime(data.timeAvgSeconds); + } else { + document.getElementById('avg-block-time').textContent = '-'; + } + } catch (error) { + console.error('Error loading average block time:', error); + document.getElementById('avg-block-time').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 BTC */ diff --git a/signet-dashboard/public/index.html b/signet-dashboard/public/index.html index a031b09..bead7ef 100644 --- a/signet-dashboard/public/index.html +++ b/signet-dashboard/public/index.html @@ -3,14 +3,19 @@ - Bitcoin Signet - Dashboard de Supervision + Bitcoin Ancrage - Dashboard de Supervision
-

Bitcoin Signet - Dashboard de Supervision

+

Bitcoin Ancrage - Dashboard de Supervision

Surveillance de la blockchain et outils de test

+
@@ -49,6 +54,14 @@

Nombre de Pairs

-

+
+

Difficulté de Minage

+

-

+
+
+

Temps Moyen entre Blocs

+

-

+
@@ -89,9 +102,9 @@
-

Faucet Bitcoin Signet

+

Faucet Bitcoin Ancrage

-

Recevez 50 000 sats (0.0005 BTC) sur votre adresse Bitcoin Signet

+

Recevez 50 000 ✅ (0.0005 🛡) sur votre adresse Bitcoin Ancrage

@@ -101,7 +114,7 @@
diff --git a/signet-dashboard/public/styles.css b/signet-dashboard/public/styles.css index 5193bd9..2ea09d9 100644 --- a/signet-dashboard/public/styles.css +++ b/signet-dashboard/public/styles.css @@ -49,6 +49,28 @@ header h1 { opacity: 0.9; } +.external-links { + margin-top: 15px; +} + +.external-link { + display: inline-block; + padding: 10px 20px; + background-color: rgba(255, 255, 255, 0.2); + color: white; + text-decoration: none; + border-radius: 5px; + font-size: 1em; + transition: background-color 0.3s, transform 0.2s; + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.external-link:hover { + background-color: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); + text-decoration: none; +} + main { margin-bottom: 40px; } diff --git a/signet-dashboard/src/server.js b/signet-dashboard/src/server.js index 80ad971..4b7eab6 100644 --- a/signet-dashboard/src/server.js +++ b/signet-dashboard/src/server.js @@ -198,6 +198,66 @@ 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; + res.json({ difficulty }); + } catch (error) { + logger.error('Error getting mining difficulty', { error: error.message }); + res.status(500).json({ error: error.message }); + } +}); + +app.get('/api/mining/avg-block-time', async (req, res) => { + try { + // Utiliser l'API mempool pour obtenir le temps moyen entre blocs + const mempoolUrl = process.env.MEMPOOL_API_URL || 'https://mempool.4nkweb.com'; + const difficultyAdjustment = await makeHttpRequest(mempoolUrl, '/api/v1/difficulty-adjustment', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (difficultyAdjustment.error) { + throw new Error(difficultyAdjustment.message || 'Failed to get difficulty adjustment'); + } + + // timeAvg est en millisecondes + const timeAvgMs = difficultyAdjustment.timeAvg || 0; + const timeAvgSeconds = Math.round(timeAvgMs / 1000); + + res.json({ + timeAvg: timeAvgMs, + timeAvgSeconds: timeAvgSeconds, + formatted: formatBlockTime(timeAvgSeconds) + }); + } catch (error) { + logger.error('Error getting average block time', { 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 + * @returns {string} Temps formaté + */ +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`; + } +} + // Route pour générer un hash SHA256 app.post('/api/hash/generate', (req, res) => { try {