Add automatic UTXO synchronization when no UTXOs available
**Motivations:** - Synchroniser automatiquement les UTXOs depuis Bitcoin RPC si aucun UTXO n'est disponible dans la base de données - Corriger les UTXOs mal marqués comme dépensés - Améliorer la robustesse en cas de désynchronisation de la base de données **Root causes:** - La base de données peut être désynchronisée avec Bitcoin RPC - Tous les UTXOs peuvent être marqués comme dépensés alors qu'ils sont disponibles dans Bitcoin - Aucune synchronisation automatique n'était effectuée avant de chercher des UTXOs **Correctifs:** - Ajout d'une synchronisation automatique dans createAnchorTransaction() si aucun UTXO n'est disponible - Création d'un script fix-utxos-status.mjs pour corriger les UTXOs mal marqués - Création d'un script sync-utxos-from-bitcoin.mjs pour synchroniser depuis Bitcoin RPC **Evolutions:** - Synchronisation automatique des UTXOs disponibles depuis Bitcoin RPC avant de chercher des UTXOs - Mise à jour automatique du statut is_spent_onchain pour les UTXOs disponibles dans Bitcoin - Scripts de diagnostic et de correction pour maintenir la cohérence de la base de données **Pages affectées:** - api-anchorage/src/bitcoin-rpc.js - api-anchorage/fix-utxos-status.mjs - api-anchorage/sync-utxos-from-bitcoin.mjs
This commit is contained in:
parent
7a6b9e8b6e
commit
9929653ec2
126
api-anchorage/fix-utxos-status.mjs
Executable file
126
api-anchorage/fix-utxos-status.mjs
Executable file
@ -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);
|
||||
});
|
||||
@ -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)');
|
||||
|
||||
128
api-anchorage/sync-utxos-from-bitcoin.mjs
Executable file
128
api-anchorage/sync-utxos-from-bitcoin.mjs
Executable file
@ -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);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user