@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.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Public type surface for x402 Cardano-v2.
3
+ *
4
+ * The shapes here match the v2 spec excerpt in
5
+ * `docs/x402-cardano-integration.md` (envelope, requirements, payload).
6
+ * Keep them as the single source of truth — middleware, facilitator and
7
+ * helpers all import from here.
8
+ */
9
+ import type { Network } from './network';
10
+ /** Asset-transfer method. v2 spec. MVP supports only `default`. */
11
+ export type AssetTransferMethod = 'default' | 'masumi' | 'script';
12
+ /** v2 resource descriptor — was a bare string in v1. */
13
+ export interface ResourceDescriptor {
14
+ url: string;
15
+ description: string;
16
+ mimeType: string;
17
+ /** Optional, free-form JSON Schema for the resource's response body. */
18
+ outputSchema?: unknown;
19
+ }
20
+ /** A single `accepts[]` entry of the 402 body. */
21
+ export interface PaymentRequirementEntry {
22
+ scheme: 'exact';
23
+ network: Network;
24
+ /**
25
+ * v2 asset format: `<policyIdHex>.<assetNameHex>` (dot-separated) OR
26
+ * the literal `'lovelace'` for ADA payments.
27
+ */
28
+ asset: string;
29
+ /** Required asset amount in raw units (BigInt-safe string). */
30
+ amount: string;
31
+ /** Bech32 recipient. */
32
+ payTo: string;
33
+ resource: ResourceDescriptor;
34
+ assetTransferMethod: AssetTransferMethod;
35
+ maxTimeoutSeconds: number;
36
+ /** Optional opaque extra fields (e.g. UI hints, decimals). */
37
+ extra?: Record<string, unknown>;
38
+ }
39
+ /** Canonical 402-response body. */
40
+ export interface PaymentRequirementsBody {
41
+ x402Version: 2;
42
+ error?: string;
43
+ accepts: PaymentRequirementEntry[];
44
+ }
45
+ /**
46
+ * Decoded `PAYMENT-SIGNATURE` envelope. The envelope payload references
47
+ * a buyer-funded UTxO (the **nonce**) which must also appear in the
48
+ * payment tx's inputs — this is the v2 replay defense, on-chain.
49
+ */
50
+ export interface PaymentEnvelope {
51
+ x402Version: 2;
52
+ scheme: 'exact';
53
+ network: string;
54
+ payload: {
55
+ /** base64 CBOR of the signed payment tx */
56
+ transaction: string;
57
+ /** `<txHash>#<outputIndex>` — UTxO acting as replay nonce */
58
+ nonce: string;
59
+ };
60
+ }
61
+ /** Result of pure validation (no chain calls). */
62
+ export interface PaymentClaim {
63
+ /** Hash of the buyer's signed payment tx (lowercase hex, 64 chars). */
64
+ txHash: string;
65
+ /** Amount actually paid to `payTo` for `asset`, summed across outputs. */
66
+ amountUnits: string;
67
+ network: Network;
68
+ /** Resolved v2 unit key (`policyId+nameHex` or empty for lovelace). */
69
+ unit: string;
70
+ /** The v2 asset string from requirements (passed-through for audit). */
71
+ asset: string;
72
+ /** The route / resource URL the buyer paid for. */
73
+ resourceUrl: string;
74
+ /** UTxO-ref nonce as `<txHash>#<index>`. */
75
+ nonceRef: string;
76
+ /** Earliest of the buyer's input tx hashes; useful for analytics. */
77
+ payerAddr?: string;
78
+ }
79
+ /** Diagnostic shape returned by `decode()` for downstream validation. */
80
+ export interface DecodedPayment {
81
+ envelope: PaymentEnvelope;
82
+ /** Hex of the signed-tx CBOR (preserved bytes, NOT a re-encode). */
83
+ txCborHex: string;
84
+ /** Hash of the tx body, lowercase 64-char hex. */
85
+ txHash: string;
86
+ outputs: DecodedOutput[];
87
+ inputs: DecodedInput[];
88
+ vkeyWitnessCount: number;
89
+ /** Validity-range upper bound in slots (`null` ⇒ no TTL set). */
90
+ ttlSlot: number | null;
91
+ /** Validity-range lower bound in slots (`null` ⇒ no lower bound set). */
92
+ validityStartSlot: number | null;
93
+ /** Parsed nonce reference. */
94
+ nonce: {
95
+ txHash: string;
96
+ index: number;
97
+ };
98
+ }
99
+ export interface DecodedOutput {
100
+ outputIndex: number;
101
+ address: string;
102
+ lovelace: string;
103
+ assets: DecodedAsset[];
104
+ }
105
+ export interface DecodedAsset {
106
+ unit: string;
107
+ policyId: string;
108
+ assetNameHex: string;
109
+ quantity: string;
110
+ }
111
+ export interface DecodedInput {
112
+ txHash: string;
113
+ outputIndex: number;
114
+ }
115
+ export type { Network };
116
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Public type surface for x402 Cardano-v2.
4
+ *
5
+ * The shapes here match the v2 spec excerpt in
6
+ * `docs/x402-cardano-integration.md` (envelope, requirements, payload).
7
+ * Keep them as the single source of truth — middleware, facilitator and
8
+ * helpers all import from here.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Validate a decoded payment against payment requirements.
3
+ *
4
+ * Implements the **6 mandatory facilitator checks** from
5
+ * Cardano-x402-v2:
6
+ *
7
+ * 1. Network validation
8
+ * 2. Recipient verification — ≥1 output to payTo
9
+ * 3. Amount verification — sum of payTo outputs for asset ≥ required
10
+ * 4. Asset verification — exact policy + name match
11
+ * 5. Nonce / replay prevention
12
+ * - 5a. UTxO referenced by `payload.nonce` appears as a tx input
13
+ * - 5b. that UTxO is still unspent on chain ← chain-touching, lives in `nonce.ts`
14
+ * 6. TTL / expiry — tx.validity_range.upper_bound in future
15
+ *
16
+ * This module covers (1), (2), (3), (4), (5a) and (6). The chain-touching
17
+ * part of (5) — checking the UTxO is unspent — and (5b) live in
18
+ * `facilitator/nonce.ts` and run after this. We also keep a sanity guard
19
+ * for "no vkey witnesses" so an unsigned CBOR is rejected with a precise
20
+ * code rather than blowing up at submit time.
21
+ *
22
+ * Pure function. No I/O.
23
+ */
24
+ import { type X402Code } from './errors';
25
+ import type { DecodedPayment, PaymentRequirementEntry, PaymentClaim } from './types';
26
+ export type ValidationResult = {
27
+ ok: true;
28
+ claim: PaymentClaim;
29
+ } | {
30
+ ok: false;
31
+ code: X402Code;
32
+ reason: string;
33
+ };
34
+ export interface ValidateOptions {
35
+ /** Required: current slot, for TTL upper-bound check. */
36
+ currentSlot: number;
37
+ /**
38
+ * If true, allow tx with no `ttl()` set (validity-range upper bound
39
+ * absent). Default false — v2 spec recommends a TTL. Callers that
40
+ * want to accept no-TTL txs (e.g. legacy wallets) opt-in.
41
+ */
42
+ allowNoTtl?: boolean;
43
+ }
44
+ export declare function validatePayment(decoded: DecodedPayment, requirements: PaymentRequirementEntry, opts: ValidateOptions): ValidationResult;
45
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ /**
3
+ * Validate a decoded payment against payment requirements.
4
+ *
5
+ * Implements the **6 mandatory facilitator checks** from
6
+ * Cardano-x402-v2:
7
+ *
8
+ * 1. Network validation
9
+ * 2. Recipient verification — ≥1 output to payTo
10
+ * 3. Amount verification — sum of payTo outputs for asset ≥ required
11
+ * 4. Asset verification — exact policy + name match
12
+ * 5. Nonce / replay prevention
13
+ * - 5a. UTxO referenced by `payload.nonce` appears as a tx input
14
+ * - 5b. that UTxO is still unspent on chain ← chain-touching, lives in `nonce.ts`
15
+ * 6. TTL / expiry — tx.validity_range.upper_bound in future
16
+ *
17
+ * This module covers (1), (2), (3), (4), (5a) and (6). The chain-touching
18
+ * part of (5) — checking the UTxO is unspent — and (5b) live in
19
+ * `facilitator/nonce.ts` and run after this. We also keep a sanity guard
20
+ * for "no vkey witnesses" so an unsigned CBOR is rejected with a precise
21
+ * code rather than blowing up at submit time.
22
+ *
23
+ * Pure function. No I/O.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.validatePayment = validatePayment;
27
+ const errors_1 = require("./errors");
28
+ const network_1 = require("./network");
29
+ const asset_1 = require("./asset");
30
+ function quantityOf(output, isLovelace, unit) {
31
+ if (isLovelace)
32
+ return BigInt(output.lovelace);
33
+ const a = output.assets.find(x => x.unit === unit);
34
+ return a ? BigInt(a.quantity) : 0n;
35
+ }
36
+ /**
37
+ * Total amount of `unit` paid to `payTo`, summed across ALL matching
38
+ * outputs. Summing is correct: a wallet may split a payment across
39
+ * multiple outputs (e.g. token + change), and we credit the full amount
40
+ * sent to our address.
41
+ */
42
+ function totalPaid(decoded, payTo, isLovelace, unit) {
43
+ let total = 0n;
44
+ let anyOutputToRecipient = false;
45
+ for (const o of decoded.outputs) {
46
+ if (o.address !== payTo)
47
+ continue;
48
+ anyOutputToRecipient = true;
49
+ total += quantityOf(o, isLovelace, unit);
50
+ }
51
+ return { total, anyOutputToRecipient };
52
+ }
53
+ function validatePayment(decoded, requirements, opts) {
54
+ // ─── Sanity: witness present ───────────────────────────────────────
55
+ // An unsigned CBOR can't be submitted. Catch this here with a precise
56
+ // code rather than letting the submit step fail with a generic 400.
57
+ if (!decoded.vkeyWitnessCount || decoded.vkeyWitnessCount < 1) {
58
+ return {
59
+ ok: false,
60
+ code: errors_1.Codes.UNSIGNED_TRANSACTION,
61
+ reason: 'transaction has no vkey witnesses',
62
+ };
63
+ }
64
+ // ─── Check 1: network ──────────────────────────────────────────────
65
+ if (!(0, network_1.networksMatch)(decoded.envelope.network, requirements.network)) {
66
+ return {
67
+ ok: false,
68
+ code: errors_1.Codes.NETWORK_MISMATCH,
69
+ reason: `payment network '${decoded.envelope.network}' does not match requirements '${requirements.network}'`,
70
+ };
71
+ }
72
+ // Parse the asset string once — also normalises the requirement's
73
+ // unit key for output comparison.
74
+ const parsed = (0, asset_1.parseAsset)(requirements.asset);
75
+ const unit = parsed.unit; // empty when lovelace; checks short-circuit via isLovelace
76
+ const required = BigInt(requirements.amount);
77
+ const { total: paid, anyOutputToRecipient } = totalPaid(decoded, requirements.payTo, parsed.isLovelace, unit);
78
+ // ─── Check 2: recipient ────────────────────────────────────────────
79
+ if (!anyOutputToRecipient) {
80
+ return {
81
+ ok: false,
82
+ code: errors_1.Codes.WRONG_RECIPIENT,
83
+ reason: `no output to payTo address ${requirements.payTo}`,
84
+ };
85
+ }
86
+ // ─── Check 4: asset (run before amount so amount=0 reports as
87
+ // WRONG_ASSET rather than INSUFFICIENT_AMOUNT) ─────────
88
+ if (paid === 0n) {
89
+ return {
90
+ ok: false,
91
+ code: errors_1.Codes.WRONG_ASSET,
92
+ reason: `outputs to payTo do not contain asset ${requirements.asset}`,
93
+ };
94
+ }
95
+ // ─── Check 3: amount ───────────────────────────────────────────────
96
+ if (paid < required) {
97
+ return {
98
+ ok: false,
99
+ code: errors_1.Codes.INSUFFICIENT_AMOUNT,
100
+ reason: `paid ${paid.toString()} < required ${required.toString()} of asset ${requirements.asset}`,
101
+ };
102
+ }
103
+ // ─── Check 5a: nonce UTxO appears in tx inputs ─────────────────────
104
+ // (5b — UTxO is unspent — runs in facilitator/nonce.ts after we've
105
+ // confirmed the buyer's structural intent here.)
106
+ const nonceInInputs = decoded.inputs.some(i => i.txHash === decoded.nonce.txHash && i.outputIndex === decoded.nonce.index);
107
+ if (!nonceInInputs) {
108
+ return {
109
+ ok: false,
110
+ code: errors_1.Codes.NONCE_NOT_REFERENCED,
111
+ reason: `nonce UTxO ${decoded.nonce.txHash}#${decoded.nonce.index} is not referenced as a tx input`,
112
+ };
113
+ }
114
+ // ─── Check 6: TTL / expiry ─────────────────────────────────────────
115
+ // Slot semantics: `ttl_bignum` is the FIRST slot at which the tx is
116
+ // INVALID — so the tx must be submitted before that slot. We require
117
+ // `currentSlot < ttlSlot`; equality means the window just closed.
118
+ if (decoded.ttlSlot === null) {
119
+ if (!opts.allowNoTtl) {
120
+ return {
121
+ ok: false,
122
+ code: errors_1.Codes.EXPIRED_TTL,
123
+ reason: 'transaction has no validity-range upper bound (ttl); set one or call with allowNoTtl=true',
124
+ };
125
+ }
126
+ }
127
+ else if (opts.currentSlot >= decoded.ttlSlot) {
128
+ return {
129
+ ok: false,
130
+ code: errors_1.Codes.EXPIRED_TTL,
131
+ reason: `ttl ${decoded.ttlSlot} already passed (current slot ${opts.currentSlot})`,
132
+ };
133
+ }
134
+ // ─── All structural checks pass ────────────────────────────────────
135
+ // `payerAddr` is intentionally omitted here — we don't have the
136
+ // buyer's input addresses without resolving the referenced UTxOs.
137
+ // The facilitator can fill it in via `bridge.getTransactionByHash`
138
+ // on the nonce input, if the caller cares for audit purposes.
139
+ const network = requirements.network;
140
+ return {
141
+ ok: true,
142
+ claim: {
143
+ txHash: decoded.txHash,
144
+ amountUnits: paid.toString(),
145
+ network,
146
+ unit,
147
+ asset: requirements.asset,
148
+ resourceUrl: requirements.resource.url,
149
+ nonceRef: `${decoded.nonce.txHash}#${decoded.nonce.index}`,
150
+ },
151
+ };
152
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Cardano-x402-v2 replay-defense check (mandatory check #5).
3
+ *
4
+ * v1 had a CDS entity `X402PaymentNonces` with a UNIQUE on txHash —
5
+ * replay defense was a DB UNIQUE-constraint race. v2 moves replay
6
+ * defense **on-chain**: the buyer references a specific UTxO in the
7
+ * envelope (`payload.nonce = "<txHash>#<index>"`), that UTxO must
8
+ * appear as an input of the payment tx, and once the tx settles the
9
+ * UTxO is permanently consumed. No DB table needed.
10
+ *
11
+ * Check #5 has two parts:
12
+ * - 5a — the nonce UTxO appears in the tx inputs (in validate.ts, pure)
13
+ * - 5b — the nonce UTxO is still unspent on chain (here, chain-touching)
14
+ *
15
+ * Order in the pipeline: `validate.ts` (which runs 5a) MUST run before
16
+ * `checkNonceUnspent` here. The chain-touching call below is a single
17
+ * `bridge.isUtxoUnspent` round-trip, backed by Blockfrost `consumed_by`
18
+ * / Koios `is_spent` / Ogmios `queryLedgerState/utxo`. Spent and
19
+ * nonexistent UTxOs both surface as `false` — both translate to REPLAY.
20
+ */
21
+ import { type X402Code } from '../core/errors';
22
+ export interface NonceCheckArgs {
23
+ /** 64-char hex tx-hash of the UTxO acting as replay nonce. */
24
+ txHash: string;
25
+ outputIndex: number;
26
+ }
27
+ export type NonceResult = {
28
+ ok: true;
29
+ } | {
30
+ ok: false;
31
+ code: X402Code;
32
+ reason: string;
33
+ };
34
+ export declare function checkNonceUnspent(args: NonceCheckArgs): Promise<NonceResult>;
35
+ //# sourceMappingURL=nonce.d.ts.map
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * Cardano-x402-v2 replay-defense check (mandatory check #5).
4
+ *
5
+ * v1 had a CDS entity `X402PaymentNonces` with a UNIQUE on txHash —
6
+ * replay defense was a DB UNIQUE-constraint race. v2 moves replay
7
+ * defense **on-chain**: the buyer references a specific UTxO in the
8
+ * envelope (`payload.nonce = "<txHash>#<index>"`), that UTxO must
9
+ * appear as an input of the payment tx, and once the tx settles the
10
+ * UTxO is permanently consumed. No DB table needed.
11
+ *
12
+ * Check #5 has two parts:
13
+ * - 5a — the nonce UTxO appears in the tx inputs (in validate.ts, pure)
14
+ * - 5b — the nonce UTxO is still unspent on chain (here, chain-touching)
15
+ *
16
+ * Order in the pipeline: `validate.ts` (which runs 5a) MUST run before
17
+ * `checkNonceUnspent` here. The chain-touching call below is a single
18
+ * `bridge.isUtxoUnspent` round-trip, backed by Blockfrost `consumed_by`
19
+ * / Koios `is_spent` / Ogmios `queryLedgerState/utxo`. Spent and
20
+ * nonexistent UTxOs both surface as `false` — both translate to REPLAY.
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.checkNonceUnspent = checkNonceUnspent;
57
+ const bridge = __importStar(require("../bridge"));
58
+ const errors_1 = require("../core/errors");
59
+ async function checkNonceUnspent(args) {
60
+ const unspent = await bridge.isUtxoUnspent(args.txHash, args.outputIndex);
61
+ if (!unspent) {
62
+ return {
63
+ ok: false,
64
+ code: errors_1.Codes.REPLAY,
65
+ reason: `nonce UTxO ${args.txHash}#${args.outputIndex} is spent or does not exist on chain`,
66
+ };
67
+ }
68
+ return { ok: true };
69
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Submit a signed payment tx to Cardano and confirm settlement.
3
+ *
4
+ * Confirmation policy (v2 spec): accept after first chain sighting.
5
+ * `mempool` status is explicitly discouraged in v2 — Cardano's
6
+ * Ouroboros Praos has probabilistic finality, so "in mempool" gives
7
+ * no economic guarantee. We poll for first-chain-sighting via
8
+ * `getTransactionByHash` (resolves to non-null when Blockfrost / Koios
9
+ * has indexed the tx; that's effectively ≥1 confirmation).
10
+ *
11
+ * Confirmation budget: middleware paths use ~60s (covers preprod's
12
+ * worst-case block time of ~20s plus indexer lag). On timeout we
13
+ * return `{ confirmed: false, pending: true }` — the spec contract is
14
+ * that the buyer retries with the same `PAYMENT-SIGNATURE`. Replay
15
+ * defense (on-chain via UTxO nonce) ensures only one retry actually
16
+ * gets served.
17
+ */
18
+ import { type X402Code } from '../core/errors';
19
+ export interface SettleArgs {
20
+ /** Hex of the signed tx (NOT base64). */
21
+ signedTxCborHex: string;
22
+ /** Locally-computed tx hash from the FixedTransaction; we cross-check submit's response. */
23
+ expectedTxHash: string;
24
+ pollBudgetMs?: number;
25
+ pollIntervalMs?: number;
26
+ }
27
+ export interface SettleResult {
28
+ confirmed: boolean;
29
+ /** True iff submit succeeded but the tx is not yet indexed. */
30
+ pending?: boolean;
31
+ txHash?: string;
32
+ code?: X402Code;
33
+ reason?: string;
34
+ }
35
+ export declare function settle({ signedTxCborHex, expectedTxHash, pollBudgetMs, pollIntervalMs, }: SettleArgs): Promise<SettleResult>;
36
+ //# sourceMappingURL=settle.d.ts.map
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * Submit a signed payment tx to Cardano and confirm settlement.
4
+ *
5
+ * Confirmation policy (v2 spec): accept after first chain sighting.
6
+ * `mempool` status is explicitly discouraged in v2 — Cardano's
7
+ * Ouroboros Praos has probabilistic finality, so "in mempool" gives
8
+ * no economic guarantee. We poll for first-chain-sighting via
9
+ * `getTransactionByHash` (resolves to non-null when Blockfrost / Koios
10
+ * has indexed the tx; that's effectively ≥1 confirmation).
11
+ *
12
+ * Confirmation budget: middleware paths use ~60s (covers preprod's
13
+ * worst-case block time of ~20s plus indexer lag). On timeout we
14
+ * return `{ confirmed: false, pending: true }` — the spec contract is
15
+ * that the buyer retries with the same `PAYMENT-SIGNATURE`. Replay
16
+ * defense (on-chain via UTxO nonce) ensures only one retry actually
17
+ * gets served.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.settle = settle;
54
+ const bridge = __importStar(require("../bridge"));
55
+ const errors_1 = require("../core/errors");
56
+ /**
57
+ * Patterns surfaced by the submit step that mean "the tx is already
58
+ * known to the network" — either in mempool or already mined. In
59
+ * both cases we should NOT treat as failure; we should fall through
60
+ * to polling.
61
+ *
62
+ * - Blockfrost: "Transaction is already in the mempool"
63
+ * - Cardano node: "ConwayMempoolFailure ... Transaction has probably already been included"
64
+ * - Ouroboros: "BadInputsUTxO" / "all inputs are spent" (already mined)
65
+ * - Generic: "transaction already exists"
66
+ *
67
+ * The submit step's failure modes are heterogeneous across backends;
68
+ * regex matching is the only portable detector.
69
+ */
70
+ const TX_ALREADY_KNOWN_RE = new RegExp([
71
+ 'already (in (the )?(mempool|chain)|exists|been included)',
72
+ 'transaction has probably already been included',
73
+ 'all inputs are spent',
74
+ 'badinputsutxo',
75
+ 'valuenotconserved',
76
+ 'inputsdepleted',
77
+ ].join('|'), 'i');
78
+ async function settle({ signedTxCborHex, expectedTxHash, pollBudgetMs = 60_000, pollIntervalMs = 2_500, }) {
79
+ if (!signedTxCborHex)
80
+ throw new TypeError('settle: signedTxCborHex required');
81
+ if (!expectedTxHash)
82
+ throw new TypeError('settle: expectedTxHash required');
83
+ // 1. Submit
84
+ let submittedHash;
85
+ try {
86
+ submittedHash = await bridge.submitTransaction(signedTxCborHex);
87
+ }
88
+ catch (err) {
89
+ const msg = String(err?.message ?? err ?? '');
90
+ if (TX_ALREADY_KNOWN_RE.test(msg)) {
91
+ // Idempotency: another submit of the same CBOR already happened.
92
+ // Proceed to polling.
93
+ submittedHash = expectedTxHash;
94
+ }
95
+ else {
96
+ return {
97
+ confirmed: false,
98
+ code: errors_1.Codes.SUBMIT_FAILED,
99
+ reason: msg.slice(0, 200),
100
+ };
101
+ }
102
+ }
103
+ // Cross-check: backend's hash must match our locally-computed one.
104
+ // If it doesn't, something is structurally off — bail loudly.
105
+ if (submittedHash && submittedHash.toLowerCase() !== expectedTxHash.toLowerCase()) {
106
+ return {
107
+ confirmed: false,
108
+ code: errors_1.Codes.SUBMIT_FAILED,
109
+ reason: `submit returned hash ${submittedHash} but tx hashes to ${expectedTxHash}`,
110
+ };
111
+ }
112
+ // 2. Poll for first chain sighting.
113
+ const deadline = Date.now() + pollBudgetMs;
114
+ while (Date.now() < deadline) {
115
+ const tx = await bridge.getTransactionByHash(expectedTxHash);
116
+ if (tx)
117
+ return { confirmed: true, txHash: expectedTxHash };
118
+ await new Promise(r => setTimeout(r, pollIntervalMs));
119
+ }
120
+ // 3. Timed out.
121
+ return {
122
+ confirmed: false,
123
+ pending: true,
124
+ txHash: expectedTxHash,
125
+ code: errors_1.Codes.PENDING,
126
+ reason: 'transaction submitted but not yet visible on chain',
127
+ };
128
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The facilitator orchestrator: end-to-end pipeline from raw header to
3
+ * an `accepted | rejected | pending` outcome.
4
+ *
5
+ * Pipeline (v2):
6
+ * 1. decode (PAYMENT-SIGNATURE → DecodedPayment)
7
+ * 2. validate (6 mandatory checks, pure)
8
+ * 3. checkNonceUnspent (chain — UTxO still spendable)
9
+ * 4. settle (submit + poll-until-confirmed)
10
+ * 5. onAccepted callback (consumer-side audit, best-effort)
11
+ *
12
+ * Order rationale:
13
+ * - `validate` runs the input-side (5a) BEFORE `checkNonceUnspent`
14
+ * does the chain-side (5b), so we avoid the round-trip for txs
15
+ * whose inputs don't include the claimed nonce.
16
+ * - `checkNonceUnspent` runs BEFORE `settle`, because submitting a
17
+ * CBOR whose nonce was already spent will fail at the network
18
+ * level anyway, and we want to return a precise REPLAY code
19
+ * instead of a generic SUBMIT_FAILED.
20
+ * - `onAccepted` runs ONLY after settle confirms — we never call it
21
+ * for pending/rejected outcomes.
22
+ */
23
+ import { type X402Code } from '../core/errors';
24
+ import type { PaymentClaim, PaymentRequirementsBody } from '../core/types';
25
+ export type ProcessKind = 'accepted' | 'rejected' | 'pending';
26
+ export interface ProcessArgs {
27
+ /** Raw header value (undefined if missing). */
28
+ paymentHeader: string | string[] | undefined;
29
+ /** Full 402 body — the validator inspects `accepts[0]`. */
30
+ requirementsBody: PaymentRequirementsBody;
31
+ /** Optional override of the settle poll budget (ms). Default 60_000. */
32
+ settlePollBudgetMs?: number;
33
+ /**
34
+ * Optional: callback invoked on successful payment. Use for consumer-
35
+ * side audit (e.g. CHAINFEED writing to FeedReads, ODATAPAY writing to
36
+ * Receipts). Throws here are swallowed and logged — the canonical
37
+ * record is on chain.
38
+ */
39
+ onAccepted?: (claim: PaymentClaim) => void | Promise<void>;
40
+ /**
41
+ * Optional: TTL check tolerance. Default false — txs without a
42
+ * validity-range upper bound are rejected.
43
+ */
44
+ allowNoTtl?: boolean;
45
+ }
46
+ export type ProcessResult = {
47
+ kind: 'accepted';
48
+ txHash: string;
49
+ payment: PaymentClaim;
50
+ /** base64 of `{ success: true, network, transaction }` for X-PAYMENT-RESPONSE header. */
51
+ paymentResponseB64: string;
52
+ } | {
53
+ kind: 'rejected';
54
+ code: X402Code;
55
+ reason: string;
56
+ requirementsBody: PaymentRequirementsBody;
57
+ } | {
58
+ kind: 'pending';
59
+ code: X402Code;
60
+ reason?: string;
61
+ txHash?: string;
62
+ requirementsBody: PaymentRequirementsBody;
63
+ };
64
+ export declare function process(args: ProcessArgs): Promise<ProcessResult>;
65
+ //# sourceMappingURL=verify.d.ts.map