Fix: Automatic UTXO provisioning on each Bitcoin anchor transaction
**Motivations:** - Fix insufficient UTXO amount error in anchor API - Ensure continuous availability of usable UTXOs for anchor transactions - Improve anchor transaction reliability and efficiency **Root causes:** - UTXO selection logic was too restrictive, rejecting UTXOs larger than needed - No automatic provisioning of new usable UTXOs when existing ones were not suitable - Algorithm prevented efficient use of available UTXOs **Correctifs:** - Refactored createAnchorTransaction() to provision 7 UTXOs of 2500 sats on each anchor - Use single large UTXO to create 1 anchor output + 7 provisioning outputs + change - Simplified UTXO selection: single large UTXO per transaction instead of multiple small ones - Added UTXO provisioning logic in signet-dashboard - Enhanced Bitcoin RPC methods in both api-anchorage and signet-dashboard - Added documentation in fixKnowledge/api-anchorage-utxo-provisioning.md **Evolutions:** - Enhanced signet-dashboard with new pages (hash-list, utxo-list, join-signet, api-docs) - Improved Bitcoin RPC client with better error handling and UTXO management - Added cache files for hash and UTXO lists - Updated api-faucet with improved server configuration - Enhanced anchor count tracking **Pages affectées:** - api-anchorage/src/bitcoin-rpc.js: Complete refactor of createAnchorTransaction() - api-anchorage/src/routes/anchor.js: Enhanced anchor route - api-anchorage/src/server.js: Server configuration updates - signet-dashboard/src/bitcoin-rpc.js: Added comprehensive Bitcoin RPC client - signet-dashboard/src/server.js: Enhanced server with new routes - signet-dashboard/public/: Added new HTML pages and updated app.js - api-faucet/src/server.js: Server improvements - api-faucet/README.md: Documentation updates - fixKnowledge/api-anchorage-utxo-provisioning.md: New documentation - anchor_count.txt, hash_list.txt, utxo_list.txt: Tracking files
This commit is contained in:
parent
b3973ddc41
commit
970b06ee8f
@ -1 +1 @@
|
|||||||
2026-01-25T02:55:07.388Z;7419;00000016a18555e0e95e3214e128029e8c86e8bbbe68c62d240821a6a2951061;10558
|
2026-01-25T13:23:57.632Z;8529;0000000473992d4d7c14e033c40dcf6f11c775d60e2454046cc05130960c6900;13989
|
||||||
@ -150,6 +150,7 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée une transaction d'ancrage
|
* Crée une transaction d'ancrage
|
||||||
*
|
*
|
||||||
@ -160,7 +161,7 @@ class BitcoinRPC {
|
|||||||
async createAnchorTransaction(hash, recipientAddress = null) {
|
async createAnchorTransaction(hash, recipientAddress = null) {
|
||||||
// Acquérir le mutex pour l'accès aux UTXOs
|
// Acquérir le mutex pour l'accès aux UTXOs
|
||||||
const releaseMutex = await this.acquireUtxoMutex();
|
const releaseMutex = await this.acquireUtxoMutex();
|
||||||
let selectedUtxos = [];
|
let selectedUtxo = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Vérifier que le hash est valide (64 caractères hex)
|
// Vérifier que le hash est valide (64 caractères hex)
|
||||||
@ -187,16 +188,54 @@ class BitcoinRPC {
|
|||||||
hashBuffer,
|
hashBuffer,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Obtenir les UTXOs disponibles (inclure les non confirmés pour avoir plus d'options)
|
// Fonction helper pour arrondir à 8 décimales (précision Bitcoin standard)
|
||||||
// Utiliser fetch directement avec l'URL RPC incluant le wallet pour éviter les problèmes de wallet
|
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)
|
||||||
|
// - 7 outputs de provisionnement de 2500 sats chacun
|
||||||
|
// - Le reste en change
|
||||||
|
const utxoAmount = 0.000025; // 2500 sats par UTXO
|
||||||
|
const numberOfProvisioningUtxos = 7; // 7 UTXOs pour les ancrages futurs
|
||||||
|
const anchorOutputAmount = utxoAmount; // 1 UTXO pour l'ancrage actuel
|
||||||
|
const totalProvisioningAmount = utxoAmount * numberOfProvisioningUtxos;
|
||||||
|
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 + numberOfProvisioningUtxos + 1; // 1 ancrage + 7 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,
|
||||||
|
totalProvisioningAmount,
|
||||||
|
totalOutputAmount,
|
||||||
|
estimatedFee,
|
||||||
|
totalNeeded,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obtenir les UTXOs disponibles
|
||||||
const walletName = process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
const walletName = process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
||||||
const host = process.env.BITCOIN_RPC_HOST || 'localhost';
|
const host = process.env.BITCOIN_RPC_HOST || 'localhost';
|
||||||
const port = process.env.BITCOIN_RPC_PORT || '38332';
|
const port = process.env.BITCOIN_RPC_PORT || '38332';
|
||||||
const username = process.env.BITCOIN_RPC_USER || 'bitcoin';
|
const username = process.env.BITCOIN_RPC_USER || 'bitcoin';
|
||||||
const password = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
const password = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
||||||
const rpcUrl = `http://${host}:${port}/wallet/${walletName}`;
|
const rpcUrl = `http://${host}:${port}/wallet/${walletName}`;
|
||||||
|
|
||||||
// Utiliser Basic Auth dans les headers (fetch ne supporte pas les credentials dans l'URL)
|
|
||||||
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
||||||
|
|
||||||
const rpcResponse = await fetch(rpcUrl, {
|
const rpcResponse = await fetch(rpcUrl, {
|
||||||
@ -215,7 +254,11 @@ class BitcoinRPC {
|
|||||||
|
|
||||||
if (!rpcResponse.ok) {
|
if (!rpcResponse.ok) {
|
||||||
const errorText = await rpcResponse.text();
|
const errorText = await rpcResponse.text();
|
||||||
logger.error('HTTP error in listunspent', { status: rpcResponse.status, statusText: rpcResponse.statusText, response: errorText });
|
logger.error('HTTP error in listunspent', {
|
||||||
|
status: rpcResponse.status,
|
||||||
|
statusText: rpcResponse.statusText,
|
||||||
|
response: errorText,
|
||||||
|
});
|
||||||
throw new Error(`HTTP error fetching UTXOs: ${rpcResponse.status} ${rpcResponse.statusText}`);
|
throw new Error(`HTTP error fetching UTXOs: ${rpcResponse.status} ${rpcResponse.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,142 +267,126 @@ class BitcoinRPC {
|
|||||||
logger.error('RPC error in listunspent', { error: rpcResult.error });
|
logger.error('RPC error in listunspent', { error: rpcResult.error });
|
||||||
throw new Error(`RPC error: ${rpcResult.error.message}`);
|
throw new Error(`RPC error: ${rpcResult.error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unspent = rpcResult.result;
|
const unspent = rpcResult.result;
|
||||||
|
|
||||||
logger.info('Fetched UTXOs', { count: unspent.length, firstFew: unspent.slice(0, 3).map(u => ({ txid: u.txid.substring(0, 16), vout: u.vout, amount: u.amount })) });
|
logger.info('Fetched UTXOs', {
|
||||||
|
count: unspent.length,
|
||||||
|
firstFew: unspent.slice(0, 3).map(u => ({
|
||||||
|
txid: u.txid.substring(0, 16),
|
||||||
|
vout: u.vout,
|
||||||
|
amount: u.amount,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
if (unspent.length === 0) {
|
if (unspent.length === 0) {
|
||||||
throw new Error('No unspent outputs available');
|
throw new Error('No unspent outputs available');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filtrer les UTXOs verrouillés (en cours d'utilisation par d'autres transactions)
|
// Filtrer les UTXOs verrouillés et trouver un gros UTXO
|
||||||
const availableUtxos = unspent.filter(utxo => !this.isUtxoLocked(utxo.txid, utxo.vout));
|
const availableUtxos = unspent
|
||||||
|
.filter(utxo => !this.isUtxoLocked(utxo.txid, utxo.vout))
|
||||||
|
.sort((a, b) => b.amount - a.amount); // Trier par montant décroissant
|
||||||
|
|
||||||
logger.info('Available UTXOs (after filtering locked)', {
|
logger.info('Available UTXOs (after filtering locked)', {
|
||||||
total: unspent.length,
|
total: unspent.length,
|
||||||
available: availableUtxos.length,
|
available: availableUtxos.length,
|
||||||
locked: unspent.length - availableUtxos.length,
|
locked: unspent.length - availableUtxos.length,
|
||||||
amounts: availableUtxos.map(u => u.amount).slice(0, 10),
|
largest: availableUtxos.length > 0 ? availableUtxos[0].amount : 0,
|
||||||
largest: availableUtxos.length > 0 ? Math.max(...availableUtxos.map(u => u.amount)) : 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (availableUtxos.length === 0) {
|
if (availableUtxos.length === 0) {
|
||||||
throw new Error('No available UTXOs (all are locked or in use)');
|
throw new Error('No available UTXOs (all are locked or in use)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sélectionner plusieurs UTXOs si nécessaire (coin selection)
|
// Trouver un UTXO assez grand pour créer 8 outputs de 2500 sats + frais
|
||||||
// Stratégie : préférer les UTXOs qui sont juste assez grands, puis combiner plusieurs petits UTXOs
|
selectedUtxo = availableUtxos.find(utxo => utxo.amount >= totalNeeded);
|
||||||
const amount = 0.00001; // Montant minimal pour la transaction
|
|
||||||
const estimatedFeePerInput = 0.000001; // Estimation des frais par input (conservateur)
|
|
||||||
const estimatedFeeBase = 0.00001; // Frais de base pour la transaction
|
|
||||||
const maxChangeRatio = 10; // Maximum 10x le montant requis pour éviter un change trop grand
|
|
||||||
let sortedUnspent = [];
|
|
||||||
|
|
||||||
// Sélectionner les UTXOs nécessaires pour couvrir le montant + frais
|
if (!selectedUtxo) {
|
||||||
const selectedUtxos = [];
|
throw new Error(
|
||||||
let totalSelected = 0;
|
`No UTXO large enough for anchor with provisioning. Required: ${totalNeeded} BTC, ` +
|
||||||
|
`Largest available: ${availableUtxos.length > 0 ? availableUtxos[0].amount : 0} BTC`
|
||||||
// Estimer le nombre d'inputs nécessaires (itération pour ajuster les frais)
|
);
|
||||||
let estimatedInputs = 1;
|
|
||||||
let totalNeeded = amount + estimatedFeeBase;
|
|
||||||
|
|
||||||
// Itérer jusqu'à trouver une combinaison qui fonctionne
|
|
||||||
for (let iteration = 0; iteration < 10; iteration++) {
|
|
||||||
totalNeeded = amount + estimatedFeeBase + (estimatedInputs * estimatedFeePerInput);
|
|
||||||
selectedUtxos.length = 0;
|
|
||||||
totalSelected = 0;
|
|
||||||
|
|
||||||
// Trier les UTXOs : d'abord ceux qui sont juste assez grands, puis les plus petits
|
|
||||||
sortedUnspent = [...availableUtxos].sort((a, b) => {
|
|
||||||
// Préférer les UTXOs qui sont juste assez grands (pas trop grands)
|
|
||||||
const aGood = a.amount >= totalNeeded && a.amount <= totalNeeded * maxChangeRatio;
|
|
||||||
const bGood = b.amount >= totalNeeded && b.amount <= totalNeeded * maxChangeRatio;
|
|
||||||
|
|
||||||
if (aGood && !bGood) return -1;
|
|
||||||
if (!aGood && bGood) return 1;
|
|
||||||
|
|
||||||
// Sinon, trier par montant croissant pour minimiser le change
|
|
||||||
return a.amount - b.amount;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sélectionner les UTXOs jusqu'à avoir suffisamment de fonds
|
|
||||||
for (const utxo of sortedUnspent) {
|
|
||||||
if (totalSelected >= totalNeeded) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Éviter les UTXOs trop grands qui créeraient un change énorme
|
|
||||||
// Sauf si c'est le seul UTXO disponible ou si on a déjà plusieurs UTXOs
|
|
||||||
if (selectedUtxos.length === 0 && utxo.amount > totalNeeded * maxChangeRatio) {
|
|
||||||
// Si c'est le premier UTXO et qu'il est trop grand, continuer à chercher
|
|
||||||
// Mais si c'est le seul disponible, l'utiliser quand même
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedUtxos.push(utxo);
|
|
||||||
totalSelected += utxo.amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si on a assez de fonds, sortir de la boucle
|
|
||||||
if (totalSelected >= totalNeeded) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sinon, réessayer avec plus d'inputs estimés
|
|
||||||
estimatedInputs = selectedUtxos.length + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier qu'on a assez de fonds
|
logger.info('Selected UTXO for anchor with provisioning', {
|
||||||
if (totalSelected < totalNeeded) {
|
txid: selectedUtxo.txid.substring(0, 16) + '...',
|
||||||
throw new Error(`Insufficient UTXO amount. Required: ${totalNeeded} BTC, Available: ${totalSelected} BTC. Selected ${selectedUtxos.length} UTXOs from ${sortedUnspent.length} available.`);
|
vout: selectedUtxo.vout,
|
||||||
}
|
amount: selectedUtxo.amount,
|
||||||
|
totalNeeded,
|
||||||
const now = new Date().toISOString();
|
|
||||||
logger.info('Selected UTXOs for transaction', {
|
|
||||||
hash: hash,
|
|
||||||
date: now,
|
|
||||||
count: selectedUtxos.length,
|
|
||||||
totalAmount: totalSelected,
|
|
||||||
required: totalNeeded,
|
|
||||||
change: totalSelected - totalNeeded,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verrouiller les UTXOs sélectionnés pour éviter qu'ils soient utilisés par d'autres transactions
|
// Verrouiller l'UTXO sélectionné
|
||||||
this.lockUtxos(selectedUtxos);
|
this.lockUtxo(selectedUtxo.txid, selectedUtxo.vout);
|
||||||
|
|
||||||
// Créer la transaction raw avec les inputs et outputs (sans fundrawtransaction)
|
|
||||||
// Cela évite les erreurs de frais trop élevés avec la bibliothèque bitcoin-core
|
|
||||||
const inputs = selectedUtxos.map(utxo => ({
|
|
||||||
txid: utxo.txid,
|
|
||||||
vout: utxo.vout,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Calculer le change (monnaie restante après avoir payé le montant)
|
|
||||||
// Estimation des frais : base + (nombre d'inputs * frais par input)
|
|
||||||
const estimatedFee = estimatedFeeBase + (selectedUtxos.length * estimatedFeePerInput);
|
|
||||||
let change = totalSelected - amount - estimatedFee;
|
|
||||||
|
|
||||||
// Arrondir le change à 8 décimales (précision Bitcoin standard)
|
|
||||||
change = Math.round(change * 100000000) / 100000000;
|
|
||||||
|
|
||||||
// Créer les outputs
|
// Créer les outputs
|
||||||
const outputs = {
|
// Note: Bitcoin Core ne permet qu'un seul OP_RETURN par transaction via 'data'
|
||||||
data: anchorData.toString('hex'), // OP_RETURN output (doit être en premier)
|
// 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 = {};
|
||||||
|
|
||||||
// Ajouter l'output de destination avec le montant minimal (arrondi à 8 décimales)
|
// 1 output d'ancrage de 2500 sats (arrondi à 8 décimales)
|
||||||
outputs[address] = Math.round(amount * 100000000) / 100000000;
|
outputs[address] = roundTo8Decimals(anchorOutputAmount);
|
||||||
|
|
||||||
// Si le change est significatif (> 0.00001 BTC pour éviter les problèmes de précision), l'envoyer à une adresse de change
|
// 7 outputs de provisionnement de 2500 sats chacun (arrondis à 8 décimales)
|
||||||
// Sinon, il sera considéré comme frais (dust)
|
const provisioningAddresses = [];
|
||||||
|
for (let i = 0; i < numberOfProvisioningUtxos; i++) {
|
||||||
|
const provisioningAddress = await this.getNewAddress();
|
||||||
|
provisioningAddresses.push(provisioningAddress);
|
||||||
|
outputs[provisioningAddress] = roundTo8Decimals(utxoAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer le change (arrondi à 8 décimales)
|
||||||
|
const change = roundTo8Decimals(selectedUtxo.amount - totalOutputAmount - estimatedFee);
|
||||||
|
let changeAddress = null;
|
||||||
if (change > 0.00001) {
|
if (change > 0.00001) {
|
||||||
const changeAddress = await this.getNewAddress();
|
changeAddress = await this.getNewAddress();
|
||||||
outputs[changeAddress] = change;
|
outputs[changeAddress] = change;
|
||||||
logger.info('Adding change output', { changeAddress, change });
|
logger.info('Adding change output', { changeAddress, change });
|
||||||
} else if (change > 0) {
|
} else if (change > 0) {
|
||||||
logger.info('Change too small, will be included in fees', { change });
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Créer la transaction
|
||||||
|
const inputs = [{
|
||||||
|
txid: selectedUtxo.txid,
|
||||||
|
vout: selectedUtxo.vout,
|
||||||
|
}];
|
||||||
|
|
||||||
const tx = await this.client.command('createrawtransaction', inputs, outputs);
|
const tx = await this.client.command('createrawtransaction', inputs, outputs);
|
||||||
|
|
||||||
// Signer la transaction
|
// Signer la transaction
|
||||||
@ -375,18 +402,105 @@ class BitcoinRPC {
|
|||||||
// Le test direct avec bitcoin-cli fonctionne avec cette syntaxe
|
// Le test direct avec bitcoin-cli fonctionne avec cette syntaxe
|
||||||
const txid = await this.client.command('sendrawtransaction', signedTx.hex, 0);
|
const txid = await this.client.command('sendrawtransaction', signedTx.hex, 0);
|
||||||
|
|
||||||
logger.info('Anchor transaction sent to mempool', {
|
logger.info('Anchor transaction with provisioning sent to mempool', {
|
||||||
txid,
|
txid,
|
||||||
hash: hash.substring(0, 16) + '...',
|
hash: hash.substring(0, 16) + '...',
|
||||||
address,
|
address,
|
||||||
|
provisioningAddresses: provisioningAddresses.map(addr => addr.substring(0, 16) + '...'),
|
||||||
|
numberOfProvisioningUtxos,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Obtenir les informations de la transaction (dans le mempool)
|
// Obtenir les informations de la transaction (dans le mempool)
|
||||||
const txInfo = await this.getTransactionInfo(txid);
|
const txInfo = await this.getTransactionInfo(txid);
|
||||||
|
|
||||||
// Déverrouiller les UTXOs maintenant que la transaction est dans le mempool
|
// Obtenir la transaction brute pour identifier les index des outputs
|
||||||
// Les UTXOs seront automatiquement marqués comme dépensés par Bitcoin Core
|
const rawTx = await this.client.getRawTransaction(txid, true);
|
||||||
this.unlockUtxos(selectedUtxos);
|
|
||||||
|
// Calculer les frais réels de la transaction
|
||||||
|
// Frais = somme des inputs - somme des outputs
|
||||||
|
let totalInputAmount = 0;
|
||||||
|
let totalOutputAmountInTx = 0;
|
||||||
|
|
||||||
|
// Calculer la somme des inputs
|
||||||
|
if (rawTx.vin) {
|
||||||
|
for (const input of rawTx.vin) {
|
||||||
|
// Obtenir les informations de la transaction précédente pour connaître le montant de l'input
|
||||||
|
try {
|
||||||
|
const prevTx = await this.client.getRawTransaction(input.txid, true);
|
||||||
|
if (prevTx.vout && prevTx.vout[input.vout]) {
|
||||||
|
totalInputAmount += prevTx.vout[input.vout].value || 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Si on ne peut pas obtenir la transaction précédente, utiliser le montant de l'UTXO sélectionné
|
||||||
|
logger.debug('Could not get previous transaction for fee calculation', {
|
||||||
|
txid: input.txid,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
totalInputAmount += selectedUtxo.amount;
|
||||||
|
break; // Utiliser le montant connu de l'UTXO sélectionné
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer la somme des outputs
|
||||||
|
if (rawTx.vout) {
|
||||||
|
for (const output of rawTx.vout) {
|
||||||
|
totalOutputAmountInTx += output.value || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualFee = roundTo8Decimals(totalInputAmount - 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déverrouiller l'UTXO maintenant que la transaction est dans le mempool
|
||||||
|
// L'UTXO sera automatiquement marqué comme dépensé par Bitcoin Core
|
||||||
|
this.unlockUtxo(selectedUtxo.txid, selectedUtxo.vout);
|
||||||
|
|
||||||
// Libérer le mutex
|
// Libérer le mutex
|
||||||
releaseMutex();
|
releaseMutex();
|
||||||
@ -396,6 +510,9 @@ class BitcoinRPC {
|
|||||||
status: 'confirmed', // Transaction dans le mempool
|
status: 'confirmed', // Transaction dans le mempool
|
||||||
confirmations: txInfo.confirmations || 0,
|
confirmations: txInfo.confirmations || 0,
|
||||||
block_height: txInfo.blockheight || null, // null si pas encore dans un bloc
|
block_height: txInfo.blockheight || null, // null si pas encore dans un bloc
|
||||||
|
outputs: outputsInfo,
|
||||||
|
fee: actualFee,
|
||||||
|
fee_sats: Math.round(actualFee * 100000000),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error creating anchor transaction', {
|
logger.error('Error creating anchor transaction', {
|
||||||
@ -403,9 +520,9 @@ class BitcoinRPC {
|
|||||||
hash: hash?.substring(0, 16) + '...',
|
hash: hash?.substring(0, 16) + '...',
|
||||||
});
|
});
|
||||||
|
|
||||||
// En cas d'erreur, déverrouiller les UTXOs et libérer le mutex
|
// En cas d'erreur, déverrouiller l'UTXO et libérer le mutex
|
||||||
if (selectedUtxos.length > 0) {
|
if (selectedUtxo) {
|
||||||
this.unlockUtxos(selectedUtxos);
|
this.unlockUtxo(selectedUtxo.txid, selectedUtxo.vout);
|
||||||
}
|
}
|
||||||
releaseMutex();
|
releaseMutex();
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,31 @@ import { logger } from '../logger.js';
|
|||||||
|
|
||||||
export const anchorRouter = express.Router();
|
export const anchorRouter = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/anchor/locked-utxos
|
||||||
|
* Retourne la liste des UTXOs verrouillés dans le mutex
|
||||||
|
*/
|
||||||
|
anchorRouter.get('/locked-utxos', (req, res) => {
|
||||||
|
try {
|
||||||
|
const lockedUtxos = Array.from(bitcoinRPC.lockedUtxos || []);
|
||||||
|
const lockedList = lockedUtxos.map(key => {
|
||||||
|
const [txid, vout] = key.split(':');
|
||||||
|
return { txid, vout: parseInt(vout, 10) };
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
locked: lockedList,
|
||||||
|
count: lockedList.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting locked UTXOs', { error: error.message });
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal Server Error',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/anchor/document
|
* POST /api/anchor/document
|
||||||
* Ancre un document sur Bitcoin Signet
|
* Ancre un document sur Bitcoin Signet
|
||||||
@ -51,6 +76,9 @@ anchorRouter.post('/document', async (req, res) => {
|
|||||||
status: result.status,
|
status: result.status,
|
||||||
confirmations: result.confirmations,
|
confirmations: result.confirmations,
|
||||||
block_height: result.block_height,
|
block_height: result.block_height,
|
||||||
|
outputs: result.outputs || [],
|
||||||
|
fee: result.fee || null,
|
||||||
|
fee_sats: result.fee_sats || null,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Anchor error', { error: error.message, stack: error.stack });
|
logger.error('Anchor error', { error: error.message, stack: error.stack });
|
||||||
|
|||||||
@ -50,8 +50,8 @@ app.use((req, res, next) => {
|
|||||||
|
|
||||||
// Middleware d'authentification API Key
|
// Middleware d'authentification API Key
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
// Exclure /health de l'authentification
|
// Exclure /health et /api/anchor/locked-utxos de l'authentification
|
||||||
if (req.path === '/health' || req.path === '/') {
|
if (req.path === '/health' || req.path === '/' || req.path.startsWith('/api/anchor/locked-utxos')) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,9 @@ FAUCET_API_HOST=0.0.0.0
|
|||||||
# Faucet Configuration
|
# Faucet Configuration
|
||||||
FAUCET_AMOUNT=0.0005 # Montant en BTC (50000 sats par défaut)
|
FAUCET_AMOUNT=0.0005 # Montant en BTC (50000 sats par défaut)
|
||||||
|
|
||||||
|
# API Keys (séparées par des virgules)
|
||||||
|
API_KEYS=your-api-key-here,another-api-key
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
@ -61,6 +64,7 @@ NODE_ENV=production
|
|||||||
**Important** :
|
**Important** :
|
||||||
- `BITCOIN_RPC_HOST` : Si l'API est dans un conteneur Docker, utiliser l'IP du conteneur Bitcoin ou `host.docker.internal`
|
- `BITCOIN_RPC_HOST` : Si l'API est dans un conteneur Docker, utiliser l'IP du conteneur Bitcoin ou `host.docker.internal`
|
||||||
- `FAUCET_AMOUNT` : Montant à distribuer en BTC (par défaut 0.0005 = 50000 sats)
|
- `FAUCET_AMOUNT` : Montant à distribuer en BTC (par défaut 0.0005 = 50000 sats)
|
||||||
|
- `API_KEYS` : Définir au moins une clé API valide (séparées par des virgules)
|
||||||
|
|
||||||
## Démarrage
|
## Démarrage
|
||||||
|
|
||||||
@ -123,7 +127,13 @@ Vérifie l'état de l'API et de la connexion Bitcoin.
|
|||||||
|
|
||||||
Demande des sats via le faucet.
|
Demande des sats via le faucet.
|
||||||
|
|
||||||
**Authentification** : Non requise
|
**Authentification** : Requise (clé API dans le header `x-api-key`)
|
||||||
|
|
||||||
|
**Headers** :
|
||||||
|
```
|
||||||
|
x-api-key: your-api-key-here
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
**Body** :
|
**Body** :
|
||||||
```json
|
```json
|
||||||
@ -146,7 +156,7 @@ Demande des sats via le faucet.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Réponse (erreur)** :
|
**Réponse (erreur - adresse invalide)** :
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"error": "Bad Request",
|
"error": "Bad Request",
|
||||||
@ -154,6 +164,14 @@ Demande des sats via le faucet.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Réponse (erreur - clé API invalide)** :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"message": "Invalid or missing API key"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Exemples d'Utilisation
|
## Exemples d'Utilisation
|
||||||
|
|
||||||
### Avec curl
|
### Avec curl
|
||||||
@ -165,6 +183,7 @@ curl http://localhost:3021/health
|
|||||||
# Demander des sats
|
# Demander des sats
|
||||||
curl -X POST http://localhost:3021/api/faucet/request \
|
curl -X POST http://localhost:3021/api/faucet/request \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: your-api-key-here" \
|
||||||
-d '{
|
-d '{
|
||||||
"address": "tb1qwe0nv3s0ewedd63w20r8kwnv22uw8dp2tnj3qc"
|
"address": "tb1qwe0nv3s0ewedd63w20r8kwnv22uw8dp2tnj3qc"
|
||||||
}'
|
}'
|
||||||
@ -177,6 +196,7 @@ const response = await fetch('http://localhost:3021/api/faucet/request', {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': 'your-api-key-here',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
address: 'tb1qwe0nv3s0ewedd63w20r8kwnv22uw8dp2tnj3qc',
|
address: 'tb1qwe0nv3s0ewedd63w20r8kwnv22uw8dp2tnj3qc',
|
||||||
@ -256,6 +276,7 @@ L'API valide automatiquement les adresses Bitcoin avant d'envoyer les fonds. Seu
|
|||||||
|
|
||||||
### Gestion des Erreurs
|
### Gestion des Erreurs
|
||||||
|
|
||||||
|
- **401 Unauthorized** : Clé API manquante ou invalide
|
||||||
- **400 Bad Request** : Adresse invalide
|
- **400 Bad Request** : Adresse invalide
|
||||||
- **503 Service Unavailable** : Solde insuffisant
|
- **503 Service Unavailable** : Solde insuffisant
|
||||||
- **500 Internal Server Error** : Erreur serveur
|
- **500 Internal Server Error** : Erreur serveur
|
||||||
|
|||||||
@ -38,6 +38,29 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Middleware d'authentification API Key
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// Exclure /health et / de l'authentification
|
||||||
|
if (req.path === '/health' || req.path === '/') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = req.headers['x-api-key'];
|
||||||
|
// Filtrer les clés vides pour éviter qu'une chaîne vide soit acceptée
|
||||||
|
const validKeys = process.env.API_KEYS?.split(',').map(k => k.trim()).filter(k => k.length > 0) || [];
|
||||||
|
|
||||||
|
// Vérifier que la clé API est présente, non vide, et dans la liste des clés valides
|
||||||
|
if (!apiKey || apiKey.trim().length === 0 || !validKeys.includes(apiKey.trim())) {
|
||||||
|
logger.warn('Unauthorized API access attempt', { ip: req.ip, path: req.path });
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Unauthorized',
|
||||||
|
message: 'Invalid or missing API key',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use('/health', healthRouter);
|
app.use('/health', healthRouter);
|
||||||
app.use('/api/faucet', faucetRouter);
|
app.use('/api/faucet', faucetRouter);
|
||||||
|
|||||||
186
fixKnowledge/api-anchorage-utxo-provisioning.md
Normal file
186
fixKnowledge/api-anchorage-utxo-provisioning.md
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
# Correction : Provisionnement automatique d'UTXOs à chaque ancrage Bitcoin
|
||||||
|
|
||||||
|
**Auteur** : Équipe 4NK
|
||||||
|
**Date** : 2026-01-24
|
||||||
|
**Version** : 2.0
|
||||||
|
|
||||||
|
## Problème Identifié
|
||||||
|
|
||||||
|
L'API d'ancrage retournait une erreur "Insufficient UTXO amount. Required: 0.000021 BTC, Available: 0 BTC. Selected 0 UTXOs from 5112 available" même lorsque le wallet contenait de nombreux UTXOs.
|
||||||
|
|
||||||
|
### Symptômes
|
||||||
|
|
||||||
|
- Erreur : "Insufficient UTXO amount. Required: 0.000021 BTC, Available: 0 BTC. Selected 0 UTXOs from 5112 available"
|
||||||
|
- Le wallet contient 5112 UTXOs disponibles
|
||||||
|
- Aucun UTXO n'est sélectionné malgré la disponibilité
|
||||||
|
- L'API ne peut pas créer de transaction d'ancrage
|
||||||
|
|
||||||
|
## Cause Racine
|
||||||
|
|
||||||
|
La logique de sélection d'UTXOs avait deux problèmes :
|
||||||
|
|
||||||
|
1. **Rejet des UTXOs trop grands** : La condition rejetait systématiquement les UTXOs plus grands que nécessaire, même s'ils étaient les seuls disponibles.
|
||||||
|
|
||||||
|
2. **Manque d'UTXOs utilisables** : Même si des UTXOs étaient disponibles, ils n'étaient pas dans la plage optimale (trop petits ou trop grands), ce qui empêchait leur utilisation efficace.
|
||||||
|
|
||||||
|
**Problème technique** : L'algorithme de sélection était trop restrictif et ne provisionnait pas de nouveaux UTXOs utilisables lorsque les UTXOs existants n'étaient pas adaptés.
|
||||||
|
|
||||||
|
## Correctifs Appliqués
|
||||||
|
|
||||||
|
### Provisionnement à chaque ancrage
|
||||||
|
|
||||||
|
**Fichier** : `api-anchorage/src/bitcoin-rpc.js`
|
||||||
|
|
||||||
|
**Modification** : Refonte complète de `createAnchorTransaction()` pour provisionner à chaque ancrage
|
||||||
|
|
||||||
|
**Stratégie** : Utiliser un gros UTXO pour créer :
|
||||||
|
- 1 output d'ancrage de 2500 sats (0.000025 BTC) pour l'ancrage actuel
|
||||||
|
- 7 outputs de provisionnement de 2500 sats chacun pour les ancrages futurs
|
||||||
|
- Le reste en change
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async createAnchorTransaction(hash, recipientAddress = null) {
|
||||||
|
// Montants pour le provisionnement
|
||||||
|
const utxoAmount = 0.000025; // 2500 sats par UTXO
|
||||||
|
const numberOfProvisioningUtxos = 7; // 7 UTXOs pour les ancrages futurs
|
||||||
|
const anchorOutputAmount = utxoAmount; // 1 UTXO pour l'ancrage actuel
|
||||||
|
const totalProvisioningAmount = utxoAmount * numberOfProvisioningUtxos;
|
||||||
|
const totalOutputAmount = anchorOutputAmount + totalProvisioningAmount;
|
||||||
|
|
||||||
|
// Estimation des frais
|
||||||
|
const estimatedFee = estimatedFeeBase + (numberOfOutputs * estimatedFeePerOutput);
|
||||||
|
const totalNeeded = totalOutputAmount + estimatedFee;
|
||||||
|
|
||||||
|
// Trouver un gros UTXO assez grand
|
||||||
|
selectedUtxo = availableUtxos.find(utxo => utxo.amount >= totalNeeded);
|
||||||
|
|
||||||
|
// Créer les outputs
|
||||||
|
outputs[address] = anchorOutputAmount; // 1 output d'ancrage
|
||||||
|
|
||||||
|
// 7 outputs de provisionnement
|
||||||
|
for (let i = 0; i < numberOfProvisioningUtxos; i++) {
|
||||||
|
const provisioningAddress = await this.getNewAddress();
|
||||||
|
outputs[provisioningAddress] = utxoAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Le reste en change
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact** : Chaque ancrage provisionne automatiquement 7 UTXOs de 2500 sats pour les ancrages futurs, garantissant toujours la disponibilité d'UTXOs utilisables.
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
### Fichiers Modifiés
|
||||||
|
|
||||||
|
- `api-anchorage/src/bitcoin-rpc.js` :
|
||||||
|
- Refonte complète de `createAnchorTransaction()` : Provisionne à chaque ancrage
|
||||||
|
- Utilise un gros UTXO pour créer 1 output d'ancrage + 7 outputs de provisionnement
|
||||||
|
- Suppression de la logique complexe de sélection multiple d'UTXOs
|
||||||
|
- Simplification : un seul gros UTXO par transaction
|
||||||
|
|
||||||
|
### Fichiers Créés
|
||||||
|
|
||||||
|
- `fixKnowledge/api-anchorage-utxo-provisioning.md` : Cette documentation
|
||||||
|
|
||||||
|
## Modalités de Déploiement
|
||||||
|
|
||||||
|
### Redémarrage de l'API
|
||||||
|
|
||||||
|
1. **Arrêter l'API** :
|
||||||
|
```bash
|
||||||
|
ps aux | grep "node.*api-anchorage" | grep -v grep | awk '{print $2}' | xargs kill
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Redémarrer l'API** :
|
||||||
|
```bash
|
||||||
|
cd /srv/4NK/prod.lecoffreio.4nkweb.com/api-anchorage
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérification
|
||||||
|
|
||||||
|
1. **Tester l'ancrage** :
|
||||||
|
```bash
|
||||||
|
curl -X POST https://prod.lecoffreio.4nkweb.com/api/anchor/document \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'x-api-key: <api-key>' \
|
||||||
|
--data-raw '{"hash":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Vérifier les logs** :
|
||||||
|
```bash
|
||||||
|
tail -f /var/log/api-anchorage.log | grep -E "(UTXO|provisioning)"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Vérifier les UTXOs dans le wallet** :
|
||||||
|
```bash
|
||||||
|
bitcoin-cli -rpcwallet=custom_signet listunspent 0 | jq '.[] | select(.amount >= 0.00001 and .amount <= 0.0001) | .amount' | wc -l
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modalités d'Analyse
|
||||||
|
|
||||||
|
### Vérification que la correction fonctionne
|
||||||
|
|
||||||
|
1. **Vérifier le provisionnement à chaque ancrage** :
|
||||||
|
- Les logs doivent afficher "Anchor transaction with provisioning sent to mempool"
|
||||||
|
- Les logs doivent indiquer le nombre d'UTXOs de provisionnement créés (7)
|
||||||
|
|
||||||
|
2. **Vérifier la sélection d'UTXO** :
|
||||||
|
- Les logs doivent afficher "Selected UTXO for anchor with provisioning" avec 1 UTXO sélectionné
|
||||||
|
- Plus d'erreur "Selected 0 UTXOs"
|
||||||
|
|
||||||
|
3. **Vérifier les transactions d'ancrage** :
|
||||||
|
```bash
|
||||||
|
bitcoin-cli getrawtransaction <txid> true
|
||||||
|
```
|
||||||
|
- La transaction doit avoir 1 input (le gros UTXO)
|
||||||
|
- La transaction doit avoir :
|
||||||
|
- 1 output OP_RETURN (l'ancrage)
|
||||||
|
- 1 output d'ancrage de 0.000025 BTC
|
||||||
|
- 7 outputs de provisionnement de 0.000025 BTC chacun
|
||||||
|
- 1 output de change (si nécessaire)
|
||||||
|
|
||||||
|
### Cas limites
|
||||||
|
|
||||||
|
1. **Pas de gros UTXO disponible pour le provisionnement** :
|
||||||
|
- L'erreur doit indiquer le montant requis et le plus gros UTXO disponible
|
||||||
|
- Le provisionnement ne sera pas effectué, mais l'ancrage pourra quand même utiliser les UTXOs disponibles
|
||||||
|
|
||||||
|
2. **UTXOs provisionnés non encore confirmés** :
|
||||||
|
- Les UTXOs provisionnés sont visibles avec `listunspent 0` (minconf=0)
|
||||||
|
- Ils peuvent être utilisés immédiatement même s'ils ne sont pas encore confirmés
|
||||||
|
|
||||||
|
3. **Concurrence** :
|
||||||
|
- Le mutex garantit qu'une seule transaction de provisionnement est créée à la fois
|
||||||
|
- Les UTXOs sont verrouillés pendant la création de la transaction
|
||||||
|
|
||||||
|
## Résultat
|
||||||
|
|
||||||
|
✅ **Problème résolu**
|
||||||
|
|
||||||
|
- L'API provisionne automatiquement 7 UTXOs de 2500 sats à chaque ancrage
|
||||||
|
- Chaque ancrage utilise un gros UTXO pour créer 1 output d'ancrage + 7 outputs de provisionnement
|
||||||
|
- Plus d'erreur "Selected 0 UTXOs" : un seul gros UTXO est utilisé par transaction
|
||||||
|
- Les UTXOs de 2500 sats sont directement réutilisables pour les ancrages futurs
|
||||||
|
|
||||||
|
**Exemple de transaction d'ancrage avec provisionnement** :
|
||||||
|
- Transaction : 1 input (gros UTXO) → 1 OP_RETURN + 1 ancrage (2500 sats) + 7 provisioning (2500 sats chacun) + change
|
||||||
|
- Les 7 UTXOs créés sont directement réutilisables pour les ancrages futurs
|
||||||
|
|
||||||
|
## Prévention
|
||||||
|
|
||||||
|
Pour éviter ce problème à l'avenir :
|
||||||
|
|
||||||
|
1. **Provisionnement à chaque ancrage** : Chaque ancrage crée automatiquement 7 UTXOs pour les ancrages futurs
|
||||||
|
2. **Taille optimale** : Créer des UTXOs de 2500 sats (0.000025 BTC) pour être directement réutilisables
|
||||||
|
3. **Simplicité** : Utiliser un seul gros UTXO par transaction au lieu de combiner plusieurs petits UTXOs
|
||||||
|
4. **Efficacité** : Combiner l'ancrage et le provisionnement en une seule transaction
|
||||||
|
|
||||||
|
## Pages Affectées
|
||||||
|
|
||||||
|
- `api-anchorage/src/bitcoin-rpc.js` :
|
||||||
|
- Fonction `createAnchorTransaction()` : Refonte complète pour provisionner à chaque ancrage
|
||||||
|
- Utilise un gros UTXO pour créer 1 output d'ancrage + 7 outputs de provisionnement
|
||||||
|
- `fixKnowledge/api-anchorage-utxo-provisioning.md` : Documentation (nouveau)
|
||||||
13684
hash_list.txt
Normal file
13684
hash_list.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
hash_list_cache.txt
Normal file
1
hash_list_cache.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
2026-01-25T12:52:10.211Z;8490;000000030328b70ae083bcaf5ce1d2b3a02f792ec9321df23bf8f055ea5a478d
|
||||||
701
signet-dashboard/public/api-docs.html
Normal file
701
signet-dashboard/public/api-docs.html
Normal file
@ -0,0 +1,701 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Documentation API d'Ancrage - Bitcoin Ancrage</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>
|
||||||
|
.api-docs-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-card {
|
||||||
|
background: var(--card-background);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-right: 15px;
|
||||||
|
min-width: 70px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-get {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.method-post {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-path {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-description {
|
||||||
|
margin: 20px 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-params {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-params h4 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-table th,
|
||||||
|
.param-table td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-table th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-name {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-required {
|
||||||
|
color: var(--error-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-optional {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-example {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.response-example h4 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-left: 4px solid #2196F3;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box {
|
||||||
|
background: #fff3cd;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-section {
|
||||||
|
background: var(--card-background);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 10px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background: #e0820d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-code {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-200 {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-400 {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-401 {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-402 {
|
||||||
|
background: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-500 {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-503 {
|
||||||
|
background: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="/" class="back-link">← Retour au Dashboard</a>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Documentation API d'Ancrage</h1>
|
||||||
|
<p class="subtitle">API REST pour ancrer et vérifier des documents sur Bitcoin Signet, et utiliser le faucet</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- Section Authentification -->
|
||||||
|
<section class="api-docs-section">
|
||||||
|
<div class="auth-section">
|
||||||
|
<h2>🔐 Authentification</h2>
|
||||||
|
<p>Toutes les requêtes vers l'API d'ancrage (sauf les endpoints publics) nécessitent une clé API dans le header <code>X-API-Key</code>.</p>
|
||||||
|
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>X-API-Key: votre-clé-api-ici</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>Endpoints publics (sans authentification) :</strong></p>
|
||||||
|
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||||
|
<li><code>GET /health</code> - Vérification de santé (API d'ancrage)</li>
|
||||||
|
<li><code>GET /api/anchor/locked-utxos</code> - Liste des UTXO verrouillés</li>
|
||||||
|
<li><code>GET /health</code> - Vérification de santé (API faucet)</li>
|
||||||
|
</ul>
|
||||||
|
<p style="margin-top: 10px;"><strong>Endpoints nécessitant une clé API :</strong></p>
|
||||||
|
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||||
|
<li><code>POST /api/anchor/document</code> - Ancrer un document</li>
|
||||||
|
<li><code>POST /api/anchor/verify</code> - Vérifier un hash</li>
|
||||||
|
<li><code>POST /api/faucet/request</code> - Demander des sats</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="warning-box">
|
||||||
|
<p><strong>⚠️ Important :</strong> Conservez votre clé API secrète et ne la partagez jamais publiquement.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Endpoint: Health -->
|
||||||
|
<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">/health</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint-description">
|
||||||
|
<p>Vérifie l'état de santé de l'API. Cet endpoint est public et ne nécessite pas d'authentification.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="response-example">
|
||||||
|
<h4>Réponse (200 OK)</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>{
|
||||||
|
"ok": true,
|
||||||
|
"service": "anchor-api",
|
||||||
|
"bitcoin": {
|
||||||
|
"connected": true,
|
||||||
|
"blocks": 12345
|
||||||
|
},
|
||||||
|
"timestamp": "2026-01-25T12:00:00.000Z"
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="response-example">
|
||||||
|
<h4>Réponse (503 Service Unavailable) - Bitcoin non connecté</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>{
|
||||||
|
"ok": false,
|
||||||
|
"service": "anchor-api",
|
||||||
|
"error": "Bitcoin RPC connection failed",
|
||||||
|
"timestamp": "2026-01-25T12:00:00.000Z"
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Endpoint: Anchor Document -->
|
||||||
|
<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/anchor/document</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint-description">
|
||||||
|
<p>Ancre un document sur la blockchain Bitcoin Signet en créant une transaction qui inclut le hash du document dans un OP_RETURN.</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">hash</td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><span class="param-required">Oui</span></td>
|
||||||
|
<td>Hash SHA256 du document en hexadécimal (64 caractères)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="param-name">documentUid</td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><span class="param-optional">Non</span></td>
|
||||||
|
<td>Identifiant optionnel du document (pour le logging)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="response-example">
|
||||||
|
<h4>Exemple de requête</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>curl -X POST https://certificator.4nkweb.com/api/anchor/document \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: votre-clé-api" \
|
||||||
|
-d '{
|
||||||
|
"hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
|
||||||
|
"documentUid": "doc-12345"
|
||||||
|
}'</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>{
|
||||||
|
"txid": "abc123def456...",
|
||||||
|
"status": "pending",
|
||||||
|
"confirmations": 0,
|
||||||
|
"block_height": null
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</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-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>
|
||||||
|
<li><span class="status-code status-500">500</span> Erreur serveur - Erreur interne lors de la création de la transaction</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-box">
|
||||||
|
<h4>Exemple d'erreur (402 Payment Required)</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>{
|
||||||
|
"error": "Insufficient Balance",
|
||||||
|
"message": "Insufficient balance to create anchor transaction"
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Endpoint: Verify -->
|
||||||
|
<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/anchor/verify</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint-description">
|
||||||
|
<p>Vérifie si un hash est ancré sur la blockchain Bitcoin Signet. Recherche dans les transactions OP_RETURN pour trouver le hash.</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">hash</td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><span class="param-required">Oui</span></td>
|
||||||
|
<td>Hash SHA256 à vérifier (64 caractères hex)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="param-name">txid</td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><span class="param-optional">Non</span></td>
|
||||||
|
<td>ID de transaction optionnel pour accélérer la recherche</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="response-example">
|
||||||
|
<h4>Exemple de requête</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>curl -X POST https://certificator.4nkweb.com/api/anchor/verify \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "X-API-Key: votre-clé-api" \
|
||||||
|
-d '{
|
||||||
|
"hash": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
|
||||||
|
"txid": "abc123def456..."
|
||||||
|
}'</pre>
|
||||||
|
</div>
|
||||||
|
<button class="copy-button" onclick="copyCode(this)">📋 Copier</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="response-example">
|
||||||
|
<h4>Réponse (200 OK) - Hash trouvé</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>{
|
||||||
|
"found": true,
|
||||||
|
"txid": "abc123def456...",
|
||||||
|
"block_height": 12345,
|
||||||
|
"confirmations": 100,
|
||||||
|
"timestamp": "2026-01-25T10:00:00.000Z"
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="response-example">
|
||||||
|
<h4>Réponse (200 OK) - Hash non trouvé</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>{
|
||||||
|
"found": false,
|
||||||
|
"txid": null,
|
||||||
|
"block_height": null,
|
||||||
|
"confirmations": null,
|
||||||
|
"timestamp": null
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</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 - Vérification effectuée</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-500">500</span> Erreur serveur - Erreur interne lors de la vérification</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Endpoint: Locked UTXOs -->
|
||||||
|
<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/locked-utxos</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint-description">
|
||||||
|
<p>Retourne la liste des UTXO actuellement verrouillés par le mutex de l'API. Cet endpoint est public et ne nécessite pas d'authentification.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="response-example">
|
||||||
|
<h4>Exemple de requête</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>curl -X GET https://certificator.4nkweb.com/api/anchor/locked-utxos</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>{
|
||||||
|
"locked": [
|
||||||
|
{
|
||||||
|
"txid": "abc123def456...",
|
||||||
|
"vout": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"txid": "def456abc123...",
|
||||||
|
"vout": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 2
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Endpoint: Faucet Request -->
|
||||||
|
<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/faucet/request</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint-description">
|
||||||
|
<p>Demande des sats (testnet coins) via le faucet. Distribue 50 000 sats (0.0005 BTC) par défaut sur une adresse Bitcoin Signet valide. Nécessite une clé API valide dans le header <code>x-api-key</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">address</td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><span class="param-required">Oui</span></td>
|
||||||
|
<td>Adresse Bitcoin Signet valide (commence par <code>tb1</code>, <code>bcrt1</code>, <code>2</code> ou <code>3</code>)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="response-example">
|
||||||
|
<h4>Exemple de requête</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>curl -X POST https://certificator.4nkweb.com/api/faucet/request \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: votre-clé-api" \
|
||||||
|
-d '{
|
||||||
|
"address": "tb1qwe0nv3s0ewedd63w20r8kwnv22uw8dp2tnj3qc"
|
||||||
|
}'</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>{
|
||||||
|
"success": true,
|
||||||
|
"txid": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890",
|
||||||
|
"address": "tb1qwe0nv3s0ewedd63w20r8kwnv22uw8dp2tnj3qc",
|
||||||
|
"amount": 0.0005,
|
||||||
|
"amount_sats": 50000,
|
||||||
|
"status": "pending",
|
||||||
|
"confirmations": 0,
|
||||||
|
"block_height": null
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</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-400">400</span> Requête invalide - Adresse manquante 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-503">503</span> Service indisponible - Solde insuffisant dans le wallet du faucet</li>
|
||||||
|
<li><span class="status-code status-500">500</span> Erreur serveur - Erreur interne lors de la création de la transaction</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-box">
|
||||||
|
<h4>Exemple d'erreur (401 Unauthorized)</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>{
|
||||||
|
"error": "Unauthorized",
|
||||||
|
"message": "Invalid or missing API key"
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-box">
|
||||||
|
<h4>Exemple d'erreur (503 Service Unavailable)</h4>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>{
|
||||||
|
"error": "Insufficient Balance",
|
||||||
|
"message": "Insufficient balance to send coins"
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h4>ℹ️ Notes importantes</h4>
|
||||||
|
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||||
|
<li>Le montant par défaut est de 50 000 sats (0.0005 BTC)</li>
|
||||||
|
<li>L'adresse doit être une adresse Bitcoin Signet valide</li>
|
||||||
|
<li>La transaction est envoyée au mempool immédiatement</li>
|
||||||
|
<li>Le statut "pending" signifie que la transaction est dans le mempool mais pas encore confirmée</li>
|
||||||
|
<li>Les confirmations augmentent à mesure que les blocs sont minés</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section Informations -->
|
||||||
|
<section class="api-docs-section">
|
||||||
|
<div class="endpoint-card">
|
||||||
|
<h2>ℹ️ Informations Complémentaires</h2>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h4>Format du Hash</h4>
|
||||||
|
<p>Le hash doit être un hash SHA256 en format hexadécimal, exactement 64 caractères.</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>Exemple valide: a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456
|
||||||
|
Longueur: 64 caractères
|
||||||
|
Format: hexadécimal (0-9, a-f, A-F)</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h4>Format de la Transaction</h4>
|
||||||
|
<p>Les transactions d'ancrage incluent :</p>
|
||||||
|
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||||
|
<li>Un output OP_RETURN contenant "ANCHOR:" suivi du hash</li>
|
||||||
|
<li>Un output d'ancrage de 2500 sats</li>
|
||||||
|
<li>7 outputs de provisionnement de 2500 sats chacun</li>
|
||||||
|
<li>Un output de change (si nécessaire)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h4>Base URL</h4>
|
||||||
|
<p>L'API est accessible à l'adresse :</p>
|
||||||
|
<div class="code-block">
|
||||||
|
<pre>https://certificator.4nkweb.com</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="warning-box">
|
||||||
|
<h4>⚠️ Notes importantes</h4>
|
||||||
|
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||||
|
<li>Les transactions sont envoyées au mempool immédiatement</li>
|
||||||
|
<li>Le statut "pending" signifie que la transaction est dans le mempool mais pas encore confirmée</li>
|
||||||
|
<li>Les confirmations augmentent à mesure que les blocs sont minés</li>
|
||||||
|
<li>En cas d'erreur 402 (Solde insuffisant), vous devez approvisionner le wallet de l'API</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Bitcoin Ancrage Dashboard - Équipe 4NK</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function copyCode(button) {
|
||||||
|
const codeBlock = button.previousElementSibling;
|
||||||
|
const code = codeBlock.querySelector('pre')?.textContent || codeBlock.textContent;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
const originalText = button.textContent;
|
||||||
|
button.textContent = '✅ Copié !';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = originalText;
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Erreur lors de la copie:', err);
|
||||||
|
alert('Erreur lors de la copie. Veuillez sélectionner et copier manuellement.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -655,8 +655,14 @@ async function anchorDocument() {
|
|||||||
* Demande des sats via le faucet
|
* Demande des sats via le faucet
|
||||||
*/
|
*/
|
||||||
async function requestFaucet() {
|
async function requestFaucet() {
|
||||||
|
const apiKey = document.getElementById('faucet-api-key').value.trim();
|
||||||
const address = document.getElementById('faucet-address').value.trim();
|
const address = document.getElementById('faucet-address').value.trim();
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
showResult('faucet-result', 'error', 'Veuillez entrer une clé API.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!address) {
|
if (!address) {
|
||||||
showResult('faucet-result', 'error', 'Veuillez entrer une adresse Bitcoin.');
|
showResult('faucet-result', 'error', 'Veuillez entrer une adresse Bitcoin.');
|
||||||
return;
|
return;
|
||||||
@ -676,6 +682,7 @@ async function requestFaucet() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': apiKey,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ address }),
|
body: JSON.stringify({ address }),
|
||||||
});
|
});
|
||||||
|
|||||||
197
signet-dashboard/public/hash-list.html
Normal file
197
signet-dashboard/public/hash-list.html
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Liste des Hash Ancrés - Bitcoin Ancrage Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>
|
||||||
|
.hash-list-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.header-section h1 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header-section .back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.header-section .back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.info-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.info-section p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
.hash-cell {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.txid-cell {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.txid-link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.txid-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.refresh-button {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.refresh-button:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
.refresh-button:disabled {
|
||||||
|
background: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="hash-list-container">
|
||||||
|
<div class="header-section">
|
||||||
|
<a href="/" class="back-link">← Retour au dashboard</a>
|
||||||
|
<h1>Liste des Hash Ancrés</h1>
|
||||||
|
<div class="info-section">
|
||||||
|
<p><strong>Total de hash ancrés :</strong> <span id="hash-count">-</span></p>
|
||||||
|
<p><strong>Dernière mise à jour :</strong> <span id="last-update">-</span></p>
|
||||||
|
<button class="refresh-button" onclick="loadHashList()">Actualiser</button>
|
||||||
|
<a href="/api/hash/list.txt" download="hash_list.txt" style="margin-left: 10px; color: #007bff; text-decoration: none;">📥 Télécharger le fichier texte</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<div class="loading">Chargement des hash...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE_URL = window.location.origin;
|
||||||
|
|
||||||
|
// Charger la liste au chargement de la page
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadHashList();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadHashList() {
|
||||||
|
const contentDiv = document.getElementById('content');
|
||||||
|
const refreshButton = document.querySelector('.refresh-button');
|
||||||
|
|
||||||
|
refreshButton.disabled = true;
|
||||||
|
contentDiv.innerHTML = '<div class="loading">Chargement des hash...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/hash/list`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const hashes = data.hashes || [];
|
||||||
|
|
||||||
|
document.getElementById('hash-count').textContent = hashes.length.toLocaleString('fr-FR');
|
||||||
|
updateLastUpdateTime();
|
||||||
|
|
||||||
|
if (hashes.length === 0) {
|
||||||
|
contentDiv.innerHTML = '<div class="info-section"><p>Aucun hash ancré pour le moment.</p></div>';
|
||||||
|
} else {
|
||||||
|
let tableHTML = '<div class="table-container"><table><thead><tr>';
|
||||||
|
tableHTML += '<th>Hash</th>';
|
||||||
|
tableHTML += '<th>Transaction ID</th>';
|
||||||
|
tableHTML += '<th>Hauteur du Bloc</th>';
|
||||||
|
tableHTML += '<th>Confirmations</th>';
|
||||||
|
tableHTML += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
hashes.forEach((item) => {
|
||||||
|
const mempoolUrl = 'https://mempool.4nkweb.com/fr';
|
||||||
|
const txLink = `${mempoolUrl}/tx/${item.txid}`;
|
||||||
|
|
||||||
|
tableHTML += '<tr>';
|
||||||
|
tableHTML += `<td class="hash-cell">${item.hash}</td>`;
|
||||||
|
tableHTML += `<td class="txid-cell"><a href="${txLink}" target="_blank" rel="noopener noreferrer" class="txid-link">${item.txid}</a></td>`;
|
||||||
|
tableHTML += `<td>${item.blockHeight !== null ? item.blockHeight.toLocaleString('fr-FR') : '-'}</td>`;
|
||||||
|
tableHTML += `<td>${item.confirmations.toLocaleString('fr-FR')}</td>`;
|
||||||
|
tableHTML += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
tableHTML += '</tbody></table></div>';
|
||||||
|
contentDiv.innerHTML = tableHTML;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading hash list:', error);
|
||||||
|
contentDiv.innerHTML = `<div class="error">Erreur lors du chargement de la liste des hash : ${error.message}</div>`;
|
||||||
|
} finally {
|
||||||
|
refreshButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLastUpdateTime() {
|
||||||
|
const now = new Date();
|
||||||
|
document.getElementById('last-update').textContent = now.toLocaleString('fr-FR');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -15,6 +15,18 @@
|
|||||||
<a href="https://mempool.4nkweb.com/fr/" target="_blank" rel="noopener noreferrer" class="external-link">
|
<a href="https://mempool.4nkweb.com/fr/" target="_blank" rel="noopener noreferrer" class="external-link">
|
||||||
🔗 Explorer Mempool
|
🔗 Explorer Mempool
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/hash-list" class="external-link">
|
||||||
|
📋 Liste des Hash
|
||||||
|
</a>
|
||||||
|
<a href="/utxo-list" class="external-link">
|
||||||
|
✅ Liste des UTXO
|
||||||
|
</a>
|
||||||
|
<a href="/join-signet" class="external-link">
|
||||||
|
🔗 Rejoindre le Réseau
|
||||||
|
</a>
|
||||||
|
<a href="/api-docs" class="external-link">
|
||||||
|
📚 Documentation API
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -122,9 +134,11 @@
|
|||||||
<h2>Faucet Bitcoin Ancrage</h2>
|
<h2>Faucet Bitcoin Ancrage</h2>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p>Recevez 50 000 ✅ (0.0005 🛡) sur votre adresse Bitcoin Ancrage</p>
|
<p>Recevez 50 000 ✅ (0.0005 🛡) sur votre adresse Bitcoin Ancrage</p>
|
||||||
|
<label for="faucet-api-key">Clé API :</label>
|
||||||
|
<input type="text" id="faucet-api-key" placeholder="Entrez votre clé API">
|
||||||
<label for="faucet-address">Adresse Bitcoin :</label>
|
<label for="faucet-address">Adresse Bitcoin :</label>
|
||||||
<input type="text" id="faucet-address" placeholder="tb1q..." pattern="^(tb1|bcrt1|2|3)[a-zA-HJ-NP-Z0-9]{25,62}$">
|
<input type="text" id="faucet-address" placeholder="tb1q..." pattern="^(tb1|bcrt1|2|3)[a-zA-HJ-NP-Z0-9]{25,62}$">
|
||||||
<button onclick="requestFaucet()">Demander des Sats</button>
|
<button onclick="requestFaucet()">Demander des ✅</button>
|
||||||
<div id="faucet-result" class="result"></div>
|
<div id="faucet-result" class="result"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
428
signet-dashboard/public/join-signet.html
Normal file
428
signet-dashboard/public/join-signet.html
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Rejoindre le Réseau Signet Custom - Bitcoin Ancrage</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Vérifier que la bibliothèque QRCode est chargée
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
if (typeof QRCode === 'undefined') {
|
||||||
|
console.error('Bibliothèque QRCode non chargée');
|
||||||
|
} else {
|
||||||
|
console.log('Bibliothèque QRCode chargée');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.join-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
background: var(--card-background);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section h2 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-code {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
margin-top: 10px;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background: #e0820d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-section {
|
||||||
|
background: var(--card-background);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-section h2 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-info {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-amount {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-address {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 15px 0;
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-container {
|
||||||
|
margin: 30px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qrcode {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-section {
|
||||||
|
background: var(--card-background);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-section h2 {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-checkbox input[type="checkbox"] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-checkbox label {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-left: 4px solid #2196F3;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background: #d4edda;
|
||||||
|
border-left: 4px solid var(--success-color);
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #155724;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<a href="/" class="back-link">← Retour au Dashboard</a>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Rejoindre le Réseau Signet Custom</h1>
|
||||||
|
<p class="subtitle">Configuration et accès au réseau Bitcoin Signet personnalisé</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<!-- Section Configuration -->
|
||||||
|
<section class="join-section">
|
||||||
|
<div class="config-section">
|
||||||
|
<h2>📋 Configuration bitcoin.conf</h2>
|
||||||
|
<p>Copiez cette configuration dans votre fichier <code>bitcoin.conf</code> pour rejoindre le réseau Signet custom :</p>
|
||||||
|
|
||||||
|
<div class="config-code" id="bitcoin-config">signet=1
|
||||||
|
txindex=1
|
||||||
|
blockfilterindex=1
|
||||||
|
peerblockfilters=1
|
||||||
|
coinstatsindex=1
|
||||||
|
dnsseed=0
|
||||||
|
persistmempool=1
|
||||||
|
uacomment=CustomSignet
|
||||||
|
|
||||||
|
[signet]
|
||||||
|
daemon=1
|
||||||
|
listen=1
|
||||||
|
server=1
|
||||||
|
discover=1
|
||||||
|
signetchallenge=5121028b8d4cea1b3d8582babc8405bc618fbbb281c0f64e6561aa85968251931cd0a651ae
|
||||||
|
rpcbind=0.0.0.0:38332
|
||||||
|
rpcallowip=0.0.0.0/0
|
||||||
|
whitelist=0.0.0.0/0
|
||||||
|
fallbackfee=0.0002
|
||||||
|
addnode=anchorage.certificator.4nkweb.com:38333</div>
|
||||||
|
|
||||||
|
<button class="copy-button" onclick="copyConfig()">📋 Copier la configuration</button>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>Note importante :</strong></p>
|
||||||
|
<p>• Remplacez <code>rpcuser</code> et <code>rpcpassword</code> par vos propres identifiants RPC</p>
|
||||||
|
<p>• Le port P2P par défaut est <code>38333</code></p>
|
||||||
|
<p>• Le port RPC par défaut est <code>38332</code></p>
|
||||||
|
<p>• L'adresse <code>anchorage.certificator.4nkweb.com:38333</code> est le nœud principal du réseau</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section Paiement -->
|
||||||
|
<section class="join-section">
|
||||||
|
<div class="payment-section">
|
||||||
|
<h2>💳 Accès au Wallet de Mining</h2>
|
||||||
|
<p>Pour recevoir le wallet de mining et les clés nécessaires pour miner sur le réseau, effectuez un paiement de :</p>
|
||||||
|
|
||||||
|
<div class="payment-amount">0,0065 BTC</div>
|
||||||
|
|
||||||
|
<p>à l'adresse suivante :</p>
|
||||||
|
|
||||||
|
<div class="payment-address" id="payment-address">bc1qerauk5yhqytl6z93ckvwkylup8s0256uenzg9y</div>
|
||||||
|
|
||||||
|
<button class="copy-button" onclick="copyAddress()">📋 Copier l'adresse</button>
|
||||||
|
|
||||||
|
<div class="qr-code-container">
|
||||||
|
<div id="qrcode"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wallet-checkbox">
|
||||||
|
<input type="checkbox" id="wallet-request" onchange="updatePaymentMessage()">
|
||||||
|
<label for="wallet-request">Je souhaite recevoir le wallet de mining après le paiement</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box" id="payment-info">
|
||||||
|
<p><strong>Instructions :</strong></p>
|
||||||
|
<p>1. Effectuez le paiement de 0,0065 BTC à l'adresse ci-dessus</p>
|
||||||
|
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir le wallet de mining</p>
|
||||||
|
<p>3. Après confirmation du paiement, vous recevrez les informations nécessaires</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="success-message" id="payment-success">
|
||||||
|
<p><strong>✅ Paiement reçu !</strong></p>
|
||||||
|
<p>Votre demande a été enregistrée. Vous recevrez le wallet de mining sous peu.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section Informations Supplémentaires -->
|
||||||
|
<section class="join-section">
|
||||||
|
<div class="wallet-section">
|
||||||
|
<h2>ℹ️ Informations Supplémentaires</h2>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>Qu'est-ce que le wallet de mining ?</strong></p>
|
||||||
|
<p>Le wallet de mining contient :</p>
|
||||||
|
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||||
|
<li>La clé privée (PRIVKEY) nécessaire pour miner des blocs</li>
|
||||||
|
<li>Les paramètres de configuration pour le mining (NBITS, BLOCKPRODUCTIONDELAY)</li>
|
||||||
|
<li>Les instructions complètes pour configurer votre nœud en mode mining</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>Que se passe-t-il après le paiement ?</strong></p>
|
||||||
|
<p>Une fois le paiement confirmé (généralement après 1 confirmation), vous recevrez par email :</p>
|
||||||
|
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||||
|
<li>Les fichiers de configuration complets</li>
|
||||||
|
<li>La clé privée du signet (si vous avez coché la case)</li>
|
||||||
|
<li>Les instructions détaillées pour démarrer votre nœud</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>Besoin d'aide ?</strong></p>
|
||||||
|
<p>Pour toute question, consultez la documentation complète dans le dépôt GitHub ou contactez l'équipe.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>Bitcoin Ancrage Dashboard - Équipe 4NK</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const PAYMENT_ADDRESS = 'bc1qerauk5yhqytl6z93ckvwkylup8s0256uenzg9y';
|
||||||
|
const PAYMENT_AMOUNT = 0.0065;
|
||||||
|
|
||||||
|
// Générer le QR code au chargement de la page
|
||||||
|
function initQRCode() {
|
||||||
|
// Attendre que le DOM soit prêt et que la bibliothèque soit chargée
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', generateQRCode);
|
||||||
|
} else {
|
||||||
|
// Si le DOM est déjà chargé, attendre un peu pour la bibliothèque
|
||||||
|
setTimeout(generateQRCode, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateQRCode() {
|
||||||
|
const qrElement = document.getElementById('qrcode');
|
||||||
|
if (!qrElement) {
|
||||||
|
console.error('Élément QR code non trouvé');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que la bibliothèque QRCode est disponible
|
||||||
|
if (typeof QRCode === 'undefined') {
|
||||||
|
console.error('Bibliothèque QRCode non disponible, réessai...');
|
||||||
|
setTimeout(generateQRCode, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentURI = `bitcoin:${PAYMENT_ADDRESS}?amount=${PAYMENT_AMOUNT}`;
|
||||||
|
|
||||||
|
// Vider l'élément avant de générer le QR code
|
||||||
|
qrElement.innerHTML = '';
|
||||||
|
|
||||||
|
// Utiliser toCanvas avec l'élément directement (QRCode créera le canvas)
|
||||||
|
QRCode.toCanvas(qrElement, paymentURI, {
|
||||||
|
width: 300,
|
||||||
|
margin: 2,
|
||||||
|
color: {
|
||||||
|
dark: '#000000',
|
||||||
|
light: '#FFFFFF'
|
||||||
|
}
|
||||||
|
}, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('Erreur lors de la génération du QR code:', error);
|
||||||
|
qrElement.innerHTML = '<p style="color: red; padding: 20px;">Erreur lors de la génération du QR code. Veuillez recharger la page.</p>';
|
||||||
|
} else {
|
||||||
|
console.log('QR code généré avec succès');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialiser le QR code
|
||||||
|
initQRCode();
|
||||||
|
|
||||||
|
function copyConfig() {
|
||||||
|
const configText = document.getElementById('bitcoin-config')?.textContent || '';
|
||||||
|
navigator.clipboard.writeText(configText).then(() => {
|
||||||
|
const button = event?.target;
|
||||||
|
if (button) {
|
||||||
|
const originalText = button.textContent;
|
||||||
|
button.textContent = '✅ Copié !';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = originalText;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Erreur lors de la copie:', err);
|
||||||
|
alert('Erreur lors de la copie. Veuillez sélectionner et copier manuellement.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyAddress() {
|
||||||
|
navigator.clipboard.writeText(PAYMENT_ADDRESS).then(() => {
|
||||||
|
const button = event?.target;
|
||||||
|
if (button) {
|
||||||
|
const originalText = button.textContent;
|
||||||
|
button.textContent = '✅ Copié !';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = originalText;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Erreur lors de la copie:', err);
|
||||||
|
alert('Erreur lors de la copie. Veuillez sélectionner et copier manuellement.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePaymentMessage() {
|
||||||
|
const checkbox = document.getElementById('wallet-request');
|
||||||
|
const paymentInfo = document.getElementById('payment-info');
|
||||||
|
|
||||||
|
if (checkbox && paymentInfo) {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
paymentInfo.innerHTML = `
|
||||||
|
<p><strong>Instructions :</strong></p>
|
||||||
|
<p>1. Effectuez le paiement de 0,0065 BTC à l'adresse ci-dessus</p>
|
||||||
|
<p>2. ✅ Vous recevrez le wallet de mining après confirmation du paiement</p>
|
||||||
|
<p>3. Les informations vous seront envoyées par email</p>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
paymentInfo.innerHTML = `
|
||||||
|
<p><strong>Instructions :</strong></p>
|
||||||
|
<p>1. Effectuez le paiement de 0,0065 BTC à l'adresse ci-dessus</p>
|
||||||
|
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir le wallet de mining</p>
|
||||||
|
<p>3. Après confirmation du paiement, vous recevrez les informations nécessaires</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
539
signet-dashboard/public/utxo-list.html
Normal file
539
signet-dashboard/public/utxo-list.html
Normal file
@ -0,0 +1,539 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Liste des UTXO - Bitcoin Ancrage Dashboard</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>
|
||||||
|
.utxo-list-container {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.header-section h1 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header-section .back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.header-section .back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.info-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.info-section p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.category-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
.category-header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.category-header.bloc-rewards {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
.category-header.ancrages {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
.category-header.changes {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
}
|
||||||
|
.category-header.fees {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
.category-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
.category-stats {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.category-stats span {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
.txid-cell {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.txid-link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.txid-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.address-cell {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.amount-cell {
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.refresh-button {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.refresh-button:hover {
|
||||||
|
background: #218838;
|
||||||
|
}
|
||||||
|
.refresh-button:disabled {
|
||||||
|
background: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.total-amount {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
.empty-message {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
}
|
||||||
|
.status-spent {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status-locked {
|
||||||
|
color: #ffc107;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status-available {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.navigation-bar {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.navigation-bar a {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: white;
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 2px solid #007bff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.navigation-bar a:hover {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.category-section {
|
||||||
|
scroll-margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="utxo-list-container">
|
||||||
|
<div class="header-section">
|
||||||
|
<a href="/" class="back-link">← Retour au dashboard</a>
|
||||||
|
<h1>Liste des UTXO</h1>
|
||||||
|
<div class="info-section">
|
||||||
|
<p><strong>Total d'UTXO :</strong> <span id="utxo-count">-</span></p>
|
||||||
|
<p><strong>Capacité d'ancrage restante :</strong> <span id="available-for-anchor">-</span> ancrages</p>
|
||||||
|
<p><strong>Montant total :</strong> <span id="total-amount" class="total-amount">-</span></p>
|
||||||
|
<p><strong>Dernière mise à jour :</strong> <span id="last-update">-</span></p>
|
||||||
|
<button class="refresh-button" onclick="loadUtxoList()">Actualiser</button>
|
||||||
|
<a href="/api/utxo/list.txt" download="utxo_list.txt" style="margin-left: 10px; color: #007bff; text-decoration: none;">📥 Télécharger le fichier texte</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="navigation" class="navigation-bar" style="display: none;">
|
||||||
|
<a href="#bloc-rewards">💰 Bloc Rewards</a>
|
||||||
|
<a href="#ancrages">🔗 Ancrages</a>
|
||||||
|
<a href="#changes">🔄 Changes</a>
|
||||||
|
<a href="#fees">💸 Frais</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<div class="loading">Chargement des UTXO...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_BASE_URL = window.location.origin;
|
||||||
|
|
||||||
|
// Charger la liste au chargement de la page
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadUtxoList();
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderTable(utxos, categoryName, categoryLabel) {
|
||||||
|
if (utxos.length === 0) {
|
||||||
|
return `
|
||||||
|
<div class="category-section" id="${categoryName}">
|
||||||
|
<div class="category-header ${categoryName}">
|
||||||
|
<h2>${categoryLabel}</h2>
|
||||||
|
<div class="category-stats">
|
||||||
|
<span><strong>Nombre :</strong> 0</span>
|
||||||
|
<span><strong>Montant total :</strong> 0 🛡</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="empty-message">Aucun UTXO dans cette catégorie.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalAmount = utxos.reduce((sum, utxo) => sum + utxo.amount, 0);
|
||||||
|
const totalSats = Math.round(totalAmount * 100000000);
|
||||||
|
const isAnchors = categoryName === 'ancrages';
|
||||||
|
const isBlocRewards = categoryName === 'bloc-rewards';
|
||||||
|
const isChanges = categoryName === 'changes';
|
||||||
|
|
||||||
|
// Pour les changes, compter les changes d'ancrage
|
||||||
|
let anchorChangesCount = 0;
|
||||||
|
let anchorChangesAmount = 0;
|
||||||
|
if (isChanges) {
|
||||||
|
const anchorChanges = utxos.filter(utxo => utxo.isAnchorChange);
|
||||||
|
anchorChangesCount = anchorChanges.length;
|
||||||
|
anchorChangesAmount = anchorChanges.reduce((sum, utxo) => sum + utxo.amount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tableHTML = `
|
||||||
|
<div class="category-section" id="${categoryName}">
|
||||||
|
<div class="category-header ${categoryName}">
|
||||||
|
<h2>${categoryLabel}</h2>
|
||||||
|
<div class="category-stats">
|
||||||
|
<span><strong>Nombre :</strong> ${utxos.length.toLocaleString('fr-FR')}</span>
|
||||||
|
<span><strong>Montant total :</strong> ${formatBTC(totalAmount)} (${totalSats.toLocaleString('fr-FR')} ✅)</span>
|
||||||
|
${isChanges && anchorChangesCount > 0 ? `<span><strong>Changes d'ancrage :</strong> ${anchorChangesCount} (${formatBTC(anchorChangesAmount)})</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Transaction ID</th>
|
||||||
|
<th>Vout</th>
|
||||||
|
<th>Adresse</th>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (isAnchors) {
|
||||||
|
tableHTML += `
|
||||||
|
<th>Numéro de bloc</th>
|
||||||
|
<th style="text-align: right;">Montant (✅)</th>
|
||||||
|
<th>Confirmations</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
`;
|
||||||
|
} else if (isBlocRewards) {
|
||||||
|
tableHTML += `
|
||||||
|
<th style="text-align: right;">Montant (🛡)</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Confirmations</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Pour la section changes, ajouter une colonne pour indiquer si c'est un change d'ancrage
|
||||||
|
if (categoryName === 'changes') {
|
||||||
|
tableHTML += `
|
||||||
|
<th style="text-align: right;">Montant (🛡)</th>
|
||||||
|
<th style="text-align: right;">Montant (✅)</th>
|
||||||
|
<th>Confirmations</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
tableHTML += `
|
||||||
|
<th style="text-align: right;">Montant (🛡)</th>
|
||||||
|
<th style="text-align: right;">Montant (✅)</th>
|
||||||
|
<th>Confirmations</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHTML += `
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
utxos.forEach((utxo) => {
|
||||||
|
const mempoolUrl = 'https://mempool.4nkweb.com/fr';
|
||||||
|
const txLink = `${mempoolUrl}/tx/${utxo.txid}`;
|
||||||
|
const amountSats = Math.round(utxo.amount * 100000000);
|
||||||
|
const amountBTC = Math.round(utxo.amount);
|
||||||
|
|
||||||
|
tableHTML += '<tr>';
|
||||||
|
tableHTML += `<td class="txid-cell"><a href="${txLink}" target="_blank" rel="noopener noreferrer" class="txid-link">${utxo.txid}</a></td>`;
|
||||||
|
tableHTML += `<td>${utxo.vout}</td>`;
|
||||||
|
tableHTML += `<td class="address-cell">${utxo.address || '-'}</td>`;
|
||||||
|
|
||||||
|
if (isAnchors) {
|
||||||
|
tableHTML += `<td>${utxo.blockHeight !== null && utxo.blockHeight !== undefined ? utxo.blockHeight.toLocaleString('fr-FR') : '-'}</td>`;
|
||||||
|
tableHTML += `<td class="amount-cell">${amountSats.toLocaleString('fr-FR')} ✅</td>`;
|
||||||
|
} else if (isBlocRewards) {
|
||||||
|
tableHTML += `<td class="amount-cell">${amountBTC.toLocaleString('fr-FR')} 🛡</td>`;
|
||||||
|
if (utxo.blockTime) {
|
||||||
|
const date = new Date(utxo.blockTime * 1000);
|
||||||
|
tableHTML += `<td>${date.toLocaleString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })}</td>`;
|
||||||
|
} else {
|
||||||
|
tableHTML += '<td>-</td>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tableHTML += `<td class="amount-cell">${amountBTC.toLocaleString('fr-FR')} 🛡</td>`;
|
||||||
|
tableHTML += `<td class="amount-cell">${amountSats.toLocaleString('fr-FR')} ✅</td>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHTML += `<td>${utxo.confirmations.toLocaleString('fr-FR')}</td>`;
|
||||||
|
|
||||||
|
// Colonne Type (uniquement pour les changes)
|
||||||
|
if (categoryName === 'changes') {
|
||||||
|
const changeType = utxo.isAnchorChange ? '🔗 Transaction d\'ancrage' : '🔄 Transaction normale';
|
||||||
|
tableHTML += `<td>${changeType}</td>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colonne Statut
|
||||||
|
let statusText = '';
|
||||||
|
let statusClass = '';
|
||||||
|
if (utxo.isSpentOnchain) {
|
||||||
|
statusText = 'Dépensé onchain';
|
||||||
|
statusClass = 'status-spent';
|
||||||
|
} else if (utxo.isLockedInMutex) {
|
||||||
|
statusText = 'Verrouillé';
|
||||||
|
statusClass = 'status-locked';
|
||||||
|
} else {
|
||||||
|
statusText = 'Disponible';
|
||||||
|
statusClass = 'status-available';
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHTML += `<td class="${statusClass}">${statusText}</td>`;
|
||||||
|
tableHTML += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
tableHTML += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return tableHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUtxoList() {
|
||||||
|
const contentDiv = document.getElementById('content');
|
||||||
|
const refreshButton = document.querySelector('.refresh-button');
|
||||||
|
|
||||||
|
refreshButton.disabled = true;
|
||||||
|
contentDiv.innerHTML = '<div class="loading">Chargement des UTXO...</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/utxo/list`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const counts = data.counts || {};
|
||||||
|
const blocRewards = data.blocRewards || [];
|
||||||
|
const anchors = data.anchors || [];
|
||||||
|
const changes = data.changes || [];
|
||||||
|
const fees = data.fees || [];
|
||||||
|
|
||||||
|
document.getElementById('utxo-count').textContent = counts.total.toLocaleString('fr-FR');
|
||||||
|
document.getElementById('available-for-anchor').textContent = (counts.availableForAnchor || 0).toLocaleString('fr-FR');
|
||||||
|
|
||||||
|
// Calculer le montant total
|
||||||
|
const totalAmount = blocRewards.reduce((sum, utxo) => sum + utxo.amount, 0) +
|
||||||
|
anchors.reduce((sum, utxo) => sum + utxo.amount, 0) +
|
||||||
|
changes.reduce((sum, utxo) => sum + utxo.amount, 0);
|
||||||
|
document.getElementById('total-amount').textContent = formatBTC(totalAmount);
|
||||||
|
|
||||||
|
updateLastUpdateTime();
|
||||||
|
|
||||||
|
// Afficher la barre de navigation
|
||||||
|
document.getElementById('navigation').style.display = 'flex';
|
||||||
|
|
||||||
|
// Afficher les 4 listes
|
||||||
|
let html = '';
|
||||||
|
html += renderTable(blocRewards, 'bloc-rewards', '💰 Bloc Rewards (Récompenses de minage)');
|
||||||
|
html += renderTable(anchors, 'ancrages', '🔗 Ancrages (Transactions d\'ancrage)');
|
||||||
|
html += renderTable(changes, 'changes', '🔄 Changes (Monnaie de retour)');
|
||||||
|
html += renderFeesTable(fees, 'fees', '💸 Frais (Transactions d\'ancrage)');
|
||||||
|
|
||||||
|
contentDiv.innerHTML = html;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading UTXO list:', error);
|
||||||
|
contentDiv.innerHTML = `<div class="error">Erreur lors du chargement de la liste des UTXO : ${error.message}</div>`;
|
||||||
|
} finally {
|
||||||
|
refreshButton.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBTC(btc) {
|
||||||
|
if (btc === 0) return '0 🛡';
|
||||||
|
// Arrondir sans décimales
|
||||||
|
const roundedBTC = Math.round(btc);
|
||||||
|
if (roundedBTC === 0 && btc > 0) {
|
||||||
|
// Si arrondi à 0 mais qu'il y a un montant, afficher en sats
|
||||||
|
const sats = Math.round(btc * 100000000);
|
||||||
|
return `${sats.toLocaleString('fr-FR')} ✅`;
|
||||||
|
}
|
||||||
|
return `${roundedBTC.toLocaleString('fr-FR')} 🛡`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFeesTable(fees, categoryName, categoryLabel) {
|
||||||
|
if (fees.length === 0) {
|
||||||
|
return `
|
||||||
|
<div class="category-section" id="${categoryName}">
|
||||||
|
<div class="category-header ${categoryName}">
|
||||||
|
<h2>${categoryLabel}</h2>
|
||||||
|
<div class="category-stats">
|
||||||
|
<span><strong>Nombre :</strong> 0</span>
|
||||||
|
<span><strong>Total des frais :</strong> 0 ✅</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="empty-message">Aucune transaction avec frais onchain enregistrée.</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalFees = fees.reduce((sum, fee) => sum + fee.fee, 0);
|
||||||
|
const totalFeesSats = Math.round(totalFees * 100000000);
|
||||||
|
|
||||||
|
let tableHTML = `
|
||||||
|
<div class="category-section" id="${categoryName}">
|
||||||
|
<div class="category-header ${categoryName}">
|
||||||
|
<h2>${categoryLabel}</h2>
|
||||||
|
<div class="category-stats">
|
||||||
|
<span><strong>Nombre :</strong> ${fees.length.toLocaleString('fr-FR')}</span>
|
||||||
|
<span><strong>Total des frais :</strong> ${formatBTC(totalFees)} (${totalFeesSats.toLocaleString('fr-FR')} ✅)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Transaction ID</th>
|
||||||
|
<th>Frais (BTC)</th>
|
||||||
|
<th>Frais (✅)</th>
|
||||||
|
<th>Change Address</th>
|
||||||
|
<th>Change Amount</th>
|
||||||
|
<th>Bloc</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Confirmations</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fees.forEach((fee) => {
|
||||||
|
const mempoolUrl = 'https://mempool.4nkweb.com/fr';
|
||||||
|
const txLink = `${mempoolUrl}/tx/${fee.txid}`;
|
||||||
|
const feeSats = fee.fee_sats || Math.round(fee.fee * 100000000);
|
||||||
|
|
||||||
|
tableHTML += '<tr>';
|
||||||
|
tableHTML += `<td class="txid-cell"><a href="${txLink}" target="_blank" rel="noopener noreferrer" class="txid-link">${fee.txid}</a></td>`;
|
||||||
|
tableHTML += `<td class="amount-cell">${fee.fee.toFixed(8)}</td>`;
|
||||||
|
tableHTML += `<td class="amount-cell">${feeSats.toLocaleString('fr-FR')} ✅</td>`;
|
||||||
|
tableHTML += `<td class="address-cell">${fee.changeAddress || '-'}</td>`;
|
||||||
|
tableHTML += `<td class="amount-cell">${fee.changeAmount ? formatBTC(fee.changeAmount) : '-'}</td>`;
|
||||||
|
tableHTML += `<td>${fee.blockHeight !== null && fee.blockHeight !== undefined ? fee.blockHeight.toLocaleString('fr-FR') : '-'}</td>`;
|
||||||
|
if (fee.blockTime) {
|
||||||
|
const date = new Date(fee.blockTime * 1000);
|
||||||
|
tableHTML += `<td>${date.toLocaleString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' })}</td>`;
|
||||||
|
} else {
|
||||||
|
tableHTML += '<td>-</td>';
|
||||||
|
}
|
||||||
|
tableHTML += `<td>${fee.confirmations.toLocaleString('fr-FR')}</td>`;
|
||||||
|
tableHTML += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
tableHTML += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return tableHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLastUpdateTime() {
|
||||||
|
const now = new Date();
|
||||||
|
document.getElementById('last-update').textContent = now.toLocaleString('fr-FR');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -9,6 +9,7 @@ import { logger } from './logger.js';
|
|||||||
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
|
||||||
class BitcoinRPC {
|
class BitcoinRPC {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -143,6 +144,594 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient la liste des hash ancrés avec leurs transactions
|
||||||
|
* Utilise un fichier de cache hash_list.txt pour éviter de tout recompter
|
||||||
|
* Format du cache: <date>;<hauteur du dernier bloc>;<hash du dernier bloc>
|
||||||
|
* Format du fichier de sortie: <hash>;<txid>;<block_height>;<confirmations>
|
||||||
|
* @returns {Promise<Array<Object>>} Liste des hash avec leurs transactions
|
||||||
|
*/
|
||||||
|
async getHashList() {
|
||||||
|
try {
|
||||||
|
const blockchainInfo = await this.client.getBlockchainInfo();
|
||||||
|
const currentHeight = blockchainInfo.blocks;
|
||||||
|
const currentBlockHash = blockchainInfo.bestblockhash;
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const cachePath = join(__dirname, '../../hash_list_cache.txt');
|
||||||
|
const outputPath = join(__dirname, '../../hash_list.txt');
|
||||||
|
|
||||||
|
let startHeight = 0;
|
||||||
|
const hashList = [];
|
||||||
|
|
||||||
|
// Lire le cache si il existe
|
||||||
|
if (existsSync(cachePath)) {
|
||||||
|
try {
|
||||||
|
const cacheContent = readFileSync(cachePath, 'utf8').trim();
|
||||||
|
const parts = cacheContent.split(';');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const cachedHeight = parseInt(parts[1], 10);
|
||||||
|
const cachedHash = parts[2];
|
||||||
|
|
||||||
|
if (cachedHeight >= 0 && cachedHeight <= currentHeight) {
|
||||||
|
try {
|
||||||
|
const cachedBlockHash = await this.client.getBlockHash(cachedHeight);
|
||||||
|
if (cachedBlockHash === cachedHash) {
|
||||||
|
startHeight = cachedHeight + 1;
|
||||||
|
// Charger les hash existants depuis le fichier de sortie
|
||||||
|
if (existsSync(outputPath)) {
|
||||||
|
const existingContent = readFileSync(outputPath, 'utf8').trim();
|
||||||
|
const lines = existingContent.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
const [hash, txid, blockHeight, confirmations] = line.split(';');
|
||||||
|
if (hash && txid) {
|
||||||
|
hashList.push({
|
||||||
|
hash,
|
||||||
|
txid,
|
||||||
|
blockHeight: blockHeight ? parseInt(blockHeight, 10) : null,
|
||||||
|
confirmations: confirmations ? parseInt(confirmations, 10) : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info('Hash list cache loaded', {
|
||||||
|
cachedHeight,
|
||||||
|
cachedCount: hashList.length,
|
||||||
|
startHeight,
|
||||||
|
currentHeight,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn('Hash list cache invalid: block hash mismatch', {
|
||||||
|
cachedHeight,
|
||||||
|
cachedHash,
|
||||||
|
actualHash: cachedBlockHash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error verifying cached block hash', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error reading hash list cache', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parcourir les blocs depuis startHeight jusqu'à currentHeight
|
||||||
|
if (startHeight <= currentHeight) {
|
||||||
|
logger.info('Collecting hash list from block', { startHeight, currentHeight });
|
||||||
|
|
||||||
|
for (let height = startHeight; height <= currentHeight; height++) {
|
||||||
|
try {
|
||||||
|
const blockHash = await this.client.getBlockHash(height);
|
||||||
|
const block = await this.client.getBlock(blockHash, 2);
|
||||||
|
|
||||||
|
if (block.tx) {
|
||||||
|
for (const tx of block.tx) {
|
||||||
|
try {
|
||||||
|
const rawTx = await this.client.getRawTransaction(tx.txid, true);
|
||||||
|
|
||||||
|
// Vérifier si la transaction contient un OP_RETURN avec "ANCHOR:"
|
||||||
|
for (const output of rawTx.vout || []) {
|
||||||
|
if (output.scriptPubKey && output.scriptPubKey.hex) {
|
||||||
|
const scriptHex = output.scriptPubKey.hex;
|
||||||
|
const anchorPrefix = Buffer.from('ANCHOR:', 'utf8').toString('hex');
|
||||||
|
|
||||||
|
if (scriptHex.includes(anchorPrefix)) {
|
||||||
|
// Extraire le hash depuis le script
|
||||||
|
const hashStart = scriptHex.indexOf(anchorPrefix) + anchorPrefix.length;
|
||||||
|
const hashHex = scriptHex.substring(hashStart, hashStart + 64);
|
||||||
|
|
||||||
|
if (/^[0-9a-fA-F]{64}$/.test(hashHex)) {
|
||||||
|
const confirmations = currentHeight - height + 1;
|
||||||
|
hashList.push({
|
||||||
|
hash: hashHex.toLowerCase(),
|
||||||
|
txid: tx.txid,
|
||||||
|
blockHeight: height,
|
||||||
|
confirmations,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break; // Un seul hash par transaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Error checking transaction for hash', { txid: tx.txid, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le cache tous les 100 blocs
|
||||||
|
if (height % 100 === 0 || height === currentHeight) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const cacheContent = `${now};${height};${blockHash}`;
|
||||||
|
writeFileSync(cachePath, cacheContent, 'utf8');
|
||||||
|
|
||||||
|
// Écrire le fichier de sortie
|
||||||
|
const outputLines = hashList.map((item) =>
|
||||||
|
`${item.hash};${item.txid};${item.blockHeight || ''};${item.confirmations || 0}`
|
||||||
|
);
|
||||||
|
writeFileSync(outputPath, outputLines.join('\n'), 'utf8');
|
||||||
|
logger.debug('Hash list cache updated', { height, count: hashList.length });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Error checking block for hashes', { height, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le cache final
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const cacheContent = `${now};${currentHeight};${currentBlockHash}`;
|
||||||
|
writeFileSync(cachePath, cacheContent, 'utf8');
|
||||||
|
|
||||||
|
// Écrire le fichier de sortie final
|
||||||
|
const outputLines = hashList.map((item) =>
|
||||||
|
`${item.hash};${item.txid};${item.blockHeight || ''};${item.confirmations || 0}`
|
||||||
|
);
|
||||||
|
writeFileSync(outputPath, outputLines.join('\n'), 'utf8');
|
||||||
|
logger.info('Hash list saved', { currentHeight, count: hashList.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashList;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting hash list', { error: error.message });
|
||||||
|
throw new Error(`Failed to get hash list: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient la liste des UTXO avec leurs montants, catégorisés en 3 types :
|
||||||
|
* - bloc_rewards : UTXO provenant de transactions coinbase (minage)
|
||||||
|
* - ancrages : UTXO provenant de transactions d'ancrage
|
||||||
|
* - changes : UTXO provenant d'autres transactions (monnaie de retour)
|
||||||
|
* Utilise un fichier de cache utxo_list_cache.txt pour éviter de tout recompter
|
||||||
|
* Format du cache: <date>
|
||||||
|
* Format du fichier de sortie: <category>;<txid>;<vout>;<address>;<amount>;<confirmations>
|
||||||
|
* @returns {Promise<Object>} Objet avec 3 listes : blocRewards, anchors, changes
|
||||||
|
*/
|
||||||
|
async getUtxoList() {
|
||||||
|
try {
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const cachePath = join(__dirname, '../../utxo_list_cache.txt');
|
||||||
|
const outputPath = join(__dirname, '../../utxo_list.txt');
|
||||||
|
|
||||||
|
// Obtenir les UTXO depuis le wallet
|
||||||
|
const walletName = process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
||||||
|
const host = process.env.BITCOIN_RPC_HOST || 'localhost';
|
||||||
|
const port = process.env.BITCOIN_RPC_PORT || '38332';
|
||||||
|
const username = process.env.BITCOIN_RPC_USER || 'bitcoin';
|
||||||
|
const password = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
||||||
|
const rpcUrl = `http://${host}:${port}/wallet/${walletName}`;
|
||||||
|
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
||||||
|
|
||||||
|
const rpcResponse = await fetch(rpcUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '1.0',
|
||||||
|
id: 'listunspent',
|
||||||
|
method: 'listunspent',
|
||||||
|
params: [0], // Inclure les non confirmés
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rpcResponse.ok) {
|
||||||
|
const errorText = await rpcResponse.text();
|
||||||
|
logger.error('HTTP error in listunspent', {
|
||||||
|
status: rpcResponse.status,
|
||||||
|
statusText: rpcResponse.statusText,
|
||||||
|
response: errorText,
|
||||||
|
});
|
||||||
|
throw new Error(`HTTP error fetching UTXOs: ${rpcResponse.status} ${rpcResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rpcResult = await rpcResponse.json();
|
||||||
|
if (rpcResult.error) {
|
||||||
|
logger.error('RPC error in listunspent', { error: rpcResult.error });
|
||||||
|
throw new Error(`RPC error: ${rpcResult.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unspent = rpcResult.result || [];
|
||||||
|
const blocRewards = [];
|
||||||
|
const anchors = [];
|
||||||
|
const changes = [];
|
||||||
|
const fees = []; // Liste des transactions avec leurs frais onchain
|
||||||
|
|
||||||
|
logger.info('Categorizing UTXOs', { total: unspent.length });
|
||||||
|
|
||||||
|
// Récupérer les UTXO verrouillés depuis l'API d'ancrage
|
||||||
|
let lockedUtxos = new Set();
|
||||||
|
try {
|
||||||
|
const anchorApiUrl = process.env.ANCHOR_API_URL || 'http://localhost:3010';
|
||||||
|
const anchorApiKey = process.env.ANCHOR_API_KEY || '';
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
if (anchorApiKey) {
|
||||||
|
headers['x-api-key'] = anchorApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockedResponse = await fetch(`${anchorApiUrl}/api/anchor/locked-utxos`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (lockedResponse.ok) {
|
||||||
|
const lockedData = await lockedResponse.json();
|
||||||
|
for (const locked of lockedData.locked || []) {
|
||||||
|
lockedUtxos.add(`${locked.txid}:${locked.vout}`);
|
||||||
|
}
|
||||||
|
logger.debug('Locked UTXOs retrieved', { count: lockedUtxos.size });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Error getting locked UTXOs from anchor API', { error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catégoriser chaque UTXO
|
||||||
|
for (const utxo of unspent) {
|
||||||
|
try {
|
||||||
|
// Obtenir la transaction source pour déterminer sa catégorie
|
||||||
|
const rawTxResponse = await fetch(rpcUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '1.0',
|
||||||
|
id: 'getrawtransaction',
|
||||||
|
method: 'getrawtransaction',
|
||||||
|
params: [utxo.txid, true],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rawTxResponse.ok) {
|
||||||
|
throw new Error(`HTTP error fetching transaction: ${rawTxResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawTxResult = await rawTxResponse.json();
|
||||||
|
if (rawTxResult.error) {
|
||||||
|
throw new Error(`RPC error: ${rawTxResult.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawTx = rawTxResult.result;
|
||||||
|
|
||||||
|
// Vérifier si l'UTXO est dépensé onchain
|
||||||
|
let isSpentOnchain = false;
|
||||||
|
try {
|
||||||
|
const txOutResponse = await fetch(rpcUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '1.0',
|
||||||
|
id: 'gettxout',
|
||||||
|
method: 'gettxout',
|
||||||
|
params: [utxo.txid, utxo.vout],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (txOutResponse.ok) {
|
||||||
|
const txOutResult = await txOutResponse.json();
|
||||||
|
// Si gettxout retourne null, l'UTXO est dépensé
|
||||||
|
isSpentOnchain = txOutResult.result === null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Error checking if UTXO is spent', { txid: utxo.txid, vout: utxo.vout, error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'UTXO est verrouillé dans le mutex de l'API d'ancrage
|
||||||
|
const utxoKey = `${utxo.txid}:${utxo.vout}`;
|
||||||
|
const isLockedInMutex = lockedUtxos.has(utxoKey);
|
||||||
|
|
||||||
|
const utxoItem = {
|
||||||
|
txid: utxo.txid,
|
||||||
|
vout: utxo.vout,
|
||||||
|
address: utxo.address || '',
|
||||||
|
amount: utxo.amount,
|
||||||
|
confirmations: utxo.confirmations || 0,
|
||||||
|
isSpentOnchain,
|
||||||
|
isLockedInMutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vérifier si c'est une transaction coinbase (récompense de minage)
|
||||||
|
// Une transaction coinbase doit avoir exactement un input avec le champ coinbase défini
|
||||||
|
const isCoinbase = rawTx.vin && rawTx.vin.length === 1 &&
|
||||||
|
rawTx.vin[0].coinbase !== undefined &&
|
||||||
|
rawTx.vin[0].coinbase !== null;
|
||||||
|
|
||||||
|
if (isCoinbase) {
|
||||||
|
utxoItem.category = 'bloc_rewards';
|
||||||
|
blocRewards.push(utxoItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si c'est une transaction d'ancrage (contient OP_RETURN avec "ANCHOR:")
|
||||||
|
let isAnchorTx = false;
|
||||||
|
let blockHeight = null;
|
||||||
|
|
||||||
|
// Obtenir la hauteur du bloc et le blocktime si la transaction est confirmée
|
||||||
|
let blockTime = null;
|
||||||
|
if (utxo.confirmations > 0) {
|
||||||
|
try {
|
||||||
|
const txInfo = await this.client.getTransaction(utxo.txid);
|
||||||
|
blockHeight = txInfo.blockheight || null;
|
||||||
|
blockTime = txInfo.blocktime || null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Error getting transaction block info', { txid: utxo.txid, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utxoItem.blockHeight = blockHeight;
|
||||||
|
utxoItem.blockTime = blockTime;
|
||||||
|
|
||||||
|
// Extraire les métadonnées onchain (change et frais) depuis l'OP_RETURN
|
||||||
|
let onchainChangeAddress = null;
|
||||||
|
let onchainChangeAmount = null;
|
||||||
|
let onchainFeeAmount = null;
|
||||||
|
|
||||||
|
if (rawTx.vout) {
|
||||||
|
for (const output of rawTx.vout) {
|
||||||
|
if (output.scriptPubKey && output.scriptPubKey.hex) {
|
||||||
|
const scriptHex = output.scriptPubKey.hex;
|
||||||
|
const anchorPrefix = Buffer.from('ANCHOR:', 'utf8').toString('hex');
|
||||||
|
|
||||||
|
if (scriptHex.includes(anchorPrefix)) {
|
||||||
|
isAnchorTx = true;
|
||||||
|
|
||||||
|
// Extraire les métadonnées depuis l'OP_RETURN
|
||||||
|
// Format: "ANCHOR:" + hash (32 bytes) + "|CHANGE:<address>:<amount_sats>|FEE:<amount_sats>"
|
||||||
|
try {
|
||||||
|
// Le script hex contient les données encodées
|
||||||
|
// "ANCHOR:" en hex = "414e43484f523a"
|
||||||
|
// Le hash suit (64 caractères hex = 32 bytes)
|
||||||
|
// Puis "|" en hex = "7c"
|
||||||
|
// Puis les métadonnées en UTF-8
|
||||||
|
|
||||||
|
const anchorPrefixHex = Buffer.from('ANCHOR:', 'utf8').toString('hex');
|
||||||
|
const hashLengthHex = 64; // 64 caractères hex pour 32 bytes
|
||||||
|
const separatorHex = Buffer.from('|', 'utf8').toString('hex');
|
||||||
|
|
||||||
|
// Trouver la position de "ANCHOR:" dans le script hex
|
||||||
|
const anchorPos = scriptHex.indexOf(anchorPrefixHex);
|
||||||
|
if (anchorPos !== -1) {
|
||||||
|
// Position après "ANCHOR:" + hash (64 caractères hex)
|
||||||
|
const afterHashPos = anchorPos + anchorPrefixHex.length + hashLengthHex;
|
||||||
|
|
||||||
|
// Chercher le séparateur "|" après le hash
|
||||||
|
const separatorPos = scriptHex.indexOf(separatorHex, afterHashPos);
|
||||||
|
if (separatorPos !== -1) {
|
||||||
|
// Extraire les métadonnées après le séparateur
|
||||||
|
const metadataHex = scriptHex.substring(separatorPos + separatorHex.length);
|
||||||
|
// Convertir de hex à UTF-8
|
||||||
|
const metadataBuffer = Buffer.from(metadataHex, 'hex');
|
||||||
|
const metadataString = metadataBuffer.toString('utf8');
|
||||||
|
|
||||||
|
// Parser les métadonnées: "CHANGE:<address>:<amount_sats>|FEE:<amount_sats>"
|
||||||
|
const parts = metadataString.split('|');
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.startsWith('CHANGE:')) {
|
||||||
|
const changeData = part.substring(7); // Enlever "CHANGE:"
|
||||||
|
const changeParts = changeData.split(':');
|
||||||
|
if (changeParts.length === 2 && changeParts[0] !== 'none') {
|
||||||
|
onchainChangeAddress = changeParts[0];
|
||||||
|
onchainChangeAmount = parseInt(changeParts[1], 10) / 100000000; // Convertir sats en BTC
|
||||||
|
}
|
||||||
|
} else if (part.startsWith('FEE:')) {
|
||||||
|
const feeData = part.substring(4); // Enlever "FEE:"
|
||||||
|
onchainFeeAmount = parseInt(feeData, 10) / 100000000; // Convertir sats en BTC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Error parsing OP_RETURN metadata', { txid: utxo.txid, error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si c'est une transaction d'ancrage avec des frais onchain, les stocker
|
||||||
|
if (isAnchorTx && onchainFeeAmount !== null) {
|
||||||
|
// Vérifier si cette transaction n'est pas déjà dans la liste des frais
|
||||||
|
const existingFee = fees.find(f => f.txid === utxo.txid);
|
||||||
|
if (!existingFee) {
|
||||||
|
fees.push({
|
||||||
|
txid: utxo.txid,
|
||||||
|
fee: onchainFeeAmount,
|
||||||
|
fee_sats: Math.round(onchainFeeAmount * 100000000),
|
||||||
|
changeAddress: onchainChangeAddress || null,
|
||||||
|
changeAmount: onchainChangeAmount || null,
|
||||||
|
blockHeight: blockHeight,
|
||||||
|
blockTime: blockTime,
|
||||||
|
confirmations: utxo.confirmations || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnchorTx) {
|
||||||
|
// Dans une transaction d'ancrage, distinguer les outputs d'ancrage/provisionnement du change
|
||||||
|
// Les transactions d'ancrage créent :
|
||||||
|
// - 1 output OP_RETURN (non dépensable, généralement le premier output)
|
||||||
|
// - 1 output d'ancrage de 2500 sats (0.000025 BTC)
|
||||||
|
// - 7 outputs de provisionnement de 2500 sats chacun
|
||||||
|
// - 1 output de change (seulement si change > 0.00001 BTC = 1000 sats)
|
||||||
|
|
||||||
|
const utxoAmountSats = Math.round(utxo.amount * 100000000);
|
||||||
|
const anchorAmountSats = 2500; // Montant standard des outputs d'ancrage/provisionnement
|
||||||
|
const minChangeSats = 1000; // Change minimum créé (0.00001 BTC)
|
||||||
|
|
||||||
|
// Identifier précisément le type d'output en analysant la transaction
|
||||||
|
let isAnchorOutput = false;
|
||||||
|
let isChangeOutput = false;
|
||||||
|
|
||||||
|
if (rawTx.vout && rawTx.vout[utxo.vout]) {
|
||||||
|
const output = rawTx.vout[utxo.vout];
|
||||||
|
|
||||||
|
// Vérifier si c'est un output OP_RETURN (non dépensable)
|
||||||
|
if (output.scriptPubKey && output.scriptPubKey.type === 'nulldata') {
|
||||||
|
// C'est l'OP_RETURN, on l'ignore (non dépensable)
|
||||||
|
continue; // Ne pas ajouter cet UTXO car OP_RETURN n'est pas dépensable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le montant correspond à un output d'ancrage/provisionnement (2500 sats)
|
||||||
|
if (Math.abs(utxoAmountSats - anchorAmountSats) <= 1) {
|
||||||
|
isAnchorOutput = true;
|
||||||
|
} else if (utxoAmountSats >= minChangeSats) {
|
||||||
|
// Le change dans une transaction d'ancrage est > 1000 sats et différent de 2500 sats
|
||||||
|
isChangeOutput = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnchorOutput) {
|
||||||
|
utxoItem.category = 'ancrages';
|
||||||
|
anchors.push(utxoItem);
|
||||||
|
} else if (isChangeOutput) {
|
||||||
|
// C'est le change de la transaction d'ancrage
|
||||||
|
utxoItem.category = 'changes';
|
||||||
|
utxoItem.isAnchorChange = true; // Marquer comme change d'une transaction d'ancrage
|
||||||
|
// Ajouter les métadonnées onchain si disponibles
|
||||||
|
if (onchainChangeAddress && onchainChangeAmount !== null) {
|
||||||
|
utxoItem.onchainChangeAddress = onchainChangeAddress;
|
||||||
|
utxoItem.onchainChangeAmount = onchainChangeAmount;
|
||||||
|
}
|
||||||
|
if (onchainFeeAmount !== null) {
|
||||||
|
utxoItem.onchainFeeAmount = onchainFeeAmount;
|
||||||
|
}
|
||||||
|
changes.push(utxoItem);
|
||||||
|
} else {
|
||||||
|
// Montant très petit (< 1000 sats), probablement du dust
|
||||||
|
// Classer comme change quand même (peu probable dans une transaction d'ancrage)
|
||||||
|
utxoItem.category = 'changes';
|
||||||
|
utxoItem.isAnchorChange = true;
|
||||||
|
changes.push(utxoItem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Transaction normale (non-ancrage, non-coinbase) = change
|
||||||
|
// Cela inclut toutes les transactions qui ne sont pas des coinbase et qui ne contiennent pas d'OP_RETURN avec "ANCHOR:"
|
||||||
|
// Ces UTXO proviennent de transactions normales (paiements, etc.)
|
||||||
|
utxoItem.category = 'changes';
|
||||||
|
utxoItem.isAnchorChange = false; // Change d'une transaction normale
|
||||||
|
changes.push(utxoItem);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// En cas d'erreur, classer comme change par défaut
|
||||||
|
logger.debug('Error categorizing UTXO', { txid: utxo.txid, error: error.message });
|
||||||
|
changes.push({
|
||||||
|
txid: utxo.txid,
|
||||||
|
vout: utxo.vout,
|
||||||
|
address: utxo.address || '',
|
||||||
|
amount: utxo.amount,
|
||||||
|
confirmations: utxo.confirmations || 0,
|
||||||
|
category: 'changes',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trier chaque catégorie par montant décroissant
|
||||||
|
blocRewards.sort((a, b) => b.amount - a.amount);
|
||||||
|
anchors.sort((a, b) => b.amount - a.amount);
|
||||||
|
// Pour les changes, trier d'abord par type (ancrage en premier), puis par montant décroissant
|
||||||
|
changes.sort((a, b) => {
|
||||||
|
// Les changes d'ancrage en premier
|
||||||
|
if (a.isAnchorChange && !b.isAnchorChange) return -1;
|
||||||
|
if (!a.isAnchorChange && b.isAnchorChange) return 1;
|
||||||
|
// Puis par montant décroissant
|
||||||
|
return b.amount - a.amount;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculer le nombre d'UTXO disponibles pour l'ancrage (> 2000 sats et non dépensés)
|
||||||
|
const allUtxos = [...blocRewards, ...anchors, ...changes];
|
||||||
|
const minAnchorAmount = 2000 / 100000000; // 2000 sats en BTC
|
||||||
|
const availableForAnchor = allUtxos.filter(utxo =>
|
||||||
|
utxo.amount >= minAnchorAmount && !utxo.isSpentOnchain && !utxo.isLockedInMutex
|
||||||
|
).length;
|
||||||
|
|
||||||
|
// Mettre à jour le cache
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
writeFileSync(cachePath, now, 'utf8');
|
||||||
|
|
||||||
|
// Écrire le fichier de sortie avec toutes les catégories
|
||||||
|
const outputLines = allUtxos.map((item) =>
|
||||||
|
`${item.category};${item.txid};${item.vout};${item.address};${item.amount};${item.confirmations}`
|
||||||
|
);
|
||||||
|
writeFileSync(outputPath, outputLines.join('\n'), 'utf8');
|
||||||
|
|
||||||
|
// Analyser la distribution pour comprendre pourquoi il y a si peu de changes
|
||||||
|
const anchorTxChanges = changes.filter(utxo => {
|
||||||
|
// Vérifier si cet UTXO provient d'une transaction d'ancrage
|
||||||
|
// (on ne peut pas le vérifier directement ici, mais on peut analyser les montants)
|
||||||
|
const utxoAmountSats = Math.round(utxo.amount * 100000000);
|
||||||
|
return utxoAmountSats > 2500; // Change d'une transaction d'ancrage serait > 2500 sats
|
||||||
|
});
|
||||||
|
const normalTxChanges = changes.length - anchorTxChanges.length;
|
||||||
|
|
||||||
|
// Trier les frais par blockHeight décroissant (plus récent en premier)
|
||||||
|
fees.sort((a, b) => {
|
||||||
|
if (a.blockHeight === null && b.blockHeight === null) return 0;
|
||||||
|
if (a.blockHeight === null) return 1;
|
||||||
|
if (b.blockHeight === null) return -1;
|
||||||
|
return b.blockHeight - a.blockHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('UTXO list saved', {
|
||||||
|
blocRewards: blocRewards.length,
|
||||||
|
anchors: anchors.length,
|
||||||
|
changes: changes.length,
|
||||||
|
changesFromAnchorTx: anchorTxChanges.length,
|
||||||
|
changesFromNormalTx: normalTxChanges,
|
||||||
|
fees: fees.length,
|
||||||
|
total: allUtxos.length,
|
||||||
|
availableForAnchor,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
blocRewards,
|
||||||
|
anchors,
|
||||||
|
changes,
|
||||||
|
fees,
|
||||||
|
total: allUtxos.length,
|
||||||
|
availableForAnchor,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting UTXO list', { error: error.message });
|
||||||
|
throw new Error(`Failed to get UTXO list: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtient le nombre d'ancrages en comptant tous les blocs depuis le début
|
* Obtient le nombre d'ancrages en comptant tous les blocs depuis le début
|
||||||
* Utilise un fichier de cache anchor_count.txt pour éviter de tout recompter
|
* Utilise un fichier de cache anchor_count.txt pour éviter de tout recompter
|
||||||
|
|||||||
@ -130,9 +130,6 @@ app.use(cors());
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
// Servir les fichiers statiques
|
|
||||||
app.use(express.static(join(__dirname, '../public')));
|
|
||||||
|
|
||||||
// Middleware de logging
|
// Middleware de logging
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
logger.info(`${req.method} ${req.path}`, {
|
logger.info(`${req.method} ${req.path}`, {
|
||||||
@ -142,11 +139,35 @@ app.use((req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Routes spécifiques AVANT le middleware static pour éviter les conflits
|
||||||
// Route pour la page principale
|
// Route pour la page principale
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.sendFile(join(__dirname, '../public/index.html'));
|
res.sendFile(join(__dirname, '../public/index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route pour la page de liste des hash
|
||||||
|
app.get('/hash-list', (req, res) => {
|
||||||
|
res.sendFile(join(__dirname, '../public/hash-list.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route pour la page de liste des UTXO
|
||||||
|
app.get('/utxo-list', (req, res) => {
|
||||||
|
res.sendFile(join(__dirname, '../public/utxo-list.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route pour la page de rejoindre le réseau Signet
|
||||||
|
app.get('/join-signet', (req, res) => {
|
||||||
|
res.sendFile(join(__dirname, '../public/join-signet.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route pour la page de documentation de l'API
|
||||||
|
app.get('/api-docs', (req, res) => {
|
||||||
|
res.sendFile(join(__dirname, '../public/api-docs.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Servir les fichiers statiques (après les routes spécifiques)
|
||||||
|
app.use(express.static(join(__dirname, '../public')));
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
app.get('/api/blockchain/info', async (req, res) => {
|
app.get('/api/blockchain/info', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -198,6 +219,79 @@ app.get('/api/anchor/count', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route pour obtenir la liste des hash (fichier texte)
|
||||||
|
app.get('/api/hash/list', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const hashList = await bitcoinRPC.getHashList();
|
||||||
|
res.json({ hashes: hashList, count: hashList.length });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting hash list', { error: error.message });
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route pour servir le fichier texte des hash
|
||||||
|
app.get('/api/hash/list.txt', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { readFileSync, existsSync } = await import('fs');
|
||||||
|
const hashListPath = join(__dirname, '../../hash_list.txt');
|
||||||
|
|
||||||
|
if (existsSync(hashListPath)) {
|
||||||
|
const content = readFileSync(hashListPath, 'utf8');
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
|
res.send(content);
|
||||||
|
} else {
|
||||||
|
res.status(404).send('Hash list file not found');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error serving hash list file', { error: error.message });
|
||||||
|
res.status(500).send('Error reading hash list file');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route pour obtenir la liste des UTXO (fichier texte)
|
||||||
|
app.get('/api/utxo/list', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const utxoData = await bitcoinRPC.getUtxoList();
|
||||||
|
res.json({
|
||||||
|
blocRewards: utxoData.blocRewards,
|
||||||
|
anchors: utxoData.anchors,
|
||||||
|
changes: utxoData.changes,
|
||||||
|
fees: utxoData.fees || [],
|
||||||
|
counts: {
|
||||||
|
blocRewards: utxoData.blocRewards.length,
|
||||||
|
anchors: utxoData.anchors.length,
|
||||||
|
changes: utxoData.changes.length,
|
||||||
|
fees: (utxoData.fees || []).length,
|
||||||
|
total: utxoData.total,
|
||||||
|
availableForAnchor: utxoData.availableForAnchor || 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error getting UTXO list', { error: error.message });
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route pour servir le fichier texte des UTXO
|
||||||
|
app.get('/api/utxo/list.txt', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { readFileSync, existsSync } = await import('fs');
|
||||||
|
const utxoListPath = join(__dirname, '../../utxo_list.txt');
|
||||||
|
|
||||||
|
if (existsSync(utxoListPath)) {
|
||||||
|
const content = readFileSync(utxoListPath, 'utf8');
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
|
res.send(content);
|
||||||
|
} else {
|
||||||
|
res.status(404).send('UTXO list file not found');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error serving UTXO list file', { error: error.message });
|
||||||
|
res.status(500).send('Error reading UTXO list file');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/mining/difficulty', async (req, res) => {
|
app.get('/api/mining/difficulty', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const blockchainInfo = await bitcoinRPC.getBlockchainInfo();
|
const blockchainInfo = await bitcoinRPC.getBlockchainInfo();
|
||||||
@ -460,13 +554,36 @@ app.use((req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialisation des fichiers au démarrage
|
||||||
|
async function initializeFiles() {
|
||||||
|
try {
|
||||||
|
logger.info('Initializing hash and UTXO lists...');
|
||||||
|
await Promise.all([
|
||||||
|
bitcoinRPC.getHashList().catch((error) => {
|
||||||
|
logger.warn('Error initializing hash list', { error: error.message });
|
||||||
|
}),
|
||||||
|
bitcoinRPC.getUtxoList().catch((error) => {
|
||||||
|
logger.warn('Error initializing UTXO list', { error: error.message });
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
logger.info('Hash and UTXO lists initialized');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error initializing files', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Démarrage du serveur
|
// Démarrage du serveur
|
||||||
const server = app.listen(PORT, HOST, () => {
|
const server = app.listen(PORT, HOST, async () => {
|
||||||
logger.info(`Dashboard Bitcoin Signet démarré`, {
|
logger.info(`Dashboard Bitcoin Signet démarré`, {
|
||||||
host: HOST,
|
host: HOST,
|
||||||
port: PORT,
|
port: PORT,
|
||||||
environment: process.env.NODE_ENV || 'production',
|
environment: process.env.NODE_ENV || 'production',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialiser les fichiers en arrière-plan
|
||||||
|
initializeFiles().catch((error) => {
|
||||||
|
logger.error('Error during file initialization', { error: error.message });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gestion de l'arrêt propre
|
// Gestion de l'arrêt propre
|
||||||
|
|||||||
8564
utxo_list.txt
Normal file
8564
utxo_list.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
utxo_list_cache.txt
Normal file
1
utxo_list_cache.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
2026-01-25T12:52:49.162Z
|
||||||
Loading…
x
Reference in New Issue
Block a user