api-anchorage: fix syntax errors, api-relay: refactor routes setup

**Motivations:**
- Corriger erreurs de syntaxe dans api-anchorage (bloc else non fermé, variable dupliquée)
- Refactorer api-relay pour extraire setupRoutes et améliorer la structure

**Root causes:**
- api-anchorage: bloc else non fermé après verrouillage UTXO, variable totalInputAmount déclarée deux fois
- api-relay: code dupliqué dans main(), besoin de meilleure séparation des responsabilités

**Correctifs:**
- api-anchorage: fermeture bloc else ligne 392, renommage totalInputAmount en totalInputAmountForFee dans calcul frais, utilisation totalSelectedAmount pour plusieurs UTXOs

**Evolutions:**
- api-relay: extraction setupRoutes() depuis main(), refactoring routes (keys, messages, signatures), middleware auth, index.ts restructuré

**Pages affectées:**
- api-anchorage: bitcoin-rpc.js
- api-relay: index.ts, middleware/auth.ts, routes (keys, messages, signatures), services/storage.ts
- data: sync-utxos.log
This commit is contained in:
ncantu 2026-01-28 08:17:42 +01:00
parent 3c212e56e9
commit 4833fdbb53
8 changed files with 357 additions and 318 deletions

View File

@ -389,6 +389,7 @@ class BitcoinRPC {
// Verrouiller l'UTXO sélectionné
this.lockUtxo(selectedUtxo.txid, selectedUtxo.vout);
}
// Créer les outputs
// Note: Bitcoin Core ne permet qu'un seul OP_RETURN par transaction via 'data'
@ -627,7 +628,7 @@ class BitcoinRPC {
// Calculer les frais réels de la transaction
// Frais = somme des inputs - somme des outputs
let totalInputAmount = 0;
let totalInputAmountForFee = 0;
let totalOutputAmountInTx = 0;
// Calculer la somme des inputs
@ -637,16 +638,18 @@ class BitcoinRPC {
try {
const prevTx = await this.client.getRawTransaction(input.txid, true);
if (prevTx.vout && prevTx.vout[input.vout]) {
totalInputAmount += prevTx.vout[input.vout].value || 0;
totalInputAmountForFee += 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é
// Si on ne peut pas obtenir la transaction précédente, utiliser le montant total des UTXOs sélectionnés
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é
// Utiliser le montant total des UTXOs sélectionnés
const totalSelectedAmountForFee = selectedUtxos.length > 1 ? totalSelectedAmount : selectedUtxo.amount;
totalInputAmountForFee += totalSelectedAmountForFee;
break; // Utiliser le montant connu des UTXOs sélectionnés
}
}
}
@ -658,7 +661,7 @@ class BitcoinRPC {
}
}
const actualFee = roundTo8Decimals(totalInputAmount - totalOutputAmountInTx);
const actualFee = roundTo8Decimals(totalInputAmountForFee - totalOutputAmountInTx);
// Construire la liste des outputs avec leur type explicite
// En analysant les outputs réels de la transaction brute

View File

@ -26,31 +26,12 @@ const PEER_RELAYS = process.env.PEER_RELAYS
: [];
const REQUIRE_API_KEY = process.env.REQUIRE_API_KEY !== 'false'; // Default: true
async function main(): Promise<void> {
const app = express();
registerMiddleware(app);
app.use(express.json({ limit: getBodyLimit() }));
// Initialize database
const dbStorage = new DatabaseStorageService(STORAGE_PATH);
await dbStorage.initialize();
// Create adapter for compatibility
const storage = new StorageAdapter(dbStorage);
// Initialize API key service
const apiKeyService = new ApiKeyService(dbStorage);
// Register authentication middleware if required
if (REQUIRE_API_KEY) {
app.use(
createAuthMiddleware((key) => apiKeyService.validateApiKey(key)),
);
}
const relay = new RelayService(storage, PEER_RELAYS);
function setupRoutes(
app: express.Application,
storage: StorageAdapter,
relay: RelayService,
apiKeyService: ApiKeyService,
): void {
app.use('/health', createHealthRouter());
app.use('/messages', createMessagesRouter(storage, relay));
app.use('/signatures', createSignaturesRouter(storage, relay));
@ -58,7 +39,6 @@ async function main(): Promise<void> {
app.use('/metrics', createMetricsRouter(storage));
app.use('/bloom', createBloomRouter(storage));
// API key management endpoint (admin only, should be protected in production)
app.post('/admin/api-keys', (req, res) => {
const { description } = req.body as { description?: string };
const { key, prefix } = apiKeyService.generateApiKey(description);
@ -79,23 +59,9 @@ async function main(): Promise<void> {
res.status(404).json({ error: 'API key not found' });
}
});
}
const server = http.createServer(app);
server.timeout = getRequestTimeoutMs();
server.listen(PORT, HOST, () => {
logger.info(
{
host: HOST,
port: PORT,
storagePath: STORAGE_PATH,
peerRelays: PEER_RELAYS.length > 0 ? PEER_RELAYS : 'none',
requireApiKey: REQUIRE_API_KEY,
},
'Relay server listening',
);
});
function setupShutdown(dbStorage: DatabaseStorageService): void {
const shutdown = async (): Promise<void> => {
logger.info('Shutting down...');
try {
@ -116,6 +82,46 @@ async function main(): Promise<void> {
});
}
async function main(): Promise<void> {
const app = express();
registerMiddleware(app);
app.use(express.json({ limit: getBodyLimit() }));
const dbStorage = new DatabaseStorageService(STORAGE_PATH);
await dbStorage.initialize();
const storage = new StorageAdapter(dbStorage);
const apiKeyService = new ApiKeyService(dbStorage);
if (REQUIRE_API_KEY) {
app.use(
createAuthMiddleware((key) => apiKeyService.validateApiKey(key)),
);
}
const relay = new RelayService(storage, PEER_RELAYS);
setupRoutes(app, storage, relay, apiKeyService);
const server = http.createServer(app);
server.timeout = getRequestTimeoutMs();
server.listen(PORT, HOST, () => {
logger.info(
{
host: HOST,
port: PORT,
storagePath: STORAGE_PATH,
peerRelays: PEER_RELAYS.length > 0 ? PEER_RELAYS : 'none',
requireApiKey: REQUIRE_API_KEY,
},
'Relay server listening',
);
});
setupShutdown(dbStorage);
}
main().catch((error) => {
logger.fatal({ err: error }, 'Fatal error');
process.exit(1);

View File

@ -1,6 +1,43 @@
import type { Request, Response, NextFunction } from 'express';
import { logger } from '../lib/logger.js';
function extractApiKey(req: Request): string | undefined {
const authHeader = req.headers.authorization;
const apiKeyHeader = req.headers['x-api-key'] as string | undefined;
if (authHeader?.startsWith('Bearer ') === true) {
return authHeader.slice(7);
}
if (apiKeyHeader !== undefined) {
return apiKeyHeader;
}
return undefined;
}
function handleMissingApiKey(req: Request, res: Response): void {
logger.warn(
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request without API key',
);
res.status(401).json({ error: 'API key required for POST requests' });
}
function handleInvalidApiKey(req: Request, res: Response): void {
logger.warn(
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request with invalid API key',
);
res.status(403).json({ error: 'Invalid API key' });
}
/**
* Middleware to authenticate POST requests using API key.
* API key should be provided in the Authorization header as: "Bearer <key>"
@ -10,46 +47,19 @@ export function createAuthMiddleware(
validateApiKey: (key: string) => boolean,
): (req: Request, res: Response, next: NextFunction) => void {
return (req: Request, res: Response, next: NextFunction): void => {
// Only require auth for POST requests
if (req.method !== 'POST') {
next();
return;
}
const authHeader = req.headers.authorization;
const apiKeyHeader = req.headers['x-api-key'] as string | undefined;
let apiKey: string | undefined;
if (authHeader?.startsWith('Bearer ') === true) {
apiKey = authHeader.slice(7);
} else if (apiKeyHeader !== undefined) {
apiKey = apiKeyHeader;
}
const apiKey = extractApiKey(req);
if (apiKey === undefined || apiKey.length === 0) {
logger.warn(
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request without API key',
);
res.status(401).json({ error: 'API key required for POST requests' });
handleMissingApiKey(req, res);
return;
}
if (!validateApiKey(apiKey)) {
logger.warn(
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request with invalid API key',
);
res.status(403).json({ error: 'Invalid API key' });
handleInvalidApiKey(req, res);
return;
}

View File

@ -5,82 +5,89 @@ import type { MsgCle, StoredKey } from '../types/message.js';
import { validateMsgCle } from '../lib/validate.js';
import { logger } from '../lib/logger.js';
function handleGetKeysInWindow(
storage: StorageServiceInterface,
req: Request,
res: Response,
): void {
const startRaw = req.query.start as string | undefined;
const endRaw = req.query.end as string | undefined;
if (startRaw === undefined || endRaw === undefined) {
res.status(400).json({ error: 'keys window requires start and end query params' });
return;
}
const start = parseInt(startRaw, 10);
const end = parseInt(endRaw, 10);
if (Number.isNaN(start) || Number.isNaN(end)) {
res.status(400).json({ error: 'invalid start or end' });
return;
}
const stored = storage.getKeysInWindow(start, end);
const keys: MsgCle[] = stored.map((k) => k.msg);
res.json(keys);
}
function handleGetKeysByHash(
storage: StorageServiceInterface,
req: Request,
res: Response,
): void {
try {
const hash = req.params.hash as string;
if (hash.length === 0) {
res.status(400).json({ error: 'Hash parameter required' });
return;
}
const stored = storage.getKeys(hash);
const keys: MsgCle[] = stored.map((k) => k.msg);
res.json(keys);
} catch (error) {
logger.error({ err: error }, 'Error getting keys');
res.status(500).json({ error: 'Internal server error' });
}
}
function handlePostKey(
storage: StorageServiceInterface,
relay: RelayService,
req: Request,
res: Response,
): void {
void (async (): Promise<void> => {
try {
if (!validateMsgCle(req.body)) {
res.status(400).json({ error: 'Invalid key format' });
return;
}
const key = req.body as MsgCle;
const stored: StoredKey = {
msg: key,
received_at: Date.now(),
relayed: false,
};
storage.storeKey(stored);
await relay.relayKey(key);
stored.relayed = true;
res.status(201).json({ stored: true });
} catch (error) {
logger.error({ err: error }, 'Error storing key');
res.status(500).json({ error: 'Internal server error' });
}
})();
}
export function createKeysRouter(
storage: StorageServiceInterface,
relay: RelayService,
): Router {
const router = Router();
/**
* GET /keys?start=&end= - Get decryption keys in time window (received_at).
* Used for scan-first flow: fetch keys, then fetch messages by hash.
*/
router.get('/', (req: Request, res: Response): void => {
const startRaw = req.query.start as string | undefined;
const endRaw = req.query.end as string | undefined;
if (startRaw === undefined || endRaw === undefined) {
res.status(400).json({ error: 'keys window requires start and end query params' });
return;
}
const start = parseInt(startRaw, 10);
const end = parseInt(endRaw, 10);
if (Number.isNaN(start) || Number.isNaN(end)) {
res.status(400).json({ error: 'invalid start or end' });
return;
}
const stored = storage.getKeysInWindow(start, end);
const keys: MsgCle[] = stored.map((k) => k.msg);
res.json(keys);
});
/**
* GET /keys/:hash - Get decryption keys for a message hash.
*/
router.get('/:hash', (req: Request, res: Response): void => {
try {
const hash = req.params.hash as string;
if (hash.length === 0) {
res.status(400).json({ error: 'Hash parameter required' });
return;
}
const stored = storage.getKeys(hash);
const keys: MsgCle[] = stored.map((k) => k.msg);
res.json(keys);
} catch (error) {
logger.error({ err: error }, 'Error getting keys');
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* POST /keys - Store and relay a decryption key.
*/
router.post('/', (req: Request, res: Response): void => {
void (async (): Promise<void> => {
try {
if (!validateMsgCle(req.body)) {
res.status(400).json({ error: 'Invalid key format' });
return;
}
const key = req.body as MsgCle;
const stored: StoredKey = {
msg: key,
received_at: Date.now(),
relayed: false,
};
storage.storeKey(stored);
await relay.relayKey(key);
stored.relayed = true;
res.status(201).json({ stored: true });
} catch (error) {
logger.error({ err: error }, 'Error storing key');
res.status(500).json({ error: 'Internal server error' });
}
})();
});
router.get('/', (req, res) => handleGetKeysInWindow(storage, req, res));
router.get('/:hash', (req, res) => handleGetKeysByHash(storage, req, res));
router.post('/', (req, res) => handlePostKey(storage, relay, req, res));
return router;
}

View File

@ -5,91 +5,98 @@ import type { MsgChiffre, StoredMessage } from '../types/message.js';
import { validateMsgChiffre } from '../lib/validate.js';
import { logger } from '../lib/logger.js';
function handleGetMessages(
storage: StorageServiceInterface,
req: Request,
res: Response,
): void {
try {
const start = parseInt(req.query.start as string, 10);
const end = parseInt(req.query.end as string, 10);
const service = req.query.service as string | undefined;
if (isNaN(start) || isNaN(end)) {
res.status(400).json({ error: 'start and end timestamps required' });
return;
}
const messages = storage.getMessages(start, end, service);
const msgChiffres: MsgChiffre[] = messages.map((m) => m.msg);
res.json(msgChiffres);
} catch (error) {
logger.error({ err: error }, 'Error getting messages');
res.status(500).json({ error: 'Internal server error' });
}
}
function handlePostMessage(
storage: StorageServiceInterface,
relay: RelayService,
req: Request,
res: Response,
): void {
void (async (): Promise<void> => {
try {
if (!validateMsgChiffre(req.body)) {
res.status(400).json({ error: 'Invalid message format' });
return;
}
const msg = req.body as MsgChiffre;
const alreadySeen = storage.hasSeenHash(msg.hash);
const stored: StoredMessage = {
msg,
received_at: Date.now(),
relayed: false,
};
storage.storeMessage(stored);
if (!alreadySeen) {
await relay.relayMessage(msg);
stored.relayed = true;
}
res.status(201).json({ hash: msg.hash, stored: true });
} catch (error) {
logger.error({ err: error }, 'Error storing message');
res.status(500).json({ error: 'Internal server error' });
}
})();
}
function handleGetMessageByHash(
storage: StorageServiceInterface,
req: Request,
res: Response,
): void {
try {
const hash = req.params.hash as string;
if (hash.length === 0) {
res.status(400).json({ error: 'Hash parameter required' });
return;
}
const stored = storage.getMessage(hash);
if (stored === undefined) {
res.status(404).json({ error: 'Message not found' });
return;
}
res.json(stored.msg);
} catch (error) {
logger.error({ err: error }, 'Error getting message by hash');
res.status(500).json({ error: 'Internal server error' });
}
}
export function createMessagesRouter(
storage: StorageServiceInterface,
relay: RelayService,
): Router {
const router = Router();
/**
* GET /messages - Retrieve encrypted messages in a time window.
* Query params: start (timestamp), end (timestamp), service (optional service UUID)
*/
router.get('/', (req: Request, res: Response): void => {
try {
const start = parseInt(req.query.start as string, 10);
const end = parseInt(req.query.end as string, 10);
const service = req.query.service as string | undefined;
if (isNaN(start) || isNaN(end)) {
res.status(400).json({ error: 'start and end timestamps required' });
return;
}
const messages = storage.getMessages(start, end, service);
const msgChiffres: MsgChiffre[] = messages.map((m) => m.msg);
res.json(msgChiffres);
} catch (error) {
logger.error({ err: error }, 'Error getting messages');
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* POST /messages - Store and relay an encrypted message.
*/
router.post('/', (req: Request, res: Response): void => {
void (async (): Promise<void> => {
try {
if (!validateMsgChiffre(req.body)) {
res.status(400).json({ error: 'Invalid message format' });
return;
}
const msg = req.body as MsgChiffre;
const alreadySeen = storage.hasSeenHash(msg.hash);
const stored: StoredMessage = {
msg,
received_at: Date.now(),
relayed: false,
};
storage.storeMessage(stored);
if (!alreadySeen) {
await relay.relayMessage(msg);
stored.relayed = true;
}
res.status(201).json({ hash: msg.hash, stored: true });
} catch (error) {
logger.error({ err: error }, 'Error storing message');
res.status(500).json({ error: 'Internal server error' });
}
})();
});
/**
* GET /messages/:hash - Get a specific message by hash.
*/
router.get('/:hash', (req: Request, res: Response): void => {
try {
const hash = req.params.hash as string;
if (hash.length === 0) {
res.status(400).json({ error: 'Hash parameter required' });
return;
}
const stored = storage.getMessage(hash);
if (stored === undefined) {
res.status(404).json({ error: 'Message not found' });
return;
}
res.json(stored.msg);
} catch (error) {
logger.error({ err: error }, 'Error getting message by hash');
res.status(500).json({ error: 'Internal server error' });
}
});
router.get('/', (req, res) => handleGetMessages(storage, req, res));
router.post('/', (req, res) => handlePostMessage(storage, relay, req, res));
router.get('/:hash', (req, res) => handleGetMessageByHash(storage, req, res));
return router;
}

View File

@ -5,60 +5,66 @@ import type { MsgSignature, StoredSignature } from '../types/message.js';
import { validateMsgSignature } from '../lib/validate.js';
import { logger } from '../lib/logger.js';
function handleGetSignatures(
storage: StorageServiceInterface,
req: Request,
res: Response,
): void {
try {
const hash = req.params.hash as string;
if (hash.length === 0) {
res.status(400).json({ error: 'Hash parameter required' });
return;
}
const stored = storage.getSignatures(hash);
const signatures: MsgSignature[] = stored.map((s) => s.msg);
res.json(signatures);
} catch (error) {
logger.error({ err: error }, 'Error getting signatures');
res.status(500).json({ error: 'Internal server error' });
}
}
function handlePostSignature(
storage: StorageServiceInterface,
relay: RelayService,
req: Request,
res: Response,
): void {
void (async (): Promise<void> => {
try {
if (!validateMsgSignature(req.body)) {
res.status(400).json({ error: 'Invalid signature format' });
return;
}
const sig = req.body as MsgSignature;
const stored: StoredSignature = {
msg: sig,
received_at: Date.now(),
relayed: false,
};
storage.storeSignature(stored);
await relay.relaySignature(sig);
stored.relayed = true;
res.status(201).json({ stored: true });
} catch (error) {
logger.error({ err: error }, 'Error storing signature');
res.status(500).json({ error: 'Internal server error' });
}
})();
}
export function createSignaturesRouter(
storage: StorageServiceInterface,
relay: RelayService,
): Router {
const router = Router();
/**
* GET /signatures/:hash - Get signatures for a message hash.
*/
router.get('/:hash', (req: Request, res: Response): void => {
try {
const hash = req.params.hash as string;
if (hash.length === 0) {
res.status(400).json({ error: 'Hash parameter required' });
return;
}
const stored = storage.getSignatures(hash);
const signatures: MsgSignature[] = stored.map((s) => s.msg);
res.json(signatures);
} catch (error) {
logger.error({ err: error }, 'Error getting signatures');
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* POST /signatures - Store and relay a signature.
*/
router.post('/', (req: Request, res: Response): void => {
void (async (): Promise<void> => {
try {
if (!validateMsgSignature(req.body)) {
res.status(400).json({ error: 'Invalid signature format' });
return;
}
const sig = req.body as MsgSignature;
const stored: StoredSignature = {
msg: sig,
received_at: Date.now(),
relayed: false,
};
storage.storeSignature(stored);
await relay.relaySignature(sig);
stored.relayed = true;
res.status(201).json({ stored: true });
} catch (error) {
logger.error({ err: error }, 'Error storing signature');
res.status(500).json({ error: 'Internal server error' });
}
})();
});
router.get('/:hash', (req, res) => handleGetSignatures(storage, req, res));
router.post('/', (req, res) => handlePostSignature(storage, relay, req, res));
return router;
}

View File

@ -81,7 +81,7 @@ export class StorageService {
const results: StoredMessage[] = [];
const iter =
candidates !== null
? (function* (self: StorageService) {
? (function* (self: StorageService): Generator<StoredMessage, void, unknown> {
for (const h of candidates) {
const m = self.messages.get(h);
if (m !== undefined) {

View File

@ -1,45 +1,3 @@
⏳ Traitement: 200000/225802 UTXOs insérés...
⏳ Traitement: 210000/225802 UTXOs insérés...
⏳ Traitement: 220000/225802 UTXOs insérés...
💾 Mise à jour des UTXOs dépensés...
📊 Résumé:
- UTXOs vérifiés: 61609
- UTXOs toujours disponibles: 61609
- UTXOs dépensés détectés: 0
📈 Statistiques finales:
- Total UTXOs: 68398
- Dépensés: 6789
- Non dépensés: 61609
✅ Synchronisation terminée
🔍 Démarrage de la synchronisation des UTXOs dépensés...
📊 UTXOs à vérifier: 61609
📡 Récupération des UTXOs depuis Bitcoin...
📊 UTXOs disponibles dans Bitcoin: 225826
💾 Création de la table temporaire...
💾 Insertion des UTXOs disponibles par batch...
⏳ Traitement: 10000/225826 UTXOs insérés...
⏳ Traitement: 20000/225826 UTXOs insérés...
⏳ Traitement: 30000/225826 UTXOs insérés...
⏳ Traitement: 40000/225826 UTXOs insérés...
⏳ Traitement: 50000/225826 UTXOs insérés...
⏳ Traitement: 60000/225826 UTXOs insérés...
⏳ Traitement: 70000/225826 UTXOs insérés...
⏳ Traitement: 80000/225826 UTXOs insérés...
⏳ Traitement: 90000/225826 UTXOs insérés...
⏳ Traitement: 100000/225826 UTXOs insérés...
⏳ Traitement: 110000/225826 UTXOs insérés...
⏳ Traitement: 120000/225826 UTXOs insérés...
⏳ Traitement: 130000/225826 UTXOs insérés...
⏳ Traitement: 140000/225826 UTXOs insérés...
⏳ Traitement: 150000/225826 UTXOs insérés...
⏳ Traitement: 160000/225826 UTXOs insérés...
⏳ Traitement: 170000/225826 UTXOs insérés...
⏳ Traitement: 180000/225826 UTXOs insérés...
⏳ Traitement: 190000/225826 UTXOs insérés...
⏳ Traitement: 200000/225826 UTXOs insérés...
⏳ Traitement: 210000/225826 UTXOs insérés...
⏳ Traitement: 220000/225826 UTXOs insérés...
@ -98,3 +56,45 @@
- Non dépensés: 61609
✅ Synchronisation terminée
🔍 Démarrage de la synchronisation des UTXOs dépensés...
📊 UTXOs à vérifier: 61609
📡 Récupération des UTXOs depuis Bitcoin...
📊 UTXOs disponibles dans Bitcoin: 225855
💾 Création de la table temporaire...
💾 Insertion des UTXOs disponibles par batch...
⏳ Traitement: 10000/225855 UTXOs insérés...
⏳ Traitement: 20000/225855 UTXOs insérés...
⏳ Traitement: 30000/225855 UTXOs insérés...
⏳ Traitement: 40000/225855 UTXOs insérés...
⏳ Traitement: 50000/225855 UTXOs insérés...
⏳ Traitement: 60000/225855 UTXOs insérés...
⏳ Traitement: 70000/225855 UTXOs insérés...
⏳ Traitement: 80000/225855 UTXOs insérés...
⏳ Traitement: 90000/225855 UTXOs insérés...
⏳ Traitement: 100000/225855 UTXOs insérés...
⏳ Traitement: 110000/225855 UTXOs insérés...
⏳ Traitement: 120000/225855 UTXOs insérés...
⏳ Traitement: 130000/225855 UTXOs insérés...
⏳ Traitement: 140000/225855 UTXOs insérés...
⏳ Traitement: 150000/225855 UTXOs insérés...
⏳ Traitement: 160000/225855 UTXOs insérés...
⏳ Traitement: 170000/225855 UTXOs insérés...
⏳ Traitement: 180000/225855 UTXOs insérés...
⏳ Traitement: 190000/225855 UTXOs insérés...
⏳ Traitement: 200000/225855 UTXOs insérés...
⏳ Traitement: 210000/225855 UTXOs insérés...
⏳ Traitement: 220000/225855 UTXOs insérés...
💾 Mise à jour des UTXOs dépensés...
📊 Résumé:
- UTXOs vérifiés: 61609
- UTXOs toujours disponibles: 61609
- UTXOs dépensés détectés: 0
📈 Statistiques finales:
- Total UTXOs: 68398
- Dépensés: 6789
- Non dépensés: 61609
✅ Synchronisation terminée