Developer Guide

For contributors and consumers extending ODATANO.

Service Surface

Three CDS services, ~31 entities (some shared via re-projection), 37 actions:

ServicePathEntitiesActions
CardanoODataService/odata/v4/cardano-odata/1816 read
CardanoTransactionService/odata/v4/cardano-transaction/812 unbound + 1 bound
CardanoSignService/odata/v4/cardano-sign/58

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:

  1. Registers the odatano-core service kind (src/plugin.ts).
  2. Hooks cds.on('served')initialize()srv/server.ts initializeFromConfig().
  3. Merges db/schema.cds and srv/*.cds into the consumer’s CSN.
  4. 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