**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é
175 lines
4.4 KiB
TypeScript
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);
|
|
}
|
|
}
|