diff --git a/api-anchorage/fix-utxos-status.mjs b/api-anchorage/fix-utxos-status.mjs new file mode 100755 index 0000000..c1f223a --- /dev/null +++ b/api-anchorage/fix-utxos-status.mjs @@ -0,0 +1,126 @@ +#!/usr/bin/env node + +/** + * Script pour corriger les UTXOs mal marqués comme dépensés + * + * Ce script vérifie tous les UTXOs marqués comme dépensés dans la base de données + * et les compare avec listunspent pour corriger leur statut is_spent_onchain. + * + * Usage: node api-anchorage/fix-utxos-status.mjs + */ + +import { getDatabase } from './src/database.js'; +import { bitcoinRPC } from './src/bitcoin-rpc.js'; + +async function fixUtxosStatus() { + console.log('🔍 Démarrage de la correction des UTXOs mal marqués...\n'); + + const db = getDatabase(); + + try { + // Compter les UTXOs marqués comme dépensés + const spentCount = db.prepare(` + SELECT COUNT(*) as count + FROM utxos + WHERE is_spent_onchain = 1 + `).get().count; + + console.log(`📊 UTXOs marqués comme dépensés: ${spentCount}`); + + if (spentCount === 0) { + console.log('✅ Aucun UTXO à vérifier'); + return; + } + + // Récupérer tous les UTXOs disponibles depuis Bitcoin + console.log('📡 Récupération des UTXOs depuis Bitcoin...'); + const unspent = await bitcoinRPC.callRPCCommandWithRetry('listunspent', 1, 9999999); // Minimum 1 confirmation + + console.log(`📊 UTXOs disponibles dans Bitcoin: ${unspent.length}`); + + if (unspent.length === 0) { + console.log('⚠️ Aucun UTXO disponible dans Bitcoin'); + return; + } + + // Créer un Set pour recherche rapide des UTXOs disponibles dans Bitcoin + const availableUtxosSet = new Set(); + for (const utxo of unspent) { + availableUtxosSet.add(`${utxo.txid}:${utxo.vout}`); + } + + // Récupérer tous les UTXOs marqués comme dépensés dans la DB + console.log('💾 Vérification des UTXOs marqués comme dépensés...'); + const spentUtxos = db.prepare(` + SELECT txid, vout, address, amount, confirmations, category, block_time + FROM utxos + WHERE is_spent_onchain = 1 + `).all(); + + console.log(`📊 UTXOs marqués comme dépensés: ${spentUtxos.length}`); + + // Vérifier et corriger les UTXOs mal marqués + const updateStmt = db.prepare(` + UPDATE utxos + SET is_spent_onchain = 0, updated_at = CURRENT_TIMESTAMP + WHERE txid = ? AND vout = ? + `); + + let correctedCount = 0; + + // Traiter par batch pour éviter la surcharge + const BATCH_SIZE = 1000; + for (let i = 0; i < spentUtxos.length; i += BATCH_SIZE) { + const batch = spentUtxos.slice(i, i + BATCH_SIZE); + + for (const utxo of batch) { + const utxoKey = `${utxo.txid}:${utxo.vout}`; + + if (availableUtxosSet.has(utxoKey)) { + // L'UTXO est disponible dans Bitcoin mais marqué comme dépensé dans la DB + updateStmt.run(utxo.txid, utxo.vout); + correctedCount++; + } + } + + if ((i + BATCH_SIZE) % 10000 === 0) { + console.log(` ⏳ Traitement: ${Math.min(i + BATCH_SIZE, spentUtxos.length)}/${spentUtxos.length} UTXOs...`); + } + } + + console.log(`\n📊 Résumé:`); + console.log(` - UTXOs disponibles dans Bitcoin: ${unspent.length}`); + console.log(` - UTXOs marqués comme dépensés dans DB: ${spentUtxos.length}`); + console.log(` - UTXOs corrigés (marqués comme non dépensés): ${correctedCount}`); + + // Afficher les statistiques finales + const finalStats = { + total: db.prepare('SELECT COUNT(*) as count FROM utxos').get().count, + spent: db.prepare('SELECT COUNT(*) as count FROM utxos WHERE is_spent_onchain = 1').get().count, + notSpent: db.prepare('SELECT COUNT(*) as count FROM utxos WHERE is_spent_onchain = 0').get().count, + }; + + console.log(`\n📈 Statistiques finales:`); + console.log(` - Total UTXOs: ${finalStats.total}`); + console.log(` - Dépensés: ${finalStats.spent}`); + console.log(` - Non dépensés: ${finalStats.notSpent}`); + + } catch (error) { + console.error('❌ Erreur lors de la correction:', error.message); + console.error(error.stack); + process.exit(1); + } finally { + db.close(); + } +} + +// Exécuter la correction +fixUtxosStatus() + .then(() => { + console.log('\n✅ Correction terminée'); + process.exit(0); + }) + .catch((error) => { + console.error('❌ Erreur fatale:', error); + process.exit(1); + }); diff --git a/api-anchorage/src/bitcoin-rpc.js b/api-anchorage/src/bitcoin-rpc.js index e9d985b..154ef29 100644 --- a/api-anchorage/src/bitcoin-rpc.js +++ b/api-anchorage/src/bitcoin-rpc.js @@ -550,7 +550,78 @@ class BitcoinRPC { AND is_locked_in_mutex = 0 ORDER BY amount DESC `); - const availableUtxos = combineUtxosQuery.all(); + let availableUtxos = combineUtxosQuery.all(); + + // Si aucun UTXO disponible dans la DB, vérifier avec Bitcoin RPC si des UTXOs sont réellement disponibles + // (peut arriver si la DB est désynchronisée) + if (availableUtxos.length === 0) { + logger.warn('No UTXOs in database, checking Bitcoin RPC for available UTXOs', { + hash: hash.substring(0, 16) + '...', + }); + + try { + // Récupérer tous les UTXOs disponibles depuis Bitcoin + const unspent = await this.callRPCCommandWithRetry('listunspent', 1, 9999999); // Minimum 1 confirmation + + if (unspent.length > 0) { + logger.info('Found UTXOs in Bitcoin RPC, synchronizing with database', { + count: unspent.length, + }); + + // Créer une table temporaire pour les UTXOs disponibles + db.exec(` + CREATE TEMP TABLE IF NOT EXISTS temp_available_utxos ( + txid TEXT, + vout INTEGER, + PRIMARY KEY (txid, vout) + ) + `); + + // Insérer les UTXOs disponibles par batch + const insertStmt = db.prepare(` + INSERT OR IGNORE INTO temp_available_utxos (txid, vout) + VALUES (?, ?) + `); + + const insertBatch = db.transaction((utxos) => { + for (const utxo of utxos) { + insertStmt.run(utxo.txid, utxo.vout); + } + }); + + const BATCH_SIZE = 1000; + for (let i = 0; i < unspent.length; i += BATCH_SIZE) { + const batch = unspent.slice(i, i + BATCH_SIZE); + insertBatch(batch); + } + + // Mettre à jour les UTXOs existants comme non dépensés + const updateStmt = db.prepare(` + UPDATE utxos + SET is_spent_onchain = 0, updated_at = CURRENT_TIMESTAMP + WHERE (txid || ':' || vout) IN ( + SELECT txid || ':' || vout FROM temp_available_utxos + ) + `); + const updateResult = updateStmt.run(); + + // Nettoyer la table temporaire + db.exec('DROP TABLE IF EXISTS temp_available_utxos'); + + logger.info('Synchronized UTXOs with Bitcoin RPC', { + updated: updateResult.changes, + total: unspent.length, + }); + + // Réessayer la requête après synchronisation + availableUtxos = combineUtxosQuery.all(); + } + } catch (rpcError) { + logger.warn('Error checking Bitcoin RPC for available UTXOs', { + error: rpcError.message, + }); + } + } if (availableUtxos.length === 0) { throw new Error('No available UTXOs in database (all are locked, spent, or unconfirmed)'); diff --git a/api-anchorage/sync-utxos-from-bitcoin.mjs b/api-anchorage/sync-utxos-from-bitcoin.mjs new file mode 100755 index 0000000..7b291c3 --- /dev/null +++ b/api-anchorage/sync-utxos-from-bitcoin.mjs @@ -0,0 +1,128 @@ +#!/usr/bin/env node + +/** + * Script pour synchroniser les UTXOs depuis Bitcoin RPC vers la base de données + * + * Ce script appelle le dashboard pour forcer une synchronisation complète des UTXOs + * depuis Bitcoin RPC vers la base de données. + * + * Usage: node api-anchorage/sync-utxos-from-bitcoin.mjs + */ + +import { bitcoinRPC } from './src/bitcoin-rpc.js'; + +async function syncUtxosFromBitcoin() { + console.log('🔍 Démarrage de la synchronisation des UTXOs depuis Bitcoin...\n'); + + try { + // Appeler le dashboard pour forcer une synchronisation + const dashboardUrl = process.env.DASHBOARD_API_URL || 'http://localhost:3020'; + + console.log('📡 Appel du dashboard pour synchroniser les UTXOs...'); + const response = await fetch(`${dashboardUrl}/api/utxo/fees`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`Dashboard API error: ${response.status} ${response.statusText}`); + } + + // Le dashboard synchronise automatiquement les UTXOs lors de l'appel à getUtxoList() + // Appeler directement Bitcoin RPC pour synchroniser + console.log('📡 Récupération des UTXOs depuis Bitcoin RPC...'); + const unspent = await bitcoinRPC.callRPCCommandWithRetry('listunspent', 1, 9999999); + + console.log(`📊 UTXOs disponibles dans Bitcoin: ${unspent.length}`); + + if (unspent.length === 0) { + console.log('⚠️ Aucun UTXO disponible dans Bitcoin'); + return; + } + + // Synchroniser avec la base de données + const { getDatabase } = await import('./src/database.js'); + const db = getDatabase(); + + // Créer une table temporaire pour les UTXOs disponibles + db.exec(` + CREATE TEMP TABLE IF NOT EXISTS temp_available_utxos ( + txid TEXT, + vout INTEGER, + PRIMARY KEY (txid, vout) + ) + `); + + // Insérer les UTXOs disponibles par batch + const insertStmt = db.prepare(` + INSERT OR IGNORE INTO temp_available_utxos (txid, vout) + VALUES (?, ?) + `); + + const insertBatch = db.transaction((utxos) => { + for (const utxo of utxos) { + insertStmt.run(utxo.txid, utxo.vout); + } + }); + + const BATCH_SIZE = 1000; + console.log('💾 Insertion des UTXOs disponibles dans la table temporaire...'); + for (let i = 0; i < unspent.length; i += BATCH_SIZE) { + const batch = unspent.slice(i, i + BATCH_SIZE); + insertBatch(batch); + + if ((i + BATCH_SIZE) % 10000 === 0) { + console.log(` ⏳ Traitement: ${Math.min(i + BATCH_SIZE, unspent.length)}/${unspent.length} UTXOs...`); + } + } + + // Mettre à jour les UTXOs existants comme non dépensés s'ils sont disponibles + console.log('💾 Mise à jour des UTXOs dans la base de données...'); + const updateStmt = db.prepare(` + UPDATE utxos + SET is_spent_onchain = 0, updated_at = CURRENT_TIMESTAMP + WHERE (txid || ':' || vout) IN ( + SELECT txid || ':' || vout FROM temp_available_utxos + ) + `); + const updateResult = updateStmt.run(); + + // Nettoyer la table temporaire + db.exec('DROP TABLE IF EXISTS temp_available_utxos'); + + console.log(`\n📊 Résumé:`); + console.log(` - UTXOs disponibles dans Bitcoin: ${unspent.length}`); + console.log(` - UTXOs mis à jour dans la base de données: ${updateResult.changes}`); + + // Afficher les statistiques finales + const finalStats = { + total: db.prepare('SELECT COUNT(*) as count FROM utxos').get().count, + spent: db.prepare('SELECT COUNT(*) as count FROM utxos WHERE is_spent_onchain = 1').get().count, + notSpent: db.prepare('SELECT COUNT(*) as count FROM utxos WHERE is_spent_onchain = 0').get().count, + }; + + console.log(`\n📈 Statistiques finales:`); + console.log(` - Total UTXOs: ${finalStats.total}`); + console.log(` - Dépensés: ${finalStats.spent}`); + console.log(` - Non dépensés: ${finalStats.notSpent}`); + + db.close(); + } catch (error) { + console.error('❌ Erreur lors de la synchronisation:', error.message); + console.error(error.stack); + process.exit(1); + } +} + +// Exécuter la synchronisation +syncUtxosFromBitcoin() + .then(() => { + console.log('\n✅ Synchronisation terminée'); + process.exit(0); + }) + .catch((error) => { + console.error('❌ Erreur fatale:', error); + process.exit(1); + });