Developer Guide
For contributors and consumers extending ODATANO.
Service Surface
Three CDS services, ~31 entities (some shared via re-projection), 37 actions:
| Service | Path | Entities | Actions |
|---|---|---|---|
CardanoODataService | /odata/v4/cardano-odata/ | 18 | 16 read |
CardanoTransactionService | /odata/v4/cardano-transaction/ | 8 | 12 unbound + 1 bound |
CardanoSignService | /odata/v4/cardano-sign/ | 5 | 8 |
Full list: API Reference.
Layered Architecture
HTTP → CDS Service handler (cardano-*.ts)
↓
App Context (server.ts → getCardanoIndexer / getCardanoClient / getHsmSigner)
↓
Indexer + Tx Builder + Signing modules
↓
CardanoClient (multi-backend orchestrator + circuit breaker + request coalescer)
↓
Backends: Ogmios (live, WebSocket) · Blockfrost (historical) · Koios (fallback)
↓
SQLite / HANA cache (temporal entities with TTL)
Plugin vs Standalone
ODATANO ships as the CAP plugin @odatano/core. CAP detects cds-plugin.js at the package root and:
- Registers the
odatano-coreservice kind (src/plugin.ts). - Hooks
cds.on('served')→initialize()→srv/server.ts initializeFromConfig(). - Merges
db/schema.cdsandsrv/*.cdsinto the consumer’s CSN. - Both modes share
loadConfigFromEnv(). Priority:cds.requires.odatano-core.<key>>process.env.<KEY>> default.
Init is guarded against double-execution: when running as a plugin, server.ts’s own served hook sees appContext already set and skips. Tests opt out via SKIP_AUTO_INIT=true and use createTestContext().
For full key reference: Configuration.
Public API (@odatano/core)
import { initialize, shutdown, getStatus,
getCardanoClient, getCardanoIndexer, getCardanoTxBuilder,
loadConfigFromEnv, loadHsmConfigFromEnv, getHsmSigner,
parseTransaction } from '@odatano/core';
import type { CardanoClientConfig, Network, BackendName,
HsmConfig, HsmSignResult,
ParsedTransaction, ParsedInput, ParsedOutput,
ParsedAsset, ParsedWitnesses } from '@odatano/core';
Handler Pattern
Validate inputs before wrapping in handleRequest():
this.on('GetTransactionByHash', async (req) => {
const hash = req.data?.hash;
if (!hash) return rejectMissing(req, 'Transactions', 'hash');
if (!isTxHash(hash)) return rejectInvalid(req, 'Transactions', 'Invalid hash', 'hash');
return handleRequest(req, async (db) => {
const cached = await db.run(SELECT.one.from(Transactions).where({ hash }));
return cached ?? await getCardanoIndexer().indexTransaction(db, hash);
});
});
handleRequest opens a cds.tx, normalizes thrown errors via the error mapper, and surfaces ODATANO_* codes. Backend-level calls additionally wrap in handleBackendRequest() so backend errors are pre-classified.
Validators & Errors
13 input validators in srv/utils/validators.ts - full list at Security › Input Validation.
14 error classes in srv/utils/errors.ts - full hierarchy + normalization rules at Error Handling.
Quick Setup
git clone https://github.com/ODATANO/ODATANO
cd ODATANO
npm ci
cp .env.example .env # set BLOCKFROST_API_KEY, NETWORK
npm run cds:watch # dev (ts-node, hot reload)
npm test # 1285 tests
npm start runs the compiled production build.
Building & Publishing the Plugin
npm run build:plugin # tsc -p tsconfig.build.json (outDir: ".")
npm pack --dry-run
npm publish --access public
tsconfig.build.json compiles in-place so CAP’s @impl: './cardano-service' resolves to <package-root>/srv/cardano-service.js when installed from node_modules.
Testing
import cds from '@sap/cds';
beforeAll(async () => {
const srv = await cds.test('serve', '--in-memory');
({ GET, POST } = srv);
});
test('GetTransactionByHash returns 400 on invalid hash', async () => {
await expect(POST('/odata/v4/cardano-odata/GetTransactionByHash', { hash: 'invalid' }))
.rejects.toMatchObject({ response: { status: 400 } });
});
For test contexts that skip CAP bootstrap, use SKIP_AUTO_INIT=true + createTestContext(['koios'], 'csl').
Deployment
- Docker (standalone) → Docker Deployment
- SAP BTP / Cloud Foundry → Production Deployment