@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,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cardano-x402-v2 asset identifier handling.
|
|
3
|
+
*
|
|
4
|
+
* v2 expresses an asset as a single string:
|
|
5
|
+
* - For native assets: `<policyIdHex>.<assetNameHex>` (DOT separator)
|
|
6
|
+
* where policyId is 28 bytes (56 hex chars) and assetNameHex is 0..64 hex chars.
|
|
7
|
+
* An empty name (NFT-style) is `<policyIdHex>.`
|
|
8
|
+
* - For ADA: the literal string `'lovelace'`
|
|
9
|
+
*
|
|
10
|
+
* v1 split policy and name across `asset` + `extra.assetNameHex`; we
|
|
11
|
+
* refuse that shape outright so consumers can't mix specs.
|
|
12
|
+
*/
|
|
13
|
+
export interface ParsedAsset {
|
|
14
|
+
/** raw v2 string as emitted by buildPaymentRequirements */
|
|
15
|
+
raw: string;
|
|
16
|
+
/** true when the asset is ADA (lovelace) */
|
|
17
|
+
isLovelace: boolean;
|
|
18
|
+
/** lowercase 56-char hex; empty string when isLovelace */
|
|
19
|
+
policyId: string;
|
|
20
|
+
/** lowercase hex, 0..64 chars; empty string when isLovelace OR when name is empty */
|
|
21
|
+
assetNameHex: string;
|
|
22
|
+
/**
|
|
23
|
+
* Concatenation used as the canonical UTxO `unit` key by Blockfrost /
|
|
24
|
+
* Koios / ODATANO (`policyId + assetNameHex`). Empty for lovelace —
|
|
25
|
+
* comparisons against UTxO assets short-circuit via `isLovelace`.
|
|
26
|
+
*/
|
|
27
|
+
unit: string;
|
|
28
|
+
}
|
|
29
|
+
/** Parse a v2 asset string. Throws X402Error on malformed input. */
|
|
30
|
+
export declare function parseAsset(s: string): ParsedAsset;
|
|
31
|
+
/** Build a v2 asset string from policy + assetName parts. */
|
|
32
|
+
export declare function buildAssetString(policyId: string, assetNameHex?: string): string;
|
|
33
|
+
//# sourceMappingURL=asset.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cardano-x402-v2 asset identifier handling.
|
|
4
|
+
*
|
|
5
|
+
* v2 expresses an asset as a single string:
|
|
6
|
+
* - For native assets: `<policyIdHex>.<assetNameHex>` (DOT separator)
|
|
7
|
+
* where policyId is 28 bytes (56 hex chars) and assetNameHex is 0..64 hex chars.
|
|
8
|
+
* An empty name (NFT-style) is `<policyIdHex>.`
|
|
9
|
+
* - For ADA: the literal string `'lovelace'`
|
|
10
|
+
*
|
|
11
|
+
* v1 split policy and name across `asset` + `extra.assetNameHex`; we
|
|
12
|
+
* refuse that shape outright so consumers can't mix specs.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.parseAsset = parseAsset;
|
|
16
|
+
exports.buildAssetString = buildAssetString;
|
|
17
|
+
const errors_1 = require("./errors");
|
|
18
|
+
const POLICY_RE = /^[0-9a-f]{56}$/i;
|
|
19
|
+
const NAME_RE = /^[0-9a-f]{0,64}$/i;
|
|
20
|
+
/** Parse a v2 asset string. Throws X402Error on malformed input. */
|
|
21
|
+
function parseAsset(s) {
|
|
22
|
+
if (typeof s !== 'string' || s.length === 0) {
|
|
23
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, 'asset must be a non-empty string');
|
|
24
|
+
}
|
|
25
|
+
if (s === 'lovelace') {
|
|
26
|
+
return { raw: s, isLovelace: true, policyId: '', assetNameHex: '', unit: '' };
|
|
27
|
+
}
|
|
28
|
+
const dot = s.indexOf('.');
|
|
29
|
+
if (dot < 0) {
|
|
30
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `asset '${s}' must be 'lovelace' or '<policyIdHex>.<assetNameHex>' (dot-separated)`);
|
|
31
|
+
}
|
|
32
|
+
const policyId = s.slice(0, dot).toLowerCase();
|
|
33
|
+
const assetNameHex = s.slice(dot + 1).toLowerCase();
|
|
34
|
+
if (!POLICY_RE.test(policyId)) {
|
|
35
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `asset policyId '${policyId}' must be 28-byte (56-char) hex`);
|
|
36
|
+
}
|
|
37
|
+
if (!NAME_RE.test(assetNameHex)) {
|
|
38
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `asset name '${assetNameHex}' must be 0..32 byte (0..64 char) hex`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
raw: s,
|
|
42
|
+
isLovelace: false,
|
|
43
|
+
policyId,
|
|
44
|
+
assetNameHex,
|
|
45
|
+
unit: (policyId + assetNameHex).toLowerCase(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Build a v2 asset string from policy + assetName parts. */
|
|
49
|
+
function buildAssetString(policyId, assetNameHex = '') {
|
|
50
|
+
if (!POLICY_RE.test(policyId)) {
|
|
51
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `buildAssetString: policyId '${policyId}' must be 56-char hex`);
|
|
52
|
+
}
|
|
53
|
+
if (assetNameHex && !NAME_RE.test(assetNameHex)) {
|
|
54
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_ASSET_FORMAT, `buildAssetString: assetNameHex '${assetNameHex}' must be 0..64-char hex`);
|
|
55
|
+
}
|
|
56
|
+
return `${policyId.toLowerCase()}.${assetNameHex.toLowerCase()}`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode the `PAYMENT-SIGNATURE` header (Cardano-x402-v2 wire format).
|
|
3
|
+
*
|
|
4
|
+
* Wire format:
|
|
5
|
+
* PAYMENT-SIGNATURE: base64(JSON.stringify({
|
|
6
|
+
* x402Version: 2,
|
|
7
|
+
* scheme: 'exact',
|
|
8
|
+
* network: 'cardano:preprod' | 'cardano:mainnet' | 'cardano:preview',
|
|
9
|
+
* payload: {
|
|
10
|
+
* transaction: '<base64 CBOR of signed tx>',
|
|
11
|
+
* nonce: '<txHash>#<outputIndex>'
|
|
12
|
+
* }
|
|
13
|
+
* }))
|
|
14
|
+
*
|
|
15
|
+
* The decoder is **pure** — no chain calls, no DB. It produces a
|
|
16
|
+
* `DecodedPayment` that downstream `validate.ts` checks against
|
|
17
|
+
* `PaymentRequirementEntry` (the 6 mandatory checks).
|
|
18
|
+
*/
|
|
19
|
+
import type { DecodedPayment } from './types';
|
|
20
|
+
/**
|
|
21
|
+
* Decode a `PAYMENT-SIGNATURE` header value end-to-end. Throws X402Error
|
|
22
|
+
* with a precise `code` on any malformed input — the caller catches and
|
|
23
|
+
* surfaces the code in the 402 response body.
|
|
24
|
+
*/
|
|
25
|
+
export declare function decode(paymentHeader: string | undefined | null): DecodedPayment;
|
|
26
|
+
//# sourceMappingURL=decode.d.ts.map
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Decode the `PAYMENT-SIGNATURE` header (Cardano-x402-v2 wire format).
|
|
4
|
+
*
|
|
5
|
+
* Wire format:
|
|
6
|
+
* PAYMENT-SIGNATURE: base64(JSON.stringify({
|
|
7
|
+
* x402Version: 2,
|
|
8
|
+
* scheme: 'exact',
|
|
9
|
+
* network: 'cardano:preprod' | 'cardano:mainnet' | 'cardano:preview',
|
|
10
|
+
* payload: {
|
|
11
|
+
* transaction: '<base64 CBOR of signed tx>',
|
|
12
|
+
* nonce: '<txHash>#<outputIndex>'
|
|
13
|
+
* }
|
|
14
|
+
* }))
|
|
15
|
+
*
|
|
16
|
+
* The decoder is **pure** — no chain calls, no DB. It produces a
|
|
17
|
+
* `DecodedPayment` that downstream `validate.ts` checks against
|
|
18
|
+
* `PaymentRequirementEntry` (the 6 mandatory checks).
|
|
19
|
+
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.decode = decode;
|
|
55
|
+
const CSL = __importStar(require("@emurgo/cardano-serialization-lib-nodejs"));
|
|
56
|
+
const errors_1 = require("./errors");
|
|
57
|
+
const SUPPORTED_VERSION = 2;
|
|
58
|
+
const SUPPORTED_SCHEME = 'exact';
|
|
59
|
+
const NONCE_RE = /^([0-9a-f]{64})#(\d+)$/i;
|
|
60
|
+
function decodeBase64ToBuffer(s, errCode) {
|
|
61
|
+
// Node's Buffer.from is lenient (silently drops bad chars). Re-encode
|
|
62
|
+
// and compare modulo padding to catch malformed input early — otherwise
|
|
63
|
+
// garbage in `transaction` would only fail at CBOR parse time with a
|
|
64
|
+
// confusing error.
|
|
65
|
+
const buf = Buffer.from(s, 'base64');
|
|
66
|
+
if (buf.toString('base64').replace(/=+$/, '') !== String(s).replace(/=+$/, '')) {
|
|
67
|
+
throw new errors_1.X402Error(errCode, 'malformed base64 payload');
|
|
68
|
+
}
|
|
69
|
+
return buf;
|
|
70
|
+
}
|
|
71
|
+
function extractOutputs(txBody) {
|
|
72
|
+
const out = txBody.outputs();
|
|
73
|
+
const result = [];
|
|
74
|
+
for (let i = 0; i < out.len(); i++) {
|
|
75
|
+
const o = out.get(i);
|
|
76
|
+
const addr = o.address().to_bech32();
|
|
77
|
+
const value = o.amount();
|
|
78
|
+
const lovelace = value.coin().to_str();
|
|
79
|
+
const assets = [];
|
|
80
|
+
const ma = value.multiasset();
|
|
81
|
+
if (ma) {
|
|
82
|
+
const policies = ma.keys();
|
|
83
|
+
for (let p = 0; p < policies.len(); p++) {
|
|
84
|
+
const policy = policies.get(p);
|
|
85
|
+
const policyHex = Buffer.from(policy.to_bytes()).toString('hex').toLowerCase();
|
|
86
|
+
const assetMap = ma.get(policy);
|
|
87
|
+
if (!assetMap)
|
|
88
|
+
continue;
|
|
89
|
+
const names = assetMap.keys();
|
|
90
|
+
for (let n = 0; n < names.len(); n++) {
|
|
91
|
+
const name = names.get(n);
|
|
92
|
+
const nameHex = Buffer.from(name.name()).toString('hex').toLowerCase();
|
|
93
|
+
const qty = assetMap.get(name);
|
|
94
|
+
if (!qty)
|
|
95
|
+
continue;
|
|
96
|
+
assets.push({
|
|
97
|
+
unit: (policyHex + nameHex),
|
|
98
|
+
policyId: policyHex,
|
|
99
|
+
assetNameHex: nameHex,
|
|
100
|
+
quantity: qty.to_str(),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
result.push({ outputIndex: i, address: addr, lovelace, assets });
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
function extractInputs(txBody) {
|
|
110
|
+
const ins = txBody.inputs();
|
|
111
|
+
const result = [];
|
|
112
|
+
for (let i = 0; i < ins.len(); i++) {
|
|
113
|
+
const inp = ins.get(i);
|
|
114
|
+
result.push({
|
|
115
|
+
txHash: Buffer.from(inp.transaction_id().to_bytes()).toString('hex').toLowerCase(),
|
|
116
|
+
outputIndex: inp.index(),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Pull validity range bounds. CSL exposes `ttl()` (upper) since Shelley
|
|
123
|
+
* and `validity_start_interval_bignum()` (lower) since Allegra. Both can
|
|
124
|
+
* be absent — in which case we return null and the TTL check is skipped
|
|
125
|
+
* (per v2 spec: only validate TTL if buyer set one).
|
|
126
|
+
*/
|
|
127
|
+
function extractValidityRange(txBody) {
|
|
128
|
+
let ttlSlot = null;
|
|
129
|
+
let validityStartSlot = null;
|
|
130
|
+
try {
|
|
131
|
+
const ttl = txBody.ttl_bignum();
|
|
132
|
+
if (ttl) {
|
|
133
|
+
// BigNum → string → number; slots fit comfortably in JS number
|
|
134
|
+
// (current preprod ~85M, max safe int 9e15).
|
|
135
|
+
ttlSlot = Number(ttl.to_str());
|
|
136
|
+
if (!Number.isFinite(ttlSlot))
|
|
137
|
+
ttlSlot = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
ttlSlot = null;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const start = txBody.validity_start_interval_bignum();
|
|
145
|
+
if (start) {
|
|
146
|
+
validityStartSlot = Number(start.to_str());
|
|
147
|
+
if (!Number.isFinite(validityStartSlot))
|
|
148
|
+
validityStartSlot = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
validityStartSlot = null;
|
|
153
|
+
}
|
|
154
|
+
return { ttlSlot, validityStartSlot };
|
|
155
|
+
}
|
|
156
|
+
function parseNonceRef(nonce) {
|
|
157
|
+
const m = NONCE_RE.exec(nonce);
|
|
158
|
+
if (!m) {
|
|
159
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_NONCE_FORMAT, `nonce '${nonce}' must be '<txHash>#<outputIndex>' (64-hex#int)`);
|
|
160
|
+
}
|
|
161
|
+
const idx = Number(m[2]);
|
|
162
|
+
if (!Number.isFinite(idx) || idx < 0 || idx > 65535) {
|
|
163
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_NONCE_FORMAT, `nonce output index ${m[2]} out of range`);
|
|
164
|
+
}
|
|
165
|
+
return { txHash: m[1].toLowerCase(), index: idx };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Decode a `PAYMENT-SIGNATURE` header value end-to-end. Throws X402Error
|
|
169
|
+
* with a precise `code` on any malformed input — the caller catches and
|
|
170
|
+
* surfaces the code in the 402 response body.
|
|
171
|
+
*/
|
|
172
|
+
function decode(paymentHeader) {
|
|
173
|
+
if (!paymentHeader || typeof paymentHeader !== 'string') {
|
|
174
|
+
throw new errors_1.X402Error(errors_1.Codes.MISSING_HEADER);
|
|
175
|
+
}
|
|
176
|
+
// 1. base64 → JSON
|
|
177
|
+
const outerBuf = decodeBase64ToBuffer(paymentHeader, errors_1.Codes.INVALID_BASE64);
|
|
178
|
+
let raw;
|
|
179
|
+
try {
|
|
180
|
+
raw = JSON.parse(outerBuf.toString('utf8'));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_JSON, 'PAYMENT-SIGNATURE body is not valid JSON');
|
|
184
|
+
}
|
|
185
|
+
// 2. Field shape
|
|
186
|
+
for (const f of ['x402Version', 'scheme', 'network', 'payload']) {
|
|
187
|
+
if (!(f in raw))
|
|
188
|
+
throw new errors_1.X402Error(errors_1.Codes.MISSING_FIELD, `missing field: ${f}`);
|
|
189
|
+
}
|
|
190
|
+
if (raw.x402Version !== SUPPORTED_VERSION) {
|
|
191
|
+
throw new errors_1.X402Error(errors_1.Codes.UNSUPPORTED_VERSION, `x402Version ${raw.x402Version} not supported (only ${SUPPORTED_VERSION})`);
|
|
192
|
+
}
|
|
193
|
+
if (raw.scheme !== SUPPORTED_SCHEME) {
|
|
194
|
+
throw new errors_1.X402Error(errors_1.Codes.UNSUPPORTED_SCHEME, `scheme '${raw.scheme}' not supported (only '${SUPPORTED_SCHEME}')`);
|
|
195
|
+
}
|
|
196
|
+
const payload = raw.payload;
|
|
197
|
+
if (!payload || typeof payload.transaction !== 'string') {
|
|
198
|
+
throw new errors_1.X402Error(errors_1.Codes.MISSING_FIELD, 'payload.transaction is required');
|
|
199
|
+
}
|
|
200
|
+
if (typeof payload.nonce !== 'string' || payload.nonce.length === 0) {
|
|
201
|
+
throw new errors_1.X402Error(errors_1.Codes.MISSING_FIELD, 'payload.nonce is required (v2 UTxO-ref)');
|
|
202
|
+
}
|
|
203
|
+
// 3. Tx CBOR → CSL Transaction (parses both `transaction` and `fixed`
|
|
204
|
+
// representation; we need both — Transaction for body access,
|
|
205
|
+
// FixedTransaction for byte-stable hash).
|
|
206
|
+
const txBuf = decodeBase64ToBuffer(payload.transaction, errors_1.Codes.INVALID_CBOR);
|
|
207
|
+
let tx;
|
|
208
|
+
try {
|
|
209
|
+
tx = CSL.Transaction.from_bytes(txBuf);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_CBOR, 'transaction CBOR did not decode');
|
|
213
|
+
}
|
|
214
|
+
// 4. Diagnostics
|
|
215
|
+
const txBody = tx.body();
|
|
216
|
+
const wits = tx.witness_set();
|
|
217
|
+
const vkeys = wits.vkeys();
|
|
218
|
+
const vkeyWitnessCount = vkeys ? vkeys.len() : 0;
|
|
219
|
+
const txHashBytes = CSL.FixedTransaction
|
|
220
|
+
.from_bytes(txBuf)
|
|
221
|
+
.transaction_hash()
|
|
222
|
+
.to_bytes();
|
|
223
|
+
const txHash = Buffer.from(txHashBytes).toString('hex').toLowerCase();
|
|
224
|
+
const validity = extractValidityRange(txBody);
|
|
225
|
+
const nonce = parseNonceRef(payload.nonce);
|
|
226
|
+
const envelope = {
|
|
227
|
+
x402Version: SUPPORTED_VERSION,
|
|
228
|
+
scheme: SUPPORTED_SCHEME,
|
|
229
|
+
network: raw.network,
|
|
230
|
+
payload: { transaction: payload.transaction, nonce: payload.nonce },
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
envelope,
|
|
234
|
+
txCborHex: Buffer.from(txBuf).toString('hex'),
|
|
235
|
+
txHash,
|
|
236
|
+
outputs: extractOutputs(txBody),
|
|
237
|
+
inputs: extractInputs(txBody),
|
|
238
|
+
vkeyWitnessCount,
|
|
239
|
+
ttlSlot: validity.ttlSlot,
|
|
240
|
+
validityStartSlot: validity.validityStartSlot,
|
|
241
|
+
nonce,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors for x402 (Cardano-v2) verification.
|
|
3
|
+
*
|
|
4
|
+
* Codes stay in lower_snake to match the masumi-spec convention so they
|
|
5
|
+
* are interoperable with v1 callers grepping for known strings. Codes
|
|
6
|
+
* specific to v2 (UTxO-ref nonce, TTL window) are marked below.
|
|
7
|
+
*
|
|
8
|
+
* The `code` field is surfaced in the 402 response body's `error` field
|
|
9
|
+
* (parenthesised after the human message), so it doubles as the
|
|
10
|
+
* machine-readable diagnostic for clients.
|
|
11
|
+
*/
|
|
12
|
+
export declare class X402Error extends Error {
|
|
13
|
+
readonly code: string;
|
|
14
|
+
constructor(code: string, message?: string);
|
|
15
|
+
}
|
|
16
|
+
export declare const Codes: Readonly<{
|
|
17
|
+
readonly MISSING_HEADER: "missing_payment_header";
|
|
18
|
+
readonly INVALID_BASE64: "invalid_base64";
|
|
19
|
+
readonly INVALID_JSON: "invalid_json";
|
|
20
|
+
readonly MISSING_FIELD: "missing_field";
|
|
21
|
+
readonly UNSUPPORTED_VERSION: "unsupported_version";
|
|
22
|
+
readonly UNSUPPORTED_SCHEME: "unsupported_scheme";
|
|
23
|
+
readonly UNSUPPORTED_METHOD: "unsupported_transfer_method";
|
|
24
|
+
readonly INVALID_CBOR: "invalid_cbor";
|
|
25
|
+
readonly INVALID_NETWORK_FORMAT: "invalid_network_format";
|
|
26
|
+
readonly INVALID_ASSET_FORMAT: "invalid_asset_format";
|
|
27
|
+
readonly INVALID_NONCE_FORMAT: "invalid_nonce_format";
|
|
28
|
+
readonly NETWORK_MISMATCH: "network_mismatch";
|
|
29
|
+
readonly WRONG_RECIPIENT: "wrong_recipient";
|
|
30
|
+
readonly INSUFFICIENT_AMOUNT: "insufficient_amount";
|
|
31
|
+
readonly WRONG_ASSET: "wrong_asset";
|
|
32
|
+
readonly REPLAY: "replay_detected";
|
|
33
|
+
readonly NONCE_NOT_REFERENCED: "nonce_not_referenced";
|
|
34
|
+
readonly EXPIRED_TTL: "expired_ttl";
|
|
35
|
+
readonly UNSIGNED_TRANSACTION: "unsigned_transaction";
|
|
36
|
+
readonly SUBMIT_FAILED: "submit_failed";
|
|
37
|
+
readonly PENDING: "invalid_transaction_state";
|
|
38
|
+
readonly BRIDGE_UNAVAILABLE: "bridge_unavailable";
|
|
39
|
+
}>;
|
|
40
|
+
export type X402Code = typeof Codes[keyof typeof Codes];
|
|
41
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Typed errors for x402 (Cardano-v2) verification.
|
|
4
|
+
*
|
|
5
|
+
* Codes stay in lower_snake to match the masumi-spec convention so they
|
|
6
|
+
* are interoperable with v1 callers grepping for known strings. Codes
|
|
7
|
+
* specific to v2 (UTxO-ref nonce, TTL window) are marked below.
|
|
8
|
+
*
|
|
9
|
+
* The `code` field is surfaced in the 402 response body's `error` field
|
|
10
|
+
* (parenthesised after the human message), so it doubles as the
|
|
11
|
+
* machine-readable diagnostic for clients.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.Codes = exports.X402Error = void 0;
|
|
15
|
+
class X402Error extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message ?? code);
|
|
19
|
+
this.name = 'X402Error';
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.X402Error = X402Error;
|
|
24
|
+
exports.Codes = Object.freeze({
|
|
25
|
+
// ---- decode ----
|
|
26
|
+
MISSING_HEADER: 'missing_payment_header',
|
|
27
|
+
INVALID_BASE64: 'invalid_base64',
|
|
28
|
+
INVALID_JSON: 'invalid_json',
|
|
29
|
+
MISSING_FIELD: 'missing_field',
|
|
30
|
+
UNSUPPORTED_VERSION: 'unsupported_version',
|
|
31
|
+
UNSUPPORTED_SCHEME: 'unsupported_scheme',
|
|
32
|
+
UNSUPPORTED_METHOD: 'unsupported_transfer_method',
|
|
33
|
+
INVALID_CBOR: 'invalid_cbor',
|
|
34
|
+
INVALID_NETWORK_FORMAT: 'invalid_network_format', // v2 requires 'cardano:<net>'
|
|
35
|
+
INVALID_ASSET_FORMAT: 'invalid_asset_format', // v2 requires '<policy>.<nameHex>'
|
|
36
|
+
INVALID_NONCE_FORMAT: 'invalid_nonce_format', // v2 requires '<txHash>#<index>'
|
|
37
|
+
// ---- validate (6 mandatory facilitator checks) ----
|
|
38
|
+
NETWORK_MISMATCH: 'network_mismatch', // check 1
|
|
39
|
+
WRONG_RECIPIENT: 'wrong_recipient', // check 2
|
|
40
|
+
INSUFFICIENT_AMOUNT: 'insufficient_amount', // check 3
|
|
41
|
+
WRONG_ASSET: 'wrong_asset', // check 4
|
|
42
|
+
REPLAY: 'replay_detected', // check 5 — UTxO already spent
|
|
43
|
+
NONCE_NOT_REFERENCED: 'nonce_not_referenced', // check 5 — UTxO not in tx inputs
|
|
44
|
+
EXPIRED_TTL: 'expired_ttl', // check 6 — validity range upper bound passed
|
|
45
|
+
// ---- supporting ----
|
|
46
|
+
UNSIGNED_TRANSACTION: 'unsigned_transaction', // sanity: no vkey witnesses
|
|
47
|
+
// ---- settle ----
|
|
48
|
+
SUBMIT_FAILED: 'submit_failed',
|
|
49
|
+
PENDING: 'invalid_transaction_state', // matches masumi spec
|
|
50
|
+
// ---- bridge / infrastructure ----
|
|
51
|
+
BRIDGE_UNAVAILABLE: 'bridge_unavailable',
|
|
52
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cardano-x402-v2 network identifiers.
|
|
3
|
+
*
|
|
4
|
+
* v2 uses **colon** as separator: `cardano:mainnet | cardano:preprod | cardano:preview`.
|
|
5
|
+
* v1 used hyphen (`cardano-mainnet`). We accept only the v2 form on input
|
|
6
|
+
* and refuse v1 strings so callers can't silently misroute funds across
|
|
7
|
+
* networks.
|
|
8
|
+
*/
|
|
9
|
+
export type Network = 'cardano:mainnet' | 'cardano:preprod' | 'cardano:preview';
|
|
10
|
+
export declare function isNetwork(s: unknown): s is Network;
|
|
11
|
+
/**
|
|
12
|
+
* Validate a network string and return it typed. Throws X402Error on
|
|
13
|
+
* malformed input — including v1-style hyphen variants — so the caller's
|
|
14
|
+
* 402 body carries a precise diagnostic.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseNetwork(s: string): Network;
|
|
17
|
+
/** True iff `payload.network` (the buyer's claim) matches the server's requirement. */
|
|
18
|
+
export declare function networksMatch(claimed: string, required: Network): boolean;
|
|
19
|
+
//# sourceMappingURL=network.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cardano-x402-v2 network identifiers.
|
|
4
|
+
*
|
|
5
|
+
* v2 uses **colon** as separator: `cardano:mainnet | cardano:preprod | cardano:preview`.
|
|
6
|
+
* v1 used hyphen (`cardano-mainnet`). We accept only the v2 form on input
|
|
7
|
+
* and refuse v1 strings so callers can't silently misroute funds across
|
|
8
|
+
* networks.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.isNetwork = isNetwork;
|
|
12
|
+
exports.parseNetwork = parseNetwork;
|
|
13
|
+
exports.networksMatch = networksMatch;
|
|
14
|
+
const errors_1 = require("./errors");
|
|
15
|
+
const VALID = new Set(['cardano:mainnet', 'cardano:preprod', 'cardano:preview']);
|
|
16
|
+
function isNetwork(s) {
|
|
17
|
+
return typeof s === 'string' && VALID.has(s);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate a network string and return it typed. Throws X402Error on
|
|
21
|
+
* malformed input — including v1-style hyphen variants — so the caller's
|
|
22
|
+
* 402 body carries a precise diagnostic.
|
|
23
|
+
*/
|
|
24
|
+
function parseNetwork(s) {
|
|
25
|
+
if (typeof s !== 'string' || s.length === 0) {
|
|
26
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_NETWORK_FORMAT, 'network must be a non-empty string');
|
|
27
|
+
}
|
|
28
|
+
if (s.includes('-') && !s.includes(':')) {
|
|
29
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_NETWORK_FORMAT, `network '${s}' uses v1 hyphen format; v2 requires colon: 'cardano:mainnet|preprod|preview'`);
|
|
30
|
+
}
|
|
31
|
+
if (!isNetwork(s)) {
|
|
32
|
+
throw new errors_1.X402Error(errors_1.Codes.INVALID_NETWORK_FORMAT, `network '${s}' is not one of cardano:mainnet | cardano:preprod | cardano:preview`);
|
|
33
|
+
}
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
/** True iff `payload.network` (the buyer's claim) matches the server's requirement. */
|
|
37
|
+
function networksMatch(claimed, required) {
|
|
38
|
+
return claimed === required;
|
|
39
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the canonical Cardano-x402-v2 PaymentRequirements body.
|
|
3
|
+
*
|
|
4
|
+
* Asset-agnostic by design — the consumer passes a v2 asset string
|
|
5
|
+
* (`<policy>.<nameHex>` or `'lovelace'`), a payTo bech32, a network, and
|
|
6
|
+
* a resource descriptor. There is NO USDM default and no decimals
|
|
7
|
+
* assumption — both belong to the consumer's product config.
|
|
8
|
+
*
|
|
9
|
+
* The v2 shape diverges from v1 in five places (see `docs/spec-v2-summary.md`
|
|
10
|
+
* once written):
|
|
11
|
+
* 1. `x402Version: 2` (was 1)
|
|
12
|
+
* 2. `accepts[].amount` (was `maxAmountRequired`)
|
|
13
|
+
* 3. `accepts[].asset` is a single (was split: `asset` + `extra.assetNameHex`)
|
|
14
|
+
* string `<policy>.<nameHex>` or `'lovelace'`
|
|
15
|
+
* 4. `accepts[].resource` is an (was a string)
|
|
16
|
+
* object `{ url, description, mimeType }`
|
|
17
|
+
* 5. `accepts[].assetTransferMethod`(new field)
|
|
18
|
+
*/
|
|
19
|
+
import { type Network } from './network';
|
|
20
|
+
import type { PaymentRequirementEntry, PaymentRequirementsBody, ResourceDescriptor, AssetTransferMethod } from './types';
|
|
21
|
+
export interface BuildPaymentRequirementsArgs {
|
|
22
|
+
/** Asset amount in raw units (BigInt-safe). */
|
|
23
|
+
amount: string | number | bigint;
|
|
24
|
+
asset: string;
|
|
25
|
+
payTo: string;
|
|
26
|
+
network: Network | string;
|
|
27
|
+
resource: ResourceDescriptor | string;
|
|
28
|
+
description?: string;
|
|
29
|
+
mimeType?: string;
|
|
30
|
+
outputSchema?: unknown;
|
|
31
|
+
assetTransferMethod?: AssetTransferMethod;
|
|
32
|
+
maxTimeoutSeconds?: number;
|
|
33
|
+
/** Free-form extras (decimals, fingerprint, UI hints). */
|
|
34
|
+
extra?: Record<string, unknown>;
|
|
35
|
+
/**
|
|
36
|
+
* If true, prepend the standard 'PAYMENT-SIGNATURE header is required'
|
|
37
|
+
* error string. Used for the missing-header path; omit for downstream
|
|
38
|
+
* rejection bodies where the caller sets a more specific `error`.
|
|
39
|
+
*/
|
|
40
|
+
withMissingHeaderError?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Construct a single `accepts[]` entry. Most callers want
|
|
44
|
+
* `buildPaymentRequirements()` which wraps this in the 402 envelope —
|
|
45
|
+
* use `buildEntry()` directly when composing multi-asset accept lists.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildEntry(args: BuildPaymentRequirementsArgs): PaymentRequirementEntry;
|
|
48
|
+
export declare function buildPaymentRequirements(args: BuildPaymentRequirementsArgs): PaymentRequirementsBody;
|
|
49
|
+
/** Pick the first `accepts[]` entry — what the validator inspects. */
|
|
50
|
+
export declare function flatRequirements(body: PaymentRequirementsBody): PaymentRequirementEntry;
|
|
51
|
+
//# sourceMappingURL=requirements.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Build the canonical Cardano-x402-v2 PaymentRequirements body.
|
|
4
|
+
*
|
|
5
|
+
* Asset-agnostic by design — the consumer passes a v2 asset string
|
|
6
|
+
* (`<policy>.<nameHex>` or `'lovelace'`), a payTo bech32, a network, and
|
|
7
|
+
* a resource descriptor. There is NO USDM default and no decimals
|
|
8
|
+
* assumption — both belong to the consumer's product config.
|
|
9
|
+
*
|
|
10
|
+
* The v2 shape diverges from v1 in five places (see `docs/spec-v2-summary.md`
|
|
11
|
+
* once written):
|
|
12
|
+
* 1. `x402Version: 2` (was 1)
|
|
13
|
+
* 2. `accepts[].amount` (was `maxAmountRequired`)
|
|
14
|
+
* 3. `accepts[].asset` is a single (was split: `asset` + `extra.assetNameHex`)
|
|
15
|
+
* string `<policy>.<nameHex>` or `'lovelace'`
|
|
16
|
+
* 4. `accepts[].resource` is an (was a string)
|
|
17
|
+
* object `{ url, description, mimeType }`
|
|
18
|
+
* 5. `accepts[].assetTransferMethod`(new field)
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.buildEntry = buildEntry;
|
|
22
|
+
exports.buildPaymentRequirements = buildPaymentRequirements;
|
|
23
|
+
exports.flatRequirements = flatRequirements;
|
|
24
|
+
const network_1 = require("./network");
|
|
25
|
+
const asset_1 = require("./asset");
|
|
26
|
+
function normalizeResource(resource, description, mimeType, outputSchema) {
|
|
27
|
+
if (typeof resource === 'string') {
|
|
28
|
+
return {
|
|
29
|
+
url: resource,
|
|
30
|
+
description: description ?? '',
|
|
31
|
+
mimeType: mimeType ?? 'application/json',
|
|
32
|
+
...(outputSchema !== undefined ? { outputSchema } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// Object form: caller's fields win, but allow per-call overrides.
|
|
36
|
+
return {
|
|
37
|
+
url: resource.url,
|
|
38
|
+
description: description ?? resource.description ?? '',
|
|
39
|
+
mimeType: mimeType ?? resource.mimeType ?? 'application/json',
|
|
40
|
+
...(outputSchema !== undefined
|
|
41
|
+
? { outputSchema }
|
|
42
|
+
: (resource.outputSchema !== undefined ? { outputSchema: resource.outputSchema } : {})),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Construct a single `accepts[]` entry. Most callers want
|
|
47
|
+
* `buildPaymentRequirements()` which wraps this in the 402 envelope —
|
|
48
|
+
* use `buildEntry()` directly when composing multi-asset accept lists.
|
|
49
|
+
*/
|
|
50
|
+
function buildEntry(args) {
|
|
51
|
+
if (!args.payTo)
|
|
52
|
+
throw new Error('buildEntry: payTo is required');
|
|
53
|
+
const network = (0, network_1.parseNetwork)(args.network);
|
|
54
|
+
const parsedAsset = (0, asset_1.parseAsset)(args.asset);
|
|
55
|
+
const amount = String(args.amount);
|
|
56
|
+
if (!/^\d+$/.test(amount) || amount === '0') {
|
|
57
|
+
throw new Error(`buildEntry: amount must be a positive integer (got '${amount}')`);
|
|
58
|
+
}
|
|
59
|
+
const resource = normalizeResource(args.resource, args.description, args.mimeType, args.outputSchema);
|
|
60
|
+
return {
|
|
61
|
+
scheme: 'exact',
|
|
62
|
+
network,
|
|
63
|
+
asset: parsedAsset.raw,
|
|
64
|
+
amount,
|
|
65
|
+
payTo: args.payTo,
|
|
66
|
+
resource,
|
|
67
|
+
assetTransferMethod: args.assetTransferMethod ?? 'default',
|
|
68
|
+
maxTimeoutSeconds: args.maxTimeoutSeconds ?? 600,
|
|
69
|
+
...(args.extra ? { extra: args.extra } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function buildPaymentRequirements(args) {
|
|
73
|
+
const accepts = [buildEntry(args)];
|
|
74
|
+
return {
|
|
75
|
+
x402Version: 2,
|
|
76
|
+
...(args.withMissingHeaderError ? { error: 'PAYMENT-SIGNATURE header is required' } : {}),
|
|
77
|
+
accepts,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Pick the first `accepts[]` entry — what the validator inspects. */
|
|
81
|
+
function flatRequirements(body) {
|
|
82
|
+
if (!body.accepts || body.accepts.length === 0) {
|
|
83
|
+
throw new Error('flatRequirements: PaymentRequirementsBody.accepts is empty');
|
|
84
|
+
}
|
|
85
|
+
return body.accepts[0];
|
|
86
|
+
}
|