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

175 lines
4.4 KiB
TypeScript

import { getSignatures } from './relay';
import { getStoredPairs } from './pairing';
import { hasEnoughSignatures, collectProgress } from './loginValidation';
import type { LoginPath } from '../types/identity';
import type { MsgSignature } from '../types/message';
export interface ProofSignature {
signature: string;
cle_publique: string;
nonce: string;
pair_uuid: string;
}
/**
* Fetch signatures for hash from all relays, aggregate and deduplicate.
*/
export async function fetchSignaturesForHash(
relayEndpoints: string[],
hash: string,
): Promise<MsgSignature[]> {
const seen = new Set<string>();
const out: MsgSignature[] = [];
for (const ep of relayEndpoints) {
try {
const list = await getSignatures(ep, hash);
for (const m of list) {
const s = m.signature;
const key = `${s.cle_publique}\t${s.nonce}\t${s.signature}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
out.push(m);
}
} catch {
continue;
}
}
return out;
}
/**
* Map pair_uuid -> membres_parents_uuid for pairs in pairsAttendus.
*/
export function buildPairToMembers(
pairsAttendus: string[],
): Map<string, string[]> {
const pairs = getStoredPairs();
const set = new Set(pairsAttendus);
const m = new Map<string, string[]>();
for (const p of pairs) {
if (!set.has(p.uuid)) {
continue;
}
m.set(p.uuid, p.membres_parents_uuid);
}
return m;
}
/**
* Map cle_publique -> pair_uuid for local (identity) and remote (pair.publicKey) pairs.
*/
export function buildPubkeyToPair(
identityPublicKey: string,
pairsAttendus: string[],
): Map<string, string> {
const pairs = getStoredPairs();
const set = new Set(pairsAttendus);
const m = new Map<string, string>();
for (const p of pairs) {
if (!set.has(p.uuid)) {
continue;
}
if (p.is_local) {
m.set(identityPublicKey, p.uuid);
} else if (p.publicKey !== undefined) {
m.set(p.publicKey, p.uuid);
}
}
return m;
}
/**
* Map MsgSignature[] to ProofSignature[] using pubkey->pair. Skip unknown pubkeys.
*/
export function mapMsgSignaturesToProofFormat(
msgs: MsgSignature[],
hash: string,
pubkeyToPair: Map<string, string>,
): ProofSignature[] {
const out: ProofSignature[] = [];
for (const m of msgs) {
const s = m.signature;
if (s.hash !== hash) {
continue;
}
const pairUuid = pubkeyToPair.get(s.cle_publique);
if (pairUuid === undefined) {
continue;
}
out.push({
signature: s.signature,
cle_publique: s.cle_publique,
nonce: s.nonce,
pair_uuid: pairUuid,
});
}
return out;
}
export const COLLECT_POLL_MS = 2000;
export const COLLECT_TIMEOUT_MS = 300000;
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
/**
* Merge our sigs with fetched, dedup by pair_uuid (keep first).
*/
function mergeDedupByPair(
ours: ProofSignature[],
fetched: ProofSignature[],
): ProofSignature[] {
const byPair = new Map<string, ProofSignature>();
for (const s of ours) {
byPair.set(s.pair_uuid, s);
}
for (const s of fetched) {
if (!byPair.has(s.pair_uuid)) {
byPair.set(s.pair_uuid, s);
}
}
return Array.from(byPair.values());
}
export interface CollectLoopOpts {
pollMs: number;
timeoutMs: number;
/** Called each poll with current merged sigs (notifications relais, UI progress). */
onProgress?: (merged: ProofSignature[]) => void;
}
/**
* Collect signatures from relays until we have enough per member, or timeout.
* Optional onProgress called each poll for UI (e.g. X/Y signatures).
*/
export async function runCollectLoop(
relayEndpoints: string[],
hash: string,
ourSigs: ProofSignature[],
path: LoginPath,
pairToMembers: Map<string, string[]>,
pubkeyToPair: Map<string, string>,
opts: CollectLoopOpts,
): Promise<ProofSignature[]> {
const start = Date.now();
let merged = ourSigs;
for (;;) {
opts.onProgress?.(merged);
if (hasEnoughSignatures(path, merged, pairToMembers)) {
return merged;
}
if (Date.now() - start >= opts.timeoutMs) {
throw new Error('Collecte distante : timeout (signatures manquantes)');
}
const msgs = await fetchSignaturesForHash(relayEndpoints, hash);
const fetched = mapMsgSignaturesToProofFormat(msgs, hash, pubkeyToPair);
merged = mergeDedupByPair(ourSigs, fetched);
await delay(opts.pollMs);
}
}