anchorage_layer_simple/data/sync-utxos-spent-status.mjs
ncantu 1d4b0d8f33 Pagination serveur, correction UTXO déjà dépensé et synchronisation automatique
**Motivations:**
- Réduire la consommation mémoire en paginant côté serveur au lieu de charger toutes les données
- Corriger les erreurs "Input not found or already spent" dans l'API d'ancrage
- Maintenir la synchronisation entre la base de données et l'état réel de Bitcoin
- Améliorer l'expérience utilisateur avec un suivi de progression pour la collecte de signatures

**Root causes:**
- Pagination effectuée côté client : le serveur retournait tous les UTXOs/hashes (68k+ UTXOs, 32k+ hashes) puis le frontend paginait en JavaScript
- Désynchronisation entre la DB et Bitcoin : UTXOs dépensés non mis à jour dans la base de données
- Détection d'erreur incomplète : ne couvrait pas tous les cas ("already spent", "input not found")
- Pas de vérification de disponibilité de l'UTXO juste avant utilisation dans une transaction

**Correctifs:**
- Implémentation de la pagination côté serveur pour `/api/utxo/list` et `/api/hash/list` avec paramètres `page` et `limit`
- Amélioration de la détection d'erreur pour inclure "already spent" et "input not found"
- Ajout d'une vérification de disponibilité de l'UTXO avant utilisation avec mécanisme de retry (max 3 tentatives)
- Mise à jour automatique de tous les UTXOs dépensés dans la base de données lors de chaque synchronisation
- Script de synchronisation périodique avec cron job toutes les heures
- Optimisation mémoire : utilisation de tables temporaires SQL au lieu de charger tous les UTXOs en mémoire

**Evolutions:**
- Pagination serveur avec métadonnées (total, totalPages, page, limit) pour les endpoints `/api/utxo/list` et `/api/hash/list`
- Adaptation du frontend pour utiliser la pagination serveur (compatibilité maintenue avec chargement jusqu'à 1000 éléments)
- Ajout de `onProgress` callback dans `runCollectLoop` pour notifier la progression de la collecte de signatures
- Nouvelle fonction `collectProgress` pour calculer la progression (satisfied vs required) pour les notifications/UI
- Refactoring de `hasEnoughSignatures` avec extraction de `pairsPerMemberFromSigs` pour réutilisabilité

**Pages affectées:**
- `api-anchorage/src/bitcoin-rpc.js` : Vérification disponibilité UTXO, amélioration détection erreur, paramètre retryCount
- `api-anchorage/src/routes/anchor.js` : Passage des nouveaux paramètres à createAnchorTransaction
- `signet-dashboard/src/server.js` : Pagination pour `/api/hash/list` et `/api/utxo/list`
- `signet-dashboard/src/bitcoin-rpc.js` : Mise à jour automatique de tous les UTXOs dépensés avec optimisation mémoire
- `signet-dashboard/public/hash-list.html` : Adaptation pour charger avec pagination serveur
- `signet-dashboard/public/utxo-list.html` : Adaptation pour utiliser la pagination serveur par catégorie
- `userwallet/src/utils/collectSignatures.ts` : Ajout interface CollectLoopOpts avec onProgress callback
- `userwallet/src/utils/loginValidation.ts` : Ajout fonction collectProgress, refactoring avec pairsPerMemberFromSigs
- `data/sync-utxos-spent-status.mjs` : Script de synchronisation périodique des UTXOs dépensés
- `data/sync-utxos-cron.sh` : Script wrapper pour cron job
- `features/pagination-serveur-base-donnees.md` : Documentation de la pagination serveur
- `features/synchronisation-automatique-utxos-depenses.md` : Documentation de la synchronisation automatique
- `fixKnowledge/api-anchorage-utxo-already-spent-error.md` : Documentation de la correction de l'erreur UTXO déjà dépensé
2026-01-27 22:21:38 +01:00

214 lines
6.4 KiB
JavaScript
Executable File

#!/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);
});