@odatano/x402 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ODATANO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,350 @@
1
+ # @odatano/x402
2
+
3
+ x402 payment gating for SAP CAP applications, backed by Cardano.
4
+
5
+ Wire a single `before('*')` hook into your CAP service and every gated request returns **HTTP 402 Payment Required** until the caller proves on-chain settlement. Asset-agnostic — pay in ADA, USDM, or any native asset.
6
+
7
+ Implements the **Cardano-x402-v2** spec on top of [`@odatano/core`](https://www.npmjs.com/package/@odatano/core).
8
+
9
+ ---
10
+
11
+ ## What is x402?
12
+
13
+ [x402](https://www.x402.org/) is the dormant HTTP `402 Payment Required` status code, revived. Servers respond `402` with a machine-readable body describing the price, asset, and recipient. Clients build, sign, and submit a payment, then retry the request with a `PAYMENT-SIGNATURE` header. Settlement happens on-chain.
14
+
15
+ The original x402 spec is Coinbase / EVM-flavoured. The **Cardano-x402-v2** spec (in progress at [masumi-network/x402-cardano](https://github.com/masumi-network/x402-cardano)) adapts it to Cardano's UTxO model. This library is a from-scratch v2 implementation in TypeScript.
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @odatano/x402 @odatano/core
23
+ ```
24
+
25
+ `@odatano/core` (the Cardano bridge) is a peer dependency — install whichever version meets `>=1.7.8`.
26
+
27
+ ---
28
+
29
+ ## Quick Start — CAP service gate
30
+
31
+ ```typescript
32
+ // srv/prices-service.ts
33
+ import cds from '@sap/cds';
34
+ import { gateService } from '@odatano/x402';
35
+
36
+ export class PricesService extends cds.ApplicationService {
37
+ async init() {
38
+ gateService(this, {
39
+ payTo: 'addr_test1...your-preprod-address...',
40
+ network: 'cardano:preprod',
41
+ asset: 'lovelace', // or '<policy>.<nameHex>' for native tokens
42
+ routePricing: {
43
+ Quotes: '500000', // 0.5 ADA per Quotes read
44
+ getBestPrice: '1000000', // 1 ADA per getBestPrice action call
45
+ },
46
+ description: 'Synthetic price feed',
47
+ onAccepted: async (claim, req) => {
48
+ // Optional audit — runs once per accepted payment, after settlement.
49
+ // Errors here are logged but don't block the response.
50
+ console.log(`paid ${claim.amountUnits} ${claim.asset} (tx=${claim.txHash})`);
51
+ },
52
+ });
53
+ return super.init();
54
+ }
55
+ }
56
+ ```
57
+
58
+ Configure the Cardano backend in `package.json`:
59
+
60
+ ```jsonc
61
+ {
62
+ "cds": {
63
+ "requires": {
64
+ "odatano-core": {
65
+ "network": "preprod",
66
+ "backends": ["blockfrost"],
67
+ "blockfrostApiKey": "preprodXXXXXXXXXXXXXXXXX"
68
+ }
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ That's it — `cds watch` and every request to a gated route gets:
75
+
76
+ ```http
77
+ HTTP/1.1 402 Payment Required
78
+ Content-Type: application/json
79
+ ```
80
+ ```json
81
+ {
82
+ "x402Version": 2,
83
+ "error": "PAYMENT-SIGNATURE header is required",
84
+ "accepts": [{
85
+ "scheme": "exact",
86
+ "network": "cardano:preprod",
87
+ "asset": "lovelace",
88
+ "amount": "500000",
89
+ "payTo": "addr_test1...",
90
+ "resource": {
91
+ "url": "/odata/v4/prices/Quotes",
92
+ "description": "Synthetic price feed",
93
+ "mimeType": "application/json"
94
+ },
95
+ "assetTransferMethod": "default",
96
+ "maxTimeoutSeconds": 600
97
+ }]
98
+ }
99
+ ```
100
+
101
+ A working version of this is in [`examples/cap-app/`](examples/cap-app/).
102
+
103
+ ---
104
+
105
+ ## Usage patterns
106
+
107
+ ### 1. CAP service gate (`gateService`)
108
+
109
+ For OData-served entities and bound/unbound actions. Pricing keys can be entity names (for CRUD) or action names — the gate tries both.
110
+
111
+ ```typescript
112
+ gateService(this, {
113
+ payTo, network, asset,
114
+ routePricing: { Quotes: '500000', getBestPrice: '1000000' },
115
+ });
116
+ ```
117
+
118
+ When a payment is accepted, the verified `PaymentClaim` is stashed on `req.payment` for downstream handlers, and an `X-PAYMENT-RESPONSE` header is set on the response.
119
+
120
+ ### 2. Express middleware (`x402Middleware`)
121
+
122
+ For plain Express routes (e.g. mounted alongside CAP via `cds.on('bootstrap', app => …)`):
123
+
124
+ ```typescript
125
+ import { x402Middleware } from '@odatano/x402';
126
+
127
+ app.use('/api/premium', x402Middleware({
128
+ payTo, network, asset,
129
+ priceUnits: '1000000',
130
+ skipPaths: /(\$metadata|^\/?$)/i,
131
+ onAccepted: async (claim, req) => { /* audit */ },
132
+ }));
133
+ ```
134
+
135
+ ### 3. Programmatic — verify a post-paid tx
136
+
137
+ For subscription / pre-paid flows where the buyer hands you a tx hash:
138
+
139
+ ```typescript
140
+ import { verifyConfirmedPayment } from '@odatano/x402';
141
+
142
+ const result = await verifyConfirmedPayment({
143
+ txHash: 'ab8f…',
144
+ requiredAmount: '1000000',
145
+ asset: 'lovelace',
146
+ payTo: 'addr_test1...',
147
+ network: 'cardano:preprod',
148
+ });
149
+
150
+ if (result.ok) {
151
+ // result.amountUnits is what was actually paid (may exceed requiredAmount)
152
+ } else {
153
+ // result.code: 'pending' | 'wrong_asset' | 'insufficient_amount' | ...
154
+ }
155
+ ```
156
+
157
+ ### 4. Programmatic — server-side unsigned-tx builder for browser buyers
158
+
159
+ When the buyer's CIP-30 wallet can sign but not coin-select:
160
+
161
+ ```typescript
162
+ import { buildUnsignedPaymentTx, buildPaymentRequirements, flatRequirements } from '@odatano/x402';
163
+
164
+ const body = buildPaymentRequirements({ amount: '1000000', asset: 'lovelace', payTo, network: 'cardano:preprod', resource: '/r' });
165
+ const requirements = flatRequirements(body);
166
+
167
+ const { unsignedTxCborHex, txHashHex, nonceRef } = await buildUnsignedPaymentTx({
168
+ buyerBech32: 'addr_test1...buyer...',
169
+ requirements,
170
+ });
171
+
172
+ // Browser signs unsignedTxCborHex via CIP-30, then assembles the
173
+ // PAYMENT-SIGNATURE envelope with `nonceRef` as payload.nonce.
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Configuration reference
179
+
180
+ ### `gateService(srv, options)` / `x402Middleware(options)`
181
+
182
+ | Option | Type | Required | Default | Notes |
183
+ |---|---|---|---|---|
184
+ | `payTo` | `string` (bech32) | yes | — | Recipient address |
185
+ | `network` | `'cardano:mainnet' \| 'cardano:preprod' \| 'cardano:preview'` | yes | — | **v2 uses colon separator** (v1 hyphen rejected) |
186
+ | `asset` | `string` | yes | — | `'lovelace'` for ADA, or `'<policyIdHex>.<assetNameHex>'` for native tokens |
187
+ | `priceUnits` | `string \| number \| bigint` | one of priceUnits / routePricing | — | Single price for everything under the mount |
188
+ | `routePricing` | `Record<string, string \| number \| bigint>` | one of priceUnits / routePricing | — | Per-entity / per-action prices. Unmapped keys pass through unless `priceUnits` is also set |
189
+ | `skipPaths` | `RegExp` | no | matches `$metadata`, `$batch`, root, `/index` | Express only — paths to bypass |
190
+ | `description` | `string` | no | `''` | Embedded in `accepts[0].resource.description` |
191
+ | `mimeType` | `string` | no | `'application/json'` | Embedded in `accepts[0].resource.mimeType` |
192
+ | `assetTransferMethod` | `'default' \| 'masumi' \| 'script'` | no | `'default'` | v2 field; MVP supports only `default` |
193
+ | `maxTimeoutSeconds` | `number` | no | `600` | Buyer-side TTL hint |
194
+ | `extra` | `Record<string, unknown>` | no | — | Free-form extras (decimals, fingerprint, UI hints) |
195
+ | `settlePollBudgetMs` | `number` | no | `60_000` | How long to poll for chain confirmation before returning `402 pending` |
196
+ | `allowNoTtl` | `boolean` | no | `false` | If `true`, accept txs with no validity-range upper bound |
197
+ | `onAccepted` | `(claim, req) => void \| Promise<void>` | no | — | Audit callback. Errors logged, never block response |
198
+ | `resourceUrl` | `(req) => string` | no (CAP only) | derives from `req.http.req.originalUrl` | Override the resource URL emitted in the 402 body |
199
+
200
+ ---
201
+
202
+ ## The buyer flow
203
+
204
+ ```
205
+ Server Buyer (browser / CLI)
206
+ │ │
207
+ │ ◄── GET /odata/v4/prices/Quotes ─────│
208
+ │ │
209
+ │ ──── 402 + accepts[0] (price, payTo) ►│
210
+ │ │
211
+ │ ┌────────┴────────┐
212
+ │ │ build tx │
213
+ │ │ sign via CIP-30 │
214
+ │ │ base64-encode │
215
+ │ └────────┬────────┘
216
+ │ │
217
+ │ ◄── GET /odata/v4/prices/Quotes ─────│
218
+ │ PAYMENT-SIGNATURE: <base64> │
219
+ │ │
220
+ ┌──────┴──────┐ │
221
+ │ 6 checks │ │
222
+ │ + submit │ │
223
+ │ + poll │ │
224
+ │ + onAccepted│ │
225
+ └──────┬──────┘ │
226
+ │ │
227
+ │ ─────── 200 OK + data ───────────────►│
228
+ │ X-PAYMENT-RESPONSE: <base64> │
229
+ ```
230
+
231
+ `PAYMENT-SIGNATURE` envelope shape:
232
+
233
+ ```json
234
+ {
235
+ "x402Version": 2,
236
+ "scheme": "exact",
237
+ "network": "cardano:preprod",
238
+ "payload": {
239
+ "transaction": "<base64-CBOR of signed tx>",
240
+ "nonce": "<txHash>#<outputIndex>"
241
+ }
242
+ }
243
+ ```
244
+
245
+ The `nonce` references a UTxO that **must also appear as an input of the payment tx**. Once the tx settles, that UTxO is consumed — replay defense is on-chain, no DB table needed.
246
+
247
+ ---
248
+
249
+ ## Six mandatory facilitator checks
250
+
251
+ Every accepted payment passes all six (in order):
252
+
253
+ | # | Check | Code on failure |
254
+ |---|---|---|
255
+ | 1 | Network matches requirements | `network_mismatch` |
256
+ | 2 | At least one output to `payTo` | `wrong_recipient` |
257
+ | 3 | Sum of payTo outputs for asset ≥ required | `insufficient_amount` |
258
+ | 4 | Exact policy + asset-name match | `wrong_asset` |
259
+ | 5 | Nonce UTxO referenced as tx input **and** unspent on chain | `nonce_not_referenced` / `replay_detected` |
260
+ | 6 | Validity-range upper bound still in future | `expired_ttl` |
261
+
262
+ Plus a sanity guard: tx has at least one vkey witness → `unsigned_transaction`.
263
+
264
+ Rejected requests get `402` with an `error` field of the form `"<base> (<code>): <reason>"` so clients can parse the code without breaking wire format.
265
+
266
+ ---
267
+
268
+ ## Architecture
269
+
270
+ ```
271
+ ┌──────────────────┐
272
+ consumer │ CAP application │
273
+ │ │
274
+ │ gateService(this,│
275
+ │ { … }) │
276
+ └────────┬─────────┘
277
+
278
+
279
+ ┌──────────────────┐ bridge.ts
280
+ │ @odatano/x402 │ ──────────────┐
281
+ │ │ │
282
+ │ core/ ──────┤ pure logic │
283
+ │ facilitator/ ────┤ chain-touching│
284
+ │ middleware/ ────┤ Express + CAP │
285
+ │ helpers/ ────┤ tx-build, │
286
+ │ │ verify-post-paid
287
+ └──────────────────┘ │
288
+
289
+ ┌────────────────────────┐
290
+ │ @odatano/core 1.7.8 │
291
+ │ ┌──────┬──────┬─────┐ │
292
+ │ │ Blkf │Koios │Ogms │ │
293
+ │ └──────┴──────┴─────┘ │
294
+ └────────────────────────┘
295
+
296
+
297
+ Cardano network
298
+ ```
299
+
300
+ Pure modules (`core/*`) are decoupled from the bridge and can be unit-tested without any chain backend. The facilitator orchestrates `decode → validate → checkNonceUnspent → settle → onAccepted`.
301
+
302
+ ---
303
+
304
+ ## Requirements
305
+
306
+ - Node.js 22+
307
+ - `@sap/cds >= 9` (peer)
308
+ - `@odatano/core >= 1.7.8` (peer)
309
+ - `express ^4` (peer) — only required if you use `x402Middleware`
310
+ - A Cardano backend reachable via `@odatano/core` (Blockfrost / Koios / Ogmios)
311
+
312
+ ---
313
+
314
+ ## Development
315
+
316
+ ```bash
317
+ npm install # Workspace install — covers root + examples/*
318
+ npm run build # tsc — emits .js/.d.ts next to .ts (outDir: .)
319
+ npm test # 144 tests, ~12s
320
+ npm run test:coverage # Coverage report
321
+ npm run watch # cds watch — for the root project itself
322
+ ```
323
+
324
+ The example app:
325
+
326
+ ```bash
327
+ cd examples/cap-app
328
+ npm start # cds-serve with in-memory SQLite
329
+ ```
330
+
331
+ Then probe:
332
+
333
+ ```bash
334
+ curl -s http://localhost:4004/odata/v4/prices/Quotes | jq .
335
+ # → 402 with v2 body
336
+ ```
337
+
338
+ ---
339
+
340
+ ## Versioning + spec compatibility
341
+
342
+ - **Library:** `0.1.0` (pre-1.0; expect minor breakages between minors until 1.0)
343
+ - **Spec:** Cardano-x402-**v2** only. v1 envelopes are rejected with `unsupported_version`. v1-style network strings (`cardano-preprod`) are rejected with `invalid_network_format`.
344
+ - **Coexistence:** A v1 facilitator and a v2 facilitator can't share the same route — they use different header names (`X-PAYMENT` vs `PAYMENT-SIGNATURE`) and incompatible 402 bodies. To migrate from v1, replace the middleware in one commit; clients must upgrade simultaneously.
345
+
346
+ ---
347
+
348
+ ## License
349
+
350
+ Apache-2.0.
package/cds-plugin.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * CAP plugin entry. CDS picks this up automatically when @odatano/x402
3
+ * is present in node_modules — it imports the compiled plugin module
4
+ * which registers the served / shutdown hooks.
5
+ *
6
+ * The .ts source is at srv/plugin.ts; tsc emits srv/plugin.js in-place
7
+ * (outDir: ".") so this require path resolves both in dev (tsx) and
8
+ * after `npm run build`.
9
+ */
10
+ module.exports = require('./srv/plugin');
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@odatano/x402",
3
+ "version": "0.1.0",
4
+ "description": "x402 Cardano-v2 payment library for SAP CAP applications",
5
+ "license": "Apache-2.0",
6
+ "main": "srv/index.js",
7
+ "types": "srv/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "imports": {
12
+ "#cds-models/*": "./@cds-models/*/index.js"
13
+ },
14
+ "workspaces": [
15
+ "examples/*"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.build.json",
19
+ "watch": "cds watch",
20
+ "start": "cds-serve",
21
+ "lint": "eslint .",
22
+ "test": "jest",
23
+ "test:coverage": "jest --coverage"
24
+ },
25
+ "dependencies": {
26
+ "@emurgo/cardano-serialization-lib-nodejs": "^15.0.3"
27
+ },
28
+ "peerDependencies": {
29
+ "@odatano/core": ">=1.7.8",
30
+ "@sap/cds": ">=9",
31
+ "express": "^4"
32
+ },
33
+ "devDependencies": {
34
+ "@cap-js/cds-types": "^0.15.0",
35
+ "@cap-js/cds-typer": "^0.38.0",
36
+ "@cap-js/sqlite": "^2",
37
+ "@odatano/core": "^1.7.8",
38
+ "@odatano/x402": ".",
39
+ "@sap/cds": "^9",
40
+ "@sap/cds-dk": "^9.4.3",
41
+ "@sap/eslint-plugin-cds": "^4",
42
+ "@types/express": "^4",
43
+ "@types/jest": "^29.5.14",
44
+ "@types/node": "^22",
45
+ "eslint": "^9",
46
+ "express": "^4",
47
+ "jest": "^29",
48
+ "ts-jest": "^29",
49
+ "tsx": "^4",
50
+ "typescript": "^5"
51
+ }
52
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Thin adapter over `@odatano/core`'s programmatic Cardano client.
3
+ *
4
+ * The x402 modules (facilitator, helpers, middleware) all import from
5
+ * here so the underlying ODATANO surface is the only thing they couple
6
+ * to — and so renames in core (`getTransaction` → `getTransactionByHash`)
7
+ * stay isolated to this file.
8
+ *
9
+ * Two methods needed by Cardano-x402-v2 are NOT (yet) first-class on
10
+ * `@odatano/core@1.7.7`:
11
+ * - `isUtxoUnspent(txHash, outputIndex)` — for replay-defense check 5b
12
+ * - `getCurrentSlot()` — for TTL check 6
13
+ *
14
+ * Both are implemented here as **shims** on top of existing core
15
+ * methods, so x402 works against an unmodified 1.7.7. When ODATANO
16
+ * exposes either method natively (planned ≥1.7.8), we can swap the
17
+ * shim for a direct call without touching downstream code.
18
+ */
19
+ export interface BridgeAsset {
20
+ unit: string;
21
+ policyId: string;
22
+ assetNameHex: string;
23
+ quantity: string;
24
+ }
25
+ export interface BridgeUtxo {
26
+ txHash: string;
27
+ outputIndex: number;
28
+ address: string;
29
+ lovelace: string;
30
+ assets: BridgeAsset[];
31
+ dataHash?: string;
32
+ inlineDatumHex?: string;
33
+ referenceScriptHash?: string;
34
+ }
35
+ /** Init the underlying @odatano/core client. Idempotent. */
36
+ export declare function init(): Promise<void>;
37
+ /** Force re-init on next call (used by tests / supervised reloads). */
38
+ export declare function shutdown(): Promise<void>;
39
+ /**
40
+ * Fetch UTxOs at a bech32 address, flat-mapped to BridgeUtxo[].
41
+ */
42
+ export declare function getUtxosAtAddress(address: string): Promise<BridgeUtxo[]>;
43
+ /**
44
+ * Fetch a tx by hash. Returns `null` on 404 (tx not on chain yet) so
45
+ * the settle/verify-confirmed paths can poll without try/catch noise.
46
+ */
47
+ export declare function getTransactionByHash(txHash: string): Promise<unknown>;
48
+ export declare function getProtocolParameters(): Promise<unknown>;
49
+ export declare function submitTransaction(signedCborHex: string): Promise<string>;
50
+ /**
51
+ * Current chain tip slot. First-class method on `CardanoClient` since
52
+ * `@odatano/core@1.7.8` — wraps `getLatestBlock().slot` with a
53
+ * `ProviderUnavailableError` translation so consumers don't deal with
54
+ * `null` slots.
55
+ */
56
+ export declare function getCurrentSlot(): Promise<number>;
57
+ /**
58
+ * Check whether a UTxO is still unspent. First-class method since
59
+ * `@odatano/core@1.7.8` — backed by `consumed_by` (Blockfrost) /
60
+ * `is_spent` (Koios) / `queryLedgerState/utxo` (Ogmios).
61
+ *
62
+ * Returns `false` for txs that don't exist on chain or for
63
+ * out-of-range output indices — both are "not spendable" from the
64
+ * caller's perspective.
65
+ */
66
+ export declare function isUtxoUnspent(txHash: string, outputIndex: number): Promise<boolean>;
67
+ export declare const parseTransaction: ((cborHex: string) => unknown) | undefined;
68
+ //# sourceMappingURL=bridge.d.ts.map
package/srv/bridge.js ADDED
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ /**
3
+ * Thin adapter over `@odatano/core`'s programmatic Cardano client.
4
+ *
5
+ * The x402 modules (facilitator, helpers, middleware) all import from
6
+ * here so the underlying ODATANO surface is the only thing they couple
7
+ * to — and so renames in core (`getTransaction` → `getTransactionByHash`)
8
+ * stay isolated to this file.
9
+ *
10
+ * Two methods needed by Cardano-x402-v2 are NOT (yet) first-class on
11
+ * `@odatano/core@1.7.7`:
12
+ * - `isUtxoUnspent(txHash, outputIndex)` — for replay-defense check 5b
13
+ * - `getCurrentSlot()` — for TTL check 6
14
+ *
15
+ * Both are implemented here as **shims** on top of existing core
16
+ * methods, so x402 works against an unmodified 1.7.7. When ODATANO
17
+ * exposes either method natively (planned ≥1.7.8), we can swap the
18
+ * shim for a direct call without touching downstream code.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.parseTransaction = void 0;
22
+ exports.init = init;
23
+ exports.shutdown = shutdown;
24
+ exports.getUtxosAtAddress = getUtxosAtAddress;
25
+ exports.getTransactionByHash = getTransactionByHash;
26
+ exports.getProtocolParameters = getProtocolParameters;
27
+ exports.submitTransaction = submitTransaction;
28
+ exports.getCurrentSlot = getCurrentSlot;
29
+ exports.isUtxoUnspent = isUtxoUnspent;
30
+ const errors_1 = require("./core/errors");
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const od = require('@odatano/core');
33
+ // ─── Init guard: cache the promise so concurrent callers share it ─────
34
+ let initPromise = null;
35
+ async function ensureInit() {
36
+ if (!initPromise) {
37
+ initPromise = od.initialize().catch(err => {
38
+ initPromise = null;
39
+ throw new errors_1.X402Error(errors_1.Codes.BRIDGE_UNAVAILABLE, `@odatano/core init failed: ${err?.message ?? err}`);
40
+ });
41
+ }
42
+ await initPromise;
43
+ }
44
+ function mapUtxo(u) {
45
+ const amount = u.amount ?? [];
46
+ const lovelaceEntry = amount.find(a => a.unit === 'lovelace');
47
+ const lovelace = String(lovelaceEntry?.quantity ?? '0');
48
+ const assets = amount
49
+ .filter(a => a.unit !== 'lovelace')
50
+ .map(a => {
51
+ const unit = String(a.unit ?? '').toLowerCase();
52
+ return {
53
+ unit,
54
+ policyId: unit.slice(0, 56),
55
+ assetNameHex: unit.slice(56),
56
+ quantity: String(a.quantity ?? '0'),
57
+ };
58
+ });
59
+ return {
60
+ txHash: String(u.txHash ?? ''),
61
+ outputIndex: Number(u.outputIndex ?? 0),
62
+ address: String(u.address ?? ''),
63
+ lovelace,
64
+ assets,
65
+ dataHash: u.datumHash ?? undefined,
66
+ inlineDatumHex: u.inlineDatum ?? undefined,
67
+ referenceScriptHash: u.scriptRef ?? undefined,
68
+ };
69
+ }
70
+ // ─── Public API ───────────────────────────────────────────────────────
71
+ /** Init the underlying @odatano/core client. Idempotent. */
72
+ async function init() { await ensureInit(); }
73
+ /** Force re-init on next call (used by tests / supervised reloads). */
74
+ async function shutdown() {
75
+ try {
76
+ await od.shutdown();
77
+ }
78
+ finally {
79
+ initPromise = null;
80
+ }
81
+ }
82
+ /**
83
+ * Fetch UTxOs at a bech32 address, flat-mapped to BridgeUtxo[].
84
+ */
85
+ async function getUtxosAtAddress(address) {
86
+ if (!address)
87
+ throw new TypeError('getUtxosAtAddress: address required');
88
+ await ensureInit();
89
+ const rows = await od.getCardanoClient().getAddressUtxos(address);
90
+ return Array.isArray(rows) ? rows.map(mapUtxo) : [];
91
+ }
92
+ /**
93
+ * Fetch a tx by hash. Returns `null` on 404 (tx not on chain yet) so
94
+ * the settle/verify-confirmed paths can poll without try/catch noise.
95
+ */
96
+ async function getTransactionByHash(txHash) {
97
+ if (!txHash)
98
+ throw new TypeError('getTransactionByHash: txHash required');
99
+ await ensureInit();
100
+ try {
101
+ return await od.getCardanoClient().getTransaction(txHash);
102
+ }
103
+ catch (err) {
104
+ const e = err;
105
+ if (e?.code === 404 || e?.statusCode === 404 || /not.?found/i.test(e?.message ?? '')) {
106
+ return null;
107
+ }
108
+ throw err;
109
+ }
110
+ }
111
+ async function getProtocolParameters() {
112
+ await ensureInit();
113
+ return od.getCardanoClient().getProtocolParameters();
114
+ }
115
+ async function submitTransaction(signedCborHex) {
116
+ if (!signedCborHex)
117
+ throw new TypeError('submitTransaction: signedCborHex required');
118
+ await ensureInit();
119
+ return od.getCardanoClient().submitTransaction(signedCborHex);
120
+ }
121
+ /**
122
+ * Current chain tip slot. First-class method on `CardanoClient` since
123
+ * `@odatano/core@1.7.8` — wraps `getLatestBlock().slot` with a
124
+ * `ProviderUnavailableError` translation so consumers don't deal with
125
+ * `null` slots.
126
+ */
127
+ async function getCurrentSlot() {
128
+ await ensureInit();
129
+ return od.getCardanoClient().getCurrentSlot();
130
+ }
131
+ /**
132
+ * Check whether a UTxO is still unspent. First-class method since
133
+ * `@odatano/core@1.7.8` — backed by `consumed_by` (Blockfrost) /
134
+ * `is_spent` (Koios) / `queryLedgerState/utxo` (Ogmios).
135
+ *
136
+ * Returns `false` for txs that don't exist on chain or for
137
+ * out-of-range output indices — both are "not spendable" from the
138
+ * caller's perspective.
139
+ */
140
+ async function isUtxoUnspent(txHash, outputIndex) {
141
+ if (!txHash)
142
+ throw new TypeError('isUtxoUnspent: txHash required');
143
+ if (!Number.isInteger(outputIndex) || outputIndex < 0) {
144
+ throw new TypeError('isUtxoUnspent: outputIndex must be a non-negative integer');
145
+ }
146
+ await ensureInit();
147
+ return od.getCardanoClient().isUtxoUnspent(txHash, outputIndex);
148
+ }
149
+ // ─── Re-export pure CBOR utilities (no bridge round-trip) ─────────────
150
+ // parseTransaction is exported from @odatano/core's barrel and runs
151
+ // entirely client-side. Re-export so x402 users don't need a second
152
+ // import for tx introspection. We declare the type loosely (unknown
153
+ // CBOR-parsed shape) — consumers cast to ODATANO's `ParsedTransaction`
154
+ // from `@odatano/core` directly if they need the structured fields.
155
+ exports.parseTransaction = od ? od.parseTransaction : undefined;