@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 +21 -0
- package/README.md +350 -0
- package/cds-plugin.js +10 -0
- package/package.json +52 -0
- package/srv/bridge.d.ts +68 -0
- package/srv/bridge.js +155 -0
- package/srv/core/asset.d.ts +33 -0
- package/srv/core/asset.js +57 -0
- package/srv/core/decode.d.ts +26 -0
- package/srv/core/decode.js +243 -0
- package/srv/core/errors.d.ts +41 -0
- package/srv/core/errors.js +52 -0
- package/srv/core/network.d.ts +19 -0
- package/srv/core/network.js +39 -0
- package/srv/core/requirements.d.ts +51 -0
- package/srv/core/requirements.js +86 -0
- package/srv/core/types.d.ts +116 -0
- package/srv/core/types.js +10 -0
- package/srv/core/validate.d.ts +45 -0
- package/srv/core/validate.js +152 -0
- package/srv/facilitator/nonce.d.ts +35 -0
- package/srv/facilitator/nonce.js +69 -0
- package/srv/facilitator/settle.d.ts +36 -0
- package/srv/facilitator/settle.js +128 -0
- package/srv/facilitator/verify.d.ts +65 -0
- package/srv/facilitator/verify.js +188 -0
- package/srv/helpers/build-unsigned-tx.d.ts +56 -0
- package/srv/helpers/build-unsigned-tx.js +203 -0
- package/srv/helpers/verify-confirmed.d.ts +43 -0
- package/srv/helpers/verify-confirmed.js +129 -0
- package/srv/index.d.ts +51 -0
- package/srv/index.js +111 -0
- package/srv/middleware/cap.d.ts +65 -0
- package/srv/middleware/cap.js +170 -0
- package/srv/middleware/express.d.ts +66 -0
- package/srv/middleware/express.js +116 -0
- package/srv/plugin.d.ts +16 -0
- package/srv/plugin.js +73 -0
|
@@ -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
|