Docs API dashboard et skipIfExists

**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
This commit is contained in:
ncantu 2026-01-28 12:18:33 +01:00
parent 31fd2c3c8b
commit 4d3028da0c
9 changed files with 2031 additions and 103 deletions

View File

@ -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 <txid>; 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;
}

View File

@ -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<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;
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<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) {
// 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<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, 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:<hash>|CHANGE:<address>:<amount>|FEE:<amount>"
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:<address>:<amount_sats>|FEE:<amount_sats>"
// 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<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();

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 lAPI dancrage.
## 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 dauth).
- **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` nest 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 nexpose 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 danalyse
- 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 limplé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)

View File

@ -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: <your-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()`

View File

@ -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: <your-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

View File

@ -340,6 +340,12 @@
<td><span class="param-optional">Non</span></td>
<td>Identifiant optionnel du document (pour le logging)</td>
</tr>
<tr>
<td class="param-name">skipIfExists</td>
<td>boolean</td>
<td><span class="param-optional">Non</span></td>
<td>Si <code>true</code>, ne réancrera pas un hash déjà ancré ; retourne les infos existantes en base avec <code>old: true</code>. Défaut <code>false</code>.</td>
</tr>
</tbody>
</table>
</div>
@ -352,28 +358,49 @@
-H "X-API-Key: votre-clé-api" \
-d '{
"hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
"documentUid": "doc-12345"
"documentUid": "doc-12345",
"skipIfExists": true
}'</pre>
</div>
<button class="copy-button" onclick="copyCode(this)">📋 Copier</button>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<h4>Réponse (200 OK) nouvelle transaction</h4>
<div class="code-block">
<pre>{
"ok": true,
"txid": "abc123def456...",
"status": "pending",
"confirmations": 0,
"block_height": null
"block_height": null,
"outputs": [...],
"fee": 0.000012,
"fee_sats": 1200,
"old": false
}</pre>
</div>
</div>
<div class="response-example">
<h4>Réponse (200 OK) hash déjà ancré (<code>skipIfExists: true</code>)</h4>
<div class="code-block">
<pre>{
"ok": true,
"txid": "abc123def456...",
"status": "confirmed",
"confirmations": 42,
"block_height": 12345,
"old": true
}</pre>
</div>
<p>Aucune transaction n'est créée ; les données viennent de la base. <code>old: true</code> indique un ancrage préexistant.</p>
</div>
<div class="response-example">
<h4>Codes de statut possibles</h4>
<ul style="margin-left: 20px;">
<li><span class="status-code status-200">200</span> Succès - Transaction créée et envoyée au mempool</li>
<li><span class="status-code status-200">200</span> Succès transaction créée et envoyée au mempool (<code>old: false</code>), ou hash déjà ancré avec <code>skipIfExists: true</code> (<code>old: true</code>)</li>
<li><span class="status-code status-400">400</span> Requête invalide - Hash manquant ou format incorrect</li>
<li><span class="status-code status-401">401</span> Non autorisé - Clé API manquante ou invalide</li>
<li><span class="status-code status-402">402</span> Solde insuffisant - Pas assez de fonds pour créer la transaction</li>
@ -404,6 +431,14 @@
<p><strong>Solution :</strong> 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.</p>
</div>
<div class="info-box">
<h4> skipIfExists</h4>
<ul style="margin-left: 20px; margin-top: 10px;">
<li>Avec <code>skipIfExists: true</code>, l'API consulte la base ; si le hash existe déjà, elle retourne <code>old: true</code> et les infos (txid, block_height, confirmations, etc.) sans créer de transaction.</li>
<li>Utile pour éviter les réancrages en double (retries, idempotence). Par défaut <code>false</code> : comportement inchangé.</li>
</ul>
</div>
<div class="info-box">
<h4> Gestion des UTXOs</h4>
<ul style="margin-left: 20px; margin-top: 10px;">
@ -997,6 +1032,54 @@
</div>
</section>
<!-- Section API Dashboard -->
<section class="api-docs-section">
<h2>📊 API Dashboard (signet-dashboard)</h2>
<p>Les endpoints ci-dessous sont servis par le Dashboard (<code>https://dashboard.certificator.4nkweb.com</code>, port 3020). Données issues de la base SQLite et du RPC Bitcoin. Pas dauthentification requise.</p>
</section>
<!-- Endpoint: UTXO Count -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/api/utxo/count</span>
</div>
<div class="endpoint-description">
<p>Retourne uniquement les compteurs UTXO (ancrages, disponibles pour ancrage, confirmés). Optimisé pour éviter de charger la liste complète.</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="response-example">
<h4>Exemple de requête</h4>
<div class="code-block">
<pre>curl -X GET https://dashboard.certificator.4nkweb.com/api/utxo/count</pre>
</div>
<button class="copy-button" onclick="copyCode(this)">📋 Copier</button>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<div class="code-block">
<pre>{
"availableForAnchor": 180,
"confirmedAvailableForAnchor": 175,
"anchors": 150
}</pre>
</div>
</div>
<div class="info-box">
<h4> Notes</h4>
<ul style="margin-left: 20px; margin-top: 10px;">
<li><code>anchors</code> : UTXOs catégorie ancrages (≥ 2000 sats, confirmés, non dépensés, non verrouillés)</li>
<li><code>confirmedAvailableForAnchor</code> : comme <code>availableForAnchor</code> mais avec ≥ 6 confirmations</li>
</ul>
</div>
</div>
</section>
<!-- Endpoint: UTXO List -->
<section class="api-docs-section">
<div class="endpoint-card">
@ -1006,26 +1089,60 @@
</div>
<div class="endpoint-description">
<p>Obtient la liste complète des UTXOs du wallet, catégorisés par type (bloc rewards, ancrages, changes, frais).</p>
<p>Liste des UTXOs du wallet par catégorie, avec pagination côté serveur. Données en base SQLite.</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="endpoint-params">
<h4>Paramètres (query)</h4>
<table class="param-table">
<thead>
<tr>
<th>Paramètre</th>
<th>Type</th>
<th>Requis</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="param-name">category</td>
<td>string</td>
<td><span class="param-optional">Non</span></td>
<td><code>all</code> (défaut) | <code>bloc_rewards</code> | <code>ancrages</code> | <code>changes</code> | <code>fees</code>. <code>anchor</code> / <code>change</code> acceptés.</td>
</tr>
<tr>
<td class="param-name">page</td>
<td>number</td>
<td><span class="param-optional">Non</span></td>
<td>Numéro de page (défaut 1). Ignoré si <code>category=all</code>.</td>
</tr>
<tr>
<td class="param-name">limit</td>
<td>number</td>
<td><span class="param-optional">Non</span></td>
<td>Éléments par page (défaut 50, max 1000). Ignoré si <code>category=all</code>.</td>
</tr>
</tbody>
</table>
</div>
<div class="response-example">
<h4>Exemple de requête</h4>
<h4>Exemple (counts seulement, category=all)</h4>
<div class="code-block">
<pre>curl -X GET https://dashboard.certificator.4nkweb.com/api/utxo/list</pre>
<pre>curl -X GET "https://dashboard.certificator.4nkweb.com/api/utxo/list"</pre>
</div>
<button class="copy-button" onclick="copyCode(this)">📋 Copier</button>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<h4>Réponse (200 OK) category=all</h4>
<div class="code-block">
<pre>{
"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"
}</pre>
</div>
</div>
<div class="response-example">
<h4>Exemple (données paginées)</h4>
<div class="code-block">
<pre>curl -X GET "https://dashboard.certificator.4nkweb.com/api/utxo/list?category=ancrages&page=1&limit=50"</pre>
</div>
<button class="copy-button" onclick="copyCode(this)">📋 Copier</button>
</div>
<div class="response-example">
<h4>Réponse (200 OK) category=ancrages (ex.)</h4>
<div class="code-block">
<pre>{
"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
}
}</pre>
</div>
</div>
<div class="info-box">
<h4> Notes importantes</h4>
<h4> Notes</h4>
<ul style="margin-left: 20px; margin-top: 10px;">
<li>Seuls les UTXOs avec au moins 1 confirmation sont retournés (pour éviter les erreurs "too-long-mempool-chain")</li>
<li><code>availableForAnchor</code> : Nombre d'UTXOs disponibles pour l'ancrage (> 2000 sats, non dépensés, non verrouillés)</li>
<li><code>confirmedAvailableForAnchor</code> : Nombre d'UTXOs confirmés disponibles pour l'ancrage</li>
<li>Les UTXOs sont triés par montant décroissant par défaut</li>
<li><code>category=fees</code> : objets <code>txid</code>, <code>fee</code>, <code>fee_sats</code>, <code>blockHeight</code>, <code>blockTime</code>, <code>confirmations</code>, <code>changeAddress</code>, <code>changeAmount</code>.</li>
<li><code>availableForAnchor</code> / <code>confirmedAvailableForAnchor</code> : mêmes définitions que <code>/api/utxo/count</code>.</li>
<li>Tri : bloc_rewards/ancrages par montant décroissant ; changes par <code>is_anchor_change</code> puis montant ; fees par <code>block_height</code> décroissant.</li>
</ul>
</div>
</div>
@ -1161,6 +1309,350 @@
</div>
</section>
<!-- Endpoint: UTXO Fees -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/api/utxo/fees</span>
</div>
<div class="endpoint-description">
<p>Liste des frais issus des transactions d'ancrage (métadonnées OP_RETURN). Données fournies par le RPC / cache.</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<div class="code-block">
<pre>{
"fees": [
{"txid":"...","fee":0.000012,"fee_sats":1200,"blockHeight":12345,"blockTime":1234567890,"confirmations":10,"changeAddress":"...","changeAmount":0.00002}
],
"count": 1
}</pre>
</div>
</div>
</div>
</section>
<!-- Endpoint: UTXO Fees Update -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-post">POST</span>
<span class="endpoint-path">/api/utxo/fees/update</span>
</div>
<div class="endpoint-description">
<p>Récupère les frais depuis les transactions d'ancrage (OP_RETURN) et les enregistre. Optionnel : limite au bloc donné.</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="endpoint-params">
<h4>Paramètres (Body JSON)</h4>
<table class="param-table">
<thead>
<tr>
<th>Paramètre</th>
<th>Type</th>
<th>Requis</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="param-name">sinceBlockHeight</td>
<td>number</td>
<td><span class="param-optional">Non</span></td>
<td>Ne traiter que les ancrages depuis ce bloc (inclus). Si absent, tous.</td>
</tr>
</tbody>
</table>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<div class="code-block">
<pre>{
"success": true,
"newFees": 5,
"totalFees": 120,
"processed": 50
}</pre>
</div>
</div>
<div class="response-example">
<h4>Réponse (500)</h4>
<div class="code-block">
<pre>{
"success": false,
"error": "Error message"
}</pre>
</div>
</div>
</div>
</section>
<!-- Endpoint: Hash List -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/api/hash/list</span>
</div>
<div class="endpoint-description">
<p>Liste des hash ancrés, paginée (base SQLite).</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="endpoint-params">
<h4>Paramètres (query)</h4>
<table class="param-table">
<thead>
<tr>
<th>Paramètre</th>
<th>Type</th>
<th>Requis</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="param-name">page</td>
<td>number</td>
<td><span class="param-optional">Non</span></td>
<td>Numéro de page (défaut 1).</td>
</tr>
<tr>
<td class="param-name">limit</td>
<td>number</td>
<td><span class="param-optional">Non</span></td>
<td>Éléments par page (défaut 50, max 1000).</td>
</tr>
</tbody>
</table>
</div>
<div class="response-example">
<h4>Exemple</h4>
<div class="code-block">
<pre>curl -X GET "https://dashboard.certificator.4nkweb.com/api/hash/list?page=1&limit=50"</pre>
</div>
<button class="copy-button" onclick="copyCode(this)">📋 Copier</button>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<div class="code-block">
<pre>{
"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
}</pre>
</div>
</div>
</div>
</section>
<!-- Endpoint: Hash Generate -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-post">POST</span>
<span class="endpoint-path">/api/hash/generate</span>
</div>
<div class="endpoint-description">
<p>Calcule le hash SHA256 d'un texte ou d'un fichier (base64). Un seul des deux doit être fourni.</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="endpoint-params">
<h4>Paramètres (Body JSON)</h4>
<table class="param-table">
<thead>
<tr>
<th>Paramètre</th>
<th>Type</th>
<th>Requis</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="param-name">text</td>
<td>string</td>
<td><span class="param-optional">Conditionnel</span></td>
<td>Texte UTF-8 à hasher. Exclure si <code>fileContent</code> fourni.</td>
</tr>
<tr>
<td class="param-name">fileContent</td>
<td>string</td>
<td><span class="param-optional">Conditionnel</span></td>
<td>Contenu fichier (base64 ou UTF-8). Exclure si <code>text</code> fourni.</td>
</tr>
<tr>
<td class="param-name">isBase64</td>
<td>boolean</td>
<td><span class="param-optional">Non</span></td>
<td>Si <code>true</code>, <code>fileContent</code> est décodé en base64 avant hash. Sinon traité en UTF-8.</td>
</tr>
</tbody>
</table>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<div class="code-block">
<pre>{
"hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
}</pre>
</div>
</div>
<div class="response-example">
<h4>Codes de statut</h4>
<ul style="margin-left: 20px;">
<li><span class="status-code status-200">200</span> Succès</li>
<li><span class="status-code status-400">400</span> <code>text</code> et <code>fileContent</code> absents ou tous deux fournis ; format invalide.</li>
<li><span class="status-code status-500">500</span> Erreur serveur</li>
</ul>
</div>
</div>
</section>
<!-- Endpoint: Mining Difficulty -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/api/mining/difficulty</span>
</div>
<div class="endpoint-description">
<p>Difficulté de minage courante (RPC <code>getblockchaininfo</code>).</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<div class="code-block">
<pre>{
"difficulty": 0.0002441371325370144
}</pre>
</div>
</div>
</div>
</section>
<!-- Endpoint: Mining Avg Block Time -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/api/mining/avg-block-time</span>
</div>
<div class="endpoint-description">
<p>Temps moyen entre blocs (source : Mempool <code>/api/v1/difficulty-adjustment</code>).</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<div class="code-block">
<pre>{
"timeAvg": 600000,
"timeAvgSeconds": 600,
"formatted": "10m"
}</pre>
</div>
</div>
</div>
</section>
<!-- Endpoint: Transactions Avg Fee / Avg Amount -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/api/transactions/avg-fee</span> · <span class="endpoint-path">/api/transactions/avg-amount</span>
</div>
<div class="endpoint-description">
<p>Frais moyen (sats) et montant moyen (sats) pour les ancrages. Valeurs fixes (1200 sats / 1000 sats) pour ce contexte.</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="response-example">
<h4>Réponse avg-fee (200 OK)</h4>
<div class="code-block">
<pre>{
"avgFee": 1200,
"avgFeeRate": 0,
"txCount": 0
}</pre>
</div>
</div>
<div class="response-example">
<h4>Réponse avg-amount (200 OK)</h4>
<div class="code-block">
<pre>{
"avgAmount": 1000,
"txCount": 0
}</pre>
</div>
</div>
</div>
</section>
<!-- Endpoint: Anchor Example -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/api/anchor/example</span>
</div>
<div class="endpoint-description">
<p>Exemple de transaction d'ancrage (la plus récente de la liste des hash) : txid, bloc, confirmations, entrées/sorties. Pour la page Learn.</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<div class="code-block">
<pre>{
"txid": "...",
"blockHeight": 12345,
"confirmations": 10,
"hash": "a1b2...",
"inputs": [{"txid":"...","vout":0,"value":0.001}],
"outputs": [{"address":"...","value":0.000025,"type":"nulldata","isOpReturn":true}]
}</pre>
</div>
</div>
<div class="response-example">
<h4>Réponse (404)</h4>
<div class="code-block">
<pre>{
"error": "No anchor transactions found"
}</pre>
</div>
</div>
</div>
</section>
<!-- Section Informations -->
<section class="api-docs-section">
<div class="endpoint-card">
@ -1191,21 +1683,24 @@ Format: hexadécimal (0-9, a-f, A-F)</pre>
<h4>Base URLs</h4>
<p>Les APIs sont accessibles aux adresses suivantes :</p>
<div class="code-block">
<pre>API d'Ancrage : https://certificator.4nkweb.com
API Filigrane : https://watermark.certificator.4nkweb.com
API ClamAV : https://antivir.certificator.4nkweb.com</pre>
<pre>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)</pre>
</div>
<p><code>/health</code> est exposé par lAPI dAncrage et lAPI Faucet, pas par le Dashboard.</p>
</div>
<div class="info-box">
<h4>Ports fixes</h4>
<p>Tous les ports sont fixes et ne peuvent pas être modifiés :</p>
<ul style="margin-left: 20px; margin-top: 10px;">
<li>Dashboard : Port 3020</li>
<li>API d'Ancrage : Port 3010</li>
<li>API Faucet : Port 3021</li>
<li>API Filigrane : Port 3022</li>
<li>API ClamAV : Port 3023</li>
<li>Dashboard : Port 3020</li>
</ul>
</div>