Skip to main contentA-REX Core

Livre blanc · 11

Guide d'implémentation

D'un clone propre à un jeton de lot déployé, doté de hooks et attesté, pas à pas. Chaque bloc JavaScript de ce chapitre est copié depuis test/standard/ImplementationGuide.test.js et s'exécute en CI ; si un bloc ici était erroné, le build échouerait.

Comment utiliser ce guide

Les étapes s'appuient les unes sur les autres et partagent des variables : token, attestations, les signataires et les identifiants créés en cours de route. Lisez-les dans l'ordre la première fois. Exécutez-les vous-même avec :

npx hardhat test test/standard/ImplementationGuide.test.js
ÉtapeCe que vous obtenezQui agitAppels principaux
1 · Préparationune copie de travail compilée et testéedéveloppeurnpm ci, npm test
2 · Déploiementproxy du jeton + registre d'attestationsdéployeurupgrades.deployProxy, ARexAttestations
3 · Rôlesdes opérateurs au moindre privilègeadministrateurgrantRole
4 · Créationun lot avec engagement sur les métadonnéesémetteurcreateBatch
5 · Lecturel'enregistrement du lot et l'offren'importe quibatchOf, totalSupply(uint256), parentsOf
6 · Transfertun transfert vérifiédétenteurtransferCheck, safeTransferFrom
7 · Filiationfractionnement, fusion, traitementdétenteur / transformateurtransform
8 · Statutcycle de vie et mises à jour des métadonnéesrôle de statutsetBatchStatus, setBatchMetadata
9 · Hooksun pipeline de règles ordonnéadministrateur des hooksregisterHook, getHooks
10 · Attestationdes preuves qui débloquent les transfertsémetteur de confiancesetTrustedIssuer, attest
11 · Sanctionséligibilité avec source et TTLgestionnaire d'éligibilitésetSanctioned
12 · Mise en applicationgel, pause, transfert forcéagent de mise en application / responsable de la pausesetFrozenTokens, forcedTransfer
13 · Retraitfin de vie d'un lotrôle de statut, détenteursetBatchStatus(…, 255, …), burn
14 · Indexationle graphe de filiation à partir des événementsindexeurqueryFilter(BatchTransformed)
15 · Mise à niveauune mise à niveau validée de l'implémentationresponsable de la mise à niveauvalidateUpgrade, upgradeProxy

1 · Préparation

Node.js 20 ou 22 LTS et npm. Les dépendances sont épinglées dans package-lock.json (OpenZeppelin 5.3.0) ; utilisez npm ci afin que votre build soit celui que construit la CI.

git clone https://github.com/Arex-Corp/A-REX.git && cd A-REX
npm ci
npm run compile
npm test                     # 46 tests: reference implementation, hooks, extensions, invariants, this guide
npm run interface-ids        # prints the ERC-165 ids; they must match the specification

Les extraits ci-dessous utilisent trois petites fonctions utilitaires et nomment dix comptes. explain retraduit un code de rejet en son nom ; la liste est l'ensemble des codes définis dans ARexCodes.

const commodity = (scheme, code) => ethers.keccak256(ethers.toUtf8Bytes(`${scheme}:${code}`));
const unit = (u) => ethers.encodeBytes32String(u);
const ref = (text) => ethers.keccak256(ethers.toUtf8Bytes(text));
const REASONS = Object.fromEntries(
  ["PAUSED", "FROZEN", "SENDER_INELIGIBLE", "RECEIVER_INELIGIBLE", "SANCTIONED", "BATCH_NOT_FOUND",
   "BATCH_RETIRED", "BATCH_EXPIRED", "BATCH_NOT_PRODUCED", "ATTESTATION_MISSING", "SEASON_CLOSED",
   "IN_TRANSIT", "LIMIT_EXCEEDED", "DOCUMENT_MISSING"].map((n) => [ethers.keccak256(ethers.toUtf8Bytes(`AREX:${n}`)), n]),
);
const explain = (code) => (code === ethers.ZeroHash ? "OK" : REASONS[code] ?? code);


const [admin, issuer, farmer, buyer, coop, mill, warehouse, certifier, enforcer, custodian] = await ethers.getSigners();

2 · Déploiement

Le jeton de référence peut être mis à niveau via UUPS ; il est donc déployé derrière un proxy ERC-1967 avec le plugin de mises à niveau d'OpenZeppelin, qui valide aussi la disposition du stockage. Le registre d'attestations est un contrat ordinaire lié au jeton.

const ARexToken = await ethers.getContractFactory("ARexToken");
const token = await upgrades.deployProxy(ARexToken, [admin.address, "ipfs://<cid>/{id}.json"], { kind: "uups" });
await token.waitForDeployment();
const attestations = await (await ethers.getContractFactory("ARexAttestations"))
  .deploy(admin.address, await token.getAddress());

En ligne de commande, la même pile, avec en plus les hooks d'éligibilité et d'expiration, se déploie avec :

npx hardhat node                                                     # terminal 1
npx hardhat run scripts/deploy-standard.js --network localhost       # token, attestations, 2 hooks
npx hardhat run scripts/deploy-full-standard.js --network localhost  # every hook in order + extensions

ou avec Hardhat Ignition :

npx hardhat ignition deploy ignition/modules/ARexToken.js --network localhost \
  --parameters '{"ARexTokenModule":{"baseURI":"ipfs://<cid>/{id}.json","allowPreHarvestTransfers":true}}'

Pour un testnet ou un L2, hardhat.config.js définit sepolia, polygon et arbitrum ; renseignez l'URL RPC et PRIVATE_KEY dans .env (modèle dans .env.example). ARexToken compile en 24,136 octets avec optimizer.runs = 1, sous la limite de 24,576 octets d'EIP-170.

3 · Rôles

initialize(admin, baseURI) attribue tous les rôles à admin, ce qui est commode pour un premier déploiement et inadapté à la production. Attribuez chaque rôle opérationnel au service ou au multisig qui l'exerce :

await token.grantRole(await token.ISSUER_ROLE(), issuer.address);
await token.grantRole(await token.TRANSFORMER_ROLE(), mill.address);
await token.grantRole(await token.STATUS_ROLE(), warehouse.address);
await token.grantRole(await token.ENFORCER_ROLE(), enforcer.address);
RôleAutoriseTitulaire recommandé
DEFAULT_ADMIN_ROLEattribuer et révoquer tous les rôles ; setBaseURImultisig de gouvernance derrière un timelock
UPGRADER_ROLEremplacer l'implémentationmême timelock
HOOK_ADMIN_ROLEregisterHook, removeHookmême timelock (les changements de hooks deviennent visibles avant leur application)
ENFORCER_ROLEsetFrozenTokens, forcedTransfermultisig de mise en application (conformité et juridique)
PAUSER_ROLEpause, unpauseclé de réponse aux incidents
ISSUER_ROLEcreateBatchservice d'origination
STATUS_ROLEsetBatchStatus, setBatchMetadataentrepôt, inspecteur ou registre
TRANSFORMER_ROLEtransformations de traitement (changement de denrée, d'unité ou de quantité)transformateurs
ATTEST_ADMIN_ROLE (sur ARexAttestations)setTrustedIssuer, révoquer n'importe quelle attestationautorité de certification ou plateforme

Lorsque chaque rôle a son titulaire, le déployeur renonce à ce dont il n'a plus besoin avec renounceRole(role, deployer).

4 · Créer un lot

Un lot est un identifiant ERC-1155. Son enregistrement on-chain est petit et fixe ; tout ce qui est descriptif vit dans un document de métadonnées dont le hash est engagé on-chain (schéma : standard/schema/batch-metadata.schema.json).

const metadata = { schemaVersion: "1.0.0", commodity: { scheme: "HS", code: "100199" }, unit: "KGM", quantity: 250000 };
const canonical = JSON.stringify(metadata);                 // RFC 8785 canonicalization in production
const metadataHash = ethers.keccak256(ethers.toUtf8Bytes(canonical));
const uri = "ipfs://<cid>/batch-1.json";                    // wherever you stored `canonical`


const init = {
  commodity: commodity("HS", "100199"),                     // wheat
  unit: unit("KGM"),
  producedAt: Math.floor(Date.parse("2026-03-18T00:00:00Z") / 1000), // 0 = not yet harvested
  expiresAt: 0,                                             // 0 = no expiry
  originRef: ref("DDS-2026-AR-000123"),                     // a reference, never coordinates
  metadataHash,
  uri,
};
const id = await token.connect(issuer).createBatch.staticCall(init, 250_000, farmer.address, "0x");
await (await token.connect(issuer).createBatch(init, 250_000, farmer.address, "0x")).wait();

createBatch.staticCall renvoie l'identifiant que la transaction attribuera ; les identifiants commencent à 1 et augmentent de un en un. Le jeton rejette :

ConditionErreur
l'appelant n'a pas ISSUER_ROLEAccessControlUnauthorizedAccount(account, role)
to est l'adresse zéroARexInvalidRecipient()
quantity == 0ARexInvalidBatch("ZERO_QUANTITY")
commodity == 0 ou unit == 0ARexInvalidBatch("COMMODITY_REQUIRED") / ("UNIT_REQUIRED")
les deux dates sont renseignées et expiresAt <= producedAtARexInvalidBatch("EXPIRY_BEFORE_PRODUCTION")
jeton en pause, destinataire inéligible, ou un hook rejette OP_MINTEnforcedPause(), ERC7943CannotReceive(to), ARexHookRejected(hook, code)

Conventions des champs :

ChampConventionExemple
commoditykeccak256("<scheme>:<code>"), nomenclature parmi HS, FAO, GTIN, UNSPSC ou la vôtrekeccak256("HS:100199") blé
unitcode de la Recommandation 20 de l'UN/CEFACT sous forme de bytes32"KGM", "TNE", "LTR", "BX"
producedAtsecondes Unix ; 0 pour un lot pas encore récoltévente à terme, créance sur récolte
expiresAtsecondes Unix ; 0 pour aucune expirationproduits périssables
originRefhash d'une référence vers des preuves d'origine ; jamais des coordonnéesréférence DDS EUDR, identifiant de registre
metadataHashkeccak256 du document de métadonnées canonique (RFC 8785)
uriemplacement du document ; gardez-la courte, elle est stockée on-chainipfs://<cid>, GS1 Digital Link

5 · Lire un lot

const batch = await token.batchOf(id);                      // reverts ARexBatchNotFound if unknown
await token.batchExists(id);                                // true / false, never reverts
await token["totalSupply(uint256)"](id);                    // units in circulation (overloaded name)
await token.parentsOf(id);                                  // [] for an original lot
await token.uri(id);                                        // batch URI, or the base URI if empty
await token.isExpired(id);                                  // reference-implementation helper

totalSupply est surchargée dans ERC-1155 Supply (totalSupply() et totalSupply(uint256)), il faut donc l'appeler par sa signature dans ethers v6. Batch.status vaut 0 (ACTIVE), 255 (RETIRED) ou une valeur définie par l'émetteur entre les deux.

6 · Transfert

Demandez toujours d'abord. transferCheck rejoue le pipeline de transfert sous forme de vue (view) et renvoie le premier code de rejet ; 0x00…00 signifie autorisé. canTransfer est la version booléenne ERC-7943 de la même réponse.

const code = await token.transferCheck(farmer.address, buyer.address, id, 10_000);
if (code !== ethers.ZeroHash) throw new Error(`transfer blocked: ${explain(code)}`);
await token.connect(farmer).safeTransferFrom(farmer.address, buyer.address, id, 10_000, "0x");

L'ordre du pipeline est fixe ; le code vous indique donc quelle règle a échoué :

OrdreVérificationCode renvoyé par transferCheckRevert de la transaction
1jeton en pauseAREX:PAUSEDEnforcedPause()
2expéditeur éligible (chaque hook ELIGIBILITY)AREX:SENDER_INELIGIBLEERC7943CannotSend(from)
3destinataire éligibleAREX:RECEIVER_INELIGIBLEERC7943CannotReceive(to)
4le lot existeAREX:BATCH_NOT_FOUNDARexBatchNotFound(id)
5le lot n'est pas retiréAREX:BATCH_RETIREDARexBatchRetired(id)
6amount <= balance − frozenAREX:FROZENERC7943InsufficientUnfrozenBalance(from, id, amount, unfrozen)
7chaque hook CHECK, dans l'ordre d'enregistrementle code du hookARexHookRejected(hook, code)

La vue et la transaction évaluent dans le même ordre ; le code désigne donc toujours la vérification que la transaction échoue en premier.

Deux détails comptent en pratique. AREX:FROZEN est aussi renvoyé lorsque l'expéditeur ne détient simplement pas assez d'unités, car la vérification porte sur le solde non gelé. Et transferCheck transmet des data vides aux hooks ; un hook qui lit les données de transfert (comme le règlement de DeliveryHook) peut répondre différemment pour la transaction réelle.

7 · Filiation : fractionner, fusionner, traiter

transform est le seul moyen de fabriquer un lot à partir de lots. Elle détruit les entrées de from, émet les sorties vers to, enregistre parentsOf(output) = inputIds et émet un BatchTransformed, qui correspond à un TransformationEvent d'EPCIS. transform.staticCall renvoie les nouveaux identifiants.

Une réorganisation de lots conserve la denrée, l'unité et la quantité totale, et ne nécessite aucun rôle, seulement la propriété ou une approbation ERC-1155. Fractionner un lot en deux pour un acheteur :

const [lotA, lotB] = await token.connect(farmer).transform.staticCall(
  farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));
await token.connect(farmer).transform(
  farmer.address, buyer.address, [id], [400], [init, init], [250, 150], ref("SALE-42"));

Fusionner deux lots en un lot mutualisé pour une coopérative :

const [pooled] = await token.connect(buyer).transform.staticCall(
  buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));
await token.connect(buyer).transform(
  buyer.address, coop.address, [lotA, lotB], [250, 150], [init], [400], ref("POOL-7"));

Le traitement modifie la denrée, l'unité ou la quantité ; il exige donc TRANSFORMER_ROLE en plus de l'approbation. La minoterie transforme 1,000 kg du blé de l'agriculteur en 750 kg de farine :

const now = (await ethers.provider.getBlock("latest")).timestamp;
const flourInit = { ...init, commodity: commodity("HS", "110100"), producedAt: now };
await token.connect(farmer).setApprovalForAll(mill.address, true);
const [flour] = await token.connect(mill).transform.staticCall(
  farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
await token.connect(mill).transform(
  farmer.address, mill.address, [id], [1_000], [flourInit], [750], ref("MILL-RUN-7"));
RègleGarantie par
l'appelant est from ou approuvé par fromtransform
chaque entrée existe, n'est pas retirée et dispose d'un solde non gelé suffisant_update
réorganisation : même denrée et même unité, Σ entrées = Σ sortiesaucun rôle nécessaire
tout le resteTRANSFORMER_ROLE, sinon AccessControlUnauthorizedAccount
les hooks voient chaque mouvementOP_TRANSFORM pour les destructions et les émissions

La filiation construite par ce guide :

flowchart LR
    B1["#1 blé 250,000 kg<br/>agriculteur"] -->|"fractionnement · SALE-42"| B2["#2 blé 250"]
    B1 -->|"fractionnement · SALE-42"| B3["#3 blé 150"]
    B2 -->|"fusion · POOL-7"| B4["#4 blé 400<br/>coop"]
    B3 -->|"fusion · POOL-7"| B4
    B1 -->|"traitement · MILL-RUN-7"| B5["#5 farine 750<br/>minoterie"]

8 · Statut et métadonnées

Seul STATUS_ROLE modifie le statut ; détenir des unités ne confère rien. Les valeurs 1..254 sont à définir par vous ; publiez leur signification dans le document de métadonnées (status.table). La mise à jour des métadonnées remplace le hash et l'URI et émet BatchMetadataUpdated, de sorte que l'historique des documents reste vérifiable.

await token.connect(warehouse).setBatchStatus(flour, 1, ref("IN_STORAGE"));  // 1..254 are yours
const labReport = { ...metadata, lab: { moisture: 14.2 } };
await token.connect(warehouse).setBatchMetadata(
  flour, ethers.keccak256(ethers.toUtf8Bytes(JSON.stringify(labReport))), "ipfs://<cid>/flour-lab.json");

9 · Hooks

Les hooks sont des contrats indépendants que le jeton appelle dans l'ordre d'enregistrement. Ici, le hook d'éligibilité s'exécute en premier, puis celui d'expiration, puis une règle selon laquelle le café exige une déclaration de diligence raisonnée EUDR attestée avant de pouvoir être vendu.

const eligibility = await (await ethers.getContractFactory("EligibilityHook")).deploy(admin.address);
const expiry = await (await ethers.getContractFactory("ExpiryHook")).deploy(true); // allow pre-harvest
const attestationHook = await (await ethers.getContractFactory("AttestationHook"))
  .deploy(admin.address, await attestations.getAddress());
const COFFEE = commodity("HS", "090111");
const EUDR_DDS = ref("AREX:EUDR:DDS");
await attestationHook.setRequirement(COFFEE, EUDR_DDS);     // coffee needs a due-diligence statement


await token.registerHook(await eligibility.getAddress());      // evaluated first
await token.registerHook(await expiry.getAddress());
await token.registerHook(await attestationHook.getAddress());  // evaluated last
await token.getHooks();                                        // the ordered list
HookDrapeauxConstructeurSe configure avec
EligibilityHookCHECK, ELIGIBILITY(admin)setAllowlistEnabled, setAllowlisted, setBlocklisted, setSanctioned
ExpiryHookCHECK(allowPreHarvestTransfers)— (lit l'enregistrement du lot)
AttestationHookCHECK(owner, attestationRegistry)setRequirement(commodity, topic)
PerishabilityHookCHECK(admin)setCommodityDecay, setBatchDecay
SeasonalHookCHECK(admin)addWindow(commodity, op, start, end), clearWindows
DeliveryHookCHECK, EXECUTED(admin)schedule, pickUp, markInTransit, recordReading, confirmDelivered, dispute, resolve
TraceabilityHookEXECUTED(admin, persist)recordEvent (RECORDER_ROLE)

L'enregistrement rejette les EOA, les contrats sans prise en charge ERC-165 de IARexHook et les hooks dont les drapeaux valent 0. Placez en premier les hooks peu coûteux qui rejettent fréquemment : un rejet interrompt le pipeline. Pour écrire votre propre hook : docs/HOOK-DEVELOPMENT.md.

10 · Attestations

Une attestation est (batch, topic) → (issuer, reference, issuedAt, expiresAt, revoked), valide uniquement tant que son émetteur est de confiance pour le thème. Avec l'exigence de l'étape 9 en place, le lot de café ne peut pas circuler tant que la déclaration n'est pas attestée :

const coffee = await token.connect(issuer).createBatch.staticCall({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
await token.connect(issuer).createBatch({ ...init, commodity: COFFEE }, 60, farmer.address, "0x");
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1));   // "ATTESTATION_MISSING"


await attestations.setTrustedIssuer(EUDR_DDS, certifier.address, true);         // ATTEST_ADMIN_ROLE
await attestations.connect(certifier).attest(coffee, EUDR_DDS, ref("DDS-2026-AR-000123"), 0);
await attestations.isAttested(coffee, EUDR_DDS);                                 // true
explain(await token.transferCheck(farmer.address, buyer.address, coffee, 1));   // "OK"

isAttested vaut faux lorsqu'il n'existe aucune attestation, lorsqu'elle est révoquée, lorsque expiresAt est dépassé, ou lorsque l'émetteur n'est plus de confiance pour le thème. L'émetteur ou ATTEST_ADMIN_ROLE peut la révoquer. Les thèmes sont des valeurs bytes32 libres ; utilisez keccak256("<NAMESPACE>:<NAME>"), par exemple AREX:EUDR:DDS ou AREX:CERT:ORGANIC_EU.

11 · Sanctions et éligibilité

EligibilityHook répond à canSend et canReceive pour le jeton. Une sanction enregistre sa source et une expiration facultative, de sorte qu'une mise à jour de liste peut expirer automatiquement.

await eligibility.setSanctioned(buyer.address, true, ethers.encodeBytes32String("OFAC-SDN"), 0);
explain(await token.transferCheck(farmer.address, buyer.address, id, 1));       // "RECEIVER_INELIGIBLE"
await eligibility.setSanctioned(buyer.address, false, ethers.encodeBytes32String("OFAC-SDN"), 0);

12 · Mise en application

Le gel et les transferts forcés constituent la surface de mise en application d'ERC-7943, pour les sûretés, les ordonnances judiciaires et les saisies. Un gel peut dépasser le solde courant et couvre alors aussi les dépôts futurs. Un transfert forcé ignore l'éligibilité et la vérification du gel, fonctionne pendant que le jeton est en pause, abaisse le montant gelé à ce qui reste, et exécute tout de même les hooks avec OP_FORCED_TRANSFER.

await token.connect(enforcer).setFrozenTokens(buyer.address, id, 6_000);          // may exceed the balance
await token.getFrozenTokens(buyer.address, id);                                   // 6_000
explain(await token.transferCheck(buyer.address, farmer.address, id, 5_000));    // "FROZEN"
await token.pause();                                                              // PAUSER_ROLE
await token.connect(enforcer).forcedTransfer(buyer.address, custodian.address, id, 6_000); // works while paused
await token.unpause();

Documentez la procédure selon laquelle ENFORCER_ROLE agit (ordonnance judiciaire, contrat de nantissement, avis de sanctions) et gardez le rôle derrière un multisig. Chaque action émet Frozen ou ForcedTransfer.

13 · Retrait

RETIRED (255) est terminal : plus de transfert, d'émission ni de transformation, et aucun retour en arrière. La destruction reste possible afin que les biens livrés ou détruits quittent l'offre.

await token.connect(warehouse).setBatchStatus(flour, 255, ref("DELIVERED"));    // RETIRED is terminal
explain(await token.transferCheck(mill.address, buyer.address, flour, 1));       // "BATCH_RETIRED"
await token.connect(mill).burn(mill.address, flour, 750);                         // burning still works

14 · Indexation

Le graphe de filiation est la liste des événements BatchTransformed ; rien n'a besoin d'être reconstitué à partir des transferts individuels.

const transforms = await token.queryFilter(token.filters.BatchTransformed());
const edges = transforms.flatMap((e) =>
  e.args.inputIds.flatMap((input) => e.args.outputIds.map((output) => [input, output, e.args.processRef])));
// edges: [1 → 2], [1 → 3], [2 → 4], [3 → 4], [1 → 5]
ÉvénementÉmis parUsage
BatchCreated(id, issuer, commodity, unit, quantity, to)createBatchnouveau lot ; ObjectEvent ADD d'EPCIS
BatchTransformed(operator, inputIds, inputAmounts, outputIds, outputAmounts, processRef)transformarête de filiation ; TransformationEvent d'EPCIS
BatchStatusChanged(id, previousStatus, newStatus, reason)setBatchStatuscycle de vie
BatchMetadataUpdated(id, metadataHash, uri)setBatchMetadatahistorique des documents
TransferSingle, TransferBatchchaque modification de soldeavoirs ; ObjectEvent OBSERVE d'EPCIS
Frozen(account, tokenId, amount), ForcedTransfer(from, to, tokenId, amount)mise en applicationsûretés, saisies
HookRegistered(hook, flags, position), HookRemoved(hook)administration des hookschangements de règles
Attested, AttestationRevoked, TrustedIssuerUpdatedARexAttestationspreuves

15 · Mise à niveau

Le stockage utilise un espace de noms ERC-7201 (erc7201:arex.storage.token) ; ajoutez des champs à TokenStorage, ne les réordonnez ni ne les supprimez jamais. Validez avant chaque mise à niveau ; le plugin refuse les dispositions qui corrompraient le stockage.

await upgrades.validateUpgrade(await token.getAddress(), ARexToken, { kind: "uups" });
await upgrades.upgradeProxy(await token.getAddress(), ARexToken);               // UPGRADER_ROLE

En production, UPGRADER_ROLE se trouve derrière un multisig et un timelock, et la mise à niveau est d'abord répétée sur un fork de la chaîne cible.

Tester votre intégration

npx hardhat test test/standard/ImplementationGuide.test.js   # this chapter, executed
npx hardhat test test/standard/ARexToken.test.js             # token behaviour and conformance
npx hardhat run scripts/smoke-standard.js                    # narrated walkthrough of one lot
npx hardhat run scripts/gas-report-standard.js               # gas with 0, 1 and 2 hooks, and per hook

test/standard/helpers.js exporte deployStack(), batchInit(), les constantes OP, HOOK, STATUS et CODES ainsi que supply(), pour vos propres tests.

Passage en production : liste de contrôle

  1. Build reproductible (npm ci), CI au vert, identifiants d'interface égaux à ceux de la spécification.
  2. Rôles redistribués comme à l'étape 3 ; rôles du déployeur abandonnés.
  3. Hooks audités, ordre documenté, getHooks() publié.
  4. Version du schéma de métadonnées et table des statuts publiées ; originRef et données restreintes exemptes de données personnelles.
  5. Instruments juridiques référencés dans les métadonnées ; qualification de l'offre arrêtée avec un conseil.
  6. Audit par un tiers du jeton, du registre d'attestations et de chaque hook enregistré (dossier : docs/AUDIT-READINESS.md).
  7. Mise à niveau répétée sur un fork avec validateUpgrade.