Add skipIfExists parameter to anchor API endpoint
**Motivations:** - Avoid unnecessary re-anchoring of already anchored hashes - Improve performance by using database as source of truth - Return consistent information without additional RPC calls **Evolutions:** - Added optional skipIfExists parameter to /api/anchor/document endpoint - Check database for existing anchors before creating new transaction - Automatically store anchors in database after transaction creation - Return old: true/false flag and ok: true in all responses - Enrich anchors table with all necessary information for retrieval without RPC calls **Pages affectées:** - api-anchorage/src/routes/anchor.js: Added skipIfExists parameter and database check - api-anchorage/src/bitcoin-rpc.js: Store anchor in database after transaction creation - api-anchorage/README.md: Updated documentation with new parameter and response format - features/api-anchorage-skip-if-exists.md: Evolution documentation
This commit is contained in:
parent
4833fdbb53
commit
cb13ab6fbf
@ -122,22 +122,56 @@ Ancre un document sur Bitcoin Signet. La transaction est créée et envoyée au
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"documentUid": "doc-123456",
|
"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é.
|
**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
|
```json
|
||||||
{
|
{
|
||||||
|
"ok": true,
|
||||||
"txid": "56504e002d95301ebcfb4b30eaedc5d3fd9a448e121ffdce4f356b8d34169e85",
|
"txid": "56504e002d95301ebcfb4b30eaedc5d3fd9a448e121ffdce4f356b8d34169e85",
|
||||||
"status": "confirmed",
|
"status": "confirmed",
|
||||||
"confirmations": 0,
|
"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** :
|
**Erreurs** :
|
||||||
- `400 Bad Request` : Hash invalide
|
- `400 Bad Request` : Hash invalide
|
||||||
- `401 Unauthorized` : Clé API invalide ou manquante
|
- `401 Unauthorized` : Clé API invalide ou manquante
|
||||||
@ -196,6 +230,16 @@ curl -X POST http://localhost:3010/api/anchor/document \
|
|||||||
"hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890"
|
"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
|
# Vérifier un ancrage
|
||||||
curl -X POST http://localhost:3010/api/anchor/verify \
|
curl -X POST http://localhost:3010/api/anchor/verify \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
|
|||||||
@ -741,6 +741,33 @@ class BitcoinRPC {
|
|||||||
this.unlockUtxo(utxo.txid, utxo.vout);
|
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
|
// Libérer le mutex
|
||||||
releaseMutex();
|
releaseMutex();
|
||||||
|
|
||||||
|
|||||||
@ -50,10 +50,11 @@ anchorRouter.get('/locked-utxos', async (req, res) => {
|
|||||||
* Body:
|
* Body:
|
||||||
* - documentUid: string (identifiant du document)
|
* - documentUid: string (identifiant du document)
|
||||||
* - hash: string (hash SHA256 du document en hex, 64 caractères)
|
* - 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) => {
|
anchorRouter.post('/document', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { documentUid, hash } = req.body;
|
const { documentUid, hash, skipIfExists } = req.body;
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!hash) {
|
if (!hash) {
|
||||||
@ -73,13 +74,43 @@ anchorRouter.post('/document', async (req, res) => {
|
|||||||
logger.info('Anchor request received', {
|
logger.info('Anchor request received', {
|
||||||
documentUid: documentUid || 'unknown',
|
documentUid: documentUid || 'unknown',
|
||||||
hash: hash.substring(0, 16) + '...',
|
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
|
// Créer la transaction d'ancrage et l'envoyer au mempool
|
||||||
const result = await bitcoinRPC.createAnchorTransaction(hash);
|
const result = await bitcoinRPC.createAnchorTransaction(hash);
|
||||||
|
|
||||||
// Retourner le résultat immédiatement (transaction dans le mempool)
|
// Retourner le résultat immédiatement (transaction dans le mempool)
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
|
ok: true,
|
||||||
txid: result.txid,
|
txid: result.txid,
|
||||||
status: result.status,
|
status: result.status,
|
||||||
confirmations: result.confirmations,
|
confirmations: result.confirmations,
|
||||||
@ -87,6 +118,7 @@ anchorRouter.post('/document', async (req, res) => {
|
|||||||
outputs: result.outputs || [],
|
outputs: result.outputs || [],
|
||||||
fee: result.fee || null,
|
fee: result.fee || null,
|
||||||
fee_sats: result.fee_sats || null,
|
fee_sats: result.fee_sats || null,
|
||||||
|
old: false,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Anchor error', { error: error.message, stack: error.stack });
|
logger.error('Anchor error', { error: error.message, stack: error.stack });
|
||||||
|
|||||||
129
features/api-anchorage-skip-if-exists.md
Normal file
129
features/api-anchorage-skip-if-exists.md
Normal file
@ -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)
|
||||||
Loading…
x
Reference in New Issue
Block a user