@odatano/core
A CAP plugin that serves the Cardano blockchain as OData V4, giving enterprise-shaped access to a public chain, funded through Cardano Catalyst.
Facts
Where it sits
Who is behind it
In the atlas: maintains 4@ODATANOmaintainerMaxContributes to this
In the atlas: contributes to 4@maxalexweber11 contributor
Before choosing this
Conditions
LicenceSAP Developer License Agreement 3.2 CAP§ 1, 2, 3A cds-plugin is used with SAP CAP materials, or an artifact is explicitly identified as a CAP plugin or application.
- § 1 License. Productive customer applications are limited to SAP BTP or another SAP-licensed platform, and mass extraction to a non-SAP product is restricted except where solely required for SAP interoperability.
- § 2 Intellectual Property. The agreement retains SAP's rights in the covered materials and limits making APIs, tools or software available to third parties, while preserving ownership of customer applications subject to SAP's underlying rights.
- § 3 Artificial Intelligence Training. The agreement prohibits use of the covered materials and customer applications for training artificial-intelligence models or systems.
What it declares
Every artifact
- Type
- CDS plugin
- Origin
- Independently maintained
As a CDS plugin
- Declared for
- >=10
- Distributed through
- npm
Connections
Exposing data and operations through OData
MCP server exposing the ODATANO Cardano-to-OData bridge as tools
Download
Install
npm add @odatano/coreDerived from the package name. No recipe recorded; the README has the configuration.
README
quoted from ODATANO/ODATANO under Apache-2.0, read
Enterprise OData Services for the Cardano Blockchain
Funded by Cardano Catalyst Fund 14 all four milestones completed and approved (April 2026).
ODATANO is an SAP CAP–based service that exposes the Cardano blockchain via a standardized OData V4 interface, enabling enterprise-grade read and write access with native transaction building.
Quick Start
As CAP Plugin (recommended)
Add ODATANO to any existing SAP CAP project:
npm install @odatano/core @cap-js/sqlite
{
"cds": {
"requires": {
"db": { "kind": "sqlite" },
"odatano-core": {
"network": "preview",
"backends": ["blockfrost"]
}
}
}
}
Put your API key in a .env file in the project root (loaded automatically by cds serve / cds watch) — don't commit it to package.json:
# .env
BLOCKFROST_API_KEY=preview_your_api_key_here
cds deploy --to sqlite
cds serve
Config in
cds.requires.odatano-coretakes precedence over environment variables; ablockfrostApiKeyset there would override the.envvalue.
Self-hosted Blockfrost-compatible node? (Dolos MiniBF, Demeter Self-Hosted) Set
blockfrostCustomBackendto the node's base URL (e.g.http://localhost:3010/api/v0). When set,blockfrostApiKeybecomes optional. See the Backend Configuration Guide.
Standalone
Run ODATANO as a self-contained service (no host CAP project needed):
git clone https://github.com/ODATANO/ODATANO && cd ODATANO
npm ci
cp .env.example .env # set BLOCKFROST_API_KEY, NETWORK, BACKENDS
cds deploy --to sqlite
cds serve
Or with Docker — this brings up a local cardano-node + Ogmios + ODATANO stack (Preview), with the service pre-wired to the local Ogmios backend:
docker compose up -d # full stack
docker compose up -d cardano-node ogmios # just the chain backend (pre-sync)
docker compose logs -f ogmios # watch sync (/health: 202 syncing, 200 near tip)
First sync from genesis takes hours; the node DB persists in a Docker volume so restarts resume. Until Ogmios catches up, historical queries fall back to Blockfrost (set
BLOCKFROST_API_KEY).
Service available at http://localhost:4004. See the Quick Start Guide for the local Ogmios stack and full details.
Dev Mode: Mocked Authentication
Heads-up.
cds serveuses CAP's mocked auth:@requires: 'authenticated-user'accepts any Basic-Auth header against a mock user (alice,bob, …) — passwords are not checked. Anonymous requests get 401, but anyone reaching the port can authenticate.Production needs the
[production]profile (NODE_ENV=production), which switches toauth: xsuaa. Don't expose a dev-mode instance. See the Security Guide.The Docker image runs
NODE_ENV=productionwith HTTP basic auth via@odatano/cap-auth(ODATANO_HTTP_PASSWORDrequired, userODATANO_HTTP_USERdefaultodatano, rolesODATANO_HTTP_ROLESdefaultAdmin; 20 failed attempts per 15 min per client address and user, then 429).ODATANO_AUTH=dummydisables authentication for local testing only.getLiveness()andVerifyDataSignaturestay anonymous.Database of the image:
ODATANO_DB_URL=postgres://user:pw@host:5432/dbselects PostgreSQL (schema deployed on every boot); otherwise SQLite atODATANO_DB_PATH(default/data/db.sqlite).docker compose run --rm --no-deps odatano migrate --from /data/db.sqlitecopies an existing SQLite file into a freshly deployed PostgreSQL.
Services
| Service | Path | Purpose |
|---|---|---|
| CardanoODataService | /odata/v4/cardano/ | Read blockchain data (29 entities, 19 actions) |
| CardanoTransactionService | /odata/v4/cardano-transaction/ | Build & submit transactions (13 actions) |
| CardanoSignService | /odata/v4/cardano-sign/ | External signing + HSM (9 actions) |
| CardanoIndexerService | /odata/v4/cardano-indexer/ | Chain crawler / pre-sync control (v2.0, off by default) |
| CardanoWorkerService | /odata/v4/cardano-worker/ | Asynchronous wallet jobs (v2.0, off by default) |
| CardanoAgentService | /odata/v4/cardano-agent/ | Agent grants: scoped bearer tokens for agents (v2.0, off by default) |
Both v2.0 services also publish CAP events, so a consumer can subscribe instead of polling — in-process, no broker required:
(await cds.connect.to('CardanoWorkerService')).on('jobConfirmed', ({ data }) => …)
(await cds.connect.to('CardanoIndexerService')).on('blockIndexed', ({ data }) => …)
Agent grants (v2.0, off by default)
Hand an agent a scoped, budgeted token instead of a user.
AGENT_GRANTS_ENABLED=true # or cds.requires.odatano-core.agentGrants.enabled
AGENT_GRANTS_DELEGATE=mocked # CAP auth kind that keeps authenticating non-token requests (default: configured kind)
AGENT_GRANT_ADMIN_RATE_LIMIT=10 # grant administration calls per principal per hour (or agentGrants.adminRateLimit)
AGENT_TOKEN_CACHE_MS=10000 # a resolved token is reused this long before the row is read again; 0 = off (or agentGrants.tokenCacheMs)
- An Admin issues a grant (
POST /odata/v4/cardano-agent/CreateAgentGrant): an allow list of actions, optionally a workerwalletId(required forSubmitWalletJob/CancelJob), allowed job kinds, a daily budget and an expiry. The response carries the tokenodat_…once; the server stores only its SHA-256. - The agent sends
x-agent-token: odat_…on any of the six service paths. It runs as the principalagent:<grantId>with the single roleagent-grant, never as the operator: every@requires: 'Admin'surface (pause/resume, HSM signing, grant administration) refuses it by construction. Reads and compute-only actions are always allowed; allow-listed actions each cost one budget unit per UTC day (a build is the service an agent buys); everything else is 403. Wallet jobs are pinned to the grant's wallet and visible only to the grant that queued them. GET /odata/v4/cardano-agent/GetGrantStatus()tells the agent what it may do and how much budget is left;RevokeAgentGrantturns the token into an unknown token immediately.- Lifecycle (Admin; the same semantics as the gateway's peer services, so one client drives
them all):
UpdateAgentGrantchanges label, allow list, job kinds, daily budget or expiry (absent = untouched, explicitnull= cleared; the wallet binding is immutable, 409GRANT_REVOKEDon a revoked grant);RotateAgentGrantTokenreturns a fresh token once, the old one is unknown from the next request;GetGrantUsage(grantId, since, until)lists admitted calls per service and action over up to 366 days (an Admin for any grant, a token for its own). All administration calls share the per-principal hourly rate limit above.
Grantable: Build*, SetCollateral, CreateSigningRequest, VerifySignature, Submit*,
CheckSubmissionStatus, SubmitWalletJob, CancelJob. Never: SignWithHsm*, Pause*/Resume*,
pauseCrawler/resumeCrawler, grant administration.
Programmatic seams for other packages (@odatano/x402 sells grants against a 402 payment):
issueAgentGrant(), updateAgentGrant(), rotateAgentGrantToken(), revokeAgentGrantById(),
getGrantUsage(), registerTransportLane() from @odatano/core.
Liveness probe
GET /odata/v4/cardano-indexer/getLiveness() answers 200 { status: "alive", timestamp, uptime, version, network } without credentials (@requires: 'any') as long as the process runs — no
backend or DB probe, no secrets. The Docker HEALTHCHECK, compose and upstream probes use it
instead of the service document, so a wrong operator password no longer reads as "ODATANO down".
Readiness (crawler, worker, backends) stays authenticated: getStatus(), GetWorkerStatus(),
GetNetworkInformation.
Requirements
- Node.js >= 22.5 and @sap/cds >= 10 (peer dependency). CAP 9 hosts cannot load
@odatano/core@2. - Upgrading a consumer from 1.x: run
cds deploy. 2.0 adds six tables (CardanoSyncState,CardanoReorgLog,CardanoWorkerWallets,CardanoWalletJobs,CardanoAgentGrants,CardanoAgentGrantUsage) plus adedupKeycolumn; without the redeploy the new services answerno such tablewhile the old ones keep working.
Documentation
| Guide | Description |
|---|---|
| Quick Start | Get running in 5 minutes |
| User Guide | API usage, entities, and examples |
| Developer Guide | Architecture and development |
| Transaction Workflow | Build → Sign → Submit flow |
| Security Guide | Authentication, signing security, HSM |
| Docker Deployment | Container deployment |
License
Apache License 2.0 - see LICENSE
Support
- Issues: GitHub Issues
- Project Lead: Max Weber
- Contact: info@odatano.dev
History
Features 3, Changed 4
Notes
Schema change: referenceScriptSize + referenceScriptType on TransactionOutputs.utxo,
TransactionInputs.utxoData, AddressUTxOs.utxodata and LedgerUTxOs.utxo;
minFeeRefScriptCostPerByte on LedgerProtocolParameters. Run cds deploy on existing databases.
Added
utxo_referenceScriptSize(bytes as the ledger charges them) andutxo_referenceScriptType(native|plutusV1|plutusV2|plutusV3) next toutxo_referenceScriptHashon every UTxO-shaped entity. Filled by Ogmios (script bytes) and Koios (reference_script.size/.type); Blockfrost delivers the hash only, so both stay null there.LedgerProtocolParameters.minFeeRefScriptCostPerBytefrom Ogmios (minFeeReferenceScripts.base), Koios and Blockfrost; the transaction builder uses it instead of the library default.importUtxoSet { source: "scripts", filePath? }: fills the reference-script columns ofLedgerUTxOsrows an earlier import left empty, from the same cardano-cli dump or, withoutfilePath, from Ogmios for the unspent rows that hold a hash but no size. Crawler paused; the set staysactive.
Changed
- The cardano-cli dump import reads the
referenceScriptTextEnvelope: hash, size and type are written with the row instead of null. Native scripts given as JSON are hashed as well. - Coin selection offers UTxOs carrying a reference script only when the other sender UTxOs run short (before: never, unless forced); their script bytes are then priced into the fee.
- Reference-script fee as the ledger computes it: scripts on spent inputs count, the price per byte rises by 1.2
for every 25 600 bytes, exact rationals. Buildooor 0.2.9 priced reference inputs only, linearly; its
calcMinFeeis replaced at load (srv/blockchain/transaction-building/buildooor-fee-patch.ts). A UTxO known by hash and size only is priced through a placeholder of that size; one known by hash alone is logged as unpriceable. backfillTransactionsfills the two new input columns as well.
Changed 2
Notes
No schema change.
Changed
Dockerfilebuilds fromnode:22.23.4-alpine(Node security release of 2026-10-21: Brotli decoder out-of-bounds write, TLS hostname checks, keep-alive response desync, dependency updates). The base image is pinned to the exact Node version instead of the floatingnode:22-alpine.follow-redirects1.16.1 in the lockfile (transitive via axios; closes two credential-header advisories).
Deprecated 1, Changed 1
Notes
No schema change.
Changed
CardanoODataServiceis served at/odata/v4/cardano(@pathinsrv/cardano-service.cds); the service name, the model and the handlers are unchanged. README, docs, the Postman collection, the wallet app and the scripts name the new path.
Deprecated
/odata/v4/cardano-odata: still answered, rewritten to/odata/v4/cardanobefore the services (srv/utils/legacy-service-paths.ts, registered by the standalone server and the plugin) and logged once per client address as deprecated. The old path goes with the 2.0.0 release.
Features 1, Fixes 2
Notes
No schema change.
Fixed
- Reward-account registration check (
BuildPlutusTransactionwithwithdrawalsJson): the node'srewardAccountSummarieslists only credentials delegated to a pool or a DRep, so a registered credential that never delegated (a withdraw-zero validator) was "not found" there and rejected as not registered since rc.37. "Not found" on the node now passes the question to Blockfrost and Koios, which answer from the certificate history; the first provider that knows the account decides, and the account is not registered when no provider knows it. With Ogmios alone the build goes on and the node decides at submit. - Blockfrost
getAccount:activeis Blockfrost's delegation state, the registration state isregistered. A registered account without delegation reportedAccounts.active = falseand failed the registration check.@blockfrost/blockfrost-js6.2 carries the field.
Added
test/integration/reward-accounts.test.ts: registered key, registered undelegated script and never registered script credentials on Ogmios, Blockfrost and Koios, alone and together, withBuildPlutusTransactionwithdraw-zero builds.scripts/testing/withdraw-zero-preview.ts: registers a script credential and withdraws zero from it end to end on preview.
- Reward-account registration check (
Fixes 1
Notes
No schema change.
Fixed
- Ogmios
getAccountfor a script stake address (stake17…,stake_test17…) asksrewardAccountSummariesunderscripts; underkeysa script credential is never found. Before,BuildPlutusTransactionwithwithdrawalsJsonfrom a script rejected every withdrawal as "not registered" (since rc.37), andGetAccountByStakeAddressanswered 404 when Ogmios was the only backend.
- Ogmios
Fixes 2
Notes
No schema change.
Fixed
BuildPlutusTransactionwithwithdrawalsJson: the reward-account registration check asks the node first and takes the first answer; "not found" from Ogmios is "not registered" (400). Before, that "not found" fell through to the providers, and a Koios timeout there failed the build with 503.- Request log (JSON format):
x-agent-tokenand every header whose name containstoken,key,secretorproject_idis written as***, in addition to CAP's defaults (authorization,cookie,cert,ssl). Set viacds.log.mask_headers; a consumer's own list replaces it.
Fixes 2, Docs 1
Notes
No schema change.
Fixed
AddressUTxOs,AddressAssets: a collection read that names the address, as$filtertermaddress_address eq '…'(alsoaddress/address,address_address in (…)) or as navigationAddresses('…')/utxos, indexes the address on a cache miss before the query runs. It returned[]for every address no keyed read or action had indexed on that instance, or whose rows had expired. A read without an address still returns only what is cached. An invalid address in the filter, or more than 10 addresses in one read, is a 400.GetUTxOsByAddress: an address whose row is valid but holds no UTxO is no longer fetched again on every call; each call wrote a newAddressesslice.
Documentation
AddressUTxOs/AddressAssets: how the child sets fill and expire;containson an inline datum matches byte strings only up to 64 bytes (Plutus data CBOR chunks longer ones).
Changed 1, Fixes 1
Notes
No schema change.
Changed
BuildPlutusTransaction,BuildMintTransaction,BuildPlutusSpendTransaction: a UTxO named inforceInputsJsonis the collateral when no other sender UTxO of at least 5 ADA qualifies. It must belong to the sender, be ADA-only, carry no reference script and hold at least 5 ADA; the excess above 5 ADA comes back through the collateral return. A wallet with a single UTxO builds without aSetCollateraltransaction first. Forced UTxOs of other addresses and forced UTxOs with assets are never collateral.
Fixed
- Agent tokens:
GetTransactionConfirmationsis always allowed like the other chain reads; it answered 403 "not allowed for this agent grant". A unit test now fails when an action or function of the service models is neither always allowed, allow-listable nor operator only.
Features 1
Notes
No schema change.
Added
parseTransaction/ParseTransactionCbor:isValid, the validity flag of the transaction.falsemeans the transaction is flagged as failing its scripts; on chain it spends only the collateral and creates no outputs.
Features 1, Changed 5, Fixes 8
Notes
No schema change.
Added
BuildPlutusTransaction:scriptInputsJsonis optional (0..16 entries), so a mint by reference script, a withdraw-zero or a certificate builds alone. Collateral is added only when a script runs.
Changed
SubmitTransaction:signedTxCbormay be the witness set a CIP-30 wallet returns fromsignTx; it is added to the build's transaction before submitting, asVerifySignatureandSubmitVerifiedTransactionalready do.- Wallet app: the CIP-30 sign result is named
witnessSetCbor. - Plutus builds: when the other sender UTxOs cannot pay the outputs, fee and change, the collateral UTxO is spent as an input as well instead of the build failing on the change output.
- A change output below min-ADA is reported as
Insufficient lovelacenaming the funding, not as an output the caller should raise; this covers all build actions. - Witness sets from CIP-30 wallets: bootstrap witnesses (Byron addresses) are taken over as well, and a witness set without any signature is a 400 before submit.
Fixed
-
Koios
getAccount: maps the/account_infoschema (status,total_balance,rewards,withdrawals,rewards_available,delegated_pool,delegated_drep). A registered account was reported inactive, so every withdrawal build over Koios was refused, and balance, rewards and delegation were empty. -
Koios
getEpoch: block count, output and fees come fromblk_count,out_sumandfees; they were empty. -
Koios
getPool:activeSizecomes fromsigma;liveSizeis null, since/pool_infohas no live share (PoolData.liveSizeisnumber | null). -
Koios
getAddress:isScriptfromscript_address, the type decoded from the address; both were empty. -
Addresses.typeis the CIP-19 kind (base,pointer,enterprise,reward,byron,unknown) on every backend; Blockfrost wroteshelley. Stored rows take the new value on their next refresh. -
Ogmios
getPool:liveSizeis null instead of 0; one pool's stake gives no live share. -
parseTransaction:cborSizereads the output bytes as received instead of relying on the library to return them; a re-encoding can be shorter. -
Transaction workflow guide: the CIP-30 example names the witness set and shows how to submit it.
Features 4, Fixes 1
Notes
No schema change.
Added
parseTransaction/ParseTransactionCbor:withdrawals,certificates(index + ledger type),referenceInputs,votingProcedures(vote count),proposalProcedures,treasuryDonation,currentTreasuryValue,networkId, andcborSizeper output (bytes as they stand in the transaction).@odatano/coreexportsverifyTxWitnesses(signedTxCbor): every vkey signature checked against the body hash, returnsvalid,txBodyHash,signerKeyHashes,errors; pure, noinitialize().@odatano/coreexportsposixToSlot(network, posixMs)andslotToPosixMs(network, slot); exact from the Shelley start on,posixToSlotthrows aRangeErrorfor an earlier time.GetTransactionConfirmations(txHash)andgetTransactionConfirmations(txHash): blocks on top of a transaction, its own block included, 0 while it is not on chain. A transaction in a crawled block counts against the synced crawler's tip; any other is looked up at the backends together with their tip.
Fixed
parseTransaction: voting procedures, proposals, treasury donation and treasury value are read from the transaction bytes; the ledger decoder drops body fields 19 to 22.
Fixes 4
Notes
No schema change.
Fixed
BuildPlutusTransaction:scriptInputs[].datumCborfor a UTxO locked by datum hash reaches the witness set byte for byte, andscriptDataHashcovers those bytes; a re-encoded datum was missing to the ledger.BuildPlutusTransaction/BuildPlutusSpendTransaction: a witness datum that does not hash to the UTxO's datum hash is a 400 naming both hashes; fordatumJsonthe message points todatumCbor.initialize()without a database: the re-drive of interrupted submissions is skipped instead of loggingdb.begin is not a function.- Transaction builds without a database ask the node for the sender's UTxOs without a "Local UTxO lookup failed" warning.
Features 3, Fixes 2
Notes
No schema change.
Added
BuildSimpleAdaTransaction/BuildMultiAssetTransaction(andTxBuildRequest):outputDatumCbor, the recipient's inline datum as PlutusData CBOR hex, written byte for byte; excludesoutputDatumJson.BuildSimpleAdaTransaction/BuildMultiAssetTransaction:ensureMinAdaraiseslovelaceAmountto the recipient output's min-ADA; without it a lower amount is a 400 with the amount needed.@odatano/coreexportsapplyScriptParameters(scriptHex, params)andplutusScriptHash(scriptHex, 'plutusV2' | 'plutusV3'); pure functions, noinitialize().
Fixed
BuildPlutusTransaction:inlineDatumCborreaches the output byte for byte; definite lists and maps were re-encoded as indefinite, which changed the datum hash. The fee and min-ADA cover the given bytes.parseTransaction:inlineDatumHexis the datum as it stands in the transaction, not a re-encoding.
Security 1, Features 3, Fixes 6
Notes
No schema change.
Added
BuildPlutusTransaction:__REF_IDX:<txHash>#<n>__in any redeemer or datum JSON resolves to the index of that reference input in the sorted reference inputs (reference-script UTxOs added by the builder included).BuildPlutusTransaction:__WDRL_IDX:<credential hash>__resolves to the index of that credential's withdrawal in ledger order (script credentials first, then by hash).BuildPlutusTransaction: amintActionsJsonentry takesreferenceScript: {txHash, outputIndex}instead ofmintingPolicyScript; the asset unit must be under the policy of that script.
Fixed
-
BuildPlutusTransaction: a key-credential withdrawal that sorts before a scripted one by hash is refused with 400; the scripted withdrawal would otherwise run with the wrong redeemer index. -
BuildPlutusTransaction: an inline minting policy that a reference input already carries runs from the reference input. -
EvaluateTransaction: an Ogmios script failure (code 3010) is a 400ODATANO_SCRIPT_VALIDATION_FAILUREwith each failing validator (purpose, index,validationError,traces) instead of a 503; it no longer opens the Ogmios circuit breaker. -
Plutus builds: an Ogmios script failure is reported to the caller instead of falling back to local execution units ("no Ogmios evaluation is available").
-
EvaluateTransaction: other Ogmios evaluation failures (3000-3099) are a 400 with their reason; 3003 (node still syncing) stays 503. -
odatanobackend: a remote script failure stays a script validation error.
Security
proxy-addr2.0.8 andhttp-cache-semantics4.3.0 (lockfile).
Changed 2, Fixes 2, Other 2
Notes
No schema change.
Changed
cds.requires.odatano-core.backendsaccepts a comma-separated string as well as an array (same form asBACKENDS).cds.requires.odatano-core: every section (hsm,crawler,walletWorker) is typed; a key the loader does not know is a compile error.
Fixed
- Koios
/pool_inforows with nullvrf_key_hash,block_countorreward_addr(retired pools) map to''/0instead of null inPoolData. - Koios pagination: an anchor block without a height reports the hash and cause instead of a generic not-found.
Internal
- Lint runs at zero warnings; unused
eslint-disabledirectives are errors;local_docs/is ignored. - Koios retry helpers are generic with row interfaces per endpoint;
@UI.Hiddenannotations unescaped; test file counts in the docs corrected (87 = 69 unit + 18 integration).
Security 1, Features 4, Fixes 2
Notes
No schema change.
Added
BuildPlutusTransaction:withdrawalsJson(reward-account withdrawals with a staking script inline or by reference, redeemer under the Reward purpose,lovelace0 for withdraw-zero oracles; without a script the stake key signs; unregistered reward accounts are refused before the build).BuildPlutusTransaction:certificatesJson(stake registration and deregistration of key and script credentials; witness-free legacy registration, deposit form with the staking script under the Certifying purpose; a credential registered in the transaction is withdrawable in it; deposits balanced).BuildPlutusTransaction:forceInputsJsontakes UTxOs of other key addresses; the owner's payment key hash must be inrequiredSignersJson, every unit they bring must be spent byoutputsJson, never collateral.protectInputsJsonon all four build actions: UTxOs left out of coin selection and collateral.
Fixed
- Mainnet Shelley start in
GENESIS_INFOS_BY_NETWORKwas 2020-08-03 instead of 2020-07-29T21:44:51Z: every mainnet value derived from a slot was five days late (Blocks.time,Transactions.blockTime,AssetHistory.blockTimefrom the crawl, epochstartTime/endTime,validityStartMs/validityEndMsof built transactions). Preprod and preview were correct. - Mainnet rows crawled before this release carry the offset: recompute
time/blockTimefromslot.
Security
axios1.20.0,fast-uri3.1.8 (lockfile).
Security 1, Fixes 1
Notes
No schema change.
Fixed
GetUTxOsByAddressandGetAssetsByAddress: the first read of an address not indexed yet returns its UTxOs / assets (was[], the rows only on the second call).
Security
brace-expansion5.0.12 / 1.1.21 (lockfile).
Features 4, Changed 4, Fixes 6
Notes
Several script inputs per build, redeemers of crawled transactions, the same rows from every source; run
cds deploy(newTransactionBuildRedeemers,TransactionRedeemers; asset sumsDecimal(38,0)).Added
BuildPlutusTransaction: several script inputs (inlinevalidatorScriptorreferenceScript), orderedoutputsJson, mints, reference/forced inputs, required signers, validity bounds; PlutusData also as CBOR (redeemerCbor,datumCbor,inlineDatumCbor).TransactionBuildRedeemers(execution units per build) andTransactionRedeemers(crawled redeemers from Ogmios chain-sync, with redeemed input or policy);redeemersin the backend service.backfillTransactions(fromSlot, toSlot): fills input outpoints, datums, addresses and redeemers of crawled transactions and rewrites rows stored in another shape;getStatus().transactionBackfill.importUtxoSet { source: "aggregates" }: rebuilds only the address sums.
Changed
- Address, account, build and
SetCollateralreads come from the crawled UTxO set, checked at the node by output reference; Ogmios address scans run on a second connection. - Every source yields the same transaction rows (
ledgerView): inputs in ledger order, phase-2 partition applied, no unproduced collateral return. - Crawler and Postgres: lease renewed with the cursor update, rows sent inline instead of
COPY. - PlutusData JSON may nest 64 levels.
Fixed
- Coin selection skips reference inputs and UTxOs carrying a reference script; collateral and
SetCollateraluse one rule (ADA-only, no reference script). - Builds right after a submit no longer reuse its inputs (pending transactions of this process).
- Min-ADA shortfall is a 400; reference scripts on outputs are
[language, bytes]; datum-hash outputs build. - Submit: a node rejection is final and carries the node's reason (Ogmios, Koios).
- Missing input outpoints are
null(was""/0); stored transactions carryspendsCollaterals. GetNextBlockson Postgres and orphaned cursor as 409; Koios asset history, protocol-parameter epoch; UTxO set sums overflow and import lock.
Features 3, Changed 1
Notes
ODATANO can run on another ODATANO (the public API) instead of Blockfrost or Koios; no schema change.
Added
- Backend
odatano(ODATANO_URL,ODATANO_API_KEY;odatanoUrl,odatanoApiKey): another ODATANO instance as backend, by default the public API of the network (mainnet needs the URL). Reads, crawler pagination, pool/DRep enumeration, credential UTxOs, script evaluation and submit go through it. CardanoBackendService(/odata/v4/cardano-backend/): the provider shape for such callers as JSON. Stored blocks and transactions come from the index (without the phase-2 flag), the rest from the instance's backends and crawled data;GetNextBlocksreports an orphaned cursor withCHAIN_POINT_MISMATCH:. Agent tokens may call the reads andEvaluateTransaction;SubmitTransactionneeds the allow list.npm publishrefuses an uncommitted tree or a HEAD without thev<version>tag (prepublishOnly).
Changed
- Script evaluation for Plutus builds uses any evaluating backend (Ogmios, or
odatano);GetUTxOsByCredentialuses Koios orodatano.
- Backend
Features 3
Notes
Published from an uncommitted tree on top of rc.22 (no tag); deprecated in favour of rc.24, which contains it.
Added
crawler.authoritative(CRAWLER_AUTHORITATIVE, off): with a crawl that started at or before the first Shelley slot and runs at the tip, a block, transaction, metadata or asset lookup the database misses is answered 404 (not in the crawled chain up to slot …) instead of asking a provider. Without the knob the same applies when no configured backend can serve the lookup (Ogmios only: 404 instead of 503).- Assets from the crawled chain in that mode:
Assets/GetAssetInfotake supply (mints − burns), mint count, first mint and the CIP-25 payload (label 721) of the latest mint fromAssetHistoryandTransactionMetadata; registry fields stay empty.GetAssetHistoryreads the crawled rows. GetMetadataByTxHashfor a crawled transaction without metadata answers an empty list.
Features 5, Changed 10
Notes
Ogmios 7 support, and epoch snapshots plus address reads without Koios or Blockfrost; additive schema change, no migration.
Changed
- Crawler epoch snapshots (
crawler.epochSnapshots) read the node's ledger state through Ogmios at the first crawled block of each epoch (stakePools,stakePoolsPerformances,delegateRepresentatives,treasuryAndReserves) while that block is within the node's last k blocks, so an epoch is recorded during a catch-up close to the tip too. Koios remains the fallback without Ogmios, still only in the tip's epoch. Ogmios snapshots leave the livePools/Drepsrows untouched;liveDelegatorsandblocksEpochstay empty,blocksMintedis counted from crawled blocks when the crawl started at or before Shelley. Active stake of epoch E is the live stake the snapshot of E-1 recorded; empty without that snapshot. GetUTxOsByCredentialis served from the crawled UTxO set while it is active and the crawler is at the tip: the credential's addresses come fromLedgerAddresses, their outputs from Ogmios at the tip (or the unspentLedgerUTxOswithout Ogmios). Koios stays the fallback.- A reorg removes the epoch snapshots taken at rolled-back blocks; the epoch is taken again.
PoolData.liveDelegatorsmay be null; OgmiosgetPoolreports null instead of 0.- Addresses from the crawled UTxO set while it is active and the crawler is at the tip:
Addresses,GetAddressByBech32,GetAssetsByAddressandGetUTxOsByAddresstake the outputs from Ogmios at the tip (or the unspentLedgerUTxOs), type and stake address decoded from the address; an address the set has never seen is an empty address. GetLatestTransactionsByAddress/AddressTransactionsfrom the crawled UTxO set: received (LedgerUTxOs.txHash) and spent (spentTxHash) outputs since the anchor, net amounts per transaction. Complete for addresses first seen after the anchor; older addresses ask a provider when the crawled history is shorter thanlimit, and keep the crawled rows when no provider has address history.Pools.activeStake/activeSizefrom the Ogmios snapshot of the running epoch when the backend reports none;Accounts.withdrawalsSumfrom crawled withdrawals when certificates are crawled since Shelley and the backend reports none.- Chain-sync socket closed by the server: after delivered blocks the crawler reopens chain-sync
at the cursor; without a delivered block it fetches the next block through the pagination
backend and hands back to chain-sync (
lastErrornames the block). The crawler no longer stops at a block the Ogmios server cannot decode. getAddress,getAddressUtxosandgetDrepask Ogmios first when it is configured (UTxOs at the tip). Ogmios reports the addresstypeasbase/enterprise/pointer/reward/byron, an address without UTxOs as an empty address instead of 404, and no DRep last activity; a DRep the ledger no longer lists is looked up at the providers.@cardano-ogmios/client7.0.0; the Docker setup and CI usecardanosolutions/ogmios:v7.0.0. Ogmios 6.x cannot decode block headers announcing protocol version 12.
Added
EpochLedgerSnapshots: one row per snapshotted epoch with treasury, reserves, total supply, live and active stake, pool and DRep counts, always-abstain / always-no-confidence stake.PoolEpochSnapshots/DrepEpochSnapshots:source(ogmios|koios) andsnapshotHash; DRep rows alsodeposit,expiresEpoch,delegatorCount.LedgerAddresses.paymentCredential(+ index), filled on write and, for existing rows and after an import, in the background.getStatus().epochSnapshots: enabled, next source, newest epoch with its source and slot.- Ogmios
getAddress: type, script flag and stake address decoded from the address, UTxOs and balance from the ledger at the tip.
- Crawler epoch snapshots (
Breaking 1, Features 2, Changed 6, Fixes 5
Notes
Input/output tables are keyed by chain position; existing databases need
migrate-txseqonce before the first start.Breaking
TransactionInputs,TransactionOutputs,TransactionInputAssetsandTransactionOutputAssetsare keyed byTransactions.txSeq(slot * 65536 + txIndex, new column) instead of the transaction hash. Inserts land at the end of the key index instead of at random positions, which keeps the crawl rate constant as the tables grow and shrinks their indexes by about half.- The rows no longer carry
tx_hash(input_tx_hash/output_tx_hashon the asset rows). The transaction is reached through thetxnavigation (tx/hash,$expand=tx), and a transaction's rows throughTransactions('<hash>')/inputsand/outputs. Keyed reads useTransactionInputs(txSeq=…,inputIndex=…). - An existing database needs
migrate-txseq(image mode) orscripts/migrate-txseq.mjsonce before the first start of this version; the additive schema deployment cannot change the keys of tables that hold rows.
- The rows no longer carry
Changed
Blocks.slotLeaderis the bech32 pool id of the block producer on every backend. Koios (/block_infopool) and Ogmios chain-sync (blake2b-224 of the issuer key) delivered the VRF key and the issuer key before.scripts/migrate-slot-leader.mjsturns the old values of existing rows into an SQL update (issuer keys derived, VRF keys matched against an OgmiosstakePoolsresponse).- Plutus spends,
forceInputsandreferenceInputsresolve output references outside the sender's UTxOs through the node ledger (OgmiosqueryLedgerState/utxo) when Ogmios is configured: one lookup proves existence and spendability. Without Ogmios, or when the lookup fails, the producing transaction is fetched as before. CheckSubmissionStatusand the wallet worker's recovery of an interrupted submit look up the transaction in the local index (crawled transactions) before asking a backend.- While the crawler runs at the tip,
Pools,AccountsandNetworkInformationtake values from crawled data over the backend's:Pools.blocksEpochcounts crawled blocks of the current epoch (crawl covering the whole epoch);Pools.blocksMintedall crawled blocks (crawl starting at or before Shelley). New index onBlocks(slotLeader, slot).Accounts.controlledAmount= UTxOs under the stake key (LedgerAccounts) + reward balance, with the crawled UTxO set active.NetworkInformation.circulatingSupply= total ofLedgerAddresses, with the crawled UTxO set active.
- Crawled epoch rows carry
blockCount,txCount,fees,firstBlockTimeandlastBlockTimesummed over the crawled blocks when the crawl started before the epoch. An Ogmios-only crawl no longer writes zero totals for the current epoch and now writes rows for past epochs too (bounds from the slots,output/activeStakeempty). getNetworkInformationasks the providers first and falls back to Ogmios.
Added
- Ogmios
getNetworkInformationfromqueryLedgerState/treasuryAndReserves: treasury, reserves, total supply (max − reserves); circulating, locked and stake totals stay'0'. Accounts.poolId/Accounts.drepIdassociations are filled from the backend's delegation.
Fixed
GetLatestBlockno longer stores the header-only ledger tip Ogmios answers with (size, fees and slot leader unknown); it returns the stored row of that hash when there is one instead of overwriting it. The crawl removes other rows at a crawled height that no transaction references, which also clears a rolled-back tip stored from Blockfrost or Koios.BackendInitErrornames the cause (Failed to initialize backend: ogmios (<reason>)), andAllBackendsFailedErrorlists why backends were skipped when none was called.- Ogmios
getPool:vrfKeyHashis filled (was always empty);blocksEpochis null instead of 0. - Ogmios
getAccount:poolIdanddrepIdare filled from the reward-account summary (were always null); predefined DReps map todrep_always_abstain/drep_always_no_confidence. - Ogmios UTxOs (
GetUTxOsByAddress, UTxO-set import) carry the reference-script hash inscriptRef(was always empty); chain-sync outputs carryreferenceScriptHash.
Features 2, Fixes 1
Notes
Certificates and withdrawals can be filled in for blocks crawled before
crawler.certificateswas on.Added
backfillCertificates(fromSlot, toSlot)onCardanoIndexerService(Admin): a second chain-sync stream (Ogmios) over the already crawled range writesTransactionCertificatesandTransactionWithdrawalsonly, keyed as the crawl writes them, so a repeat is idempotent.fromSlotdefaults to the crawl start,toSlotto the cursor;toSlotmay not pass the cursor. The stream intersects at the newest crawled block belowfromSlot(the crawl start point when there is none) and ends at the first block pasttoSlot. A block the index does not hold is skipped. The crawler may keep running; rows are written per 200 blocks. Runs detached;getStatus().certificateBackfillreports{status, fromSlot, toSlot, atSlot, blocks, certificates, withdrawals, startedAt, finishedAt, error}, process-local (noneafter a restart). One backfill per process at a time.srv/blockchain/crawler/certificate-backfill.ts(backfillCertificates,intersectionBefore); unit tests for the range, the skipped block, the end condition and a stream error.
Fixed
- Ogmios frame guard: every open chain-sync stream registers its own report callback and removes it on close. A second stream no longer replaces the crawler's callback, so an unparseable frame still halts the crawler's stream with an error.
Features 14
Notes
The crawl now carries certificates, withdrawals, input outpoints and, opt-in, a UTxO set of its own.
Added
-
crawler.utxoSet/CRAWLER_UTXO_SET(defaultfalse): the crawl maintains its own UTxO set in new non-temporal tablesLedgerUTxOs(+LedgerUTxOAssets),LedgerAddresses(+LedgerAddressAssets),LedgerAccounts, exposed read-only onCardanoODataService. Unspent =spentTxHash eq null; runningtotalLovelace/utxoCount/ asset balances per address,controlledAmountper stake key. Applied inside the block transaction. -
importUtxoSet(source, filePath, anchorSlot, anchorHash)onCardanoIndexerService(Admin): one-off anchor import,source: ogmiosat the crawler cursor orsource: filefrom acardano-cli query utxo --whole-utxodump (.json, or.ndjsonviajq -c 'to_entries[]'). Crawler must be paused; cursor must not be past the anchor. Runs detached;getStatus().utxoSetreports{enabled, status, anchorSlot, anchorHash, importedAt, error}. Anchor and status persist inCardanoSyncState.utxoSet*. -
Only blocks after the anchor are applied; a reorg after it drops what the rolled-back blocks created, reopens what they spent and recounts the touched addresses; a reorg before it marks the set
invalid(re-import). Without an active anchor the knob is inert and the crawler logs an error at start. -
Safety: the import holds the cursor lease for its duration and re-takes it inside every write transaction (crawler start and a second import are refused cluster-wide, a takeover aborts before the next commit and writes nothing further); the anchor is verified against the crawler cursor before the first apply (covers the configured start block);
CardanoSyncState.utxoAppliedSlottracks ledger progress, a reorg moves it back with the cursor and a cursor ahead of it invalidates the set at start; cardano-cli dumps are parsed losslessly (parseJsonLossless); the Ogmios whole-set query runs on its own WebSocket connection so live queries stay at the tip. -
Koios
/tx_infomapper now carriesvalid_contract,collateral_inputs,collateral_outputandreference_inputs(spendsCollaterals,isCollateral,isReference), so the collateral-fee, mint-delta and ledger paths are exact on Koios too. Certificates/withdrawals are requested from Koios only whilecrawler.certificatesis on (PaginatingBackend.configureCrawl). -
Import lease read-back no longer requires
desiredRunning(the cluster is paused during an import); a ledger invalidation decided inside a block transaction takes effect in memory only after that transaction committed; the reorg undo and the progress-marker reset run from the persisted set state, also in a process with the mode off. -
Known limitation (documented, pre-existing):
@cap-js/sqlitestores everyDecimalas a double, so amounts above 2^53 lovelace are rounded on SQLite in all tables; exact on PostgreSQL and HANA. -
decodeShelleyAddress()insrv/utils/mappers.ts(type, script flag, stake address from the address bytes);LedgerStateBackend(queryUtxoSetAt) on the Ogmios backend; five new secondary indexes on the ledger tables. -
TransactionInputs.spentTxHash/spentOutputIndex: the consumed outpoint on every input row, all paths, no knob. Null on rows written before this version. -
TransactionCertificates(keytx, certIndex, kind;stakeAddress,poolId,drepId,deposit,epoch) andTransactionWithdrawals(keytx, stakeAddress;lovelace), exposed read-only onCardanoODataService, compositions onTransactions. -
crawler.certificates/CRAWLER_CERTIFICATES(defaultfalse): writes both tables per block in the block transaction. Ogmios chain-sync and Koios/tx_info(_certs,_withdrawalson the batch call); Blockfrost reports none and the indexer warns once. -
Certificate kinds normalized across sources (
stake_registration,pool_delegation,vote_delegation,pool_retirement,drep_registration, …); a Conway stake+vote delegation is two rows with onecertIndex; unknown types keep the raw source name. -
credentialToStakeAddress()/credentialToDrepId()insrv/utils/mappers.tsfor the bare Ogmios credentials. -
Reorg deletes the new rows with their transactions; five new secondary indexes (
spentTxHash, spentOutputIndex; certificatestakeAddress,poolId,drepId; withdrawalstakeAddress).
-
Fixes 3
Notes
Fixed
installPostgresOrderNulls()andensureDbIndexes()run ininitializeAppContext, shared by the plugin path and the standalone served hook. rc.16 / rc.17 called them frominitializeFromConfigonly, so the standalone server (cds serve, the container image) never ran them.- Standalone on Postgres:
$topreads renderORDER BY … ASCand use the rc.16 indexes; start log showsPgOrderNullsand18 indexes ensured. - No schema change, no new index, no config change.
Fixes 4
Notes
Fixed
$top/$orderbyreads on PostgreSQL use the indexes again.@cap-js/postgresrenders every ordering term asASC NULLS FIRST/DESC NULLS LAST, the opposite of a btree index, so every$topread sorted the whole table (Blocks?$top=1: 0.8 to 7.6 s over 2M rows).srv/utils/pg-order-nulls.tswraps the renderer's_orderByonce at start (installPostgresOrderNulls) and drops the clause forkey,not nulland temporalvalidFromcolumns. Nullable columns and an explicitnullskeep it; the nullableheightstill needs its rc.16DESC NULLS LASTindex.- A Postgres deployment whose driver no longer exposes the hook logs a warning and runs as before.
- No schema change, no new index. Rendered through the real driver in the unit test.
Features 1, Changed 1, Fixes 3, Other 1
Notes
Fixed
- The reads no longer scan their tables.
cds deploycreates primary keys only, and the temporal entities (Assets, Pools, Accounts, Addresses, AddressUTxOs, AddressAssets, UTxOAssets, Dreps) carryvalidFromFIRST in their key, so every lookup by unit, poolId, stakeAddress or address read the whole table; Blocks by height, slot and epoch, Transactions by block and height, TransactionMetadata by tx and AssetHistory by tx (crawler rollback) had no index at all. On the hosted preprod box (930k blocks, 1.4M transactions, 521k metadata rows) that was 200 ms for the latest block, 1.0 s forGetMetadataByTxHash, 78 ms forGetAssetInfo, 168 ms for a block by height, 767 ms for one rollback lookup; with the indexes 0.2 ms each, the latest block through the OData layer 10 ms instead of 156. - The buffered usage counters of rc.15: the flush timer no longer runs under the unit-test mode override (it raced the explicit flush of the test on a slow full-suite run), and a flush without a database service keeps its deltas instead of failing them one by one.
$orderby=height descon Blocks is rendered asORDER BY height DESC NULLS LAST, which a plain index cannot serve backwards (still a 200 ms sort with the plain index in place). Blocks carry a second index(height DESC NULLS LAST)on PostgreSQL / HANA (height DESCon SQLite) for exactly that shape; the plain one stays for ascending order and equality.
Changed
- The transport lane caches a resolved token (the grant row) for
AGENT_TOKEN_CACHE_MS(agentGrants.tokenCacheMs, default 10 s, 0 = off) instead of one SELECT per request. Only positive results are cached, so an unknown, revoked or just-rotated token is never remembered;validUntilis checked per request against the cached row; the budget and usage counters never read the cached row for admission (their UPDATEs are conditional on the database). Revoke, rotate and update drop the entry on this process at once (a lookup in flight during the change does not re-cache its row); another replica honours them when its entry expires, at most 10 s later. Seconds, not minutes, on purpose.
Added
srv/utils/db-indexes.ts: the list (DB_INDEXES, 18 entries) andensureDbIndexes(), run at every start frominitializeFromConfigwithCREATE INDEX IF NOT EXISTS(idempotent, plain SQL for SQLite and PostgreSQL, identifiers unquoted so Postgres folds them like the tables; a failing statement is logged and skipped; HANA is skipped altogether, its CREATE INDEX has no IF NOT EXISTS and the column store needs none). The agent token lane's lookup bytokenHashis on the list too.
Notes
- No schema change for
cds deploy. A plain CREATE INDEX locks the table against writes while it builds, so on a large live PostgreSQL create the set once beforehand with CONCURRENTLY under the same names; the start is then a no-op. On the hosted box the build took 1 to 7 s per index, 230 MB in all.
- The reads no longer scan their tables.
Features 1, Changed 2, Other 1
Notes
Changed
- The agent grant hook no longer writes the usage counter per request on
PostgreSQL and HANA. Every admitted call was
calls + 1on the row (grant, day, service, action), awaited before the handler ran, so all calls of one grant and action queued on that row's lock: 100 parallel reads through the gateway on one grant landed at 30 to 50 calls per second on the hosted preprod box while the reads themselves took milliseconds. The deltas are now summed in memory and written by one timer per second, one UPDATE (or INSERT) per touched key; a refund of a refused request goes to the same key.GetGrantUsageflushes the buffer before it reads, so its answer stays exact; a crash loses at most one second of counters, which is accounting, not admission. The daily budget (maxJobsPerDay) is untouched: its conditional UPDATE is the admission itself and stays synchronous. - SQLite keeps the awaited per-request write: a timer writing on a second
connection while a request transaction is open is the WAL snapshot race
(
SQLITE_BUSY_SNAPSHOT) this hook must never cause. A request inside its own changeset transaction keeps it on every database (its rollback is the refund).
Added
flushGrantUsage()andpendingGrantUsageKeys()insrv/utils/agent-grants(the flush also runs oncds.on('shutdown')).
Notes
- No schema change, no migration. One replica per database is assumed for the buffer, as it already is for the rate limiters.
- The agent grant hook no longer writes the usage counter per request on
PostgreSQL and HANA. Every admitted call was
Features 1, Fixes 3, Other 1
Notes
Fixed
- The crawler no longer stays on pagination for the rest of the process when
no chain-sync backend was usable at start. On a box that restarts node and
ODATANO together the node replays its ledger for minutes while ODATANO is
up in seconds, so Ogmios refused its init and the crawl degraded to
Blockfrost/Koios pagination — some fifty times slower — without ever looking
back. With
source: autothe crawler now retries the chain-sync backend every 30 seconds while on pagination — on its own timer, so neither an idle poll interval nor a slow batch delays the retry — and hands over as soon as it is usable: a pending poll sleep is cut short, a block in flight is completed first, and the rest of the batch is streamed instead. blocksEpochisnullinstead of0on rows filled from Koios, which has no per-epoch block count. A consumer can now tell "unknown" from a real zero; Blockfrost rows are unchanged.liveSaturationwidened fromDecimal(5, 4)toDecimal(9, 4)onPoolsandPoolEpochSnapshots. The value is a fraction, and on a test network a single pool can hold several times the saturation point: the first preprod snapshot already had a pool at 7.6 against a ceiling of 9.9999, and one pool past it fails the whole epoch's snapshot.
Added
getStatus()onCardanoIndexerServicereportssource—chain-syncorpaginationfor the crawler in this process,nullwhen this instance is not the one crawling. A crawl degraded to pagination still advances the cursor, so this is how monitoring tells the two apart.
Notes
- Schema change (column widening): a deployment coming from rc.13 needs a
schema update (
cds deploy).
- The crawler no longer stays on pagination for the rest of the process when
no chain-sync backend was usable at start. On a box that restarts node and
ODATANO together the node replays its ledger for minutes while ODATANO is
up in seconds, so Ogmios refused its init and the crawl degraded to
Blockfrost/Koios pagination — some fifty times slower — without ever looking
back. With
Fixes 1, Other 1
Notes
Fixed
- Koios reports
live_saturationin percent, Blockfrost as a fraction, and both values were passed through unchanged into aDecimal(5, 4)column. Every pool at or above 10 % saturation overflowed the column, and because a snapshot writes all pools in one statement,CRAWLER_EPOCH_SNAPSHOTSnever wrote a single row on a Koios backend — it retried every 30 seconds instead. The Koios mapper now converts to the fraction the model expects (75.42 % -> 0.7542), which also correctsPools.liveSaturationon the lazy read path.
Notes
Poolsrows written from Koios before this release hold percent values until their temporal slice expires and is re-read.PoolEpochSnapshotswas empty on those deployments, so nothing has to be corrected there.
- Koios reports
Features 2, Fixes 2, Other 1
Notes
Added
- The crawler now fills the tables analytics needs, not only blocks and
transactions:
AssetHistory— every mint and burn of the crawled range, taken from the ledger's mint field (Ogmios, Koios) or the output-minus-input delta (Blockfrost). On by default, no provider call (CRAWLER_ASSET_HISTORY).Assets— one row per native-asset unit seen, derived from the unit itself: policyId, assetNameHex, decoded name, CIP-14 fingerprint. Supply and registry data are still filled in by the lazy path on first read. On by default, no provider call (CRAWLER_ASSET_CATALOGUE=bare);enrichresolves the registry in the background.PoolEpochSnapshots/DrepEpochSnapshots— new read-only entities, one dated observation per pool and DRep per epoch, so stake and governance read as a time series instead of current state. Taken only while the crawl is at the chain tip, because the enumerating providers report the set as it is now and take no epoch parameter. Off by default (CRAWLER_EPOCH_SNAPSHOTS), requires Koios.
EnumeratingBackendcapability (full pool/DRep listing), implemented by Koios.
Fixed
- Blockfrost inputs now carry
isCollateral/isReference. The mapper filled two field names nothing reads, so a reference input was indistinguishable from a consumed one andTransactionInputs.isReferencewas always false. - A phase-2 failure fetched through Blockfrost now reports the collateral the ledger charged as its fee, not the declared fee that was never collected — the correction the chain-sync path has had since rc.11.
Notes
- Ranges crawled by an earlier build keep their empty
AssetHistoryandAssetstables until they are crawled again.CRAWLER_ASSET_CATALOGUE=offwithCRAWLER_ASSET_HISTORY=falserestores rc.11 behaviour.
- The crawler now fills the tables analytics needs, not only blocks and
transactions:
Fixes 2, Other 1
Notes
Fixed
- Phase-2-invalid transactions carried the fee declared in the transaction body.
The ledger charges the collateral instead, so
Transactions.feeand the derivedBlocks.feesunderstated every failed script transaction — 127 of them over a preprod backfill from 2026-01-01, 287 185 600 lovelace, and the only divergence in an otherwise byte-identical comparison against cardano-db-sync.mapOgmiosTxnow takes the body'stotal_collateralwhere it declares one, andCardanoIndexer.applyCollateralFees()derives the rest from the resolved collateral inputs minus the collateral return. A collateral input that cannot be resolved (produced before the crawl start) keeps the declared fee and is logged, rather than summing short. Only the Ogmios chain-sync path was affected; Blockfrost and Koios pass through untouched. - A chain-sync frame nested deeper than the client's parser can handle no longer
ends the process.
@cardano-ogmios/clientparses every frame with a recursive descent parser plus an equally recursivesanitize; preprod block 5183974 carries a native script nested 10 774 levels deep (205 kB as JSON), and theRangeErrorsurfaced as an unhandled rejection from a socket handler nobody awaits — the server died, Docker restarted it, the crawler hit the same block again, 53 times in seven minutes, andlastErrorstayed empty throughout. The frame guard wrapssafeJSON.parse: the fast path is unchanged, an overflowing frame has integers beyond 2^53 rewritten as strings and is parsed with V8's iterativeJSON.parse, and the guard never throws. A frame that survives neither parser becomes aChainSyncFrameErrornaming the block; withsource: 'auto'the crawler fetches that block through the paginating backend and resumes chain-sync instead of halting. It still halts undersource: 'ogmios', without a paginating backend, or when the frame named no block.
Notes
- Transactions indexed by an earlier build keep the declared fee; only blocks crawled from this version on carry the collateral. Re-crawl the affected blocks, or correct the rows in place — a phase-2 failure is the only transaction the crawler stores without a regular input, which identifies the set without consulting another indexer.
- Phase-2-invalid transactions carried the fee declared in the transaction body.
The ledger charges the collateral instead, so
Fixes 3, Other 1
Notes
Fixed
- PostgreSQL: asset names containing a NUL byte or invalid UTF-8 halted the
crawler ("unsupported Unicode escape sequence").
decodeAssetName()now returns the hex form for such names (assetNameHexwas always exact), and a db-level sanitizer strips U+0000 from every row this plugin writes — crawler and lazy paths alike. - PostgreSQL: metadata labels >= 2^63 overflowed the int64 key
TransactionMetadata.idand halted the crawler. Labels now map into int64 exactly (two's complement, exact beyond 2^53);labelkeeps the original text. Non-numeric labels are skipped with a warning instead of producing an invalid key. - Crawler restart loop: a block whose persist keeps failing no longer restarts
the crawler every 5 s. The standby delay backs off (5 s → 5 min) after a local
failure, and a block that PostgreSQL/HANA/SQLite deterministically rejects
5 restarts in a row latches the crawler off with
lastError = "poison block …";resumeCrawler()continues once the cause is fixed. Transient failures (DB outage, timeouts) never latch.
Notes
- Metadata rows for labels above 2^53 indexed by an earlier rc build carry a
rounded
id; re-indexing such a transaction writes the exact key beside it. Delete those rows before re-crawling if you need a clean table (they are rare: standard labels are far below 2^53).
- PostgreSQL: asset names containing a NUL byte or invalid UTF-8 halted the
crawler ("unsupported Unicode escape sequence").
Breaking 5, Features 14, Changed 7, Fixes 21
Notes
Added (rc.9)
- PostgreSQL in the image:
ODATANO_DB_URL=postgres://user:pw@host:5432/db(sslmode=disable|require|verify-full) selects@cap-js/postgres; the entrypoint waits for the listener and deploys the schema on every boot (CAP's additive evolution;ODATANO_DB_DEPLOY=neverskips it). Without a URL the image stays on SQLite atODATANO_DB_PATH(default/data/db.sqlite, seeded on the first boot).migratemode copies a SQLite file into a freshly deployed PostgreSQL through CAP (docker compose run --rm --no-deps odatano migrate --from /data/db.sqlite;scripts/migrate-sqlite-to-postgres.mjs, row counts verified). The mapping is pinned intest/unit/docker-cds-config.test.ts.
Changed (rc.8)
- Transport auth moves to
@odatano/cap-auth. With agent grants enabled the plugin setscds.requires.auth.implto that package (a host's own impl is kept as itsdelegateImpl) and registers thex-agent-tokenlane fromsrv/utils/agent-token-auth.ts; the package runs its basic lane (timing-safe compare, failure throttle), the registered lanes, then CAP's own strategy forkind, and sends no terminal 401 of its own. The lane registry, delegate loader andagentGrantsDelegate*options leave this package;registerTransportLaneand the lane types are re-exported from@odatano/cap-auth. The contract table runs against the real server (test/integration/transport-auth-contract.test.ts). - The Docker image runs
NODE_ENV=productionwith HTTP basic auth (ODATANO_HTTP_PASSWORD,ODATANO_HTTP_USERdefaultodatano,ODATANO_HTTP_ROLESdefaultAdmin,ODATANO_AUTH=dummyfor local testing) throughdocker/cds-config.mjsanddocker/entrypoint.sh(node as PID 1).getLiveness()andVerifyDataSignaturestay anonymous; everything else challenges. The compose file no longer setsCDS_REQUIRES_AUTH=mocked.
Fixed (rc.8)
getLiveness()andVerifyDataSignaturewere anonymous only underNODE_ENV=development. CAP treats a service WITHOUT a service-level@requiresasauthenticated-userin production before it looks at the operation, so the rc.7 element-level layout still answered 401 to anonymous probes on a production container (the hosted odatano-preprod runs in development mode, which is why it worked there). Both services now carry@requires: 'any'at the service level; every element keeps its own requirement, nothing else changes for callers. The model test pins it.
All notable changes to ODATANO will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Fixed (rc.7)
getLiveness()andVerifyDataSignatureanswered 401 under basic/XSUAA auth. CAP checks a service-level@requiresbefore the operation's own annotation, so an operation's@requires: 'any'never applied: the rc.6 DockerHEALTHCHECKflagged the container unhealthy, and the anonymous CIP-30 verify behind wallet login only ever worked under mocked auth.CardanoIndexerServiceandCardanoSignServicenow carry theauthenticated-userrequirement on each entity and operation instead of on the service (pause/resume stay Admin, HSM actions keephsm.requiresRole); auto-exposed entities remain unreachable directly (405) and guarded via navigation. Nothing else changes for callers.- SQLite busy timeout.
cds.requires.db.client.timeout = 5000(node:sqliteDatabaseSyncoption): a second connection on the same file — the previous test file's process still winding down, a detached write — now waits up to 5 s instead of failing at once withdatabase is locked.
Added (rc.6) — agent-grant lifecycle, Ogmios DRep
- Agent grants: rotate, update and usage history; grant admin rate limit
configurable; unauthenticated liveness.
CardanoAgentServicegainsRotateAgentGrantToken(fresh token, old one unknown from the next request),UpdateAgentGrant(label, allow list, job kinds, daily budget, expiry; absent = untouched, explicitnull= cleared; wallet binding immutable;409 GRANT_REVOKEDon a revoked grant) andGetGrantUsage(grantId, since, until)(admitted calls per service and action over up to 366 days, from the newCardanoAgentGrantUsagecounters — runcds deploy). Semantics match the gateway's peer services, so one client drives them all.AGENT_GRANT_ADMIN_RATE_LIMIT/agentGrants.adminRateLimit(default 10/h per principal) now covers all four administration actions.CardanoIndexerService.getLiveness()answers without credentials (@requires: 'any'); the DockerHEALTHCHECKand compose probe use it. - DRep lookup via Ogmios.
OgmiosBackend.getDrepnow queries the live ledger state (queryLedgerState/delegateRepresentatives, Ogmios ≥ 6.4) instead of declaring the method unsupported, soGetDRepByIdworks on an Ogmios-only setup. Routing is unchanged (Blockfrost/Koios first). Ogmios only lists registered DReps: a retired DRep is a 404 there,expiredis derived from the mandate epoch,lastActiveEpochis 0.
Added (rc.5) — agent grants
CardanoAgentService(/odata/v4/cardano-agent/): scoped, budgeted bearer tokens for agents.CreateAgentGrant/RevokeAgentGrantare Admin-only; the tokenodat_…is returned once and stored as SHA-256.AgentGrants(Admin; a token sees its own row) andGetGrantStatus()(token self-service: allow list, wallet, remaining budget, expiry). New tableCardanoAgentGrants.- Token principal. A request with
x-agent-tokenruns asagent:<grantId>with the single roleagent-grant, never as the operator, so every@requires: 'Admin'surface refuses it. Reads and compute-only actions are always allowed; allow-listed actions (Build*,SetCollateral,CreateSigningRequest,VerifySignature,Submit*,CheckSubmissionStatus,SubmitWalletJob,CancelJob) cost one budget unit per UTC day; everything else is 403. Wallet jobs are pinned to the grant's wallet (walletIdinjected,allowedJobKindsnarrowed) and scoped to the grant throughcreatedBy = agent:<grantId>. - Transport lane (
srv/utils/agent-token-auth.ts), installed ascds.requires.auth.implbyactivateAgentGrants()— called from the plugin (consumer apps) and fromsrv/server.ts(standalonecds serve) — whencds.requires.odatano-core.agentGrants.enabled/AGENT_GRANTS_ENABLED=trueis set: admits the token header on the six service paths (unknown 401, expired 410, 20 failed attempts per client per 15 min → 429) and delegates every other request to the CAP strategy configured before (agentGrants.delegate, default: the configuredauth.kind). Off by default: no behaviour change for existing consumers. - Public API for other packages:
issueAgentGrant(),revokeAgentGrantById(),registerTransportLane()(the seams@odatano/x402uses to sell grants and add its payment lane), plushashAgentToken,AGENT_TOKEN_HEADER,AGENT_ALLOWLISTABLE_ACTIONS,AGENT_ALWAYS_ALLOWED_EVENTS. - Tests:
test/unit/agent-grants.test.ts,test/unit/agent-token-auth.test.ts,test/integration/agent-grants.test.ts. Verified live on the hosted preprod instance (Ogmios backend): 36 end-to-end checks — issue, self-service status, free reads, Admin gates, allow list, budget with refund and 429, revoke.
Fixed (rc.5)
- Dockerfile compiled into
dist/but served fromsrv/.RUN npm run buildemitted todist/(tsconfig.json), whilecds serve srvresolves each service's@implassrv/<name>.jsand the runtime image has no TypeScript loader. The image only ever worked because stale in-place.jstwins from a previousbuild:pluginsat in the build context; a clean checkout produced a container that crashed withCannot find module '/app/srv/…-service'. NowRUN npm run build:plugin(in place,tsconfig.build.json). - Polling could confirm a rolled-back transaction (wallet worker). Once a
tx had been found at some height, the confirmation tracker stopped looking
it up and confirmed on the tip alone; without the crawler there is no reorg
signal, so a tx found at height 100, rolled back, with the tip reaching 102
became
confirmedat depth 3. The polling path now re-reads the tx before confirming any inclusion not seen in the current round: gone → the confirmation point is cleared (same-CBOR re-submit as after a crawler reorg) and the tip is re-learned; moved → re-anchored at the new height and judged there. - Repeated state transitions reported success (wallet worker). The
guarded transition ignored the UPDATE's affected rows and only checked the
row's status afterwards, so
markConfirmedtwice returnedtruetwice and two trackers bumpedjobsConfirmedto 2 for one job. Transitions now report whether THIS call moved the row; the confirmation tracker emits the terminal event and the stats bump only from the winner. - Job deduplication crossed principal boundaries (wallet worker). The
idempotency claim was (walletId, kind, key) regardless of who submitted, so
a second agent grant using
order-1on the same wallet received the first agent's job id as a "deduplicated" success and never got its own job. The stored claim (dedupKey) is now scoped to the submitting principal (dedupKeyFor(createdBy, key): a hash of owner + key, plain key for an owner-less caller); the caller-visibleidempotencyKeyis unchanged. Rows written before this change keep their plain claim; a retry by an owned principal creates a fresh job for them. - Enabling agent grants discarded a host's own
auth.impl. Activation replacedcds.requires.auth.impland the lane then delegated to CAP's built-in strategy forkind, so a custom gate (a consumer's own auth middleware) stopped running for every non-token request, on every path. The original impl is now kept asagentGrantsDelegateImpland resolved like CAP resolves it (relative tocds.root); the lane only adds the token path in front of it.agentGrants.delegateacceptscustomfor that case and defaults to it when a custom impl is configured. - Budgeted actions deadlocked inside a
$batchchangeset. The hook charged the daily budget on a detached connection while the changeset's open transaction held SQLite's single pooled connection: twoSubmitWalletJobparts in one atomicity group never returned. The hook now detects an already-begun request transaction (ready/dbconcds.tx(req)) and charges on it; the changeset's rollback is then the refund. Plain requests keep the detached charge (spend sticks, no lock across backend calls). - A late refund reduced the wrong day's budget. The refund selected the
window by the current date; a request charged at 23:59 and refused after
midnight gave its unit to the new day.
consumeDailyBudgetnow returns the charged window and the refund names it. - Agent-grant hook wrote
lastUsedAtfire-and-forget, racing the request's own transaction: on SQLite in WAL mode the handler's first UPSERT then failed withdatabase is locked(SQLITE_BUSY_SNAPSHOT), a 500 seen live on the second build within a minute. Every write the hook makes is now awaited before the handler runs; the budget refund on a refused request is deferred off the failing request's tick.
Shipping first as v2.0.0-rc.1, published to npm as
latest(14-08-2026) — a plainnpm i @odatano/coreinstalls the RC.^2.0.0does not match a pre-release, so pin@odatano/core@2.0.0-rc.5inpackage.json; consumers that need to stay on the 1.x line pin@odatano/core@^1.11.0.v2.0.0-rc.2 (15-08-2026): keyed reads honour
$expand/$select,cds watchstartup fix. v2.0.0-rc.3 (16-08-2026): keyed reads no longer answer with a row the query excludes. See Fixed below — no schema change, nocds deployneeded when coming from rc.1 or rc.2. v2.0.0-rc.4 (20-08-2026): multi-policy mints (per-actionmintingPolicyScript+redeemerJsoninmintActionsJson) andextraOutputsJsononBuildMintTransaction. Additive; no schema change, nocds deployneeded when coming from rc.1-rc.3. v2.0.0-rc.5 (07-09-2026): agent grants (CardanoAgentService, scoped bearer tokens for agents, off by default) and wallet-worker fixes. Adds theCardanoAgentGrantstable: runcds deployonce when coming from rc.1-rc.4.⚠ Breaking
- SAP CAP 10 — peer dependency is now
@sap/cds >=10(was^9). Consumer projects on CAP 9 must upgrade before adopting@odatano/core@2. - Node.js >= 22.5 — required by
@cap-js/sqlitev3 (node:sqlitefloor). - Numeric OData fields serialize as strings — CAP 10 renders
Decimal,Int64and$countvalues as JSON strings. API clients that parse these fields as JSON numbers must be adapted. - XSUAA role separation — the
$XSAPPNAME.Adminscope (crawler/worker control) is no longer part of theCardanoUserrole template or the app authorities; assign the newCardanoAdmintemplate explicitly. ExistingCardanoUserrole collections lose crawler/worker control on redeploy (intentional least-privilege fix). - Database redeploy required — 2.0 adds five tables (
CardanoSyncState,CardanoReorgLog,CardanoWorkerWallets,CardanoWalletJobs,CardanoAgentGrants— the last one since rc.5) plus adedupKeycolumn and its unique constraint on the jobs table. Consumers upgrading from 1.x must runcds deploy; without it the new services answerno such tableandgetStatus/GetWorkerStatusreturn 500. (Verified against a real 1.10 consumer project.)
Added
- Chain crawler / pre-sync (opt-in) —
CRAWLER_ENABLED/cds.requires.odatano-core.crawler: streams the chain forward from a configured start block intoBlocks/Transactions(+inputs/outputs/assets/metadata) so queries hit local data instead of a backend per request. Ogmios chain-sync (native rollForward/rollBackward) with Blockfrost/Koios pagination fallback, parent-hash reorg recovery, cursor (CardanoSyncState) + audit log (CardanoReorgLog), cluster-safe via DB lease. New CardanoIndexerService (/odata/v4/cardano-indexer/): SyncState + ReorgLog (read-only),getStatus/pauseCrawler/resumeCrawler(Admin). - Wallet worker (opt-in) —
WALLET_WORKER_ENABLED/cds.requires.odatano-core.walletWorker: asynchronous per-wallet transaction queue (build → sign → submit → confirm) with software/HSM signers, idempotency keys, exponential retry, per-wallet DB leases (multi-instance safe), and a confirmation tracker (crawler hook or polling) with rollback re-submit of the SAME signed CBOR. New CardanoWorkerService (/odata/v4/cardano-worker/):SubmitWalletJob/CancelJob/GetJobStatus/GetWorkerStatus/PauseWorker/ResumeWorker. - CAP events on both v2.0 services — consumers can subscribe instead of polling.
CardanoIndexerServicepublishesblockIndexed(hash, slot, height, txHashes, tip) andreorg(forkSlot, forkHeight, blocksRolledBack);CardanoWorkerServicepublishes the terminaljobConfirmedandjobFailed(jobId, walletId, kind, txHash, + errorCode/errorMessage). ODATANO runs in the consumer's process as a plugin, so this needs no broker and nocds.requires.messaging; configuring one later routes the same emits through it. All emits happen AFTER the corresponding commit, are fire-and-forget, and swallow subscriber failures so a broken observer cannot stall the crawler or a wallet job. Events are absent from$metadata(OData V4 has no event concept), so the change is additive for existing HTTP clients. - KoiosBackend.getDrep with the new Koios schema.
- Multi-policy mints (rc.4) —
mintActionsJsonentries accept optional per-actionmintingPolicyScript(CBOR hex, pre-applied) andredeemerJson(JSON-encoded PlutusData string); absent fields fall back to the top-level policy/redeemer. A bare asset name expands under the action's own policy id, and a full unit must carry it. Actions resolving to the same policy must carry the same redeemer. Applies toBuildMintTransaction, the combined spend+mint flow ofBuildPlutusSpendTransaction, and the wallet-worker lane; per-action redeemers get__INPUT_IDX__placeholder resolution. extraOutputsJsononBuildMintTransaction(rc.4) — same entry shape as onBuildPlutusSpendTransaction(address,lovelaceAmount,assets,inlineDatumJson,referenceScriptHex, max 32, per-entry min-ADA check). When present, the extra outputs carry the minted assets and the primary recipient output stays ADA(+datum)-only; coin selection requests the extra outputs' lovelace plus, per asset unit, the output demand the transaction's own mints do not cover.
Changed
- Test suite migrated from Jest to Vitest 4 (unit + integration projects, coverage via
@vitest/coverage-v8). 1983 tests across 62 files (46 unit + 16 integration). - Integration suites for both v2.0 subsystems —
test/integration/wallet-worker.test.ts(real CAP + real SQLite, backends stubbed: guards the deployedUNIQUE(walletId, kind, dedupKey), real transactions and the OData layer; needs no network or funds) andtest/integration/crawler.test.ts(real Ogmios: contiguous ingest, recovery from a fork staged while the crawler was down,getStatus; self-skips when Ogmios is unreachable or behind the tip, so it runs in both CI lanes). - HarmonicLabs stack bumped:
buildooor0.2.9,cardano-ledger-ts^0.5.6,cardano-costmodels-ts~1.6.1 (Plutus V3 cost model atN_COST_MODEL_PLUTUS_V3= 350, post-Plomin²). - Vendored patches removed — upstream releases contain both fixes:
keep-relevant.ts(buildooor keepRelevant) andauxiliary-data-patch.ts(ledger-ts Conway tag-259 AuxiliaryData decode). - Dependency security pass:
axios^1.17.1,fast-uri^3.1.5, approuterbody-parseroverride —npm auditclean (prod + dev).
Fixed
- Keyed reads no longer answer with a row the query excludes. A keyed read whose query legitimately matched nothing — a composite key whose second value does not match (
TransactionMetadata(id=721,tx_hash='…')for a label the transaction does not carry), or a$filterthat excludes the row — fell back to the row found for the entity's main key and returned it with a 200. Such requests now return 404. Affects all keyed reads ofBlocks,Epochs,Pools,Accounts,Dreps,Assets,Addresses,TransactionsandTransactionMetadata. - Keyed reads honour
$expand/$select.GET Transactions('<hash>')?$expand=inputs,outputs(and every other keyed read ofBlocks,Epochs,Pools,Accounts,Dreps,Assets,Addresses,Transactions,TransactionMetadata) returned the bare row and dropped the query options; only the collection form honoured them. Keyed reads now index on a miss and then run the client's own query, so they behave exactly like the collection form. For temporal entities the handler widens the request's validity window before its first DB statement so the slice written during the request is visible to that query (expired slices stay hidden). Composite-key reads (TransactionMetadata(id=…,tx_hash=…)) honour both keys. cds watchno longer logsERR_MODULE_NOT_FOUNDat startup. The boot-time re-drive of interrupted deferred submissions used an extensionless dynamicimport()that tsx cannot resolve, so it never ran from TypeScript sources; compiled builds were unaffected. Now a lazyrequire.- Hardening pass: wallet-worker request transformation for all job kinds (shared parsers in
srv/utils/tx-request-parsers.ts), crawler reorg guards (null-slot fork point, BlockfrostCHAIN_POINT_MISMATCHsignal, Koios partial-batch rejection), confirmation-depth correctness across rollbacks, multi-instance-safe crash recovery and lease CAS, idempotency-key release for cancelled jobs.
Wallet worker — payment safety
- HSM-backed wallet jobs now require the configured signing role.
SubmitWalletJobwas gated onauthenticated-useronly, so any authenticated account could queue a value transfer with an arbitrary recipient and amount that the server-held HSM key would sign — bypassing thehsm.requiresRolegate the synchronousSignWithHsmpath enforces. The role is now checked against both the instance's wallet config and the registered wallet row (403, newODATANO_FORBIDDENcode). - A crash around submit can no longer cause a duplicate payment. New durable pre-submit state
submitting: the signed CBOR and its hash are committed before the transaction can reach a backend, and the row stays non-terminal so it keeps holding its idempotency key. Interrupted submits are reconciled against the chain — the exact stored bytes are re-submitted, never a rebuild — instead of being failed asPROCESS_RESTART, which released the key and let the documented caller retry build and pay a second transaction. - The per-wallet lease survives long builds. Only one renewal happened before the build, so a build+sign exceeding
WORKER_LEASE_TTL_MS(15s — routine with a multi-backend build plus an HSM round-trip) let another instance adopt the wallet while the first kept working. A heartbeat now renews for the whole execution, and ownership is fenced again immediately before the irreversible submit. - Idempotency is enforced by the database, not by a lookup. Unique constraint
(walletId, kind, dedupKey)via@assert.unique.dedup— two concurrent retries of the same key could both read "no such job" and both insert. The loser of the race now returns the winner's job.dedupKeycarries the caller key while the job owns it and the job's own ID otherwise, so keyless jobs never contend and terminal jobs release the key without depending on per-database NULL semantics.
Chain crawler — reorg across a restart
- Chain-sync recovers from a fork it slept through. Only a single intersection point (the cursor) was offered to Ogmios, so a cursor orphaned while the crawler was down produced
No intersection foundand killed the ingest pipeline. The crawler now offers a ladder of its own crawled ancestors — dense over the last 10 blocks, doubling out to −16384 — so the node intersects at the last common block and reports an ordinaryrollBackward, which the existing reorg handling resolves. The dense head keeps shallow rollbacks (the common case) exact instead of rolling back further than necessary. - A crashed crawler no longer stays down across restarts. Every terminal error cleared
desiredRunning, which no restart undoes — a dropped chain-sync socket (a routine node restart) silently disabled the pre-sync until an operator calledresumeCrawler. Only unrecoverable configuration failures latch the cluster now; runtime failures leavesyncStatus: errorwithdesiredRunning: trueso coming back up resumes.
- PostgreSQL in the image:
Changed 2
Notes
Changed
- Enhanced coin-selection logic and error handling in the Buildooor transaction path; Plutus V3 cost-model handling updated to the current parameter count (
N_COST_MODEL_PLUTUS_V3). - Vendored
keep-relevant.tsremoved — buildooor 0.2.9 ships the fixedkeepRelevantupstream.
- Enhanced coin-selection logic and error handling in the Buildooor transaction path; Plutus V3 cost-model handling updated to the current parameter count (
Features 2
Notes
Added
- Deferred submit for in-process consumers:
SubmitVerifiedTransaction/HSM flows can persist the signed tx and hand the network submit to a detached transaction, with restart re-drive (redriveInterruptedSubmissions). - Nested-transaction deadlock guard (
srv/utils/tx-utils.ts):detachedTxwith pool-timeout diagnosis and abort fencing;runWithoutAmbientTxfor CAP ambient-tx isolation.
- Deferred submit for in-process consumers:
Features 1, Changed 1
Notes
Added
-
Typed script parameters in
applyScriptParameters(srv/utils/tx-build-helper.ts, new exportedencodeScriptParam+ScriptParamUplcType). A parameter entry may now be{ "uplc": "data" | "bytes" | "int" | "bool" | "unit", "value": … }, applied to the parameterized script as a UPLC constant of that native type instead of always asData:data→UPLCConst.data(jsonToPlutusData(value))(value: PlutusData JSON)bytes→UPLCConst.byteString(value)(value: even-length hex string)int→UPLCConst.int(value)(value: number | numeric string)bool→UPLCConst.bool(value)/unit→UPLCConst.unit
Why: some compilers type scalar parameters natively rather than as
Data— e.g. Pebble, whereparam owner: PubKeyHashexpects a nativebytestring; applying it as aData-wrapped bytestring makes the validator reject (case: expected constr or constant value). The previous Data-only application could not parameterize such scripts. The off-chain side must know each param's UPLC type (which it does — it authors/consumes the contract), and can now mix native and Data params in one call (e.g. a nativePubKeyHashplus aDataTxOutRef).
Changed
applyScriptParametersis fully backward-compatible in effect. A bare PlutusData entry (e.g.{ "bytes": "ab.." },{ "constructor": 0, … }) is treated as shorthand for{ "uplc": "data", … }— the Aiken / CIP-57 blueprint convention where every parameter isData. ExistingscriptParamsJsoninputs produce byte-identical applied scripts; no consumer or on-chain behavior changes.
-
Features 5, Changed 14, Fixes 5, Other 5
Notes
A broad correctness-and-resilience pass across every layer (PR #60,
dev/general-improvements). No new OData actions, but several schema data-type changes, error-status changes, and public-surface tightenings that consumers should be aware of. Highlights: all read entities are now@readonly(writes → 405), Epoch/Metadata time/label columns widened to avoid overflow/truncation, Ogmios stops fabricating placeholder data and is skipped for unsupported methods, the signing/submit flow is now crash-durable, and Buildooor protocol-param/datum/collateral/metadata handling is hardened.Added
- UTxO-only indexing fallback for
GetUTxOsByAddress(srv/blockchain/cardano-indexer.ts,srv/cardano-service.ts). NewCardanoIndexer.indexAddressUtxos(tx, addr)indexes only the live UTxO set viagetAddressUtxos— nogetAddress(address-aggregation) call and no parentAddressesrow (same pattern asindexCredentialUtxos).GetUTxOsByAddressnow falls back to it when no configured backend supportsgetAddress(detected asAllBackendsFailedErrorwith zero collected errors — every backend skipped the method). Net effect: an Ogmios-only deployment can now serveGetUTxOsByAddress(Ogmios has the live UTxO set but not address aggregation) instead of failing. SigningInstructions.cardanoCliCommand(srv/utils/types.ts,srv/cardano-sign-service.ts): signing instructions now include a copy-pasteablecardano-clisigning recipe for CLI / hardware signers.extractPaymentCredential(address)(srv/utils/validators.ts): decodes a Shelley bech32 address to its 28-byte payment-credential hash + anisScriptflag, ornullfor undecodable / stake addresses. Used to bind signature verification to the build's fee-payer key.getStatus().backends(src/index.ts): the programmatic status object now reports the configured backend list viacardanoClient.listBackends().CardanoBackend.unsupportedMethods?: ReadonlySet<string>(srv/blockchain/backends/cardano-backend.ts): backends can declare methods they don't support; the orchestrator skips them without counting a circuit-breaker failure.
Changed
- All 20 read-service entity projections are now
@readonly(srv/cardano-service.cds). ExternalCREATE/UPDATE/DELETEagainstCardanoODataServiceentities now return HTTP 405 instead of mutating the cache that is served as authoritative blockchain data. Behavior change for any consumer that was (incorrectly) writing to these projections. - Schema data-type corrections (
db/schema.cds,db/types.cds) — these change the OData$metadatatypes consumers see:Epochs.startTime/endTime/firstBlockTime/lastBlockTime,Transactions.blockTime/slot,Assets.initialMintTime,AssetHistory.blockTime:Integer→Integer64(32-bit Integer overflows for Unix-second timestamps after 2038-01-19).TransactionMetadata.id:Integer→Integer64(the metadata label is a uint64; the 32-bit column overflowed for labels > 2³¹).MetadataLabeltype:String(5)→String(20)(a uint64 label is up to 20 digits;String(5)truncated any label above 5 digits).Assets.assetNameHex:HexBytes(String(5000)) →String(64)(32-byte ledger cap = 64 hex).- Key columns
Pools.poolId,Dreps.drepId,Accounts.stakeAddress, address fields:String→Bech32(bounded type). Pools,DrepsandAssetsentities are nowtemporal, so live pool/drep/asset stats refresh on TTL lapse instead of freezing on first index.
ASSET_UNIT_REGEXtightened to the 32-byte ledger cap (srv/utils/const.ts): asset-name portion bounded to 0-64 hex.GetAssetInfo/GetAssetHistorydescriptions updated from "0-128 hex" to "0-64 hex; ledger caps asset names at 32 bytes". Over-long asset names now reject with a clear 400.GetAssetHistorylimitis clamped to 1-100 (srv/cardano-service.ts,srv/cardano-service.cds): previously unbounded; nowMath.min(Math.max(limit, 1), 100).SubmitSignedTransactionnow verifies itsnetworkparameter against the deployment network (previously silently ignored).- Error-status corrections (consumer-visible HTTP codes):
- Missing build / submission record → 404
NotFoundError. - Malformed tx CBOR → 400
TX_PARSE_FAILED. - Script-parameter application failures in
BuildMintTransaction/BuildPlutusSpendTransaction→ field-attributed 400. AllBackendsFailedErrorwith no collected errors (every backend skipped the method) now surfaces 503 instead of 502.normalizeBackendError: address-shaped hints (invalid address/malformed address) now classify as 404 before the generic validation hints; removednot available/ bareno datafrom the not-found hints so provider outages stay 503 (and circuit-breaker-eligible) instead of being mislabeled 404.
- Missing build / submission record → 404
- Durable, crash-safe signing/submit flow (
srv/cardano-sign-service.ts):SubmitVerifiedTransactionnow submits in 3 committed phases (claim →submitting, network submit outside any open DB lock, finalize →submitted). A post-accept crash leaves a durablesubmittingrecord instead of silently reverting topending/verifiedwith no on-chain trace.VerifySignatureatomically claimspending→signedbefore verifying, fixing a concurrent-verify double-insert race.- Expiry transitions are now status-filtered (only
pendingrequests may expire). - Signature verification is now bound to the unsigned tx body's
required_signers(extra_signatories) plus the build's fee-payer key, not just any present witness.
- HSM hardening (
srv/blockchain/signing/hsm-signer.ts): rejects on failed verification, fully clears the PIN across singleton + env + config, slot documented as an index. - Buildooor tx-build hardening (
srv/blockchain/transaction-building/buildooor-tx.ts,srv/blockchain/cardano-tx-builder.ts):- All protocol parameters mapped with null guards; backend cost-model arrays converted to named-key form (raw arrays crash Buildooor's CEK machine); the
TxBuilderis rebuilt per request when thenetwork#epochfingerprint changes. datumHashnow carried into resolvedTxOuts and fabricated script UTxOs so datum preimages reach the witness set (fixesMissingRequiredDatumson hash-locked spends).- Collateral: picks the smallest ADA-only UTxO covering the 5-ADA floor and returns the excess via explicit
collateralReturn. - Metadata: text/bytes > 64 bytes are chunked (UTF-8-safe),
0xbyte strings supported, non-integer numbers and invalid labels/keys rejected with clear 400s. - UPLC 1.0.0 (Plutus V1/V2) scripts are now rejected at build time instead of being silently hashed as V3 (which produced a wrong policy ID / unspendable address).
- Force / reference / script UTxOs are verified unspent before building (clear 400 instead of a node-side rejection).
- All protocol parameters mapped with null guards; backend cost-model arrays converted to named-key form (raw arrays crash Buildooor's CEK machine); the
TxBuildRequest.lovelaceAmountis now typedstring(srv/utils/types.ts): ODataLovelaceisDecimal(20,0)and arrives as a string at runtime (CAP preserves precision); validators acceptstring | number | bigint.- Backend pagination fixes (
srv/blockchain/backends/*):- Blockfrost:
getAddressUtxos/ account-addresses use the...Allvariants — the plain variants capped at 100 entries and silently truncated larger wallets. - Koios:
address_txssorted newest-first before limiting; transactions fetched viagetTransactionsBatchinstead of an unboundedPromise.all; extendedreference_scriptobject unwrapped.
- Blockfrost:
- Ogmios data-correctness fixes (
srv/blockchain/backends/ogmios-backend.ts):- Protocol-parameter Ratio strings (e.g.
"3/1000") parsed properly —Number()previously yieldedNaNforrho,tau,a0and exec-unit prices; therho/tauswap is fixed. getAccountextracts lovelace from{ ada: { lovelace } }value objects (was rendering"[object Object]").- Network-aware epoch geometry via new
EPOCH_CONFIG_BY_NETWORKand Shelley-anchored slot times from the genesis infos the tx builder uses. - Ogmios no longer fabricates
getAddress/getNetworkInformationplaceholder data (whichpreferLiverouting then preferred over correct historical data) and now maps inline datums onto UTxOs.
- Protocol-parameter Ratio strings (e.g.
- Backend capability routing & resilience (
srv/blockchain/cardano-client.ts,srv/blockchain/circuit-breaker.ts):- Orchestrator skips a backend for any method in its
unsupportedMethodsset without counting a circuit-breaker failure. - All 4xx responses are exempted from the circuit breaker — a definitive answer from a healthy backend must not take it out of rotation.
- New
callWithResiliencewraps every single-backend path with a breaker gate + timeout — most importantlyevaluateTransaction, which previously hung Plutus builds indefinitely on a dead Ogmios socket. - Lazy Ogmios reconnect on dead sockets, retryable client init, half-open probe cap.
- Orchestrator skips a backend for any method in its
- Caching / lifecycle / config robustness (
srv/server.ts,src/plugin.ts,srv/blockchain/cardano-indexer.ts,srv/blockchain/cardano-client.ts):indexTtlMsis now wired into the indexer's cache TTL (was hardcoded 60s).- Config loading moved inside the served-hook
try(a malformed config previously crashed the plugin-mode bootstrap);bootstrapErroris now set in plugin mode too. - Plugin mode now honors
SKIP_AUTO_INIT=true(matchessrv/server.ts), so consumer test suites can mount the plugin without opening real backend connections.
Fixed
mapBlockstores a realnullslotLeaderinstead of a placeholder; BigInt asset-quantity fallback added (srv/utils/mappers.ts).- Hash-length-only
scriptRefwritten into theBlake2b256column for Koios CBOR-truncation cases (srv/utils/mappers.ts). - Falsy-key guard in
indexOnMissRead(srv/cardano-service.ts):key &&skipped validation for falsy keys likeepoch=0or an empty string; now an explicitnull/undefinedcheck. READ AssetHistorynow runs throughhandleRequest(was returningreq.queryraw, bypassing error handling).- Malformed witness sets now produce a warning instead of an opaque failure during signing.
Internal
- Type-aware ESLint enabled (
eslint.config.mjs):projectService+ curated typed rules, CAP lifecycle hooks exempted, generated/repro artifacts ignored;src/**added totsconfig.jsoninclude. - Dead code removed: unused exports/request fields (
parseOptionalJson*,containsIndexPlaceholder,feeLovelace,executionUnits) and their tests;ProtocolParameterstype imported from the package root;__INPUT_IDX__regex made case-insensitive. - Tx-building paths consolidated: shared
_resolveInputRefs,_buildSimpleTransfer, and_buildMintEntriesacrosscardano-tx-builder.tsandbuildooor-tx.ts. - Test additions:
indexAddressUtxosunit tests, OgmiosBackendunsupportedMethodscases, and updated error-handling expectations across the unit and integration suites. Suite total now 35 suites (25 unit + 10 integration), 1549 tests, 96.58% statement coverage. - Version bumped
1.8.0→1.9.0.
- UTxO-only indexing fallback for
Removed 3, Features 1, Changed 2, Fixes 4, Other 4
Notes
Removes the
@emurgo/cardano-serialization-lib-nodejs(CSL) dependency entirely. Buildooor (@harmoniclabs/buildooor) becomes the single transaction-building engine, and all hashing / signature-verification work moves onto the HarmonicLabs raw-CBOR stack. Net effect: −2806 lines, one fewer native WASM dependency, and the long-standing Plutus V3PPViewHashesDontMatchbug is resolved by construction.Changed
- Buildooor is now the only transaction builder.
TxBuilderRegistryand the builder-factory indirection are gone —CardanoTransactionBuilderconstructsBuildooorTxBuilderdirectly.TransactionBuilderNameis narrowed to the single literal'buildooor'; theTX_BUILDERSenv var /txBuildersconfig key is still accepted for backward compatibility but is effectively a no-op (any value resolves to Buildooor). No public read/write OData action signatures changed. - Transaction-body hash and Ed25519 signature verification ported off CSL to
@harmoniclabs/cbor+@harmoniclabs/crypto(srv/blockchain/signing/signature-verifier.ts,srv/utils/tx-build-helper.ts,srv/utils/signing-helper.ts). The body hash is now computed asblake2b_256over the original transaction-body bytes (CborArrayindex 0, viasubCborRef) with no re-serialization — so the hash always matches what was signed, and operating at the raw-CBOR level also sidesteps the@harmoniclabs/cardano-ledger-tsAuxiliaryData.fromCborbug on metadata-onlyaux_data.
Fixed
- CSL
PPViewHashesDontMatchon Plutus V3 — resolved. With CSL removed, Buildooor computes the correctscriptDataHashfor both mint and spend builds (raw-CBOR, byte-preserving body hash), so Plutus V3 transactions no longer fail script-data-hash validation on submit. - Buildooor no longer aborts unsigned-tx builds on local script evaluation failure (
srv/blockchain/transaction-building/buildooor-tx.ts, commit8a77cbf). Buildooor evaluates every Plutus script locally insidebuild(); unlike CSL it would throw if that evaluation errored, turning script-bearing builds (e.g. parameterized validators applied viascriptParamsJson+lockOnScript) into HTTP 500s. A sharedSCRIPT_BUILD_OPTSwith anonScriptInvalidhandler now downgrades a local evaluation failure to a warning and still returns the unsigned CBOR + fee estimate — restoring the pre-Buildooor contract where on-chain validation at submit time is authoritative. Validators that pass local evaluation are unaffected, so genuine failures surfaced by Ogmios (ctx.evaluateTransaction) are still reported. Fixes the twotx-handler-validationcases (scriptParams + lockOnScript + fingerprint,BuildPlutusSpendTransaction — lockOnScript). extractVkeyWitnessesparses witness pairs defensively (signature-verifier.ts): a malformed or unexpected witness structure previously threw a runtimeTypeErrorvia unchecked CBOR casts and surfaced as an opaque internal error. Entries that are not a[vkey, signature]pair of byte strings are now skipped viainstanceofguards.getTxHashFromCborhex validation rejects odd-length input (tx-build-helper.ts): a hex byte string must be even-length, so"abc"now fails with the clearInvalid input: txCbor must be a valid hex stringinstead of a confusing downstreamFailed to parse transaction CBOR.
Removed
- Dependency
@emurgo/cardano-serialization-lib-nodejsdropped frompackage.json(it remains only as a transitive, never-imported dependency of@blockfrost/blockfrost-js). - Source:
srv/blockchain/transaction-building/csl-tx.ts(1064 lines) andsrv/blockchain/transaction-building/tx-builder-registry.ts(69 lines). - Tests:
test/unit/csl-tx-builder.test.ts(767 lines),test/unit/tx-builder-registry.test.ts(172 lines),test/integration/tx.csl.test.ts.
Added
- Dependencies promoted to direct:
@harmoniclabs/cbor,@harmoniclabs/crypto,@harmoniclabs/uint8array-utils(previously transitive via Buildooor; now imported directly for hashing + signature verification).
Internal
scripts/testing/lock-ada-at-script-preview.tsported off CSL —deriveScriptAddressnow uses the same server path (Script.fromCbor(...).hash→scriptHashToEnterpriseAddress) so a fresh install with CSL removed still resolves all imports and derives an identical enterprise script address.- Docs refreshed to reflect the single-builder architecture:
DEVELOPER_GUIDE.mdsource tree (listscardano-tx.tsinterface +buildooor-tx.ts, registry line removed), plusQUICK_START.md,BACKEND_CONFIGURATION.md,PRODUCTION_DEPLOYMENT.md,TRANSACTION_WORKFLOW.md,INDEXING.md, andREADME.md. - Test suite migrated off the CSL/registry fixtures across
tx-handler-validation,signing-services,signing,cardano-tx-builder,server, and the sharedtx-test-suiteharness; CI workflows (test.yaml,ogmios-sync.yaml) updated. - Version bumped
1.7.9→1.8.0.
- Buildooor is now the only transaction builder.
Changed 1, Fixes 1, Other 2
Notes
Fixed
- Koios
/block_infomapper readslot_no/epoch_slot_no— fields that Koios has never returned. The real wire keys areabs_slotandepoch_slot(stable since Koios v1). Symptom: every call throughKoiosBackend.getLatestBlock()produced aBlockDatawhose.slotand.epochSlotwereundefined, which made the v1.7.8getCurrentSlot()implementation throwProviderUnavailableError: koios: latest block has no sloton every chain-tip query. Cascaded into anything ttl-bounded: x402 nonce checks, ttl-bounded tx builds,getCurrentSlot-routed paths on Koios-only deployments. One-line mapping correction atsrv/blockchain/backends/koios-backend.ts:214-216(epoch_nowas already correct). DirectgetBlock(hash)callers also benefit — previously they received aBlockDatawithundefinedslot fields silently.
Changed
KoiosBackend.getCurrentSlot()rewritten to read/tipdirectly. Previously it calledgetLatestBlock(), which fetched/tipand then/block_infofor that tip's hash — two round-trips for a single integer. The new implementation readsabs_slotstraight off/tip. Side benefits: avoids a real race where/tipreturns a freshly-minted hash that/block_infothen returns[]for several seconds while Koios's read replicas catch up (was surfacing as a spuriousNotFoundErrorpropagating out ofgetCurrentSlot), and removes the chain-tip-only path's dependency ongetLatestBlock's mapper. Negative-path behavior preserved: empty/tip→NotFoundError,/tiprow missingabs_slot→ProviderUnavailableError.
Internal
- Test fixture corrected in
test/unit/koios-backend.test.ts:getCurrentSlotmock now uses the real Koios/tipshape (abs_slot/epoch_slot) instead of the never-realslot_no/epoch_slot_no, which is the reason the original mapper bug slipped through unit tests. Two new negative cases added (empty/tipreturningNotFoundErrorafter 3 retries;/tiprow missingabs_slotreturningProviderUnavailableError). - Version bumped
1.7.8→1.7.9.
- Koios
Features 4, Changed 2, Fixes 1, Other 4
Notes
Added
CardanoClient.getCurrentSlot(): Promise<number>— convenience wrapper overgetLatestBlock().slotwith a guaranteed non-null return. ThrowsProviderUnavailableErrorwhen the backend's latest block reports a null slot (very early chain or backend lag). Centralizes thenull→ error translation that consumers were re-inventing per call site. Method routing:preferLive: true.CardanoClient.isUtxoUnspent(txHash, outputIndex): Promise<boolean>— checks whether a UTxO is still spendable. Returnsfalsefor both spent UTxOs and txs that never existed on chain; throws on transport/provider failure. Replaces x402's prior 3-arg shim ((txHash, outputIndex, holdingAddress)) which required a separategetTransactionByHashto resolve the address first — saves one chain round-trip per nonce check on the facilitator happy path. Method routing:preferLive: true.- Per-backend implementations with edge-case parity across all three providers:
- Blockfrost —
/txs/{hash}/utxos→outputs[].consumed_by_tx(null ⇒ unspent, string ⇒ spent). Output entries are matched byoutput_index(not array position) for defensive ordering.consumed_by_txis an optional field on the Blockfrost openapi type (added in server v0.1.59, mid-2024) — if absent, ODATANO throwsProviderUnavailableErrorso the router falls through to the next backend rather than silently lying with "always true". - Koios —
POST /utxo_infowith_utxo_refs: ["<txHash>#<index>"]and_extended: false→is_spent === false. Empty response array maps tofalse(nonexistent UTxO).txHashis lowercased before building the ref to match Koios's case-sensitive lookups. - Ogmios —
queryLedgerState/utxowith{ outputReferences: [{ transaction: { id: txHash }, index }] }(schema-typed param shape). Non-empty result ⇒ unspent; empty ⇒ spent OR nonexistent (Ogmios can't distinguish).
- Blockfrost —
- Fast-path on invalid
outputIndex— negative or non-integer values short-circuit tofalsewithout a network round-trip on all three backends.
Changed
- Error semantics for unsupported / provider-down paths (commit
af17b46): genericErrorthrows upgraded to typedProviderUnavailableErrorso callers can branch on the error class and the router's circuit breaker registers them as backend failures rather than uncategorized exceptions. Affected sites:OgmiosBackend.getDrep,getAssetInfo,ensureNotShutdown— capability mismatch + shutdown guard now carry theogmiosbackend name on the typed error.CardanoClient.evaluateTransaction— missing evaluating backend now throwsProviderUnavailableErrorinstead of bareError.
- Transaction-validation errors in tx builders (commit
af17b46): missing-script-UTxO throws inCardanoTransactionBuilder._resolveReferenceInputsandBuildooorTxBuilderare nowTransactionValidationErrorinstead of genericError. Surfaces as HTTP 400 with a structured code throughhandleRequest()rather than a generic 500.
Fixed
- Test typecheck cleanups (pre-existing on
main, unblockingtsc --noEmit):test/unit/cardano-tx-builder.test.ts:298—ScriptValidatorrequirespurpose; mock validator object was missing it.test/unit/errors.test.ts:614—BackendInitError.originalErroris typedunknown; access narrowed viaas Error | undefined.test/unit/ogmios-backend.test.ts:500—ScriptValidatorisstring | { purpose, index }; structured branch narrowed via type assertion.test/unit/plugin.test.ts—cds.listeners/cds.emit/cds.removeAllListenersare runtime EventEmitter methods not on the public typedcdsimport; single typed aliascdsBus = cds as unknown as EventEmitteradded.
Internal
CardanoBackendinterface (srv/blockchain/backends/cardano-backend.ts): two new required method signatures inserted aftergetLatestBlock(). All three backends implement them — no?:optional escape hatch.CardanoClientrouting: two newMETHOD_ROUTINGentries (getCurrentSlot,isUtxoUnspent, bothpreferLive: true). No request-coalescing (consistent with other live-tip methods likegetLatestBlock).- Test additions (mocked, no live preprod):
test/unit/blockfrost-backend.test.ts(+9 cases — slot null, consumed states, out-of-range/negative index, 404, missing field),test/unit/koios-backend.test.ts(+7 cases — is_spent true/false/empty, lowercasing, negative + non-integer index),test/unit/ogmios-backend.test.ts(+5 cases — non-empty/empty result, outputReferences shape assertion, negative index),test/unit/cardano-client.test.ts(+4 cases — Ogmios-preferred routing + historical fallback for both methods). - Version bumped
1.7.7→1.7.8.
Features 3, Changed 1, Other 3
Notes
Added
blockfrostCustomBackendconfig +BLOCKFROST_CUSTOM_BACKENDenv var: optional base URL that redirects ODATANO's Blockfrost backend at a Blockfrost-wire-compatible self-hosted node — Dolos MiniBF, Demeter Self-Hosted, or any compatible proxy. Forwarded straight through to@blockfrost/blockfrost-js's upstreamcustomBackendoption; the entire Blockfrost surface (blocks, txs, utxos, assets, governance, mint history) works against the self-hosted node with zero ODATANO-side mapping changes.- API key becomes optional when
BLOCKFROST_CUSTOM_BACKENDis set: theBlockfrostBackendconstructor now accepts(network, timeoutMs, projectId, customBackend?)and requires onlyprojectId OR customBackend(matching the upstream SDK validator at@blockfrost/blockfrost-js/lib/utils/index.js). When pointed at a customBackend without a key, ODATANO sendsself-hostedas theproject_idheader — Dolos rejects empty header values even though it doesn't authenticate against them. Startup-log lineBlockfrost will use customBackend: <url>makes the redirect visible to operators. CardanoClientConfig.blockfrostCustomBackend?: stringadded to the public TypeScript surface (re-exported fromsrc/index.ts). Additive — non-breaking.
Changed
loadConfigFromEnvwarning atsrv/server.ts: theBLOCKFROST_API_KEY is not setwarning now fires only when bothBLOCKFROST_API_KEYANDBLOCKFROST_CUSTOM_BACKENDare empty — previously a confusing warning would appear for self-hosted setups that only set the URL.
Internal
- Light URL validation:
loadConfigFromEnvrejectsBLOCKFROST_CUSTOM_BACKENDvalues that do not begin withhttp://orhttps://upfront, with a clear error message — matches the existing throw style used for timeout validation. - Test additions:
test/unit/blockfrost-backend.test.tsconstructor describe block expanded to 6 cases (missing-both error path, projectId-only, customBackend-only, customBackend forwarded into SDK, dummy'self-hosted'substitution, customBackend omitted when absent). Newtest/integration/blockfrost-custom-backend.test.ts(4 cases) exercises the real SDK againstnock— the only way to prove URL forwarding actually works end-to-end vs. just being stored on the options object. - Version bumped
1.7.6→1.7.7.
Features 5, Changed 3, Other 6
Notes
Driven by CHAINFEED's Sprint-1 oracle-adapter integration feedback. Goal: every direct Koios/Blockfrost call CHAINFEED currently bypasses the bridge with should be routable through ODATANO instead.
Added
- Inline-datum hydration on
AddressUTxOs:utxodata.inlineDatumis now populated for every UTxO returned byGetUTxOsByAddress/Addresses.utxos/ cached child rows. Previously the field existed in the schema but was discarded by the Blockfrost mapper, which forced consumers (e.g. Indigo CDP / Liqwid / Minswap V2 readers) to issue ~500 extraGetTransactionByHashcalls per protocol-state read. New helperinlineDatumToHex(datum)insrv/utils/tx-build-helper.tsnormalizes Blockfrost's CBOR-hex strings, Koios's_extended{bytes, value}wrapper, and rawPlutusDataJSON forms to a single canonical lowercase hex CBOR — consistent regardless of which backend served the row. GetUTxOsByCredentialaction onCardanoODataService: returns UTxOs across all bech32 forms sharing a 28-byte payment credential (key hash or script hash). Solves the Indigo-style "two bech32 variants of the same script" problem (addr1z…with stake-cred vsaddr1w…without) with a single round-trip. Always-fresh fetch (no cache check) — credential queries serve dApp state-read use cases that need current data. New backend methodgetCredentialUtxos(credHash), new indexer methodindexCredentialUtxos, new validatorisValidCredential(s)(56-char lowercase hex) backed byHEX_56_REGEXinsrv/utils/const.ts. Koios-only —CardanoClient.getCredentialUtxosthrowsProviderUnavailableErroron Blockfrost-/Ogmios-only deployments rather than silently returning incomplete results from a one-bech32 fallback. Concurrent calls for the same credential are deduplicated through a newcredCoalescer(analogous to the existingtxCoalescer/addrCoalescer).Assetsentity +GetAssetInfoaction: native-asset metadata lookup (total supply, mint/burn count, initial mint tx, CIP-25 on-chain metadata, CIP-26 off-chain Cardano-Foundation registry fields). Closes the gap that previously forced consumers to call Blockfrost / Minswap directly for supply telemetry. Multi-backend (Blockfrost viaassetsById, Koios viaPOST /asset_info); Ogmios throws "not supported". Backend divergence is documented in the action description: Blockfrost lacksinitialMintTime(would need an extra tx fetch), Koios provides it fromcreation_time. NewmapAsset(providerData, max_age)mapper, newindexAsset(tx, unit)indexer method.AssetHistoryentity +GetAssetHistory(unit, limit)action: paged mint/burn-event lookup for "supply growth rate" telemetry (CHAINFEED Sprint-2 use case for DJED / iUSD adapters). Free-standing (not a Composition) and@readonlybecause mint events are immutable on-chain — UPSERT keyed on(unit, txHash)is idempotent. Koios-preferred routing inCardanoClient.getAssetHistory(provides block timestamps viablock_time); Blockfrost is the fallback and now backfillsblockTime/blockHeightvia concurrentapi.txs(...)calls (cost: 1 extra API call per history entry, capped atMAX_CONCURRENT = 10; failed fetches leave the timestamp fields null rather than aborting the whole call). Both backends deriveaction: 'mint' | 'burn'and absolutequantity: Blockfrost from theaction: 'minted'/'burned'enum, Koios from the sign ofquantity.Addresses.utxoCount: pre-aggregated UTxO count on theAddressesentity. Removes the need for dashboard / health-check consumers to fetch and.lengththe full UTxO array client-side. Populated from the existinggetAddressresponse — no extra API call.
Changed
- Inline-datum format harmonized across backends. Previously
AddressUTxOs.utxodata.inlineDatumandTransactionInputs/Outputs.utxoData.inlineDatumcould carry hex CBOR (Blockfrost), JSON-stringified PlutusData (KoiosgetAddressUtxos), or a raw object wrapper (KoiosgetTransaction). All three paths now route throughinlineDatumToHex(...)and produce hex CBOR ornull. Cached rows from before this release have stale formats until their TTL expires. Buildooor _parseInlineDatumremoved fromsrv/blockchain/transaction-building/buildooor-tx.ts— the JSON-string and raw-object branches became defensive dead code after the inline-datum normalization. The remaining call site at_mapMultiAssetUtxoToLedgerUtxonow invokesdataFromCbor(utxo.inlineDatum)directly. Corresponding branch-coverage tests removed fromtest/unit/buildooor-tx-builder.test.ts.- CDS using-clauses:
db/schema.cdsnow importsBlake2b224andHexBytes(used by the newAssetsentity);srv/cardano-service.cdsnow importsAssetUnit(used as parameter type on the two new asset actions).
Internal
- CardanoClient routing matrix expanded —
getAssetInfo(preferLive: false, both Blockfrost + Koios),getAssetHistory(Koios-preferred, Blockfrost-fallback),getCredentialUtxos(Koios-required, throws otherwise). The Koios-/Blockfrost-/Ogmios-feature matrix is now documented in.claude/CLAUDE.mdunder "Backend-Specific Features (no fallback)". - Test additions:
test/unit/tx-build-helper.test.ts(12 cases forinlineDatumToHex),test/unit/validators.test.ts(5 cases forisValidCredential),test/unit/blockfrost-backend.test.ts(+8 cases: address-utxos hydration + asset-info + asset-history),test/unit/koios-backend.test.ts(+12 cases: credential-utxos + asset-info + asset-history),test/unit/cardano-client.test.ts(4 cases for credential-routing hard-fail),test/unit/cardano-indexer.test.ts(+8 cases forindexCredentialUtxos/indexAsset/indexAssetHistory),test/unit/mappers.test.ts(+5 cases formapAsset,mapAssetHistory,Address.utxoCount). - Version bumped
1.7.5→1.7.6.
Known limitations
- Asset entity field availability differs per backend:
initialMintTimeis null on Blockfrost (would require 1 extra tx fetch per asset); CIP-25onchainMetadatashape varies per minter and is stored as a JSON-stringifiedLargeString(consumers parse it). The action@descriptioncalls this out. AddressUTxOsrows from before this release:utxoCountand the new normalizedinlineDatumonly appear after the parent address's TTL (indexTtlMs, default 1 h) lapses andindexAddressre-runs.AssetHistoryBlockfrost cost: backfilling block metadata costs 1 extraapi.txs(...)call per entry (seeAdded). Forlimit=100, that's ≤101 Blockfrost calls; consumers paying per-call should sizelimitappropriately. Concurrency capped atMAX_CONCURRENT = 10.
- Inline-datum hydration on
Features 6, Changed 4, Fixes 3, Other 2
Notes
Added
ParseTransactionCboraction onCardanoODataService: decodes a hex-encoded transaction CBOR (signed or unsigned) into a structured representation (inputs/outputs/fee/witnesses/auxiliary data). Implementation lives in pure utilities atsrv/cbor/parse.tsand is re-exported fromsrc/index.tsfor direct programmatic use.- CBOR hex validation for
ParseTransactionCbor: explicit length cap and hex-shape checks reject oversized payloads upfront (memory-exhaustion guard) and surface dedicated error codes fromsrv/utils/error-codes.ts. lockOnScriptflag onBuildPlutusSpendTransaction: whentrue, change is sent back to the script address instead of the sender — required for stateful validators that must keep their UTxO under the script.DeriveScriptAddressaction: derives the bech32 script address (network-aware) from a Plutus V3 validator hex, optionally applying script parameters first. Useful for clients that need the address before locking funds.ExtractPaymentKeyHashaction: bech32-decodes a payment address and returns the 28-byte payment credential hash — convenience for building required-signers / datum fields client-side.- Validity-window bounds (
validityStartMs/validityEndMs): both Build endpoints accept Posix-ms validity bounds. Buildooor converts viaposixToSlot()usingGENESIS_INFOS_BY_NETWORK(const.ts); CSL still ignores them pending the PPViewHashesDontMatch fix.
Changed
- Buildooor validity-window passthrough: when no explicit bounds are provided, Buildooor falls back to
DEFAULT_VALIDITY_START_OFFSET_MS(-2 min) /DEFAULT_VALIDITY_END_OFFSET_MS(+1 h) to absorb clock skew while staying generous for human sign+submit latency. getTxHashFromCborparameter rename:signedTxCbor→txCbor. The function accepts both signed and unsigned CBOR (hash is body-only). JSDoc and validation error messages updated accordingly.BlockfrostBackendconstructor: network is now passed explicitly intoBlockFrostAPIto fix preprod initialization, which previously fell back to mainnet under certaincardanoNetworkresolution paths.- Validity-bounds validation added to
validateTransactionInputs(): rejects non-numeric values, negative timestamps, more thanMAX_POSIX_MS_DIGITS(13) digits, andvalidityStartMs > validityEndMs.
Fixed
- Forced/reference input UTxO synthesis:
_resolveForceInputsand_resolveReferenceInputsnow copydataHashandreferenceScriptHashfromTransaction.outputs[]into the synthesizedUTxO'sdatumHash/scriptReffields. Previously dropped, which prevented Buildooor input-side ref-script preservation from seeing them on resolved (non-sender) UTxOs. forcedInputsUsedaccuracy (CSL): mint and Plutus-spend paths now derive the count from the actual_partitionForcedInputsresult instead ofreq.forceInputs.length. Eliminates over-count from request-side duplicates and refs not present inctx.utxos. The Plutus-spend path additionally subtracts the script-UTxO ref so it never counts toward forced inputs.MAX_POSIX_MS_DIGITSdoc: corrected the comment ("year 9999" → "Unix ms timestamps through ~Nov 2286"). The 13-digit cap itself is unchanged.
Internal
- New
srv/cbor/module:parse.ts(decoder) +index.ts(barrel). Pure utilities — no CSL or Buildooor dependency. - Version bumped
1.6.1→1.7.5. Intermediate1.7.0–1.7.3were not released externally.
Features 3, Changed 6, Fixes 2, Other 7
Notes
Added
- CIP-33 reference script deploy (
referenceScriptHexparameter): attach a Plutus V3 validator as a referenceScript on the primary output so consumers can deploy ref-scripts through ODATANO instead of bypassing it.- Available on
BuildSimpleAdaTransaction,BuildMultiAssetTransaction,BuildMintTransaction,BuildPlutusSpendTransaction. - Supported by both Buildooor and CSL builders. CSL uses
PlutusScript.new_v3()(CBOR-wrapped) per the v15 hashing rule. - Note: attaching a ref script inflates output min-ADA significantly (typically 15–30+ ADA depending on script size). Consumers must supply enough
lovelaceAmountto cover it — aTransactionValidationErroris thrown upfront with the required min-ADA if underfunded.
- Available on
- Per-extraOutput
referenceScriptHex: each entry inextraOutputsJsonmay now carry its ownreferenceScriptHex, enabling "spend + deploy ref-script on a dedicated extra output" flows in a single atomic transaction. - Input-side
refScriptpreservation (Buildooor only, Koios-sourced UTxOs): when a forced or reference input carries its ref-script bytes, the Buildooor UTxO mapper now passes them through for local Plutus evaluation. Blockfrost and Ogmios backends return hash-onlyscriptReftoday — those UTxOs continue to resolve server-side at validation.
Changed
- Buildooor upgrade:
@harmoniclabs/buildooorbumped from^0.1.28to^0.2.6. Buildooor's public API is byte-identical between these versions; the migration is driven entirely by transitive deps. - Cardano ledger types:
@harmoniclabs/cardano-ledger-tsbumped from^0.4.6to^0.5.1. - Cost models:
@harmoniclabs/cardano-costmodels-tsbumped from~1.3.0to~1.4.0. The 1.4.0 API dropped.toBuffer()oncostModelsToLanguageViewCbor()return values, now a rawUint8Array. - CBOR / UPLC encoding sites: removed
.toBuffer()on nine call sites inbuildooor-tx.ts,csl-tx.ts,signing-helper.ts,hsm-signer.ts,tx-build-helper.ts, and two integration test sites. The@harmoniclabs/cbor2.x and@harmoniclabs/uplc2.x packages now returnUint8Arraydirectly, matchingtoHex()consumption. applyScriptParameters(tx-build-helper.ts): rewritten for uplc 2.x —compileUPLC()now returnsUint8Arraydirectly; outputtoString()replaced withtoHex()(needed becauseUint8Array.toString()returns a CSV of bytes, not hex).- Blockfrost-js pin:
@blockfrost/blockfrost-jsnarrowed from^6.0.0to~6.0.0to keep nock-based test mocks aligned with the 6.0.x HTTP behaviour (6.1.x retries triggertimes()mock exhaustion).
Fixed
- Buildooor transaction build crashes with
costModelsToLanguageViewCbor(...).toBuffer is not a function— root cause was the v1.5.xcardano-costmodels-tspin leaking 1.4.0 through the^range. The Buildooor upgrade + removed.toBuffer()calls close this permanently. - Test suite hygiene (caused by transitive-dep drift, not production regressions):
cardano-client.test.ts:setupBlockfrostHealthMockwas mocking/api/health, butBlockfrostBackend.init()hits/api/v0/blocks/latest. Corrected, plus added.times(5)on 500-response mocks to absorb got's default 5xx retries during init.koios-backend.test.ts:fetchWithRetryOnEmptyperforms 1 initial + 3 retries = 4 attempts. Two tests that mocked.times(2)were leaking unhandled async errors (via the 2000 ms retrysetTimeout) into subsequent tests. Corrected to.times(4).error-paths.test.tscircuit-breaker test:nock.pendingMocks()counting was unreliable because got's internal retries consume multiple HTTP requests per backend-level call. Replaced withjest.spyOn(backend, 'getNetworkInformation')to count backend invocations directly.hsm-signer.test.ts:jest.mock('pkcs11js', ...)now uses{ virtual: true }so the suite runs on machines without the optionalpkcs11jsnative module installed.server.test.ts: removed the stale "should rethrow served hook initialization errors" test — contradicts the served hook's intentional error-swallow (plugin contract: host app must not crash on plugin init failure).
Internal
- CDS
extraOutputsJson@descriptionexpanded to document the new per-entryreferenceScriptHexfield. - Version bumped
1.5.2→1.6.1(first 1.6.x release).
Known limitations
- Hash-only
scriptRefresolution: Blockfrost and Ogmios returnreferenceScriptHashonly, not the script bytes. A future release may add a/scripts/{hash}/cborresolver so hash-only UTxOs reach Koios parity for local Plutus evaluation. - CSL still rejects
__INPUT_IDX__placeholders: input-index placeholder substitution remains Buildooor-only (CSL's coin selection is opaque to enumeration).
Follow-ups (deferred)
/scripts/{hash}/cborresolver across all three backends.- Normalizing
UTxO.scriptRefinto separatereferenceScriptHashandreferenceScriptfields (currently overloaded). - Upgrading Buildooor beyond 0.2.6.
- CIP-33 reference script deploy (
Features 4, Changed 6, Fixes 4, Docs 3, Other 7
Notes
Added
- Request Coalescing: Deduplicates concurrent backend requests for the same resource, reducing redundant API calls and improving performance under load
- CardanoIndexer Unit Tests: 19 new tests covering entity mapping, cache TTL validation, error handling, and metadata indexing edge cases
- Request Coalescer Tests: 3 new tests for concurrent deduplication, retry-after-failure, and key isolation
- Expanded Test Coverage: Additional branch coverage tests for validators, backends, transaction handlers, and builders
Changed
- Hardened Error Handling: Improved null/undefined guards across service layer, address flags, and debug logging
- Protocol Parameters Refresh: Hardened refresh logic with improved input validation and datum mapping
- Koios Backend Resilience: Added retry-on-empty-array for block and epoch queries, null/array validation before array access
- Service Layer Resilience: Strengthened error propagation, edge case handling, and fallback behavior
- Performance Optimizations: Batch methods and request coalescing for transaction fetching (N+1 query elimination)
- Authentication: Added
@requires: 'authenticated-user'on all 3 services with XSUAA production configf
Fixed
- Type safety improvements across codebase
- Edge cases in protocol parameter parsing and cost model handling
- Plutus datum mapping errors for spend transactions with Koios/Buildooor combination
- Various small bugs in backend logic and service handlers
Documentation
- Updated all documentation to v1.0
- Reworked performance report with raw benchmark result files
- Updated test statistics: 31 test suites (21 unit + 10 integration), 1285 tests, 99% statement coverage
Stats
- Test Suites: 31 (21 unit + 10 integration)
- Tests: 1285 (all passing)
- Statement Coverage: 99%
- CDS Entities: 29
- Actions: 34 (15 read + 11 transaction + 8 signing)
- Services: 3 (CardanoODataService, CardanoTransactionService, CardanoSignService)
- Backends: 3 (Blockfrost, Koios, Ogmios)
Features 18, Changed 4
Notes
Added
-
External Signing Module: Complete external signing workflow with private key isolation
ExternalSignerModule- Signing request creation and workflow managementSignatureVerifier- Cryptographic signature verification- CIP-30 browser wallet support (Nami, Eternl, Yoroi, etc.)
- Cardano CLI signing support
- HSM signing support (PKCS#11 compatible hardware wallets)
-
CardanoSignService (
/odata/v4/cardano-sign/): New dedicated signing workflow service (3rd CDS service) -
5 New Entities for signing workflow:
SigningRequests- Unsigned transaction export with TTL-based expirationSignatureVerifications- Cryptographic verification results and audit trailAddressSigningRequests- Address-to-signing-request associationsAddressTransactionBuilds- Address-to-build associationsAddressTransactions- Address transaction history with net amounts
-
5 External Signing Actions (OData POST endpoints on CardanoSignService):
CreateSigningRequest- Create signing request for external signingGetSigningRequest- Retrieve signing request (auto-expires if TTL exceeded)VerifySignature- Cryptographically verify signed transactionSubmitVerifiedTransaction- Verify and submit in one stepGetSigningRequestsByAddress- Get signing requests for an address
-
Centralized App Context Architecture: Refactored initialization in
server.tsgetAppContext()- Get singleton application contextgetCardanoIndexer()- Convenience function for servicesgetCardanoClient()- Convenience function for servicescreateTestContext()- Create isolated test contextsshutdownAppContext()- Graceful connection cleanup
-
CIP-30 Wallet Integration:
combineTransactionWithWitnesses()- Combine unsigned TX with CIP-30 witness setisWitnessSetCbor()- Detect witness set vs full transaction- Automatic handling in SubmitVerifiedTransaction
-
HSM Signing Integration (Hardware Security Module):
SignWithHsm- Sign transaction using configured HSM (PKCS#11)SignAndSubmitWithHsm- Sign and submit transaction atomically via HSMGetHsmStatus- Check HSM connection status, key info, and Cardano address
-
Signing Workflow States:
SigningStatusenumpending- Request created, awaiting signingsigned- Transaction has been signedverified- Signature verified, ready for submissionsubmitted- Transaction submitted to networkexpired- Request expired (30 minute default TTL)failed- Signing or verification failed
-
New Test Suites (5 new test files):
signing-services.test.ts- External signing integration testssigning.test.ts- SignatureVerifier and ExternalSignerModule unit testshsm-signer.test.ts- HSM signer unit testscip14-fingerprint.test.ts- CIP-14 asset fingerprint computation teststx-build-helper.test.ts- Transaction build helper utility tests
-
Production Deployment Guide:
PRODUCTION_DEPLOYMENT.mdwith deployment patterns (incl. BTP) -
SAP Integration Examples: New guide with detailed examples of SAP workflows integrated with ODATANO, including screenshots and ABAP code snippets for real-world use cases (e.g., invoice payment verification, tokenized asset management)
-
Security Guide:
SECURITY.mdwith best practices for secure deployment, key management, and external signing workflows -
Postman Collection M3: Pre-configured requests for all M3 endpoints (signing, Plutus, HSM)
-
2 New Transaction Actions (Plutus Smart Contracts & Collateral):
BuildPlutusSpendTransaction- Spend UTxO locked at a Plutus validator script address (supports PlutusV3, redeemer/datum JSON, Ogmios execution unit evaluation, optionalinlineDatumJsonfor state-machine continuing outputs)SetCollateral- Ensure a dedicated ADA-only collateral UTxO exists for Plutus transactions (auto-checks address UTxOs, builds self-send if needed)
-
End-to-End Plutus Scripts:
lock-ada-at-script-preview.ts- Lock ADA at a PlutusV3 script address with inline datumplutus-spend-preview.ts- Spend locked UTxO with redeemer, verified on Preview testnetsend-ada-hsm-preview.ts- HSM signing workflow on Preview testnetsign-cbor.ts- Offline CBOR signing (Cardano CLI pattern)
-
Plutus Parameterized Validator Support:
scriptParamsJsononBuildMintTransactionandBuildPlutusSpendTransaction— apply UPLC parameters to unapplied validators, returnsscriptHash(= policy ID)requiredSignersJson— setrequired_signersin tx body for Plutusextra_signatorieschecksinlineDatumJsononBuildMintTransaction— attach inline datum on minted token output (for spend validators that readInlineDatum)inlineDatumJsononBuildPlutusSpendTransaction— attach inline datum on continuing output (state-machine patterns)mintRedeemerJson— custom redeemer for minting policy (defaults to integer 0)fingerprint— CIP-14 asset fingerprint (asset1...) returned inBuildMintTransactionresponse
-
lockOnScriptonBuildMintTransactionandBuildPlutusSpendTransaction:- When
trueandscriptParamsJsonis provided, routes the output to the enterprise script address derived from the applied script hash - Returns
scriptAddress(bech32) in the response — eliminates consumer-side script address computation - New
scriptAddressfield onTransactionBuildsentity
- When
-
Extended Transaction Actions for Script Locking:
BuildSimpleAdaTransactionnow supports optionaloutputDatumJsonandassetsJson— send ADA + native assets with inline datum to script addressesBuildMultiAssetTransactionnow supports optionaloutputDatumJson— attach inline datum when sending assets to script addresses
Changed
- Architecture refactored to centralized App Context pattern
- Services now use
getCardanoIndexer()instead of direct instantiation - Test suite updated: 29 test files (19 unit + 10 integration), 1122 tests
- Enhanced error handling with signing-specific error cases
-
Features 10, Changed 3, Other 4
Notes
Added
- Transaction Builder Module: Dual-builder architecture with CSL (Cardano Serialization Lib) and Buildooor engines
- Transaction Types: Support for 4 transaction types
- Simple ADA transfers
- Token minting with policy scripts
- Multi-asset transfers (ADA + native tokens)
- Transactions with metadata
- 6 Transaction Actions (OData POST endpoints):
BuildSimpleAdaTransaction- Build simple ADA transferBuildTransactionWithMetadata- Build ADA transfer with metadataBuildMintTransaction- Build token minting transactionBuildMultiAssetTransaction- Build multi-asset transferSubmitTransaction- Submit signed transaction to CardanoSubmitSignedTransaction- Submit externally built transaction
- Ogmios Live Backend: WebSocket-based real-time data access for protocol parameters, UTxO queries, and transaction submission
- TX Builder Registry: Factory pattern for runtime builder selection and initialization
- End-to-End Example Scripts:
send-ada-preview.ts- Simple ADA transfer workflowmint-token-preview.ts- Token minting workflowsend-ada-with-metadata-preview.ts- Metadata transaction workflowsend-multi-asset-preview.ts- Multi-asset transfer workflow
- Postman Collection M2: Pre-configured requests for all transaction endpoints
- Transaction Error Handling: 5 specialized error scenarios
- Insufficient funds (
ODATANO_INSUFFICIENT_FUNDS) - Invalid input data (
ODATANO_INVALID_INPUT) - Invalid signature (
ODATANO_TX_VALIDATION_FAILED) - Network failure (
ODATANO_PROVIDER_UNAVAILABLE) - Duplicate transaction (
ODATANO_TX_ALREADY_SUBMITTED)
- Insufficient funds (
- 327 new tests (6 new test suites): Ogmios Tests, Transaction builder tests (CSL, Buildooor), mocked submission tests, error handling tests
- Transaction Workflow Documentation: Build → Sign → Submit flow guide
Changed
- Extended multi-provider architecture: Ogmios (live) + Blockfrost (primary historical) + Koios (fallback)
- Updated test suite: 692 tests across 19 test suites (from 340 tests / 11 suites)
- Enhanced error handling with 8 specialized error classes
Technical Details
- UTXO selection: LargestFirstMultiAsset strategy
- Fee calculation: Based on current protocol parameters
- Output format: CBOR hex (unsigned transactions)
- External signing: Cardano CLI, browser wallets, hardware wallets supported
Features 9, Other 5
Notes
Added
-
Project Infrastructure
- Public GitHub repository with Apache 2.0 license
- SAP CAP project structure with complete scaffolding
- CI/CD pipeline with automated tests on Node.js 20.x and 22.x
- Code coverage reporting via Codecov (96%+ statement, 81%+ branch)
- Docker deployment support
-
OData V4 Service (
/odata/v4/cardano-odata)- Full OData V4 query support:
$filter,$select,$expand,$top,$skip,$count,$orderby - SAP Fiori UI annotations for rapid UI development
- Multi-network support: mainnet, preview, preprod
- Full OData V4 query support:
-
18 Entities defining Cardano Core Components:
NetworkInformation- Network statistics (supply, stake)Blocks- Block headersEpochs- Epoch summariesTransactions- Transaction details with inputs/outputsTransactionInputs- Inputs of a transactionTransactionOutputs- Outputs of a transactionTransactionInputAssets- Assets per transaction inputTransactionOutputAssets- Assets per transaction outputTransactionMetadata- Transaction metadata by tx + labelAddresses- Address balances and metadataAddressAssets- Native assets at an addressAddressUTxOs- Unspent outputs at an addressUTxOAssets- Assets contained in a specific UTxOPools- Stake poolsAccounts- Stake accountsDreps- Delegated representativesAddressTransactions- Address transaction historyLedgerProtocolParameters- Protocol parameters
-
15 Read Actions (OData POST endpoints):
GetNetworkInformationGetBlockByHashGetEpochByNumberGetTransactionByHashGetMetadataByTxHashGetAddressByBech32GetUTxOsByAddressGetAssetsByAddressGetPoolByIdGetAccountByStakeAddressGetDrepByIdGetLatestTransactionsByAddressGetLatestBlockGetLatestEpochGetLedgerProtocolParameters
-
Multi-Provider Architecture
- Blockfrost (primary, 8s timeout)
- Koios (fallback, 10s timeout)
- Automatic failover on timeout, network error, or backend error
- Response normalization into canonical internal data model
-
Lazy On-Demand Indexing
- Data fetched from Cardano on first access
- Persisted with TTL-based refresh (configurable via
INDEX_TTL_MS) - Temporal entities: only currently valid rows returned
- No background jobs; all refresh is request-driven
-
Input Validation
- Transaction/pool/drep IDs: 64-char hex validation
- Addresses: network-aware bech32 validation
- Stake addresses: network-aware bech32 stake HRP validation
-
340 Tests across 11 test suites:
- Integration tests for Blockfrost and Koios backends
- OData query feature tests
- Error handling and failover tests
- Input validation tests
-
Documentation Package
- Quick Start Guide
- Developer Guide (architecture, setup, development)
- User Guide (deployment, querying, examples)
- Docker Deployment Guide
- Data Model Documentation
- Indexing Concept Documentation
- Error Handling Documentation
- Postman Collection M1
Technical Stack
- SAP CAP v9.x
- TypeScript v5.9
- Node.js v20.x / v22.x
- SQLite (persistent caching via @cap-js/sqlite)
- Jest v29.x (testing)
-
Others providing
Compare all 12 on the OData page →- launchpad-kpilaunchpad-kpiAn OData service and a one-method ABAP interface that put a number from an abap2UI5 app on a SAP Fiori launchpad tile.community org, abapPuts a number from an abap2UI5 app on a Fiori launchpad tilefrom ABAP
- com.sap.cds:cds-adapter-odata-v2com.sap.cds:cds-adapter-odata-v2The OData V2 protocol adapter for CAP Java, which the runtime serves natively once this beta module is on the classpath, so a V2 frontend keeps working without a change to the CDS model.sap official, capOData V2 protocol adapter built into the CAP Java runtimefrom CAP
- com.sap.cds:cds-adapter-odata-v4com.sap.cds:cds-adapter-odata-v4The OData V4 protocol adapter for CAP Java, auto-exposing every Application Service in the model at /odata/v4/<Service> without protocol code of its own, active by default in the CDS Maven Archetype.sap official, capOData V4 protocol adapter for CAP Java Application Servicesfrom CAP
- @cap-js-community/odata-v2-adapter@cap-js-community/odata-v2-adapterExposes a CAP service as OData V2 for clients that cannot move to V4.community org, capExposes a CAP service as OData V2from CAP
- odata-fwodata-fwA framework for building OData services in ABAP with each entity implemented as its own class, reusable across services, so a model change no longer forces the SEGW service to be regenerated.independent, abapABAP framework for OData services with one class per entityfrom ABAP
- @odatano/nightgate@odatano/nightgateIndexes the Midnight blockchain and submits transactions to it from CAP, exposed as OData V4, with command-line tools for schema deltas, key rotation and migration.independent, capCAP plugin indexing the Midnight blockchain, exposed as OData V4from CAP
- open-steamgateopen-steamgateAn ABAP application server you clone and run on your own machine: transpiled ABAP serving classic /IWBEP/ OData v2 to Fiori over a local database, with no SAP system attached.independent, abapLocal ABAP server serving /IWBEP/ OData v2 without an SAP systemfrom ABAP
- open-abap-odataopen-abap-odataA set of OData and Gateway shims in ABAP that serve services built the SEGW way, from a model provider and a data provider class, on Node.js and in "Steampunk".community org, abapOData and Gateway shims serving SEGW-style services on Node.jsfrom ABAP
- @root3287/cap-segw@root3287/cap-segwA CAP build tool that compiles a CDS service into the ABAP Model Provider and Data Provider classes a SAP Gateway SEGW project expects, for OData V4 by default or V2.independent, capCompiler from a CDS service to SEGW model and data provider classesfrom CAP
- @sap/cds@sap/cdsThe CAP Node.js runtime, which compiles the CDS model, serves the services declared in it, answers generic requests before any handler runs, and loads every cds-plugin.sap official, capCAP Node.js runtimefrom CAP
- OData Batch CreatorOData Batch CreatorBuilds the payload for an OData batch request from an uploaded $metadata XML file, generating sample data from the metadata rather than asking for it by hand.independent, abapBuilds OData batch request payloads from an uploaded $metadata filefrom ABAP
Details
Sources
Sources are graph entities: the same page may back several entries, and an unreachable source flags every row that rests on it.
Compatibility and provenance
Flavors
“**Node.js >= 22.5** and **@sap/cds >= 10** (peer dependency)” — ODATANO/ODATANO — README
Contributors
History in the atlas
- Serves the Cardano blockchain as OData V4 from CAP — enterprise-shaped access to a public chain, funded through Cardano Catalyst.
- First seen.
For your agent
- Ref
artifact:odatano-core- JSON
- /artifacts/odatano-core.json
- MCP
- Connecting an agent