Ajouts du 28 août 2026 — guide d'intégration client
Julien Béranger
+ Claude Opus 5
Récapitulatif des trois dernières PR mergées, destiné à l'équipe qui intègre l'API côté client (UI registrar).
| PR | Titre | Apport |
|---|---|---|
| #37 | Add IpfsModule | Nouveau endpoint POST /ipfs/cid : calcul d'un CID IPFS sans upload ni pinning |
| #39 | Add images upload and processing | Upload du fichier source et de l'image d'aperçu dans POST /nft/create et POST /nft/register + endpoint public GET /nft/preview/:sourceFileHash |
| #41 | Make NFT creation optional | Champ nftCreation : enregistrer une œuvre sans frapper de NFT, et frapper plus tard |
Rien n'est cassé. Tous les nouveaux champs de requête sont optionnels (à une exception près, décrite au §3). Un client existant qui n'envoie aucun de ces champs continue de fonctionner exactement comme avant.
Sommaire
- Rappels d'authentification
- Upload de fichiers (PR #39)
- Création de NFT optionnelle (PR #41)
- Calcul de CID IPFS (PR #37)
- Configuration serveur
- Erreurs
- Checklist d'intégration
1. Rappels d'authentification
Tous les endpoints ci-dessous exigent une session valide avec rôle Admin ou
Registrar, à une exception près : GET /nft/preview/:sourceFileHash est
public (voir §2.4).
Obtenir un token :
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"registrar@example.com","password":"..."}'{
"session_token": "abc...",
"user": { "...": "..." }
}Puis, au choix, sur chaque appel :
Authorization: Bearer <session_token>ou
x-session-token: <session_token>2. Upload de fichiers (PR #39)
Un registrar manipule deux fichiers par œuvre. Ils sont désormais envoyés
inline dans le corps JSON, encodés en base64 — pas de multipart/form-data.
| Fichier | Obligatoire | Visibilité | Rôle |
|---|---|---|---|
sourceFile | Non* | Privé | L'œuvre originale. Ses octets sont stockés en base et leur SHA‑384 est le sourceFileHash. Aucun endpoint ne le sert. |
previewImage | Non | Public | Image d'aperçu, servie sans authentification sur GET /nft/preview/:sourceFileHash et référencée comme image dans les métadonnées du NFT. |
* Optionnel pour la rétro-compatibilité — sauf en mode nftCreation: false, où
il devient obligatoire (§3).
2.1 Format d'un fichier
| Champ | Type | Description |
|---|---|---|
filename | string | Nom d'origine, stocké avec les octets. Max 255 caractères. |
mimeType | string | Type MIME. Max 100 caractères. |
content | string | Le fichier en base64. Un préfixe data:<mime>;base64, est accepté et retiré automatiquement — le résultat brut d'un FileReader navigateur peut donc être passé tel quel. |
textData | string | Optionnel. Note libre stockée avec le fichier. |
Limites : 50 Mo par fichier après décodage. Le base64 coûte ~33 % de plus
que les octets bruts, d'où une limite de corps HTTP à 150 Mo
(MAX_REQUEST_BODY_SIZE).
Côté navigateur :
const toBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
// On peut envoyer le data: URI tel quel, l'API retire le préfixe.
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file);
});
const sourceFile = {
filename: file.name,
mimeType: file.type,
content: await toBase64(file),
};2.2 POST /nft/create avec fichiers
Requête
curl -X POST http://localhost:3000/nft/create \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SESSION_TOKEN" \
-d @create.json{
"blockchain": "sepolia",
"recipientAddress": "0x1234567890123456789012345678901234567890",
"name": "Artwork #1",
"description": "A beautiful piece of digital art",
"creatorName": "John Doe",
"creatorAddress": "0x1234567890123456789012345678901234567890",
"imageUrl": "https://example.com/image.png",
"thumbnail": "https://example.com/thumb.png",
"symbol": "ART",
"resaleRights": 400,
"redeemable": false,
"tangible": true,
"assetType": 0,
"status": 0,
"info": "Initial creation",
"license": "https://example.com/license",
"nftCreation": true,
"sourceFile": {
"filename": "artwork-scan.tif",
"mimeType": "image/tiff",
"content": "<base64>",
"textData": "Scan haute résolution"
},
"previewImage": {
"filename": "preview.png",
"mimeType": "image/png",
"content": "<base64>"
}
}Réponse (200)
{
"message": "NFT created, minted, and registered successfully",
"nftCreation": true,
"contractAddress": "0x...",
"tokenId": 1,
"blockchainTokenId": 0,
"transactionHash": "0x...",
"explorerLink": "https://sepolia.etherscan.io/tx/0x...",
"recipient": "0x1234567890123456789012345678901234567890",
"sourceFileHash": "abc123...",
"metadata": {
"name": "Artwork #1",
"description": "...",
"image": "https://api.example.com/nft/preview/abc123...",
"image_ipfs": "ipfs://bafkrei...",
"thumbnail": "https://example.com/thumb.png",
"source_file_hash": "abc123...",
"attributes": [
{ "trait_type": "Creator", "value": "John Doe" },
{ "trait_type": "Creator Address", "value": "0x1234..." },
{ "trait_type": "Resale Rights", "value": "4%" },
{ "trait_type": "Tangible", "value": "Yes" },
{ "trait_type": "Redeemable", "value": "No" }
],
"external_url": "https://example.com/license"
},
"files": {
"sourceFile": {
"filename": "artwork-scan.tif",
"mimeType": "image/tiff",
"byteLength": 2481920,
"sha384": "abc123...",
"cid": "bafkrei...",
"uri": "ipfs://bafkrei..."
},
"previewImage": {
"filename": "preview.png",
"mimeType": "image/png",
"byteLength": 48210,
"sha384": "def456...",
"cid": "bafkrei...",
"uri": "ipfs://bafkrei...",
"url": "https://api.example.com/nft/preview/abc123..."
}
}
}Points d'attention côté client :
urln'apparaît que surpreviewImage. Le fichier source n'a pas d'adresse publique, par conception.- Si aucun fichier n'est envoyé :
files.sourceFileetfiles.previewImagevalentnull,metadata.imageconserve l'imageUrlfourni par l'appelant, etimage_ipfs/source_file_hashsont absents des métadonnées. - Pour afficher une œuvre : utiliser
files.previewImage.url, ou le reconstruire soi-même :{PUBLIC_BASE_URL}/nft/preview/{sourceFileHash}.
2.3 POST /nft/register avec fichiers
Mêmes objets sourceFile et previewImage. Différence importante : ici c'est
l'appelant qui fournit sourceFileHash, donc si un sourceFile est joint,
ses octets doivent hasher (SHA‑384) exactement vers cette valeur, sinon la
requête est rejetée en 400.
{
"blockchain": "sepolia",
"contractAddress": "0x1234567890123456789012345678901234567890",
"tokenId": 1,
"sourceFileHash": "abc123...",
"creatorWalletPublicKey": "0x1234567890123456789012345678901234567890",
"assetType": 0,
"tangible": true,
"redeemable": false,
"status": 0,
"resaleRights": 400,
"creatorName": "John Doe",
"info": "Registered existing NFT",
"metadata": "{\"name\": \"Artwork\", \"description\": \"...\"}",
"sourceFile": {
"filename": "artwork-scan.tif",
"mimeType": "image/tiff",
"content": "<base64>"
},
"previewImage": {
"filename": "preview.png",
"mimeType": "image/png",
"content": "<base64>"
}
}Réponse (200)
{
"message": "NFT registered successfully",
"nft": { "...": "objet NFT complet" },
"asset": {
"sourceFileHash": "abc123...",
"tokenId": 1,
"certIssuerId": null,
"measurements": null,
"creationDate": null,
"distinguishingFeatures": null,
"inscriptions": null
},
"files": {
"sourceFile": { "...": "..." },
"previewImage": { "...": "...", "url": "https://api.example.com/nft/preview/abc123..." }
}
}Backfill : enregistrer un token supplémentaire pour une œuvre déjà connue met à jour les lignes existantes avec les fichiers portés par l'appel. Une œuvre d'abord enregistrée sans ses fichiers peut donc être complétée plus tard.
2.4 GET /nft/preview/:sourceFileHash (public)
Aucune authentification. C'est délibéré : cette URL est écrite dans les métadonnées du NFT, les wallets et marketplaces doivent pouvoir la récupérer anonymement.
curl http://localhost:3000/nft/preview/abc123... --output preview.png- Renvoie les octets de l'aperçu avec le
Content-Typed'origine. Content-Disposition: inline(le navigateur affiche au lieu de télécharger).Cache-Control: public, max-age=31536000, immutable— le contenu est adressé par le hash d'un fichier immuable, il est donc cachable indéfiniment.
| Statut | Cause |
|---|---|
| 200 | Les octets de l'image d'aperçu |
| 404 | Aucune œuvre pour ce hash, ou aucun aperçu uploadé pour elle |
Il n'existe aucun endpoint équivalent pour le fichier source. Il est stocké, hashé et référencé par son hash, mais jamais servi.
2.5 Le sourceFileHash
Quand un sourceFile est uploadé, sourceFileHash = SHA‑384 de ses octets.
La clé en base est donc content-addressed : deux fichiers identiques désignent la
même œuvre.
- Sur
POST /nft/createle hash est dérivé de l'upload. Sans upload, on retombe sur le hash synthétique historiquesha384(contractAddress-tokenId-timestamp), qui ne décrit aucun fichier. - Sur
POST /nft/register, le hash est fourni par l'appelant et vérifié contre le fichier s'il est joint.
Ce hash est public : il apparaît dans les métadonnées (source_file_hash) et
dans l'URL d'aperçu. C'est voulu — il permet à quiconque détient une copie de
l'œuvre de la vérifier contre l'original enregistré, ce qui est tout l'objet d'un
certificat d'authenticité. Il ne divulgue rien du contenu du fichier.
CID IPFS des fichiers
Les deux fichiers passent par le calcul de CID décrit au §4, et les cid/uri
obtenus sont renvoyés dans files. Rien n'est pinné. Les CID étant
déterministes, les enregistrer maintenant permet de pousser les octets vers un
service de pinning plus tard sans que l'identifiant change.
Les métadonnées portent donc l'aperçu deux fois :
| Champ métadonnée | Valeur | Résout aujourd'hui ? |
|---|---|---|
image | https://<PUBLIC_BASE_URL>/nft/preview/<hash> | Oui |
image_ipfs | ipfs://<cid> | Pas tant que les octets ne sont pas pinnés |
Le CID du fichier source est calculé et renvoyé dans la réponse API, mais
n'est pas écrit dans les métadonnées : publier une URI ipfs:// pour un
fichier volontairement jamais pinné serait un lien mort permanent. Les
métadonnées portent source_file_hash à la place, comme engagement d'intégrité.
3. Création de NFT optionnelle (PR #41)
Nouveau champ sur POST /nft/create :
{ "nftCreation": false }nftCreation (booléen, optionnel, défaut true) contrôle si un token est
créé. Il existe parce que toute œuvre enregistrée n'a pas vocation à devenir un
NFT au moment où elle est enregistrée.
Avec nftCreation: false
- Aucune interaction avec la chaîne : pas de contrat déployé, pas de token frappé, pas besoin d'endpoint RPC ni de wallet système approvisionné.
- Seules les lignes au niveau œuvre sont écrites :
source_file,desc_imageetasset_properties. Pas de ligneassetninft, toutes deux étant clefées par un token ID qui n'existe pas encore. sourceFiledevient obligatoire. Le chemin de mint peut retomber sur un hash synthétique construit depuis l'adresse du contrat et le token ID ; sans ni l'un ni l'autre, le SHA‑384 du fichier source est la seule chose qui identifie l'œuvre.symbol,imageUrletthumbnailne sont plus requis — ils ne servent qu'au token et à ses métadonnées.
Requête minimale
{
"name": "Artwork #1",
"description": "A beautiful piece of digital art",
"creatorName": "John Doe",
"creatorAddress": "0x1234567890123456789012345678901234567890",
"resaleRights": 400,
"redeemable": false,
"tangible": true,
"assetType": 0,
"status": 0,
"info": "Initial creation",
"nftCreation": false,
"sourceFile": {
"filename": "artwork-scan.tif",
"mimeType": "image/tiff",
"content": "<base64>"
},
"previewImage": {
"filename": "preview.png",
"mimeType": "image/png",
"content": "<base64>"
}
}Réponse (200)
{
"message": "Artwork registered successfully. No NFT was created.",
"nftCreation": false,
"sourceFileHash": "abc123...",
"asset": {
"sourceFileHash": "abc123...",
"title": "Artwork #1",
"description": "A beautiful piece of digital art",
"assetType": 0,
"tangible": true,
"redeemable": false
},
"files": {
"sourceFile": { "filename": "artwork-scan.tif", "...": "..." },
"previewImage": { "filename": "preview.png", "...": "...", "url": "..." }
}
}Distinguer les deux réponses
POST /nft/create peut désormais renvoyer deux formes différentes. Le champ
nftCreation de la réponse les discrimine, sans avoir à inspecter les autres
champs :
const res = await createNft(body);
if (res.nftCreation) {
// Mint effectué : res.contractAddress, res.tokenId, res.transactionHash,
// res.explorerLink, res.recipient, res.metadata
} else {
// Œuvre seulement : res.sourceFileHash, res.asset
// (ni contractAddress, ni tokenId, ni transactionHash)
}Frapper le NFT plus tard
Rappeler POST /nft/create avec le même sourceFile et
nftCreation: true (ou champ omis). asset_properties étant clefé sur
source_file_hash seul, l'enregistrement retrouve l'œuvre déjà consignée et y
rattache le nouveau token, au lieu de la dupliquer.
Comme la même recherche fait aussi le backfill des octets sur une œuvre connue, le fichier source et l'image d'aperçu peuvent également être fournis à ce second appel s'ils ne l'avaient pas été au premier.
À ne pas confondre avec
POST /nft/register, qui enregistre un NFT existant déjà on-chain et exige donccontractAddressettokenId.
Comparatif des trois flux
create + nftCreation: true (défaut) | create + nftCreation: false | register | |
|---|---|---|---|
| Déploie un contrat | Oui | Non | Non |
| Frappe un token | Oui | Non | Non (déjà frappé) |
| Requiert RPC + gas | Oui | Non | Oui (lecture ownerOf()) |
Requiert sourceFile | Non | Oui | Non |
Requiert symbol/imageUrl/thumbnail | Oui | Non | — |
Requiert contractAddress/tokenId | Non | Non | Oui |
| Lignes écrites | source_file, desc_image, asset_properties, asset, nft | source_file, desc_image, asset_properties | idem create |
4. Calcul de CID IPFS (PR #37)
Endpoint : POST /ipfs/cid
Authentification : requise (session + rôle Admin ou Registrar)
Calcule le content identifier IPFS (CIDv1) qu'aurait un fichier, sans rien
uploader ni pinner. Les octets sont hashés dans un DAG UnixFS en mémoire, puis
jetés — l'équivalent de
ipfs add --only-hash --cid-version=1 --raw-leaves.
IPFS étant content-addressed, le CID renvoyé ici est celui que le contenu aura une fois pinné sur n'importe quel backend : on peut donc enregistrer un CID maintenant et uploader les octets plus tard sans que l'identifiant change. Les réglages de l'importer sont alignés sur ceux du front affix-ui, donc un CID calculé par l'API et un CID calculé dans le navigateur sont directement comparables.
Corps de requête
Fournir soit url, soit content — jamais les deux.
| Champ | Type | Description |
|---|---|---|
url | string | URL du fichier à hasher. Les octets sont téléchargés, hashés, jetés. http/https uniquement, 50 Mo max. |
content | string | Contenu inline, décodé selon encoding. |
encoding | utf8 | base64 | Comment décoder content. Défaut : utf8. |
filename | string | Nom enregistré pour le fichier. N'affecte pas le CID, sert seulement à rendre les erreurs lisibles. |
Exemple — hasher un fichier distant
curl -X POST http://localhost:3000/ipfs/cid \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $SESSION_TOKEN" \
-d '{"url":"https://example.com/artwork.png"}'{
"cid": "bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4",
"uri": "ipfs://bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4",
"byteLength": 12,
"url": "https://example.com/artwork.png",
"mimeType": "image/png"
}Exemple — hasher un contenu inline
{
"content": "aGVsbG8gd29ybGQK",
"encoding": "base64",
"filename": "hello.txt"
}url et mimeType ne sont présents dans la réponse que si la requête a fourni
un url.
Erreurs
| Statut | Cause |
|---|---|
| 400 | Ni url ni content, ou les deux à la fois |
| 400 | url malformé, protocole autre que http/https, injoignable, statut non‑2xx, ou taille dépassée |
| 500 | L'importer UnixFS n'a pas pu être chargé ou a échoué |
5. Configuration serveur
Deux variables d'environnement ajoutées (.env.example) :
# URL publique à laquelle l'API est joignable depuis Internet. Les URL d'images
# d'aperçu en sont construites et écrites dans les métadonnées NFT : ce doit
# donc être une adresse qu'un wallet ou une marketplace peut atteindre.
# Défaut : http://localhost:$PORT
PUBLIC_BASE_URL=http://localhost:3000
# Taille maximale du corps HTTP. Les registrars uploadent les fichiers inline en
# base64, ce qui coûte ~33 % au-dessus des octets bruts, en plus de la limite de
# 50 Mo par fichier.
MAX_REQUEST_BODY_SIZE=150mb
PUBLIC_BASE_URLpar défaut vauthttp://localhost:$PORT: correct en local, faux en production. Les métadonnées NFT étant immuables une fois frappées, cette variable doit être correcte avant le premier mint.
Express plafonne les corps de requête à 100 ko par défaut ; MAX_REQUEST_BODY_SIZE
est appliqué au démarrage sur json() et urlencoded()
(src/main.ts).
Où atterrissent les octets
| Fichier | Table | Colonnes |
|---|---|---|
sourceFile | source_files | filename, mime_type, binary_data, source_file_hash |
previewImage | desc_images | filename, mime_type, binary_data |
asset_properties.asset_desc_image_id est NOT NULL, donc une ligne
desc_images est toujours écrite ; sans aperçu uploadé, son binary_data vaut
NULL.
6. Erreurs
Toutes les erreurs suivent le format de l'API, avec un message préfixé par l'endpoint concerné.
| Statut | Message (extrait) | Cause |
|---|---|---|
| 400 | POST /nft/create failed: sourceFile.content is empty or not valid base64 | Base64 vide ou invalide |
| 400 | POST /nft/create failed: previewImage.content is not valid base64 | Base64 corrompu (détecté par ré-encodage) |
| 400 | ... is N bytes, over the 52428800 byte limit | Fichier > 50 Mo après décodage |
| 400 | POST /nft/create failed: sourceFile is required when nftCreation is false | nftCreation: false sans fichier source |
| 400 | hash mismatch | Sur register, le sourceFile ne hashe pas vers le sourceFileHash fourni |
| 400 | POST /ipfs/cid failed: provide either url or content, not both | Les deux champs fournis |
| 400 | POST /ipfs/cid failed: url or content is required | Aucun des deux |
| 401 | Session token is missing / Invalid or expired session | Header d'auth absent ou session invalide |
| 404 | — | GET /nft/preview/:hash : œuvre inconnue ou sans aperçu |
| 413 | (Express) | Corps HTTP au-delà de MAX_REQUEST_BODY_SIZE |
7. Checklist d'intégration
- Le client envoie
sourceFileetpreviewImageen base64 inline, dans le corps JSON (pas demultipart). - Les fichiers > 50 Mo sont rejetés côté client avant l'envoi, pour éviter un aller-retour de 50 Mo.
- L'affichage d'une œuvre utilise
files.previewImage.url(ou{PUBLIC_BASE_URL}/nft/preview/{sourceFileHash}), sans en-tête d'auth. - Le client ne tente jamais de récupérer le fichier source par HTTP : il n'y a pas d'endpoint.
- Les deux formes de réponse de
POST /nft/createsont gérées, discriminées par le booléennftCreation. - Le parcours « enregistrer maintenant, frapper plus tard » réutilise
exactement le même
sourceFileau second appel. -
image_ipfs/files.*.cidsont affichés comme informatifs : rien n'est pinné, ces URI ne résolvent pas encore. -
PUBLIC_BASE_URLest configuré sur l'environnement cible avant le premier mint.
Références
- docs/API_REFERENCE.md — référence complète des endpoints
- docs/REGISTRAR_INTEGRATION.md — guide d'intégration registrar
- docs/DATABASE.md — schéma et stockage des octets
- Swagger :
http://localhost:3000/api