#!/usr/bin/env node /** * Script de synchronisation des UTXOs dépensés * * Ce script vérifie tous les UTXOs marqués comme non dépensés dans la base de données * et les compare avec listunspent pour mettre à jour leur statut is_spent_onchain. * * Usage: node data/sync-utxos-spent-status.mjs */ import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import https from 'https'; import http from 'http'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Utiliser better-sqlite3 depuis signet-dashboard const signetDashboardPath = join(__dirname, '../signet-dashboard'); const require = createRequire(join(signetDashboardPath, 'package.json')); const Database = require('better-sqlite3'); // Configuration RPC Bitcoin const RPC_URL = process.env.BITCOIN_RPC_URL || 'http://localhost:38332'; const RPC_USER = process.env.BITCOIN_RPC_USER || 'bitcoin'; const RPC_PASS = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin'; const RPC_WALLET = process.env.BITCOIN_RPC_WALLET || 'custom_signet'; // Chemin de la base de données const DB_PATH = join(__dirname, 'signet.db'); /** * Effectue un appel RPC Bitcoin */ function rpcCall(method, params = []) { return new Promise((resolve, reject) => { const url = new URL(RPC_URL); const isHttps = url.protocol === 'https:'; const httpModule = isHttps ? https : http; const auth = Buffer.from(`${RPC_USER}:${RPC_PASS}`).toString('base64'); const postData = JSON.stringify({ jsonrpc: '2.0', id: 1, method: method, params: params, }); const options = { hostname: url.hostname, port: url.port || (isHttps ? 443 : 80), path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${auth}`, 'Content-Length': Buffer.byteLength(postData), }, }; const req = httpModule.request(options, (res) => { let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { try { const json = JSON.parse(body); if (json.error) { reject(new Error(json.error.message || JSON.stringify(json.error))); } else { resolve(json.result); } } catch (e) { reject(new Error(`Parse error: ${e.message}`)); } }); }); req.on('error', (error) => { reject(error); }); req.write(postData); req.end(); }); } /** * Synchronise les UTXOs dépensés */ async function syncSpentUtxos() { console.log('🔍 Démarrage de la synchronisation des UTXOs dépensés...\n'); // Ouvrir la base de données const db = new Database(DB_PATH); try { // Compter les UTXOs à vérifier sans les charger en mémoire const notSpentCount = db.prepare(` SELECT COUNT(*) as count FROM utxos WHERE is_spent_onchain = 0 `).get().count; console.log(`📊 UTXOs à vérifier: ${notSpentCount}`); if (notSpentCount === 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 rpcCall('listunspent', [0, 9999999, [], false, { minimumAmount: 0, maximumCount: 9999999, }]); console.log(`📊 UTXOs disponibles dans Bitcoin: ${unspent.length}`); // Utiliser une table temporaire pour éviter de charger tous les UTXOs en mémoire console.log('💾 Création de la table temporaire...'); 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 pour réduire la mémoire const BATCH_SIZE = 1000; const insertAvailableUtxo = db.prepare(` INSERT OR IGNORE INTO temp_available_utxos (txid, vout) VALUES (?, ?) `); const insertBatch = db.transaction((utxos) => { for (const utxo of utxos) { insertAvailableUtxo.run(utxo.txid, utxo.vout); } }); console.log('💾 Insertion des UTXOs disponibles par batch...'); 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 insérés...`); } } // Mettre à jour les UTXOs dépensés en une seule requête SQL (optimisé pour la mémoire) console.log('💾 Mise à jour des UTXOs dépensés...'); const updateSpentStmt = db.prepare(` UPDATE utxos SET is_spent_onchain = 1, updated_at = CURRENT_TIMESTAMP WHERE is_spent_onchain = 0 AND (txid || ':' || vout) NOT IN ( SELECT txid || ':' || vout FROM temp_available_utxos ) `); const updateResult = updateSpentStmt.run(); const updatedCount = updateResult.changes; // Nettoyer la table temporaire db.exec('DROP TABLE IF EXISTS temp_available_utxos'); const stillAvailableCount = notSpentCount - updatedCount; console.log(`\n📊 Résumé:`); console.log(` - UTXOs vérifiés: ${notSpentCount}`); console.log(` - UTXOs toujours disponibles: ${stillAvailableCount}`); console.log(` - UTXOs dépensés détectés: ${updatedCount}`); // 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 synchronisation:', error.message); process.exit(1); } finally { db.close(); } } // Exécuter la synchronisation syncSpentUtxos() .then(() => { console.log('\n✅ Synchronisation terminée'); process.exit(0); }) .catch((error) => { console.error('❌ Erreur fatale:', error); process.exit(1); });