diff --git a/api-anchorage/README.md b/api-anchorage/README.md index 39dd6a4..65757a1 100644 --- a/api-anchorage/README.md +++ b/api-anchorage/README.md @@ -122,22 +122,56 @@ Ancre un document sur Bitcoin Signet. La transaction est créée et envoyée au ```json { "documentUid": "doc-123456", - "hash": "a1b2c3d4e5f6..." + "hash": "a1b2c3d4e5f6...", + "skipIfExists": false } ``` +**Paramètres** : +- `documentUid` : string (optionnel, identifiant du document) +- `hash` : string (requis, hash SHA256 du document en hex, 64 caractères) +- `skipIfExists` : boolean (optionnel, par défaut `false`, si `true`, ne réancrera pas un hash déjà ancré et retournera les informations existantes) + **Note** : La transaction est envoyée au mempool et retournée immédiatement. Aucun callback n'est supporté. -**Réponse** : +**Réponse** (nouvel ancrage) : ```json { + "ok": true, "txid": "56504e002d95301ebcfb4b30eaedc5d3fd9a448e121ffdce4f356b8d34169e85", "status": "confirmed", "confirmations": 0, - "block_height": 152321 + "block_height": 152321, + "outputs": [...], + "fee": 0.00001, + "fee_sats": 1000, + "old": false } ``` +**Réponse** (hash déjà ancré avec `skipIfExists: true`) : +```json +{ + "ok": true, + "txid": "56504e002d95301ebcfb4b30eaedc5d3fd9a448e121ffdce4f356b8d34169e85", + "status": "confirmed", + "confirmations": 5, + "block_height": 152316, + "old": true +} +``` + +**Champs de réponse** : +- `ok` : boolean, toujours `true` en cas de succès +- `txid` : string, ID de la transaction Bitcoin +- `status` : string, statut de la transaction (`"confirmed"` ou `"pending"`) +- `confirmations` : number, nombre de confirmations +- `block_height` : number|null, hauteur du bloc (null si pas encore dans un bloc) +- `outputs` : array, liste des outputs de la transaction (uniquement pour nouveaux ancrages) +- `fee` : number|null, frais de transaction en BTC (uniquement pour nouveaux ancrages) +- `fee_sats` : number|null, frais de transaction en sats (uniquement pour nouveaux ancrages) +- `old` : boolean, `true` si le hash était déjà ancré, `false` si c'est un nouvel ancrage + **Erreurs** : - `400 Bad Request` : Hash invalide - `401 Unauthorized` : Clé API invalide ou manquante @@ -196,6 +230,16 @@ curl -X POST http://localhost:3010/api/anchor/document \ "hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890" }' +# Ancrer un document sans réancrer si déjà ancré +curl -X POST http://localhost:3010/api/anchor/document \ + -H "Content-Type: application/json" \ + -H "x-api-key: your-api-key-here" \ + -d '{ + "documentUid": "doc-123", + "hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890", + "skipIfExists": true + }' + # Vérifier un ancrage curl -X POST http://localhost:3010/api/anchor/verify \ -H "Content-Type: application/json" \ diff --git a/api-anchorage/src/bitcoin-rpc.js b/api-anchorage/src/bitcoin-rpc.js index 3dcf5e4..0d834aa 100644 --- a/api-anchorage/src/bitcoin-rpc.js +++ b/api-anchorage/src/bitcoin-rpc.js @@ -741,6 +741,33 @@ class BitcoinRPC { this.unlockUtxo(utxo.txid, utxo.vout); } + // Stocker l'ancre dans la base de données + try { + const dbForAnchor = getDatabase(); + const date = new Date().toISOString(); + dbForAnchor.prepare(` + INSERT OR REPLACE INTO anchors (hash, txid, block_height, confirmations, date, updated_at) + VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `).run( + hash, + txid, + txInfo.blockheight || null, + txInfo.confirmations || 0, + date + ); + logger.debug('Anchor stored in database', { + hash: hash.substring(0, 16) + '...', + txid: txid.substring(0, 16) + '...', + }); + } catch (error) { + logger.warn('Error storing anchor in database', { + error: error.message, + hash: hash.substring(0, 16) + '...', + txid: txid.substring(0, 16) + '...', + }); + // Ne pas faire échouer la transaction si le stockage en base échoue + } + // Libérer le mutex releaseMutex(); diff --git a/api-anchorage/src/routes/anchor.js b/api-anchorage/src/routes/anchor.js index 58365c1..6601da2 100644 --- a/api-anchorage/src/routes/anchor.js +++ b/api-anchorage/src/routes/anchor.js @@ -50,10 +50,11 @@ anchorRouter.get('/locked-utxos', async (req, res) => { * Body: * - documentUid: string (identifiant du document) * - hash: string (hash SHA256 du document en hex, 64 caractères) + * - skipIfExists: boolean (optionnel, si true, ne réancrera pas un hash déjà ancré) */ anchorRouter.post('/document', async (req, res) => { try { - const { documentUid, hash } = req.body; + const { documentUid, hash, skipIfExists } = req.body; // Validation if (!hash) { @@ -73,13 +74,43 @@ anchorRouter.post('/document', async (req, res) => { logger.info('Anchor request received', { documentUid: documentUid || 'unknown', hash: hash.substring(0, 16) + '...', + skipIfExists: skipIfExists || false, }); + // Si skipIfExists est activé, vérifier en base si le hash existe déjà + if (skipIfExists) { + const { getDatabase } = await import('../database.js'); + const db = getDatabase(); + const existingAnchor = db.prepare(` + SELECT txid, block_height, confirmations, date + FROM anchors + WHERE hash = ? + `).get(hash); + + if (existingAnchor) { + logger.info('Hash already anchored, returning existing anchor', { + hash: hash.substring(0, 16) + '...', + txid: existingAnchor.txid.substring(0, 16) + '...', + }); + + // Retourner le résultat avec old: true + return res.status(200).json({ + ok: true, + txid: existingAnchor.txid, + status: existingAnchor.confirmations > 0 ? 'confirmed' : 'pending', + confirmations: existingAnchor.confirmations || 0, + block_height: existingAnchor.block_height || null, + old: true, + }); + } + } + // Créer la transaction d'ancrage et l'envoyer au mempool const result = await bitcoinRPC.createAnchorTransaction(hash); // Retourner le résultat immédiatement (transaction dans le mempool) res.status(200).json({ + ok: true, txid: result.txid, status: result.status, confirmations: result.confirmations, @@ -87,6 +118,7 @@ anchorRouter.post('/document', async (req, res) => { outputs: result.outputs || [], fee: result.fee || null, fee_sats: result.fee_sats || null, + old: false, }); } catch (error) { logger.error('Anchor error', { error: error.message, stack: error.stack }); diff --git a/features/api-anchorage-skip-if-exists.md b/features/api-anchorage-skip-if-exists.md new file mode 100644 index 0000000..da48093 --- /dev/null +++ b/features/api-anchorage-skip-if-exists.md @@ -0,0 +1,129 @@ +# API d'ancrage - Paramètre skipIfExists + +**Date:** 2026-01-28 +**Auteur:** Équipe 4NK + +## Objectif + +Ajouter un paramètre optionnel `skipIfExists` à l'endpoint `/api/anchor/document` pour éviter de réancrer des hash déjà ancrés. L'API doit renvoyer le résultat avec un tag `old: true/false` et les informations de la transaction depuis la base de données sans appel RPC supplémentaire. + +## Motivations + +- **Éviter les réancrages inutiles** : Si un hash est déjà ancré, ne pas créer une nouvelle transaction +- **Performance** : Utiliser la base de données comme source de vérité pour éviter les appels RPC coûteux +- **Cohérence** : Retourner toujours les mêmes informations (txid, confirmations, block_height) qu'un ancrage normal + +## Impacts + +### Fonctionnels + +- L'endpoint `/api/anchor/document` accepte maintenant un paramètre optionnel `skipIfExists` +- Si `skipIfExists: true` et que le hash existe déjà en base, retourne immédiatement les informations sans créer de transaction +- Toutes les réponses incluent maintenant `ok: true` et `old: true/false` +- Les ancres sont automatiquement stockées dans la table `anchors` après création + +### Techniques + +- Enrichissement de la table `anchors` : Les ancres sont maintenant stockées automatiquement lors de la création +- Pas d'appel RPC supplémentaire : Les informations sont récupérées directement depuis la base de données +- Compatibilité : Le paramètre est optionnel, le comportement par défaut reste inchangé + +## Modifications + +### Fichiers modifiés + +1. **`api-anchorage/src/routes/anchor.js`** + - Ajout du paramètre `skipIfExists` dans le body de la requête + - Vérification en base de données si le hash existe déjà + - Retour des informations existantes avec `old: true` si le hash est trouvé + - Ajout de `ok: true` et `old: false` dans la réponse pour les nouveaux ancrages + +2. **`api-anchorage/src/bitcoin-rpc.js`** + - Stockage automatique de l'ancre dans la table `anchors` après création de la transaction + - Utilisation de `INSERT OR REPLACE` pour gérer les cas de mise à jour + +3. **`api-anchorage/README.md`** + - Documentation du nouveau paramètre `skipIfExists` + - Documentation des nouveaux champs de réponse (`ok`, `old`) + - Exemples d'utilisation avec `skipIfExists: true` + +### Structure de la réponse + +**Nouvel ancrage** : +```json +{ + "ok": true, + "txid": "...", + "status": "confirmed", + "confirmations": 0, + "block_height": 152321, + "outputs": [...], + "fee": 0.00001, + "fee_sats": 1000, + "old": false +} +``` + +**Hash déjà ancré (avec skipIfExists: true)** : +```json +{ + "ok": true, + "txid": "...", + "status": "confirmed", + "confirmations": 5, + "block_height": 152316, + "old": true +} +``` + +## Modalités de déploiement + +1. **Vérifier la base de données** : S'assurer que la table `anchors` existe (créée par `init-db.mjs`) +2. **Déployer le code** : Les modifications sont rétrocompatibles, aucun changement de schéma requis +3. **Tester** : Vérifier que les ancres existantes sont bien retournées avec `old: true` +4. **Monitoring** : Surveiller les logs pour vérifier que les ancres sont bien stockées en base + +## Modalités d'analyse + +### Vérification du fonctionnement + +1. **Test avec hash nouveau** : + ```bash + curl -X POST http://localhost:3010/api/anchor/document \ + -H "Content-Type: application/json" \ + -H "x-api-key: your-api-key" \ + -d '{"hash": "nouveau_hash_64_caracteres_hex", "skipIfExists": true}' + ``` + - Doit créer une nouvelle transaction + - Doit retourner `old: false` + - Doit stocker l'ancre en base + +2. **Test avec hash existant** : + ```bash + curl -X POST http://localhost:3010/api/anchor/document \ + -H "Content-Type: application/json" \ + -H "x-api-key: your-api-key" \ + -d '{"hash": "hash_deja_ancré_64_caracteres_hex", "skipIfExists": true}' + ``` + - Ne doit pas créer de nouvelle transaction + - Doit retourner `old: true` + - Doit retourner les informations depuis la base de données + +3. **Vérification en base** : + ```sql + SELECT * FROM anchors WHERE hash = 'hash_test'; + ``` + - Doit contenir l'enregistrement avec txid, block_height, confirmations + +### Logs à surveiller + +- `Hash already anchored, returning existing anchor` : Hash trouvé en base avec skipIfExists +- `Anchor stored in database` : Ancre stockée après création +- `Error storing anchor in database` : Erreur lors du stockage (ne fait pas échouer la transaction) + +## Notes + +- Le paramètre `skipIfExists` est optionnel et par défaut à `false` pour maintenir la compatibilité +- Si `skipIfExists: false` ou non fourni, le comportement reste identique à avant (création systématique) +- Les ancres sont stockées avec `INSERT OR REPLACE` pour gérer les cas de mise à jour (confirmations, block_height) +- Le stockage en base ne fait pas échouer la transaction si une erreur survient (log warning uniquement)