From 4d3028da0c062939abb31edda37cc4fc67d83b0e Mon Sep 17 00:00:00 2001 From: ncantu Date: Wed, 28 Jan 2026 12:18:33 +0100 Subject: [PATCH] Docs API dashboard et skipIfExists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivations:** - Aligner la doc API du dashboard avec les évolutions (pagination serveur, base SQLite, frais, mining, hash, UTXO, etc.) - Documenter le paramètre skipIfExists de l'API d'ancrage - Corriger les références /health pour le dashboard (utiliser /api/blockchain/info) **Root causes:** - N/A (évolution documentation) **Correctifs:** - N/A **Evolutions:** - Section API Dashboard dans api-docs.html ; endpoints utxo/count, utxo/list (pagination, category), utxo/fees, fees/update, hash/list, hash/generate, mining, transactions, anchor/example - Paramètre skipIfExists et réponses old: true/false pour POST /api/anchor/document - DASHBOARD.md : liste endpoints à jour, tests sans /health - DOMAINS_AND_PORTS.md : tests dashboard via /api/blockchain/info - features/dashboard-api-docs-update.md **Pages affectées:** - signet-dashboard/public/api-docs.html - docs/DASHBOARD.md - docs/DOMAINS_AND_PORTS.md - features/dashboard-api-docs-update.md - api-anchorage/src/bitcoin-rpc.js, bitcoin-rpc.js.backup - data/sync-utxos.log - fixKnowledge/api-anchorage-null-includes-error.md, api-anchorage-rbf-replacement-error.md --- api-anchorage/src/bitcoin-rpc.js | 100 +- api-anchorage/src/bitcoin-rpc.js.backup | 968 ++++++++++++++++++ data/sync-utxos.log | 84 +- docs/DASHBOARD.md | 73 +- docs/DOMAINS_AND_PORTS.md | 4 +- features/dashboard-api-docs-update.md | 62 ++ .../api-anchorage-null-includes-error.md | 124 +++ .../api-anchorage-rbf-replacement-error.md | 182 ++++ signet-dashboard/public/api-docs.html | 537 +++++++++- 9 files changed, 2031 insertions(+), 103 deletions(-) create mode 100644 api-anchorage/src/bitcoin-rpc.js.backup create mode 100644 features/dashboard-api-docs-update.md create mode 100644 fixKnowledge/api-anchorage-null-includes-error.md create mode 100644 fixKnowledge/api-anchorage-rbf-replacement-error.md diff --git a/api-anchorage/src/bitcoin-rpc.js b/api-anchorage/src/bitcoin-rpc.js index c70a35c..e54c66e 100644 --- a/api-anchorage/src/bitcoin-rpc.js +++ b/api-anchorage/src/bitcoin-rpc.js @@ -231,6 +231,7 @@ class BitcoinRPC { // Acquérir le mutex pour l'accès aux UTXOs const releaseMutex = await this.acquireUtxoMutex(); let selectedUtxo = null; + let selectedUtxos = []; try { // Vérifier que le hash est valide (64 caractères hex) @@ -332,7 +333,6 @@ class BitcoinRPC { let utxoFromDb = utxoQuery.get(totalNeeded); // Si aucun UTXO assez grand, essayer de combiner plusieurs petits UTXOs - let selectedUtxos = []; let totalSelectedAmount = 0; let estimatedFeeForMultipleInputs = estimatedFee; @@ -645,7 +645,101 @@ class BitcoinRPC { // 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); + let txid; + try { + txid = await this.client.command('sendrawtransaction', signedTx.hex, 0); + } catch (sendError) { + // Gérer l'erreur de remplacement RBF (Replace By Fee) + // Si une transaction avec les mêmes inputs existe déjà, Bitcoin Core rejette la nouvelle + // si les frais ne sont pas plus élevés + const errorMessage = sendError.message || sendError.toString() || JSON.stringify(sendError); + logger.warn('Error sending transaction to mempool', { + error: errorMessage, + hash: hash.substring(0, 16) + '...', + }); + + // Vérifier si c'est une erreur de remplacement RBF + const isRbfError = errorMessage.includes('insufficient fee') && + (errorMessage.includes('rejecting replacement') || errorMessage.includes('replacement')); + + if (isRbfError) { + // Extraire le txid de la transaction existante depuis le message d'erreur + // Format: "insufficient fee, rejecting replacement ; new feerate ... <= old feerate ..." + const replacementMatch = errorMessage.match(/rejecting replacement ([a-fA-F0-9]{64})/); + if (replacementMatch && replacementMatch[1]) { + const existingTxid = replacementMatch[1]; + logger.info('Transaction replacement rejected, using existing transaction', { + existingTxid: existingTxid.substring(0, 16) + '...', + hash: hash.substring(0, 16) + '...', + }); + + // Vérifier si la transaction existe dans le mempool ou dans la blockchain + try { + const mempoolEntry = await this.client.command('getmempoolentry', existingTxid); + if (mempoolEntry) { + // La transaction existe dans le mempool, utiliser cette transaction + txid = existingTxid; + logger.info('Using existing transaction from mempool', { + txid: txid.substring(0, 16) + '...', + }); + } else { + // La transaction n'existe pas dans le mempool, vérifier si elle est confirmée + logger.warn('Existing transaction not found in mempool, checking blockchain', { + existingTxid: existingTxid.substring(0, 16) + '...', + }); + throw new Error('Transaction not in mempool'); + } + } catch (mempoolError) { + // Si getmempoolentry échoue, la transaction n'existe peut-être pas dans le mempool + // mais elle pourrait être confirmée dans la blockchain + const errorMsg = mempoolError.message || mempoolError.toString(); + if (errorMsg.includes('not in mempool') || errorMsg.includes('Transaction not in mempool')) { + // La transaction n'est pas dans le mempool, vérifier si elle est confirmée + try { + const txInfo = await this.client.getTransaction(existingTxid); + if (txInfo && txInfo.txid) { + // La transaction existe dans la blockchain (confirmée), utiliser cette transaction + txid = existingTxid; + logger.info('Using existing confirmed transaction from blockchain', { + txid: txid.substring(0, 16) + '...', + confirmations: txInfo.confirmations || 0, + }); + } else { + // La transaction n'existe ni dans le mempool ni dans la blockchain + logger.warn('Existing transaction not found in mempool or blockchain', { + existingTxid: existingTxid.substring(0, 16) + '...', + }); + throw sendError; + } + } catch (txError) { + // La transaction n'existe pas, relancer l'erreur originale + logger.warn('Could not verify existing transaction in mempool or blockchain', { + error: txError.message || txError.toString(), + existingTxid: existingTxid.substring(0, 16) + '...', + }); + throw sendError; + } + } else { + // Autre erreur, relancer l'erreur originale + logger.warn('Error checking mempool entry', { + error: errorMsg, + existingTxid: existingTxid.substring(0, 16) + '...', + }); + throw sendError; + } + } + } else { + // Impossible d'extraire le txid, relancer l'erreur + logger.warn('Could not extract txid from RBF error message', { + errorMessage: errorMessage.substring(0, 200), + }); + throw sendError; + } + } else { + // Autre type d'erreur, relancer + throw sendError; + } + } logger.info('Anchor transaction with provisioning sent to mempool', { txid, @@ -707,7 +801,7 @@ class BitcoinRPC { matchedAddress = address; } // Vérifier si c'est un output de provisionnement (adresse dans la liste et montant = 2500 sats) - else if (provisioningAddresses.includes(outputAddress) && Math.abs(outputAmount - provisioningAmountRounded) < 0.00000001) { + else if (finalProvisioningAddresses && finalProvisioningAddresses.includes(outputAddress) && Math.abs(outputAmount - provisioningAmountRounded) < 0.00000001) { outputType = 'provisioning'; matchedAddress = outputAddress; } diff --git a/api-anchorage/src/bitcoin-rpc.js.backup b/api-anchorage/src/bitcoin-rpc.js.backup new file mode 100644 index 0000000..c70a35c --- /dev/null +++ b/api-anchorage/src/bitcoin-rpc.js.backup @@ -0,0 +1,968 @@ +/** + * 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'; +import dns from 'dns'; +import { getDatabase } from './database.js'; + +// Force IPv4 first to avoid IPv6 connection issues +// This ensures that even if the system prefers IPv6, Node.js will try IPv4 first +dns.setDefaultResultOrder('ipv4first'); + +class BitcoinRPC { + constructor() { + this.client = new Client({ + host: process.env.BITCOIN_RPC_HOST || '127.0.0.1', + 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(); + + // Timeout pour l'attente du mutex (180s = 3 minutes) + // Si une requête prend plus de 180s, elle sera automatiquement libérée + this.utxoMutexTimeout = 180000; + + // Note: Les UTXOs verrouillés sont maintenant gérés uniquement dans la base de données + // via is_locked_in_mutex pour éviter la duplication et réduire la consommation mémoire + } + + /** + * Acquiert le mutex pour l'accès aux UTXOs avec timeout + * @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; + let timeoutId; + + // Créer une nouvelle Promise qui sera résolue quand le mutex est libéré + this.utxoMutexPromise = new Promise((resolve) => { + releaseMutex = resolve; + }); + + // Créer une Promise avec timeout pour éviter les blocages indéfinis + const mutexWithTimeout = Promise.race([ + previousMutex, + new Promise((_, reject) => { + timeoutId = setTimeout(() => { + logger.warn('Mutex acquisition timeout, forcing release', { + timeout: this.utxoMutexTimeout, + }); + reject(new Error(`Mutex acquisition timeout after ${this.utxoMutexTimeout}ms`)); + }, this.utxoMutexTimeout); + }), + ]); + + try { + await mutexWithTimeout; + } finally { + if (timeoutId) { + clearTimeout(timeoutId); + } + } + + // 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) { + try { + const db = getDatabase(); + const result = db.prepare(` + SELECT is_locked_in_mutex + FROM utxos + WHERE txid = ? AND vout = ? + `).get(txid, vout); + return result?.is_locked_in_mutex === 1; + } catch (error) { + logger.warn('Error checking UTXO lock status in database', { + error: error.message, + txid: txid.substring(0, 16) + '...', + vout, + }); + return false; + } + } + + /** + * Verrouille un UTXO + * @param {string} txid - ID de la transaction + * @param {number} vout - Index de l'output + */ + lockUtxo(txid, vout) { + // Marquer l'UTXO comme verrouillé dans la base de données uniquement + try { + const db = getDatabase(); + db.prepare(` + UPDATE utxos + SET is_locked_in_mutex = 1, updated_at = CURRENT_TIMESTAMP + WHERE txid = ? AND vout = ? + `).run(txid, vout); + logger.debug('UTXO locked', { txid: txid.substring(0, 16) + '...', vout }); + } catch (error) { + logger.warn('Error updating UTXO lock status in database', { + error: error.message, + 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) { + // Marquer l'UTXO comme non verrouillé dans la base de données uniquement + try { + const db = getDatabase(); + db.prepare(` + UPDATE utxos + SET is_locked_in_mutex = 0, updated_at = CURRENT_TIMESTAMP + WHERE txid = ? AND vout = ? + `).run(txid, vout); + logger.debug('UTXO unlocked', { txid: txid.substring(0, 16) + '...', vout }); + } catch (error) { + logger.warn('Error updating UTXO unlock status in database', { + error: error.message, + 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, provisioningAddresses = null, numberOfProvisioningUtxos = null, retryCount = 0) { + // Acquérir le mutex pour l'accès aux UTXOs + const releaseMutex = await this.acquireUtxoMutex(); + let selectedUtxo = null; + + 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 les adresses nécessaires en parallèle pour optimiser les performances + // On a besoin de : 1 adresse principale + N adresses de provisioning + 1 adresse de change (si nécessaire) + // Utiliser le paramètre fourni ou la valeur par défaut de 7 + const provisioningCount = numberOfProvisioningUtxos ?? 7; + const addressesNeeded = 1 + provisioningCount + 1; // principal + provisioning + change + + // Générer toutes les adresses en parallèle + const addressPromises = []; + for (let i = 0; i < addressesNeeded; i++) { + addressPromises.push(this.getNewAddress()); + } + const allAddresses = await Promise.all(addressPromises); + + // Utiliser l'adresse fournie ou la première générée + const address = recipientAddress || allAddresses[0]; + const finalProvisioningAddresses = provisioningAddresses || allAddresses.slice(1, 1 + provisioningCount); + const changeAddressCandidate = allAddresses[addressesNeeded - 1]; + + // 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, + ]); + + // Fonction helper pour arrondir à 8 décimales (précision Bitcoin standard) + const roundTo8Decimals = (amount) => { + return Math.round(amount * 100000000) / 100000000; + }; + + // Stratégie : Provisionner à chaque ancrage + // Utiliser un gros UTXO pour créer : + // - 1 output d'ancrage de 2500 sats (0.000025 BTC) + // - N outputs de provisionnement de 2500 sats chacun + // - Le reste en change + const utxoAmount = 0.000025; // 2500 sats par UTXO + const anchorOutputAmount = utxoAmount; // 1 UTXO pour l'ancrage actuel + const totalProvisioningAmount = utxoAmount * provisioningCount; + const totalOutputAmount = anchorOutputAmount + totalProvisioningAmount; + + // Estimation des frais : base + frais par output + // On va ajouter 2 OP_RETURN supplémentaires (change + frais), donc 3 OP_RETURN au total + const estimatedFeePerOutput = 0.000001; // Frais par output (conservateur) + const estimatedFeePerOpReturn = 0.0000015; // Frais par OP_RETURN (légèrement plus cher) + const estimatedFeeBase = 0.00001; // Frais de base + const numberOfOpReturns = 3; // OP_RETURN anchor + OP_RETURN change + OP_RETURN fees + const numberOfRegularOutputs = 1 + provisioningCount + 1; // 1 ancrage + N provisioning + 1 change (si nécessaire) + const estimatedFeeBeforeMargin = estimatedFeeBase + (numberOfOpReturns * estimatedFeePerOpReturn) + (numberOfRegularOutputs * estimatedFeePerOutput); + + // Prendre une marge de sécurité de 30% sur les frais + const feeMargin = 0.3; // 30% de marge + const estimatedFee = roundTo8Decimals(estimatedFeeBeforeMargin * (1 + feeMargin)); + + const totalNeeded = totalOutputAmount + estimatedFee; + + logger.info('Anchor transaction with provisioning', { + hash: hash.substring(0, 16) + '...', + anchorOutputAmount, + numberOfProvisioningUtxos: provisioningCount, + totalProvisioningAmount, + totalOutputAmount, + estimatedFee, + totalNeeded, + }); + + // Obtenir un UTXO disponible depuis la base de données + // Optimisation : ne charger qu'un seul UTXO au lieu de tous les UTXOs + // Le filtrage des UTXOs verrouillés se fait directement dans la requête SQL + const db = getDatabase(); + + // Sélectionner un UTXO disponible depuis la DB + // Critères : confirmé, non dépensé, non verrouillé, montant suffisant + const utxoQuery = db.prepare(` + SELECT txid, vout, address, amount, confirmations, block_time + FROM utxos + WHERE confirmations > 0 + AND is_spent_onchain = 0 + AND is_locked_in_mutex = 0 + AND amount >= ? + ORDER BY amount DESC + LIMIT 1 + `); + let utxoFromDb = utxoQuery.get(totalNeeded); + + // Si aucun UTXO assez grand, essayer de combiner plusieurs petits UTXOs + let selectedUtxos = []; + let totalSelectedAmount = 0; + let estimatedFeeForMultipleInputs = estimatedFee; + + if (!utxoFromDb) { + // Chercher plusieurs petits UTXOs dont la somme est suffisante + const combineUtxosQuery = db.prepare(` + SELECT txid, vout, address, amount, confirmations, block_time + FROM utxos + WHERE confirmations > 0 + AND is_spent_onchain = 0 + AND is_locked_in_mutex = 0 + ORDER BY amount DESC + `); + const availableUtxos = combineUtxosQuery.all(); + + if (availableUtxos.length === 0) { + throw new Error('No available UTXOs in database (all are locked, spent, or unconfirmed)'); + } + + // Calculer le montant total nécessaire avec une marge pour les frais supplémentaires + // (combiner plusieurs UTXOs augmente la taille de la transaction) + // Estimation: ~148 bytes par input supplémentaire + const estimatedBytesPerInput = 148; + const estimatedFeePerInput = 0.0000001; // Conservateur + const maxUtxosToCombine = 20; // Limite pour éviter des transactions trop grandes + estimatedFeeForMultipleInputs = estimatedFee; + + // Sélectionner les UTXOs jusqu'à atteindre le montant nécessaire + for (let i = 0; i < availableUtxos.length && i < maxUtxosToCombine; i++) { + const utxo = availableUtxos[i]; + if (totalSelectedAmount >= totalNeeded + estimatedFeeForMultipleInputs) { + break; + } + selectedUtxos.push({ + txid: utxo.txid, + vout: utxo.vout, + address: utxo.address || '', + amount: utxo.amount, + confirmations: utxo.confirmations || 0, + blockTime: utxo.block_time, + }); + totalSelectedAmount += utxo.amount; + // Ajuster l'estimation des frais pour chaque input supplémentaire + if (selectedUtxos.length > 1) { + estimatedFeeForMultipleInputs += estimatedFeePerInput; + } + } + + if (totalSelectedAmount < totalNeeded + estimatedFeeForMultipleInputs) { + const largestUtxo = availableUtxos[0]; + throw new Error( + `No UTXO large enough for anchor with provisioning. Required: ${totalNeeded.toFixed(8)} BTC, ` + + `Largest available: ${largestUtxo.amount} BTC. ` + + `Total from ${selectedUtxos.length} UTXOs: ${totalSelectedAmount.toFixed(8)} BTC` + ); + } + + logger.info('Combining multiple UTXOs for anchor transaction', { + numberOfUtxos: selectedUtxos.length, + totalAmount: totalSelectedAmount, + totalNeeded: totalNeeded + estimatedFeeForMultipleInputs, + }); + + // Verrouiller tous les UTXOs sélectionnés + for (const utxo of selectedUtxos) { + this.lockUtxo(utxo.txid, utxo.vout); + } + + // Utiliser le premier UTXO comme référence pour la compatibilité avec le code existant + selectedUtxo = selectedUtxos[0]; + } else { + // Un seul UTXO assez grand trouvé + selectedUtxos = [{ + txid: utxoFromDb.txid, + vout: utxoFromDb.vout, + address: utxoFromDb.address || '', + amount: utxoFromDb.amount, + confirmations: utxoFromDb.confirmations || 0, + blockTime: utxoFromDb.block_time, + }]; + totalSelectedAmount = utxoFromDb.amount; + selectedUtxo = selectedUtxos[0]; + + logger.info('Selected UTXO from database', { + txid: selectedUtxo.txid.substring(0, 16) + '...', + vout: selectedUtxo.vout, + amount: selectedUtxo.amount, + confirmations: selectedUtxo.confirmations, + totalNeeded, + }); + + // Verrouiller l'UTXO sélectionné + this.lockUtxo(selectedUtxo.txid, selectedUtxo.vout); + } + + // Créer les outputs + // Note: Bitcoin Core ne permet qu'un seul OP_RETURN par transaction via 'data' + // Pour plusieurs OP_RETURN, il faut créer la transaction manuellement avec des scripts + // Pour l'instant, on utilise un seul OP_RETURN combiné avec format: "ANCHOR:|CHANGE:
:|FEE:" + const outputs = {}; + + // 1 output d'ancrage de 2500 sats (arrondi à 8 décimales) + outputs[address] = roundTo8Decimals(anchorOutputAmount); + + // N outputs de provisionnement de 2500 sats chacun (arrondis à 8 décimales) + // Les adresses ont déjà été générées en parallèle plus haut + for (let i = 0; i < provisioningCount; i++) { + outputs[finalProvisioningAddresses[i]] = roundTo8Decimals(utxoAmount); + } + + // Calculer le change (arrondi à 8 décimales) + // Utiliser totalSelectedAmount si plusieurs UTXOs sont combinés + const totalInputAmount = selectedUtxos.length > 1 ? totalSelectedAmount : selectedUtxo.amount; + // Ajuster les frais si plusieurs inputs + const finalEstimatedFee = selectedUtxos.length > 1 ? estimatedFeeForMultipleInputs : estimatedFee; + const change = roundTo8Decimals(totalInputAmount - totalOutputAmount - finalEstimatedFee); + let changeAddress = null; + if (change > 0.00001) { + changeAddress = changeAddressCandidate; + 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 }); + } + + // Construire les données OP_RETURN avec marquage onchain du change et des frais + // Format: "ANCHOR:" + hash (32 bytes) + "|CHANGE:
:|FEE:" + // Le hash doit être en bytes, pas en hex string, pour compatibilité avec la vérification + const changeAmountSats = change > 0.00001 ? Math.round(change * 100000000) : 0; + const changeAddressStr = changeAddress || 'none'; + const feeAmountSats = Math.round(estimatedFee * 100000000); + + // Construire les métadonnées de change et frais (format compact avec sats) + const metadataParts = [ + `CHANGE:${changeAddressStr}:${changeAmountSats}`, + `FEE:${feeAmountSats}`, + ]; + const metadataString = metadataParts.join('|'); + + // Créer l'OP_RETURN : "ANCHOR:" + hash (bytes) + "|" + métadonnées + const metadataBuffer = Buffer.from(metadataString, 'utf8'); + const combinedData = Buffer.concat([ + Buffer.from('ANCHOR:', 'utf8'), + hashBuffer, // hash en bytes (32 bytes) + Buffer.from('|', 'utf8'), + metadataBuffer, // métadonnées en UTF-8 + ]); + + // Ajouter l'OP_RETURN (doit être en premier dans les outputs) + outputs.data = combinedData.toString('hex'); + + logger.info('OP_RETURN metadata created', { + hash: hash.substring(0, 16) + '...', + changeAddress: changeAddressStr.substring(0, 16) + '...', + changeAmountSats, + feeAmountSats, + totalSize: combinedData.length, + }); + + // Vérifier que tous les UTXOs sont toujours disponibles avant de les utiliser + // (peut avoir été dépensés entre la sélection et l'utilisation) + // Limiter les tentatives pour éviter les boucles infinies + if (retryCount < 3) { + try { + // Récupérer toutes les adresses uniques des UTXOs sélectionnés + const uniqueAddresses = [...new Set(selectedUtxos.map(u => u.address))]; + const utxoCheck = await this.client.listunspent(0, 9999999, uniqueAddresses); + + // Vérifier que tous les UTXOs sont toujours disponibles + let allUtxosAvailable = true; + for (const utxo of selectedUtxos) { + const utxoStillAvailable = utxoCheck.some(u => + u.txid === utxo.txid && u.vout === utxo.vout + ); + if (!utxoStillAvailable) { + allUtxosAvailable = false; + logger.warn('Selected UTXO no longer available, marking as spent', { + txid: utxo.txid.substring(0, 16) + '...', + vout: utxo.vout, + }); + + // Marquer l'UTXO comme dépensé dans la DB + try { + const dbForUpdate = getDatabase(); + dbForUpdate.prepare(` + UPDATE utxos + SET is_spent_onchain = 1, is_locked_in_mutex = 0, updated_at = CURRENT_TIMESTAMP + WHERE txid = ? AND vout = ? + `).run(utxo.txid, utxo.vout); + } catch (dbError) { + logger.warn('Error updating UTXO in database', { error: dbError.message }); + } + } + } + + if (!allUtxosAvailable) { + // Au moins un UTXO n'est plus disponible, déverrouiller tous et réessayer + logger.warn('Some UTXOs no longer available, unlocking all and retrying', { + retryCount, + numberOfUtxos: selectedUtxos.length, + }); + + // Déverrouiller tous les UTXOs + for (const utxo of selectedUtxos) { + this.unlockUtxo(utxo.txid, utxo.vout); + } + + // Réessayer (récursion limitée à 3 tentatives) + return this.createAnchorTransaction(hash, recipientAddress, provisioningAddresses, numberOfProvisioningUtxos, retryCount + 1); + } + } catch (checkError) { + logger.warn('Error checking UTXO availability, proceeding anyway', { + error: checkError.message, + numberOfUtxos: selectedUtxos.length, + }); + // Continuer même si la vérification échoue (peut être un problème réseau temporaire) + } + } else { + logger.error('Max retry count reached for UTXO selection', { retryCount }); + throw new Error('Failed to find available UTXOs after multiple attempts'); + } + + // Créer la transaction avec tous les UTXOs sélectionnés + const inputs = selectedUtxos.map(utxo => ({ + txid: utxo.txid, + vout: utxo.vout, + })); + + let tx; + try { + tx = await this.client.command('createrawtransaction', inputs, outputs); + } catch (error) { + logger.error('Error creating raw transaction', { + error: error.message, + txid: selectedUtxo.txid.substring(0, 16) + '...', + vout: selectedUtxo.vout, + }); + // Marquer tous les UTXOs comme dépensés si l'erreur suggère qu'ils n'existent plus + if (error.message.includes('not found') || error.message.includes('does not exist')) { + try { + const dbForUpdate = getDatabase(); + const updateStmt = dbForUpdate.prepare(` + UPDATE utxos + SET is_spent_onchain = 1, updated_at = CURRENT_TIMESTAMP + WHERE txid = ? AND vout = ? + `); + for (const utxo of selectedUtxos) { + updateStmt.run(utxo.txid, utxo.vout); + } + } catch (dbError) { + logger.warn('Error updating UTXOs in database', { error: dbError.message }); + } + } + throw new Error(`Failed to create transaction: ${error.message}`); + } + + // 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) { + const errorDetails = signedTx.errors || []; + const errorMessages = errorDetails.map(e => { + const errorMsg = e.error || 'Unknown error'; + const txid = e.txid || selectedUtxo.txid.substring(0, 16) + '...'; + const vout = e.vout !== undefined ? e.vout : selectedUtxo.vout; + return `${errorMsg} (txid: ${txid}, vout: ${vout})`; + }).join('; '); + + logger.error('Transaction signing failed', { + txid: selectedUtxo.txid.substring(0, 16) + '...', + vout: selectedUtxo.vout, + errors: errorDetails, + signedTxHex: signedTx.hex ? signedTx.hex.substring(0, 32) + '...' : 'none', + }); + + // Si l'erreur indique que l'UTXO n'existe plus ou est déjà dépensé, le marquer comme dépensé + const hasUtxoNotFoundError = errorDetails.some(e => { + const errorMsg = (e.error || '').toLowerCase(); + return errorMsg.includes('not found') || + errorMsg.includes('does not exist') || + errorMsg.includes('missing') || + errorMsg.includes('already spent') || + errorMsg.includes('input not found'); + }); + + if (hasUtxoNotFoundError) { + try { + const dbForUpdate = getDatabase(); + const updateStmt = dbForUpdate.prepare(` + UPDATE utxos + SET is_spent_onchain = 1, is_locked_in_mutex = 0, updated_at = CURRENT_TIMESTAMP + WHERE txid = ? AND vout = ? + `); + for (const utxo of selectedUtxos) { + updateStmt.run(utxo.txid, utxo.vout); + } + logger.info('UTXOs marked as spent due to signing error', { + numberOfUtxos: selectedUtxos.length, + error: errorMessages, + }); + } catch (dbError) { + logger.warn('Error updating UTXO in database', { error: dbError.message }); + } + } + + throw new Error(`Transaction signing failed: ${errorMessages || 'Unknown error'}`); + } + + // 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 with provisioning sent to mempool', { + txid, + hash: hash.substring(0, 16) + '...', + address, + provisioningAddresses: finalProvisioningAddresses.map(addr => addr.substring(0, 16) + '...'), + numberOfProvisioningUtxos: provisioningCount, + }); + + // Obtenir les informations de la transaction (dans le mempool) + const txInfo = await this.getTransactionInfo(txid); + + // Obtenir la transaction brute pour identifier les index des outputs + const rawTx = await this.client.getRawTransaction(txid, true); + + // Calculer les frais réels de la transaction + // Frais = somme des inputs - somme des outputs + // Optimisation : utiliser les montants déjà connus des UTXOs sélectionnés au lieu de faire des appels RPC + // Cela évite N appels RPC supplémentaires (un par input) + const totalInputAmountForFee = selectedUtxos.length > 1 + ? totalSelectedAmount + : selectedUtxo.amount; + let totalOutputAmountInTx = 0; + + // Calculer la somme des outputs + if (rawTx.vout) { + for (const output of rawTx.vout) { + totalOutputAmountInTx += output.value || 0; + } + } + + const actualFee = roundTo8Decimals(totalInputAmountForFee - totalOutputAmountInTx); + + // Construire la liste des outputs avec leur type explicite + // En analysant les outputs réels de la transaction brute + const outputsInfo = []; + const anchorAmountRounded = roundTo8Decimals(anchorOutputAmount); + const provisioningAmountRounded = roundTo8Decimals(utxoAmount); + + // Parcourir tous les outputs de la transaction brute + if (rawTx.vout) { + for (let i = 0; i < rawTx.vout.length; i++) { + const output = rawTx.vout[i]; + const outputAddresses = output.scriptPubKey?.addresses || []; + const outputAddress = outputAddresses.length > 0 ? outputAddresses[0] : null; + const outputAmount = output.value || 0; + + // Identifier le type d'output + let outputType = 'unknown'; + let matchedAddress = null; + + // Vérifier si c'est un OP_RETURN + if (output.scriptPubKey?.type === 'nulldata') { + outputType = 'op_return'; + } + // Vérifier si c'est l'output d'ancrage (adresse correspond et montant = 2500 sats) + else if (outputAddress === address && Math.abs(outputAmount - anchorAmountRounded) < 0.00000001) { + outputType = 'anchor'; + matchedAddress = address; + } + // Vérifier si c'est un output de provisionnement (adresse dans la liste et montant = 2500 sats) + else if (provisioningAddresses.includes(outputAddress) && Math.abs(outputAmount - provisioningAmountRounded) < 0.00000001) { + outputType = 'provisioning'; + matchedAddress = outputAddress; + } + // Vérifier si c'est le change (adresse correspond à changeAddress) + else if (change > 0.00001 && outputAddress === changeAddress) { + outputType = 'change'; + matchedAddress = changeAddress; + } + + outputsInfo.push({ + index: i, + type: outputType, + address: matchedAddress || outputAddress, + amount: outputAmount, + }); + } + } + + // Marquer tous les UTXOs comme dépensés dans la base de données + // Les UTXOs sont dépensés dans une transaction (mempool), mais pas encore confirmés dans un bloc + try { + const dbForUpdate = getDatabase(); + const updateStmt = dbForUpdate.prepare(` + UPDATE utxos + SET is_spent_onchain = 1, is_locked_in_mutex = 0, updated_at = CURRENT_TIMESTAMP + WHERE txid = ? AND vout = ? + `); + for (const utxo of selectedUtxos) { + updateStmt.run(utxo.txid, utxo.vout); + } + logger.debug('UTXOs marked as spent in database', { + numberOfUtxos: selectedUtxos.length, + txid: selectedUtxo.txid.substring(0, 16) + '...', + vout: selectedUtxo.vout, + }); + } catch (error) { + logger.warn('Error updating UTXO in database', { + error: error.message, + txid: selectedUtxo.txid.substring(0, 16) + '...', + vout: selectedUtxo.vout, + }); + } + + // Déverrouiller tous les UTXOs maintenant que la transaction est dans le mempool + // (mise à jour DB déjà faite ci-dessus, mais on déverrouille aussi en mémoire) + for (const utxo of selectedUtxos) { + this.unlockUtxo(utxo.txid, utxo.vout); + } + + // Stocker l'ancre dans la base de données + try { + const dbForAnchor = getDatabase(); + const date = new Date().toISOString(); + dbForAnchor.prepare(` + INSERT OR REPLACE INTO anchors (hash, txid, block_height, confirmations, date, updated_at) + VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `).run( + hash, + txid, + txInfo.blockheight || null, + txInfo.confirmations || 0, + date + ); + logger.debug('Anchor stored in database', { + hash: hash.substring(0, 16) + '...', + txid: txid.substring(0, 16) + '...', + }); + } catch (error) { + logger.warn('Error storing anchor in database', { + error: error.message, + hash: hash.substring(0, 16) + '...', + txid: txid.substring(0, 16) + '...', + }); + // Ne pas faire échouer la transaction si le stockage en base échoue + } + + // Libérer le mutex (sera aussi libéré dans finally, mais on le fait ici pour être explicite) + 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 + outputs: outputsInfo, + fee: actualFee, + fee_sats: Math.round(actualFee * 100000000), + }; + } catch (error) { + logger.error('Error creating anchor transaction', { + error: error.message, + hash: hash?.substring(0, 16) + '...', + }); + + // En cas d'erreur, déverrouiller tous les UTXOs + if (selectedUtxos && selectedUtxos.length > 0) { + // Déverrouiller tous les UTXOs (mise à jour DB + mémoire) + for (const utxo of selectedUtxos) { + this.unlockUtxo(utxo.txid, utxo.vout); + } + } + // Le mutex sera libéré dans le bloc finally pour garantir la libération même en cas d'erreur non gérée + throw error; + } finally { + // Garantir que le mutex est toujours libéré, même en cas d'erreur non gérée + try { + releaseMutex(); + } catch (releaseError) { + logger.warn('Error releasing mutex', { error: releaseError.message }); + } + } + } + + /** + * 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/data/sync-utxos.log b/data/sync-utxos.log index d5fc36e..df2364d 100644 --- a/data/sync-utxos.log +++ b/data/sync-utxos.log @@ -1,45 +1,3 @@ - ⏳ Traitement: 200000/225867 UTXOs insérés... - ⏳ Traitement: 210000/225867 UTXOs insérés... - ⏳ Traitement: 220000/225867 UTXOs insérés... -💾 Mise à jour des UTXOs dépensés... - -📊 Résumé: - - UTXOs vérifiés: 61598 - - UTXOs toujours disponibles: 61598 - - UTXOs dépensés détectés: 0 - -📈 Statistiques finales: - - Total UTXOs: 68398 - - Dépensés: 6800 - - Non dépensés: 61598 - -✅ Synchronisation terminée -🔍 Démarrage de la synchronisation des UTXOs dépensés... - -📊 UTXOs à vérifier: 61565 -📡 Récupération des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 225882 -💾 Création de la table temporaire... -💾 Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/225882 UTXOs insérés... - ⏳ Traitement: 20000/225882 UTXOs insérés... - ⏳ Traitement: 30000/225882 UTXOs insérés... - ⏳ Traitement: 40000/225882 UTXOs insérés... - ⏳ Traitement: 50000/225882 UTXOs insérés... - ⏳ Traitement: 60000/225882 UTXOs insérés... - ⏳ Traitement: 70000/225882 UTXOs insérés... - ⏳ Traitement: 80000/225882 UTXOs insérés... - ⏳ Traitement: 90000/225882 UTXOs insérés... - ⏳ Traitement: 100000/225882 UTXOs insérés... - ⏳ Traitement: 110000/225882 UTXOs insérés... - ⏳ Traitement: 120000/225882 UTXOs insérés... - ⏳ Traitement: 130000/225882 UTXOs insérés... - ⏳ Traitement: 140000/225882 UTXOs insérés... - ⏳ Traitement: 150000/225882 UTXOs insérés... - ⏳ Traitement: 160000/225882 UTXOs insérés... - ⏳ Traitement: 170000/225882 UTXOs insérés... - ⏳ Traitement: 180000/225882 UTXOs insérés... - ⏳ Traitement: 190000/225882 UTXOs insérés... ⏳ Traitement: 200000/225882 UTXOs insérés... ⏳ Traitement: 210000/225882 UTXOs insérés... ⏳ Traitement: 220000/225882 UTXOs insérés... @@ -98,3 +56,45 @@ - Non dépensés: 49190 ✅ Synchronisation terminée +🔍 Démarrage de la synchronisation des UTXOs dépensés... + +📊 UTXOs à vérifier: 49190 +📡 Récupération des UTXOs depuis Bitcoin... +📊 UTXOs disponibles dans Bitcoin: 223667 +💾 Création de la table temporaire... +💾 Insertion des UTXOs disponibles par batch... + ⏳ Traitement: 10000/223667 UTXOs insérés... + ⏳ Traitement: 20000/223667 UTXOs insérés... + ⏳ Traitement: 30000/223667 UTXOs insérés... + ⏳ Traitement: 40000/223667 UTXOs insérés... + ⏳ Traitement: 50000/223667 UTXOs insérés... + ⏳ Traitement: 60000/223667 UTXOs insérés... + ⏳ Traitement: 70000/223667 UTXOs insérés... + ⏳ Traitement: 80000/223667 UTXOs insérés... + ⏳ Traitement: 90000/223667 UTXOs insérés... + ⏳ Traitement: 100000/223667 UTXOs insérés... + ⏳ Traitement: 110000/223667 UTXOs insérés... + ⏳ Traitement: 120000/223667 UTXOs insérés... + ⏳ Traitement: 130000/223667 UTXOs insérés... + ⏳ Traitement: 140000/223667 UTXOs insérés... + ⏳ Traitement: 150000/223667 UTXOs insérés... + ⏳ Traitement: 160000/223667 UTXOs insérés... + ⏳ Traitement: 170000/223667 UTXOs insérés... + ⏳ Traitement: 180000/223667 UTXOs insérés... + ⏳ Traitement: 190000/223667 UTXOs insérés... + ⏳ Traitement: 200000/223667 UTXOs insérés... + ⏳ Traitement: 210000/223667 UTXOs insérés... + ⏳ Traitement: 220000/223667 UTXOs insérés... +💾 Mise à jour des UTXOs dépensés... + +📊 Résumé: + - UTXOs vérifiés: 49190 + - UTXOs toujours disponibles: 49168 + - UTXOs dépensés détectés: 22 + +📈 Statistiques finales: + - Total UTXOs: 68398 + - Dépensés: 19230 + - Non dépensés: 49168 + +✅ Synchronisation terminée diff --git a/docs/DASHBOARD.md b/docs/DASHBOARD.md index 32b4f8f..16315e7 100644 --- a/docs/DASHBOARD.md +++ b/docs/DASHBOARD.md @@ -1,8 +1,8 @@ # Dashboard - Pages et Fonctionnalités **Auteur** : Équipe 4NK -**Date** : 2026-01-24 -**Version** : 1.0 +**Date** : 2026-01-28 +**Version** : 1.1 ## Vue d'Ensemble @@ -92,16 +92,9 @@ Le Dashboard Bitcoin Signet est une interface web de supervision et de test acce **Fichier :** `signet-dashboard/public/hash-list.html` **Fonctionnalités :** -- Liste de tous les hash ancrés sur la blockchain -- Informations pour chaque hash : - - Hash SHA256 - - TXID de la transaction - - Hauteur de bloc - - Nombre de confirmations - - Timestamp - - Liens vers mempool.4nkweb.com -- Recherche et filtrage -- Pagination +- Liste des hash ancrés (source : base SQLite, `GET /api/hash/list?page=&limit=`) +- Informations pour chaque hash : hash SHA256, TXID, hauteur de bloc, confirmations, date ; liens vers mempool.4nkweb.com +- Pagination côté serveur ### 3. Page Liste des UTXO (`/utxo-list`) @@ -120,12 +113,13 @@ Le Dashboard Bitcoin Signet est une interface web de supervision et de test acce - Adresse - Confirmations - Statut (Disponible, Verrouillé, Dépensé onchain) -- **Pagination** : 100 UTXOs par page avec contrôles précédent/suivant +- **Pagination** : chargement par catégorie avec `?category=&page=&limit=` (ex. 100 UTXOs par page), contrôles précédent/suivant - **Tri** : Tri par montant ou confirmations (croissant/décroissant) en cliquant sur les en-têtes - **Capacité d'ancrage restante** : Affiche le nombre d'ancrages possibles et le nombre d'UTXOs confirmés disponibles -- **Consolidation** : Bouton pour consolider les petits UTXOs (< 2500 sats) en un gros UTXO +- **Consolidation** : Bouton pour consolider les petits UTXOs (< 2500 sats) en un gros UTXO (`POST /api/utxo/consolidate`) - Affiche dynamiquement le nombre d'UTXOs et le montant total concernés - Désactivé si aucun UTXO à consolider +- **Récupérer les frais** : Bouton « Récupérer les frais depuis les ancrages » appelant `POST /api/utxo/fees/update` - **Filtrage confirmés** : Seuls les UTXOs avec au moins 1 confirmation sont affichés (pour éviter les erreurs "too-long-mempool-chain") - Export des données (fichier texte téléchargeable) @@ -150,14 +144,10 @@ Le Dashboard Bitcoin Signet est une interface web de supervision et de test acce **Fonctionnalités :** - Documentation complète de toutes les APIs - Endpoints documentés : - - API d'Ancrage (`/api/anchor/document`, `/api/anchor/verify`) - - API Faucet (`/api/faucet/request`) - - API Filigrane (`/api/watermark/document`) - - API ClamAV (`/api/scan/buffer`) -- Exemples de requêtes curl -- Codes de statut HTTP -- Authentification et clés API -- Format des réponses + - API d'Ancrage (`/api/anchor/document`, `/api/anchor/verify`), Faucet, Filigrane, ClamAV + - **API Dashboard** : `/api/utxo/count`, `/api/utxo/list` (pagination, catégories), `/api/utxo/fees`, `POST /api/utxo/fees/update`, `/api/utxo/small-info`, `POST /api/utxo/consolidate`, `/api/hash/list` (pagination), `POST /api/hash/generate`, `/api/mining/difficulty`, `/api/mining/avg-block-time`, `/api/transactions/avg-fee`, `/api/transactions/avg-amount`, `/api/anchor/example`, etc. +- Exemples de requêtes curl, paramètres (query/body), réponses +- Codes de statut HTTP, authentification (APIs externes), format des réponses ### 6. Page Apprendre Bitcoin (`/learn`) @@ -234,31 +224,44 @@ Le menu principal est accessible depuis toutes les pages : ## API Endpoints Utilisés -Le dashboard utilise les endpoints suivants : +Le dashboard utilise les endpoints suivants. Tous les endpoints internes sont servis par le Dashboard (port 3020), sans authentification. -### Endpoints Internes +### Endpoints internes (Dashboard) - `GET /api/blockchain/info` : Informations sur la blockchain - `GET /api/blockchain/latest-block` : Dernier bloc miné - `GET /api/wallet/balance` : Solde du wallet -- `GET /api/utxo/list` : Liste des UTXO (catégorisés par type) - - Retourne : blocRewards, anchors, changes, fees - - Compteurs : total, availableForAnchor, confirmedAvailableForAnchor -- `GET /api/utxo/small-info` : Informations sur les petits UTXOs (< 2500 sats) disponibles pour consolidation -- `POST /api/utxo/consolidate` : Consolide les petits UTXOs en un gros UTXO - `GET /api/network/peers` : Nombre de pairs - `GET /api/anchor/count` : Nombre d'ancrages -- `GET /api/hash/list` : Liste des hash ancrés -- `POST /api/anchor/test` : Test d'ancrage (via le dashboard) -- `POST /api/watermark/document` : Ancrage avec filigrane (via le dashboard) +- `GET /api/utxo/count` : Compteurs UTXO (ancrages, disponibles pour ancrage, confirmés) ; optimisé, pas de liste +- `GET /api/utxo/list` : Liste des UTXO (pagination serveur, base SQLite) + - Query : `category` (all | bloc_rewards | ancrages | changes | fees), `page`, `limit` + - `category=all` : uniquement les counts (blocRewards, anchors, changes, fees, total, availableForAnchor, confirmedAvailableForAnchor) + - Sinon : données paginées + `pagination` (category, page, limit, total, totalPages) +- `GET /api/utxo/fees` : Liste des frais (transactions d'ancrage, métadonnées OP_RETURN) +- `POST /api/utxo/fees/update` : Récupère les frais depuis les ancrages et les enregistre (body optionnel : `sinceBlockHeight`) +- `GET /api/utxo/small-info` : Petits UTXOs (< 2500 sats) disponibles pour consolidation +- `POST /api/utxo/consolidate` : Consolide les petits UTXOs en un gros UTXO +- `GET /api/hash/list` : Liste des hash ancrés (pagination : `page`, `limit`) +- `POST /api/hash/generate` : Génère un hash SHA256 (body : `text` ou `fileContent`, optionnel `isBase64`) +- `GET /api/mining/difficulty` : Difficulté de minage +- `GET /api/mining/avg-block-time` : Temps moyen entre blocs (Mempool) +- `GET /api/transactions/avg-fee` : Frais moyen ancrages (1200 sats) +- `GET /api/transactions/avg-amount` : Montant moyen ancrages (1000 sats) +- `GET /api/anchor/example` : Exemple de transaction d'ancrage (page Learn) +- `POST /api/anchor/verify` : Vérification d'un hash (proxy vers API ancrage ; body : `hash`, `txid?`, `apiKey`) +- `POST /api/anchor/test` : Test d'ancrage (proxy vers API ancrage ; body : `hash`, `apiKey`) +- `POST /api/watermark/document` : Ancrage avec filigrane (proxy vers API filigrane) -### Endpoints Externes +### Endpoints externes - `https://certificator.4nkweb.com/api/anchor/document` : API d'ancrage - `https://watermark.certificator.4nkweb.com/api/watermark/document` : API filigrane - `https://faucet.certificator.4nkweb.com/api/faucet/request` : API faucet - `https://mempool.4nkweb.com/fr/tx/{txid}` : Explorateur de transactions +Le Dashboard n'expose pas de route `/health`. Pour vérifier qu'il répond, utiliser par exemple `GET /api/blockchain/info`. + ## Styles et Thème **Fichier CSS :** `signet-dashboard/public/styles.css` @@ -289,8 +292,8 @@ sudo systemctl status signet-dashboard # Vérifier les logs sudo journalctl -u signet-dashboard -f -# Tester l'accès -curl http://localhost:3020/health +# Tester l'accès (le Dashboard n'expose pas /health) +curl -s http://localhost:3020/api/blockchain/info | head -c 200 ``` ### Redémarrer le dashboard diff --git a/docs/DOMAINS_AND_PORTS.md b/docs/DOMAINS_AND_PORTS.md index 8ec7e7b..f22b7a8 100644 --- a/docs/DOMAINS_AND_PORTS.md +++ b/docs/DOMAINS_AND_PORTS.md @@ -135,14 +135,14 @@ sudo ss -tlnp | grep -E ':(3010|3020|3021|3022|3023|3024)' ```bash # Tester depuis le serveur curl http://localhost:3010/health -curl http://localhost:3020/health +curl -s http://localhost:3020/api/blockchain/info | head -c 200 # Dashboard : pas de /health curl http://localhost:3021/health curl http://localhost:3022/health curl http://localhost:3023/health # Tester depuis l'extérieur (via domaine) curl https://certificator.4nkweb.com/health -curl https://dashboard.certificator.4nkweb.com/health +curl -s https://dashboard.certificator.4nkweb.com/api/blockchain/info | head -c 200 ``` ## Certificats SSL/TLS diff --git a/features/dashboard-api-docs-update.md b/features/dashboard-api-docs-update.md new file mode 100644 index 0000000..31ac4b6 --- /dev/null +++ b/features/dashboard-api-docs-update.md @@ -0,0 +1,62 @@ +# Mise à jour documentation API Dashboard + +**Auteur :** Équipe 4NK +**Date :** 2026-01-28 +**Version :** 1.0 + +## Objectif + +Aligner la documentation API du Dashboard avec les évolutions récentes : pagination serveur, base SQLite, compteurs UTXO, frais, mining, transactions, hash, ancrage. Inclure le paramètre `skipIfExists` de l’API d’ancrage. + +## Impacts + +- **api-docs.html** : Section « API Dashboard », nouveaux endpoints, paramètres et réponses corrigés. +- **DASHBOARD.md** : Liste des endpoints internes/externes à jour, vérification du service sans `/health`, description des pages Hash/UTXO. +- **DOMAINS_AND_PORTS.md** : Tests de connectivité Dashboard utilisent `/api/blockchain/info` au lieu de `/health`. + +## Modifications + +### `signet-dashboard/public/api-docs.html` + +- Nouvelle section **API Dashboard** (intro, base URL, pas d’auth). +- **GET /api/utxo/count** : documenté. +- **GET /api/utxo/list** : pagination (`page`, `limit`), `category` (all | bloc_rewards | ancrages | changes | fees), réponses `category=all` vs données paginées, `pagination`, `counts`. +- **GET /api/utxo/fees**, **POST /api/utxo/fees/update** : documentés. +- **GET /api/hash/list** : pagination, format réponse. +- **POST /api/hash/generate** : `text` | `fileContent`, `isBase64`. +- **GET /api/mining/difficulty**, **GET /api/mining/avg-block-time** : documentés. +- **GET /api/transactions/avg-fee**, **GET /api/transactions/avg-amount** : documentés. +- **GET /api/anchor/example** : documenté. +- **Base URLs** : Dashboard ajouté, précision que `/health` n’est pas sur le Dashboard. +- **POST /api/anchor/document** : paramètre `skipIfExists` (boolean, défaut false), réponses avec `old: true/false`, exemple « hash déjà ancré », encadré « skipIfExists ». + +### `docs/DASHBOARD.md` + +- **API Endpoints Utilisés** : liste complète des endpoints internes (blockchain, wallet, network, utxo, hash, mining, transactions, anchor). +- Précision : Dashboard n’expose pas `/health` ; utiliser `GET /api/blockchain/info` pour vérifier. +- **Maintenance** : `curl` de test remplacé par `GET /api/blockchain/info`. +- **Page Documentation API** : mention des endpoints Dashboard documentés. +- **Page Liste des Hash** : source `GET /api/hash/list`, pagination. +- **Page Liste des UTXO** : pagination par catégorie, bouton « Récupérer les frais » + `POST /api/utxo/fees/update`. +- Version 1.1, date 2026-01-28. + +### `docs/DOMAINS_AND_PORTS.md` + +- Tests de connectivité Dashboard : `curl` vers `/api/blockchain/info` au lieu de `/health`, avec note. + +## Modalités de déploiement + +Aucun déploiement applicatif. Les fichiers modifiés sont statiques (HTML) ou documentation (Markdown). Redéploiement du front Dashboard si nécessaire pour servir `api-docs.html` à jour. + +## Modalités d’analyse + +- Vérifier que `api-docs.html` décrit correctement les endpoints et paramètres en les comparant à `signet-dashboard/src/server.js`. +- Vérifier que les exemples curl et réponses JSON sont cohérents avec l’implémentation. +- Confirmer que les tests `curl` du Dashboard (DASHBOARD.md, DOMAINS_AND_PORTS.md) fonctionnent. + +## Pages affectées + +- `signet-dashboard/public/api-docs.html` +- `docs/DASHBOARD.md` +- `docs/DOMAINS_AND_PORTS.md` +- `features/dashboard-api-docs-update.md` (ce fichier) diff --git a/fixKnowledge/api-anchorage-null-includes-error.md b/fixKnowledge/api-anchorage-null-includes-error.md new file mode 100644 index 0000000..a795efd --- /dev/null +++ b/fixKnowledge/api-anchorage-null-includes-error.md @@ -0,0 +1,124 @@ +# Correction: Erreur "Cannot read properties of null (reading 'includes')" dans l'API d'ancrage + +**Auteur** : Équipe 4NK +**Date** : 2026-01-28 +**Fichier concerné** : `api-anchorage/src/bitcoin-rpc.js` + +## Problème + +L'API d'ancrage retournait une erreur `500 Internal Server Error` avec le message : +``` +Cannot read properties of null (reading 'includes') +``` + +### Symptômes + +- Erreur lors de l'appel à l'endpoint `/api/anchor/document` +- L'API Key est correctement validée (visible dans les logs) +- L'erreur se produit lors de l'analyse des outputs de la transaction créée + +### Impact + +- Les requêtes d'ancrage échouent avec une erreur 500 +- Aucune transaction n'est créée +- Les utilisateurs ne peuvent pas ancrer de documents + +## Root Cause + +Dans la fonction `createAnchorTransaction()` du fichier `bitcoin-rpc.js`, à la ligne 710, le code utilisait la variable `provisioningAddresses` (paramètre de la fonction qui peut être `null`) au lieu de `finalProvisioningAddresses` (variable locale définie à la ligne 257 qui contient toujours un tableau valide). + +**Code problématique (ligne 710) :** +```javascript +else if (provisioningAddresses.includes(outputAddress) && Math.abs(outputAmount - provisioningAmountRounded) < 0.00000001) { +``` + +Quand `provisioningAddresses` est `null` (valeur par défaut du paramètre), l'appel à `.includes()` lève l'erreur "Cannot read properties of null (reading 'includes')". + +## Correctifs + +### Modification du code + +**Fichier** : `api-anchorage/src/bitcoin-rpc.js` + +**Ligne 710** : Remplacer l'utilisation de `provisioningAddresses` par `finalProvisioningAddresses` avec une vérification de sécurité. + +**Avant :** +```javascript +else if (provisioningAddresses.includes(outputAddress) && Math.abs(outputAmount - provisioningAmountRounded) < 0.00000001) { +``` + +**Après :** +```javascript +else if (finalProvisioningAddresses && finalProvisioningAddresses.includes(outputAddress) && Math.abs(outputAmount - provisioningAmountRounded) < 0.00000001) { +``` + +### Explication + +- `finalProvisioningAddresses` est défini à la ligne 257 avec la logique : `provisioningAddresses || allAddresses.slice(1, 1 + provisioningCount)` +- Cette variable contient toujours un tableau valide (même vide) et ne peut pas être `null` +- La vérification `finalProvisioningAddresses &&` ajoute une sécurité supplémentaire pour éviter toute erreur future + +## Modifications + +### Fichiers Modifiés + +- `api-anchorage/src/bitcoin-rpc.js` : Correction de la ligne 710 + +## Modalités de Déploiement + +1. **Redémarrer le service systemd** : + ```bash + sudo systemctl restart anchorage-api + ``` + +2. **Vérifier que le service est actif** : + ```bash + sudo systemctl status anchorage-api + ``` + +3. **Tester l'endpoint** : + ```bash + curl -X POST http://localhost:3010/api/anchor/document \ + -H "Content-Type: application/json" \ + -H "x-api-key: " \ + -d '{ + "hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890" + }' + ``` + +## Modalités d'Analyse + +### Vérification du problème + +1. **Consulter les logs** : + ```bash + sudo journalctl -u anchorage-api -n 100 --no-pager + ``` + +2. **Rechercher l'erreur** : + - Chercher "Cannot read properties of null (reading 'includes')" + - Vérifier la stack trace pour identifier la ligne exacte + +3. **Vérifier les paramètres** : + - Vérifier que `provisioningAddresses` peut être `null` dans les appels + - Vérifier que `finalProvisioningAddresses` est bien défini avant utilisation + +### Tests de validation + +1. **Test avec provisioningAddresses null** (cas par défaut) : + - L'appel sans paramètre `provisioningAddresses` doit fonctionner + - `finalProvisioningAddresses` doit être un tableau généré automatiquement + +2. **Test avec provisioningAddresses fourni** : + - L'appel avec un tableau d'adresses doit fonctionner + - Les outputs de provisioning doivent être correctement identifiés + +3. **Test avec provisioningCount = 0** : + - Si aucun provisioning n'est nécessaire, `finalProvisioningAddresses` doit être un tableau vide + - Aucune erreur ne doit être levée + +## Notes + +- Cette erreur se produisait uniquement lors de l'analyse des outputs de la transaction créée +- La transaction était créée avec succès, mais l'analyse des outputs échouait +- La correction garantit que `finalProvisioningAddresses` est toujours un tableau valide avant l'appel à `.includes()` diff --git a/fixKnowledge/api-anchorage-rbf-replacement-error.md b/fixKnowledge/api-anchorage-rbf-replacement-error.md new file mode 100644 index 0000000..3efac37 --- /dev/null +++ b/fixKnowledge/api-anchorage-rbf-replacement-error.md @@ -0,0 +1,182 @@ +# Correction: Erreur "insufficient fee, rejecting replacement" dans l'API d'ancrage + +**Auteur** : Équipe 4NK +**Date** : 2026-01-28 +**Fichier concerné** : `api-anchorage/src/bitcoin-rpc.js` + +## Problème + +L'API d'ancrage retournait une erreur `500 Internal Server Error` avec le message : +``` +insufficient fee, rejecting replacement e14ed98d2285fd3a5342d0bb2b0c43bea633042ebf264531252c0890618495ab; new feerate 0.00002743 BTC/kvB <= old feerate 0.00002743 BTC/kvB +``` + +### Symptômes + +- Erreur lors de l'envoi d'une transaction au mempool +- L'erreur se produit quand une transaction avec les mêmes inputs existe déjà dans le mempool +- Bitcoin Core rejette la nouvelle transaction car les frais ne sont pas plus élevés que la transaction existante (RBF - Replace By Fee) + +### Impact + +- Les requêtes d'ancrage échouent avec une erreur 500 +- Aucune transaction n'est créée même si une transaction similaire existe déjà dans le mempool +- Les utilisateurs ne peuvent pas ancrer de documents dans ce cas + +## Root Cause + +Bitcoin Core implémente la politique RBF (Replace By Fee) qui permet de remplacer une transaction non confirmée dans le mempool par une nouvelle transaction avec les mêmes inputs, mais uniquement si les frais de la nouvelle transaction sont **strictement supérieurs** à ceux de l'ancienne. + +Quand l'API essaie d'envoyer une transaction et qu'une transaction avec les mêmes inputs existe déjà dans le mempool avec des frais identiques ou supérieurs, Bitcoin Core rejette la nouvelle transaction avec l'erreur "insufficient fee, rejecting replacement". + +Le code ne gérait pas cette erreur spécifique et la propageait directement, causant un échec de l'opération d'ancrage. + +## Correctifs + +### Modification du code + +**Fichier** : `api-anchorage/src/bitcoin-rpc.js` + +**Lignes 645-697** : Ajout de la gestion de l'erreur de remplacement RBF lors de l'envoi de la transaction. + +**Avant :** +```javascript +// Envoyer la transaction au mempool +const txid = await this.client.command('sendrawtransaction', signedTx.hex, 0); +``` + +**Après :** +```javascript +// Envoyer la transaction au mempool +let txid; +try { + txid = await this.client.command('sendrawtransaction', signedTx.hex, 0); +} catch (sendError) { + // Gérer l'erreur de remplacement RBF (Replace By Fee) + const errorMessage = sendError.message || sendError.toString(); + if (errorMessage.includes('insufficient fee') && errorMessage.includes('rejecting replacement')) { + // Extraire le txid de la transaction existante depuis le message d'erreur + const replacementMatch = errorMessage.match(/rejecting replacement ([a-fA-F0-9]{64})/); + if (replacementMatch && replacementMatch[1]) { + const existingTxid = replacementMatch[1]; + + // Vérifier si la transaction existe dans le mempool ou dans la blockchain + try { + const mempoolEntry = await this.client.command('getmempoolentry', existingTxid); + if (mempoolEntry) { + // La transaction existe dans le mempool, utiliser cette transaction + txid = existingTxid; + } else { + throw new Error('Transaction not in mempool'); + } + } catch (mempoolError) { + // Si la transaction n'est pas dans le mempool, vérifier si elle est confirmée + if (mempoolError.message.includes('not in mempool')) { + try { + const txInfo = await this.client.getTransaction(existingTxid); + if (txInfo && txInfo.txid) { + // La transaction existe dans la blockchain (confirmée), utiliser cette transaction + txid = existingTxid; + } else { + throw sendError; + } + } catch (txError) { + throw sendError; + } + } else { + throw sendError; + } + } + } else { + throw sendError; + } + } else { + throw sendError; + } +} +``` + +### Explication + +1. **Détection de l'erreur RBF** : Le code détecte l'erreur "insufficient fee, rejecting replacement" dans le message d'erreur +2. **Extraction du txid** : Le txid de la transaction existante est extrait depuis le message d'erreur avec une expression régulière +3. **Vérification dans le mempool** : Le code vérifie d'abord si la transaction existe dans le mempool avec `getmempoolentry` +4. **Vérification dans la blockchain** : Si la transaction n'est pas dans le mempool, le code vérifie si elle est confirmée dans la blockchain avec `getTransaction` +5. **Utilisation de la transaction existante** : Si la transaction existe (dans le mempool ou confirmée), son txid est utilisé au lieu de créer une nouvelle transaction +6. **Propagation de l'erreur** : Si la transaction n'existe ni dans le mempool ni dans la blockchain, ou si l'erreur est d'un autre type, l'erreur originale est relancée + +### Comportement + +- Si une transaction avec les mêmes inputs existe déjà dans le mempool, l'API utilise cette transaction existante au lieu d'échouer +- Si la transaction n'est plus dans le mempool mais a été confirmée, l'API utilise la transaction confirmée +- Les informations de la transaction existante sont récupérées et retournées normalement +- L'utilisateur reçoit une réponse réussie avec les informations de la transaction existante (mempool ou confirmée) +- Les UTXOs sont toujours marqués comme dépensés (car ils sont utilisés dans la transaction existante) + +## Modifications + +### Fichiers Modifiés + +- `api-anchorage/src/bitcoin-rpc.js` : Ajout de la gestion de l'erreur RBF lors de l'envoi de transaction (lignes 645-697) + +## Modalités de Déploiement + +1. **Redémarrer le service systemd** : + ```bash + sudo systemctl restart anchorage-api + ``` + +2. **Vérifier que le service est actif** : + ```bash + sudo systemctl status anchorage-api + ``` + +3. **Tester l'endpoint** : + ```bash + curl -X POST http://localhost:3010/api/anchor/document \ + -H "Content-Type: application/json" \ + -H "x-api-key: " \ + -d '{ + "hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890" + }' + ``` + +## Modalités d'Analyse + +### Vérification du problème + +1. **Consulter les logs** : + ```bash + sudo journalctl -u anchorage-api -n 100 --no-pager + ``` + +2. **Rechercher l'erreur** : + - Chercher "insufficient fee, rejecting replacement" + - Vérifier si le code détecte et gère correctement l'erreur + - Vérifier les logs "Transaction replacement rejected, using existing transaction" + +3. **Vérifier le mempool** : + ```bash + bitcoin-cli getrawmempool + ``` + +### Tests de validation + +1. **Test avec transaction existante** : + - Créer une transaction d'ancrage + - Essayer de créer une autre transaction avec les mêmes inputs (même hash) + - Vérifier que l'API utilise la transaction existante au lieu d'échouer + +2. **Test avec transaction confirmée** : + - Si la transaction existante a été confirmée, l'erreur doit être relancée normalement + - Vérifier que l'API gère correctement ce cas + +3. **Test avec erreur différente** : + - Vérifier que les autres erreurs (solde insuffisant, etc.) sont toujours propagées correctement + +## Notes + +- Cette correction permet à l'API de gérer gracieusement les cas où une transaction similaire existe déjà dans le mempool +- L'utilisateur reçoit toujours une réponse réussie avec les informations de la transaction (existante ou nouvelle) +- Les frais de la transaction existante sont utilisés, ce qui est acceptable car la transaction est déjà dans le mempool +- Cette approche évite d'avoir à augmenter les frais pour remplacer la transaction, ce qui serait plus complexe et coûteux diff --git a/signet-dashboard/public/api-docs.html b/signet-dashboard/public/api-docs.html index 5180eda..fb8c048 100644 --- a/signet-dashboard/public/api-docs.html +++ b/signet-dashboard/public/api-docs.html @@ -340,6 +340,12 @@ Non Identifiant optionnel du document (pour le logging) + + skipIfExists + boolean + Non + Si true, ne réancrera pas un hash déjà ancré ; retourne les infos existantes en base avec old: true. Défaut false. + @@ -352,28 +358,49 @@ -H "X-API-Key: votre-clé-api" \ -d '{ "hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456", - "documentUid": "doc-12345" + "documentUid": "doc-12345", + "skipIfExists": true }'
-

Réponse (200 OK)

+

Réponse (200 OK) – nouvelle transaction

{
+  "ok": true,
   "txid": "abc123def456...",
   "status": "pending",
   "confirmations": 0,
-  "block_height": null
+  "block_height": null,
+  "outputs": [...],
+  "fee": 0.000012,
+  "fee_sats": 1200,
+  "old": false
 }
+
+

Réponse (200 OK) – hash déjà ancré (skipIfExists: true)

+
+
{
+  "ok": true,
+  "txid": "abc123def456...",
+  "status": "confirmed",
+  "confirmations": 42,
+  "block_height": 12345,
+  "old": true
+}
+
+

Aucune transaction n'est créée ; les données viennent de la base. old: true indique un ancrage préexistant.

+
+

Codes de statut possibles

    -
  • 200 Succès - Transaction créée et envoyée au mempool
  • +
  • 200 Succès – transaction créée et envoyée au mempool (old: false), ou hash déjà ancré avec skipIfExists: true (old: true)
  • 400 Requête invalide - Hash manquant ou format incorrect
  • 401 Non autorisé - Clé API manquante ou invalide
  • 402 Solde insuffisant - Pas assez de fonds pour créer la transaction
  • @@ -404,6 +431,14 @@

    Solution : L'API utilise maintenant uniquement des UTXOs confirmés (au moins 1 confirmation) pour éviter cette erreur. Attendez qu'un bloc soit miné pour que les UTXOs soient confirmés.

+
+

ℹ️ skipIfExists

+
    +
  • Avec skipIfExists: true, l'API consulte la base ; si le hash existe déjà, elle retourne old: true et les infos (txid, block_height, confirmations, etc.) sans créer de transaction.
  • +
  • Utile pour éviter les réancrages en double (retries, idempotence). Par défaut false : comportement inchangé.
  • +
+
+

ℹ️ Gestion des UTXOs

    @@ -997,6 +1032,54 @@
+ +
+

📊 API Dashboard (signet-dashboard)

+

Les endpoints ci-dessous sont servis par le Dashboard (https://dashboard.certificator.4nkweb.com, port 3020). Données issues de la base SQLite et du RPC Bitcoin. Pas d’authentification requise.

+
+ + +
+
+
+ GET + /api/utxo/count +
+ +
+

Retourne uniquement les compteurs UTXO (ancrages, disponibles pour ancrage, confirmés). Optimisé pour éviter de charger la liste complète.

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Exemple de requête

+
+
curl -X GET https://dashboard.certificator.4nkweb.com/api/utxo/count
+
+ +
+ +
+

Réponse (200 OK)

+
+
{
+  "availableForAnchor": 180,
+  "confirmedAvailableForAnchor": 175,
+  "anchors": 150
+}
+
+
+ +
+

ℹ️ Notes

+
    +
  • anchors : UTXOs catégorie ancrages (≥ 2000 sats, confirmés, non dépensés, non verrouillés)
  • +
  • confirmedAvailableForAnchor : comme availableForAnchor mais avec ≥ 6 confirmations
  • +
+
+
+
+
@@ -1006,26 +1089,60 @@
-

Obtient la liste complète des UTXOs du wallet, catégorisés par type (bloc rewards, ancrages, changes, frais).

+

Liste des UTXOs du wallet par catégorie, avec pagination côté serveur. Données en base SQLite.

Base URL : https://dashboard.certificator.4nkweb.com

+
+

Paramètres (query)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamètreTypeRequisDescription
categorystringNonall (défaut) | bloc_rewards | ancrages | changes | fees. anchor / change acceptés.
pagenumberNonNuméro de page (défaut 1). Ignoré si category=all.
limitnumberNonÉléments par page (défaut 50, max 1000). Ignoré si category=all.
+
+
-

Exemple de requête

+

Exemple (counts seulement, category=all)

-
curl -X GET https://dashboard.certificator.4nkweb.com/api/utxo/list
+
curl -X GET "https://dashboard.certificator.4nkweb.com/api/utxo/list"
-

Réponse (200 OK)

+

Réponse (200 OK) – category=all

{
-  "blocRewards": [...],
-  "anchors": [...],
-  "changes": [...],
-  "fees": [...],
+  "blocRewards": [],
+  "anchors": [],
+  "changes": [],
+  "fees": [],
   "counts": {
     "blocRewards": 10,
     "anchors": 150,
@@ -1034,18 +1151,49 @@
     "total": 190,
     "availableForAnchor": 180,
     "confirmedAvailableForAnchor": 175
+  },
+  "page": 1,
+  "limit": 0,
+  "totalPages": 0,
+  "message": "Use ?category=bloc_rewards|ancrages|changes|fees&page=X&limit=Y to get paginated data"
+}
+
+
+ +
+

Exemple (données paginées)

+
+
curl -X GET "https://dashboard.certificator.4nkweb.com/api/utxo/list?category=ancrages&page=1&limit=50"
+
+ +
+ +
+

Réponse (200 OK) – category=ancrages (ex.)

+
+
{
+  "blocRewards": [],
+  "anchors": [{"txid":"...","vout":0,"address":"...","amount":0.000025,"confirmations":12,"category":"ancrages","isSpentOnchain":false,"isLockedInMutex":false,"blockHeight":null,"blockTime":1234567890,"isAnchorChange":false}],
+  "changes": [],
+  "fees": [],
+  "counts": {...},
+  "pagination": {
+    "category": "ancrages",
+    "page": 1,
+    "limit": 50,
+    "total": 150,
+    "totalPages": 3
   }
 }
-

ℹ️ Notes importantes

+

ℹ️ Notes

    -
  • Seuls les UTXOs avec au moins 1 confirmation sont retournés (pour éviter les erreurs "too-long-mempool-chain")
  • -
  • availableForAnchor : Nombre d'UTXOs disponibles pour l'ancrage (> 2000 sats, non dépensés, non verrouillés)
  • -
  • confirmedAvailableForAnchor : Nombre d'UTXOs confirmés disponibles pour l'ancrage
  • -
  • Les UTXOs sont triés par montant décroissant par défaut
  • +
  • category=fees : objets txid, fee, fee_sats, blockHeight, blockTime, confirmations, changeAddress, changeAmount.
  • +
  • availableForAnchor / confirmedAvailableForAnchor : mêmes définitions que /api/utxo/count.
  • +
  • Tri : bloc_rewards/ancrages par montant décroissant ; changes par is_anchor_change puis montant ; fees par block_height décroissant.
@@ -1161,6 +1309,350 @@
+ +
+
+
+ GET + /api/utxo/fees +
+ +
+

Liste des frais issus des transactions d'ancrage (métadonnées OP_RETURN). Données fournies par le RPC / cache.

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Réponse (200 OK)

+
+
{
+  "fees": [
+    {"txid":"...","fee":0.000012,"fee_sats":1200,"blockHeight":12345,"blockTime":1234567890,"confirmations":10,"changeAddress":"...","changeAmount":0.00002}
+  ],
+  "count": 1
+}
+
+
+
+
+ + +
+
+
+ POST + /api/utxo/fees/update +
+ +
+

Récupère les frais depuis les transactions d'ancrage (OP_RETURN) et les enregistre. Optionnel : limite au bloc donné.

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Paramètres (Body JSON)

+ + + + + + + + + + + + + + + + + +
ParamètreTypeRequisDescription
sinceBlockHeightnumberNonNe traiter que les ancrages depuis ce bloc (inclus). Si absent, tous.
+
+ +
+

Réponse (200 OK)

+
+
{
+  "success": true,
+  "newFees": 5,
+  "totalFees": 120,
+  "processed": 50
+}
+
+
+ +
+

Réponse (500)

+
+
{
+  "success": false,
+  "error": "Error message"
+}
+
+
+
+
+ + +
+
+
+ GET + /api/hash/list +
+ +
+

Liste des hash ancrés, paginée (base SQLite).

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Paramètres (query)

+ + + + + + + + + + + + + + + + + + + + + + + +
ParamètreTypeRequisDescription
pagenumberNonNuméro de page (défaut 1).
limitnumberNonÉléments par page (défaut 50, max 1000).
+
+ +
+

Exemple

+
+
curl -X GET "https://dashboard.certificator.4nkweb.com/api/hash/list?page=1&limit=50"
+
+ +
+ +
+

Réponse (200 OK)

+
+
{
+  "hashes": [
+    {"hash":"a1b2...","txid":"...","blockHeight":12345,"confirmations":10,"date":"2026-01-28T12:00:00.000Z"}
+  ],
+  "count": 50,
+  "total": 32000,
+  "page": 1,
+  "limit": 50,
+  "totalPages": 640
+}
+
+
+
+
+ + +
+
+
+ POST + /api/hash/generate +
+ +
+

Calcule le hash SHA256 d'un texte ou d'un fichier (base64). Un seul des deux doit être fourni.

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Paramètres (Body JSON)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamètreTypeRequisDescription
textstringConditionnelTexte UTF-8 à hasher. Exclure si fileContent fourni.
fileContentstringConditionnelContenu fichier (base64 ou UTF-8). Exclure si text fourni.
isBase64booleanNonSi true, fileContent est décodé en base64 avant hash. Sinon traité en UTF-8.
+
+ +
+

Réponse (200 OK)

+
+
{
+  "hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
+}
+
+
+ +
+

Codes de statut

+
    +
  • 200 Succès
  • +
  • 400 text et fileContent absents ou tous deux fournis ; format invalide.
  • +
  • 500 Erreur serveur
  • +
+
+
+
+ + +
+
+
+ GET + /api/mining/difficulty +
+ +
+

Difficulté de minage courante (RPC getblockchaininfo).

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Réponse (200 OK)

+
+
{
+  "difficulty": 0.0002441371325370144
+}
+
+
+
+
+ + +
+
+
+ GET + /api/mining/avg-block-time +
+ +
+

Temps moyen entre blocs (source : Mempool /api/v1/difficulty-adjustment).

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Réponse (200 OK)

+
+
{
+  "timeAvg": 600000,
+  "timeAvgSeconds": 600,
+  "formatted": "10m"
+}
+
+
+
+
+ + +
+
+
+ GET + /api/transactions/avg-fee · /api/transactions/avg-amount +
+ +
+

Frais moyen (sats) et montant moyen (sats) pour les ancrages. Valeurs fixes (1200 sats / 1000 sats) pour ce contexte.

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Réponse avg-fee (200 OK)

+
+
{
+  "avgFee": 1200,
+  "avgFeeRate": 0,
+  "txCount": 0
+}
+
+
+ +
+

Réponse avg-amount (200 OK)

+
+
{
+  "avgAmount": 1000,
+  "txCount": 0
+}
+
+
+
+
+ + +
+
+
+ GET + /api/anchor/example +
+ +
+

Exemple de transaction d'ancrage (la plus récente de la liste des hash) : txid, bloc, confirmations, entrées/sorties. Pour la page Learn.

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Réponse (200 OK)

+
+
{
+  "txid": "...",
+  "blockHeight": 12345,
+  "confirmations": 10,
+  "hash": "a1b2...",
+  "inputs": [{"txid":"...","vout":0,"value":0.001}],
+  "outputs": [{"address":"...","value":0.000025,"type":"nulldata","isOpReturn":true}]
+}
+
+
+ +
+

Réponse (404)

+
+
{
+  "error": "No anchor transactions found"
+}
+
+
+
+
+
@@ -1191,21 +1683,24 @@ Format: hexadécimal (0-9, a-f, A-F)

Base URLs

Les APIs sont accessibles aux adresses suivantes :

-
API d'Ancrage : https://certificator.4nkweb.com
-API Filigrane : https://watermark.certificator.4nkweb.com
-API ClamAV : https://antivir.certificator.4nkweb.com
+
Dashboard : https://dashboard.certificator.4nkweb.com  (port 3020)
+API d'Ancrage : https://certificator.4nkweb.com       (port 3010)
+API Faucet : https://faucet.certificator.4nkweb.com  (port 3021)
+API Filigrane : https://watermark.certificator.4nkweb.com (port 3022)
+API ClamAV : https://antivir.certificator.4nkweb.com (port 3023)
+

/health est exposé par l’API d’Ancrage et l’API Faucet, pas par le Dashboard.

Ports fixes

Tous les ports sont fixes et ne peuvent pas être modifiés :

    +
  • Dashboard : Port 3020
  • API d'Ancrage : Port 3010
  • API Faucet : Port 3021
  • API Filigrane : Port 3022
  • API ClamAV : Port 3023
  • -
  • Dashboard : Port 3020