Update anchor count, API anchorage, mempool submodule and signet dashboard
**Motivations:** - Update anchor count tracking - Improve API anchorage Bitcoin RPC handling - Update mempool submodule - Enhance signet dashboard functionality and UI **Root causes:** - N/A (evolutions and updates) **Correctifs:** - N/A **Evolutions:** - Updated anchor count tracking - Enhanced Bitcoin RPC handling in API anchorage - Updated mempool submodule to latest version - Added new features and improvements to signet dashboard (app.js, server.js) - Enhanced signet dashboard UI (index.html, styles.css) **Pages affectées:** - anchor_count.txt - api-anchorage/src/bitcoin-rpc.js - api-anchorage/src/bitcoin-rpc.js.backup - mempool (submodule) - signet-dashboard/public/app.js - signet-dashboard/public/index.html - signet-dashboard/public/styles.css - signet-dashboard/src/server.js
This commit is contained in:
parent
e34b6ee43a
commit
21438530a1
@ -1 +1 @@
|
|||||||
2026-01-25T00:20:14.085Z;6982;00000004b2ea52142ffebb57483d6aa53b9b21334e3067f00e54b5df506bf039;9088
|
2026-01-25T01:33:13.690Z;7182;00000002fe6bb5f10aa5f01688bc0e6f862df0e4a4571babd2df5dd30d919b0b;9752
|
||||||
@ -253,6 +253,7 @@ class BitcoinRPC {
|
|||||||
const estimatedFeePerInput = 0.000001; // Estimation des frais par input (conservateur)
|
const estimatedFeePerInput = 0.000001; // Estimation des frais par input (conservateur)
|
||||||
const estimatedFeeBase = 0.00001; // Frais de base pour la transaction
|
const estimatedFeeBase = 0.00001; // Frais de base pour la transaction
|
||||||
const maxChangeRatio = 10; // Maximum 10x le montant requis pour éviter un change trop grand
|
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
|
// Sélectionner les UTXOs nécessaires pour couvrir le montant + frais
|
||||||
const selectedUtxos = [];
|
const selectedUtxos = [];
|
||||||
@ -269,7 +270,7 @@ class BitcoinRPC {
|
|||||||
totalSelected = 0;
|
totalSelected = 0;
|
||||||
|
|
||||||
// Trier les UTXOs : d'abord ceux qui sont juste assez grands, puis les plus petits
|
// 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)
|
// Préférer les UTXOs qui sont juste assez grands (pas trop grands)
|
||||||
const aGood = a.amount >= totalNeeded && a.amount <= totalNeeded * maxChangeRatio;
|
const aGood = a.amount >= totalNeeded && a.amount <= totalNeeded * maxChangeRatio;
|
||||||
const bGood = b.amount >= totalNeeded && b.amount <= totalNeeded * maxChangeRatio;
|
const bGood = b.amount >= totalNeeded && b.amount <= totalNeeded * maxChangeRatio;
|
||||||
|
|||||||
559
api-anchorage/src/bitcoin-rpc.js.backup
Normal file
559
api-anchorage/src/bitcoin-rpc.js.backup
Normal file
@ -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<Function>} 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<Object>} 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<Object>} 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<Object>} 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<string>} 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<number>} 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<Object>} 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<Object>} 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<Object>} 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();
|
||||||
@ -33,6 +33,8 @@ async function loadData() {
|
|||||||
loadWalletBalance(),
|
loadWalletBalance(),
|
||||||
loadAnchorCount(),
|
loadAnchorCount(),
|
||||||
loadNetworkPeers(),
|
loadNetworkPeers(),
|
||||||
|
loadMiningDifficulty(),
|
||||||
|
loadAvgBlockTime(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
updateLastUpdateTime();
|
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
|
* Formate un montant en BTC
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -3,14 +3,19 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Bitcoin Signet - Dashboard de Supervision</title>
|
<title>Bitcoin Ancrage - Dashboard de Supervision</title>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1>Bitcoin Signet - Dashboard de Supervision</h1>
|
<h1>Bitcoin Ancrage - Dashboard de Supervision</h1>
|
||||||
<p class="subtitle">Surveillance de la blockchain et outils de test</p>
|
<p class="subtitle">Surveillance de la blockchain et outils de test</p>
|
||||||
|
<p class="external-links">
|
||||||
|
<a href="https://mempool.4nkweb.com/fr/" target="_blank" rel="noopener noreferrer" class="external-link">
|
||||||
|
🔗 Explorer Mempool
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
@ -49,6 +54,14 @@
|
|||||||
<h3>Nombre de Pairs</h3>
|
<h3>Nombre de Pairs</h3>
|
||||||
<p class="value" id="peer-count">-</p>
|
<p class="value" id="peer-count">-</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>Difficulté de Minage</h3>
|
||||||
|
<p class="value" id="mining-difficulty">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>Temps Moyen entre Blocs</h3>
|
||||||
|
<p class="value" id="avg-block-time">-</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -89,9 +102,9 @@
|
|||||||
|
|
||||||
<!-- Section Faucet -->
|
<!-- Section Faucet -->
|
||||||
<section class="faucet-section">
|
<section class="faucet-section">
|
||||||
<h2>Faucet Bitcoin Signet</h2>
|
<h2>Faucet Bitcoin Ancrage</h2>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p>Recevez 50 000 sats (0.0005 BTC) sur votre adresse Bitcoin Signet</p>
|
<p>Recevez 50 000 ✅ (0.0005 🛡) sur votre adresse Bitcoin Ancrage</p>
|
||||||
<label for="faucet-address">Adresse Bitcoin :</label>
|
<label for="faucet-address">Adresse Bitcoin :</label>
|
||||||
<input type="text" id="faucet-address" placeholder="tb1q..." pattern="^(tb1|bcrt1|2|3)[a-zA-HJ-NP-Z0-9]{25,62}$">
|
<input type="text" id="faucet-address" placeholder="tb1q..." pattern="^(tb1|bcrt1|2|3)[a-zA-HJ-NP-Z0-9]{25,62}$">
|
||||||
<button onclick="requestFaucet()">Demander des Sats</button>
|
<button onclick="requestFaucet()">Demander des Sats</button>
|
||||||
@ -101,7 +114,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>Bitcoin Signet Dashboard - Équipe 4NK</p>
|
<p>Bitcoin Ancrage Dashboard - Équipe 4NK</p>
|
||||||
<p>Dernière mise à jour : <span id="last-update">-</span></p>
|
<p>Dernière mise à jour : <span id="last-update">-</span></p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -49,6 +49,28 @@ header h1 {
|
|||||||
opacity: 0.9;
|
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 {
|
main {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
// Route pour générer un hash SHA256
|
||||||
app.post('/api/hash/generate', (req, res) => {
|
app.post('/api/hash/generate', (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user