TRACE is Example SAP FIORI Application for a pharmaceutical supply chain tracking system built on SAP CAP and the Cardano blockchain. It provides tamper-proof chain-of-custody for drug batches from manufacturer to pharmacy, using NFTs as on-chain proof and CIP-30 browser wallets for transaction signing.
Blockchain integration via ODATANO — a CAP plugin that exposes Cardano as OData V4 services.
| Layer | Technology |
|---|---|
| Smart Contracts | Aiken (Plutus V3) |
| Blockchain | Cardano preview testnet |
| Chain Gateway | @odatano/core v0.3.17 (CAP plugin) |
| Backend | SAP CAP v9, Node.js, TypeScript |
| Database | SQLite (dev), SAP HANA (prod) |
| Frontend | Freestyle SAPUI5 (OpenUI5 CDN, sap_horizon theme) |
| Signing | CIP-30 browser wallets (external signing, zero key custody) |
- Batch NFT Minting — Manufacturer mints a unique NFT per drug batch with on-chain datum
- Chain of Custody Transfers — Each handoff (manufacturer → distributor → pharmacy) is a Plutus spend transaction
- Document Anchoring — SHA-256 hashes of certificates, lab reports, and cold-chain telemetry anchored via CIP-20 metadata transactions
- Public Verification — Anyone can verify a batch's full custody chain against on-chain data
- Transaction Monitoring — 30-second background polling with automatic PENDING → CONFIRMED/FAILED transitions
- Retry Mechanics — Failed transactions can be rebuilt and resubmitted
- Node.js >= 18
- npm >= 9
- A CIP-30 compatible Cardano wallet (Nami, Eternl, or Lace) set to preview network
git clone <repo-url> && cd TRACE
npm install
npm run deploy # Creates SQLite DB with all tables + seed dataTX_BUILDERS=buildooor npx cds watchOpen http://localhost:4004/trace/webapp/index.html
Sample data is loaded from CSV files in db/data/:
| File | Contents |
|---|---|
trace-Participants.csv |
3 participants (manufacturer, distributor, pharmacy) |
trace-Batches.csv |
2 draft batches (Aspirin, Ibuprofen) |
TRACE/
├── db/
│ ├── schema.cds # 5 entities: Participants, Batches, OnChainAssets,
│ │ # ProofEvents, DocumentAnchors
│ └── data/ # CSV seed data
├── srv/
│ ├── trace-service.cds # OData V4 service definition (8 actions + 1 function)
│ ├── trace-service.ts # Full handler implementation
│ └── lib/
│ ├── chain-adapter.ts # ODATANO service wrapper (cds.connect.to)
│ └── digest.ts # SHA-256 + JSON canonicalization
├── contracts/
│ ├── validators/
│ │ └── pharma_trace.ak # Aiken smart contract (mint + spend + else)
│ └── plutus.json # Compiled Plutus V3 validators
├── app/
│ └── trace/webapp/ # SAPUI5 frontend
│ ├── index.html # UI5 CDN bootstrap
│ ├── manifest.json # App descriptor + OData V4 datasource
│ ├── Component.js # Router (4 routes)
│ ├── model/
│ │ ├── CardanoWallet.js # CIP-30 wallet integration
│ │ └── models.js # JSONModel factories
│ ├── controller/ # App, BatchList, BatchDetail,
│ │ # Participants, Verify
│ ├── view/ # XML views for each page
│ └── fragment/ # WalletConnect, TransferDialog,
│ # AnchorDialog
├── scripts/
│ ├── deploy-db.js # Full DB deployment (TRACE + ODATANO tables)
│ └── seed-protocol-params.js # Cache Cardano protocol parameters
└── package.json # CAP config + ODATANO plugin config
Participants (Manufacturer | Distributor | Pharmacy | Regulator)
│
├──< Batches (DRAFT → MINTED → IN_TRANSIT → DELIVERED | RECALLED)
│ │
│ ├──< OnChainAssets 1:1 (policyId, assetName, UTxO ref, step)
│ ├──< ProofEvents 1:N (MINT, TRANSFER, VERIFY, DOCUMENT_ANCHOR)
│ └──< DocumentAnchors 1:N (doc hash, type, cold-chain temps)
The Aiken validator (contracts/validators/pharma_trace.ak) enforces two rules on-chain:
Mint policy — Only the manufacturer (parameterized VKH) can mint, and exactly 1 token per transaction.
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 }
| Action | Description |
|---|---|
MintBatchNft(batchId) |
Build unsigned mint transaction |
TransferBatch(batchId, toParticipantId, ...) |
Build unsigned transfer transaction |
SubmitSigned(buildId, signedTxCbor) |
Submit externally signed transaction |
CheckPendingTransactions() |
Poll all SUBMITTED events for confirmation |
RetryFailedTransaction(proofEventId) |
Rebuild a failed transaction |
AnchorDocument(batchId, documentHash, ...) |
Anchor document hash on-chain |
AnchorColdChain(batchId, telemetryHash, ...) |
Anchor cold-chain telemetry |
VerifyBatch(batchIdOrFingerprint) |
Public chain-of-custody 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 in wallet → 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 all batches with status chips, create new DRAFT batches |
| Batch Detail | Actions (Mint/Transfer/Anchor), event timeline, on-chain asset info |
| Participants | Inline CRUD table for supply chain participants |
| Verify | Public verification of batch custody chain |
ODATANO plugin config in package.json:
{
"cds": {
"requires": {
"db": {
"kind": "sqlite",
"credentials": { "url": "db.sqlite" }
},
"odatano-core": {
"network": "preview",
"backends": ["blockfrost"],
"blockfrostApiKey": "<your-preview-key>",
"txBuilders": ["buildooor"]
}
}
}
}| Script | Description |
|---|---|
npm run deploy |
Create/recreate SQLite DB with all tables + seed data |
npm start |
Start production server (cds-serve) |
npx cds watch |
Start dev server with live reload |

