**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é
245 lines
7.8 KiB
Markdown
245 lines
7.8 KiB
Markdown
# Pagination côté serveur avec base de données
|
|
|
|
**Date:** 2026-01-27
|
|
**Auteur:** Équipe 4NK
|
|
|
|
## Objectif
|
|
|
|
Implémenter la pagination côté serveur pour les endpoints `/api/utxo/list` et `/api/hash/list` afin de réduire la consommation mémoire en ne chargeant que les données nécessaires.
|
|
|
|
## Problème identifié
|
|
|
|
### Root cause
|
|
|
|
La pagination était effectuée côté client (frontend) :
|
|
- Le serveur retournait **TOUS** les UTXOs/hashes (68k+ UTXOs, 32k+ hashes)
|
|
- Le frontend chargeait toutes les données en mémoire
|
|
- La pagination se faisait ensuite en JavaScript avec `.slice()`
|
|
|
|
**Impact :**
|
|
- Toutes les données chargées en mémoire côté serveur ET côté client
|
|
- Consommation mémoire inutile pour afficher seulement 50 éléments par page
|
|
- Temps de réponse lent pour charger toutes les données
|
|
|
|
## Solution
|
|
|
|
### Stratégie
|
|
|
|
Implémenter la pagination côté serveur avec :
|
|
1. **Paramètres de pagination** : `page` et `limit` dans les query parameters
|
|
2. **Requêtes SQL paginées** : Utiliser `LIMIT` et `OFFSET` dans les requêtes SQL
|
|
3. **Métadonnées de pagination** : Retourner `total`, `totalPages`, `page`, `limit`
|
|
4. **Compatibilité frontend** : Adapter le frontend pour utiliser la pagination serveur
|
|
|
|
### Avantages
|
|
|
|
- **Réduction mémoire** : Ne charger que les données de la page demandée
|
|
- **Performance** : Requêtes SQL indexées avec LIMIT/OFFSET beaucoup plus rapides
|
|
- **Scalabilité** : Fonctionne même avec des millions d'UTXOs/hashes
|
|
|
|
## Modifications
|
|
|
|
### 1. Endpoint `/api/hash/list` avec pagination
|
|
|
|
**Fichier:** `signet-dashboard/src/server.js`
|
|
|
|
**Avant:**
|
|
```javascript
|
|
app.get('/api/hash/list', async (req, res) => {
|
|
const hashList = await bitcoinRPC.getHashList(); // Charge TOUS les hashes
|
|
res.json({ hashes: hashList, count: hashList.length });
|
|
});
|
|
```
|
|
|
|
**Après:**
|
|
```javascript
|
|
app.get('/api/hash/list', async (req, res) => {
|
|
const page = parseInt(req.query.page || '1', 10);
|
|
const limit = parseInt(req.query.limit || '50', 10);
|
|
const offset = (page - 1) * limit;
|
|
|
|
const db = getDatabase();
|
|
const totalCount = db.prepare('SELECT COUNT(*) as count FROM anchors').get();
|
|
const hashes = db.prepare(`
|
|
SELECT hash, txid, block_height, confirmations, date
|
|
FROM anchors
|
|
ORDER BY block_height ASC, id ASC
|
|
LIMIT ? OFFSET ?
|
|
`).all(limit, offset);
|
|
|
|
res.json({
|
|
hashes,
|
|
count: hashes.length,
|
|
total: totalCount?.count || 0,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil((totalCount?.count || 0) / limit),
|
|
});
|
|
});
|
|
```
|
|
|
|
### 2. Endpoint `/api/utxo/list` avec pagination par catégorie
|
|
|
|
**Fichier:** `signet-dashboard/src/server.js`
|
|
|
|
**Avant:**
|
|
```javascript
|
|
app.get('/api/utxo/list', async (req, res) => {
|
|
const utxoData = await bitcoinRPC.getUtxoList(); // Charge TOUS les UTXOs
|
|
res.json({
|
|
blocRewards: utxoData.blocRewards, // Tous
|
|
anchors: utxoData.anchors, // Tous
|
|
changes: utxoData.changes, // Tous
|
|
fees: utxoData.fees || [], // Tous
|
|
});
|
|
});
|
|
```
|
|
|
|
**Après:**
|
|
```javascript
|
|
app.get('/api/utxo/list', async (req, res) => {
|
|
const category = req.query.category || 'all';
|
|
const page = parseInt(req.query.page || '1', 10);
|
|
const limit = parseInt(req.query.limit || '50', 10);
|
|
const offset = (page - 1) * limit;
|
|
|
|
// Requête SQL paginée selon la catégorie
|
|
if (category === 'ancrages') {
|
|
utxos = db.prepare(`
|
|
SELECT txid, vout, address, amount, confirmations, category,
|
|
is_spent_onchain, is_locked_in_mutex, block_time, is_anchor_change
|
|
FROM utxos
|
|
WHERE category = 'ancrages' OR category = 'anchor'
|
|
ORDER BY amount DESC
|
|
LIMIT ? OFFSET ?
|
|
`).all(limit, offset);
|
|
}
|
|
// ... autres catégories
|
|
});
|
|
```
|
|
|
|
### 3. Adaptation du frontend
|
|
|
|
**Fichier:** `signet-dashboard/public/utxo-list.html`
|
|
|
|
**Avant:**
|
|
```javascript
|
|
const response = await fetch(`${API_BASE_URL}/api/utxo/list`);
|
|
const data = await response.json();
|
|
// Charge TOUS les UTXOs, puis pagine côté client
|
|
```
|
|
|
|
**Après:**
|
|
```javascript
|
|
// Charger chaque catégorie avec pagination (limit=1000 pour compatibilité)
|
|
const [blocRewardsRes, anchorsRes, changesRes, feesRes] = await Promise.all([
|
|
fetch(`${API_BASE_URL}/api/utxo/list?category=bloc_rewards&page=1&limit=1000`),
|
|
fetch(`${API_BASE_URL}/api/utxo/list?category=ancrages&page=1&limit=1000`),
|
|
fetch(`${API_BASE_URL}/api/utxo/list?category=changes&page=1&limit=1000`),
|
|
fetch(`${API_BASE_URL}/api/utxo/list?category=fees&page=1&limit=1000`),
|
|
]);
|
|
```
|
|
|
|
**Fichier:** `signet-dashboard/public/hash-list.html`
|
|
|
|
**Avant:**
|
|
```javascript
|
|
const response = await fetch(`${API_BASE_URL}/api/hash/list`);
|
|
const data = await response.json();
|
|
allHashes = data.hashes || []; // Charge TOUS les hashes
|
|
```
|
|
|
|
**Après:**
|
|
```javascript
|
|
// Charger tous les hashes avec pagination (boucle pour compatibilité)
|
|
let allHashesLoaded = [];
|
|
let currentPage = 1;
|
|
const limit = 1000;
|
|
|
|
while (hasMore) {
|
|
const response = await fetch(`${API_BASE_URL}/api/hash/list?page=${currentPage}&limit=${limit}`);
|
|
const data = await response.json();
|
|
allHashesLoaded = allHashesLoaded.concat(data.hashes || []);
|
|
if (data.hashes.length < limit || currentPage >= data.totalPages) {
|
|
hasMore = false;
|
|
} else {
|
|
currentPage++;
|
|
}
|
|
}
|
|
```
|
|
|
|
## Evolutions
|
|
|
|
### Pagination optimale (future)
|
|
|
|
Pour une pagination vraiment optimale, le frontend devrait :
|
|
- Charger page par page à la demande (lazy loading)
|
|
- Ne charger que la page actuelle + 1 page en cache
|
|
- Utiliser les métadonnées `totalPages` pour la navigation
|
|
|
|
**Exemple:**
|
|
```javascript
|
|
// Charger seulement la page demandée
|
|
async function loadPage(category, page) {
|
|
const response = await fetch(`${API_BASE_URL}/api/utxo/list?category=${category}&page=${page}&limit=50`);
|
|
const data = await response.json();
|
|
return data;
|
|
}
|
|
```
|
|
|
|
### Compatibilité actuelle
|
|
|
|
Pour maintenir la compatibilité avec le code frontend existant :
|
|
- Le frontend charge encore jusqu'à 1000 éléments par catégorie
|
|
- La pagination côté client continue de fonctionner
|
|
- Réduction mémoire : de 68k+ → 1000 max par catégorie (réduction de 98.5%)
|
|
|
|
## Pages affectées
|
|
|
|
- `signet-dashboard/src/server.js` : Pagination pour `/api/hash/list` et `/api/utxo/list`
|
|
- `signet-dashboard/public/utxo-list.html` : Adaptation pour utiliser la pagination serveur
|
|
- `signet-dashboard/public/hash-list.html` : Adaptation pour charger avec pagination
|
|
|
|
## Modalités de déploiement
|
|
|
|
1. **Redémarrer le service:**
|
|
```bash
|
|
sudo systemctl restart signet-dashboard.service
|
|
```
|
|
|
|
2. **Vérifier les logs:**
|
|
```bash
|
|
journalctl -u signet-dashboard.service -f
|
|
```
|
|
|
|
3. **Tester les endpoints:**
|
|
```bash
|
|
curl "http://localhost:3014/api/hash/list?page=1&limit=10"
|
|
curl "http://localhost:3014/api/utxo/list?category=ancrages&page=1&limit=10"
|
|
```
|
|
|
|
## Modalités d'analyse
|
|
|
|
### Avant optimisation
|
|
- **Mémoire par requête** : Tous les UTXOs/hashes chargés (68k+ UTXOs, 32k+ hashes)
|
|
- **Temps de réponse** : ~500-2000ms (chargement de toutes les données)
|
|
- **Mémoire frontend** : Toutes les données en mémoire
|
|
|
|
### Après optimisation
|
|
- **Mémoire par requête** : Seulement les données de la page demandée (50-1000 éléments)
|
|
- **Temps de réponse** : ~10-100ms (requête SQL indexée avec LIMIT)
|
|
- **Mémoire frontend** : Maximum 1000 éléments par catégorie (au lieu de 68k+)
|
|
|
|
### Métriques à surveiller
|
|
- Temps de réponse des endpoints avec pagination
|
|
- Consommation mémoire de `signet-dashboard` (devrait rester stable)
|
|
- Nombre d'éléments chargés par requête
|
|
- Performance des requêtes SQL avec LIMIT/OFFSET
|
|
|
|
## Notes
|
|
|
|
- La pagination utilise `LIMIT` et `OFFSET` qui sont efficaces avec les index SQLite
|
|
- Les requêtes sont indexées (`idx_utxos_category`, `idx_anchors_block_height`, etc.)
|
|
- Le frontend charge encore jusqu'à 1000 éléments pour compatibilité, mais peut être optimisé davantage
|
|
- Les counts totaux sont toujours disponibles via `/api/utxo/count` sans charger les données
|