api-anchorage: fix syntax errors, api-relay: refactor routes setup

**Motivations:**
- Corriger erreurs de syntaxe dans api-anchorage (bloc else non fermé, variable dupliquée)
- Refactorer api-relay pour extraire setupRoutes et améliorer la structure

**Root causes:**
- api-anchorage: bloc else non fermé après verrouillage UTXO, variable totalInputAmount déclarée deux fois
- api-relay: code dupliqué dans main(), besoin de meilleure séparation des responsabilités

**Correctifs:**
- api-anchorage: fermeture bloc else ligne 392, renommage totalInputAmount en totalInputAmountForFee dans calcul frais, utilisation totalSelectedAmount pour plusieurs UTXOs

**Evolutions:**
- api-relay: extraction setupRoutes() depuis main(), refactoring routes (keys, messages, signatures), middleware auth, index.ts restructuré

**Pages affectées:**
- api-anchorage: bitcoin-rpc.js
- api-relay: index.ts, middleware/auth.ts, routes (keys, messages, signatures), services/storage.ts
- data: sync-utxos.log
This commit is contained in:
ncantu 2026-01-28 08:17:42 +01:00
parent 3c212e56e9
commit 4833fdbb53
8 changed files with 357 additions and 318 deletions

View File

@ -389,6 +389,7 @@ class BitcoinRPC {
// Verrouiller l'UTXO sélectionné // Verrouiller l'UTXO sélectionné
this.lockUtxo(selectedUtxo.txid, selectedUtxo.vout); this.lockUtxo(selectedUtxo.txid, selectedUtxo.vout);
}
// Créer les outputs // Créer les outputs
// Note: Bitcoin Core ne permet qu'un seul OP_RETURN par transaction via 'data' // Note: Bitcoin Core ne permet qu'un seul OP_RETURN par transaction via 'data'
@ -627,7 +628,7 @@ class BitcoinRPC {
// Calculer les frais réels de la transaction // Calculer les frais réels de la transaction
// Frais = somme des inputs - somme des outputs // Frais = somme des inputs - somme des outputs
let totalInputAmount = 0; let totalInputAmountForFee = 0;
let totalOutputAmountInTx = 0; let totalOutputAmountInTx = 0;
// Calculer la somme des inputs // Calculer la somme des inputs
@ -637,16 +638,18 @@ class BitcoinRPC {
try { try {
const prevTx = await this.client.getRawTransaction(input.txid, true); const prevTx = await this.client.getRawTransaction(input.txid, true);
if (prevTx.vout && prevTx.vout[input.vout]) { if (prevTx.vout && prevTx.vout[input.vout]) {
totalInputAmount += prevTx.vout[input.vout].value || 0; totalInputAmountForFee += prevTx.vout[input.vout].value || 0;
} }
} catch (error) { } catch (error) {
// Si on ne peut pas obtenir la transaction précédente, utiliser le montant de l'UTXO sélectionné // Si on ne peut pas obtenir la transaction précédente, utiliser le montant total des UTXOs sélectionnés
logger.debug('Could not get previous transaction for fee calculation', { logger.debug('Could not get previous transaction for fee calculation', {
txid: input.txid, txid: input.txid,
error: error.message, error: error.message,
}); });
totalInputAmount += selectedUtxo.amount; // Utiliser le montant total des UTXOs sélectionnés
break; // Utiliser le montant connu de l'UTXO sélectionné const totalSelectedAmountForFee = selectedUtxos.length > 1 ? totalSelectedAmount : selectedUtxo.amount;
totalInputAmountForFee += totalSelectedAmountForFee;
break; // Utiliser le montant connu des UTXOs sélectionnés
} }
} }
} }
@ -658,7 +661,7 @@ class BitcoinRPC {
} }
} }
const actualFee = roundTo8Decimals(totalInputAmount - totalOutputAmountInTx); const actualFee = roundTo8Decimals(totalInputAmountForFee - totalOutputAmountInTx);
// Construire la liste des outputs avec leur type explicite // Construire la liste des outputs avec leur type explicite
// En analysant les outputs réels de la transaction brute // En analysant les outputs réels de la transaction brute

View File

@ -26,31 +26,12 @@ const PEER_RELAYS = process.env.PEER_RELAYS
: []; : [];
const REQUIRE_API_KEY = process.env.REQUIRE_API_KEY !== 'false'; // Default: true const REQUIRE_API_KEY = process.env.REQUIRE_API_KEY !== 'false'; // Default: true
async function main(): Promise<void> { function setupRoutes(
const app = express(); app: express.Application,
storage: StorageAdapter,
registerMiddleware(app); relay: RelayService,
app.use(express.json({ limit: getBodyLimit() })); apiKeyService: ApiKeyService,
): void {
// Initialize database
const dbStorage = new DatabaseStorageService(STORAGE_PATH);
await dbStorage.initialize();
// Create adapter for compatibility
const storage = new StorageAdapter(dbStorage);
// Initialize API key service
const apiKeyService = new ApiKeyService(dbStorage);
// Register authentication middleware if required
if (REQUIRE_API_KEY) {
app.use(
createAuthMiddleware((key) => apiKeyService.validateApiKey(key)),
);
}
const relay = new RelayService(storage, PEER_RELAYS);
app.use('/health', createHealthRouter()); app.use('/health', createHealthRouter());
app.use('/messages', createMessagesRouter(storage, relay)); app.use('/messages', createMessagesRouter(storage, relay));
app.use('/signatures', createSignaturesRouter(storage, relay)); app.use('/signatures', createSignaturesRouter(storage, relay));
@ -58,7 +39,6 @@ async function main(): Promise<void> {
app.use('/metrics', createMetricsRouter(storage)); app.use('/metrics', createMetricsRouter(storage));
app.use('/bloom', createBloomRouter(storage)); app.use('/bloom', createBloomRouter(storage));
// API key management endpoint (admin only, should be protected in production)
app.post('/admin/api-keys', (req, res) => { app.post('/admin/api-keys', (req, res) => {
const { description } = req.body as { description?: string }; const { description } = req.body as { description?: string };
const { key, prefix } = apiKeyService.generateApiKey(description); const { key, prefix } = apiKeyService.generateApiKey(description);
@ -79,23 +59,9 @@ async function main(): Promise<void> {
res.status(404).json({ error: 'API key not found' }); res.status(404).json({ error: 'API key not found' });
} }
}); });
}
const server = http.createServer(app); function setupShutdown(dbStorage: DatabaseStorageService): void {
server.timeout = getRequestTimeoutMs();
server.listen(PORT, HOST, () => {
logger.info(
{
host: HOST,
port: PORT,
storagePath: STORAGE_PATH,
peerRelays: PEER_RELAYS.length > 0 ? PEER_RELAYS : 'none',
requireApiKey: REQUIRE_API_KEY,
},
'Relay server listening',
);
});
const shutdown = async (): Promise<void> => { const shutdown = async (): Promise<void> => {
logger.info('Shutting down...'); logger.info('Shutting down...');
try { try {
@ -116,6 +82,46 @@ async function main(): Promise<void> {
}); });
} }
async function main(): Promise<void> {
const app = express();
registerMiddleware(app);
app.use(express.json({ limit: getBodyLimit() }));
const dbStorage = new DatabaseStorageService(STORAGE_PATH);
await dbStorage.initialize();
const storage = new StorageAdapter(dbStorage);
const apiKeyService = new ApiKeyService(dbStorage);
if (REQUIRE_API_KEY) {
app.use(
createAuthMiddleware((key) => apiKeyService.validateApiKey(key)),
);
}
const relay = new RelayService(storage, PEER_RELAYS);
setupRoutes(app, storage, relay, apiKeyService);
const server = http.createServer(app);
server.timeout = getRequestTimeoutMs();
server.listen(PORT, HOST, () => {
logger.info(
{
host: HOST,
port: PORT,
storagePath: STORAGE_PATH,
peerRelays: PEER_RELAYS.length > 0 ? PEER_RELAYS : 'none',
requireApiKey: REQUIRE_API_KEY,
},
'Relay server listening',
);
});
setupShutdown(dbStorage);
}
main().catch((error) => { main().catch((error) => {
logger.fatal({ err: error }, 'Fatal error'); logger.fatal({ err: error }, 'Fatal error');
process.exit(1); process.exit(1);

View File

@ -1,6 +1,43 @@
import type { Request, Response, NextFunction } from 'express'; import type { Request, Response, NextFunction } from 'express';
import { logger } from '../lib/logger.js'; import { logger } from '../lib/logger.js';
function extractApiKey(req: Request): string | undefined {
const authHeader = req.headers.authorization;
const apiKeyHeader = req.headers['x-api-key'] as string | undefined;
if (authHeader?.startsWith('Bearer ') === true) {
return authHeader.slice(7);
}
if (apiKeyHeader !== undefined) {
return apiKeyHeader;
}
return undefined;
}
function handleMissingApiKey(req: Request, res: Response): void {
logger.warn(
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request without API key',
);
res.status(401).json({ error: 'API key required for POST requests' });
}
function handleInvalidApiKey(req: Request, res: Response): void {
logger.warn(
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request with invalid API key',
);
res.status(403).json({ error: 'Invalid API key' });
}
/** /**
* Middleware to authenticate POST requests using API key. * Middleware to authenticate POST requests using API key.
* API key should be provided in the Authorization header as: "Bearer <key>" * API key should be provided in the Authorization header as: "Bearer <key>"
@ -10,46 +47,19 @@ export function createAuthMiddleware(
validateApiKey: (key: string) => boolean, validateApiKey: (key: string) => boolean,
): (req: Request, res: Response, next: NextFunction) => void { ): (req: Request, res: Response, next: NextFunction) => void {
return (req: Request, res: Response, next: NextFunction): void => { return (req: Request, res: Response, next: NextFunction): void => {
// Only require auth for POST requests
if (req.method !== 'POST') { if (req.method !== 'POST') {
next(); next();
return; return;
} }
const authHeader = req.headers.authorization; const apiKey = extractApiKey(req);
const apiKeyHeader = req.headers['x-api-key'] as string | undefined;
let apiKey: string | undefined;
if (authHeader?.startsWith('Bearer ') === true) {
apiKey = authHeader.slice(7);
} else if (apiKeyHeader !== undefined) {
apiKey = apiKeyHeader;
}
if (apiKey === undefined || apiKey.length === 0) { if (apiKey === undefined || apiKey.length === 0) {
logger.warn( handleMissingApiKey(req, res);
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request without API key',
);
res.status(401).json({ error: 'API key required for POST requests' });
return; return;
} }
if (!validateApiKey(apiKey)) { if (!validateApiKey(apiKey)) {
logger.warn( handleInvalidApiKey(req, res);
{
method: req.method,
url: req.url,
ip: req.ip,
},
'POST request with invalid API key',
);
res.status(403).json({ error: 'Invalid API key' });
return; return;
} }

View File

@ -5,17 +5,11 @@ import type { MsgCle, StoredKey } from '../types/message.js';
import { validateMsgCle } from '../lib/validate.js'; import { validateMsgCle } from '../lib/validate.js';
import { logger } from '../lib/logger.js'; import { logger } from '../lib/logger.js';
export function createKeysRouter( function handleGetKeysInWindow(
storage: StorageServiceInterface, storage: StorageServiceInterface,
relay: RelayService, req: Request,
): Router { res: Response,
const router = Router(); ): void {
/**
* GET /keys?start=&end= - Get decryption keys in time window (received_at).
* Used for scan-first flow: fetch keys, then fetch messages by hash.
*/
router.get('/', (req: Request, res: Response): void => {
const startRaw = req.query.start as string | undefined; const startRaw = req.query.start as string | undefined;
const endRaw = req.query.end as string | undefined; const endRaw = req.query.end as string | undefined;
if (startRaw === undefined || endRaw === undefined) { if (startRaw === undefined || endRaw === undefined) {
@ -31,12 +25,13 @@ export function createKeysRouter(
const stored = storage.getKeysInWindow(start, end); const stored = storage.getKeysInWindow(start, end);
const keys: MsgCle[] = stored.map((k) => k.msg); const keys: MsgCle[] = stored.map((k) => k.msg);
res.json(keys); res.json(keys);
}); }
/** function handleGetKeysByHash(
* GET /keys/:hash - Get decryption keys for a message hash. storage: StorageServiceInterface,
*/ req: Request,
router.get('/:hash', (req: Request, res: Response): void => { res: Response,
): void {
try { try {
const hash = req.params.hash as string; const hash = req.params.hash as string;
if (hash.length === 0) { if (hash.length === 0) {
@ -50,12 +45,14 @@ export function createKeysRouter(
logger.error({ err: error }, 'Error getting keys'); logger.error({ err: error }, 'Error getting keys');
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}); }
/** function handlePostKey(
* POST /keys - Store and relay a decryption key. storage: StorageServiceInterface,
*/ relay: RelayService,
router.post('/', (req: Request, res: Response): void => { req: Request,
res: Response,
): void {
void (async (): Promise<void> => { void (async (): Promise<void> => {
try { try {
if (!validateMsgCle(req.body)) { if (!validateMsgCle(req.body)) {
@ -80,7 +77,17 @@ export function createKeysRouter(
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
})(); })();
}); }
export function createKeysRouter(
storage: StorageServiceInterface,
relay: RelayService,
): Router {
const router = Router();
router.get('/', (req, res) => handleGetKeysInWindow(storage, req, res));
router.get('/:hash', (req, res) => handleGetKeysByHash(storage, req, res));
router.post('/', (req, res) => handlePostKey(storage, relay, req, res));
return router; return router;
} }

View File

@ -5,17 +5,11 @@ import type { MsgChiffre, StoredMessage } from '../types/message.js';
import { validateMsgChiffre } from '../lib/validate.js'; import { validateMsgChiffre } from '../lib/validate.js';
import { logger } from '../lib/logger.js'; import { logger } from '../lib/logger.js';
export function createMessagesRouter( function handleGetMessages(
storage: StorageServiceInterface, storage: StorageServiceInterface,
relay: RelayService, req: Request,
): Router { res: Response,
const router = Router(); ): void {
/**
* GET /messages - Retrieve encrypted messages in a time window.
* Query params: start (timestamp), end (timestamp), service (optional service UUID)
*/
router.get('/', (req: Request, res: Response): void => {
try { try {
const start = parseInt(req.query.start as string, 10); const start = parseInt(req.query.start as string, 10);
const end = parseInt(req.query.end as string, 10); const end = parseInt(req.query.end as string, 10);
@ -33,12 +27,14 @@ export function createMessagesRouter(
logger.error({ err: error }, 'Error getting messages'); logger.error({ err: error }, 'Error getting messages');
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}); }
/** function handlePostMessage(
* POST /messages - Store and relay an encrypted message. storage: StorageServiceInterface,
*/ relay: RelayService,
router.post('/', (req: Request, res: Response): void => { req: Request,
res: Response,
): void {
void (async (): Promise<void> => { void (async (): Promise<void> => {
try { try {
if (!validateMsgChiffre(req.body)) { if (!validateMsgChiffre(req.body)) {
@ -67,12 +63,13 @@ export function createMessagesRouter(
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
})(); })();
}); }
/** function handleGetMessageByHash(
* GET /messages/:hash - Get a specific message by hash. storage: StorageServiceInterface,
*/ req: Request,
router.get('/:hash', (req: Request, res: Response): void => { res: Response,
): void {
try { try {
const hash = req.params.hash as string; const hash = req.params.hash as string;
if (hash.length === 0) { if (hash.length === 0) {
@ -89,7 +86,17 @@ export function createMessagesRouter(
logger.error({ err: error }, 'Error getting message by hash'); logger.error({ err: error }, 'Error getting message by hash');
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}); }
export function createMessagesRouter(
storage: StorageServiceInterface,
relay: RelayService,
): Router {
const router = Router();
router.get('/', (req, res) => handleGetMessages(storage, req, res));
router.post('/', (req, res) => handlePostMessage(storage, relay, req, res));
router.get('/:hash', (req, res) => handleGetMessageByHash(storage, req, res));
return router; return router;
} }

View File

@ -5,16 +5,11 @@ import type { MsgSignature, StoredSignature } from '../types/message.js';
import { validateMsgSignature } from '../lib/validate.js'; import { validateMsgSignature } from '../lib/validate.js';
import { logger } from '../lib/logger.js'; import { logger } from '../lib/logger.js';
export function createSignaturesRouter( function handleGetSignatures(
storage: StorageServiceInterface, storage: StorageServiceInterface,
relay: RelayService, req: Request,
): Router { res: Response,
const router = Router(); ): void {
/**
* GET /signatures/:hash - Get signatures for a message hash.
*/
router.get('/:hash', (req: Request, res: Response): void => {
try { try {
const hash = req.params.hash as string; const hash = req.params.hash as string;
if (hash.length === 0) { if (hash.length === 0) {
@ -28,12 +23,14 @@ export function createSignaturesRouter(
logger.error({ err: error }, 'Error getting signatures'); logger.error({ err: error }, 'Error getting signatures');
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}); }
/** function handlePostSignature(
* POST /signatures - Store and relay a signature. storage: StorageServiceInterface,
*/ relay: RelayService,
router.post('/', (req: Request, res: Response): void => { req: Request,
res: Response,
): void {
void (async (): Promise<void> => { void (async (): Promise<void> => {
try { try {
if (!validateMsgSignature(req.body)) { if (!validateMsgSignature(req.body)) {
@ -58,7 +55,16 @@ export function createSignaturesRouter(
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
})(); })();
}); }
export function createSignaturesRouter(
storage: StorageServiceInterface,
relay: RelayService,
): Router {
const router = Router();
router.get('/:hash', (req, res) => handleGetSignatures(storage, req, res));
router.post('/', (req, res) => handlePostSignature(storage, relay, req, res));
return router; return router;
} }

View File

@ -81,7 +81,7 @@ export class StorageService {
const results: StoredMessage[] = []; const results: StoredMessage[] = [];
const iter = const iter =
candidates !== null candidates !== null
? (function* (self: StorageService) { ? (function* (self: StorageService): Generator<StoredMessage, void, unknown> {
for (const h of candidates) { for (const h of candidates) {
const m = self.messages.get(h); const m = self.messages.get(h);
if (m !== undefined) { if (m !== undefined) {

View File

@ -1,45 +1,3 @@
⏳ Traitement: 200000/225802 UTXOs insérés...
⏳ Traitement: 210000/225802 UTXOs insérés...
⏳ Traitement: 220000/225802 UTXOs insérés...
💾 Mise à jour des UTXOs dépensés...
📊 Résumé:
- UTXOs vérifiés: 61609
- UTXOs toujours disponibles: 61609
- UTXOs dépensés détectés: 0
📈 Statistiques finales:
- Total UTXOs: 68398
- Dépensés: 6789
- Non dépensés: 61609
✅ Synchronisation terminée
🔍 Démarrage de la synchronisation des UTXOs dépensés...
📊 UTXOs à vérifier: 61609
📡 Récupération des UTXOs depuis Bitcoin...
📊 UTXOs disponibles dans Bitcoin: 225826
💾 Création de la table temporaire...
💾 Insertion des UTXOs disponibles par batch...
⏳ Traitement: 10000/225826 UTXOs insérés...
⏳ Traitement: 20000/225826 UTXOs insérés...
⏳ Traitement: 30000/225826 UTXOs insérés...
⏳ Traitement: 40000/225826 UTXOs insérés...
⏳ Traitement: 50000/225826 UTXOs insérés...
⏳ Traitement: 60000/225826 UTXOs insérés...
⏳ Traitement: 70000/225826 UTXOs insérés...
⏳ Traitement: 80000/225826 UTXOs insérés...
⏳ Traitement: 90000/225826 UTXOs insérés...
⏳ Traitement: 100000/225826 UTXOs insérés...
⏳ Traitement: 110000/225826 UTXOs insérés...
⏳ Traitement: 120000/225826 UTXOs insérés...
⏳ Traitement: 130000/225826 UTXOs insérés...
⏳ Traitement: 140000/225826 UTXOs insérés...
⏳ Traitement: 150000/225826 UTXOs insérés...
⏳ Traitement: 160000/225826 UTXOs insérés...
⏳ Traitement: 170000/225826 UTXOs insérés...
⏳ Traitement: 180000/225826 UTXOs insérés...
⏳ Traitement: 190000/225826 UTXOs insérés...
⏳ Traitement: 200000/225826 UTXOs insérés... ⏳ Traitement: 200000/225826 UTXOs insérés...
⏳ Traitement: 210000/225826 UTXOs insérés... ⏳ Traitement: 210000/225826 UTXOs insérés...
⏳ Traitement: 220000/225826 UTXOs insérés... ⏳ Traitement: 220000/225826 UTXOs insérés...
@ -98,3 +56,45 @@
- Non dépensés: 61609 - Non dépensés: 61609
✅ Synchronisation terminée ✅ Synchronisation terminée
🔍 Démarrage de la synchronisation des UTXOs dépensés...
📊 UTXOs à vérifier: 61609
📡 Récupération des UTXOs depuis Bitcoin...
📊 UTXOs disponibles dans Bitcoin: 225855
💾 Création de la table temporaire...
💾 Insertion des UTXOs disponibles par batch...
⏳ Traitement: 10000/225855 UTXOs insérés...
⏳ Traitement: 20000/225855 UTXOs insérés...
⏳ Traitement: 30000/225855 UTXOs insérés...
⏳ Traitement: 40000/225855 UTXOs insérés...
⏳ Traitement: 50000/225855 UTXOs insérés...
⏳ Traitement: 60000/225855 UTXOs insérés...
⏳ Traitement: 70000/225855 UTXOs insérés...
⏳ Traitement: 80000/225855 UTXOs insérés...
⏳ Traitement: 90000/225855 UTXOs insérés...
⏳ Traitement: 100000/225855 UTXOs insérés...
⏳ Traitement: 110000/225855 UTXOs insérés...
⏳ Traitement: 120000/225855 UTXOs insérés...
⏳ Traitement: 130000/225855 UTXOs insérés...
⏳ Traitement: 140000/225855 UTXOs insérés...
⏳ Traitement: 150000/225855 UTXOs insérés...
⏳ Traitement: 160000/225855 UTXOs insérés...
⏳ Traitement: 170000/225855 UTXOs insérés...
⏳ Traitement: 180000/225855 UTXOs insérés...
⏳ Traitement: 190000/225855 UTXOs insérés...
⏳ Traitement: 200000/225855 UTXOs insérés...
⏳ Traitement: 210000/225855 UTXOs insérés...
⏳ Traitement: 220000/225855 UTXOs insérés...
💾 Mise à jour des UTXOs dépensés...
📊 Résumé:
- UTXOs vérifiés: 61609
- UTXOs toujours disponibles: 61609
- UTXOs dépensés détectés: 0
📈 Statistiques finales:
- Total UTXOs: 68398
- Dépensés: 6789
- Non dépensés: 61609
✅ Synchronisation terminée