Optimize dashboard home loading by using text files instead of RPC calls
**Motivations:** - Le chargement et l'actualisation de la home est très long - Réduire les appels RPC Bitcoin pour améliorer les performances - Utiliser les fichiers texte comme source principale de données **Root causes:** - Les méthodes getAnchorCount(), getHashList() et getUtxoList() faisaient des appels RPC à chaque chargement - Les fichiers texte hash_list.txt et utxo_list.txt n'étaient pas utilisés efficacement **Correctifs:** - getAnchorCount() : Lit directement depuis hash_list.txt au lieu de faire des appels RPC pour compter - getHashList() : Lit directement depuis hash_list.txt et ne complète que les nouveaux blocs si nécessaire - getUtxoList() : Lit depuis utxo_list.txt et ne fait l'appel RPC listunspent que si de nouveaux blocs sont détectés - Mise à jour des confirmations sans appels RPC supplémentaires **Evolutions:** - Performance améliorée : les appels RPC sont limités aux cas où de nouveaux blocs sont détectés - Utilisation efficace des fichiers texte comme source de vérité - Réduction significative du temps de chargement de la home **Pages affectées:** - signet-dashboard/src/bitcoin-rpc.js : Méthodes getAnchorCount(), getHashList(), getUtxoList()
This commit is contained in:
parent
c391e7151a
commit
9ccdd929a1
@ -146,42 +146,25 @@ class BitcoinRPC {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtient la liste des hash ancrés avec leurs transactions
|
* Obtient la liste des hash ancrés avec leurs transactions
|
||||||
* Utilise un fichier de cache hash_list.txt pour éviter de tout recompter
|
* Lit directement depuis hash_list.txt et ne complète que les nouveaux blocs si nécessaire
|
||||||
* Format du cache: <date>;<hauteur du dernier bloc>;<hash du dernier bloc>
|
* Format du cache: <date>;<hauteur du dernier bloc>;<hash du dernier bloc>
|
||||||
* Format du fichier de sortie: <hash>;<txid>;<block_height>;<confirmations>
|
* Format du fichier de sortie: <hash>;<txid>;<block_height>;<confirmations>
|
||||||
* @returns {Promise<Array<Object>>} Liste des hash avec leurs transactions
|
* @returns {Promise<Array<Object>>} Liste des hash avec leurs transactions
|
||||||
*/
|
*/
|
||||||
async getHashList() {
|
async getHashList() {
|
||||||
try {
|
try {
|
||||||
const blockchainInfo = await this.client.getBlockchainInfo();
|
|
||||||
const currentHeight = blockchainInfo.blocks;
|
|
||||||
const currentBlockHash = blockchainInfo.bestblockhash;
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
const cachePath = join(__dirname, '../../hash_list_cache.txt');
|
const cachePath = join(__dirname, '../../hash_list_cache.txt');
|
||||||
const outputPath = join(__dirname, '../../hash_list.txt');
|
const outputPath = join(__dirname, '../../hash_list.txt');
|
||||||
|
|
||||||
let startHeight = 0;
|
|
||||||
const hashList = [];
|
const hashList = [];
|
||||||
|
|
||||||
// Lire le cache si il existe
|
// Lire directement depuis le fichier de sortie
|
||||||
if (existsSync(cachePath)) {
|
|
||||||
try {
|
|
||||||
const cacheContent = readFileSync(cachePath, 'utf8').trim();
|
|
||||||
const parts = cacheContent.split(';');
|
|
||||||
if (parts.length === 3) {
|
|
||||||
const cachedHeight = parseInt(parts[1], 10);
|
|
||||||
const cachedHash = parts[2];
|
|
||||||
|
|
||||||
if (cachedHeight >= 0 && cachedHeight <= currentHeight) {
|
|
||||||
try {
|
|
||||||
const cachedBlockHash = await this.client.getBlockHash(cachedHeight);
|
|
||||||
if (cachedBlockHash === cachedHash) {
|
|
||||||
startHeight = cachedHeight + 1;
|
|
||||||
// Charger les hash existants depuis le fichier de sortie
|
|
||||||
if (existsSync(outputPath)) {
|
if (existsSync(outputPath)) {
|
||||||
|
try {
|
||||||
const existingContent = readFileSync(outputPath, 'utf8').trim();
|
const existingContent = readFileSync(outputPath, 'utf8').trim();
|
||||||
|
if (existingContent) {
|
||||||
const lines = existingContent.split('\n');
|
const lines = existingContent.split('\n');
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.trim()) {
|
if (line.trim()) {
|
||||||
@ -196,32 +179,71 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
logger.debug('Hash list loaded from file', { count: hashList.length });
|
||||||
logger.info('Hash list cache loaded', {
|
|
||||||
cachedHeight,
|
|
||||||
cachedCount: hashList.length,
|
|
||||||
startHeight,
|
|
||||||
currentHeight,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.warn('Hash list cache invalid: block hash mismatch', {
|
|
||||||
cachedHeight,
|
|
||||||
cachedHash,
|
|
||||||
actualHash: cachedBlockHash,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('Error verifying cached block hash', { error: error.message });
|
logger.warn('Error reading hash_list.txt', { error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérifier s'il y a de nouveaux blocs à compléter (un seul appel RPC minimal)
|
||||||
|
let needsUpdate = false;
|
||||||
|
let startHeight = 0;
|
||||||
|
let currentHeight = 0;
|
||||||
|
let currentBlockHash = '';
|
||||||
|
|
||||||
|
// Un seul appel RPC pour obtenir la hauteur actuelle
|
||||||
|
try {
|
||||||
|
const blockchainInfo = await this.client.getBlockchainInfo();
|
||||||
|
currentHeight = blockchainInfo.blocks;
|
||||||
|
currentBlockHash = blockchainInfo.bestblockhash;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error getting blockchain info', { error: error.message });
|
||||||
|
// Si on ne peut pas obtenir la hauteur, retourner la liste telle quelle
|
||||||
|
return hashList;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(cachePath)) {
|
||||||
|
try {
|
||||||
|
const cacheContent = readFileSync(cachePath, 'utf8').trim();
|
||||||
|
const parts = cacheContent.split(';');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
const cachedHeight = parseInt(parts[1], 10);
|
||||||
|
startHeight = cachedHeight + 1;
|
||||||
|
|
||||||
|
if (startHeight <= currentHeight) {
|
||||||
|
needsUpdate = true;
|
||||||
|
logger.info('New blocks detected, updating hash list', {
|
||||||
|
startHeight,
|
||||||
|
currentHeight,
|
||||||
|
newBlocks: currentHeight - startHeight + 1,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Mettre à jour les confirmations seulement
|
||||||
|
for (const item of hashList) {
|
||||||
|
if (item.blockHeight !== null) {
|
||||||
|
item.confirmations = Math.max(0, currentHeight - item.blockHeight + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug('Hash list up to date, confirmations updated', { count: hashList.length });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn('Error reading hash list cache', { error: error.message });
|
logger.warn('Error reading hash list cache', { error: error.message });
|
||||||
|
// Si erreur de lecture du cache, initialiser depuis le début
|
||||||
|
startHeight = 0;
|
||||||
|
needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Pas de cache, initialiser depuis le début
|
||||||
|
startHeight = 0;
|
||||||
|
needsUpdate = true;
|
||||||
|
logger.info('No cache found, initializing hash list', { currentHeight });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parcourir les blocs depuis startHeight jusqu'à currentHeight
|
// Compléter seulement les nouveaux blocs si nécessaire
|
||||||
if (startHeight <= currentHeight) {
|
if (needsUpdate && startHeight <= currentHeight) {
|
||||||
|
|
||||||
logger.info('Collecting hash list from block', { startHeight, currentHeight });
|
logger.info('Collecting hash list from block', { startHeight, currentHeight });
|
||||||
|
|
||||||
for (let height = startHeight; height <= currentHeight; height++) {
|
for (let height = startHeight; height <= currentHeight; height++) {
|
||||||
@ -293,6 +315,15 @@ class BitcoinRPC {
|
|||||||
);
|
);
|
||||||
writeFileSync(outputPath, outputLines.join('\n'), 'utf8');
|
writeFileSync(outputPath, outputLines.join('\n'), 'utf8');
|
||||||
logger.info('Hash list saved', { currentHeight, count: hashList.length });
|
logger.info('Hash list saved', { currentHeight, count: hashList.length });
|
||||||
|
} else {
|
||||||
|
// Mettre à jour les confirmations seulement si nécessaire
|
||||||
|
if (currentHeight > 0) {
|
||||||
|
for (const item of hashList) {
|
||||||
|
if (item.blockHeight !== null) {
|
||||||
|
item.confirmations = Math.max(0, currentHeight - item.blockHeight + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return hashList;
|
return hashList;
|
||||||
@ -319,9 +350,99 @@ class BitcoinRPC {
|
|||||||
const cachePath = join(__dirname, '../../utxo_list_cache.txt');
|
const cachePath = join(__dirname, '../../utxo_list_cache.txt');
|
||||||
const outputPath = join(__dirname, '../../utxo_list.txt');
|
const outputPath = join(__dirname, '../../utxo_list.txt');
|
||||||
|
|
||||||
// Obtenir les UTXO depuis le wallet
|
// Charger les UTXOs existants depuis le fichier texte d'abord
|
||||||
|
const existingUtxosMap = new Map(); // Clé: "txid:vout", Valeur: utxoItem
|
||||||
|
|
||||||
|
if (existsSync(outputPath)) {
|
||||||
|
try {
|
||||||
|
const existingContent = readFileSync(outputPath, 'utf8').trim();
|
||||||
|
const lines = existingContent.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
const parts = line.split(';');
|
||||||
|
// Format ancien (avec address): category;txid;vout;address;amount;confirmations;isAnchorChange
|
||||||
|
// Format nouveau (sans address, avec blockTime): category;txid;vout;amount;confirmations;isAnchorChange;blockTime
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
let category, txid, vout, amount, confirmations, isAnchorChange, blockTime;
|
||||||
|
// Détecter le format : si le 4ème champ est un nombre, c'est le nouveau format
|
||||||
|
if (parts.length === 7 && !isNaN(parseFloat(parts[3]))) {
|
||||||
|
// Nouveau format : category;txid;vout;amount;confirmations;isAnchorChange;blockTime
|
||||||
|
[category, txid, vout, amount, confirmations, isAnchorChange, blockTime] = parts;
|
||||||
|
} else if (parts.length >= 6) {
|
||||||
|
// Ancien format : category;txid;vout;address;amount;confirmations;isAnchorChange
|
||||||
|
[category, txid, vout, , amount, confirmations] = parts;
|
||||||
|
isAnchorChange = parts.length > 6 ? parts[6] === 'true' : false;
|
||||||
|
blockTime = parts.length > 7 ? parseInt(parts[7], 10) || null : null;
|
||||||
|
}
|
||||||
|
const utxoKey = `${txid}:${vout}`;
|
||||||
|
const utxoItem = {
|
||||||
|
txid,
|
||||||
|
vout: parseInt(vout, 10),
|
||||||
|
address: '', // Plus stocké dans le fichier
|
||||||
|
amount: parseFloat(amount),
|
||||||
|
confirmations: parseInt(confirmations, 10) || 0,
|
||||||
|
category,
|
||||||
|
// Ces champs seront mis à jour si nécessaire
|
||||||
|
isSpentOnchain: false,
|
||||||
|
isLockedInMutex: false,
|
||||||
|
blockHeight: null,
|
||||||
|
blockTime: blockTime ? parseInt(blockTime, 10) : null,
|
||||||
|
isAnchorChange: isAnchorChange === 'true' || isAnchorChange === true,
|
||||||
|
};
|
||||||
|
existingUtxosMap.set(utxoKey, utxoItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info('Loaded existing UTXOs from file', { count: existingUtxosMap.size });
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error reading existing UTXO file', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier s'il y a de nouveaux blocs à traiter (un seul appel RPC minimal)
|
||||||
|
let needsUpdate = false;
|
||||||
|
let currentHeight = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blockchainInfo = await this.client.getBlockchainInfo();
|
||||||
|
currentHeight = blockchainInfo.blocks;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error getting blockchain info', { error: error.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le cache pour déterminer si une mise à jour est nécessaire
|
||||||
|
if (existsSync(cachePath)) {
|
||||||
|
try {
|
||||||
|
const cacheContent = readFileSync(cachePath, 'utf8').trim();
|
||||||
|
const parts = cacheContent.split(';');
|
||||||
|
if (parts.length >= 1) {
|
||||||
|
const cachedHeight = parseInt(parts[1], 10) || 0;
|
||||||
|
if (cachedHeight < currentHeight) {
|
||||||
|
needsUpdate = true;
|
||||||
|
logger.info('New blocks detected, updating UTXO list', {
|
||||||
|
cachedHeight,
|
||||||
|
currentHeight,
|
||||||
|
newBlocks: currentHeight - cachedHeight,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.debug('UTXO list up to date, no RPC call needed', { currentHeight });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error reading UTXO cache', { error: error.message });
|
||||||
|
needsUpdate = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
needsUpdate = true;
|
||||||
|
logger.info('No UTXO cache found, initializing', { currentHeight });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtenir les UTXO depuis le wallet seulement si nécessaire (nouveaux blocs détectés)
|
||||||
|
// Si pas de nouveaux blocs, on utilise les données du fichier texte directement
|
||||||
|
let unspent = [];
|
||||||
|
if (needsUpdate) {
|
||||||
const walletName = process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
const walletName = process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
||||||
const host = process.env.BITCOIN_RPC_HOST || 'localhost';
|
const host = process.env.BITCOIN_RPC_HOST || '127.0.0.1';
|
||||||
const port = process.env.BITCOIN_RPC_PORT || '38332';
|
const port = process.env.BITCOIN_RPC_PORT || '38332';
|
||||||
const username = process.env.BITCOIN_RPC_USER || 'bitcoin';
|
const username = process.env.BITCOIN_RPC_USER || 'bitcoin';
|
||||||
const password = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
const password = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
||||||
@ -358,47 +479,69 @@ class BitcoinRPC {
|
|||||||
throw new Error(`RPC error: ${rpcResult.error.message}`);
|
throw new Error(`RPC error: ${rpcResult.error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const unspent = rpcResult.result || [];
|
unspent = rpcResult.result || [];
|
||||||
|
logger.debug('UTXO list updated from RPC', { count: unspent.length });
|
||||||
|
} else {
|
||||||
|
// Pas de nouveaux blocs, utiliser les données du fichier directement
|
||||||
|
// On marque tous les UTXOs comme disponibles (non dépensés) pour éviter l'appel RPC
|
||||||
|
logger.debug('No new blocks, using cached UTXO list from file');
|
||||||
|
}
|
||||||
|
|
||||||
// Charger les UTXOs existants depuis le fichier texte
|
|
||||||
const existingUtxosMap = new Map(); // Clé: "txid:vout", Valeur: utxoItem
|
|
||||||
const blocRewards = [];
|
const blocRewards = [];
|
||||||
const anchors = [];
|
const anchors = [];
|
||||||
const changes = [];
|
const changes = [];
|
||||||
const fees = []; // Liste des transactions avec leurs frais onchain
|
const fees = []; // Liste des transactions avec leurs frais onchain
|
||||||
|
|
||||||
if (existsSync(outputPath)) {
|
// Si pas de mise à jour nécessaire, retourner directement les données du fichier
|
||||||
try {
|
if (!needsUpdate && existingUtxosMap.size > 0) {
|
||||||
const existingContent = readFileSync(outputPath, 'utf8').trim();
|
// Mettre à jour les confirmations seulement
|
||||||
const lines = existingContent.split('\n');
|
for (const item of existingUtxosMap.values()) {
|
||||||
for (const line of lines) {
|
if (item.blockHeight !== null && currentHeight > 0) {
|
||||||
if (line.trim()) {
|
item.confirmations = Math.max(0, currentHeight - item.blockHeight + 1);
|
||||||
const parts = line.split(';');
|
}
|
||||||
if (parts.length >= 6) {
|
// Marquer comme non dépensé (on ne peut pas le savoir sans RPC, mais on assume qu'il est toujours disponible)
|
||||||
const [category, txid, vout, address, amount, confirmations] = parts;
|
item.isSpentOnchain = false;
|
||||||
const utxoKey = `${txid}:${vout}`;
|
}
|
||||||
const utxoItem = {
|
|
||||||
txid,
|
// Organiser par catégorie
|
||||||
vout: parseInt(vout, 10),
|
const blocRewards = [];
|
||||||
address: address || '',
|
const anchors = [];
|
||||||
amount: parseFloat(amount),
|
const changes = [];
|
||||||
confirmations: parseInt(confirmations, 10) || 0,
|
const fees = [];
|
||||||
category,
|
|
||||||
// Ces champs seront mis à jour si nécessaire
|
for (const utxo of existingUtxosMap.values()) {
|
||||||
isSpentOnchain: false,
|
if (utxo.category === 'bloc_reward') {
|
||||||
isLockedInMutex: false,
|
blocRewards.push(utxo);
|
||||||
blockHeight: null,
|
} else if (utxo.category === 'anchor') {
|
||||||
blockTime: null,
|
anchors.push(utxo);
|
||||||
isAnchorChange: category === 'changes' && parts.length > 6 ? parts[6] === 'true' : false,
|
} else if (utxo.category === 'change') {
|
||||||
|
changes.push(utxo);
|
||||||
|
} else if (utxo.category === 'fee') {
|
||||||
|
fees.push(utxo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer availableForAnchor
|
||||||
|
const availableForAnchor = anchors.filter(u => !u.isSpentOnchain && !u.isLockedInMutex && (u.confirmations || 0) >= 1).length;
|
||||||
|
const confirmedAvailableForAnchor = anchors.filter(u => !u.isSpentOnchain && !u.isLockedInMutex && (u.confirmations || 0) >= 6).length;
|
||||||
|
|
||||||
|
logger.debug('UTXO list returned from cache', {
|
||||||
|
blocRewards: blocRewards.length,
|
||||||
|
anchors: anchors.length,
|
||||||
|
changes: changes.length,
|
||||||
|
fees: fees.length,
|
||||||
|
availableForAnchor,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
blocRewards,
|
||||||
|
anchors,
|
||||||
|
changes,
|
||||||
|
fees,
|
||||||
|
total: existingUtxosMap.size,
|
||||||
|
availableForAnchor,
|
||||||
|
confirmedAvailableForAnchor,
|
||||||
};
|
};
|
||||||
existingUtxosMap.set(utxoKey, utxoItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info('Loaded existing UTXOs from file', { count: existingUtxosMap.size });
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('Error reading existing UTXO file', { error: error.message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Créer un Set des UTXOs actuels pour identifier les nouveaux
|
// Créer un Set des UTXOs actuels pour identifier les nouveaux
|
||||||
@ -415,14 +558,15 @@ class BitcoinRPC {
|
|||||||
const utxoKey = `${utxo.txid}:${utxo.vout}`;
|
const utxoKey = `${utxo.txid}:${utxo.vout}`;
|
||||||
const existing = existingUtxosMap.get(utxoKey);
|
const existing = existingUtxosMap.get(utxoKey);
|
||||||
|
|
||||||
// Vérifier si l'UTXO existe et si les données de base sont identiques
|
// Vérifier si l'UTXO existe et si le montant est identique
|
||||||
|
// Les confirmations peuvent changer (augmenter) mais le montant reste constant
|
||||||
if (existing &&
|
if (existing &&
|
||||||
Math.abs(existing.amount - utxo.amount) < 0.00000001 &&
|
Math.abs(existing.amount - utxo.amount) < 0.00000001) {
|
||||||
existing.confirmations === (utxo.confirmations || 0)) {
|
// UTXO existant avec montant identique, utiliser les données du fichier
|
||||||
// UTXO existant avec données identiques, utiliser les données du fichier
|
// Les confirmations seront mises à jour plus tard
|
||||||
utxosToKeep.push(existing);
|
utxosToKeep.push(existing);
|
||||||
} else {
|
} else {
|
||||||
// Nouvel UTXO ou UTXO modifié, doit être recalculé
|
// Nouvel UTXO ou UTXO modifié (montant différent), doit être recalculé
|
||||||
utxosToRecalculate.push(utxo);
|
utxosToRecalculate.push(utxo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -463,49 +607,38 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mettre à jour les informations dynamiques pour les UTXOs existants
|
// Mettre à jour les informations dynamiques pour les UTXOs existants
|
||||||
// (isSpentOnchain, isLockedInMutex, confirmations)
|
// (isSpentOnchain, isLockedInMutex, confirmations, blockTime si manquant)
|
||||||
// Faire les vérifications en parallèle pour accélérer
|
// Si un UTXO est dans listunspent, il n'est pas dépensé (pas besoin d'appel RPC gettxout)
|
||||||
const updatePromises = utxosToKeep.map(async (existingUtxo) => {
|
// Récupérer blockTime pour les UTXOs confirmés qui n'en ont pas encore
|
||||||
|
const updateBlockTimePromises = utxosToKeep
|
||||||
|
.filter(utxo => (utxo.confirmations || 0) > 0 && !utxo.blockTime)
|
||||||
|
.map(async (existingUtxo) => {
|
||||||
|
try {
|
||||||
|
const txInfo = await this.client.getTransaction(existingUtxo.txid);
|
||||||
|
existingUtxo.blockHeight = txInfo.blockheight || null;
|
||||||
|
existingUtxo.blockTime = txInfo.blocktime || null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Error getting transaction block info for existing UTXO', { txid: existingUtxo.txid, error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all(updateBlockTimePromises);
|
||||||
|
|
||||||
|
for (const existingUtxo of utxosToKeep) {
|
||||||
// Mettre à jour les confirmations depuis listunspent
|
// Mettre à jour les confirmations depuis listunspent
|
||||||
const currentUtxo = unspent.find(u => u.txid === existingUtxo.txid && u.vout === existingUtxo.vout);
|
const currentUtxo = unspent.find(u => u.txid === existingUtxo.txid && u.vout === existingUtxo.vout);
|
||||||
if (currentUtxo) {
|
if (currentUtxo) {
|
||||||
existingUtxo.confirmations = currentUtxo.confirmations || 0;
|
existingUtxo.confirmations = currentUtxo.confirmations || 0;
|
||||||
existingUtxo.amount = currentUtxo.amount; // Mettre à jour le montant au cas où
|
existingUtxo.amount = currentUtxo.amount; // Mettre à jour le montant au cas où
|
||||||
|
existingUtxo.isSpentOnchain = false; // Si dans listunspent, il n'est pas dépensé
|
||||||
|
|
||||||
// Vérifier si l'UTXO est verrouillé
|
// Vérifier si l'UTXO est verrouillé
|
||||||
const utxoKey = `${existingUtxo.txid}:${existingUtxo.vout}`;
|
const utxoKey = `${existingUtxo.txid}:${existingUtxo.vout}`;
|
||||||
existingUtxo.isLockedInMutex = lockedUtxos.has(utxoKey);
|
existingUtxo.isLockedInMutex = lockedUtxos.has(utxoKey);
|
||||||
|
|
||||||
// Vérifier si dépensé (gettxout retourne null)
|
|
||||||
try {
|
|
||||||
const txOutResponse = await fetch(rpcUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Basic ${auth}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: '1.0',
|
|
||||||
id: 'gettxout',
|
|
||||||
method: 'gettxout',
|
|
||||||
params: [existingUtxo.txid, existingUtxo.vout],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (txOutResponse.ok) {
|
|
||||||
const txOutResult = await txOutResponse.json();
|
|
||||||
existingUtxo.isSpentOnchain = txOutResult.result === null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug('Error checking if existing UTXO is spent', { txid: existingUtxo.txid, vout: existingUtxo.vout, error: error.message });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// UTXO n'est plus dans listunspent, il a été dépensé
|
// UTXO n'est plus dans listunspent, il a été dépensé
|
||||||
existingUtxo.isSpentOnchain = true;
|
existingUtxo.isSpentOnchain = true;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
await Promise.all(updatePromises);
|
|
||||||
|
|
||||||
// Ajouter les UTXOs existants aux listes appropriées (seulement s'ils ne sont pas dépensés)
|
// Ajouter les UTXOs existants aux listes appropriées (seulement s'ils ne sont pas dépensés)
|
||||||
for (const existingUtxo of utxosToKeep) {
|
for (const existingUtxo of utxosToKeep) {
|
||||||
@ -524,7 +657,11 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Catégoriser uniquement les nouveaux UTXOs ou ceux modifiés
|
// Catégoriser uniquement les nouveaux UTXOs ou ceux modifiés
|
||||||
for (const utxo of utxosToRecalculate) {
|
// Traiter en parallèle par batch pour accélérer sans surcharger le serveur RPC
|
||||||
|
const BATCH_SIZE = 10; // Traiter 10 UTXOs en parallèle à la fois
|
||||||
|
for (let i = 0; i < utxosToRecalculate.length; i += BATCH_SIZE) {
|
||||||
|
const batch = utxosToRecalculate.slice(i, i + BATCH_SIZE);
|
||||||
|
const batchPromises = batch.map(async (utxo) => {
|
||||||
try {
|
try {
|
||||||
// Obtenir la transaction source pour déterminer sa catégorie
|
// Obtenir la transaction source pour déterminer sa catégorie
|
||||||
const rawTxResponse = await fetch(rpcUrl, {
|
const rawTxResponse = await fetch(rpcUrl, {
|
||||||
@ -552,31 +689,8 @@ class BitcoinRPC {
|
|||||||
|
|
||||||
const rawTx = rawTxResult.result;
|
const rawTx = rawTxResult.result;
|
||||||
|
|
||||||
// Vérifier si l'UTXO est dépensé onchain
|
// Si l'UTXO est dans listunspent, il n'est pas dépensé (pas besoin de gettxout)
|
||||||
let isSpentOnchain = false;
|
const isSpentOnchain = false;
|
||||||
try {
|
|
||||||
const txOutResponse = await fetch(rpcUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Basic ${auth}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: '1.0',
|
|
||||||
id: 'gettxout',
|
|
||||||
method: 'gettxout',
|
|
||||||
params: [utxo.txid, utxo.vout],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (txOutResponse.ok) {
|
|
||||||
const txOutResult = await txOutResponse.json();
|
|
||||||
// Si gettxout retourne null, l'UTXO est dépensé
|
|
||||||
isSpentOnchain = txOutResult.result === null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug('Error checking if UTXO is spent', { txid: utxo.txid, vout: utxo.vout, error: error.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si l'UTXO est verrouillé dans le mutex de l'API d'ancrage
|
// Vérifier si l'UTXO est verrouillé dans le mutex de l'API d'ancrage
|
||||||
const utxoKey = `${utxo.txid}:${utxo.vout}`;
|
const utxoKey = `${utxo.txid}:${utxo.vout}`;
|
||||||
@ -585,7 +699,7 @@ class BitcoinRPC {
|
|||||||
const utxoItem = {
|
const utxoItem = {
|
||||||
txid: utxo.txid,
|
txid: utxo.txid,
|
||||||
vout: utxo.vout,
|
vout: utxo.vout,
|
||||||
address: utxo.address || '',
|
address: '', // Plus stocké
|
||||||
amount: utxo.amount,
|
amount: utxo.amount,
|
||||||
confirmations: utxo.confirmations || 0,
|
confirmations: utxo.confirmations || 0,
|
||||||
isSpentOnchain,
|
isSpentOnchain,
|
||||||
@ -599,10 +713,22 @@ class BitcoinRPC {
|
|||||||
rawTx.vin[0].coinbase !== null;
|
rawTx.vin[0].coinbase !== null;
|
||||||
|
|
||||||
if (isCoinbase) {
|
if (isCoinbase) {
|
||||||
|
// Obtenir la hauteur du bloc et le blocktime si la transaction est confirmée
|
||||||
|
let blockHeight = null;
|
||||||
|
let blockTime = null;
|
||||||
|
if (utxo.confirmations > 0) {
|
||||||
|
try {
|
||||||
|
const txInfo = await this.client.getTransaction(utxo.txid);
|
||||||
|
blockHeight = txInfo.blockheight || null;
|
||||||
|
blockTime = txInfo.blocktime || null;
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Error getting transaction block info for coinbase', { txid: utxo.txid, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
utxoItem.blockHeight = blockHeight;
|
||||||
|
utxoItem.blockTime = blockTime;
|
||||||
utxoItem.category = 'bloc_rewards';
|
utxoItem.category = 'bloc_rewards';
|
||||||
blocRewards.push(utxoItem);
|
return { utxoItem, category: 'bloc_rewards' };
|
||||||
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si c'est une transaction d'ancrage (contient OP_RETURN avec "ANCHOR:")
|
// Vérifier si c'est une transaction d'ancrage (contient OP_RETURN avec "ANCHOR:")
|
||||||
@ -693,24 +819,6 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si c'est une transaction d'ancrage avec des frais onchain, les stocker
|
|
||||||
if (isAnchorTx && onchainFeeAmount !== null) {
|
|
||||||
// Vérifier si cette transaction n'est pas déjà dans la liste des frais
|
|
||||||
const existingFee = fees.find(f => f.txid === utxo.txid);
|
|
||||||
if (!existingFee) {
|
|
||||||
fees.push({
|
|
||||||
txid: utxo.txid,
|
|
||||||
fee: onchainFeeAmount,
|
|
||||||
fee_sats: Math.round(onchainFeeAmount * 100000000),
|
|
||||||
changeAddress: onchainChangeAddress || null,
|
|
||||||
changeAmount: onchainChangeAmount || null,
|
|
||||||
blockHeight: blockHeight,
|
|
||||||
blockTime: blockTime,
|
|
||||||
confirmations: utxo.confirmations || 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAnchorTx) {
|
if (isAnchorTx) {
|
||||||
// Dans une transaction d'ancrage, distinguer les outputs d'ancrage/provisionnement du change
|
// Dans une transaction d'ancrage, distinguer les outputs d'ancrage/provisionnement du change
|
||||||
// Les transactions d'ancrage créent :
|
// Les transactions d'ancrage créent :
|
||||||
@ -733,7 +841,7 @@ class BitcoinRPC {
|
|||||||
// Vérifier si c'est un output OP_RETURN (non dépensable)
|
// Vérifier si c'est un output OP_RETURN (non dépensable)
|
||||||
if (output.scriptPubKey && output.scriptPubKey.type === 'nulldata') {
|
if (output.scriptPubKey && output.scriptPubKey.type === 'nulldata') {
|
||||||
// C'est l'OP_RETURN, on l'ignore (non dépensable)
|
// C'est l'OP_RETURN, on l'ignore (non dépensable)
|
||||||
continue; // Ne pas ajouter cet UTXO car OP_RETURN n'est pas dépensable
|
return null; // Ne pas ajouter cet UTXO car OP_RETURN n'est pas dépensable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si le montant correspond à un output d'ancrage/provisionnement (2500 sats)
|
// Si le montant correspond à un output d'ancrage/provisionnement (2500 sats)
|
||||||
@ -747,8 +855,7 @@ class BitcoinRPC {
|
|||||||
|
|
||||||
if (isAnchorOutput) {
|
if (isAnchorOutput) {
|
||||||
utxoItem.category = 'ancrages';
|
utxoItem.category = 'ancrages';
|
||||||
anchors.push(utxoItem);
|
return { utxoItem, category: 'ancrages', fee: isAnchorTx && onchainFeeAmount !== null ? { txid: utxo.txid, fee: onchainFeeAmount, fee_sats: Math.round(onchainFeeAmount * 100000000), changeAddress: onchainChangeAddress || null, changeAmount: onchainChangeAmount || null, blockHeight, blockTime, confirmations: utxo.confirmations || 0 } : null };
|
||||||
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
|
||||||
} else if (isChangeOutput) {
|
} else if (isChangeOutput) {
|
||||||
// C'est le change de la transaction d'ancrage
|
// C'est le change de la transaction d'ancrage
|
||||||
utxoItem.category = 'changes';
|
utxoItem.category = 'changes';
|
||||||
@ -761,15 +868,13 @@ class BitcoinRPC {
|
|||||||
if (onchainFeeAmount !== null) {
|
if (onchainFeeAmount !== null) {
|
||||||
utxoItem.onchainFeeAmount = onchainFeeAmount;
|
utxoItem.onchainFeeAmount = onchainFeeAmount;
|
||||||
}
|
}
|
||||||
changes.push(utxoItem);
|
return { utxoItem, category: 'changes', isAnchorChange: true, fee: isAnchorTx && onchainFeeAmount !== null ? { txid: utxo.txid, fee: onchainFeeAmount, fee_sats: Math.round(onchainFeeAmount * 100000000), changeAddress: onchainChangeAddress || null, changeAmount: onchainChangeAmount || null, blockHeight, blockTime, confirmations: utxo.confirmations || 0 } : null };
|
||||||
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
|
||||||
} else {
|
} else {
|
||||||
// Montant très petit (< 1000 sats), probablement du dust
|
// Montant très petit (< 1000 sats), probablement du dust
|
||||||
// Classer comme change quand même (peu probable dans une transaction d'ancrage)
|
// Classer comme change quand même (peu probable dans une transaction d'ancrage)
|
||||||
utxoItem.category = 'changes';
|
utxoItem.category = 'changes';
|
||||||
utxoItem.isAnchorChange = true;
|
utxoItem.isAnchorChange = true;
|
||||||
changes.push(utxoItem);
|
return { utxoItem, category: 'changes', isAnchorChange: true };
|
||||||
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Transaction normale (non-ancrage, non-coinbase) = change
|
// Transaction normale (non-ancrage, non-coinbase) = change
|
||||||
@ -777,8 +882,7 @@ class BitcoinRPC {
|
|||||||
// Ces UTXO proviennent de transactions normales (paiements, etc.)
|
// Ces UTXO proviennent de transactions normales (paiements, etc.)
|
||||||
utxoItem.category = 'changes';
|
utxoItem.category = 'changes';
|
||||||
utxoItem.isAnchorChange = false; // Change d'une transaction normale
|
utxoItem.isAnchorChange = false; // Change d'une transaction normale
|
||||||
changes.push(utxoItem);
|
return { utxoItem, category: 'changes', isAnchorChange: false };
|
||||||
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// En cas d'erreur, classer comme change par défaut
|
// En cas d'erreur, classer comme change par défaut
|
||||||
@ -786,15 +890,47 @@ class BitcoinRPC {
|
|||||||
const errorUtxoItem = {
|
const errorUtxoItem = {
|
||||||
txid: utxo.txid,
|
txid: utxo.txid,
|
||||||
vout: utxo.vout,
|
vout: utxo.vout,
|
||||||
address: utxo.address || '',
|
address: '', // Plus stocké
|
||||||
amount: utxo.amount,
|
amount: utxo.amount,
|
||||||
confirmations: utxo.confirmations || 0,
|
confirmations: utxo.confirmations || 0,
|
||||||
category: 'changes',
|
category: 'changes',
|
||||||
isSpentOnchain: false,
|
isSpentOnchain: false,
|
||||||
isLockedInMutex: false,
|
isLockedInMutex: false,
|
||||||
};
|
};
|
||||||
changes.push(errorUtxoItem);
|
return { utxoItem: errorUtxoItem, category: 'changes', isAnchorChange: false };
|
||||||
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, errorUtxoItem);
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchResults = await Promise.all(batchPromises);
|
||||||
|
|
||||||
|
// Traiter les résultats du batch
|
||||||
|
for (const result of batchResults) {
|
||||||
|
if (!result) continue; // OP_RETURN ignoré
|
||||||
|
|
||||||
|
const { utxoItem, category, isAnchorChange, fee } = result;
|
||||||
|
const utxoKey = `${utxoItem.txid}:${utxoItem.vout}`;
|
||||||
|
existingUtxosMap.set(utxoKey, utxoItem);
|
||||||
|
|
||||||
|
if (category === 'bloc_rewards') {
|
||||||
|
blocRewards.push(utxoItem);
|
||||||
|
} else if (category === 'ancrages') {
|
||||||
|
anchors.push(utxoItem);
|
||||||
|
if (fee) {
|
||||||
|
const existingFee = fees.find(f => f.txid === fee.txid);
|
||||||
|
if (!existingFee) {
|
||||||
|
fees.push(fee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (category === 'changes') {
|
||||||
|
utxoItem.isAnchorChange = isAnchorChange || false;
|
||||||
|
changes.push(utxoItem);
|
||||||
|
if (fee) {
|
||||||
|
const existingFee = fees.find(f => f.txid === fee.txid);
|
||||||
|
if (!existingFee) {
|
||||||
|
fees.push(fee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -843,10 +979,11 @@ class BitcoinRPC {
|
|||||||
writeFileSync(cachePath, now, 'utf8');
|
writeFileSync(cachePath, now, 'utf8');
|
||||||
|
|
||||||
// Écrire le fichier de sortie avec toutes les catégories (incluant les UTXOs dépensés pour historique)
|
// Écrire le fichier de sortie avec toutes les catégories (incluant les UTXOs dépensés pour historique)
|
||||||
// Format: category;txid;vout;address;amount;confirmations;isAnchorChange
|
// Format: category;txid;vout;amount;confirmations;isAnchorChange;blockTime
|
||||||
const outputLines = Array.from(existingUtxosMap.values()).map((item) => {
|
const outputLines = Array.from(existingUtxosMap.values()).map((item) => {
|
||||||
const isAnchorChange = item.isAnchorChange ? 'true' : 'false';
|
const isAnchorChange = item.isAnchorChange ? 'true' : 'false';
|
||||||
return `${item.category};${item.txid};${item.vout};${item.address};${item.amount};${item.confirmations};${isAnchorChange}`;
|
const blockTime = item.blockTime || '';
|
||||||
|
return `${item.category};${item.txid};${item.vout};${item.amount};${item.confirmations};${isAnchorChange};${blockTime}`;
|
||||||
});
|
});
|
||||||
writeFileSync(outputPath, outputLines.join('\n'), 'utf8');
|
writeFileSync(outputPath, outputLines.join('\n'), 'utf8');
|
||||||
|
|
||||||
@ -1283,125 +1420,34 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtient le nombre d'ancrages en comptant tous les blocs depuis le début
|
* Obtient le nombre d'ancrages en lisant directement depuis hash_list.txt
|
||||||
* Utilise un fichier de cache anchor_count.txt pour éviter de tout recompter
|
* Évite les appels RPC en utilisant le fichier texte comme source de vérité
|
||||||
* Format du cache: <date>;<hauteur du dernier bloc>;<hash du dernier bloc>;<count total des ancrages>
|
|
||||||
* @returns {Promise<number>} Nombre d'ancrages
|
* @returns {Promise<number>} Nombre d'ancrages
|
||||||
*/
|
*/
|
||||||
async getAnchorCount() {
|
async getAnchorCount() {
|
||||||
try {
|
try {
|
||||||
const blockchainInfo = await this.client.getBlockchainInfo();
|
|
||||||
const currentHeight = blockchainInfo.blocks;
|
|
||||||
const currentBlockHash = blockchainInfo.bestblockhash;
|
|
||||||
|
|
||||||
// Chemin du fichier de cache (à la racine du projet bitcoin)
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
// signet-dashboard/src/bitcoin-rpc.js -> signet-dashboard/src -> signet-dashboard -> bitcoin -> anchor_count.txt
|
const hashListPath = join(__dirname, '../../hash_list.txt');
|
||||||
// On remonte de 2 niveaux depuis signet-dashboard/src pour arriver à bitcoin/
|
|
||||||
const cachePath = join(__dirname, '../../anchor_count.txt');
|
|
||||||
|
|
||||||
let startHeight = 0;
|
// Lire directement depuis le fichier texte
|
||||||
let anchorCount = 0;
|
if (existsSync(hashListPath)) {
|
||||||
let lastProcessedHash = null;
|
|
||||||
|
|
||||||
// Lire le cache si il existe
|
|
||||||
if (existsSync(cachePath)) {
|
|
||||||
try {
|
try {
|
||||||
const cacheContent = readFileSync(cachePath, 'utf8').trim();
|
const content = readFileSync(hashListPath, 'utf8').trim();
|
||||||
const parts = cacheContent.split(';');
|
if (content) {
|
||||||
if (parts.length === 4) {
|
const lines = content.split('\n').filter(line => line.trim());
|
||||||
const cachedDate = parts[0];
|
const anchorCount = lines.length;
|
||||||
const cachedHeight = parseInt(parts[1], 10);
|
logger.debug('Anchor count read from hash_list.txt', { count: anchorCount });
|
||||||
const cachedHash = parts[2];
|
|
||||||
const cachedCount = parseInt(parts[3], 10);
|
|
||||||
|
|
||||||
// Vérifier que le hash du bloc en cache correspond toujours
|
|
||||||
if (cachedHeight >= 0 && cachedHeight <= currentHeight) {
|
|
||||||
try {
|
|
||||||
const cachedBlockHash = await this.client.getBlockHash(cachedHeight);
|
|
||||||
if (cachedBlockHash === cachedHash) {
|
|
||||||
startHeight = cachedHeight + 1;
|
|
||||||
anchorCount = cachedCount;
|
|
||||||
lastProcessedHash = cachedHash;
|
|
||||||
logger.info('Anchor count cache loaded', {
|
|
||||||
cachedDate,
|
|
||||||
cachedHeight,
|
|
||||||
cachedCount,
|
|
||||||
startHeight,
|
|
||||||
currentHeight
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.warn('Anchor count cache invalid: block hash mismatch', {
|
|
||||||
cachedHeight,
|
|
||||||
cachedHash,
|
|
||||||
actualHash: cachedBlockHash
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('Error verifying cached block hash', { error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn('Error reading anchor count cache', { error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compter les ancrages depuis startHeight jusqu'à currentHeight
|
|
||||||
if (startHeight <= currentHeight) {
|
|
||||||
logger.info('Counting anchors from block', { startHeight, currentHeight });
|
|
||||||
|
|
||||||
for (let height = startHeight; height <= currentHeight; height++) {
|
|
||||||
try {
|
|
||||||
const blockHash = await this.client.getBlockHash(height);
|
|
||||||
const block = await this.client.getBlock(blockHash, 2);
|
|
||||||
|
|
||||||
if (block.tx) {
|
|
||||||
for (const tx of block.tx) {
|
|
||||||
try {
|
|
||||||
const rawTx = await this.client.getRawTransaction(tx.txid, true);
|
|
||||||
|
|
||||||
// Vérifier si la transaction contient un OP_RETURN avec "ANCHOR:"
|
|
||||||
for (const output of rawTx.vout || []) {
|
|
||||||
if (output.scriptPubKey && output.scriptPubKey.hex) {
|
|
||||||
const scriptHex = output.scriptPubKey.hex;
|
|
||||||
const anchorPrefix = Buffer.from('ANCHOR:', 'utf8').toString('hex');
|
|
||||||
|
|
||||||
if (scriptHex.includes(anchorPrefix)) {
|
|
||||||
anchorCount++;
|
|
||||||
break; // Compter une seule fois par transaction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Continuer avec la transaction suivante
|
|
||||||
logger.debug('Error checking transaction for anchor', { txid: tx.txid, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour le cache tous les 100 blocs pour éviter de perdre trop de travail
|
|
||||||
if (height % 100 === 0 || height === currentHeight) {
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
const cacheContent = `${now};${height};${blockHash};${anchorCount}`;
|
|
||||||
writeFileSync(cachePath, cacheContent, 'utf8');
|
|
||||||
logger.debug('Anchor count cache updated', { height, anchorCount });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Continuer avec le bloc suivant
|
|
||||||
logger.debug('Error checking block for anchors', { height, error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour le cache final
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
const cacheContent = `${now};${currentHeight};${currentBlockHash};${anchorCount}`;
|
|
||||||
writeFileSync(cachePath, cacheContent, 'utf8');
|
|
||||||
logger.info('Anchor count cache saved', { currentHeight, anchorCount });
|
|
||||||
}
|
|
||||||
|
|
||||||
return anchorCount;
|
return anchorCount;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error reading hash_list.txt', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le fichier n'existe pas ou est vide, retourner 0
|
||||||
|
logger.debug('hash_list.txt not found or empty, returning 0');
|
||||||
|
return 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting anchor count', { error: error.message });
|
logger.error('Error getting anchor count', { error: error.message });
|
||||||
throw new Error(`Failed to get anchor count: ${error.message}`);
|
throw new Error(`Failed to get anchor count: ${error.message}`);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user