Update API anchorage, services, and website skeleton
**Motivations:** - Synchronisation des modifications sur l'API anchorage, les services et le website skeleton - Ajout de scripts de monitoring et de diagnostic pour l'API anchorage - Documentation des problèmes de mutex et de provisioning UTXO **Root causes:** - N/A (commit de synchronisation) **Correctifs:** - N/A (commit de synchronisation) **Evolutions:** - Ajout de scripts de monitoring et de diagnostic pour l'API anchorage - Amélioration de la gestion des mutex et des UTXOs - Mise à jour de la documentation **Pages affectées:** - api-anchorage/src/bitcoin-rpc.js - api-anchorage/src/routes/anchor.js - api-anchorage/src/routes/health.js - api-anchorage/src/server.js - api-anchorage/README-MONITORING.md - api-anchorage/cleanup-stale-locks.mjs - api-anchorage/diagnose.mjs - api-anchorage/unlock-utxos.mjs - service-login-verify/src/persistentNonceCache.ts - signet-dashboard/src/server.js - signet-dashboard/public/* - userwallet/src/hooks/useChannel.ts - userwallet/src/services/relayNotificationService.ts - userwallet/src/utils/defaultContract.ts - website-skeleton/src/* - docs/DOMAINS_AND_PORTS.md - docs/INTERFACES.md - features/* - fixKnowledge/*
This commit is contained in:
parent
4d3028da0c
commit
fe7f49b6cd
@ -1,87 +0,0 @@
|
||||
# État du Système Bitcoin Signet
|
||||
|
||||
**Date** : 2026-01-23
|
||||
|
||||
## ✅ État de la Chaîne
|
||||
|
||||
- **Chaîne** : Signet
|
||||
- **Hauteur** : 0 (bloc genesis)
|
||||
- **Hash du meilleur bloc** : `00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6`
|
||||
- **Statut** : ✅ **UP** - Chaîne opérationnelle
|
||||
|
||||
## ✅ État du Miner
|
||||
|
||||
- **bitcoind** : ✅ **UP** - Processus actif (PID visible)
|
||||
- **mine.sh** : ✅ **UP** - Script de mining actif
|
||||
- **Statut** : ✅ **UP** - Miner opérationnel
|
||||
|
||||
## 💰 Balance du Miner
|
||||
|
||||
- **Balance mature** : 0.00000000 BTC
|
||||
- **Balance immature** : 0.00000000 BTC
|
||||
- **Total** : 0.00000000 BTC
|
||||
|
||||
**Note** : La balance est à 0 car aucun bloc n'a encore été miné depuis le bloc genesis. Le prochain bloc miné apportera la récompense de minage.
|
||||
|
||||
## ⚠️ API d'Ancrage
|
||||
|
||||
- **Port** : 3010
|
||||
- **Statut** : ❌ **DOWN** - API non accessible
|
||||
- **Action requise** : Lancer l'API d'ancrage
|
||||
|
||||
## ⚠️ Dashboard et API Faucet
|
||||
|
||||
- **Node.js** : ❌ **NON INSTALLÉ** sur cette machine
|
||||
- **Dashboard (port 3020)** : ❌ **NON LANCÉ** - Nécessite Node.js
|
||||
- **API Faucet (port 3021)** : ❌ **NON LANCÉ** - Nécessite Node.js
|
||||
|
||||
## Actions Requises
|
||||
|
||||
### Pour lancer le Dashboard et l'API Faucet
|
||||
|
||||
1. **Installer Node.js** (version >= 18.0.0) :
|
||||
```bash
|
||||
# Sur Ubuntu/Debian
|
||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
```
|
||||
|
||||
2. **Installer les dépendances** :
|
||||
```bash
|
||||
cd signet-dashboard && npm install
|
||||
cd ../api-faucet && npm install
|
||||
```
|
||||
|
||||
3. **Configurer les fichiers .env** :
|
||||
```bash
|
||||
cd signet-dashboard && cp .env.example .env
|
||||
cd ../api-faucet && cp .env.example .env
|
||||
```
|
||||
|
||||
4. **Lancer les services** :
|
||||
```bash
|
||||
# Dashboard
|
||||
cd signet-dashboard && npm start &
|
||||
|
||||
# API Faucet
|
||||
cd api-faucet && npm start &
|
||||
```
|
||||
|
||||
### Pour lancer l'API d'Ancrage
|
||||
|
||||
Vérifier si l'API d'ancrage est configurée et la lancer si nécessaire.
|
||||
|
||||
## Vérification
|
||||
|
||||
Une fois les services lancés, vérifier avec :
|
||||
|
||||
```bash
|
||||
# Dashboard
|
||||
curl http://localhost:3020/api/blockchain/info
|
||||
|
||||
# API Faucet
|
||||
curl http://localhost:3021/health
|
||||
|
||||
# API Anchor
|
||||
curl http://localhost:3010/health
|
||||
```
|
||||
282
api-anchorage/README-MONITORING.md
Normal file
282
api-anchorage/README-MONITORING.md
Normal file
@ -0,0 +1,282 @@
|
||||
# Monitoring et Maintenance de l'API d'Ancrage
|
||||
|
||||
**Auteur:** Équipe 4NK
|
||||
**Date:** 2026-01-28
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document décrit les outils de monitoring et de maintenance pour l'API d'ancrage Bitcoin Signet.
|
||||
|
||||
## Endpoints de Monitoring
|
||||
|
||||
### GET `/health`
|
||||
|
||||
Vérifie l'état de base de l'API et de la connexion Bitcoin.
|
||||
|
||||
**Exemple:**
|
||||
```bash
|
||||
curl https://anchorage.certificator.4nkweb.com/health
|
||||
```
|
||||
|
||||
**Réponse:**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"service": "anchor-api",
|
||||
"bitcoin": {
|
||||
"connected": true,
|
||||
"blocks": 10833
|
||||
},
|
||||
"timestamp": "2026-01-28T11:45:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### GET `/health/detailed`
|
||||
|
||||
Vérifie l'état détaillé de l'API, incluant l'état du mutex, des UTXOs verrouillés et de la connexion Bitcoin.
|
||||
|
||||
**Exemple:**
|
||||
```bash
|
||||
curl https://anchorage.certificator.4nkweb.com/health/detailed
|
||||
```
|
||||
|
||||
**Réponse (200 OK):**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"service": "anchor-api",
|
||||
"mutex": {
|
||||
"locked": false,
|
||||
"waiting": 0,
|
||||
"timeout": 180000
|
||||
},
|
||||
"utxos": {
|
||||
"locked": 0,
|
||||
"locked_since": null,
|
||||
"stale_locks": 0,
|
||||
"stale_locks_details": []
|
||||
},
|
||||
"bitcoin": {
|
||||
"connected": true,
|
||||
"blocks": 10833,
|
||||
"chain": "signet",
|
||||
"rpc_timeout": 60000
|
||||
},
|
||||
"timestamp": "2026-01-28T11:45:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Codes de statut:**
|
||||
- `200` - Tout fonctionne correctement
|
||||
- `200` - Warning si 5-10 UTXOs verrouillés (mais < 10 min)
|
||||
- `503` - Service Unavailable si UTXOs verrouillés depuis > 10 min ou > 10 UTXOs verrouillés
|
||||
- `503` - Service Unavailable si Bitcoin non connecté
|
||||
|
||||
### GET `/api/anchor/locked-utxos`
|
||||
|
||||
Liste tous les UTXOs actuellement verrouillés.
|
||||
|
||||
**Exemple:**
|
||||
```bash
|
||||
curl https://anchorage.certificator.4nkweb.com/api/anchor/locked-utxos
|
||||
```
|
||||
|
||||
**Réponse:**
|
||||
```json
|
||||
{
|
||||
"locked": [
|
||||
{
|
||||
"txid": "abc123...",
|
||||
"vout": 0
|
||||
}
|
||||
],
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Scripts de Maintenance
|
||||
|
||||
### 1. `unlock-utxos.mjs`
|
||||
|
||||
Déverrouille tous les UTXOs verrouillés dans la base de données.
|
||||
|
||||
**Utilisation:**
|
||||
```bash
|
||||
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
node unlock-utxos.mjs
|
||||
```
|
||||
|
||||
**Sortie:**
|
||||
```
|
||||
✅ UTXOs déverrouillés: 22
|
||||
```
|
||||
|
||||
### 2. `cleanup-stale-locks.mjs`
|
||||
|
||||
Déverrouille automatiquement les UTXOs verrouillés depuis plus de 10 minutes.
|
||||
|
||||
**Utilisation:**
|
||||
```bash
|
||||
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
node cleanup-stale-locks.mjs
|
||||
```
|
||||
|
||||
**Sortie:**
|
||||
```
|
||||
✅ UTXOs déverrouillés: 5
|
||||
```
|
||||
|
||||
**Cron job recommandé (toutes les 5 minutes):**
|
||||
```bash
|
||||
*/5 * * * * cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && node cleanup-stale-locks.mjs >> /var/log/anchorage-cleanup.log 2>&1
|
||||
```
|
||||
|
||||
### 3. `diagnose.mjs`
|
||||
|
||||
Affiche un diagnostic complet de l'état des UTXOs verrouillés.
|
||||
|
||||
**Utilisation:**
|
||||
```bash
|
||||
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
node diagnose.mjs
|
||||
```
|
||||
|
||||
**Sortie:**
|
||||
```
|
||||
📊 UTXOs verrouillés: 3
|
||||
|
||||
Détails des UTXOs verrouillés:
|
||||
🔒 [1] abc123... vout:0 amount:0.000025 locked_for:5.2min
|
||||
⚠️ STALE [2] def456... vout:1 amount:0.000025 locked_for:15.5min
|
||||
|
||||
⚠️ UTXOs verrouillés depuis plus de 10 minutes: 1
|
||||
Action recommandée: Exécuter cleanup-stale-locks.mjs
|
||||
|
||||
📈 Statistiques UTXOs:
|
||||
Total: 150
|
||||
Verrouillés: 3
|
||||
Dépensés: 50
|
||||
Disponibles: 97
|
||||
```
|
||||
|
||||
## Monitoring des Logs
|
||||
|
||||
### Timeouts de mutex
|
||||
|
||||
**Vérification:**
|
||||
```bash
|
||||
sudo journalctl -u anchorage-api --since "1 hour ago" | grep -c "Mutex acquisition timeout"
|
||||
```
|
||||
|
||||
**Alerte recommandée:** Si > 1 timeout par heure
|
||||
|
||||
### Erreurs RPC Bitcoin
|
||||
|
||||
**Vérification:**
|
||||
```bash
|
||||
sudo journalctl -u anchorage-api --since "1 hour ago" | grep -c "ESOCKETTIMEDOUT\|ETIMEDOUT"
|
||||
```
|
||||
|
||||
**Alerte recommandée:** Si > 1 erreur RPC par heure
|
||||
|
||||
### Opérations longues
|
||||
|
||||
**Vérification:**
|
||||
```bash
|
||||
sudo journalctl -u anchorage-api --since "1 hour ago" | grep "took too long"
|
||||
```
|
||||
|
||||
**Alerte recommandée:** Si > 1 opération > 30s par heure
|
||||
|
||||
## Alertes Recommandées
|
||||
|
||||
### Niveau Warning
|
||||
|
||||
- **UTXOs verrouillés > 5:** Plus de 5 UTXOs verrouillés
|
||||
- **Timeout de mutex:** 1+ timeout par heure
|
||||
- **Erreur RPC:** 1+ erreur RPC par heure
|
||||
|
||||
### Niveau Critical
|
||||
|
||||
- **UTXOs verrouillés > 10:** Plus de 10 UTXOs verrouillés
|
||||
- **UTXOs stale:** UTXOs verrouillés depuis > 10 minutes
|
||||
- **Timeout de mutex:** 5+ timeouts par heure
|
||||
- **Erreur RPC:** 5+ erreurs RPC par heure
|
||||
|
||||
## Configuration Cron
|
||||
|
||||
### Nettoyage automatique (recommandé)
|
||||
|
||||
Créer un fichier `/etc/cron.d/anchorage-cleanup`:
|
||||
|
||||
```bash
|
||||
# Nettoyage automatique des UTXOs verrouillés depuis plus de 10 minutes
|
||||
# Toutes les 5 minutes
|
||||
*/5 * * * * ncantu cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && /usr/bin/node cleanup-stale-locks.mjs >> /var/log/anchorage-cleanup.log 2>&1
|
||||
```
|
||||
|
||||
### Diagnostic périodique (optionnel)
|
||||
|
||||
Créer un fichier `/etc/cron.d/anchorage-diagnose`:
|
||||
|
||||
```bash
|
||||
# Diagnostic de l'état des UTXOs
|
||||
# Toutes les heures
|
||||
0 * * * * ncantu cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && /usr/bin/node diagnose.mjs >> /var/log/anchorage-diagnose.log 2>&1
|
||||
```
|
||||
|
||||
## Procédure de Dépannage
|
||||
|
||||
### 1. Vérifier l'état actuel
|
||||
|
||||
```bash
|
||||
# Health check détaillé
|
||||
curl https://anchorage.certificator.4nkweb.com/health/detailed | jq .
|
||||
|
||||
# UTXOs verrouillés
|
||||
curl https://anchorage.certificator.4nkweb.com/api/anchor/locked-utxos | jq '.count'
|
||||
```
|
||||
|
||||
### 2. Si UTXOs verrouillés
|
||||
|
||||
```bash
|
||||
# Diagnostic
|
||||
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
node diagnose.mjs
|
||||
|
||||
# Déverrouiller manuellement
|
||||
node unlock-utxos.mjs
|
||||
|
||||
# Ou nettoyer seulement les stale locks
|
||||
node cleanup-stale-locks.mjs
|
||||
```
|
||||
|
||||
### 3. Si mutex bloqué
|
||||
|
||||
```bash
|
||||
# Redémarrer le service
|
||||
sudo systemctl restart anchorage-api
|
||||
|
||||
# Vérifier les logs
|
||||
sudo journalctl -u anchorage-api -n 50 --no-pager
|
||||
```
|
||||
|
||||
### 4. Si erreurs RPC Bitcoin
|
||||
|
||||
```bash
|
||||
# Vérifier la connexion Bitcoin
|
||||
curl -s --user bitcoin:bitcoin --data-binary '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}' -H 'content-type: text/plain;' http://localhost:38332/
|
||||
|
||||
# Vérifier les logs
|
||||
sudo journalctl -u anchorage-api --since "10 minutes ago" | grep "ESOCKETTIMEDOUT"
|
||||
```
|
||||
|
||||
## Pages Affectées
|
||||
|
||||
- `api-anchorage/src/routes/health.js`: Endpoint `/health/detailed`
|
||||
- `api-anchorage/src/bitcoin-rpc.js`: Monitoring de durée, timeout de sécurité, retry avec backoff
|
||||
- `api-anchorage/src/server.js`: Déverrouillage automatique au démarrage
|
||||
- `api-anchorage/unlock-utxos.mjs`: Script de déverrouillage (existant)
|
||||
- `api-anchorage/cleanup-stale-locks.mjs`: Script de nettoyage automatique (nouveau)
|
||||
- `api-anchorage/diagnose.mjs`: Script de diagnostic (nouveau)
|
||||
- `api-anchorage/README-MONITORING.md`: Documentation (nouveau)
|
||||
24
api-anchorage/cleanup-stale-locks.mjs
Executable file
24
api-anchorage/cleanup-stale-locks.mjs
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script pour déverrouiller automatiquement les UTXOs verrouillés depuis plus de 10 minutes
|
||||
* À exécuter via cron job toutes les 5 minutes
|
||||
*/
|
||||
|
||||
import { getDatabase } from './src/database.js';
|
||||
|
||||
const db = getDatabase();
|
||||
const result = db.prepare(`
|
||||
UPDATE utxos
|
||||
SET is_locked_in_mutex = 0
|
||||
WHERE is_locked_in_mutex = 1
|
||||
AND updated_at < datetime('now', '-10 minutes')
|
||||
`).run();
|
||||
|
||||
if (result.changes > 0) {
|
||||
console.log(`✅ UTXOs déverrouillés: ${result.changes}`);
|
||||
} else {
|
||||
console.log('ℹ️ Aucun UTXO à déverrouiller');
|
||||
}
|
||||
|
||||
db.close();
|
||||
55
api-anchorage/diagnose.mjs
Executable file
55
api-anchorage/diagnose.mjs
Executable file
@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script de diagnostic pour l'API d'ancrage
|
||||
* Affiche l'état des UTXOs verrouillés, leur durée de verrouillage, etc.
|
||||
*/
|
||||
|
||||
import { getDatabase } from './src/database.js';
|
||||
|
||||
const db = getDatabase();
|
||||
|
||||
// UTXOs verrouillés
|
||||
const locked = db.prepare(`
|
||||
SELECT txid, vout, address, amount, updated_at,
|
||||
(julianday('now') - julianday(updated_at)) * 24 * 60 as minutes_locked
|
||||
FROM utxos
|
||||
WHERE is_locked_in_mutex = 1
|
||||
ORDER BY updated_at
|
||||
`).all();
|
||||
|
||||
console.log(`\n📊 UTXOs verrouillés: ${locked.length}`);
|
||||
|
||||
if (locked.length > 0) {
|
||||
console.log('\nDétails des UTXOs verrouillés:');
|
||||
locked.forEach((u, index) => {
|
||||
const minutes = Math.round(u.minutes_locked * 100) / 100;
|
||||
const status = minutes > 10 ? '⚠️ STALE' : '🔒';
|
||||
console.log(`${status} [${index + 1}] ${u.txid.substring(0, 16)}... vout:${u.vout} amount:${u.amount} locked_for:${minutes}min`);
|
||||
});
|
||||
}
|
||||
|
||||
// UTXOs verrouillés depuis plus de 10 minutes
|
||||
const stale = locked.filter(u => u.minutes_locked > 10);
|
||||
if (stale.length > 0) {
|
||||
console.log(`\n⚠️ UTXOs verrouillés depuis plus de 10 minutes: ${stale.length}`);
|
||||
console.log(' Action recommandée: Exécuter cleanup-stale-locks.mjs');
|
||||
}
|
||||
|
||||
// Statistiques générales
|
||||
const stats = db.prepare(`
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN is_locked_in_mutex = 1 THEN 1 ELSE 0 END) as locked,
|
||||
SUM(CASE WHEN is_spent_onchain = 1 THEN 1 ELSE 0 END) as spent,
|
||||
SUM(CASE WHEN is_spent_onchain = 0 AND is_locked_in_mutex = 0 AND confirmations > 0 THEN 1 ELSE 0 END) as available
|
||||
FROM utxos
|
||||
`).get();
|
||||
|
||||
console.log(`\n📈 Statistiques UTXOs:`);
|
||||
console.log(` Total: ${stats.total}`);
|
||||
console.log(` Verrouillés: ${stats.locked}`);
|
||||
console.log(` Dépensés: ${stats.spent}`);
|
||||
console.log(` Disponibles: ${stats.available}`);
|
||||
|
||||
db.close();
|
||||
@ -26,6 +26,8 @@ class BitcoinRPC {
|
||||
// Mutex pour gérer l'accès concurrent aux UTXOs
|
||||
// Utilise une Promise-based queue pour sérialiser les accès
|
||||
this.utxoMutexPromise = Promise.resolve();
|
||||
this.utxoMutexLocked = false;
|
||||
this.utxoMutexWaiting = 0;
|
||||
|
||||
// Timeout pour l'attente du mutex (180s = 3 minutes)
|
||||
// Si une requête prend plus de 180s, elle sera automatiquement libérée
|
||||
@ -35,6 +37,17 @@ class BitcoinRPC {
|
||||
// via is_locked_in_mutex pour éviter la duplication et réduire la consommation mémoire
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient l'état actuel du mutex
|
||||
* @returns {Object} État du mutex
|
||||
*/
|
||||
getMutexState() {
|
||||
return {
|
||||
locked: this.utxoMutexLocked,
|
||||
waiting: this.utxoMutexWaiting,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquiert le mutex pour l'accès aux UTXOs avec timeout
|
||||
* @returns {Promise<Function>} Fonction pour libérer le mutex
|
||||
@ -45,9 +58,16 @@ class BitcoinRPC {
|
||||
let releaseMutex;
|
||||
let timeoutId;
|
||||
|
||||
// Incrémenter le compteur d'attente
|
||||
this.utxoMutexWaiting++;
|
||||
|
||||
// Créer une nouvelle Promise qui sera résolue quand le mutex est libéré
|
||||
this.utxoMutexPromise = new Promise((resolve) => {
|
||||
releaseMutex = resolve;
|
||||
releaseMutex = () => {
|
||||
this.utxoMutexLocked = false;
|
||||
this.utxoMutexWaiting = Math.max(0, this.utxoMutexWaiting - 1);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
// Créer une Promise avec timeout pour éviter les blocages indéfinis
|
||||
@ -58,6 +78,7 @@ class BitcoinRPC {
|
||||
logger.warn('Mutex acquisition timeout, forcing release', {
|
||||
timeout: this.utxoMutexTimeout,
|
||||
});
|
||||
this.utxoMutexWaiting = Math.max(0, this.utxoMutexWaiting - 1);
|
||||
reject(new Error(`Mutex acquisition timeout after ${this.utxoMutexTimeout}ms`));
|
||||
}, this.utxoMutexTimeout);
|
||||
}),
|
||||
@ -65,6 +86,8 @@ class BitcoinRPC {
|
||||
|
||||
try {
|
||||
await mutexWithTimeout;
|
||||
this.utxoMutexLocked = true;
|
||||
this.utxoMutexWaiting = Math.max(0, this.utxoMutexWaiting - 1);
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
@ -174,8 +197,8 @@ class BitcoinRPC {
|
||||
*/
|
||||
async checkConnection() {
|
||||
try {
|
||||
const networkInfo = await this.client.getNetworkInfo();
|
||||
const blockchainInfo = await this.client.getBlockchainInfo();
|
||||
const networkInfo = await this.callRPCWithRetry('getNetworkInfo', []);
|
||||
const blockchainInfo = await this.callRPCWithRetry('getBlockchainInfo', []);
|
||||
|
||||
return {
|
||||
connected: true,
|
||||
@ -193,13 +216,78 @@ class BitcoinRPC {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelle une méthode RPC avec retry et backoff exponentiel
|
||||
* @param {string} method - Nom de la méthode RPC
|
||||
* @param {Array} params - Paramètres de la méthode
|
||||
* @param {number} maxRetries - Nombre maximum de tentatives (défaut: 3)
|
||||
* @returns {Promise<any>} Résultat de l'appel RPC
|
||||
*/
|
||||
async callRPCWithRetry(method, params = [], maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await this.client[method](...params);
|
||||
} catch (error) {
|
||||
const isTimeoutError = error.message.includes('ESOCKETTIMEDOUT') ||
|
||||
error.message.includes('ETIMEDOUT') ||
|
||||
error.message.includes('timeout');
|
||||
|
||||
if (i === maxRetries - 1 || !isTimeoutError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const delay = Math.min(1000 * Math.pow(2, i), 10000); // Backoff exponentiel, max 10s
|
||||
logger.warn(`RPC call failed, retrying in ${delay}ms`, {
|
||||
method,
|
||||
attempt: i + 1,
|
||||
maxRetries,
|
||||
error: error.message,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelle une commande RPC avec retry et backoff exponentiel
|
||||
* @param {string} command - Nom de la commande RPC
|
||||
* @param {...any} args - Arguments de la commande
|
||||
* @param {number} maxRetries - Nombre maximum de tentatives (défaut: 3)
|
||||
* @returns {Promise<any>} Résultat de l'appel RPC
|
||||
*/
|
||||
async callRPCCommandWithRetry(command, ...args) {
|
||||
const maxRetries = 3;
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await this.client.command(command, ...args);
|
||||
} catch (error) {
|
||||
const isTimeoutError = error.message.includes('ESOCKETTIMEDOUT') ||
|
||||
error.message.includes('ETIMEDOUT') ||
|
||||
error.message.includes('timeout');
|
||||
|
||||
if (i === maxRetries - 1 || !isTimeoutError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const delay = Math.min(1000 * Math.pow(2, i), 10000); // Backoff exponentiel, max 10s
|
||||
logger.warn(`RPC command failed, retrying in ${delay}ms`, {
|
||||
command,
|
||||
attempt: i + 1,
|
||||
maxRetries,
|
||||
error: error.message,
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient une nouvelle adresse depuis le wallet
|
||||
* @returns {Promise<string>} Adresse Bitcoin
|
||||
*/
|
||||
async getNewAddress() {
|
||||
try {
|
||||
return await this.client.getNewAddress();
|
||||
return await this.callRPCWithRetry('getNewAddress', []);
|
||||
} catch (error) {
|
||||
logger.error('Error getting new address', { error: error.message });
|
||||
throw new Error(`Failed to get new address: ${error.message}`);
|
||||
@ -212,7 +300,7 @@ class BitcoinRPC {
|
||||
*/
|
||||
async getBalance() {
|
||||
try {
|
||||
return await this.client.getBalance();
|
||||
return await this.callRPCWithRetry('getBalance', []);
|
||||
} catch (error) {
|
||||
logger.error('Error getting balance', { error: error.message });
|
||||
throw new Error(`Failed to get balance: ${error.message}`);
|
||||
@ -228,10 +316,21 @@ class BitcoinRPC {
|
||||
* @returns {Promise<Object>} Transaction créée avec txid
|
||||
*/
|
||||
async createAnchorTransaction(hash, recipientAddress = null, provisioningAddresses = null, numberOfProvisioningUtxos = null, retryCount = 0) {
|
||||
const startTime = Date.now();
|
||||
// Acquérir le mutex pour l'accès aux UTXOs
|
||||
const releaseMutex = await this.acquireUtxoMutex();
|
||||
let selectedUtxo = null;
|
||||
let selectedUtxos = [];
|
||||
let mutexSafetyTimeout;
|
||||
|
||||
// Timeout de sécurité: libérer le mutex après 5 minutes maximum
|
||||
mutexSafetyTimeout = setTimeout(() => {
|
||||
logger.error('Mutex held for too long, forcing release', {
|
||||
hash: hash?.substring(0, 16) + '...',
|
||||
duration: Date.now() - startTime,
|
||||
});
|
||||
releaseMutex();
|
||||
}, 300000); // 5 minutes
|
||||
|
||||
try {
|
||||
// Vérifier que le hash est valide (64 caractères hex)
|
||||
@ -245,12 +344,21 @@ class BitcoinRPC {
|
||||
const provisioningCount = numberOfProvisioningUtxos ?? 7;
|
||||
const addressesNeeded = 1 + provisioningCount + 1; // principal + provisioning + change
|
||||
|
||||
// Générer toutes les adresses en parallèle
|
||||
// Générer toutes les adresses en parallèle avec timeout
|
||||
const addressPromises = [];
|
||||
for (let i = 0; i < addressesNeeded; i++) {
|
||||
addressPromises.push(this.getNewAddress());
|
||||
}
|
||||
const allAddresses = await Promise.all(addressPromises);
|
||||
|
||||
// Timeout de 30 secondes pour la génération d'adresses
|
||||
const allAddresses = await Promise.race([
|
||||
Promise.all(addressPromises),
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Address generation timeout after 30s'));
|
||||
}, 30000);
|
||||
}),
|
||||
]);
|
||||
|
||||
// Utiliser l'adresse fournie ou la première générée
|
||||
const address = recipientAddress || allAddresses[0];
|
||||
@ -499,7 +607,7 @@ class BitcoinRPC {
|
||||
try {
|
||||
// Récupérer toutes les adresses uniques des UTXOs sélectionnés
|
||||
const uniqueAddresses = [...new Set(selectedUtxos.map(u => u.address))];
|
||||
const utxoCheck = await this.client.listunspent(0, 9999999, uniqueAddresses);
|
||||
const utxoCheck = await this.callRPCWithRetry('listunspent', [0, 9999999, uniqueAddresses]);
|
||||
|
||||
// Vérifier que tous les UTXOs sont toujours disponibles
|
||||
let allUtxosAvailable = true;
|
||||
@ -563,7 +671,7 @@ class BitcoinRPC {
|
||||
|
||||
let tx;
|
||||
try {
|
||||
tx = await this.client.command('createrawtransaction', inputs, outputs);
|
||||
tx = await this.callRPCCommandWithRetry('createrawtransaction', inputs, outputs);
|
||||
} catch (error) {
|
||||
logger.error('Error creating raw transaction', {
|
||||
error: error.message,
|
||||
@ -591,7 +699,7 @@ class BitcoinRPC {
|
||||
|
||||
// Signer la transaction
|
||||
// Utiliser command() directement pour éviter les problèmes avec la bibliothèque
|
||||
const signedTx = await this.client.command('signrawtransactionwithwallet', tx);
|
||||
const signedTx = await this.callRPCCommandWithRetry('signrawtransactionwithwallet', tx);
|
||||
|
||||
if (!signedTx.complete) {
|
||||
const errorDetails = signedTx.errors || [];
|
||||
@ -647,7 +755,7 @@ class BitcoinRPC {
|
||||
// Le test direct avec bitcoin-cli fonctionne avec cette syntaxe
|
||||
let txid;
|
||||
try {
|
||||
txid = await this.client.command('sendrawtransaction', signedTx.hex, 0);
|
||||
txid = await this.callRPCCommandWithRetry('sendrawtransaction', signedTx.hex, 0);
|
||||
} catch (sendError) {
|
||||
// Gérer l'erreur de remplacement RBF (Replace By Fee)
|
||||
// Si une transaction avec les mêmes inputs existe déjà, Bitcoin Core rejette la nouvelle
|
||||
@ -675,7 +783,7 @@ class BitcoinRPC {
|
||||
|
||||
// Vérifier si la transaction existe dans le mempool ou dans la blockchain
|
||||
try {
|
||||
const mempoolEntry = await this.client.command('getmempoolentry', existingTxid);
|
||||
const mempoolEntry = await this.callRPCCommandWithRetry('getmempoolentry', existingTxid);
|
||||
if (mempoolEntry) {
|
||||
// La transaction existe dans le mempool, utiliser cette transaction
|
||||
txid = existingTxid;
|
||||
@ -696,7 +804,7 @@ class BitcoinRPC {
|
||||
if (errorMsg.includes('not in mempool') || errorMsg.includes('Transaction not in mempool')) {
|
||||
// La transaction n'est pas dans le mempool, vérifier si elle est confirmée
|
||||
try {
|
||||
const txInfo = await this.client.getTransaction(existingTxid);
|
||||
const txInfo = await this.callRPCWithRetry('getTransaction', [existingTxid]);
|
||||
if (txInfo && txInfo.txid) {
|
||||
// La transaction existe dans la blockchain (confirmée), utiliser cette transaction
|
||||
txid = existingTxid;
|
||||
@ -753,7 +861,7 @@ class BitcoinRPC {
|
||||
const txInfo = await this.getTransactionInfo(txid);
|
||||
|
||||
// Obtenir la transaction brute pour identifier les index des outputs
|
||||
const rawTx = await this.client.getRawTransaction(txid, true);
|
||||
const rawTx = await this.callRPCWithRetry('getRawTransaction', [txid, true]);
|
||||
|
||||
// Calculer les frais réels de la transaction
|
||||
// Frais = somme des inputs - somme des outputs
|
||||
@ -906,6 +1014,20 @@ class BitcoinRPC {
|
||||
// Le mutex sera libéré dans le bloc finally pour garantir la libération même en cas d'erreur non gérée
|
||||
throw error;
|
||||
} finally {
|
||||
// Nettoyer le timeout de sécurité
|
||||
if (mutexSafetyTimeout) {
|
||||
clearTimeout(mutexSafetyTimeout);
|
||||
}
|
||||
|
||||
// Logger la durée de l'opération
|
||||
const duration = Date.now() - startTime;
|
||||
if (duration > 30000) {
|
||||
logger.warn('Anchor transaction took too long', {
|
||||
duration,
|
||||
hash: hash?.substring(0, 16) + '...',
|
||||
});
|
||||
}
|
||||
|
||||
// Garantir que le mutex est toujours libéré, même en cas d'erreur non gérée
|
||||
try {
|
||||
releaseMutex();
|
||||
@ -922,8 +1044,8 @@ class BitcoinRPC {
|
||||
*/
|
||||
async getTransactionInfo(txid) {
|
||||
try {
|
||||
const tx = await this.client.getTransaction(txid);
|
||||
const blockchainInfo = await this.client.getBlockchainInfo();
|
||||
const tx = await this.callRPCWithRetry('getTransaction', [txid]);
|
||||
const blockchainInfo = await this.callRPCWithRetry('getBlockchainInfo', []);
|
||||
|
||||
return {
|
||||
txid: tx.txid,
|
||||
@ -956,8 +1078,8 @@ class BitcoinRPC {
|
||||
// Si un txid est fourni, vérifier directement cette transaction
|
||||
if (txid) {
|
||||
try {
|
||||
const tx = await this.client.getTransaction(txid, true);
|
||||
const rawTx = await this.client.getRawTransaction(txid, true);
|
||||
const tx = await this.callRPCWithRetry('getTransaction', [txid, true]);
|
||||
const rawTx = await this.callRPCWithRetry('getRawTransaction', [txid, true]);
|
||||
|
||||
// Vérifier si le hash est dans les outputs OP_RETURN
|
||||
const hashFound = this.checkHashInTransaction(rawTx, hash);
|
||||
@ -979,19 +1101,19 @@ class BitcoinRPC {
|
||||
}
|
||||
|
||||
// Rechercher dans les blocs récents (derniers 100 blocs)
|
||||
const blockchainInfo = await this.client.getBlockchainInfo();
|
||||
const blockchainInfo = await this.callRPCWithRetry('getBlockchainInfo', []);
|
||||
const currentHeight = blockchainInfo.blocks;
|
||||
const searchRange = 100; // Rechercher dans les 100 derniers blocs
|
||||
|
||||
for (let height = currentHeight; height >= Math.max(0, currentHeight - searchRange); height--) {
|
||||
try {
|
||||
const blockHash = await this.client.getBlockHash(height);
|
||||
const block = await this.client.getBlock(blockHash, 2); // Verbose level 2
|
||||
const blockHash = await this.callRPCWithRetry('getBlockHash', [height]);
|
||||
const block = await this.callRPCWithRetry('getBlock', [blockHash, 2]); // Verbose level 2
|
||||
|
||||
// Parcourir toutes les transactions du bloc
|
||||
for (const tx of block.tx || []) {
|
||||
try {
|
||||
const rawTx = await this.client.getRawTransaction(tx.txid, true);
|
||||
const rawTx = await this.callRPCWithRetry('getRawTransaction', [tx.txid, true]);
|
||||
const hashFound = this.checkHashInTransaction(rawTx, hash);
|
||||
|
||||
if (hashFound) {
|
||||
|
||||
@ -41,6 +41,38 @@ anchorRouter.get('/locked-utxos', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/anchor/unlock-utxos
|
||||
* Déverrouille tous les UTXOs verrouillés dans la base de données
|
||||
*
|
||||
* Authentification : Requise (header `x-api-key`)
|
||||
*/
|
||||
anchorRouter.post('/unlock-utxos', async (req, res) => {
|
||||
try {
|
||||
const { getDatabase } = await import('../database.js');
|
||||
const db = getDatabase();
|
||||
const result = db.prepare(`
|
||||
UPDATE utxos
|
||||
SET is_locked_in_mutex = 0
|
||||
WHERE is_locked_in_mutex = 1
|
||||
`).run();
|
||||
|
||||
logger.info('UTXOs unlocked via API', { count: result.changes });
|
||||
|
||||
res.status(200).json({
|
||||
ok: true,
|
||||
unlocked: result.changes,
|
||||
message: `${result.changes} UTXO(s) déverrouillé(s)`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error unlocking UTXOs', { error: error.message });
|
||||
res.status(500).json({
|
||||
error: 'Internal Server Error',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/anchor/document
|
||||
* Ancre un document sur Bitcoin Signet
|
||||
@ -145,6 +177,38 @@ anchorRouter.post('/document', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/anchor/unlock-utxos
|
||||
* Déverrouille tous les UTXOs verrouillés dans la base de données
|
||||
*
|
||||
* Authentification : Requise (header `x-api-key`)
|
||||
*/
|
||||
anchorRouter.post('/unlock-utxos', async (req, res) => {
|
||||
try {
|
||||
const { getDatabase } = await import('../database.js');
|
||||
const db = getDatabase();
|
||||
const result = db.prepare(`
|
||||
UPDATE utxos
|
||||
SET is_locked_in_mutex = 0
|
||||
WHERE is_locked_in_mutex = 1
|
||||
`).run();
|
||||
|
||||
logger.info('UTXOs unlocked via API', { count: result.changes });
|
||||
|
||||
res.status(200).json({
|
||||
ok: true,
|
||||
unlocked: result.changes,
|
||||
message: `${result.changes} UTXO(s) déverrouillé(s)`,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error unlocking UTXOs', { error: error.message });
|
||||
res.status(500).json({
|
||||
error: 'Internal Server Error',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/anchor/verify
|
||||
* Vérifie si un hash est ancré sur Bitcoin Signet
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
import express from 'express';
|
||||
import { bitcoinRPC } from '../bitcoin-rpc.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { getDatabase } from '../database.js';
|
||||
|
||||
export const healthRouter = express.Router();
|
||||
|
||||
@ -38,3 +39,77 @@ healthRouter.get('/', async (req, res) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /health/detailed
|
||||
* Vérifie l'état détaillé de l'API, du mutex, des UTXOs et de la connexion Bitcoin
|
||||
*/
|
||||
healthRouter.get('/detailed', async (req, res) => {
|
||||
try {
|
||||
const bitcoinStatus = await bitcoinRPC.checkConnection();
|
||||
|
||||
// Vérifier l'état du mutex
|
||||
const mutexState = bitcoinRPC.getMutexState();
|
||||
|
||||
// Vérifier les UTXOs verrouillés
|
||||
const db = getDatabase();
|
||||
const lockedUtxos = db.prepare(`
|
||||
SELECT txid, vout, address, amount, updated_at,
|
||||
(julianday('now') - julianday(updated_at)) * 24 * 60 as minutes_locked
|
||||
FROM utxos
|
||||
WHERE is_locked_in_mutex = 1
|
||||
ORDER BY updated_at
|
||||
`).all();
|
||||
|
||||
const staleLocks = lockedUtxos.filter(u => u.minutes_locked > 10);
|
||||
|
||||
const health = {
|
||||
ok: bitcoinStatus.connected && lockedUtxos.length === 0 && staleLocks.length === 0,
|
||||
service: 'anchor-api',
|
||||
mutex: {
|
||||
locked: mutexState.locked,
|
||||
waiting: mutexState.waiting,
|
||||
timeout: 180000, // 3 minutes (180000ms)
|
||||
},
|
||||
utxos: {
|
||||
locked: lockedUtxos.length,
|
||||
locked_since: lockedUtxos.length > 0 ? lockedUtxos[0].updated_at : null,
|
||||
stale_locks: staleLocks.length,
|
||||
stale_locks_details: staleLocks.map(u => ({
|
||||
txid: u.txid.substring(0, 16) + '...',
|
||||
vout: u.vout,
|
||||
minutes_locked: Math.round(u.minutes_locked * 100) / 100,
|
||||
})),
|
||||
},
|
||||
bitcoin: {
|
||||
connected: bitcoinStatus.connected,
|
||||
blocks: bitcoinStatus.blocks || 0,
|
||||
chain: bitcoinStatus.chain || null,
|
||||
rpc_timeout: parseInt(process.env.BITCOIN_RPC_TIMEOUT || '30000'),
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Déterminer le code de statut
|
||||
let statusCode = 200;
|
||||
if (!bitcoinStatus.connected) {
|
||||
statusCode = 503;
|
||||
} else if (staleLocks.length > 0) {
|
||||
statusCode = 503; // UTXOs verrouillés depuis trop longtemps
|
||||
} else if (lockedUtxos.length > 10) {
|
||||
statusCode = 503; // Trop d'UTXOs verrouillés
|
||||
} else if (lockedUtxos.length > 5) {
|
||||
statusCode = 200; // Warning mais OK
|
||||
}
|
||||
|
||||
res.status(statusCode).json(health);
|
||||
} catch (error) {
|
||||
logger.error('Detailed health check error', { error: error.message });
|
||||
res.status(503).json({
|
||||
ok: false,
|
||||
service: 'anchor-api',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -51,8 +51,8 @@ app.use((req, res, next) => {
|
||||
|
||||
// Middleware d'authentification API Key
|
||||
app.use((req, res, next) => {
|
||||
// Exclure /health et /api/anchor/locked-utxos de l'authentification
|
||||
if (req.path === '/health' || req.path === '/' || req.path.startsWith('/api/anchor/locked-utxos')) {
|
||||
// Exclure /health, /health/detailed et /api/anchor/locked-utxos de l'authentification
|
||||
if (req.path === '/health' || req.path === '/health/detailed' || req.path === '/' || req.path.startsWith('/api/anchor/locked-utxos')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@ -83,8 +83,10 @@ app.get('/', (req, res) => {
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
health: '/health',
|
||||
healthDetailed: '/health/detailed',
|
||||
anchor: '/api/anchor/document',
|
||||
verify: '/api/anchor/verify',
|
||||
lockedUtxos: '/api/anchor/locked-utxos',
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -106,6 +108,24 @@ app.use((req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Déverrouiller automatiquement les UTXOs verrouillés depuis plus de 10 minutes au démarrage
|
||||
try {
|
||||
const { getDatabase } = await import('./database.js');
|
||||
const db = getDatabase();
|
||||
const result = db.prepare(`
|
||||
UPDATE utxos
|
||||
SET is_locked_in_mutex = 0
|
||||
WHERE is_locked_in_mutex = 1
|
||||
AND updated_at < datetime('now', '-10 minutes')
|
||||
`).run();
|
||||
|
||||
if (result.changes > 0) {
|
||||
logger.info('Unlocked stale UTXOs on startup', { count: result.changes });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Error unlocking stale UTXOs on startup', { error: error.message });
|
||||
}
|
||||
|
||||
// Démarrage du serveur
|
||||
const server = app.listen(PORT, HOST, () => {
|
||||
logger.info(`API d'ancrage Bitcoin Signet démarrée`, {
|
||||
|
||||
14
api-anchorage/unlock-utxos.mjs
Executable file
14
api-anchorage/unlock-utxos.mjs
Executable file
@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script pour déverrouiller tous les UTXOs verrouillés dans la base de données
|
||||
*/
|
||||
|
||||
import { getDatabase } from './src/database.js';
|
||||
|
||||
const db = getDatabase();
|
||||
const result = db.prepare('UPDATE utxos SET is_locked_in_mutex = 0 WHERE is_locked_in_mutex = 1').run();
|
||||
|
||||
console.log(`✅ UTXOs déverrouillés: ${result.changes}`);
|
||||
|
||||
db.close();
|
||||
143
check-anchor-api.sh
Executable file
143
check-anchor-api.sh
Executable file
@ -0,0 +1,143 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script de diagnostic pour l'API d'Ancrage
|
||||
# Vérifie l'état du service, la connectivité et les logs
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=== Diagnostic API d'Ancrage ==="
|
||||
echo ""
|
||||
|
||||
# Configuration
|
||||
BITCOIN_SERVER="192.168.1.105" # Machine bitcoin avec API d'ancrage
|
||||
PROD_SERVER="192.168.1.103" # Machine prod (pour référence)
|
||||
PROD_USER="ncantu"
|
||||
PROXY_SERVER="4nk.myftp.biz"
|
||||
API_URL="https://anchorage.certificator.4nkweb.com" # Domaine externe de l'API d'ancrage Bitcoin
|
||||
SERVICE_NAME="anchorage-api"
|
||||
PORT=3010
|
||||
|
||||
# Fonction pour exécuter une commande sur le serveur bitcoin (API d'ancrage)
|
||||
run_on_bitcoin() {
|
||||
ssh -J "${PROD_USER}@${PROXY_SERVER}" "${PROD_USER}@${BITCOIN_SERVER}" "$@"
|
||||
}
|
||||
|
||||
echo "📡 Test de connectivité externe..."
|
||||
echo ""
|
||||
|
||||
# Test 1: Vérifier que l'API répond depuis l'extérieur
|
||||
echo "1. Test de l'endpoint /health depuis l'extérieur..."
|
||||
HEALTH_RESPONSE=$(curl -s --max-time 10 "${API_URL}/health" || echo "FAILED")
|
||||
if [ "$HEALTH_RESPONSE" = "FAILED" ]; then
|
||||
echo "❌ L'API ne répond pas depuis l'extérieur"
|
||||
echo ""
|
||||
else
|
||||
echo "✅ L'API répond depuis l'extérieur"
|
||||
echo " Réponse: $HEALTH_RESPONSE"
|
||||
echo ""
|
||||
|
||||
# Vérifier si la connexion Bitcoin fonctionne
|
||||
if echo "$HEALTH_RESPONSE" | grep -q '"connected":true'; then
|
||||
echo "✅ Connexion Bitcoin: OK"
|
||||
BLOCKS=$(echo "$HEALTH_RESPONSE" | grep -o '"blocks":[0-9]*' | grep -o '[0-9]*' || echo "0")
|
||||
echo " Blocs: $BLOCKS"
|
||||
else
|
||||
echo "⚠️ Connexion Bitcoin: PROBLÈME"
|
||||
echo " L'API répond mais ne peut pas se connecter à Bitcoin Core"
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Test 2: Vérifier l'endpoint racine
|
||||
echo "2. Test de l'endpoint racine..."
|
||||
ROOT_RESPONSE=$(curl -s --max-time 10 "${API_URL}/" || echo "FAILED")
|
||||
if [ "$ROOT_RESPONSE" = "FAILED" ]; then
|
||||
echo "❌ L'endpoint racine ne répond pas"
|
||||
else
|
||||
echo "✅ L'endpoint racine répond"
|
||||
echo " Réponse: $ROOT_RESPONSE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 3: Vérifier l'état du service sur le serveur bitcoin
|
||||
echo "3. État du service systemd sur le serveur bitcoin (192.168.1.105)..."
|
||||
echo " (Connexion via SSH via proxy...)"
|
||||
echo ""
|
||||
|
||||
SERVICE_STATUS=$(run_on_bitcoin "sudo systemctl status ${SERVICE_NAME} --no-pager" 2>&1 || echo "FAILED")
|
||||
if echo "$SERVICE_STATUS" | grep -q "Active: active (running)"; then
|
||||
echo "✅ Service actif et en cours d'exécution"
|
||||
elif echo "$SERVICE_STATUS" | grep -q "Active: inactive"; then
|
||||
echo "❌ Service inactif"
|
||||
elif echo "$SERVICE_STATUS" | grep -q "Active: failed"; then
|
||||
echo "❌ Service en échec"
|
||||
else
|
||||
echo "⚠️ État du service indéterminé"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 4: Vérifier que le port est en écoute
|
||||
echo "4. Vérification du port ${PORT}..."
|
||||
PORT_CHECK=$(run_on_bitcoin "sudo ss -tlnp | grep :${PORT}" 2>&1 || echo "NOT_LISTENING")
|
||||
if [ "$PORT_CHECK" = "NOT_LISTENING" ] || [ -z "$PORT_CHECK" ]; then
|
||||
echo "❌ Le port ${PORT} n'est pas en écoute"
|
||||
else
|
||||
echo "✅ Le port ${PORT} est en écoute"
|
||||
echo " $PORT_CHECK"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 5: Vérifier les logs récents (dernières 50 lignes)
|
||||
echo "5. Logs récents du service (dernières 50 lignes)..."
|
||||
echo ""
|
||||
run_on_bitcoin "sudo journalctl -u ${SERVICE_NAME} -n 50 --no-pager" 2>&1 | tail -50
|
||||
echo ""
|
||||
|
||||
# Test 6: Vérifier les logs d'erreur récents
|
||||
echo "6. Logs d'erreur récents (dernières 20 lignes)..."
|
||||
echo ""
|
||||
run_on_bitcoin "sudo journalctl -u ${SERVICE_NAME} -p err -n 20 --no-pager" 2>&1 | tail -20
|
||||
echo ""
|
||||
|
||||
# Test 7: Vérifier la connexion Bitcoin RPC depuis le serveur bitcoin
|
||||
echo "7. Test de connexion Bitcoin RPC depuis le serveur bitcoin..."
|
||||
echo ""
|
||||
BITCOIN_RPC_TEST=$(run_on_bitcoin "curl -s --user bitcoin:bitcoin --data-binary '{\"jsonrpc\":\"1.0\",\"id\":\"test\",\"method\":\"getblockchaininfo\",\"params\":[]}' -H 'content-type: text/plain;' http://localhost:38332/" 2>&1 || echo "FAILED")
|
||||
if echo "$BITCOIN_RPC_TEST" | grep -q '"result"'; then
|
||||
echo "✅ Bitcoin RPC répond"
|
||||
BLOCKS_RPC=$(echo "$BITCOIN_RPC_TEST" | grep -o '"blocks":[0-9]*' | grep -o '[0-9]*' || echo "0")
|
||||
echo " Blocs: $BLOCKS_RPC"
|
||||
else
|
||||
echo "❌ Bitcoin RPC ne répond pas"
|
||||
echo " Réponse: $BITCOIN_RPC_TEST"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 8: Vérifier l'état du conteneur Docker Bitcoin (si applicable)
|
||||
echo "8. État du conteneur Docker Bitcoin..."
|
||||
echo ""
|
||||
DOCKER_STATUS=$(run_on_bitcoin "sudo docker ps --filter 'name=bitcoin' --format '{{.Names}} {{.Status}}'" 2>&1 || echo "NO_DOCKER")
|
||||
if [ "$DOCKER_STATUS" = "NO_DOCKER" ] || [ -z "$DOCKER_STATUS" ]; then
|
||||
echo "⚠️ Aucun conteneur Bitcoin trouvé ou Docker non disponible"
|
||||
else
|
||||
echo "✅ Conteneur Bitcoin:"
|
||||
echo "$DOCKER_STATUS" | while read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Résumé
|
||||
echo "=== Résumé ==="
|
||||
echo ""
|
||||
echo "Pour redémarrer le service:"
|
||||
echo " ssh -J ${PROD_USER}@${PROXY_SERVER} ${PROD_USER}@${BITCOIN_SERVER} 'sudo systemctl restart ${SERVICE_NAME}'"
|
||||
echo ""
|
||||
echo "Pour voir les logs en temps réel:"
|
||||
echo " ssh -J ${PROD_USER}@${PROXY_SERVER} ${PROD_USER}@${BITCOIN_SERVER} 'sudo journalctl -u ${SERVICE_NAME} -f'"
|
||||
echo ""
|
||||
echo "Note: L'API d'ancrage Bitcoin est sur la machine 192.168.1.105 (bitcoin), pas sur 192.168.1.103 (prod)"
|
||||
echo ""
|
||||
@ -1,100 +1,100 @@
|
||||
⏳ Traitement: 200000/225882 UTXOs insérés...
|
||||
⏳ Traitement: 210000/225882 UTXOs insérés...
|
||||
⏳ Traitement: 220000/225882 UTXOs insérés...
|
||||
⏳ Traitement: 190000/223585 UTXOs insérés...
|
||||
⏳ Traitement: 200000/223585 UTXOs insérés...
|
||||
⏳ Traitement: 210000/223585 UTXOs insérés...
|
||||
⏳ Traitement: 220000/223585 UTXOs insérés...
|
||||
💾 Mise à jour des UTXOs dépensés...
|
||||
|
||||
📊 Résumé:
|
||||
- UTXOs vérifiés: 61565
|
||||
- UTXOs toujours disponibles: 61565
|
||||
- UTXOs vérifiés: 48651
|
||||
- UTXOs toujours disponibles: 48651
|
||||
- UTXOs dépensés détectés: 0
|
||||
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 68398
|
||||
- Dépensés: 6888
|
||||
- Non dépensés: 61510
|
||||
- Dépensés: 19747
|
||||
- Non dépensés: 48651
|
||||
|
||||
✅ Synchronisation terminée
|
||||
🔍 Démarrage de la synchronisation des UTXOs dépensés...
|
||||
|
||||
📊 UTXOs à vérifier: 49190
|
||||
📊 UTXOs à vérifier: 37046
|
||||
📡 Récupération des UTXOs depuis Bitcoin...
|
||||
📊 UTXOs disponibles dans Bitcoin: 223652
|
||||
📊 UTXOs disponibles dans Bitcoin: 221494
|
||||
💾 Création de la table temporaire...
|
||||
💾 Insertion des UTXOs disponibles par batch...
|
||||
⏳ Traitement: 10000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 20000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 30000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 40000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 50000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 60000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 70000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 80000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 90000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 100000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 110000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 120000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 130000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 140000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 150000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 160000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 170000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 180000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 190000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 200000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 210000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 220000/223652 UTXOs insérés...
|
||||
⏳ Traitement: 10000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 20000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 30000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 40000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 50000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 60000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 70000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 80000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 90000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 100000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 110000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 120000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 130000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 140000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 150000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 160000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 170000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 180000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 190000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 200000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 210000/221494 UTXOs insérés...
|
||||
⏳ Traitement: 220000/221494 UTXOs insérés...
|
||||
💾 Mise à jour des UTXOs dépensés...
|
||||
|
||||
📊 Résumé:
|
||||
- UTXOs vérifiés: 49190
|
||||
- UTXOs toujours disponibles: 49190
|
||||
- UTXOs vérifiés: 37046
|
||||
- UTXOs toujours disponibles: 37046
|
||||
- UTXOs dépensés détectés: 0
|
||||
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 68398
|
||||
- Dépensés: 19208
|
||||
- Non dépensés: 49190
|
||||
- Dépensés: 31352
|
||||
- Non dépensés: 37046
|
||||
|
||||
✅ Synchronisation terminée
|
||||
🔍 Démarrage de la synchronisation des UTXOs dépensés...
|
||||
|
||||
📊 UTXOs à vérifier: 49190
|
||||
📊 UTXOs à vérifier: 5146
|
||||
📡 Récupération des UTXOs depuis Bitcoin...
|
||||
📊 UTXOs disponibles dans Bitcoin: 223667
|
||||
📊 UTXOs disponibles dans Bitcoin: 215703
|
||||
💾 Création de la table temporaire...
|
||||
💾 Insertion des UTXOs disponibles par batch...
|
||||
⏳ Traitement: 10000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 20000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 30000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 40000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 50000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 60000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 70000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 80000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 90000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 100000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 110000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 120000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 130000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 140000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 150000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 160000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 170000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 180000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 190000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 200000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 210000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 220000/223667 UTXOs insérés...
|
||||
⏳ Traitement: 10000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 20000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 30000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 40000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 50000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 60000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 70000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 80000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 90000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 100000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 110000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 120000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 130000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 140000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 150000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 160000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 170000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 180000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 190000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 200000/215703 UTXOs insérés...
|
||||
⏳ Traitement: 210000/215703 UTXOs insérés...
|
||||
💾 Mise à jour des UTXOs dépensés...
|
||||
|
||||
📊 Résumé:
|
||||
- UTXOs vérifiés: 49190
|
||||
- UTXOs toujours disponibles: 49168
|
||||
- UTXOs dépensés détectés: 22
|
||||
- UTXOs vérifiés: 5146
|
||||
- UTXOs toujours disponibles: 5146
|
||||
- UTXOs dépensés détectés: 0
|
||||
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 68398
|
||||
- Dépensés: 19230
|
||||
- Non dépensés: 49168
|
||||
- Dépensés: 63307
|
||||
- Non dépensés: 5091
|
||||
|
||||
✅ Synchronisation terminée
|
||||
|
||||
@ -14,7 +14,8 @@ Ce document liste tous les domaines, ports et services de l'infrastructure Certi
|
||||
|
||||
| Domaine | Service | Port Local | Description |
|
||||
|---------|---------|------------|-------------|
|
||||
| `certificator.4nkweb.com` | API d'Ancrage | 3010 | API REST pour ancrer des documents |
|
||||
| `anchorage.certificator.4nkweb.com` | API d'Ancrage | 3010 | API REST pour ancrer des documents (machine bitcoin 192.168.1.105) |
|
||||
| `certificator.4nkweb.com` | API LeCoffre Anchor | 3004 | API REST LeCoffre pour ancrer (machine prod 192.168.1.103) |
|
||||
| `watermark.certificator.4nkweb.com` | API Filigrane | 3022 | API REST pour ajouter des filigranes et ancrer |
|
||||
| `antivir.certificator.4nkweb.com` | API ClamAV | 3023 | API REST pour scanner les fichiers (antivirus) |
|
||||
| `dashboard.certificator.4nkweb.com` | Dashboard | 3020 | Interface web de supervision |
|
||||
@ -109,7 +110,8 @@ Internet
|
||||
├─→ 4nk.myftp.biz (DynDNS)
|
||||
│ └─→ 192.168.1.100 (proxy) - Point d'entrée unique
|
||||
│ │
|
||||
│ ├─→ certificator.4nkweb.com → 192.168.1.103:3010 (API Anchorage)
|
||||
│ ├─→ anchorage.certificator.4nkweb.com → 192.168.1.105:3010 (API Anchorage Bitcoin)
|
||||
│ ├─→ certificator.4nkweb.com → 192.168.1.103:3004 (API LeCoffre Anchor)
|
||||
│ ├─→ watermark.certificator.4nkweb.com → 192.168.1.103:3022 (API Filigrane)
|
||||
│ ├─→ antivir.certificator.4nkweb.com → 192.168.1.103:3023 (API ClamAV)
|
||||
│ ├─→ dashboard.certificator.4nkweb.com → 192.168.1.103:3020 (Dashboard)
|
||||
@ -141,6 +143,7 @@ curl http://localhost:3022/health
|
||||
curl http://localhost:3023/health
|
||||
|
||||
# Tester depuis l'extérieur (via domaine)
|
||||
curl https://anchorage.certificator.4nkweb.com/health
|
||||
curl https://certificator.4nkweb.com/health
|
||||
curl -s https://dashboard.certificator.4nkweb.com/api/blockchain/info | head -c 200
|
||||
```
|
||||
@ -173,6 +176,7 @@ Tous les domaines utilisent des enregistrements CNAME pointant vers `4nk.myftp.b
|
||||
|
||||
**Exemple de configuration DNS (Gandi) :**
|
||||
```
|
||||
anchorage.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz.
|
||||
certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz.
|
||||
watermark.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz.
|
||||
antivir.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz.
|
||||
@ -180,6 +184,31 @@ dashboard.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz.
|
||||
faucet.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz.
|
||||
```
|
||||
|
||||
## APIs d'Ancrage
|
||||
|
||||
Il existe deux APIs d'ancrage distinctes :
|
||||
|
||||
### 1. API d'Ancrage Bitcoin (`anchorage.certificator.4nkweb.com`)
|
||||
|
||||
- **Domaine :** `anchorage.certificator.4nkweb.com`
|
||||
- **Machine :** 192.168.1.105 (bitcoin)
|
||||
- **Port :** 3010
|
||||
- **Service systemd :** `anchorage-api`
|
||||
- **Répertoire :** `/home/ncantu/Bureau/code/bitcoin/api-anchorage`
|
||||
- **Bitcoin RPC :** 127.0.0.1:38332 (Bitcoin Core dans Docker sur la même machine)
|
||||
- **Description :** API principale pour ancrer des documents sur Bitcoin Signet
|
||||
|
||||
### 2. API LeCoffre Anchor (`certificator.4nkweb.com`)
|
||||
|
||||
- **Domaine :** `certificator.4nkweb.com`
|
||||
- **Machine :** 192.168.1.103 (prod)
|
||||
- **Port :** 3004
|
||||
- **Service :** Processus Node.js dans `/srv/4NK/certificator.4nkweb.com/lecoffre-anchor-api`
|
||||
- **Bitcoin RPC :** localhost:18443 (Bitcoin Core sur machine distante)
|
||||
- **Description :** API LeCoffre pour ancrer des documents (service séparé)
|
||||
|
||||
**Important :** Pour l'API d'ancrage Bitcoin principale, utiliser `anchorage.certificator.4nkweb.com` qui pointe vers la machine bitcoin (192.168.1.105).
|
||||
|
||||
## Notes Importantes
|
||||
|
||||
1. **Ports fixes** : Tous les ports des APIs sont fixes et définis dans les services systemd. Ne pas modifier sans mettre à jour les services.
|
||||
|
||||
@ -10,17 +10,22 @@ Ce document liste toutes les interfaces et IHM (Interfaces Homme-Machine) dispon
|
||||
|
||||
---
|
||||
|
||||
## 1. API REST d'Ancrage
|
||||
## 1. API REST d'Ancrage Bitcoin
|
||||
|
||||
### Description
|
||||
API REST HTTP/JSON pour ancrer des documents sur la blockchain Bitcoin Signet.
|
||||
|
||||
### Accès
|
||||
- **URL** : `https://certificator.4nkweb.com` (via nginx proxy)
|
||||
- **URL** : `https://anchorage.certificator.4nkweb.com` (via nginx proxy)
|
||||
- **Machine** : 192.168.1.105 (bitcoin)
|
||||
- **Port local** : `3010`
|
||||
- **Protocole** : HTTPS (production) / HTTP (développement)
|
||||
- **Format** : JSON
|
||||
|
||||
**Note :** Il existe deux APIs d'ancrage distinctes :
|
||||
- **API Bitcoin** : `https://anchorage.certificator.4nkweb.com` (machine bitcoin 192.168.1.105, port 3010) - API principale
|
||||
- **API LeCoffre** : `https://certificator.4nkweb.com` (machine prod 192.168.1.103, port 3004) - API LeCoffre séparée
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### GET `/`
|
||||
@ -33,8 +38,10 @@ Informations sur l'API
|
||||
"version": "1.0.0",
|
||||
"endpoints": {
|
||||
"health": "/health",
|
||||
"healthDetailed": "/health/detailed",
|
||||
"anchor": "/api/anchor/document",
|
||||
"verify": "/api/anchor/verify"
|
||||
"verify": "/api/anchor/verify",
|
||||
"lockedUtxos": "/api/anchor/locked-utxos"
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -57,6 +64,69 @@ Vérifie l'état de l'API et de la connexion Bitcoin
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/health/detailed`
|
||||
Vérifie l'état détaillé de l'API, incluant l'état du mutex, des UTXOs verrouillés et de la connexion Bitcoin
|
||||
|
||||
**Authentification** : Non requise
|
||||
|
||||
**Réponse (200 OK)** :
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"service": "anchor-api",
|
||||
"mutex": {
|
||||
"locked": false,
|
||||
"waiting": 0,
|
||||
"timeout": 180000
|
||||
},
|
||||
"utxos": {
|
||||
"locked": 0,
|
||||
"locked_since": null,
|
||||
"stale_locks": 0,
|
||||
"stale_locks_details": []
|
||||
},
|
||||
"bitcoin": {
|
||||
"connected": true,
|
||||
"blocks": 152321,
|
||||
"chain": "signet",
|
||||
"rpc_timeout": 60000
|
||||
},
|
||||
"timestamp": "2026-01-23T16:35:27.821Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Réponse (503 Service Unavailable)** : Si UTXOs verrouillés depuis > 10 min ou > 10 UTXOs verrouillés
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"service": "anchor-api",
|
||||
"mutex": {
|
||||
"locked": false,
|
||||
"waiting": 0,
|
||||
"timeout": 180000
|
||||
},
|
||||
"utxos": {
|
||||
"locked": 5,
|
||||
"locked_since": "2026-01-23T16:25:00.000Z",
|
||||
"stale_locks": 5,
|
||||
"stale_locks_details": [
|
||||
{
|
||||
"txid": "abc123...",
|
||||
"vout": 0,
|
||||
"minutes_locked": 15.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"bitcoin": {
|
||||
"connected": true,
|
||||
"blocks": 152321,
|
||||
"chain": "signet",
|
||||
"rpc_timeout": 60000
|
||||
},
|
||||
"timestamp": "2026-01-23T16:35:27.821Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST `/api/anchor/document`
|
||||
Ancre un document sur Bitcoin Signet
|
||||
|
||||
|
||||
190
features/api-anchorage-monitoring-and-prevention.md
Normal file
190
features/api-anchorage-monitoring-and-prevention.md
Normal file
@ -0,0 +1,190 @@
|
||||
# API d'Ancrage - Monitoring et Prévention des Blocages
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Auteur:** Équipe 4NK
|
||||
|
||||
## Objectif
|
||||
|
||||
Implémenter des moyens de contrôle et de correction pour prévenir et résoudre les blocages du mutex et des UTXOs verrouillés dans l'API d'ancrage.
|
||||
|
||||
## Motivations
|
||||
|
||||
- **Prévention des blocages:** Éviter que le mutex reste bloqué indéfiniment
|
||||
- **Détection précoce:** Identifier les problèmes avant qu'ils n'affectent les utilisateurs
|
||||
- **Récupération automatique:** Déverrouiller automatiquement les UTXOs verrouillés depuis trop longtemps
|
||||
- **Robustesse:** Gérer les timeouts RPC Bitcoin avec retry et backoff
|
||||
- **Observabilité:** Fournir des endpoints de monitoring détaillés
|
||||
|
||||
## Impacts
|
||||
|
||||
### Fonctionnels
|
||||
|
||||
- **Endpoint `/health/detailed`:** Fournit un état détaillé du mutex, des UTXOs et de la connexion Bitcoin
|
||||
- **Déverrouillage automatique:** Les UTXOs verrouillés depuis > 10 minutes sont automatiquement déverrouillés au démarrage
|
||||
- **Timeout de sécurité:** Le mutex est automatiquement libéré après 5 minutes maximum
|
||||
- **Retry avec backoff:** Les appels RPC Bitcoin sont automatiquement réessayés en cas de timeout
|
||||
- **Monitoring de durée:** Les opérations > 30 secondes sont loggées avec un warning
|
||||
|
||||
### Techniques
|
||||
|
||||
- **Timeout sur Promise.all():** La génération d'adresses a un timeout de 30 secondes
|
||||
- **État du mutex:** Suivi en temps réel de l'état du mutex (locked, waiting)
|
||||
- **Scripts de maintenance:** Scripts pour diagnostic et nettoyage automatique
|
||||
- **Documentation:** Documentation complète du monitoring et de la maintenance
|
||||
|
||||
## Modifications
|
||||
|
||||
### Fichiers modifiés
|
||||
|
||||
1. **`api-anchorage/src/routes/health.js`**
|
||||
- Ajout de l'endpoint `/health/detailed` avec état du mutex et UTXOs
|
||||
- Vérification des UTXOs verrouillés depuis > 10 minutes
|
||||
- Codes de statut adaptatifs selon l'état
|
||||
|
||||
2. **`api-anchorage/src/bitcoin-rpc.js`**
|
||||
- Ajout de `getMutexState()` pour exposer l'état du mutex
|
||||
- Suivi de l'état du mutex (`utxoMutexLocked`, `utxoMutexWaiting`)
|
||||
- Timeout de sécurité de 5 minutes sur le mutex dans `createAnchorTransaction()`
|
||||
- Timeout de 30 secondes sur `Promise.all()` pour la génération d'adresses
|
||||
- Méthode `callRPCWithRetry()` pour retry avec backoff exponentiel
|
||||
- Méthode `callRPCCommandWithRetry()` pour les commandes RPC
|
||||
- Tous les appels RPC critiques utilisent maintenant le retry avec backoff
|
||||
- Monitoring de la durée des opérations avec alerte si > 30s
|
||||
|
||||
3. **`api-anchorage/src/server.js`**
|
||||
- Déverrouillage automatique des UTXOs verrouillés depuis > 10 minutes au démarrage
|
||||
- Exclusion de `/health/detailed` de l'authentification API Key
|
||||
- Mise à jour de la liste des endpoints dans la route racine
|
||||
|
||||
4. **`api-anchorage/cleanup-stale-locks.mjs`** (nouveau)
|
||||
- Script pour déverrouiller les UTXOs verrouillés depuis > 10 minutes
|
||||
- À exécuter via cron job toutes les 5 minutes
|
||||
|
||||
5. **`api-anchorage/diagnose.mjs`** (nouveau)
|
||||
- Script de diagnostic complet de l'état des UTXOs
|
||||
- Affiche les UTXOs verrouillés avec leur durée de verrouillage
|
||||
- Statistiques générales des UTXOs
|
||||
|
||||
6. **`signet-dashboard/public/api-docs.html`**
|
||||
- Ajout de la documentation de l'endpoint `/health/detailed`
|
||||
- Mise à jour de la liste des endpoints publics
|
||||
- Correction de l'URL de l'API d'ancrage (anchorage.certificator.4nkweb.com)
|
||||
|
||||
7. **`docs/INTERFACES.md`**
|
||||
- Ajout de la documentation de l'endpoint `/health/detailed`
|
||||
- Mise à jour de la liste des endpoints
|
||||
|
||||
8. **`api-anchorage/README-MONITORING.md`** (nouveau)
|
||||
- Documentation complète du monitoring et de la maintenance
|
||||
- Procédures de dépannage
|
||||
- Configuration des cron jobs
|
||||
|
||||
### Appels RPC mis à jour avec retry
|
||||
|
||||
- `getNewAddress()` → `callRPCWithRetry('getNewAddress', [])`
|
||||
- `getBalance()` → `callRPCWithRetry('getBalance', [])`
|
||||
- `checkConnection()` → `callRPCWithRetry('getNetworkInfo', [])` et `callRPCWithRetry('getBlockchainInfo', [])`
|
||||
- `listunspent()` → `callRPCWithRetry('listunspent', [...])`
|
||||
- `createrawtransaction` → `callRPCCommandWithRetry('createrawtransaction', ...)`
|
||||
- `signrawtransactionwithwallet` → `callRPCCommandWithRetry('signrawtransactionwithwallet', ...)`
|
||||
- `sendrawtransaction` → `callRPCCommandWithRetry('sendrawtransaction', ...)`
|
||||
- `getmempoolentry` → `callRPCCommandWithRetry('getmempoolentry', ...)`
|
||||
- `getTransaction()` → `callRPCWithRetry('getTransaction', [...])`
|
||||
- `getRawTransaction()` → `callRPCWithRetry('getRawTransaction', [...])`
|
||||
- `getTransactionInfo()` → Utilise `callRPCWithRetry()` pour tous les appels
|
||||
|
||||
## Modalités de déploiement
|
||||
|
||||
### 1. Redémarrage du service
|
||||
|
||||
```bash
|
||||
sudo systemctl restart anchorage-api
|
||||
```
|
||||
|
||||
### 2. Vérification
|
||||
|
||||
```bash
|
||||
# Health check détaillé
|
||||
curl http://localhost:3010/health/detailed
|
||||
|
||||
# Depuis l'extérieur
|
||||
curl https://anchorage.certificator.4nkweb.com/health/detailed
|
||||
```
|
||||
|
||||
### 3. Configuration du cron job (optionnel mais recommandé)
|
||||
|
||||
Créer `/etc/cron.d/anchorage-cleanup`:
|
||||
|
||||
```bash
|
||||
# Nettoyage automatique des UTXOs verrouillés depuis plus de 10 minutes
|
||||
# Toutes les 5 minutes
|
||||
*/5 * * * * ncantu cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && /usr/bin/node cleanup-stale-locks.mjs >> /var/log/anchorage-cleanup.log 2>&1
|
||||
```
|
||||
|
||||
### 4. Test des scripts
|
||||
|
||||
```bash
|
||||
# Diagnostic
|
||||
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
node diagnose.mjs
|
||||
|
||||
# Nettoyage
|
||||
node cleanup-stale-locks.mjs
|
||||
|
||||
# Déverrouillage manuel
|
||||
node unlock-utxos.mjs
|
||||
```
|
||||
|
||||
## Modalités d'analyse
|
||||
|
||||
### Endpoints de monitoring
|
||||
|
||||
1. **GET `/health/detailed`**
|
||||
- Vérifier l'état du mutex (`locked`, `waiting`)
|
||||
- Vérifier le nombre d'UTXOs verrouillés
|
||||
- Vérifier les UTXOs verrouillés depuis > 10 minutes (`stale_locks`)
|
||||
- Vérifier la connexion Bitcoin
|
||||
|
||||
2. **GET `/api/anchor/locked-utxos`**
|
||||
- Liste complète des UTXOs verrouillés
|
||||
- Compteur total
|
||||
|
||||
### Logs à surveiller
|
||||
|
||||
1. **Timeouts de mutex:**
|
||||
```bash
|
||||
sudo journalctl -u anchorage-api | grep "Mutex acquisition timeout"
|
||||
```
|
||||
|
||||
2. **Opérations longues:**
|
||||
```bash
|
||||
sudo journalctl -u anchorage-api | grep "took too long"
|
||||
```
|
||||
|
||||
3. **Erreurs RPC Bitcoin:**
|
||||
```bash
|
||||
sudo journalctl -u anchorage-api | grep "ESOCKETTIMEDOUT\|ETIMEDOUT"
|
||||
```
|
||||
|
||||
4. **Retry RPC:**
|
||||
```bash
|
||||
sudo journalctl -u anchorage-api | grep "RPC call failed, retrying"
|
||||
```
|
||||
|
||||
### Scripts de diagnostic
|
||||
|
||||
1. **`diagnose.mjs`:** Diagnostic complet de l'état des UTXOs
|
||||
2. **`cleanup-stale-locks.mjs`:** Nettoyage automatique des UTXOs stale
|
||||
3. **`unlock-utxos.mjs`:** Déverrouillage manuel de tous les UTXOs
|
||||
|
||||
## Pages affectées
|
||||
|
||||
- `api-anchorage/src/routes/health.js`: Endpoint `/health/detailed`
|
||||
- `api-anchorage/src/bitcoin-rpc.js`: Monitoring, timeouts, retry avec backoff
|
||||
- `api-anchorage/src/server.js`: Déverrouillage automatique au démarrage
|
||||
- `api-anchorage/cleanup-stale-locks.mjs`: Script de nettoyage (nouveau)
|
||||
- `api-anchorage/diagnose.mjs`: Script de diagnostic (nouveau)
|
||||
- `api-anchorage/README-MONITORING.md`: Documentation (nouveau)
|
||||
- `signet-dashboard/public/api-docs.html`: Documentation API mise à jour
|
||||
- `docs/INTERFACES.md`: Documentation API mise à jour
|
||||
- `features/api-anchorage-monitoring-and-prevention.md`: Documentation (nouveau)
|
||||
207
features/login-bout-en-bout-implémentations.md
Normal file
207
features/login-bout-en-bout-implémentations.md
Normal file
@ -0,0 +1,207 @@
|
||||
# Login fonctionnel de bout en bout - Implémentations réalisées
|
||||
|
||||
**Author:** Équipe 4NK
|
||||
**Date:** 2026-01-28
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document décrit les implémentations réalisées pour compléter le login fonctionnel de bout en bout sur PC et mobile.
|
||||
|
||||
---
|
||||
|
||||
## 1. userwallet - Notifications relais pour mobile
|
||||
|
||||
**Note importante :** Le modèle est **pull-only** (HTTP REST uniquement). Les WebSockets ne sont pas souhaités selon la documentation (`userwallet/features/userwallet-notifications-relais.md`).
|
||||
|
||||
### 1.1 Modèle pull-only (HTTP REST)
|
||||
|
||||
**Fichier modifié :** `userwallet/src/services/relayNotificationService.ts`
|
||||
|
||||
**Implémentations :**
|
||||
|
||||
1. **Polling HTTP REST uniquement**
|
||||
- Modèle pull-only : uniquement des appels GET HTTP REST
|
||||
- Pas de WebSocket (non souhaité selon la documentation)
|
||||
- Polling périodique des relais pour détecter nouveaux hashes
|
||||
- Détection via `GET /keys?start=&end=` pour scanner les nouveaux messages
|
||||
|
||||
2. **Gestion du polling**
|
||||
- `startPolling(intervalMs)` : démarre le polling périodique
|
||||
- `stopPolling()` : arrête le polling
|
||||
- Fenêtre temporelle configurable pour le scan des clés
|
||||
- Émission d'événements `RelayHashEvent` avec `source: 'polling'`
|
||||
|
||||
### 1.2 Optimisations mobile (backoff, reconnexions)
|
||||
|
||||
**Fichier modifié :** `userwallet/src/services/relayNotificationService.ts`
|
||||
|
||||
**Implémentations :**
|
||||
|
||||
1. **Stratégie de backoff exponentiel**
|
||||
- Interface `BackoffState` pour gérer l'état de retry par relais
|
||||
- `getBackoffDelay(endpoint)` : calcul du délai avec exponentiel (max 60s)
|
||||
- `recordFailure(endpoint)` : enregistre un échec et incrémente les tentatives
|
||||
- `recordSuccess(endpoint)` : réinitialise le backoff après succès
|
||||
- `shouldRetry(endpoint)` : vérifie si un retry est autorisé (max 5 tentatives)
|
||||
|
||||
2. **Gestion réseau mobile**
|
||||
- `setupNetworkListeners()` : écoute des événements `online`/`offline`
|
||||
- `resumeOperations()` : reprend les opérations après reconnexion
|
||||
- `pauseOperations()` : pause les opérations en mode offline
|
||||
- Vérification `isOnline` avant chaque opération réseau
|
||||
|
||||
3. **Optimisation des appels réseau**
|
||||
- Retry avec backoff pour `getMessageByHash`, `getSignatures`, `getKeys`
|
||||
- Skip des relais en échec répétés
|
||||
- Réduction des appels inutiles en mode offline
|
||||
|
||||
### 1.3 Fetch automatique des données
|
||||
|
||||
**Fichier modifié :** `userwallet/src/services/relayNotificationService.ts`
|
||||
|
||||
**Implémentations :**
|
||||
|
||||
1. **ProcessHash amélioré**
|
||||
- Fetch automatique de message, signatures, clés quand un hash est connu
|
||||
- Déchiffrement et mise à jour du graphe automatiques
|
||||
- Gestion des erreurs avec backoff par relais
|
||||
|
||||
2. **ProcessHashByType optimisé**
|
||||
- Optimisation selon le type d'objet (signature, contrat, membre, pair, action, champ)
|
||||
- Fetch sélectif selon le type (ex: pas besoin de clés pour une signature seule)
|
||||
|
||||
3. **Intégration avec useRelayNotifications**
|
||||
- Auto-processing des hashes détectés via polling
|
||||
- Démarrage automatique du polling avec `startPolling()`
|
||||
- Arrêt propre avec `stopPolling()` et `cleanup()`
|
||||
|
||||
**Fichier modifié :** `userwallet/src/hooks/useRelayNotifications.ts`
|
||||
|
||||
**Implémentations :**
|
||||
|
||||
1. **Polling HTTP REST**
|
||||
- `startPolling()` démarre le polling périodique
|
||||
- `stopPolling()` arrête le polling
|
||||
- Gestion du cycle de vie du polling
|
||||
|
||||
---
|
||||
|
||||
## 2. userwallet - Responsive design mobile
|
||||
|
||||
### 2.1 CSS responsive
|
||||
|
||||
**Fichier modifié :** `userwallet/src/index.css`
|
||||
|
||||
**Implémentations :**
|
||||
|
||||
1. **Media queries**
|
||||
- `@media (max-width: 768px)` : tablettes et petits écrans
|
||||
- `@media (max-width: 480px)` : smartphones
|
||||
- Adaptation des paddings, marges, tailles de police
|
||||
|
||||
2. **Tailles tactiles**
|
||||
- `min-height: 44px` pour tous les boutons (recommandation Apple/Google)
|
||||
- `min-width: 44px` pour les éléments interactifs
|
||||
- Boutons en `width: 100%` sur mobile
|
||||
|
||||
3. **Gestion du clavier virtuel**
|
||||
- `font-size: 16px` pour les inputs (évite le zoom automatique iOS)
|
||||
- `scroll-margin-top: 100px` pour le focus des inputs
|
||||
- Adaptation des hauteurs d'iframe selon la taille d'écran
|
||||
|
||||
4. **Optimisations tactiles**
|
||||
- `@media (hover: none) and (pointer: coarse)` : détection tactile
|
||||
- Feedback visuel avec `opacity` et `transform` sur `:active`
|
||||
- Espacement suffisant entre les éléments interactifs
|
||||
|
||||
---
|
||||
|
||||
## 3. service-login-verify - Persistance NonceCache
|
||||
|
||||
### 3.1 Amélioration PersistentNonceCache
|
||||
|
||||
**Fichier modifié :** `service-login-verify/src/persistentNonceCache.ts`
|
||||
|
||||
**Implémentations :**
|
||||
|
||||
1. **Persistance IndexedDB en arrière-plan**
|
||||
- `persistToIndexedDB(nonce, timestamp)` : persiste en IndexedDB de manière asynchrone
|
||||
- Utilisation de localStorage comme stockage principal (synchrone, requis par l'interface)
|
||||
- IndexedDB utilisé comme backup persistant
|
||||
|
||||
2. **Nettoyage IndexedDB**
|
||||
- `cleanupIndexedDB(now)` : supprime les entrées expirées depuis IndexedDB
|
||||
- Utilisation de l'index `timestamp` pour les requêtes efficaces
|
||||
- Nettoyage asynchrone non-bloquant
|
||||
|
||||
3. **Double persistance**
|
||||
- localStorage : accès synchrone (requis par `NonceCacheLike`)
|
||||
- IndexedDB : persistance entre sessions (backup)
|
||||
- Les deux sont maintenus en synchronisation
|
||||
|
||||
**Note :** `PersistentNonceCache` est déjà disponible et peut être utilisé à la place de `NonceCache` dans `website-skeleton` si persistance entre redémarrages nécessaire.
|
||||
|
||||
---
|
||||
|
||||
## Synthèse des modifications
|
||||
|
||||
### Fichiers modifiés
|
||||
|
||||
1. `userwallet/src/services/relayNotificationService.ts` :
|
||||
- Modèle pull-only (HTTP REST)
|
||||
- Optimisations mobile (backoff, reconnexions)
|
||||
- Gestion réseau online/offline
|
||||
|
||||
2. `userwallet/src/hooks/useRelayNotifications.ts` :
|
||||
- Polling HTTP REST automatique
|
||||
- Gestion du cycle de vie
|
||||
|
||||
3. `userwallet/src/index.css` :
|
||||
- Media queries responsive
|
||||
- Tailles tactiles
|
||||
- Gestion clavier virtuel
|
||||
|
||||
4. `service-login-verify/src/persistentNonceCache.ts` :
|
||||
- Persistance IndexedDB améliorée
|
||||
|
||||
---
|
||||
|
||||
## Utilisation
|
||||
|
||||
### userwallet - Activer les notifications
|
||||
|
||||
Le hook `useRelayNotifications` active le polling HTTP REST :
|
||||
|
||||
```typescript
|
||||
const { startPolling, stopPolling } = useRelayNotifications(graphResolver, true);
|
||||
// Démarrer polling HTTP REST (pull-only)
|
||||
startPolling(60000); // 60 secondes
|
||||
```
|
||||
|
||||
### service-login-verify - Utiliser PersistentNonceCache
|
||||
|
||||
Dans `website-skeleton/src/main.ts` :
|
||||
|
||||
```typescript
|
||||
import { PersistentNonceCache } from 'service-login-verify';
|
||||
|
||||
const nonceCache = new PersistentNonceCache(3600000);
|
||||
await nonceCache.init(); // Initialiser IndexedDB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes importantes
|
||||
|
||||
- **Modèle pull-only** : Le système utilise uniquement des appels HTTP REST (GET). Les WebSockets ne sont pas souhaités selon la documentation (`userwallet/features/userwallet-notifications-relais.md`).
|
||||
- **Persistance NonceCache** : `PersistentNonceCache` utilise localStorage comme stockage principal (synchrone) et IndexedDB en arrière-plan (asynchrone).
|
||||
- **Backoff** : Limité à 5 tentatives maximum avec délai exponentiel jusqu'à 60 secondes.
|
||||
- **Mobile** : Les optimisations réduisent les appels réseau et gèrent les reconnexions automatiquement.
|
||||
|
||||
---
|
||||
|
||||
## Références
|
||||
|
||||
- `features/login-bout-en-bout-reste-a-faire.md` : Liste initiale des éléments à implémenter
|
||||
- `userwallet/src/services/relayNotificationService.ts` : Service de notifications
|
||||
- `service-login-verify/src/persistentNonceCache.ts` : Cache persistant
|
||||
276
features/login-bout-en-bout-reste-a-faire.md
Normal file
276
features/login-bout-en-bout-reste-a-faire.md
Normal file
@ -0,0 +1,276 @@
|
||||
# Login fonctionnel de bout en bout PC et Mobile - Reste à faire
|
||||
|
||||
**Author:** Équipe 4NK
|
||||
**Date:** 2026-01-28
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document liste ce qui reste à implémenter pour avoir un login fonctionnel de bout en bout sur PC et mobile pour les projets :
|
||||
- `website-skeleton`
|
||||
- `userwallet`
|
||||
- `service-login-verify`
|
||||
- `api-relay`
|
||||
|
||||
---
|
||||
|
||||
## 1. website-skeleton
|
||||
|
||||
### 1.1 Envoi du contrat au UserWallet (Priorité : Haute)
|
||||
|
||||
**Statut :** ✅ Implémenté
|
||||
**Référence :** `website-skeleton/src/main.ts`
|
||||
|
||||
**Fait :**
|
||||
- ✅ Fonction `sendContractToIframe()` qui envoie le contrat stocké à l'iframe
|
||||
- ✅ Stockage du contrat reçu pour envoi ultérieur (`storedContract`, `storedContratsFils`, `storedActions`)
|
||||
- ✅ Envoi automatique du contrat au chargement de l'iframe (événement `load`)
|
||||
- ✅ Envoi du contrat lors de la réception d'un nouveau contrat via `postMessage`
|
||||
- ✅ Réception des messages `contract` depuis le parent (écoute `postMessage`)
|
||||
- ✅ Extraction des validateurs depuis les contrats reçus
|
||||
- ✅ Mise à jour des clés autorisées pour la vérification
|
||||
|
||||
**Note :** L'implémentation est complète. Le contrat est envoyé automatiquement à l'iframe dès qu'il est disponible.
|
||||
|
||||
### 1.2 Gestion de session utilisateur (Priorité : Haute)
|
||||
|
||||
**Statut :** ✅ Implémenté
|
||||
**Référence :** `website-skeleton/src/main.ts`, `website-skeleton/index.html`
|
||||
|
||||
**Fait :**
|
||||
- ✅ Stockage de session dans `sessionStorage` après acceptation de la preuve
|
||||
- ✅ Fonctions `getSession()`, `setSession()`, `clearSession()`, `isLoggedIn()`
|
||||
- ✅ Masquage de l'iframe après login réussi
|
||||
- ✅ Affichage d'une interface "connecté" avec informations utilisateur (clés publiques)
|
||||
- ✅ Bouton de déconnexion fonctionnel
|
||||
- ✅ Vérification de session au chargement de la page
|
||||
- ✅ Mise à jour automatique de l'interface selon l'état de connexion
|
||||
- ✅ Vérification de la preuve de login avec `verifyLoginProof()`
|
||||
- ✅ Affichage du statut (accepté/refusé)
|
||||
|
||||
**Note :** L'implémentation est complète. La protection des routes/endpoints reste à implémenter si nécessaire selon les besoins spécifiques du service.
|
||||
|
||||
### 1.3 Interface responsive mobile (Priorité : Moyenne)
|
||||
|
||||
**Statut :** ✅ Implémenté (à tester)
|
||||
|
||||
**Fait :**
|
||||
- ✅ Meta viewport configuré (`width=device-width, initial-scale=1.0`)
|
||||
- ✅ Styles CSS responsive avec media queries (768px, 480px)
|
||||
- ✅ Adaptation de l'iframe pour mobile (hauteurs différentes selon taille d'écran)
|
||||
- ✅ Boutons adaptés au tactile (min-height: 44px, width: 100% sur mobile)
|
||||
- ✅ Layout flex adaptatif pour petits écrans
|
||||
- ✅ Tailles de police adaptées
|
||||
|
||||
**À tester :**
|
||||
- Vérifier le comportement sur différents appareils mobiles
|
||||
- Tester le clavier virtuel (focus, scroll)
|
||||
- Valider les gestes tactiles si nécessaire
|
||||
|
||||
### 1.4 Amélioration UX (Priorité : Moyenne)
|
||||
|
||||
**Statut :** ✅ Implémenté (améliorations possibles)
|
||||
|
||||
**Fait :**
|
||||
- ✅ Bouton "Se connecter" clair et visible (style primary)
|
||||
- ✅ Gestion de l'affichage/masquage de l'iframe selon l'état
|
||||
- ✅ Indicateur de statut de connexion avec couleurs (accepted/rejected/pending)
|
||||
- ✅ Messages d'erreur avec raison du refus
|
||||
- ✅ Interface "connecté" avec informations utilisateur
|
||||
- ✅ Styles améliorés (couleurs, transitions, bordures arrondies)
|
||||
|
||||
**Améliorations possibles (optionnel) :**
|
||||
- Loading states plus visibles pendant la vérification
|
||||
- Guide utilisateur pour le premier login
|
||||
- Messages d'aide contextuels
|
||||
|
||||
---
|
||||
|
||||
## 2. userwallet
|
||||
|
||||
### 2.1 Validation des écrans login (Priorité : Haute)
|
||||
|
||||
**Statut :** À valider avant implémentation
|
||||
**Référence :** `features/userwallet-ecrans-login-a-valider.md`, `RESTE_A_FAIRE.md` (§ 1.1)
|
||||
|
||||
**Action requise :** Tester les écrans login existants et valider leur fonctionnement sur PC et mobile.
|
||||
|
||||
**Écrans à valider :**
|
||||
- Sélection service / sélection membre
|
||||
- Construction du chemin login
|
||||
- Message de login à valider
|
||||
- Collecte signatures mFA
|
||||
- Publication
|
||||
- Vérification locale + résultat
|
||||
|
||||
### 2.2 Notifications relais pour mobile (Priorité : Haute)
|
||||
|
||||
**Statut :** Partiellement implémenté
|
||||
**Référence :** `RESTE_A_FAIRE.md` (§ 1.2), `features/userwallet-contrat-login-reste-a-faire.md` (§ 3.2)
|
||||
|
||||
**Fait :**
|
||||
- Progression collecte signatures (X/Y) implémentée via `onProgress` dans `runCollectLoop`
|
||||
- Affichage dans `LoginCollectShare`
|
||||
|
||||
**Reste à faire :**
|
||||
|
||||
1. **Notifications push (si extension mobile)**
|
||||
- Réagir aux événements push du relais pour savoir quel hash fetch
|
||||
- Implémenter un mécanisme de notification (Service Worker, Web Push, etc.)
|
||||
|
||||
2. **Fetch automatique des données**
|
||||
- Une fois le hash connu (via notification ou autre) : récupération sur le relai des signatures, contrats, membres, pairs, actions, champs
|
||||
- Les notifications doivent piloter : quel hash fetch, puis fetch signatures/clés et mise à jour du graphe
|
||||
|
||||
3. **Optimisation pour mobile**
|
||||
- Réduire les appels réseau inutiles
|
||||
- Gérer les reconnexions réseau
|
||||
- Optimiser la collecte de signatures sur mobile (batterie, données)
|
||||
|
||||
**Fichiers concernés :**
|
||||
- `userwallet/src/services/relayNotificationService.ts`
|
||||
- `userwallet/src/hooks/useRelayNotifications.ts`
|
||||
- `userwallet/src/components/LoginCollectShare.tsx`
|
||||
|
||||
### 2.3 Responsive design mobile (Priorité : Moyenne)
|
||||
|
||||
**Statut :** À vérifier et améliorer
|
||||
|
||||
**Ce qui manque :**
|
||||
|
||||
1. **Interface responsive**
|
||||
- Vérifier que tous les écrans login sont utilisables sur mobile
|
||||
- Adapter les formulaires pour petits écrans
|
||||
- Gérer le clavier virtuel (focus, scroll)
|
||||
|
||||
2. **UX mobile**
|
||||
- Gestes tactiles (swipe, etc.)
|
||||
- Tailles de boutons adaptées au tactile
|
||||
- Feedback visuel pour les actions
|
||||
|
||||
3. **Performance mobile**
|
||||
- Optimiser le chargement initial
|
||||
- Réduire la consommation mémoire
|
||||
- Gérer les limites de stockage IndexedDB sur mobile
|
||||
|
||||
---
|
||||
|
||||
## 3. service-login-verify
|
||||
|
||||
### 3.1 Intégration dans website-skeleton (Priorité : Basse)
|
||||
|
||||
**Statut :** Déjà intégré et fonctionnel
|
||||
|
||||
**Fait :**
|
||||
- Package `service-login-verify` intégré dans `website-skeleton`
|
||||
- Utilisation de `verifyLoginProof`, `NonceCache`, `buildAllowedPubkeysFromValidateurs`
|
||||
- Instance de `NonceCache` créée et utilisée
|
||||
- Construction des clés autorisées depuis les validateurs
|
||||
- Vérification des preuves reçues
|
||||
|
||||
**Note :** L'intégration est complète et fonctionnelle. Aucune action requise.
|
||||
|
||||
### 3.2 Persistance du NonceCache (Priorité : Basse - Optionnel)
|
||||
|
||||
**Statut :** Actuellement en mémoire uniquement
|
||||
**Référence :** `RESTE_A_FAIRE.md` (§ 2.1)
|
||||
|
||||
**Description :** Le `NonceCache` actuel est en mémoire avec TTL configurable. Une persistance optionnelle (IndexedDB, localStorage, etc.) peut être ajoutée si nécessaire pour éviter les rejeux après redémarrage du service.
|
||||
|
||||
**Action requise :** Implémenter uniquement si besoin de persistance entre redémarrages du service.
|
||||
|
||||
---
|
||||
|
||||
## 4. api-relay
|
||||
|
||||
### 4.1 Support notifications push pour mobile (Priorité : Moyenne)
|
||||
|
||||
**Statut :** À vérifier
|
||||
|
||||
**Ce qui manque :**
|
||||
|
||||
1. **Notifications push (optionnel)**
|
||||
- Si support Web Push : endpoint pour s'abonner aux notifications
|
||||
- Envoyer des notifications quand de nouveaux messages/signatures/clés sont disponibles
|
||||
- Gérer les abonnements par hash ou par service
|
||||
|
||||
2. **Optimisation pour mobile**
|
||||
- Réduire la taille des réponses
|
||||
- Support de la compression
|
||||
- Pagination pour les grandes listes
|
||||
|
||||
**Note :** Le modèle actuel est pull-only (GET). Les notifications push sont optionnelles mais amélioreraient l'expérience mobile.
|
||||
|
||||
---
|
||||
|
||||
## Synthèse par priorité
|
||||
|
||||
### ✅ Complété dans website-skeleton
|
||||
|
||||
1. ✅ **website-skeleton** : Envoi du contrat au UserWallet (1.1)
|
||||
2. ✅ **website-skeleton** : Gestion de session utilisateur (1.2)
|
||||
3. ✅ **website-skeleton** : Interface responsive mobile (1.3)
|
||||
4. ✅ **website-skeleton** : Amélioration UX (1.4)
|
||||
|
||||
### Priorité Haute (Blocant pour login fonctionnel)
|
||||
|
||||
1. **userwallet** : Validation des écrans login (2.1) - **À valider par tests**
|
||||
|
||||
### Priorité Moyenne (Améliore l'expérience)
|
||||
|
||||
2. **userwallet** : Notifications relais pour mobile (2.2)
|
||||
3. **userwallet** : Responsive design mobile (2.3) - **À vérifier**
|
||||
4. **api-relay** : Support notifications push (4.1) - **Optionnel**
|
||||
|
||||
### Priorité Basse (Optionnel)
|
||||
|
||||
5. **service-login-verify** : Persistance NonceCache (3.2)
|
||||
|
||||
---
|
||||
|
||||
## Workflow de login complet (cible)
|
||||
|
||||
### Sur PC
|
||||
|
||||
1. Utilisateur ouvre website-skeleton
|
||||
2. Clique sur "Se connecter" (ou iframe s'affiche automatiquement)
|
||||
3. Iframe UserWallet s'affiche
|
||||
4. website-skeleton envoie le contrat via `postMessage` à l'iframe
|
||||
5. UserWallet reçoit le contrat et met à jour le graphe
|
||||
6. Utilisateur sélectionne le service et le membre
|
||||
7. UserWallet construit le challenge et collecte les signatures
|
||||
8. UserWallet publie la preuve sur les relais
|
||||
9. UserWallet envoie `login-proof` au parent via `postMessage`
|
||||
10. website-skeleton vérifie la preuve avec `service-login-verify`
|
||||
11. Si acceptée : session ouverte, iframe masquée, interface "connecté" affichée
|
||||
12. Si refusée : erreur affichée, possibilité de réessayer
|
||||
|
||||
### Sur Mobile
|
||||
|
||||
1. Utilisateur ouvre website-skeleton (responsive)
|
||||
2. Clique sur "Se connecter"
|
||||
3. Iframe UserWallet s'affiche (adaptatif, responsive)
|
||||
4. Même workflow que PC
|
||||
5. **Différence :** Notifications push pour informer des nouvelles signatures disponibles (si implémenté)
|
||||
|
||||
---
|
||||
|
||||
## Notes importantes
|
||||
|
||||
- Les éléments marqués "À valider" nécessitent une validation explicite avant implémentation.
|
||||
- `website-skeleton` a déjà une intégration de base fonctionnelle (iframe, vérification, écoute messages).
|
||||
- Le package `service-login-verify` est déjà intégré et fonctionnel dans `website-skeleton`.
|
||||
- Les points bloquants principaux sont : l'envoi du contrat à l'iframe et la gestion de session.
|
||||
- Les écrans login de userwallet sont en place mais nécessitent validation.
|
||||
- Les notifications push sont optionnelles mais amélioreraient l'expérience mobile.
|
||||
|
||||
---
|
||||
|
||||
## Références
|
||||
|
||||
- `features/userwallet-contrat-login-reste-a-faire.md`
|
||||
- `features/userwallet-ecrans-login-a-valider.md`
|
||||
- `features/service-login-verify.md`
|
||||
- `RESTE_A_FAIRE.md`
|
||||
- `website-skeleton/src/main.ts` : exemple d'intégration complète
|
||||
- `userwallet/docs/specs.md` : spécifications complètes
|
||||
- `service-login-verify/README.md` : documentation du package
|
||||
476
fixKnowledge/api-anchorage-mutex-blockage-analysis.md
Normal file
476
fixKnowledge/api-anchorage-mutex-blockage-analysis.md
Normal file
@ -0,0 +1,476 @@
|
||||
# Analyse: Causes possibles du blocage du mutex et UTXOs verrouillés
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Auteur:** Équipe 4NK
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Ce document analyse les causes possibles du blocage du mutex et des UTXOs verrouillés dans l'API d'ancrage, ainsi que les moyens de contrôle et de correction.
|
||||
|
||||
## Causes possibles identifiées
|
||||
|
||||
### 1. Timeout RPC Bitcoin (ESOCKETTIMEDOUT)
|
||||
|
||||
**Cause:**
|
||||
- Les appels RPC vers Bitcoin Core timeout (défaut: 30s, configuré: 60s)
|
||||
- Si un appel RPC bloque indéfiniment, le code ne peut pas continuer
|
||||
- Le mutex reste acquis et les UTXOs restent verrouillés
|
||||
|
||||
**Scénarios:**
|
||||
- Bitcoin Core surchargé ou non réactif
|
||||
- Problème réseau entre l'API et Bitcoin Core
|
||||
- Bitcoin Core en cours de synchronisation ou de traitement lourd
|
||||
- Wallet Bitcoin verrouillé ou non accessible
|
||||
|
||||
**Code concerné:**
|
||||
- `bitcoin-rpc.js` ligne 23: `timeout: parseInt(process.env.BITCOIN_RPC_TIMEOUT || '30000')`
|
||||
- `.env` ligne 6: `BITCOIN_RPC_TIMEOUT=60000`
|
||||
|
||||
**Indicateurs:**
|
||||
- Logs: `Error getting balance: ESOCKETTIMEDOUT`
|
||||
- Logs: `Failed to get balance: ESOCKETTIMEDOUT`
|
||||
- Mutex bloqué pendant plus de 60 secondes
|
||||
|
||||
### 2. Promise.all() qui ne se résout jamais
|
||||
|
||||
**Cause:**
|
||||
- Ligne 253: `await Promise.all(addressPromises)` peut bloquer si une Promise ne se résout jamais
|
||||
- Si `getNewAddress()` timeout ou échoue silencieusement, la Promise reste en attente
|
||||
- Le mutex reste acquis
|
||||
|
||||
**Scénarios:**
|
||||
- Bitcoin RPC ne répond pas à `getNewAddress()`
|
||||
- Timeout RPC trop court pour certaines opérations
|
||||
- Erreur réseau non gérée
|
||||
|
||||
**Code concerné:**
|
||||
- `bitcoin-rpc.js` lignes 248-253: Génération des adresses en parallèle
|
||||
|
||||
**Indicateurs:**
|
||||
- Logs montrent "Anchor request received" mais pas de suite
|
||||
- Pas d'erreur dans les logs, mais la requête ne se termine jamais
|
||||
|
||||
### 3. Erreur non gérée avant le bloc finally
|
||||
|
||||
**Cause:**
|
||||
- Si une erreur se produit avant d'atteindre le bloc `finally` (ligne 908), le mutex peut ne pas être libéré
|
||||
- Exemple: erreur fatale Node.js, OOM (Out of Memory), crash du processus
|
||||
|
||||
**Scénarios:**
|
||||
- Erreur fatale Node.js (segfault, assertion failed)
|
||||
- Manque de mémoire (OOM killer)
|
||||
- Arrêt brutal du processus (kill -9)
|
||||
- Exception non catchée dans une fonction asynchrone
|
||||
|
||||
**Code concerné:**
|
||||
- `bitcoin-rpc.js` ligne 908-914: Bloc `finally` qui libère le mutex
|
||||
|
||||
**Indicateurs:**
|
||||
- Processus Node.js redémarré par systemd
|
||||
- Logs montrent un arrêt brutal sans message d'erreur
|
||||
- UTXOs verrouillés après redémarrage
|
||||
|
||||
### 4. Erreur dans le bloc finally lui-même
|
||||
|
||||
**Cause:**
|
||||
- Si `releaseMutex()` échoue dans le bloc `finally`, le mutex reste bloqué
|
||||
- Actuellement, l'erreur est seulement loggée en WARN, mais le mutex n'est pas libéré
|
||||
|
||||
**Scénarios:**
|
||||
- Exception lors de l'appel à `releaseMutex()`
|
||||
- Problème avec la Promise du mutex
|
||||
|
||||
**Code concerné:**
|
||||
- `bitcoin-rpc.js` lignes 910-914: Gestion d'erreur dans le `finally`
|
||||
|
||||
**Indicateurs:**
|
||||
- Logs: `Error releasing mutex`
|
||||
- Mutex reste bloqué malgré le `finally`
|
||||
|
||||
### 5. Déverrouillage des UTXOs qui échoue silencieusement
|
||||
|
||||
**Cause:**
|
||||
- Ligne 903: `this.unlockUtxo()` peut échouer silencieusement (ligne 152-158)
|
||||
- Si la mise à jour de la base de données échoue, l'UTXO reste verrouillé
|
||||
- Le mutex est libéré, mais les UTXOs restent verrouillés en base
|
||||
|
||||
**Scénarios:**
|
||||
- Base de données verrouillée (WAL mode, autre transaction en cours)
|
||||
- Erreur SQL (contrainte, table verrouillée)
|
||||
- Problème de permissions sur la base de données
|
||||
|
||||
**Code concerné:**
|
||||
- `bitcoin-rpc.js` lignes 142-159: `unlockUtxo()` avec gestion d'erreur silencieuse
|
||||
- `bitcoin-rpc.js` lignes 899-905: Déverrouillage en cas d'erreur
|
||||
|
||||
**Indicateurs:**
|
||||
- Mutex libéré (pas de timeout)
|
||||
- UTXOs toujours verrouillés dans la base de données
|
||||
- Logs: `Error updating UTXO unlock status in database`
|
||||
|
||||
### 6. Transaction SQL qui échoue partiellement
|
||||
|
||||
**Cause:**
|
||||
- Si plusieurs UTXOs sont verrouillés mais que le déverrouillage échoue pour certains
|
||||
- La boucle continue mais certains UTXOs restent verrouillés
|
||||
|
||||
**Scénarios:**
|
||||
- Erreur SQL pour un UTXO spécifique
|
||||
- UTXO supprimé de la base pendant le déverrouillage
|
||||
- Contrainte de base de données violée
|
||||
|
||||
**Code concerné:**
|
||||
- `bitcoin-rpc.js` lignes 900-904: Boucle de déverrouillage
|
||||
|
||||
**Indicateurs:**
|
||||
- Certains UTXOs déverrouillés, d'autres non
|
||||
- Logs montrant des erreurs pour certains UTXOs
|
||||
|
||||
### 7. Race condition lors de l'acquisition du mutex
|
||||
|
||||
**Cause:**
|
||||
- Si plusieurs requêtes arrivent simultanément, elles peuvent toutes acquérir le mutex
|
||||
- Le timeout de 180s peut être atteint avant que toutes les requêtes ne se terminent
|
||||
|
||||
**Scénarios:**
|
||||
- Pic de charge avec de nombreuses requêtes simultanées
|
||||
- Requêtes qui prennent plus de 180s chacune
|
||||
- Accumulation de requêtes en attente
|
||||
|
||||
**Code concerné:**
|
||||
- `bitcoin-rpc.js` lignes 42-76: `acquireUtxoMutex()` avec timeout de 180s
|
||||
|
||||
**Indicateurs:**
|
||||
- Nombreux timeouts de mutex dans les logs
|
||||
- Requêtes qui attendent plus de 180s
|
||||
|
||||
### 8. Problème de connexion à la base de données
|
||||
|
||||
**Cause:**
|
||||
- Si la connexion à la base de données est perdue, les opérations de verrouillage/déverrouillage échouent
|
||||
- Le mutex peut être libéré en mémoire, mais les UTXOs restent verrouillés en base
|
||||
|
||||
**Scénarios:**
|
||||
- Base de données corrompue
|
||||
- Connexion SQLite fermée
|
||||
- Problème de permissions
|
||||
|
||||
**Code concerné:**
|
||||
- `database.js`: Gestion de la connexion SQLite
|
||||
- Toutes les opérations de verrouillage/déverrouillage
|
||||
|
||||
**Indicateurs:**
|
||||
- Erreurs SQL dans les logs
|
||||
- Base de données inaccessible
|
||||
|
||||
## Moyens de contrôle
|
||||
|
||||
### 1. Monitoring des UTXOs verrouillés
|
||||
|
||||
**Script de vérification:**
|
||||
```bash
|
||||
# Vérifier le nombre d'UTXOs verrouillés
|
||||
curl http://localhost:3010/api/anchor/locked-utxos | jq '.count'
|
||||
|
||||
# Si > 0, il y a un problème
|
||||
```
|
||||
|
||||
**Alertes recommandées:**
|
||||
- Warning si > 5 UTXOs verrouillés
|
||||
- Critical si > 10 UTXOs verrouillés
|
||||
- Critical si UTXOs verrouillés depuis > 10 minutes
|
||||
|
||||
### 2. Monitoring des timeouts de mutex
|
||||
|
||||
**Vérification dans les logs:**
|
||||
```bash
|
||||
# Compter les timeouts de mutex dans les dernières heures
|
||||
sudo journalctl -u anchorage-api --since "1 hour ago" | grep -c "Mutex acquisition timeout"
|
||||
|
||||
# Si > 0, il y a un problème
|
||||
```
|
||||
|
||||
**Alertes recommandées:**
|
||||
- Warning si > 1 timeout par heure
|
||||
- Critical si > 5 timeouts par heure
|
||||
|
||||
### 3. Monitoring des erreurs RPC Bitcoin
|
||||
|
||||
**Vérification dans les logs:**
|
||||
```bash
|
||||
# Compter les erreurs RPC Bitcoin
|
||||
sudo journalctl -u anchorage-api --since "1 hour ago" | grep -c "ESOCKETTIMEDOUT\|ETIMEDOUT\|ECONNRESET"
|
||||
|
||||
# Si > 0, problème de connexion Bitcoin RPC
|
||||
```
|
||||
|
||||
**Alertes recommandées:**
|
||||
- Warning si > 1 erreur RPC par heure
|
||||
- Critical si > 5 erreurs RPC par heure
|
||||
|
||||
### 4. Monitoring de la durée des opérations
|
||||
|
||||
**Vérification:**
|
||||
- Logger le temps d'exécution de `createAnchorTransaction()`
|
||||
- Alerter si > 30 secondes (normal: < 10 secondes)
|
||||
|
||||
**Code à ajouter:**
|
||||
```javascript
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
// ... opération ...
|
||||
} finally {
|
||||
const duration = Date.now() - startTime;
|
||||
if (duration > 30000) {
|
||||
logger.warn('Anchor transaction took too long', { duration, hash });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Health check amélioré
|
||||
|
||||
**Endpoint à créer:**
|
||||
```javascript
|
||||
GET /health/detailed
|
||||
{
|
||||
"ok": true,
|
||||
"mutex": {
|
||||
"locked": false,
|
||||
"waiting": 0
|
||||
},
|
||||
"utxos": {
|
||||
"locked": 0,
|
||||
"locked_since": null
|
||||
},
|
||||
"bitcoin": {
|
||||
"connected": true,
|
||||
"rpc_timeout": 60000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Script de maintenance automatique
|
||||
|
||||
**Script cron à créer:**
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Déverrouiller les UTXOs verrouillés depuis plus de 10 minutes
|
||||
LOCKED_COUNT=$(curl -s http://localhost:3010/api/anchor/locked-utxos | jq '.count')
|
||||
if [ "$LOCKED_COUNT" -gt 0 ]; then
|
||||
# Vérifier l'âge des verrouillages
|
||||
# Si > 10 minutes, déverrouiller automatiquement
|
||||
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
node unlock-utxos.mjs
|
||||
fi
|
||||
```
|
||||
|
||||
## Moyens de correction
|
||||
|
||||
### 1. Correction immédiate (déjà implémentée)
|
||||
|
||||
**Script de déverrouillage:**
|
||||
- `api-anchorage/unlock-utxos.mjs`: Déverrouille tous les UTXOs verrouillés
|
||||
|
||||
**Utilisation:**
|
||||
```bash
|
||||
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
node unlock-utxos.mjs
|
||||
```
|
||||
|
||||
### 2. Améliorations préventives à implémenter
|
||||
|
||||
#### A. Timeout de sécurité sur le mutex
|
||||
|
||||
**Problème:** Le mutex peut rester acquis indéfiniment si l'opération se bloque.
|
||||
|
||||
**Solution:** Ajouter un timeout de sécurité qui libère automatiquement le mutex après un délai maximum.
|
||||
|
||||
**Code à ajouter:**
|
||||
```javascript
|
||||
async createAnchorTransaction(...) {
|
||||
const releaseMutex = await this.acquireUtxoMutex();
|
||||
let mutexSafetyTimeout;
|
||||
|
||||
// Timeout de sécurité: libérer le mutex après 5 minutes maximum
|
||||
mutexSafetyTimeout = setTimeout(() => {
|
||||
logger.error('Mutex held for too long, forcing release', { hash });
|
||||
releaseMutex();
|
||||
}, 300000); // 5 minutes
|
||||
|
||||
try {
|
||||
// ... opération ...
|
||||
} finally {
|
||||
clearTimeout(mutexSafetyTimeout);
|
||||
releaseMutex();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Timeout sur Promise.all()
|
||||
|
||||
**Problème:** `Promise.all()` peut bloquer indéfiniment si une Promise ne se résout jamais.
|
||||
|
||||
**Solution:** Ajouter un timeout sur `Promise.all()`.
|
||||
|
||||
**Code à modifier:**
|
||||
```javascript
|
||||
// Ligne 253: Remplacer
|
||||
const allAddresses = await Promise.all(addressPromises);
|
||||
|
||||
// Par:
|
||||
const allAddresses = await Promise.race([
|
||||
Promise.all(addressPromises),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Address generation timeout')), 30000)
|
||||
)
|
||||
]);
|
||||
```
|
||||
|
||||
#### C. Déverrouillage automatique des UTXOs anciens
|
||||
|
||||
**Problème:** Les UTXOs peuvent rester verrouillés si le processus crash.
|
||||
|
||||
**Solution:** Déverrouiller automatiquement les UTXOs verrouillés depuis plus de 10 minutes au démarrage.
|
||||
|
||||
**Code à ajouter dans `server.js`:**
|
||||
```javascript
|
||||
// Au démarrage du serveur
|
||||
const db = getDatabase();
|
||||
const result = db.prepare(`
|
||||
UPDATE utxos
|
||||
SET is_locked_in_mutex = 0
|
||||
WHERE is_locked_in_mutex = 1
|
||||
AND updated_at < datetime('now', '-10 minutes')
|
||||
`).run();
|
||||
if (result.changes > 0) {
|
||||
logger.info('Unlocked stale UTXOs on startup', { count: result.changes });
|
||||
}
|
||||
```
|
||||
|
||||
#### D. Retry avec backoff pour les appels RPC
|
||||
|
||||
**Problème:** Les timeouts RPC peuvent être temporaires.
|
||||
|
||||
**Solution:** Implémenter un retry avec backoff exponentiel.
|
||||
|
||||
**Code à ajouter:**
|
||||
```javascript
|
||||
async callRPCWithRetry(method, params, maxRetries = 3) {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await this.client[method](...params);
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) throw error;
|
||||
const delay = Math.min(1000 * Math.pow(2, i), 10000);
|
||||
logger.warn(`RPC call failed, retrying in ${delay}ms`, { method, attempt: i + 1 });
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### E. Monitoring et alertes
|
||||
|
||||
**Problème:** Les problèmes ne sont détectés qu'après coup.
|
||||
|
||||
**Solution:** Implémenter un système de monitoring proactif.
|
||||
|
||||
**À implémenter:**
|
||||
- Endpoint `/health/detailed` avec état du mutex et UTXOs
|
||||
- Script cron pour vérifier et déverrouiller automatiquement
|
||||
- Alertes (email, webhook) en cas de problème
|
||||
|
||||
### 3. Scripts de diagnostic
|
||||
|
||||
#### A. Script de diagnostic complet
|
||||
|
||||
**Créer:** `api-anchorage/diagnose.mjs`
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
import { getDatabase } from './src/database.js';
|
||||
|
||||
const db = getDatabase();
|
||||
|
||||
// UTXOs verrouillés
|
||||
const locked = db.prepare(`
|
||||
SELECT txid, vout, address, amount, updated_at,
|
||||
(julianday('now') - julianday(updated_at)) * 24 * 60 as minutes_locked
|
||||
FROM utxos
|
||||
WHERE is_locked_in_mutex = 1
|
||||
ORDER BY updated_at
|
||||
`).all();
|
||||
|
||||
console.log(`\n📊 UTXOs verrouillés: ${locked.length}`);
|
||||
if (locked.length > 0) {
|
||||
console.table(locked.map(u => ({
|
||||
txid: u.txid.substring(0, 16) + '...',
|
||||
vout: u.vout,
|
||||
amount: u.amount,
|
||||
locked_for_minutes: Math.round(u.minutes_locked * 100) / 100
|
||||
})));
|
||||
}
|
||||
|
||||
// UTXOs verrouillés depuis plus de 10 minutes
|
||||
const stale = locked.filter(u => u.minutes_locked > 10);
|
||||
if (stale.length > 0) {
|
||||
console.log(`\n⚠️ UTXOs verrouillés depuis plus de 10 minutes: ${stale.length}`);
|
||||
}
|
||||
|
||||
db.close();
|
||||
```
|
||||
|
||||
#### B. Script de nettoyage automatique
|
||||
|
||||
**Créer:** `api-anchorage/cleanup-stale-locks.mjs`
|
||||
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
import { getDatabase } from './src/database.js';
|
||||
|
||||
const db = getDatabase();
|
||||
const result = db.prepare(`
|
||||
UPDATE utxos
|
||||
SET is_locked_in_mutex = 0
|
||||
WHERE is_locked_in_mutex = 1
|
||||
AND updated_at < datetime('now', '-10 minutes')
|
||||
`).run();
|
||||
|
||||
console.log(`✅ UTXOs déverrouillés: ${result.changes}`);
|
||||
db.close();
|
||||
```
|
||||
|
||||
**Cron job:**
|
||||
```bash
|
||||
# Toutes les 5 minutes
|
||||
*/5 * * * * cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && node cleanup-stale-locks.mjs
|
||||
```
|
||||
|
||||
## Recommandations prioritaires
|
||||
|
||||
### Priorité 1 (Critique - à implémenter immédiatement)
|
||||
|
||||
1. **Déverrouillage automatique au démarrage** (solution C)
|
||||
2. **Timeout de sécurité sur le mutex** (solution A)
|
||||
3. **Script cron de nettoyage automatique** (solution B)
|
||||
|
||||
### Priorité 2 (Important - à implémenter rapidement)
|
||||
|
||||
4. **Health check amélioré** (solution 5)
|
||||
5. **Monitoring des UTXOs verrouillés** (solution 1)
|
||||
6. **Timeout sur Promise.all()** (solution B)
|
||||
|
||||
### Priorité 3 (Amélioration - à planifier)
|
||||
|
||||
7. **Retry avec backoff pour RPC** (solution D)
|
||||
8. **Monitoring et alertes** (solution E)
|
||||
9. **Scripts de diagnostic** (solution 3)
|
||||
|
||||
## Pages affectées
|
||||
|
||||
- `api-anchorage/src/bitcoin-rpc.js`: Améliorations du mutex et gestion d'erreurs
|
||||
- `api-anchorage/src/server.js`: Déverrouillage au démarrage
|
||||
- `api-anchorage/unlock-utxos.mjs`: Script de déverrouillage (existant)
|
||||
- `api-anchorage/cleanup-stale-locks.mjs`: Script de nettoyage automatique (à créer)
|
||||
- `api-anchorage/diagnose.mjs`: Script de diagnostic (à créer)
|
||||
- `fixKnowledge/api-anchorage-mutex-blockage-analysis.md`: Documentation (nouveau)
|
||||
197
fixKnowledge/api-anchorage-mutex-timeout-blocked.md
Normal file
197
fixKnowledge/api-anchorage-mutex-timeout-blocked.md
Normal file
@ -0,0 +1,197 @@
|
||||
# Correction: Mutex bloqué causant des timeouts dans l'API d'ancrage
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Auteur:** Équipe 4NK
|
||||
|
||||
## Problème
|
||||
|
||||
L'API d'ancrage Bitcoin ne répondait plus correctement. Toutes les requêtes d'ancrage échouaient avec des erreurs de timeout de mutex après 180 secondes (3 minutes).
|
||||
|
||||
**Machine concernée :** 192.168.1.105 (bitcoin)
|
||||
**Domaine externe :** https://anchorage.certificator.4nkweb.com
|
||||
**Service :** anchorage-api (port 3010)
|
||||
|
||||
### Symptômes
|
||||
|
||||
- **Erreurs répétées:** Des centaines d'erreurs `Mutex acquisition timeout after 180000ms` dans les logs
|
||||
- **UTXOs verrouillés:** 22 UTXOs restaient verrouillés dans la base de données (`is_locked_in_mutex = 1`)
|
||||
- **API non fonctionnelle:** Les requêtes d'ancrage échouaient systématiquement
|
||||
- **Blocage du mutex:** Le mutex était bloqué depuis une requête d'ancrage à 12:29:40 qui n'avait jamais libéré le mutex
|
||||
|
||||
### Impact
|
||||
|
||||
- **Indisponibilité:** L'API d'ancrage ne pouvait plus traiter de nouvelles requêtes
|
||||
- **UTXOs inutilisables:** 22 UTXOs étaient verrouillés et non disponibles pour de nouvelles transactions
|
||||
- **Requêtes en attente:** Toutes les nouvelles requêtes attendaient le timeout de 3 minutes avant d'échouer
|
||||
|
||||
## Root cause
|
||||
|
||||
Une requête d'ancrage reçue à 12:29:40 (`test-deploy-1769599785`) a acquis le mutex mais s'est bloquée avant d'atteindre le bloc `finally` qui libère le mutex. Le mutex est resté bloqué indéfiniment, empêchant toutes les requêtes suivantes de s'exécuter.
|
||||
|
||||
**Causes possibles:**
|
||||
1. **Appel RPC Bitcoin bloqué:** Un appel RPC vers Bitcoin Core n'a jamais répondu (timeout ou blocage réseau)
|
||||
2. **Opération asynchrone non résolue:** Une Promise n'a jamais été résolue ou rejetée
|
||||
3. **Erreur non gérée:** Une erreur s'est produite avant d'atteindre le bloc `finally`
|
||||
|
||||
**Code concerné:**
|
||||
- `api-anchorage/src/bitcoin-rpc.js` : Méthode `createAnchorTransaction()` ligne 230
|
||||
- Le mutex est acquis ligne 232 avec `await this.acquireUtxoMutex()`
|
||||
- Le mutex devrait être libéré dans le bloc `finally` ligne 908-914, mais si l'opération se bloque avant, le `finally` n'est jamais atteint
|
||||
|
||||
## Correctifs
|
||||
|
||||
### Solution immédiate (appliquée)
|
||||
|
||||
1. **Redémarrage du service:** Redémarrage forcé du service `anchorage-api` pour libérer le mutex en mémoire
|
||||
```bash
|
||||
sudo systemctl restart anchorage-api
|
||||
```
|
||||
|
||||
2. **Déverrouillage des UTXOs dans la base de données:** Le redémarrage libère le mutex en mémoire, mais les UTXOs restent verrouillés dans la base de données. Il faut les déverrouiller manuellement :
|
||||
```bash
|
||||
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
node unlock-utxos.mjs
|
||||
```
|
||||
|
||||
Ou directement avec SQL :
|
||||
```sql
|
||||
UPDATE utxos SET is_locked_in_mutex = 0 WHERE is_locked_in_mutex = 1;
|
||||
```
|
||||
|
||||
3. **Vérification:** Après redémarrage et déverrouillage, l'API répond correctement et les UTXOs sont disponibles
|
||||
|
||||
### Solutions préventives à implémenter
|
||||
|
||||
1. **Timeout sur les appels RPC Bitcoin:** Ajouter des timeouts explicites sur tous les appels RPC pour éviter les blocages indéfinis
|
||||
2. **Libération forcée du mutex:** Implémenter un mécanisme de libération automatique du mutex après un certain délai même si l'opération est en cours
|
||||
3. **Monitoring des opérations longues:** Logger les opérations qui prennent plus de 30 secondes pour identifier les blocages potentiels
|
||||
4. **Health check amélioré:** Vérifier l'état du mutex dans le health check et signaler si des UTXOs sont verrouillés depuis trop longtemps
|
||||
|
||||
## Modifications
|
||||
|
||||
### Fichiers concernés
|
||||
|
||||
- `api-anchorage/src/bitcoin-rpc.js` : Méthode `createAnchorTransaction()` et gestion du mutex
|
||||
- `api-anchorage/src/routes/anchor.js` : Gestion des erreurs et timeouts
|
||||
|
||||
### Modifications recommandées (à implémenter)
|
||||
|
||||
1. **Ajouter des timeouts sur les appels RPC:**
|
||||
```javascript
|
||||
// Dans bitcoin-rpc.js, ajouter des timeouts sur tous les appels RPC
|
||||
const rpcCallWithTimeout = async (method, params, timeoutMs = 30000) => {
|
||||
return Promise.race([
|
||||
this.client[method](...params),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`RPC timeout: ${method}`)), timeoutMs)
|
||||
)
|
||||
]);
|
||||
};
|
||||
```
|
||||
|
||||
2. **Libération automatique du mutex:**
|
||||
```javascript
|
||||
// Dans createAnchorTransaction, ajouter un timer de sécurité
|
||||
const mutexTimeout = setTimeout(() => {
|
||||
logger.error('Mutex held for too long, forcing release', { hash });
|
||||
releaseMutex();
|
||||
}, 300000); // 5 minutes max
|
||||
|
||||
try {
|
||||
// ... opération ...
|
||||
} finally {
|
||||
clearTimeout(mutexTimeout);
|
||||
releaseMutex();
|
||||
}
|
||||
```
|
||||
|
||||
3. **Déverrouillage automatique des UTXOs:**
|
||||
```javascript
|
||||
// Script de maintenance pour déverrouiller les UTXOs verrouillés depuis plus de 10 minutes
|
||||
// À exécuter périodiquement via cron
|
||||
```
|
||||
|
||||
## Modalités de déploiement
|
||||
|
||||
### Vérification de l'état actuel
|
||||
|
||||
```bash
|
||||
# Vérifier que le service est actif
|
||||
sudo systemctl status anchorage-api
|
||||
|
||||
# Vérifier que l'API répond
|
||||
curl http://localhost:3010/health
|
||||
|
||||
# Vérifier les UTXOs verrouillés
|
||||
curl http://localhost:3010/api/anchor/locked-utxos
|
||||
|
||||
# Vérifier les logs récents
|
||||
sudo journalctl -u anchorage-api -n 50 --no-pager
|
||||
```
|
||||
|
||||
### En cas de nouveau blocage
|
||||
|
||||
1. **Identifier le problème:**
|
||||
```bash
|
||||
# Vérifier les UTXOs verrouillés
|
||||
curl http://localhost:3010/api/anchor/locked-utxos
|
||||
|
||||
# Vérifier les logs pour identifier la requête bloquée
|
||||
sudo journalctl -u anchorage-api --since "10 minutes ago" | grep "Anchor request"
|
||||
```
|
||||
|
||||
2. **Redémarrer le service:**
|
||||
```bash
|
||||
sudo systemctl restart anchorage-api
|
||||
```
|
||||
|
||||
3. **Déverrouiller les UTXOs dans la base de données:**
|
||||
```bash
|
||||
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
node unlock-utxos.mjs
|
||||
```
|
||||
|
||||
4. **Vérifier la récupération:**
|
||||
```bash
|
||||
sleep 3
|
||||
curl http://localhost:3010/health
|
||||
curl http://localhost:3010/api/anchor/locked-utxos
|
||||
# Le count doit être 0
|
||||
```
|
||||
|
||||
## Modalités d'analyse
|
||||
|
||||
### Logs à surveiller
|
||||
|
||||
1. **Erreurs de timeout de mutex:**
|
||||
```bash
|
||||
sudo journalctl -u anchorage-api | grep "Mutex acquisition timeout"
|
||||
```
|
||||
|
||||
2. **Requêtes d'ancrage:**
|
||||
```bash
|
||||
sudo journalctl -u anchorage-api | grep "Anchor request received"
|
||||
```
|
||||
|
||||
3. **UTXOs verrouillés:**
|
||||
```bash
|
||||
curl http://localhost:3010/api/anchor/locked-utxos | jq '.count'
|
||||
```
|
||||
|
||||
### Métriques à surveiller
|
||||
|
||||
- **Nombre d'UTXOs verrouillés:** Doit être 0 en temps normal
|
||||
- **Durée des opérations d'ancrage:** Ne devrait pas dépasser 30 secondes
|
||||
- **Erreurs de timeout:** Ne devrait pas se produire en fonctionnement normal
|
||||
|
||||
### Alertes recommandées
|
||||
|
||||
- **UTXOs verrouillés > 5:** Alerte warning
|
||||
- **UTXOs verrouillés > 10:** Alerte critique
|
||||
- **Timeout de mutex:** Alerte critique immédiate
|
||||
|
||||
## Pages affectées
|
||||
|
||||
- `api-anchorage/src/bitcoin-rpc.js` : Gestion du mutex et des timeouts
|
||||
- `api-anchorage/src/routes/anchor.js` : Gestion des erreurs
|
||||
- `fixKnowledge/api-anchorage-mutex-timeout-blocked.md` : Documentation (nouveau)
|
||||
@ -15,19 +15,26 @@ export declare class PersistentNonceCache implements NonceCacheLike {
|
||||
init(): Promise<void>;
|
||||
/**
|
||||
* Check if nonce is valid (not seen within TTL). Records nonce on success.
|
||||
* Note: IndexedDB operations are async, but NonceCacheLike interface requires sync.
|
||||
* This implementation uses localStorage for synchronous access.
|
||||
* For true IndexedDB persistence, consider making the interface async.
|
||||
* Uses localStorage for synchronous access (required by NonceCacheLike interface).
|
||||
* Also persists to IndexedDB in background if available.
|
||||
*/
|
||||
isValid(nonce: string, timestamp: number): boolean;
|
||||
/**
|
||||
* Synchronous validation using localStorage (fallback).
|
||||
* Synchronous validation using localStorage (primary storage).
|
||||
*/
|
||||
private isValidSync;
|
||||
/**
|
||||
* Cleanup expired entries (localStorage).
|
||||
* Persist nonce to IndexedDB in background (async, non-blocking).
|
||||
*/
|
||||
private persistToIndexedDB;
|
||||
/**
|
||||
* Cleanup expired entries (localStorage and IndexedDB).
|
||||
*/
|
||||
private cleanupSync;
|
||||
/**
|
||||
* Cleanup expired entries from IndexedDB (async, non-blocking).
|
||||
*/
|
||||
private cleanupIndexedDB;
|
||||
/**
|
||||
* Clear all entries.
|
||||
*/
|
||||
|
||||
@ -1 +1 @@
|
||||
{"version":3,"file":"persistentNonceCache.d.ts","sourceRoot":"","sources":["../src/persistentNonceCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,EAAE,CAA4B;gBAE1B,KAAK,GAAE,MAAgB,EAAE,UAAU,GAAE,MAAsB;IAMvE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B3B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAIlD;;OAEG;IACH,OAAO,CAAC,WAAW;IAiBnB;;OAEG;IACH,OAAO,CAAC,WAAW;IAoBnB;;OAEG;IACH,KAAK,IAAI,IAAI;CAkBd"}
|
||||
{"version":3,"file":"persistentNonceCache.d.ts","sourceRoot":"","sources":["../src/persistentNonceCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,EAAE,CAA4B;gBAE1B,KAAK,GAAE,MAAgB,EAAE,UAAU,GAAE,MAAsB;IAMvE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B3B;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IASlD;;OAEG;IACH,OAAO,CAAC,WAAW;IAiBnB;;OAEG;YACW,kBAAkB;IAuBhC;;OAEG;IACH,OAAO,CAAC,WAAW;IAyBnB;;OAEG;YACW,gBAAgB;IAgC9B;;OAEG;IACH,KAAK,IAAI,IAAI;CAkBd"}
|
||||
@ -39,15 +39,19 @@ export class PersistentNonceCache {
|
||||
}
|
||||
/**
|
||||
* Check if nonce is valid (not seen within TTL). Records nonce on success.
|
||||
* Note: IndexedDB operations are async, but NonceCacheLike interface requires sync.
|
||||
* This implementation uses localStorage for synchronous access.
|
||||
* For true IndexedDB persistence, consider making the interface async.
|
||||
* Uses localStorage for synchronous access (required by NonceCacheLike interface).
|
||||
* Also persists to IndexedDB in background if available.
|
||||
*/
|
||||
isValid(nonce, timestamp) {
|
||||
return this.isValidSync(nonce, timestamp);
|
||||
const result = this.isValidSync(nonce, timestamp);
|
||||
// Persist to IndexedDB in background if available
|
||||
if (this.useIndexedDB && this.db !== null) {
|
||||
void this.persistToIndexedDB(nonce, timestamp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Synchronous validation using localStorage (fallback).
|
||||
* Synchronous validation using localStorage (primary storage).
|
||||
*/
|
||||
isValidSync(nonce, timestamp) {
|
||||
const now = Date.now();
|
||||
@ -64,7 +68,32 @@ export class PersistentNonceCache {
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Cleanup expired entries (localStorage).
|
||||
* Persist nonce to IndexedDB in background (async, non-blocking).
|
||||
*/
|
||||
async persistToIndexedDB(nonce, timestamp) {
|
||||
if (this.db === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const transaction = this.db.transaction(['nonces'], 'readwrite');
|
||||
const store = transaction.objectStore('nonces');
|
||||
await new Promise((resolve, reject) => {
|
||||
const request = store.put({ nonce, timestamp });
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
// IndexedDB errors are non-critical, localStorage is the primary storage
|
||||
console.warn('Failed to persist nonce to IndexedDB:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Cleanup expired entries (localStorage and IndexedDB).
|
||||
*/
|
||||
cleanupSync(now) {
|
||||
const keys = [];
|
||||
@ -83,6 +112,44 @@ export class PersistentNonceCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cleanup IndexedDB in background
|
||||
if (this.useIndexedDB && this.db !== null) {
|
||||
void this.cleanupIndexedDB(now);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Cleanup expired entries from IndexedDB (async, non-blocking).
|
||||
*/
|
||||
async cleanupIndexedDB(now) {
|
||||
if (this.db === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const transaction = this.db.transaction(['nonces'], 'readwrite');
|
||||
const store = transaction.objectStore('nonces');
|
||||
const index = store.index('timestamp');
|
||||
const range = IDBKeyRange.upperBound(now - this.ttlMs);
|
||||
await new Promise((resolve, reject) => {
|
||||
const request = index.openCursor(range);
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = event.target.result;
|
||||
if (cursor !== null && cursor !== undefined) {
|
||||
cursor.delete();
|
||||
cursor.continue();
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
// IndexedDB errors are non-critical
|
||||
console.warn('Failed to cleanup IndexedDB:', error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Clear all entries.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { NonceCacheLike } from './types.js';
|
||||
|
||||
/**
|
||||
* Persistent nonce cache using IndexedDB (browser) or localStorage (fallback).
|
||||
* Persistent nonce cache using IndexedDB (browser) and localStorage.
|
||||
* Implements NonceCacheLike interface for use with verifyLoginProof.
|
||||
*/
|
||||
export class PersistentNonceCache implements NonceCacheLike {
|
||||
@ -48,16 +48,20 @@ export class PersistentNonceCache implements NonceCacheLike {
|
||||
|
||||
/**
|
||||
* Check if nonce is valid (not seen within TTL). Records nonce on success.
|
||||
* Note: IndexedDB operations are async, but NonceCacheLike interface requires sync.
|
||||
* This implementation uses localStorage for synchronous access.
|
||||
* For true IndexedDB persistence, consider making the interface async.
|
||||
* Uses localStorage for synchronous access (required by NonceCacheLike interface).
|
||||
* Also persists to IndexedDB in background if available.
|
||||
*/
|
||||
isValid(nonce: string, timestamp: number): boolean {
|
||||
return this.isValidSync(nonce, timestamp);
|
||||
const result = this.isValidSync(nonce, timestamp);
|
||||
// Persist to IndexedDB in background if available
|
||||
if (this.useIndexedDB && this.db !== null) {
|
||||
void this.persistToIndexedDB(nonce, timestamp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous validation using localStorage (fallback).
|
||||
* Synchronous validation using localStorage (primary storage).
|
||||
*/
|
||||
private isValidSync(nonce: string, timestamp: number): boolean {
|
||||
const now = Date.now();
|
||||
@ -77,7 +81,33 @@ export class PersistentNonceCache implements NonceCacheLike {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired entries (localStorage).
|
||||
* Persist nonce to IndexedDB in background (async, non-blocking).
|
||||
*/
|
||||
private async persistToIndexedDB(nonce: string, timestamp: number): Promise<void> {
|
||||
if (this.db === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transaction = this.db.transaction(['nonces'], 'readwrite');
|
||||
const store = transaction.objectStore('nonces');
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = store.put({ nonce, timestamp });
|
||||
request.onsuccess = (): void => {
|
||||
resolve();
|
||||
};
|
||||
request.onerror = (): void => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
// IndexedDB errors are non-critical, localStorage is the primary storage
|
||||
console.warn('Failed to persist nonce to IndexedDB:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired entries (localStorage and IndexedDB).
|
||||
*/
|
||||
private cleanupSync(now: number): void {
|
||||
const keys: string[] = [];
|
||||
@ -97,6 +127,46 @@ export class PersistentNonceCache implements NonceCacheLike {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup IndexedDB in background
|
||||
if (this.useIndexedDB && this.db !== null) {
|
||||
void this.cleanupIndexedDB(now);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired entries from IndexedDB (async, non-blocking).
|
||||
*/
|
||||
private async cleanupIndexedDB(now: number): Promise<void> {
|
||||
if (this.db === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transaction = this.db.transaction(['nonces'], 'readwrite');
|
||||
const store = transaction.objectStore('nonces');
|
||||
const index = store.index('timestamp');
|
||||
const range = IDBKeyRange.upperBound(now - this.ttlMs);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = index.openCursor(range);
|
||||
request.onsuccess = (event: Event): void => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if (cursor !== null && cursor !== undefined) {
|
||||
cursor.delete();
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
request.onerror = (): void => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
// IndexedDB errors are non-critical
|
||||
console.warn('Failed to cleanup IndexedDB:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -245,6 +245,7 @@
|
||||
<p><strong>Endpoints publics (sans authentification) :</strong></p>
|
||||
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||
<li><code>GET /health</code> - Vérification de santé (API d'ancrage)</li>
|
||||
<li><code>GET /health/detailed</code> - Vérification de santé détaillée avec état mutex et UTXOs</li>
|
||||
<li><code>GET /api/anchor/locked-utxos</code> - Liste des UTXO verrouillés</li>
|
||||
<li><code>GET /health</code> - Vérification de santé (API faucet)</li>
|
||||
</ul>
|
||||
@ -304,6 +305,120 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Endpoint: Health Detailed -->
|
||||
<section class="api-docs-section">
|
||||
<div class="endpoint-card">
|
||||
<div class="endpoint-header">
|
||||
<span class="method-badge method-get">GET</span>
|
||||
<span class="endpoint-path">/health/detailed</span>
|
||||
</div>
|
||||
|
||||
<div class="endpoint-description">
|
||||
<p>Vérifie l'état détaillé de l'API, incluant l'état du mutex, des UTXOs verrouillés et de la connexion Bitcoin. Cet endpoint est public et ne nécessite pas d'authentification.</p>
|
||||
</div>
|
||||
|
||||
<div class="response-example">
|
||||
<h4>Réponse (200 OK) - Tout fonctionne</h4>
|
||||
<div class="code-block">
|
||||
<pre>{
|
||||
"ok": true,
|
||||
"service": "anchor-api",
|
||||
"mutex": {
|
||||
"locked": false,
|
||||
"waiting": 0,
|
||||
"timeout": 180000
|
||||
},
|
||||
"utxos": {
|
||||
"locked": 0,
|
||||
"locked_since": null,
|
||||
"stale_locks": 0,
|
||||
"stale_locks_details": []
|
||||
},
|
||||
"bitcoin": {
|
||||
"connected": true,
|
||||
"blocks": 12345,
|
||||
"chain": "signet",
|
||||
"rpc_timeout": 60000
|
||||
},
|
||||
"timestamp": "2026-01-25T12:00:00.000Z"
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-example">
|
||||
<h4>Réponse (200 OK) - Warning (UTXOs verrouillés mais < 10 min)</h4>
|
||||
<div class="code-block">
|
||||
<pre>{
|
||||
"ok": true,
|
||||
"service": "anchor-api",
|
||||
"mutex": {
|
||||
"locked": false,
|
||||
"waiting": 0,
|
||||
"timeout": 180000
|
||||
},
|
||||
"utxos": {
|
||||
"locked": 3,
|
||||
"locked_since": "2026-01-25T11:55:00.000Z",
|
||||
"stale_locks": 0,
|
||||
"stale_locks_details": []
|
||||
},
|
||||
"bitcoin": {
|
||||
"connected": true,
|
||||
"blocks": 12345,
|
||||
"chain": "signet",
|
||||
"rpc_timeout": 60000
|
||||
},
|
||||
"timestamp": "2026-01-25T12:00:00.000Z"
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="response-example">
|
||||
<h4>Réponse (503 Service Unavailable) - UTXOs verrouillés depuis > 10 min</h4>
|
||||
<div class="code-block">
|
||||
<pre>{
|
||||
"ok": false,
|
||||
"service": "anchor-api",
|
||||
"mutex": {
|
||||
"locked": false,
|
||||
"waiting": 0,
|
||||
"timeout": 180000
|
||||
},
|
||||
"utxos": {
|
||||
"locked": 5,
|
||||
"locked_since": "2026-01-25T11:45:00.000Z",
|
||||
"stale_locks": 5,
|
||||
"stale_locks_details": [
|
||||
{
|
||||
"txid": "abc123...",
|
||||
"vout": 0,
|
||||
"minutes_locked": 15.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"bitcoin": {
|
||||
"connected": true,
|
||||
"blocks": 12345,
|
||||
"chain": "signet",
|
||||
"rpc_timeout": 60000
|
||||
},
|
||||
"timestamp": "2026-01-25T12:00:00.000Z"
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>Codes de statut :</strong></p>
|
||||
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||
<li><code>200</code> - Tout fonctionne correctement</li>
|
||||
<li><code>200</code> - Warning si 5-10 UTXOs verrouillés (mais < 10 min)</li>
|
||||
<li><code>503</code> - Service Unavailable si UTXOs verrouillés depuis > 10 min ou > 10 UTXOs verrouillés</li>
|
||||
<li><code>503</code> - Service Unavailable si Bitcoin non connecté</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Endpoint: Anchor Document -->
|
||||
<section class="api-docs-section">
|
||||
<div class="endpoint-card">
|
||||
@ -1684,7 +1799,7 @@ Format: hexadécimal (0-9, a-f, A-F)</pre>
|
||||
<p>Les APIs sont accessibles aux adresses suivantes :</p>
|
||||
<div class="code-block">
|
||||
<pre>Dashboard : https://dashboard.certificator.4nkweb.com (port 3020)
|
||||
API d'Ancrage : https://certificator.4nkweb.com (port 3010)
|
||||
API d'Ancrage : https://anchorage.certificator.4nkweb.com (port 3010, machine bitcoin 192.168.1.105)
|
||||
API Faucet : https://faucet.certificator.4nkweb.com (port 3021)
|
||||
API Filigrane : https://watermark.certificator.4nkweb.com (port 3022)
|
||||
API ClamAV : https://antivir.certificator.4nkweb.com (port 3023)</pre>
|
||||
|
||||
@ -15,6 +15,8 @@ if (window.location.hostname.includes('dashboard.certificator.4nkweb.com')) {
|
||||
}
|
||||
|
||||
let selectedFile = null;
|
||||
let selectedClamavFile = null;
|
||||
let selectedFiligraneFile = null;
|
||||
let lastBlockHeight = null;
|
||||
let blockPollingInterval = null;
|
||||
let dataRefreshInterval = null;
|
||||
@ -722,15 +724,6 @@ async function verifyHash() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche/masque les options de filigrane
|
||||
*/
|
||||
function toggleWatermarkOptions() {
|
||||
const enabled = document.getElementById('watermark-enabled').checked;
|
||||
const optionsDiv = document.getElementById('watermark-options');
|
||||
optionsDiv.style.display = enabled ? 'block' : 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ancre le document
|
||||
*/
|
||||
@ -743,7 +736,6 @@ async function anchorDocument() {
|
||||
|
||||
const apiKey = apiKeyElement.value.trim();
|
||||
const hash = document.getElementById('anchor-hash').value;
|
||||
const watermarkEnabled = document.getElementById('watermark-enabled').checked;
|
||||
|
||||
if (!apiKey || apiKey.length === 0) {
|
||||
showResult('anchor-result', 'error', 'Veuillez entrer une clé API.');
|
||||
@ -760,11 +752,6 @@ async function anchorDocument() {
|
||||
try {
|
||||
showResult('anchor-result', 'info', 'Ancrage en cours...');
|
||||
|
||||
// Si le filigrane est activé, utiliser l'API filigrane
|
||||
if (watermarkEnabled) {
|
||||
await anchorWithWatermark(apiKey);
|
||||
} else {
|
||||
// Sinon, utiliser l'API d'ancrage classique
|
||||
const response = await fetch(`${API_BASE_URL}/api/anchor/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -787,200 +774,11 @@ async function anchorDocument() {
|
||||
} else {
|
||||
showResult('anchor-result', 'error', data.message || data.error || 'Erreur lors de l\'ancrage.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ancre le document avec filigrane
|
||||
*/
|
||||
async function anchorWithWatermark(apiKey) {
|
||||
// Vérifier que la clé API est bien fournie
|
||||
if (!apiKey || apiKey.trim().length === 0) {
|
||||
showResult('anchor-result', 'error', 'Veuillez entrer une clé API.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Utiliser le backend du dashboard comme proxy vers l'API filigrane
|
||||
const watermarkApiUrl = API_BASE_URL;
|
||||
|
||||
const textContent = document.getElementById('anchor-text').value;
|
||||
const fileInput = document.getElementById('anchor-file');
|
||||
const currentSelectedFile = selectedFile || (fileInput.files.length > 0 ? fileInput.files[0] : null);
|
||||
|
||||
// Préparer les données du fichier
|
||||
let fileData = null;
|
||||
let fileName = null;
|
||||
let mimeType = null;
|
||||
|
||||
if (currentSelectedFile) {
|
||||
const reader = new FileReader();
|
||||
await new Promise((resolve, reject) => {
|
||||
reader.onload = (e) => {
|
||||
const arrayBuffer = e.target.result;
|
||||
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||
fileData = base64;
|
||||
fileName = currentSelectedFile.name;
|
||||
mimeType = currentSelectedFile.type;
|
||||
resolve();
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(currentSelectedFile);
|
||||
});
|
||||
}
|
||||
|
||||
// Préparer les options de filigrane
|
||||
const watermarkOptionsRaw = {
|
||||
enabled: true,
|
||||
text: document.getElementById('watermark-text').value.trim() || undefined,
|
||||
signature: document.getElementById('watermark-signature').value.trim() || undefined,
|
||||
depositor: document.getElementById('watermark-depositor').value.trim() || undefined,
|
||||
watermarkedFileName: document.getElementById('watermarked-filename').value.trim() || undefined,
|
||||
originalFileName: document.getElementById('original-filename').value.trim() || undefined,
|
||||
dateUTC: document.getElementById('watermark-date-utc').checked,
|
||||
dateLocal: document.getElementById('watermark-date-local').checked,
|
||||
blockNumber: document.getElementById('watermark-block-number').checked,
|
||||
blockHash: document.getElementById('watermark-block-hash').checked,
|
||||
documentHash: document.getElementById('watermark-document-hash').checked,
|
||||
};
|
||||
|
||||
// Nettoyer l'objet en supprimant les valeurs undefined
|
||||
const watermarkOptions = Object.fromEntries(
|
||||
Object.entries(watermarkOptionsRaw).filter(([_, value]) => value !== undefined)
|
||||
);
|
||||
|
||||
// S'assurer que enabled est toujours présent et true
|
||||
watermarkOptions.enabled = true;
|
||||
|
||||
const requestBody = {
|
||||
apiKey,
|
||||
watermarkOptions,
|
||||
};
|
||||
|
||||
if (textContent) {
|
||||
requestBody.textContent = textContent;
|
||||
} else if (fileData) {
|
||||
requestBody.fileData = fileData;
|
||||
requestBody.fileName = fileName;
|
||||
requestBody.mimeType = mimeType;
|
||||
} else {
|
||||
showResult('anchor-result', 'error', 'Veuillez saisir un texte ou sélectionner un fichier.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${watermarkApiUrl}/api/watermark/document`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (jsonError) {
|
||||
const text = await response.text();
|
||||
showResult('anchor-result', 'error', `Erreur de parsing de la réponse: ${text}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data.message || data.error || `Erreur ${response.status}: ${response.statusText}`;
|
||||
console.error('Watermark API error:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: data,
|
||||
requestBody: {
|
||||
hasApiKey: !!requestBody.apiKey,
|
||||
hasWatermarkOptions: !!requestBody.watermarkOptions,
|
||||
watermarkEnabled: requestBody.watermarkOptions?.enabled,
|
||||
hasFileData: !!requestBody.fileData,
|
||||
hasTextContent: !!requestBody.textContent,
|
||||
},
|
||||
});
|
||||
showResult('anchor-result', 'error', errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
const mempoolBaseUrl = 'https://mempool.4nkweb.com/fr';
|
||||
const originalTxidLink = data.original.txid
|
||||
? `<a href="${mempoolBaseUrl}/tx/${data.original.txid}" target="_blank" style="color: #6ec6ff; text-decoration: underline;">${data.original.txid}</a>`
|
||||
: 'N/A';
|
||||
const watermarkedTxidLink = data.watermarked.txid
|
||||
? `<a href="${mempoolBaseUrl}/tx/${data.watermarked.txid}" target="_blank" style="color: #6ec6ff; text-decoration: underline;">${data.watermarked.txid}</a>`
|
||||
: 'N/A';
|
||||
|
||||
let resultHtml = `
|
||||
<strong>✅ Documents ancrés avec succès !</strong><br><br>
|
||||
<strong>Document original :</strong><br>
|
||||
<strong>Hash SHA256 :</strong> ${data.original.hash || 'N/A'}<br>
|
||||
<strong>TXID :</strong> ${originalTxidLink}<br>
|
||||
<strong>Statut :</strong> ${data.original.status}<br>
|
||||
<strong>Confirmations :</strong> ${data.original.confirmations || 0}<br>
|
||||
<strong>Fichier :</strong> ${data.original.file.name}<br><br>
|
||||
<strong>Document filigrané :</strong><br>
|
||||
<strong>Hash SHA256 :</strong> ${data.watermarked.hash || 'N/A'}<br>
|
||||
<strong>TXID :</strong> ${watermarkedTxidLink}<br>
|
||||
<strong>Statut :</strong> ${data.watermarked.status}<br>
|
||||
<strong>Confirmations :</strong> ${data.watermarked.confirmations || 0}<br>
|
||||
<strong>Fichier :</strong> ${data.watermarked.file.name}<br><br>
|
||||
`;
|
||||
|
||||
if (data.certificate && data.certificate.data) {
|
||||
resultHtml += `
|
||||
<strong>Certificat :</strong><br>
|
||||
<strong>Fichier :</strong> ${data.certificate.name}<br><br>
|
||||
`;
|
||||
}
|
||||
|
||||
if (data.merged && data.merged.data) {
|
||||
resultHtml += `
|
||||
<strong>Document fusionné (filigrané + certificat) :</strong><br>
|
||||
<strong>Fichier :</strong> ${data.merged.name}<br><br>
|
||||
`;
|
||||
}
|
||||
|
||||
resultHtml += `<p style="color: #90ee90; font-weight: bold;">📥 Téléchargement automatique en cours...</p>`;
|
||||
|
||||
showResult('anchor-result', 'success', resultHtml);
|
||||
|
||||
// Télécharger automatiquement les 4 documents avec un délai entre chaque
|
||||
setTimeout(() => {
|
||||
downloadFile(data.original.file.name, data.original.file.data);
|
||||
}, 500);
|
||||
|
||||
setTimeout(() => {
|
||||
downloadFile(data.watermarked.file.name, data.watermarked.file.data);
|
||||
}, 1000);
|
||||
|
||||
if (data.certificate && data.certificate.data) {
|
||||
setTimeout(() => {
|
||||
downloadFile(data.certificate.name, data.certificate.data);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
if (data.merged && data.merged.data) {
|
||||
setTimeout(() => {
|
||||
downloadFile(data.merged.name, data.merged.data);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Recharger le nombre d'ancrages après un court délai
|
||||
setTimeout(loadAnchorCount, 2000);
|
||||
} else {
|
||||
showResult('anchor-result', 'error', data.message || data.error || 'Erreur lors de l\'ancrage avec filigrane.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in anchorWithWatermark:', error);
|
||||
showResult('anchor-result', 'error', `Erreur lors de l'ancrage avec filigrane: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Télécharge un fichier depuis base64
|
||||
*/
|
||||
@ -1062,3 +860,350 @@ function showResult(elementId, type, message) {
|
||||
element.className = `result ${type}`;
|
||||
element.innerHTML = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la sélection de fichier pour ClamAV
|
||||
*/
|
||||
function handleClamavFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
selectedClamavFile = file;
|
||||
const fileInfo = document.getElementById('clamav-file-info');
|
||||
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
||||
const fileSize = file.size;
|
||||
const isOverLimit = fileSize > maxSize;
|
||||
|
||||
let infoHtml = `
|
||||
<strong>Fichier sélectionné :</strong> ${file.name}<br>
|
||||
<strong>Taille :</strong> ${formatFileSize(file.size)}<br>
|
||||
<strong>Type :</strong> ${file.type || 'Non spécifié'}
|
||||
`;
|
||||
|
||||
if (isOverLimit) {
|
||||
infoHtml += `<br><span style="color: #ff6b6b; font-weight: bold;">⚠️ Fichier trop volumineux (limite : 100 MB)</span>`;
|
||||
}
|
||||
|
||||
fileInfo.innerHTML = infoHtml;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scanne un fichier avec ClamAV
|
||||
*/
|
||||
async function scanWithClamav() {
|
||||
if (!selectedClamavFile) {
|
||||
showResult('clamav-result', 'error', 'Veuillez sélectionner un fichier.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier la taille du fichier (limite : 100 MB)
|
||||
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
||||
if (selectedClamavFile.size > maxSize) {
|
||||
showResult('clamav-result', 'error', `Le fichier est trop volumineux (${formatFileSize(selectedClamavFile.size)}). La limite est de 100 MB.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showResult('clamav-result', 'info', 'Scan en cours...');
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
reader.onload = async (e) => {
|
||||
try {
|
||||
const arrayBuffer = e.target.result;
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
let binaryString = '';
|
||||
for (let i = 0; i < uint8Array.length; i++) {
|
||||
binaryString += String.fromCharCode(uint8Array[i]);
|
||||
}
|
||||
const base64 = btoa(binaryString);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/clamav/scan`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
data: base64,
|
||||
filename: selectedClamavFile.name,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status}: ${response.statusText}` }));
|
||||
throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.infected) {
|
||||
showResult('clamav-result', 'error',
|
||||
`⚠️ Fichier infecté détecté !<br>
|
||||
<strong>Fichier :</strong> ${data.filename || selectedClamavFile.name}<br>
|
||||
<strong>Taille :</strong> ${formatFileSize(data.size || selectedClamavFile.size)}<br>
|
||||
<strong>Virus détectés :</strong> ${data.viruses ? data.viruses.join(', ') : 'Inconnu'}`);
|
||||
} else {
|
||||
showResult('clamav-result', 'success',
|
||||
`✅ Fichier propre !<br>
|
||||
<strong>Fichier :</strong> ${data.filename || selectedClamavFile.name}<br>
|
||||
<strong>Taille :</strong> ${formatFileSize(data.size || selectedClamavFile.size)}<br>
|
||||
<strong>Statut :</strong> Aucun virus détecté`);
|
||||
}
|
||||
resolve();
|
||||
} catch (error) {
|
||||
showResult('clamav-result', 'error', `Erreur : ${error.message}`);
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = (error) => {
|
||||
showResult('clamav-result', 'error', `Erreur lors de la lecture du fichier : ${error.message || 'Erreur inconnue'}`);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(selectedClamavFile);
|
||||
});
|
||||
} catch (error) {
|
||||
showResult('clamav-result', 'error', `Erreur lors du scan : ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change d'onglet pour le formulaire filigrane
|
||||
*/
|
||||
function switchTabFiligrane(tab, buttonElement) {
|
||||
// Désactiver tous les onglets filigrane
|
||||
document.querySelectorAll('#filigrane-text-tab, #filigrane-file-tab').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
// Désactiver tous les boutons d'onglet dans la section filigrane
|
||||
const filigraneSection = document.querySelector('.filigrane-section');
|
||||
if (filigraneSection) {
|
||||
filigraneSection.querySelectorAll('.tab-button').forEach(button => {
|
||||
button.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
// Activer l'onglet sélectionné
|
||||
document.getElementById(`filigrane-${tab}-tab`).classList.add('active');
|
||||
// Activer le bouton correspondant
|
||||
if (buttonElement) {
|
||||
buttonElement.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la sélection de fichier pour Filigrane
|
||||
*/
|
||||
function handleFiligraneFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
selectedFiligraneFile = file;
|
||||
const fileInfo = document.getElementById('filigrane-file-info');
|
||||
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
||||
const fileSize = file.size;
|
||||
const isOverLimit = fileSize > maxSize;
|
||||
|
||||
let infoHtml = `
|
||||
<strong>Fichier sélectionné :</strong> ${file.name}<br>
|
||||
<strong>Taille :</strong> ${formatFileSize(file.size)}<br>
|
||||
<strong>Type :</strong> ${file.type || 'Non spécifié'}
|
||||
`;
|
||||
|
||||
if (isOverLimit) {
|
||||
infoHtml += `<br><span style="color: #ff6b6b; font-weight: bold;">⚠️ Fichier trop volumineux (limite : 100 MB)</span>`;
|
||||
}
|
||||
|
||||
fileInfo.innerHTML = infoHtml;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste l'API Filigrane
|
||||
*/
|
||||
async function testFiligrane() {
|
||||
const apiKey = document.getElementById('filigrane-api-key').value.trim();
|
||||
|
||||
if (!apiKey) {
|
||||
showResult('filigrane-result', 'error', 'Veuillez entrer une clé API.');
|
||||
return;
|
||||
}
|
||||
|
||||
const textContent = document.getElementById('filigrane-text').value;
|
||||
const fileInput = document.getElementById('filigrane-file');
|
||||
const currentSelectedFile = selectedFiligraneFile || (fileInput.files.length > 0 ? fileInput.files[0] : null);
|
||||
|
||||
if (!textContent && !currentSelectedFile) {
|
||||
showResult('filigrane-result', 'error', 'Veuillez saisir un texte ou sélectionner un fichier.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier la taille du fichier si un fichier est sélectionné
|
||||
if (currentSelectedFile) {
|
||||
const maxSize = 100 * 1024 * 1024; // 100 MB en bytes
|
||||
if (currentSelectedFile.size > maxSize) {
|
||||
showResult('filigrane-result', 'error', `Le fichier est trop volumineux (${formatFileSize(currentSelectedFile.size)}). La limite est de 100 MB.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
showResult('filigrane-result', 'info', 'Traitement en cours...');
|
||||
|
||||
// Préparer les données du fichier
|
||||
let fileData = null;
|
||||
let fileName = null;
|
||||
let mimeType = null;
|
||||
|
||||
if (currentSelectedFile) {
|
||||
const reader = new FileReader();
|
||||
await new Promise((resolve, reject) => {
|
||||
reader.onload = (e) => {
|
||||
const arrayBuffer = e.target.result;
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
let binaryString = '';
|
||||
for (let i = 0; i < uint8Array.length; i++) {
|
||||
binaryString += String.fromCharCode(uint8Array[i]);
|
||||
}
|
||||
const base64 = btoa(binaryString);
|
||||
fileData = base64;
|
||||
fileName = currentSelectedFile.name;
|
||||
mimeType = currentSelectedFile.type;
|
||||
resolve();
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsArrayBuffer(currentSelectedFile);
|
||||
});
|
||||
}
|
||||
|
||||
// Préparer les options de filigrane
|
||||
const watermarkOptions = {
|
||||
enabled: true,
|
||||
text: document.getElementById('filigrane-watermark-text').value.trim() || undefined,
|
||||
signature: document.getElementById('filigrane-watermark-signature').value.trim() || undefined,
|
||||
depositor: document.getElementById('filigrane-watermark-depositor').value.trim() || undefined,
|
||||
watermarkedFileName: document.getElementById('filigrane-watermarked-filename').value.trim() || undefined,
|
||||
originalFileName: document.getElementById('filigrane-original-filename').value.trim() || undefined,
|
||||
dateUTC: document.getElementById('filigrane-watermark-date-utc').checked,
|
||||
dateLocal: document.getElementById('filigrane-watermark-date-local').checked,
|
||||
blockNumber: document.getElementById('filigrane-watermark-block-number').checked,
|
||||
blockHash: document.getElementById('filigrane-watermark-block-hash').checked,
|
||||
documentHash: document.getElementById('filigrane-watermark-document-hash').checked,
|
||||
};
|
||||
|
||||
// Nettoyer l'objet en supprimant les valeurs undefined
|
||||
const cleanedWatermarkOptions = Object.fromEntries(
|
||||
Object.entries(watermarkOptions).filter(([_, value]) => value !== undefined)
|
||||
);
|
||||
|
||||
const requestBody = {
|
||||
apiKey,
|
||||
watermarkOptions: cleanedWatermarkOptions,
|
||||
};
|
||||
|
||||
if (textContent) {
|
||||
requestBody.textContent = textContent;
|
||||
} else if (fileData) {
|
||||
requestBody.fileData = fileData;
|
||||
requestBody.fileName = fileName;
|
||||
requestBody.mimeType = mimeType;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/watermark/document`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (jsonError) {
|
||||
const text = await response.text();
|
||||
showResult('filigrane-result', 'error', `Erreur de parsing de la réponse: ${text}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = data.message || data.error || `Erreur ${response.status}: ${response.statusText}`;
|
||||
showResult('filigrane-result', 'error', errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
const mempoolBaseUrl = 'https://mempool.4nkweb.com/fr';
|
||||
const originalTxidLink = data.original.txid
|
||||
? `<a href="${mempoolBaseUrl}/tx/${data.original.txid}" target="_blank" style="color: #6ec6ff; text-decoration: underline;">${data.original.txid}</a>`
|
||||
: 'N/A';
|
||||
const watermarkedTxidLink = data.watermarked.txid
|
||||
? `<a href="${mempoolBaseUrl}/tx/${data.watermarked.txid}" target="_blank" style="color: #6ec6ff; text-decoration: underline;">${data.watermarked.txid}</a>`
|
||||
: 'N/A';
|
||||
|
||||
let resultHtml = `
|
||||
<strong>✅ Documents ancrés avec succès !</strong><br><br>
|
||||
<strong>Document original :</strong><br>
|
||||
<strong>Hash SHA256 :</strong> ${data.original.hash || 'N/A'}<br>
|
||||
<strong>TXID :</strong> ${originalTxidLink}<br>
|
||||
<strong>Statut :</strong> ${data.original.status}<br>
|
||||
<strong>Confirmations :</strong> ${data.original.confirmations || 0}<br>
|
||||
<strong>Fichier :</strong> ${data.original.file.name}<br><br>
|
||||
<strong>Document filigrané :</strong><br>
|
||||
<strong>Hash SHA256 :</strong> ${data.watermarked.hash || 'N/A'}<br>
|
||||
<strong>TXID :</strong> ${watermarkedTxidLink}<br>
|
||||
<strong>Statut :</strong> ${data.watermarked.status}<br>
|
||||
<strong>Confirmations :</strong> ${data.watermarked.confirmations || 0}<br>
|
||||
<strong>Fichier :</strong> ${data.watermarked.file.name}<br><br>
|
||||
`;
|
||||
|
||||
if (data.certificate && data.certificate.data) {
|
||||
resultHtml += `
|
||||
<strong>Certificat :</strong><br>
|
||||
<strong>Fichier :</strong> ${data.certificate.name}<br><br>
|
||||
`;
|
||||
}
|
||||
|
||||
if (data.merged && data.merged.data) {
|
||||
resultHtml += `
|
||||
<strong>Document fusionné (filigrané + certificat) :</strong><br>
|
||||
<strong>Fichier :</strong> ${data.merged.name}<br><br>
|
||||
`;
|
||||
}
|
||||
|
||||
resultHtml += `<p style="color: #90ee90; font-weight: bold;">📥 Téléchargement automatique en cours...</p>`;
|
||||
|
||||
showResult('filigrane-result', 'success', resultHtml);
|
||||
|
||||
// Télécharger automatiquement les documents avec un délai entre chaque
|
||||
setTimeout(() => {
|
||||
downloadFile(data.original.file.name, data.original.file.data);
|
||||
}, 500);
|
||||
|
||||
setTimeout(() => {
|
||||
downloadFile(data.watermarked.file.name, data.watermarked.file.data);
|
||||
}, 1000);
|
||||
|
||||
if (data.certificate && data.certificate.data) {
|
||||
setTimeout(() => {
|
||||
downloadFile(data.certificate.name, data.certificate.data);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
if (data.merged && data.merged.data) {
|
||||
setTimeout(() => {
|
||||
downloadFile(data.merged.name, data.merged.data);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Recharger le nombre d'ancrages après un court délai
|
||||
setTimeout(loadAnchorCount, 2000);
|
||||
} else {
|
||||
showResult('filigrane-result', 'error', data.message || data.error || 'Erreur lors du test de l\'API filigrane.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in testFiligrane:', error);
|
||||
showResult('filigrane-result', 'error', `Erreur : ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,6 +164,75 @@
|
||||
.confirmed-check.no {
|
||||
color: #dc3545;
|
||||
}
|
||||
.health-section {
|
||||
background: var(--card-background);
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.health-status {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.health-item {
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.health-item label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.health-item .value {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.health-item .value.ok {
|
||||
color: #28a745;
|
||||
}
|
||||
.health-item .value.warning {
|
||||
color: #ffc107;
|
||||
}
|
||||
.health-item .value.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
.unlock-button {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
}
|
||||
.unlock-button:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
.unlock-button:disabled {
|
||||
background: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.stale-locks-list {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
border-radius: 5px;
|
||||
border: 1px solid #dc3545;
|
||||
}
|
||||
.stale-locks-list h4 {
|
||||
margin-top: 0;
|
||||
color: #dc3545;
|
||||
}
|
||||
.stale-lock-item {
|
||||
padding: 5px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -184,6 +253,15 @@
|
||||
<input type="text" id="hash-search" class="search-input" placeholder="Entrez un hash (64 caractères hexadécimaux)..." oninput="filterHashList()">
|
||||
</div>
|
||||
|
||||
<div class="health-section" style="background: var(--card-background); padding: 15px; border-radius: 5px; margin-bottom: 20px; border: 1px solid var(--border-color);">
|
||||
<h2 style="margin-top: 0; margin-bottom: 15px;">État de l'API d'Ancrage</h2>
|
||||
<div id="health-status">Chargement de l'état...</div>
|
||||
<div style="margin-top: 15px;">
|
||||
<button id="unlock-utxos-button" class="unlock-button" onclick="unlockUtxos()" style="display: none;">Déverrouiller les UTXOs</button>
|
||||
<button id="refresh-health-button" class="refresh-button" onclick="loadHealthStatus()" style="margin-left: 10px;">Actualiser l'état</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
<div class="loading">Chargement des hash...</div>
|
||||
</div>
|
||||
@ -202,6 +280,7 @@
|
||||
// Charger la liste au chargement de la page
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadHashList();
|
||||
loadHealthStatus();
|
||||
});
|
||||
|
||||
async function loadHashList() {
|
||||
@ -341,6 +420,141 @@
|
||||
const now = new Date();
|
||||
document.getElementById('last-update').textContent = now.toLocaleString('fr-FR');
|
||||
}
|
||||
|
||||
async function loadHealthStatus() {
|
||||
const healthDiv = document.getElementById('health-status');
|
||||
const unlockButton = document.getElementById('unlock-utxos-button');
|
||||
const refreshButton = document.getElementById('refresh-health-button');
|
||||
|
||||
refreshButton.disabled = true;
|
||||
healthDiv.innerHTML = '<div style="text-align: center; padding: 20px; font-size: 1em; color: var(--text-color);">Chargement de l\'état...</div>';
|
||||
|
||||
try {
|
||||
// Utiliser l'endpoint proxy du dashboard
|
||||
const response = await fetch(`${API_BASE_URL}/api/anchor/health/detailed`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const health = await response.json();
|
||||
|
||||
let html = '<div class="health-status">';
|
||||
|
||||
// État général
|
||||
const statusClass = health.ok ? 'ok' : 'error';
|
||||
html += `<div class="health-item">
|
||||
<label>État général</label>
|
||||
<div class="value ${statusClass}">${health.ok ? '✓ OK' : '✗ Problème'}</div>
|
||||
</div>`;
|
||||
|
||||
// Mutex
|
||||
const mutexClass = health.mutex.locked ? 'warning' : 'ok';
|
||||
html += `<div class="health-item">
|
||||
<label>Mutex</label>
|
||||
<div class="value ${mutexClass}">
|
||||
${health.mutex.locked ? '🔒 Verrouillé' : '✓ Libre'}
|
||||
${health.mutex.waiting > 0 ? `(${health.mutex.waiting} en attente)` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// UTXOs verrouillés
|
||||
const utxosClass = health.utxos.locked === 0 ? 'ok' : (health.utxos.locked > 10 ? 'error' : 'warning');
|
||||
html += `<div class="health-item">
|
||||
<label>UTXOs verrouillés</label>
|
||||
<div class="value ${utxosClass}">${health.utxos.locked}</div>
|
||||
</div>`;
|
||||
|
||||
// UTXOs stale
|
||||
if (health.utxos.stale_locks > 0) {
|
||||
html += `<div class="health-item">
|
||||
<label>UTXOs stale (>10 min)</label>
|
||||
<div class="value error">${health.utxos.stale_locks}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Bitcoin
|
||||
const bitcoinClass = health.bitcoin.connected ? 'ok' : 'error';
|
||||
html += `<div class="health-item">
|
||||
<label>Bitcoin</label>
|
||||
<div class="value ${bitcoinClass}">
|
||||
${health.bitcoin.connected ? '✓ Connecté' : '✗ Déconnecté'}
|
||||
${health.bitcoin.blocks > 0 ? ` (${health.bitcoin.blocks.toLocaleString('fr-FR')} blocs)` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Afficher les détails des UTXOs stale
|
||||
if (health.utxos.stale_locks > 0 && health.utxos.stale_locks_details.length > 0) {
|
||||
html += '<div class="stale-locks-list">';
|
||||
html += '<h4>UTXOs verrouillés depuis plus de 10 minutes :</h4>';
|
||||
health.utxos.stale_locks_details.forEach(utxo => {
|
||||
html += `<div class="stale-lock-item">${utxo.txid} vout:${utxo.vout} (${utxo.minutes_locked.toFixed(1)} min)</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Afficher le timestamp
|
||||
html += `<div style="margin-top: 10px; font-size: 0.9em; color: var(--text-color); opacity: 0.7;">
|
||||
Dernière mise à jour : ${new Date(health.timestamp).toLocaleString('fr-FR')}
|
||||
</div>`;
|
||||
|
||||
healthDiv.innerHTML = html;
|
||||
|
||||
// Afficher le bouton de déverrouillage si des UTXOs sont verrouillés
|
||||
if (health.utxos.locked > 0) {
|
||||
unlockButton.style.display = 'inline-block';
|
||||
} else {
|
||||
unlockButton.style.display = 'none';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading health status:', error);
|
||||
healthDiv.innerHTML = `<div class="error">Erreur lors du chargement de l'état : ${error.message}</div>`;
|
||||
unlockButton.style.display = 'none';
|
||||
} finally {
|
||||
refreshButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function unlockUtxos() {
|
||||
const unlockButton = document.getElementById('unlock-utxos-button');
|
||||
const originalText = unlockButton.textContent;
|
||||
|
||||
if (!confirm('Êtes-vous sûr de vouloir déverrouiller tous les UTXOs ? Cette action libère le mutex et peut affecter les opérations en cours.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
unlockButton.disabled = true;
|
||||
unlockButton.textContent = 'Déverrouillage en cours...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/anchor/unlock-utxos`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
alert(`✅ ${result.unlocked || 0} UTXO(s) déverrouillé(s) avec succès.`);
|
||||
|
||||
// Recharger l'état de santé
|
||||
loadHealthStatus();
|
||||
} catch (error) {
|
||||
console.error('Error unlocking UTXOs:', error);
|
||||
alert(`❌ Erreur lors du déverrouillage : ${error.message}`);
|
||||
} finally {
|
||||
unlockButton.disabled = false;
|
||||
unlockButton.textContent = originalText;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<footer>
|
||||
<p>Bitcoin Ancrage Dashboard - Équipe 4NK</p>
|
||||
|
||||
@ -104,6 +104,18 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Avertissement de sécurité -->
|
||||
<section class="security-warning-section">
|
||||
<div class="security-warning">
|
||||
<strong>⚠️ Avertissement de sécurité</strong>
|
||||
<p>
|
||||
<strong>Le serveur ne stocke pas vos documents.</strong> Cependant, soyez vigilant :
|
||||
HTTPS n'est pas une protection parfaite et les navigateurs sont très imparfaits.
|
||||
Évitez d'envoyer des documents contenant des informations sensibles ou confidentielles.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section Test d'Ancrage -->
|
||||
<section class="anchor-section">
|
||||
<h2>Test de l'API d'Ancrage</h2>
|
||||
@ -142,61 +154,6 @@
|
||||
</div>
|
||||
|
||||
<div id="anchor-result" class="result"></div>
|
||||
|
||||
<div class="watermark-section" style="margin-top: 20px; padding-top: 20px; border-top: 2px solid var(--border-color);">
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="watermark-enabled" onchange="toggleWatermarkOptions()">
|
||||
<label for="watermark-enabled">Ajouter un filigrane au document</label>
|
||||
</div>
|
||||
|
||||
<div id="watermark-options" style="display: none; margin-top: 15px;">
|
||||
<div class="security-warning">
|
||||
<strong>⚠️ Avertissement de sécurité</strong>
|
||||
<p>
|
||||
<strong>Le serveur ne stocke pas vos documents.</strong> Cependant, soyez vigilant :
|
||||
HTTPS n'est pas une protection parfaite et les navigateurs sont très imparfaits.
|
||||
Évitez d'envoyer des documents contenant des informations sensibles ou confidentielles.
|
||||
</p>
|
||||
</div>
|
||||
<label for="watermark-text">Texte libre du filigrane (optionnel) :</label>
|
||||
<input type="text" id="watermark-text" placeholder="Texte à afficher dans le filigrane">
|
||||
|
||||
<label for="watermark-signature">Signature cryptographique (optionnel) :</label>
|
||||
<input type="text" id="watermark-signature" placeholder="Signature à afficher dans le filigrane">
|
||||
|
||||
<label for="watermark-depositor">Nom du dépositaire (optionnel) :</label>
|
||||
<input type="text" id="watermark-depositor" placeholder="Nom du dépositaire">
|
||||
|
||||
<label for="watermarked-filename">Nom de fichier du PDF filigrané (optionnel) :</label>
|
||||
<input type="text" id="watermarked-filename" placeholder="Si vide, utilise le nom d'origine avec extension .pdf">
|
||||
|
||||
<label for="original-filename">Nom de fichier du document d'origine (optionnel) :</label>
|
||||
<input type="text" id="original-filename" placeholder="Si vide et texte saisi, utilise 'origin.md'">
|
||||
|
||||
<div class="watermark-checkboxes" style="margin-top: 15px;">
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="watermark-date-utc">
|
||||
<label for="watermark-date-utc">Ajouter la date UTC</label>
|
||||
</div>
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="watermark-date-local">
|
||||
<label for="watermark-date-local">Ajouter la date locale</label>
|
||||
</div>
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="watermark-block-number">
|
||||
<label for="watermark-block-number">Ajouter le numéro de bloc</label>
|
||||
</div>
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="watermark-block-hash">
|
||||
<label for="watermark-block-hash">Ajouter le hash du bloc</label>
|
||||
</div>
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="watermark-document-hash">
|
||||
<label for="watermark-document-hash">Ajouter le hash du document d'origine</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -213,6 +170,93 @@
|
||||
<div id="faucet-result" class="result"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section Test API ClamAV -->
|
||||
<section class="clamav-section">
|
||||
<h2>Test de l'API ClamAV</h2>
|
||||
<div class="card">
|
||||
<p>Scannez un fichier pour détecter les virus avec ClamAV</p>
|
||||
<label for="clamav-file">Fichier à scanner :</label>
|
||||
<input type="file" id="clamav-file" onchange="handleClamavFileSelect(event)">
|
||||
<p class="file-limit-info" style="font-size: 0.9em; color: #999; margin-top: 5px; margin-bottom: 10px;">
|
||||
<strong>Limite de taille :</strong> 100 MB maximum
|
||||
</p>
|
||||
<div id="clamav-file-info" class="file-info"></div>
|
||||
<button onclick="scanWithClamav()">Scanner le Fichier</button>
|
||||
<div id="clamav-result" class="result"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section Test API Filigrane -->
|
||||
<section class="filigrane-section">
|
||||
<h2>Test de l'API Filigrane</h2>
|
||||
<div class="card">
|
||||
<label for="filigrane-api-key">Clé API :</label>
|
||||
<input type="text" id="filigrane-api-key" placeholder="Entrez votre clé API">
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab-button active" onclick="switchTabFiligrane('text', this)">Saisie de Texte</button>
|
||||
<button class="tab-button" onclick="switchTabFiligrane('file', this)">Sélection de Fichier</button>
|
||||
</div>
|
||||
|
||||
<div id="filigrane-text-tab" class="tab-content active">
|
||||
<label for="filigrane-text">Texte à convertir en PDF et filigraner :</label>
|
||||
<textarea id="filigrane-text" rows="5" placeholder="Entrez le texte à convertir en PDF et filigraner..."></textarea>
|
||||
</div>
|
||||
|
||||
<div id="filigrane-file-tab" class="tab-content">
|
||||
<label for="filigrane-file">Fichier à filigraner :</label>
|
||||
<input type="file" id="filigrane-file" onchange="handleFiligraneFileSelect(event)">
|
||||
<p class="file-limit-info" style="font-size: 0.9em; color: #999; margin-top: 5px; margin-bottom: 10px;">
|
||||
<strong>Limite de taille :</strong> 100 MB maximum
|
||||
</p>
|
||||
<div id="filigrane-file-info" class="file-info"></div>
|
||||
</div>
|
||||
|
||||
<div class="watermark-section" style="margin-top: 20px; padding-top: 20px; border-top: 2px solid var(--border-color);">
|
||||
<label for="filigrane-watermark-text">Texte libre du filigrane (optionnel) :</label>
|
||||
<input type="text" id="filigrane-watermark-text" placeholder="Texte à afficher dans le filigrane">
|
||||
|
||||
<label for="filigrane-watermark-signature">Signature cryptographique (optionnel) :</label>
|
||||
<input type="text" id="filigrane-watermark-signature" placeholder="Signature à afficher dans le filigrane">
|
||||
|
||||
<label for="filigrane-watermark-depositor">Nom du dépositaire (optionnel) :</label>
|
||||
<input type="text" id="filigrane-watermark-depositor" placeholder="Nom du dépositaire">
|
||||
|
||||
<label for="filigrane-watermarked-filename">Nom de fichier du PDF filigrané (optionnel) :</label>
|
||||
<input type="text" id="filigrane-watermarked-filename" placeholder="Si vide, utilise le nom d'origine avec extension .pdf">
|
||||
|
||||
<label for="filigrane-original-filename">Nom de fichier du document d'origine (optionnel) :</label>
|
||||
<input type="text" id="filigrane-original-filename" placeholder="Si vide et texte saisi, utilise 'origin.md'">
|
||||
|
||||
<div class="watermark-checkboxes" style="margin-top: 15px;">
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="filigrane-watermark-date-utc">
|
||||
<label for="filigrane-watermark-date-utc">Ajouter la date UTC</label>
|
||||
</div>
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="filigrane-watermark-date-local">
|
||||
<label for="filigrane-watermark-date-local">Ajouter la date locale</label>
|
||||
</div>
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="filigrane-watermark-block-number">
|
||||
<label for="filigrane-watermark-block-number">Ajouter le numéro de bloc</label>
|
||||
</div>
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="filigrane-watermark-block-hash">
|
||||
<label for="filigrane-watermark-block-hash">Ajouter le hash du bloc</label>
|
||||
</div>
|
||||
<div class="watermark-checkbox">
|
||||
<input type="checkbox" id="filigrane-watermark-document-hash">
|
||||
<label for="filigrane-watermark-document-hash">Ajouter le hash du document d'origine</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onclick="testFiligrane()" style="margin-top: 20px;">Tester l'API Filigrane</button>
|
||||
<div id="filigrane-result" class="result"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
|
||||
@ -337,6 +337,10 @@ footer .git-icon {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.security-warning-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.security-warning {
|
||||
background-color: rgba(255, 193, 7, 0.15);
|
||||
border: 2px solid #ffc107;
|
||||
@ -349,6 +353,7 @@ footer .git-icon {
|
||||
color: #ffc107;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.security-warning p {
|
||||
|
||||
@ -954,6 +954,115 @@ app.post('/api/anchor/test', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Route pour déverrouiller les UTXOs (appelle l'API d'ancrage externe)
|
||||
app.post('/api/anchor/unlock-utxos', async (req, res) => {
|
||||
try {
|
||||
// Toujours utiliser l'URL publique HTTPS
|
||||
let anchorApiUrl = process.env.ANCHOR_API_URL || 'https://anchorage.certificator.4nkweb.com';
|
||||
if (!anchorApiUrl.startsWith('https://')) {
|
||||
anchorApiUrl = 'https://anchorage.certificator.4nkweb.com';
|
||||
}
|
||||
|
||||
const anchorApiKey = process.env.ANCHOR_API_KEY || '';
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (anchorApiKey) {
|
||||
headers['x-api-key'] = anchorApiKey;
|
||||
}
|
||||
|
||||
const result = await makeHttpRequest(anchorApiUrl, '/api/anchor/unlock-utxos', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Error unlocking UTXOs', { error: error.message });
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Route pour obtenir l'état de santé détaillé (appelle l'API d'ancrage externe)
|
||||
app.get('/api/anchor/health/detailed', async (req, res) => {
|
||||
try {
|
||||
// Toujours utiliser l'URL publique HTTPS
|
||||
let anchorApiUrl = process.env.ANCHOR_API_URL || 'https://anchorage.certificator.4nkweb.com';
|
||||
if (!anchorApiUrl.startsWith('https://')) {
|
||||
anchorApiUrl = 'https://anchorage.certificator.4nkweb.com';
|
||||
}
|
||||
|
||||
const result = await makeHttpRequest(anchorApiUrl, '/health/detailed', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Error getting detailed health', { error: error.message });
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Route pour scanner un fichier avec ClamAV (appelle l'API ClamAV externe)
|
||||
app.post('/api/clamav/scan', async (req, res) => {
|
||||
try {
|
||||
const { data, filename } = req.body;
|
||||
|
||||
if (!data) {
|
||||
return res.status(400).json({
|
||||
error: 'Bad Request',
|
||||
message: 'data is required',
|
||||
});
|
||||
}
|
||||
|
||||
// Toujours utiliser l'URL publique HTTPS, même en interne
|
||||
let clamavApiUrl = process.env.CLAMAV_API_URL;
|
||||
|
||||
// Si CLAMAV_API_URL n'est pas défini ou contient localhost/127.0.0.1, utiliser l'URL HTTPS publique
|
||||
if (!clamavApiUrl || clamavApiUrl.includes('localhost') || clamavApiUrl.includes('127.0.0.1')) {
|
||||
clamavApiUrl = 'https://antivir.certificator.4nkweb.com';
|
||||
logger.info('Forcing public HTTPS ClamAV API URL', { url: clamavApiUrl, reason: 'always use public URL' });
|
||||
} else if (!clamavApiUrl.startsWith('https://')) {
|
||||
// Si l'URL est définie mais n'est pas HTTPS, forcer l'URL publique HTTPS
|
||||
clamavApiUrl = 'https://antivir.certificator.4nkweb.com';
|
||||
logger.info('Forcing public HTTPS ClamAV API URL (non-HTTPS URL detected)', { url: clamavApiUrl });
|
||||
}
|
||||
|
||||
logger.info('Calling ClamAV API', {
|
||||
url: clamavApiUrl,
|
||||
hasData: !!data,
|
||||
dataLength: data ? data.length : 0,
|
||||
filename: filename || 'unknown',
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await makeHttpRequest(clamavApiUrl, '/api/scan/buffer', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ data, filename }),
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (fetchError) {
|
||||
logger.error('Error calling ClamAV API', {
|
||||
error: fetchError.message,
|
||||
url: clamavApiUrl,
|
||||
stack: fetchError.stack
|
||||
});
|
||||
res.status(500).json({ error: 'Failed to connect to ClamAV API', message: fetchError.message });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error scanning with ClamAV', { error: error.message });
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Route pour le filigrane (appelle l'API filigrane externe)
|
||||
app.post('/api/watermark/document', async (req, res) => {
|
||||
try {
|
||||
|
||||
@ -11,22 +11,13 @@ import { useIdentity } from './useIdentity';
|
||||
import { signMessage, generateChallenge } from '../utils/crypto';
|
||||
import { GraphResolver } from '../services/graphResolver';
|
||||
import { updateGraphFromMessage } from '../services/syncUpdateGraph';
|
||||
import {
|
||||
loadDefaultContract,
|
||||
hasDefaultContract,
|
||||
} from '../utils/defaultContract';
|
||||
import type { LoginProof } from '../types/identity';
|
||||
import type { Contrat, Action } from '../types/contract';
|
||||
|
||||
export function useChannel() {
|
||||
const { identity } = useIdentity();
|
||||
const graphResolver = useState(() => {
|
||||
const resolver = new GraphResolver();
|
||||
// Load default contract if in iframe and no contract received yet
|
||||
if (isInIframe()) {
|
||||
loadDefaultContract(resolver);
|
||||
}
|
||||
return resolver;
|
||||
return new GraphResolver();
|
||||
})[0];
|
||||
|
||||
const handleAuthRequest = useCallback(
|
||||
@ -63,7 +54,6 @@ export function useChannel() {
|
||||
const handleContract = useCallback(
|
||||
(message: ContractMessage): void => {
|
||||
const payload = message.payload;
|
||||
let hasValidContract = false;
|
||||
|
||||
if (payload?.contrat !== undefined) {
|
||||
// Valider et ajouter le contrat principal
|
||||
@ -76,7 +66,6 @@ export function useChannel() {
|
||||
contrat.validateurs !== null
|
||||
) {
|
||||
updateGraphFromMessage(contrat, graphResolver);
|
||||
hasValidContract = true;
|
||||
} else {
|
||||
console.warn(
|
||||
'Invalid contract structure received via channel: missing required fields',
|
||||
@ -128,11 +117,6 @@ export function useChannel() {
|
||||
}
|
||||
}
|
||||
|
||||
// Si un contrat valide a été reçu, on peut remplacer le contrat par défaut
|
||||
if (hasValidContract && hasDefaultContract(graphResolver)) {
|
||||
// Le contrat par défaut reste en cache mais le nouveau contrat prend le dessus
|
||||
// pour les résolutions de graphe
|
||||
}
|
||||
},
|
||||
[graphResolver],
|
||||
);
|
||||
|
||||
@ -297,3 +297,107 @@ a:focus-visible {
|
||||
}
|
||||
|
||||
/* Dark theme is now the default with Bitcoin colors */
|
||||
|
||||
/* Mobile responsive styles */
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
padding: var(--spacing-md);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
section {
|
||||
padding: var(--spacing-md);
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 44px;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input {
|
||||
min-height: 44px;
|
||||
font-size: 1rem;
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
form > div {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
main {
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
section {
|
||||
padding: var(--spacing-sm);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
button:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keyboard virtual handling */
|
||||
@media (max-width: 768px) {
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
scroll-margin-top: 100px;
|
||||
}
|
||||
|
||||
/* Prevent zoom on input focus (iOS) */
|
||||
input[type='text'],
|
||||
input[type='password'],
|
||||
input[type='email'],
|
||||
input[type='number'],
|
||||
textarea,
|
||||
select {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch-friendly interactive elements */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
button,
|
||||
a,
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
button:active {
|
||||
opacity: 0.7;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ export interface RelayHashEvent {
|
||||
relay: string;
|
||||
timestamp: number;
|
||||
objectType?: RelayObjectType;
|
||||
source?: 'polling' | 'push' | 'manual';
|
||||
source?: 'polling' | 'manual';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,9 +65,20 @@ export interface ProcessHashResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backoff strategy for mobile network optimization.
|
||||
*/
|
||||
interface BackoffState {
|
||||
attempts: number;
|
||||
lastAttempt: number;
|
||||
baseDelay: number;
|
||||
maxDelay: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for handling relay notifications and automatic fetching.
|
||||
* Supports pull-based model (polling) and can be extended for push (WebSocket).
|
||||
* Pull-based model (polling) only: HTTP REST calls, no WebSocket.
|
||||
* Optimized for mobile with backoff and reconnection strategies.
|
||||
*/
|
||||
export class RelayNotificationService {
|
||||
private readonly graphResolver: GraphResolver;
|
||||
@ -75,6 +86,11 @@ export class RelayNotificationService {
|
||||
private hashListeners: Set<HashEventListener> = new Set();
|
||||
private isPolling: boolean = false;
|
||||
private pollingInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private backoffStates: Map<string, BackoffState> = new Map();
|
||||
private readonly maxRetries: number = 5;
|
||||
private readonly baseBackoffMs: number = 1000;
|
||||
private readonly maxBackoffMs: number = 60000;
|
||||
private isOnline: boolean = true;
|
||||
|
||||
constructor(
|
||||
graphResolver: GraphResolver,
|
||||
@ -82,6 +98,102 @@ export class RelayNotificationService {
|
||||
) {
|
||||
this.graphResolver = graphResolver;
|
||||
this.identity = identity ?? null;
|
||||
this.setupNetworkListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup network online/offline listeners for mobile.
|
||||
*/
|
||||
private setupNetworkListeners(): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOnline = navigator.onLine ?? true;
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
this.isOnline = true;
|
||||
console.info('[RelayNotifications] Network online, resuming operations');
|
||||
this.resumeOperations();
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
this.isOnline = false;
|
||||
console.warn('[RelayNotifications] Network offline, pausing operations');
|
||||
this.pauseOperations();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume operations after network reconnection.
|
||||
*/
|
||||
private resumeOperations(): void {
|
||||
// Reset backoff states
|
||||
this.backoffStates.clear();
|
||||
// Polling will continue automatically on next interval
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause operations when network is offline.
|
||||
*/
|
||||
private pauseOperations(): void {
|
||||
// Polling will fail gracefully when offline
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate backoff delay for retry.
|
||||
*/
|
||||
private getBackoffDelay(endpoint: string): number {
|
||||
const state = this.backoffStates.get(endpoint) ?? {
|
||||
attempts: 0,
|
||||
lastAttempt: 0,
|
||||
baseDelay: this.baseBackoffMs,
|
||||
maxDelay: this.maxBackoffMs,
|
||||
};
|
||||
|
||||
const delay = Math.min(
|
||||
state.baseDelay * Math.pow(2, state.attempts),
|
||||
state.maxDelay,
|
||||
);
|
||||
return delay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record failed attempt and update backoff state.
|
||||
*/
|
||||
private recordFailure(endpoint: string): void {
|
||||
const state = this.backoffStates.get(endpoint) ?? {
|
||||
attempts: 0,
|
||||
lastAttempt: Date.now(),
|
||||
baseDelay: this.baseBackoffMs,
|
||||
maxDelay: this.maxBackoffMs,
|
||||
};
|
||||
|
||||
state.attempts += 1;
|
||||
state.lastAttempt = Date.now();
|
||||
this.backoffStates.set(endpoint, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record successful attempt and reset backoff.
|
||||
*/
|
||||
private recordSuccess(endpoint: string): void {
|
||||
this.backoffStates.delete(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if should retry based on backoff state.
|
||||
*/
|
||||
private shouldRetry(endpoint: string): boolean {
|
||||
const state = this.backoffStates.get(endpoint);
|
||||
if (state === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (state.attempts >= this.maxRetries) {
|
||||
return false;
|
||||
}
|
||||
const delay = this.getBackoffDelay(endpoint);
|
||||
return Date.now() - state.lastAttempt >= delay;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,6 +225,7 @@ export class RelayNotificationService {
|
||||
|
||||
/**
|
||||
* Process a hash: fetch message, signatures, keys, decrypt and update graph.
|
||||
* Optimized for mobile with backoff and retry logic.
|
||||
*/
|
||||
async processHash(
|
||||
hash: string,
|
||||
@ -134,6 +247,11 @@ export class RelayNotificationService {
|
||||
keysFetched: 0,
|
||||
};
|
||||
|
||||
if (!this.isOnline) {
|
||||
result.error = 'Network offline';
|
||||
return result;
|
||||
}
|
||||
|
||||
const relays = getStoredRelays().filter((r) => r.enabled);
|
||||
if (relays.length === 0) {
|
||||
result.error = 'No enabled relays';
|
||||
@ -141,40 +259,67 @@ export class RelayNotificationService {
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch message
|
||||
// Fetch message with retry and backoff
|
||||
let msgChiffre: MsgChiffre | null = null;
|
||||
if (fetchMessage) {
|
||||
for (const relay of relays) {
|
||||
if (!this.shouldRetry(relay.endpoint)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
msgChiffre = await getMessageByHash(relay.endpoint, hash);
|
||||
result.messageFetched = true;
|
||||
this.recordSuccess(relay.endpoint);
|
||||
break;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
this.recordFailure(relay.endpoint);
|
||||
console.warn(
|
||||
`Failed to fetch message from ${relay.endpoint}:`,
|
||||
error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch signatures
|
||||
// Fetch signatures with retry and backoff
|
||||
if (fetchSignatures) {
|
||||
for (const relay of relays) {
|
||||
if (!this.shouldRetry(relay.endpoint)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const sigs = await getSignatures(relay.endpoint, hash);
|
||||
result.signaturesFetched += sigs.length;
|
||||
} catch {
|
||||
this.recordSuccess(relay.endpoint);
|
||||
} catch (error) {
|
||||
this.recordFailure(relay.endpoint);
|
||||
console.warn(
|
||||
`Failed to fetch signatures from ${relay.endpoint}:`,
|
||||
error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch keys
|
||||
// Fetch keys with retry and backoff
|
||||
let allKeys: MsgCle[] = [];
|
||||
if (fetchKeys) {
|
||||
for (const relay of relays) {
|
||||
if (!this.shouldRetry(relay.endpoint)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const keys = await getKeys(relay.endpoint, hash);
|
||||
allKeys.push(...keys);
|
||||
} catch {
|
||||
this.recordSuccess(relay.endpoint);
|
||||
} catch (error) {
|
||||
this.recordFailure(relay.endpoint);
|
||||
console.warn(
|
||||
`Failed to fetch keys from ${relay.endpoint}:`,
|
||||
error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -202,10 +347,15 @@ export class RelayNotificationService {
|
||||
async (h: string): Promise<MsgSignature[]> => {
|
||||
const allSigs: MsgSignature[] = [];
|
||||
for (const relay of relays) {
|
||||
if (!this.shouldRetry(relay.endpoint)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const sigs = await getSignatures(relay.endpoint, h);
|
||||
allSigs.push(...sigs);
|
||||
this.recordSuccess(relay.endpoint);
|
||||
} catch {
|
||||
this.recordFailure(relay.endpoint);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -296,7 +446,7 @@ export class RelayNotificationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger processing of a hash (e.g., from push notification).
|
||||
* Manually trigger processing of a hash (e.g., from manual trigger).
|
||||
* Supports specifying object type for better handling.
|
||||
*/
|
||||
async triggerHashProcessing(
|
||||
@ -311,7 +461,7 @@ export class RelayNotificationService {
|
||||
relay,
|
||||
timestamp: Date.now(),
|
||||
objectType: objectType ?? 'unknown',
|
||||
source: 'push',
|
||||
source: 'manual',
|
||||
});
|
||||
|
||||
// Process hash
|
||||
@ -338,4 +488,13 @@ export class RelayNotificationService {
|
||||
|
||||
return await this.processHash(hash, optimizedOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup: stop polling, remove listeners.
|
||||
*/
|
||||
cleanup(): void {
|
||||
this.stopPolling();
|
||||
this.hashListeners.clear();
|
||||
this.backoffStates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,9 +3,8 @@ import { GraphResolver } from '../services/graphResolver';
|
||||
import { updateGraphFromMessage } from '../services/syncUpdateGraph';
|
||||
|
||||
/**
|
||||
* Default contract configuration (placeholder).
|
||||
* Default contract configuration.
|
||||
* Used when no contract is received via channel message.
|
||||
* Should be replaced with actual default contract in production.
|
||||
*/
|
||||
export const DEFAULT_CONTRACT: Contrat = {
|
||||
uuid: 'default-contract-uuid',
|
||||
@ -43,7 +42,7 @@ export const DEFAULT_CONTRACT: Contrat = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Default login action (placeholder).
|
||||
* Default login action.
|
||||
*/
|
||||
export const DEFAULT_LOGIN_ACTION: Action = {
|
||||
uuid: 'default-login-action-uuid',
|
||||
@ -95,7 +94,7 @@ export const DEFAULT_LOGIN_ACTION: Action = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Default service (placeholder).
|
||||
* Default service.
|
||||
*/
|
||||
export const DEFAULT_SERVICE: Service = {
|
||||
uuid: 'default-service-uuid',
|
||||
|
||||
2
website-skeleton/.gitignore
vendored
2
website-skeleton/.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
*.local
|
||||
.env.private
|
||||
.env.backup
|
||||
|
||||
@ -26,7 +26,9 @@ Ouvre par défaut sur `http://localhost:3024`. L'iframe pointe vers UserWallet (
|
||||
## Configuration
|
||||
|
||||
- **Origine UserWallet** : `src/config.ts` définit `USERWALLET_ORIGIN`. En dev, défaut `http://localhost:3018` (si UserWallet tourne en dev sur ce port). En prod, défaut `https://userwallet.certificator.4nkweb.com`. Pour override : variable d'environnement `VITE_USERWALLET_ORIGIN` (ex. `VITE_USERWALLET_ORIGIN=http://localhost:3018 npm run dev`).
|
||||
- **Validateurs** : `DEFAULT_VALIDATEURS` dans `src/config.ts` est un placeholder. Le skeleton charge dynamiquement les validateurs depuis les contrats reçus via channel messages (type `contract`). Si aucun contrat n'est reçu, les validateurs par défaut sont utilisés.
|
||||
- **Contrat de service** : Le skeleton a un contrat de service réel défini dans `src/serviceContract.ts` avec UUID `skeleton-service-uuid-4nkweb-2026`. Le contrat est chargé automatiquement au démarrage et envoyé à l'iframe UserWallet.
|
||||
- **Clé publique du service** : Configurez `VITE_SKELETON_SERVICE_PUBLIC_KEY` avec une clé publique secp256k1 compressée valide (66 hex chars, commençant par 02 ou 03). Exemple : `VITE_SKELETON_SERVICE_PUBLIC_KEY=02abc123... npm run dev`. Si non configurée, un placeholder est utilisé mais les signatures ne pourront pas être vérifiées.
|
||||
- **Validateurs** : Les validateurs sont extraits automatiquement du contrat de service skeleton. Le skeleton peut aussi recevoir un contrat personnalisé via `postMessage` (type `contract`) qui remplacera le contrat par défaut.
|
||||
|
||||
### Chargement dynamique des contrats
|
||||
|
||||
@ -54,7 +56,7 @@ Le skeleton :
|
||||
3. Met à jour les `allowedPubkeys` utilisés pour la vérification
|
||||
4. Affiche un statut de confirmation
|
||||
|
||||
Si aucun contrat n'est reçu, les `DEFAULT_VALIDATEURS` sont utilisés comme fallback.
|
||||
Le skeleton utilise automatiquement le contrat de service skeleton au démarrage. Si un contrat personnalisé est reçu via `postMessage`, il remplace le contrat par défaut.
|
||||
|
||||
## Utilisation
|
||||
|
||||
@ -165,7 +167,8 @@ Les raisons de refus possibles :
|
||||
|
||||
- `index.html` : page avec iframe, zone de statut, bouton auth.
|
||||
- `src/main.ts` : chargement de l'iframe, écoute `message`, envoi `auth-request`, appel à `verifyLoginProof`, mise à jour du statut, gestion des messages `contract`.
|
||||
- `src/config.ts` : `USERWALLET_ORIGIN`, `DEFAULT_VALIDATEURS`, types `Contrat` et `Action`.
|
||||
- `src/config.ts` : `USERWALLET_ORIGIN`, `DEFAULT_VALIDATEURS` (extraits du contrat skeleton), types `Contrat` et `Action`.
|
||||
- `src/serviceContract.ts` : contrat de service skeleton avec UUID dédié, action login, et configuration de la clé publique via `VITE_SKELETON_SERVICE_PUBLIC_KEY`.
|
||||
- `src/contract.ts` : extraction des validateurs depuis les contrats (`extractLoginValidators`), validation de structure (`isValidContract`, `isValidAction`).
|
||||
|
||||
## Déploiement
|
||||
|
||||
83
website-skeleton/generate-service-wallet.mjs
Executable file
83
website-skeleton/generate-service-wallet.mjs
Executable file
@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Generate a service wallet (secp256k1 key pair) for website-skeleton.
|
||||
* Creates .env file with VITE_SKELETON_SERVICE_PUBLIC_KEY and .env.private with private key.
|
||||
*/
|
||||
|
||||
import { getPublicKey, utils as secpUtils } from '@noble/secp256k1';
|
||||
import { bytesToHex } from '@noble/hashes/utils';
|
||||
import { writeFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { webcrypto } from 'crypto';
|
||||
|
||||
// Set up crypto for @noble/secp256k1 in Node.js
|
||||
if (typeof globalThis.crypto === 'undefined') {
|
||||
globalThis.crypto = webcrypto;
|
||||
}
|
||||
|
||||
// Generate key pair
|
||||
const privateKeyBytes = secpUtils.randomSecretKey();
|
||||
const publicKeyBytes = getPublicKey(privateKeyBytes, true); // compressed
|
||||
const privateKey = bytesToHex(privateKeyBytes);
|
||||
const publicKey = bytesToHex(publicKeyBytes);
|
||||
|
||||
// Verify format
|
||||
if (publicKey.length !== 66 || (!publicKey.startsWith('02') && !publicKey.startsWith('03'))) {
|
||||
throw new Error(`Invalid public key format: ${publicKey}`);
|
||||
}
|
||||
if (privateKey.length !== 64) {
|
||||
throw new Error(`Invalid private key format: ${privateKey}`);
|
||||
}
|
||||
|
||||
// Paths
|
||||
const envPath = join(process.cwd(), '.env');
|
||||
const envPrivatePath = join(process.cwd(), '.env.private');
|
||||
|
||||
// Check if .env already exists
|
||||
if (existsSync(envPath)) {
|
||||
console.warn('⚠️ .env already exists. Backing up to .env.backup');
|
||||
const { readFileSync } = await import('fs');
|
||||
const backup = readFileSync(envPath, 'utf-8');
|
||||
writeFileSync(join(process.cwd(), '.env.backup'), backup);
|
||||
}
|
||||
|
||||
// Write .env with public key
|
||||
const envContent = `# Service wallet public key for website-skeleton
|
||||
# Generated on ${new Date().toISOString()}
|
||||
# Service UUID: skeleton-service-uuid-4nkweb-2026
|
||||
VITE_SKELETON_SERVICE_PUBLIC_KEY=${publicKey}
|
||||
`;
|
||||
|
||||
writeFileSync(envPath, envContent, { mode: 0o600 });
|
||||
|
||||
// Write .env.private with private key (more restrictive permissions)
|
||||
const envPrivateContent = `# Service wallet private key for website-skeleton
|
||||
# ⚠️ SECRET: Keep this file secure and never commit it to version control
|
||||
# Generated on ${new Date().toISOString()}
|
||||
# Service UUID: skeleton-service-uuid-4nkweb-2026
|
||||
#
|
||||
# This private key is used to sign service operations.
|
||||
# Store it securely and never share it.
|
||||
SKELETON_SERVICE_PRIVATE_KEY=${privateKey}
|
||||
`;
|
||||
|
||||
writeFileSync(envPrivatePath, envPrivateContent, { mode: 0o400 });
|
||||
|
||||
console.log('✅ Service wallet generated successfully!');
|
||||
console.log('');
|
||||
console.log('📁 Files created:');
|
||||
console.log(` - .env (public key, mode 600)`);
|
||||
console.log(` - .env.private (private key, mode 400)`);
|
||||
console.log('');
|
||||
console.log('🔑 Public key (for VITE_SKELETON_SERVICE_PUBLIC_KEY):');
|
||||
console.log(` ${publicKey}`);
|
||||
console.log('');
|
||||
console.log('🔐 Private key (stored in .env.private):');
|
||||
console.log(` ${privateKey}`);
|
||||
console.log('');
|
||||
console.log('⚠️ Security notes:');
|
||||
console.log(' - .env.private contains the private key - keep it secure');
|
||||
console.log(' - Add .env.private to .gitignore if not already present');
|
||||
console.log(' - The public key in .env is safe to commit');
|
||||
console.log(' - Never share the private key');
|
||||
@ -5,44 +5,158 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Website skeleton – UserWallet iframe</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: system-ui, sans-serif;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h1 { font-size: 1.25rem; }
|
||||
#status {
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 1rem 0;
|
||||
border-radius: 4px;
|
||||
background: #eee;
|
||||
border-radius: 6px;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
#status.accepted {
|
||||
background: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
#status.rejected {
|
||||
background: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
#status.pending {
|
||||
background: #fff3cd;
|
||||
border-color: #ffeaa7;
|
||||
color: #856404;
|
||||
}
|
||||
.button-group {
|
||||
margin: 1rem 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
button {
|
||||
padding: 0.625rem 1.25rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-height: 44px;
|
||||
}
|
||||
button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #999;
|
||||
}
|
||||
button:active {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
button.primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
button.primary:hover {
|
||||
background: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
button.danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
button.danger:hover {
|
||||
background: #c82333;
|
||||
border-color: #c82333;
|
||||
}
|
||||
#status.accepted { background: #cfc; }
|
||||
#status.rejected { background: #fcc; }
|
||||
#iframe-container {
|
||||
margin: 1rem 0;
|
||||
min-height: 400px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
#iframe-container iframe {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
height: 600px;
|
||||
border: 0;
|
||||
display: block;
|
||||
}
|
||||
#connected-section {
|
||||
padding: 1.5rem;
|
||||
background: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
#user-info {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
#iframe-container iframe {
|
||||
height: 500px;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
#iframe-container iframe {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
button { margin-right: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Website skeleton – intégration iframe UserWallet</h1>
|
||||
<p id="status">En attente du login depuis l’iframe.</p>
|
||||
<div>
|
||||
<p id="status">En attente du login depuis l'iframe.</p>
|
||||
|
||||
<div id="login-section">
|
||||
<div class="button-group">
|
||||
<button type="button" id="btn-login" class="primary">Se connecter</button>
|
||||
<button type="button" id="btn-auth">Demander auth (auth-request)</button>
|
||||
</div>
|
||||
<div id="iframe-container">
|
||||
<iframe id="userwallet" title="UserWallet"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="connected-section" style="display: none;">
|
||||
<h2>Vous êtes connecté</h2>
|
||||
<div id="user-info"></div>
|
||||
<div class="button-group">
|
||||
<button type="button" id="btn-logout" class="danger">Se déconnecter</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
25
website-skeleton/package-lock.json
generated
25
website-skeleton/package-lock.json
generated
@ -11,6 +11,8 @@
|
||||
"service-login-verify": "file:../service-login-verify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@noble/secp256k1": "^3.0.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
@ -416,6 +418,29 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/secp256k1": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.0.0.tgz",
|
||||
"integrity": "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.57.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz",
|
||||
|
||||
@ -7,12 +7,15 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "tsc --noEmit"
|
||||
"type-check": "tsc --noEmit",
|
||||
"generate-wallet": "node generate-service-wallet.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"service-login-verify": "file:../service-login-verify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@noble/secp256k1": "^3.0.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
|
||||
@ -9,20 +9,29 @@ const origin = env?.VITE_USERWALLET_ORIGIN ?? (env?.DEV ? 'http://localhost:3018
|
||||
/** UserWallet iframe URL and postMessage target origin. */
|
||||
export const USERWALLET_ORIGIN = origin;
|
||||
|
||||
/** Default validators (placeholder). Replace with your contract action login validators. */
|
||||
export const DEFAULT_VALIDATEURS = {
|
||||
membres_du_role: [
|
||||
{
|
||||
membre_uuid: 'placeholder',
|
||||
signatures_obligatoires: [
|
||||
{
|
||||
membre_uuid: 'placeholder',
|
||||
cle_publique: '02' + '0'.repeat(64),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
import { getSkeletonServiceContractData } from './serviceContract.js';
|
||||
import type { Validateurs } from 'service-login-verify';
|
||||
|
||||
/**
|
||||
* Get default validators from skeleton service contract.
|
||||
* Uses the skeleton service contract login action validators.
|
||||
*/
|
||||
function getDefaultValidateurs(): Validateurs {
|
||||
const contractData = getSkeletonServiceContractData();
|
||||
const loginAction = contractData.actions.find((a) =>
|
||||
a.datajson?.types_names_chiffres?.includes('login')
|
||||
);
|
||||
|
||||
if (loginAction?.validateurs_action !== undefined) {
|
||||
return loginAction.validateurs_action;
|
||||
}
|
||||
|
||||
// Fallback to contract validators if no action found
|
||||
return contractData.contrat.validateurs;
|
||||
}
|
||||
|
||||
/** Default validators from skeleton service contract. */
|
||||
export const DEFAULT_VALIDATEURS = getDefaultValidateurs();
|
||||
|
||||
/**
|
||||
* Contract structure (matches userwallet types).
|
||||
|
||||
@ -20,7 +20,7 @@ export function extractLoginValidators(
|
||||
return loginAction.validateurs_action;
|
||||
}
|
||||
|
||||
// If no action provided, return contract validators as fallback
|
||||
// If no action provided, return contract validators
|
||||
// (assuming contract validators are for login action)
|
||||
if (contrat.validateurs !== undefined) {
|
||||
return contrat.validateurs;
|
||||
|
||||
@ -10,19 +10,125 @@ import {
|
||||
isValidContract,
|
||||
isValidAction,
|
||||
} from './contract.js';
|
||||
import { getSkeletonServiceContractData } from './serviceContract.js';
|
||||
import type { Contrat, Action } from './config.js';
|
||||
|
||||
const iframe = document.getElementById('userwallet') as HTMLIFrameElement;
|
||||
const statusEl = document.getElementById('status') as HTMLParagraphElement;
|
||||
const btnAuth = document.getElementById('btn-auth') as HTMLButtonElement;
|
||||
const btnLogin = document.getElementById('btn-login') as HTMLButtonElement;
|
||||
const btnLogout = document.getElementById('btn-logout') as HTMLButtonElement;
|
||||
const iframeContainer = document.getElementById('iframe-container') as HTMLDivElement;
|
||||
const userInfo = document.getElementById('user-info') as HTMLDivElement;
|
||||
const loginSection = document.getElementById('login-section') as HTMLDivElement;
|
||||
const connectedSection = document.getElementById('connected-section') as HTMLDivElement;
|
||||
|
||||
const nonceCache = new NonceCache(3600000);
|
||||
let currentValidateurs: Validateurs = DEFAULT_VALIDATEURS as Validateurs;
|
||||
let allowedPubkeys = buildAllowedPubkeysFromValidateurs(currentValidateurs);
|
||||
|
||||
// Store received contract to send to iframe
|
||||
// Initialize with skeleton service contract
|
||||
const skeletonContractData = getSkeletonServiceContractData();
|
||||
let storedContract: Contrat | null = skeletonContractData.contrat;
|
||||
let storedContratsFils: Contrat[] = skeletonContractData.contrats_fils;
|
||||
let storedActions: Action[] = skeletonContractData.actions;
|
||||
|
||||
// Session management
|
||||
const SESSION_STORAGE_KEY = 'website-skeleton-session';
|
||||
|
||||
interface SessionData {
|
||||
proof: LoginProof;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
function getSession(): SessionData | null {
|
||||
const stored = sessionStorage.getItem(SESSION_STORAGE_KEY);
|
||||
if (stored === null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(stored) as SessionData;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setSession(proof: LoginProof): void {
|
||||
const session: SessionData = {
|
||||
proof,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
|
||||
}
|
||||
|
||||
function clearSession(): void {
|
||||
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
||||
}
|
||||
|
||||
function isLoggedIn(): boolean {
|
||||
return getSession() !== null;
|
||||
}
|
||||
|
||||
function setStatus(text: string, kind: 'pending' | 'accepted' | 'rejected'): void {
|
||||
if (statusEl !== null) {
|
||||
statusEl.textContent = text;
|
||||
statusEl.className = kind === 'accepted' ? 'accepted' : kind === 'rejected' ? 'rejected' : '';
|
||||
}
|
||||
}
|
||||
|
||||
function showLoginInterface(): void {
|
||||
if (loginSection !== null) {
|
||||
loginSection.style.display = 'block';
|
||||
}
|
||||
if (connectedSection !== null) {
|
||||
connectedSection.style.display = 'none';
|
||||
}
|
||||
if (iframeContainer !== null) {
|
||||
iframeContainer.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function showConnectedInterface(): void {
|
||||
if (loginSection !== null) {
|
||||
loginSection.style.display = 'none';
|
||||
}
|
||||
if (connectedSection !== null) {
|
||||
connectedSection.style.display = 'block';
|
||||
}
|
||||
if (iframeContainer !== null) {
|
||||
iframeContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI(): void {
|
||||
if (isLoggedIn()) {
|
||||
showConnectedInterface();
|
||||
const session = getSession();
|
||||
if (session !== null && userInfo !== null) {
|
||||
const publicKeys = session.proof.signatures.map((sig) => sig.cle_publique.substring(0, 16) + '...').join(', ');
|
||||
userInfo.textContent = `Connecté - Clés publiques: ${publicKeys}`;
|
||||
}
|
||||
} else {
|
||||
showLoginInterface();
|
||||
}
|
||||
}
|
||||
|
||||
function sendContractToIframe(): void {
|
||||
if (iframe?.contentWindow == null || storedContract === null) {
|
||||
return;
|
||||
}
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: 'contract',
|
||||
payload: {
|
||||
contrat: storedContract,
|
||||
contrats_fils: storedContratsFils,
|
||||
actions: storedActions,
|
||||
},
|
||||
},
|
||||
USERWALLET_ORIGIN,
|
||||
);
|
||||
}
|
||||
|
||||
function sendAuthRequest(): void {
|
||||
@ -37,24 +143,54 @@ function sendAuthRequest(): void {
|
||||
setStatus('Auth demandé (auth-request envoyé).', 'pending');
|
||||
}
|
||||
|
||||
function handleLogin(): void {
|
||||
if (iframe?.contentWindow == null) {
|
||||
setStatus('Iframe non prêt.', 'rejected');
|
||||
return;
|
||||
}
|
||||
// Send contract if available
|
||||
sendContractToIframe();
|
||||
// Show iframe
|
||||
if (iframeContainer !== null) {
|
||||
iframeContainer.style.display = 'block';
|
||||
}
|
||||
setStatus('Connexion en cours...', 'pending');
|
||||
}
|
||||
|
||||
function handleLogout(): void {
|
||||
clearSession();
|
||||
updateUI();
|
||||
setStatus('Déconnecté.', 'pending');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update validators from contract and rebuild allowed pubkeys.
|
||||
* Also stores contract to send to iframe.
|
||||
*/
|
||||
function updateValidatorsFromContract(
|
||||
contrat: Contrat,
|
||||
contratsFils: Contrat[] = [],
|
||||
actions: Action[] = [],
|
||||
): void {
|
||||
// Store contract for sending to iframe
|
||||
storedContract = contrat;
|
||||
storedContratsFils = contratsFils;
|
||||
storedActions = actions;
|
||||
|
||||
const validateurs = extractLoginValidators(contrat, contratsFils, actions);
|
||||
if (validateurs !== null) {
|
||||
currentValidateurs = validateurs;
|
||||
allowedPubkeys = buildAllowedPubkeysFromValidateurs(validateurs);
|
||||
setStatus('Contrat reçu et validateurs mis à jour.', 'pending');
|
||||
// Send contract to iframe if it's ready
|
||||
sendContractToIframe();
|
||||
} else {
|
||||
setStatus(
|
||||
'Contrat reçu mais validateurs login introuvables. Utilisation des validateurs par défaut.',
|
||||
'pending',
|
||||
'Contrat reçu mais validateurs login introuvables. Le login ne pourra pas être vérifié.',
|
||||
'rejected',
|
||||
);
|
||||
// Still send contract to iframe even if validators not found
|
||||
sendContractToIframe();
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +238,9 @@ function handleMessage(event: MessageEvent): void {
|
||||
timestampWindowMs: 300000,
|
||||
});
|
||||
if (result.accept) {
|
||||
setSession(proof);
|
||||
setStatus('Login accepté. Session ouverte.', 'accepted');
|
||||
updateUI();
|
||||
} else {
|
||||
setStatus(`Login refusé: ${result.reason ?? 'inconnu'}`, 'rejected');
|
||||
}
|
||||
@ -122,16 +260,48 @@ function handleMessage(event: MessageEvent): void {
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
if (iframe == null || statusEl == null) {
|
||||
if (iframe == null || statusEl === null) {
|
||||
return;
|
||||
}
|
||||
iframe.src = USERWALLET_ORIGIN;
|
||||
btnAuth?.addEventListener('click', sendAuthRequest);
|
||||
btnLogin?.addEventListener('click', handleLogin);
|
||||
btnLogout?.addEventListener('click', handleLogout);
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
// Initialize validators from skeleton service contract
|
||||
const validateurs = extractLoginValidators(
|
||||
storedContract ?? skeletonContractData.contrat,
|
||||
storedContratsFils,
|
||||
storedActions,
|
||||
);
|
||||
if (validateurs !== null) {
|
||||
currentValidateurs = validateurs;
|
||||
allowedPubkeys = buildAllowedPubkeysFromValidateurs(validateurs);
|
||||
}
|
||||
|
||||
// Send contract to iframe when it's loaded
|
||||
iframe.addEventListener('load', () => {
|
||||
sendContractToIframe();
|
||||
});
|
||||
// Check if already logged in
|
||||
if (isLoggedIn()) {
|
||||
updateUI();
|
||||
setStatus('Session active.', 'accepted');
|
||||
} else {
|
||||
updateUI();
|
||||
if (storedContract !== null) {
|
||||
setStatus(
|
||||
'En attente du login depuis l’iframe. Utilisation des validateurs par défaut jusqu’à réception d’un contrat.',
|
||||
'Contrat de service skeleton chargé. Prêt pour le login.',
|
||||
'pending',
|
||||
);
|
||||
} else {
|
||||
setStatus(
|
||||
'En attente du login. Un contrat avec validateurs login doit être reçu pour vérifier les preuves.',
|
||||
'pending',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
130
website-skeleton/src/serviceContract.ts
Normal file
130
website-skeleton/src/serviceContract.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import type { Contrat, Action } from './config.js';
|
||||
|
||||
/**
|
||||
* Service UUID for website-skeleton.
|
||||
* This is a real service contract for the skeleton service.
|
||||
*/
|
||||
export const SKELETON_SERVICE_UUID = 'skeleton-service-uuid-4nkweb-2026';
|
||||
|
||||
/**
|
||||
* Contract UUID for website-skeleton.
|
||||
*/
|
||||
export const SKELETON_CONTRACT_UUID = 'skeleton-contract-uuid-4nkweb-2026';
|
||||
|
||||
/**
|
||||
* Login action UUID for website-skeleton.
|
||||
*/
|
||||
export const SKELETON_LOGIN_ACTION_UUID = 'skeleton-login-action-uuid-4nkweb-2026';
|
||||
|
||||
/**
|
||||
* Member UUID for website-skeleton validators.
|
||||
*/
|
||||
export const SKELETON_MEMBER_UUID = 'skeleton-member-uuid-4nkweb-2026';
|
||||
|
||||
/**
|
||||
* Get skeleton service contract with current public key.
|
||||
*/
|
||||
function getSkeletonServiceContract(): Contrat {
|
||||
return {
|
||||
uuid: SKELETON_CONTRACT_UUID,
|
||||
validateurs: {
|
||||
membres_du_role: [
|
||||
{
|
||||
membre_uuid: SKELETON_MEMBER_UUID,
|
||||
signatures_obligatoires: [
|
||||
{
|
||||
membre_uuid: SKELETON_MEMBER_UUID,
|
||||
cle_publique: getSkeletonPublicKey(),
|
||||
cardinalite_minimale: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
datajson: {
|
||||
types_names_chiffres: 'contrat',
|
||||
services_uuid: [SKELETON_SERVICE_UUID],
|
||||
types_uuid: ['skeleton-contract-type-uuid'],
|
||||
label: 'Contrat de service Website Skeleton',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skeleton login action with current public key.
|
||||
*/
|
||||
function getSkeletonLoginAction(): Action {
|
||||
return {
|
||||
uuid: SKELETON_LOGIN_ACTION_UUID,
|
||||
validateurs_action: {
|
||||
membres_du_role: [
|
||||
{
|
||||
membre_uuid: SKELETON_MEMBER_UUID,
|
||||
signatures_obligatoires: [
|
||||
{
|
||||
membre_uuid: SKELETON_MEMBER_UUID,
|
||||
cle_publique: getSkeletonPublicKey(),
|
||||
cardinalite_minimale: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
datajson: {
|
||||
types_names_chiffres: 'action,login',
|
||||
services_uuid: [SKELETON_SERVICE_UUID],
|
||||
types_uuid: ['skeleton-action-type-uuid'],
|
||||
label: 'Action login Website Skeleton',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skeleton service public key from environment.
|
||||
* The public key must be a valid secp256k1 compressed public key (66 hex chars: 02/03 + 64 hex).
|
||||
*
|
||||
* To configure: set VITE_SKELETON_SERVICE_PUBLIC_KEY environment variable.
|
||||
* Example: VITE_SKELETON_SERVICE_PUBLIC_KEY=02abc123... npm run dev
|
||||
*
|
||||
* If not configured, returns a placeholder and logs a warning.
|
||||
* The placeholder will not work for real signature verification.
|
||||
*/
|
||||
function getSkeletonPublicKey(): string {
|
||||
const env = typeof import.meta !== 'undefined' ? (import.meta as { env?: { VITE_SKELETON_SERVICE_PUBLIC_KEY?: string } }).env : undefined;
|
||||
const configuredKey = env?.VITE_SKELETON_SERVICE_PUBLIC_KEY;
|
||||
|
||||
if (configuredKey !== undefined && configuredKey.length === 66 && /^[0-9a-fA-F]{66}$/.test(configuredKey)) {
|
||||
if (configuredKey.startsWith('02') || configuredKey.startsWith('03')) {
|
||||
return configuredKey;
|
||||
}
|
||||
console.error(
|
||||
'VITE_SKELETON_SERVICE_PUBLIC_KEY must start with 02 or 03 (compressed secp256k1 public key). ' +
|
||||
'Using placeholder which will not work for signature verification.'
|
||||
);
|
||||
return '02' + '0'.repeat(64);
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'VITE_SKELETON_SERVICE_PUBLIC_KEY not configured. ' +
|
||||
'Set it to a valid secp256k1 compressed public key (66 hex chars, starting with 02 or 03). ' +
|
||||
'Example: VITE_SKELETON_SERVICE_PUBLIC_KEY=02abc123... npm run dev. ' +
|
||||
'Using placeholder which will not work for signature verification.'
|
||||
);
|
||||
return '02' + '0'.repeat(64);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the complete service contract data to send to UserWallet iframe.
|
||||
* The contract is generated dynamically to use the current public key configuration.
|
||||
*/
|
||||
export function getSkeletonServiceContractData(): {
|
||||
contrat: Contrat;
|
||||
contrats_fils: Contrat[];
|
||||
actions: Action[];
|
||||
} {
|
||||
return {
|
||||
contrat: getSkeletonServiceContract(),
|
||||
contrats_fils: [],
|
||||
actions: [getSkeletonLoginAction()],
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user