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).

PRTitreApport
#37Add IpfsModuleNouveau endpoint POST /ipfs/cid : calcul d'un CID IPFS sans upload ni pinning
#39Add images upload and processingUpload du fichier source et de l'image d'aperçu dans POST /nft/create et POST /nft/register + endpoint public GET /nft/preview/:sourceFileHash
#41Make NFT creation optionalChamp 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

  1. Rappels d'authentification
  2. Upload de fichiers (PR #39)
  3. Création de NFT optionnelle (PR #41)
  4. Calcul de CID IPFS (PR #37)
  5. Configuration serveur
  6. Erreurs
  7. 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.

FichierObligatoireVisibilitéRôle
sourceFileNon*PrivéL'œuvre originale. Ses octets sont stockés en base et leur SHA‑384 est le sourceFileHash. Aucun endpoint ne le sert.
previewImageNonPublicImage 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

ChampTypeDescription
filenamestringNom d'origine, stocké avec les octets. Max 255 caractères.
mimeTypestringType MIME. Max 100 caractères.
contentstringLe 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.
textDatastringOptionnel. 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 :

  • url n'apparaît que sur previewImage. Le fichier source n'a pas d'adresse publique, par conception.
  • Si aucun fichier n'est envoyé : files.sourceFile et files.previewImage valent null, metadata.image conserve l'imageUrl fourni par l'appelant, et image_ipfs / source_file_hash sont 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-Type d'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.
StatutCause
200Les octets de l'image d'aperçu
404Aucune œ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/create le hash est dérivé de l'upload. Sans upload, on retombe sur le hash synthétique historique sha384(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éeValeurRésout aujourd'hui ?
imagehttps://<PUBLIC_BASE_URL>/nft/preview/<hash>Oui
image_ipfsipfs://<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_image et asset_properties. Pas de ligne asset ni nft, toutes deux étant clefées par un token ID qui n'existe pas encore.
  • sourceFile devient 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, imageUrl et thumbnail ne 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 donc contractAddress et tokenId.

Comparatif des trois flux

create + nftCreation: true (défaut)create + nftCreation: falseregister
Déploie un contratOuiNonNon
Frappe un tokenOuiNonNon (déjà frappé)
Requiert RPC + gasOuiNonOui (lecture ownerOf())
Requiert sourceFileNonOuiNon
Requiert symbol/imageUrl/thumbnailOuiNon
Requiert contractAddress/tokenIdNonNonOui
Lignes écritessource_file, desc_image, asset_properties, asset, nftsource_file, desc_image, asset_propertiesidem 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.

ChampTypeDescription
urlstringURL du fichier à hasher. Les octets sont téléchargés, hashés, jetés. http/https uniquement, 50 Mo max.
contentstringContenu inline, décodé selon encoding.
encodingutf8 | base64Comment décoder content. Défaut : utf8.
filenamestringNom 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

StatutCause
400Ni url ni content, ou les deux à la fois
400url malformé, protocole autre que http/https, injoignable, statut non‑2xx, ou taille dépassée
500L'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_URL par défaut vaut http://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

FichierTableColonnes
sourceFilesource_filesfilename, mime_type, binary_data, source_file_hash
previewImagedesc_imagesfilename, 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é.

StatutMessage (extrait)Cause
400POST /nft/create failed: sourceFile.content is empty or not valid base64Base64 vide ou invalide
400POST /nft/create failed: previewImage.content is not valid base64Base64 corrompu (détecté par ré-encodage)
400... is N bytes, over the 52428800 byte limitFichier > 50 Mo après décodage
400POST /nft/create failed: sourceFile is required when nftCreation is falsenftCreation: false sans fichier source
400hash mismatchSur register, le sourceFile ne hashe pas vers le sourceFileHash fourni
400POST /ipfs/cid failed: provide either url or content, not bothLes deux champs fournis
400POST /ipfs/cid failed: url or content is requiredAucun des deux
401Session token is missing / Invalid or expired sessionHeader d'auth absent ou session invalide
404GET /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 sourceFile et previewImage en base64 inline, dans le corps JSON (pas de multipart).
  • 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/create sont gérées, discriminées par le booléen nftCreation.
  • Le parcours « enregistrer maintenant, frapper plus tard » réutilise exactement le même sourceFile au second appel.
  • image_ipfs / files.*.cid sont affichés comme informatifs : rien n'est pinné, ces URI ne résolvent pas encore.
  • PUBLIC_BASE_URL est configuré sur l'environnement cible avant le premier mint.

Références