Two validators live in contracts/contracts-aiken/validators/ (full datum/redeemer
reference incl. the Pebble cross-check build: contracts/README.md).
Mint policy: Only the manufacturer (parameterized VKH) can mint, exactly 1 token per tx.
Spend validator: Transfers require:
- Current holder's signature
- A continuing output with updated datum (
current_holder = next_holder,step + 1)
Datum: ChainOfCustody { manufacturer, current_holder, batch_id, step }
Redeemer: Action::Transfer { next_holder }
A per-batch temperature monitor as an on-chain state thread: InitColdChainMonitor
locks a MonitorDatum (allowed range, reading digest chain) at the script address,
each RecordSensorReadings spends and re-locks it with the updated state — the
validator enforces that out-of-range readings flip breached = true and that the
flag can never be cleared again — and CloseColdChainMonitor seals the thread.
VerifyBatch exposes the result publicly (coldChain.breached, excursion count).
| Action | Description |
|---|---|
MintBatchNft(batchId) |
Build unsigned mint tx |
TransferBatch(batchId, toParticipantId, ...) |
Build unsigned transfer tx |
SubmitSigned(buildId, signedTxCbor) |
Submit externally signed tx |
CheckPendingTransactions() |
Poll SUBMITTED events |
RetryFailedTransaction(proofEventId) |
Rebuild failed tx |
AnchorDocument(batchId, documentHash, ...) |
Anchor doc hash on-chain |
AnchorColdChain(batchId, telemetryHash, ...) |
Anchor cold-chain telemetry (CIP-20 metadata) |
InitColdChainMonitor(batchId, minMilliC, maxMilliC, ...) |
Open an on-chain monitor (Plutus lock) |
RecordSensorReadings(monitorId, readingsJson) |
Commit readings, breach detection on-chain |
CloseColdChainMonitor(monitorId) |
Seal the monitor thread |
VerifyBatch(batchIdOrFingerprint) |
Public custody + cold-chain verification |
1. User clicks action (e.g. "Mint NFT")
2. App calls OData action > { unsignedCbor, buildId }
3. App calls wallet.signTx(cbor) > browser wallet popup
4. User confirms > signedCbor
5. App calls SubmitSigned > { txHash, status: "SUBMITTED" }
6. Background polling (30s) > status: "CONFIRMED" | "FAILED"
TRACE/ODATANO never hold private keys. All signing happens in the user's browser wallet.
| Page | Description |
|---|---|
| Batches | List batches with status chips, create DRAFT batches |
| Batch Detail | Mint/Transfer/Anchor actions, event timeline, asset info |
| Participants | Inline CRUD table |
| Verify | Public verification of batch custody chain |