@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,188 @@
1
+ "use strict";
2
+ /**
3
+ * The facilitator orchestrator: end-to-end pipeline from raw header to
4
+ * an `accepted | rejected | pending` outcome.
5
+ *
6
+ * Pipeline (v2):
7
+ * 1. decode (PAYMENT-SIGNATURE → DecodedPayment)
8
+ * 2. validate (6 mandatory checks, pure)
9
+ * 3. checkNonceUnspent (chain — UTxO still spendable)
10
+ * 4. settle (submit + poll-until-confirmed)
11
+ * 5. onAccepted callback (consumer-side audit, best-effort)
12
+ *
13
+ * Order rationale:
14
+ * - `validate` runs the input-side (5a) BEFORE `checkNonceUnspent`
15
+ * does the chain-side (5b), so we avoid the round-trip for txs
16
+ * whose inputs don't include the claimed nonce.
17
+ * - `checkNonceUnspent` runs BEFORE `settle`, because submitting a
18
+ * CBOR whose nonce was already spent will fail at the network
19
+ * level anyway, and we want to return a precise REPLAY code
20
+ * instead of a generic SUBMIT_FAILED.
21
+ * - `onAccepted` runs ONLY after settle confirms — we never call it
22
+ * for pending/rejected outcomes.
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ var __importDefault = (this && this.__importDefault) || function (mod) {
58
+ return (mod && mod.__esModule) ? mod : { "default": mod };
59
+ };
60
+ Object.defineProperty(exports, "__esModule", { value: true });
61
+ exports.process = process;
62
+ const cds_1 = __importDefault(require("@sap/cds"));
63
+ const decode_1 = require("../core/decode");
64
+ const validate_1 = require("../core/validate");
65
+ const requirements_1 = require("../core/requirements");
66
+ const errors_1 = require("../core/errors");
67
+ const nonce_1 = require("./nonce");
68
+ const settle_1 = require("./settle");
69
+ const bridge = __importStar(require("../bridge"));
70
+ const log = cds_1.default.log('x402');
71
+ function paymentResponseHeaderB64(network, txHash) {
72
+ return Buffer.from(JSON.stringify({
73
+ success: true, network, transaction: txHash,
74
+ }), 'utf8').toString('base64');
75
+ }
76
+ async function runOnAccepted(claim, cb) {
77
+ if (!cb)
78
+ return;
79
+ try {
80
+ await cb(claim);
81
+ }
82
+ catch (err) {
83
+ log.warn('onAccepted callback failed (non-fatal):', err?.message ?? err);
84
+ }
85
+ }
86
+ async function process(args) {
87
+ const headerStr = Array.isArray(args.paymentHeader)
88
+ ? args.paymentHeader[0]
89
+ : args.paymentHeader;
90
+ if (!headerStr) {
91
+ return {
92
+ kind: 'rejected',
93
+ code: errors_1.Codes.MISSING_HEADER,
94
+ reason: 'PAYMENT-SIGNATURE header is required',
95
+ requirementsBody: args.requirementsBody,
96
+ };
97
+ }
98
+ const requirements = (0, requirements_1.flatRequirements)(args.requirementsBody);
99
+ // ─── 1. Decode ──────────────────────────────────────────────────────
100
+ let decoded;
101
+ try {
102
+ decoded = (0, decode_1.decode)(headerStr);
103
+ }
104
+ catch (err) {
105
+ if (err instanceof errors_1.X402Error) {
106
+ return {
107
+ kind: 'rejected',
108
+ code: err.code,
109
+ reason: err.message,
110
+ requirementsBody: args.requirementsBody,
111
+ };
112
+ }
113
+ throw err;
114
+ }
115
+ // ─── 2. Validate (6 checks, pure) ───────────────────────────────────
116
+ let currentSlot;
117
+ try {
118
+ currentSlot = await bridge.getCurrentSlot();
119
+ }
120
+ catch (err) {
121
+ return {
122
+ kind: 'rejected',
123
+ code: err.code ?? errors_1.Codes.BRIDGE_UNAVAILABLE,
124
+ reason: `bridge.getCurrentSlot failed: ${err?.message ?? err}`,
125
+ requirementsBody: args.requirementsBody,
126
+ };
127
+ }
128
+ const v = (0, validate_1.validatePayment)(decoded, requirements, {
129
+ currentSlot,
130
+ allowNoTtl: args.allowNoTtl,
131
+ });
132
+ if (!v.ok) {
133
+ return {
134
+ kind: 'rejected',
135
+ code: v.code,
136
+ reason: v.reason,
137
+ requirementsBody: args.requirementsBody,
138
+ };
139
+ }
140
+ // ─── 3. Nonce — UTxO still unspent (chain) ──────────────────────────
141
+ const nonceResult = await (0, nonce_1.checkNonceUnspent)({
142
+ txHash: decoded.nonce.txHash,
143
+ outputIndex: decoded.nonce.index,
144
+ });
145
+ if (!nonceResult.ok) {
146
+ return {
147
+ kind: 'rejected',
148
+ code: nonceResult.code,
149
+ reason: nonceResult.reason,
150
+ requirementsBody: args.requirementsBody,
151
+ };
152
+ }
153
+ // ─── 4. Settle (submit + poll-until-confirmed) ──────────────────────
154
+ const settleArgs = {
155
+ signedTxCborHex: decoded.txCborHex,
156
+ expectedTxHash: decoded.txHash,
157
+ };
158
+ if (args.settlePollBudgetMs !== undefined) {
159
+ settleArgs.pollBudgetMs = args.settlePollBudgetMs;
160
+ }
161
+ const settled = await (0, settle_1.settle)(settleArgs);
162
+ if (!settled.confirmed) {
163
+ if (settled.pending) {
164
+ return {
165
+ kind: 'pending',
166
+ code: settled.code ?? errors_1.Codes.PENDING,
167
+ ...(settled.reason !== undefined ? { reason: settled.reason } : {}),
168
+ ...(settled.txHash !== undefined ? { txHash: settled.txHash } : {}),
169
+ requirementsBody: args.requirementsBody,
170
+ };
171
+ }
172
+ return {
173
+ kind: 'rejected',
174
+ code: settled.code ?? errors_1.Codes.SUBMIT_FAILED,
175
+ reason: settled.reason ?? 'submit failed',
176
+ requirementsBody: args.requirementsBody,
177
+ };
178
+ }
179
+ // ─── 5. onAccepted (consumer audit, best-effort) ────────────────────
180
+ await runOnAccepted(v.claim, args.onAccepted);
181
+ // ─── 6. Success ─────────────────────────────────────────────────────
182
+ return {
183
+ kind: 'accepted',
184
+ txHash: v.claim.txHash,
185
+ payment: v.claim,
186
+ paymentResponseB64: paymentResponseHeaderB64(v.claim.network, v.claim.txHash),
187
+ };
188
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Server-side unsigned payment-tx builder for browser-buyer flows.
3
+ *
4
+ * The browser knows the buyer's bech32 (via CIP-30) but not the
5
+ * signing keys. Replicating CSL coin-selection + protocol-params
6
+ * fetch in the browser would mean shipping ~2 MB of WASM. So we
7
+ * build the unsigned tx server-side, return the CBOR for the
8
+ * wallet to sign, and let the browser submit the signed CBOR as
9
+ * `payload.transaction` in the PAYMENT-SIGNATURE envelope.
10
+ *
11
+ * Diff vs v1 of CHAINFEED's same-named helper:
12
+ * - Asset-agnostic — parses requirements.asset as a v2 string
13
+ * (`'lovelace'` or `'<policy>.<nameHex>'`).
14
+ * - Returns `nonceRef` alongside the unsigned CBOR — the server
15
+ * picks one of the buyer's chosen inputs as the v2 nonce UTxO,
16
+ * so the browser doesn't have to reason about it.
17
+ *
18
+ * **x402-spec deviation:** strict v2 has the buyer construct the
19
+ * tx end-to-end. This helper is a "self-facilitator" pattern: the
20
+ * server builds, the buyer signs, the server still validates the
21
+ * signed tx against requirements before settling. Same security
22
+ * model (the buyer's signature still authorises the spend), easier
23
+ * browser ergonomics.
24
+ */
25
+ import type { PaymentRequirementEntry } from '../core/types';
26
+ export interface BuildUnsignedTxArgs {
27
+ /** Buyer's bech32 address (must be Base or Enterprise with VKey-hash payment cred). */
28
+ buyerBech32: string;
29
+ /** A single accepts[] entry — call `flatRequirements(body)` to extract. */
30
+ requirements: PaymentRequirementEntry;
31
+ /**
32
+ * Optional TTL in slots from "now" (= current chain tip slot).
33
+ * Default 1800 (≈30 min on Cardano's 1s-slot networks).
34
+ */
35
+ ttlSlotsFromNow?: number;
36
+ }
37
+ export interface UnsignedTxResult {
38
+ /** CBOR hex of the unsigned tx (empty witness set). Ready for CIP-30 signTx. */
39
+ unsignedTxCborHex: string;
40
+ /** Hex tx hash — what the buyer's wallet will display. */
41
+ txHashHex: string;
42
+ /** Buyer's payment-cred VKey hash — wallet must sign for this. */
43
+ requiredSignerHex: string;
44
+ /** v2 nonce reference `<txHash>#<index>`, picked from the buyer's chosen inputs. */
45
+ nonceRef: string;
46
+ /** Echo of the inputs chosen so the buyer's UI can show "spends these UTxOs". */
47
+ inputs: Array<{
48
+ txHash: string;
49
+ outputIndex: number;
50
+ lovelace: string;
51
+ }>;
52
+ /** TTL slot used for the validity-range upper bound. */
53
+ ttlSlot: number;
54
+ }
55
+ export declare function buildUnsignedPaymentTx(args: BuildUnsignedTxArgs): Promise<UnsignedTxResult>;
56
+ //# sourceMappingURL=build-unsigned-tx.d.ts.map
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ /**
3
+ * Server-side unsigned payment-tx builder for browser-buyer flows.
4
+ *
5
+ * The browser knows the buyer's bech32 (via CIP-30) but not the
6
+ * signing keys. Replicating CSL coin-selection + protocol-params
7
+ * fetch in the browser would mean shipping ~2 MB of WASM. So we
8
+ * build the unsigned tx server-side, return the CBOR for the
9
+ * wallet to sign, and let the browser submit the signed CBOR as
10
+ * `payload.transaction` in the PAYMENT-SIGNATURE envelope.
11
+ *
12
+ * Diff vs v1 of CHAINFEED's same-named helper:
13
+ * - Asset-agnostic — parses requirements.asset as a v2 string
14
+ * (`'lovelace'` or `'<policy>.<nameHex>'`).
15
+ * - Returns `nonceRef` alongside the unsigned CBOR — the server
16
+ * picks one of the buyer's chosen inputs as the v2 nonce UTxO,
17
+ * so the browser doesn't have to reason about it.
18
+ *
19
+ * **x402-spec deviation:** strict v2 has the buyer construct the
20
+ * tx end-to-end. This helper is a "self-facilitator" pattern: the
21
+ * server builds, the buyer signs, the server still validates the
22
+ * signed tx against requirements before settling. Same security
23
+ * model (the buyer's signature still authorises the spend), easier
24
+ * browser ergonomics.
25
+ */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.buildUnsignedPaymentTx = buildUnsignedPaymentTx;
61
+ const CSL = __importStar(require("@emurgo/cardano-serialization-lib-nodejs"));
62
+ const bridge = __importStar(require("../bridge"));
63
+ const asset_1 = require("../core/asset");
64
+ async function buildUnsignedPaymentTx(args) {
65
+ const { buyerBech32, requirements } = args;
66
+ // 1. Decode buyer address; derive payment-cred VKey hash.
67
+ let buyerAddress;
68
+ try {
69
+ buyerAddress = CSL.Address.from_bech32(buyerBech32);
70
+ }
71
+ catch {
72
+ throw new Error(`buildUnsignedPaymentTx: invalid bech32 address: ${buyerBech32}`);
73
+ }
74
+ const baseAddr = CSL.BaseAddress.from_address(buyerAddress);
75
+ const enterpriseAddr = CSL.EnterpriseAddress.from_address(buyerAddress);
76
+ const paymentCred = baseAddr?.payment_cred() ?? enterpriseAddr?.payment_cred();
77
+ if (!paymentCred) {
78
+ throw new Error('buildUnsignedPaymentTx: only Base / Enterprise addresses are supported');
79
+ }
80
+ const buyerVkeyHash = paymentCred.to_keyhash();
81
+ if (!buyerVkeyHash) {
82
+ throw new Error('buildUnsignedPaymentTx: payment credential must be a VKey hash, not a script');
83
+ }
84
+ const requiredSignerHex = Buffer.from(buyerVkeyHash.to_bytes()).toString('hex');
85
+ // 2. Fetch buyer UTxOs + protocol params + current slot in parallel.
86
+ const [utxos, params, currentSlot] = await Promise.all([
87
+ bridge.getUtxosAtAddress(buyerBech32),
88
+ bridge.getProtocolParameters(),
89
+ bridge.getCurrentSlot(),
90
+ ]);
91
+ if (utxos.length === 0) {
92
+ throw new Error(`buildUnsignedPaymentTx: no UTxOs at ${buyerBech32}`);
93
+ }
94
+ // 3. Pick the input(s) for coin-selection.
95
+ // Strategy:
96
+ // - If lovelace asset: pick largest-ADA UTxO; add second-largest as padding if first < 3 ADA.
97
+ // - If native asset: pick largest-ADA UTxO that ALSO holds enough of the asset;
98
+ // add padding the same way.
99
+ const parsedAsset = (0, asset_1.parseAsset)(requirements.asset);
100
+ const required = BigInt(requirements.amount);
101
+ const sortedByAda = [...utxos].sort((a, b) => (BigInt(b.lovelace) - BigInt(a.lovelace) > 0n ? 1 : -1));
102
+ let inputs;
103
+ if (parsedAsset.isLovelace) {
104
+ // ADA payment: largest UTxO must cover required + fees + min-ADA change.
105
+ // Heuristic: required + 2_000_000 (≈2 ADA fee+change headroom).
106
+ const headroom = required + 2000000n;
107
+ const ok = sortedByAda.find(u => BigInt(u.lovelace) >= headroom);
108
+ if (!ok) {
109
+ throw new Error(`buildUnsignedPaymentTx: no UTxO at ${buyerBech32} with ≥ ${headroom} lovelace`);
110
+ }
111
+ inputs = [ok];
112
+ }
113
+ else {
114
+ const candidates = sortedByAda.filter(u => u.assets.some(a => a.unit === parsedAsset.unit && BigInt(a.quantity) >= required));
115
+ if (candidates.length === 0) {
116
+ throw new Error(`buildUnsignedPaymentTx: no UTxO at ${buyerBech32} holds ≥ ${required} of ${parsedAsset.unit}`);
117
+ }
118
+ const tokenInput = candidates[0];
119
+ inputs = [tokenInput];
120
+ if (BigInt(tokenInput.lovelace) < 3000000n) {
121
+ const padding = sortedByAda.find(u => u !== tokenInput);
122
+ if (!padding) {
123
+ throw new Error('buildUnsignedPaymentTx: no second UTxO available to fund fees');
124
+ }
125
+ inputs.push(padding);
126
+ }
127
+ }
128
+ // 4. Configure CSL TransactionBuilder from live protocol params.
129
+ const builder = CSL.TransactionBuilder.new(CSL.TransactionBuilderConfigBuilder.new()
130
+ .fee_algo(CSL.LinearFee.new(CSL.BigNum.from_str(String(params.minFeeA)), CSL.BigNum.from_str(String(params.minFeeB))))
131
+ .pool_deposit(CSL.BigNum.from_str(String(params.poolDeposit)))
132
+ .key_deposit(CSL.BigNum.from_str(String(params.keyDeposit)))
133
+ .max_value_size(Number(params.maxValSize))
134
+ .max_tx_size(Number(params.maxTxSize))
135
+ .coins_per_utxo_byte(CSL.BigNum.from_str(String(params.coinsPerUtxoSize)))
136
+ .build());
137
+ // 5. Wire inputs (preserve full multi-asset payload).
138
+ for (const u of inputs) {
139
+ const inMa = CSL.MultiAsset.new();
140
+ const byPolicy = new Map();
141
+ for (const a of u.assets) {
142
+ const arr = byPolicy.get(a.policyId) ?? [];
143
+ arr.push({ name: a.assetNameHex, qty: a.quantity });
144
+ byPolicy.set(a.policyId, arr);
145
+ }
146
+ for (const [policyHex, items] of byPolicy) {
147
+ const policyHash = CSL.ScriptHash.from_bytes(Buffer.from(policyHex, 'hex'));
148
+ const assetMap = CSL.Assets.new();
149
+ for (const { name, qty } of items) {
150
+ assetMap.insert(CSL.AssetName.new(Buffer.from(name, 'hex')), CSL.BigNum.from_str(qty));
151
+ }
152
+ inMa.insert(policyHash, assetMap);
153
+ }
154
+ const inV = CSL.Value.new(CSL.BigNum.from_str(u.lovelace));
155
+ if (u.assets.length)
156
+ inV.set_multiasset(inMa);
157
+ builder.add_key_input(buyerVkeyHash, CSL.TransactionInput.new(CSL.TransactionHash.from_bytes(Buffer.from(u.txHash, 'hex')), u.outputIndex), inV);
158
+ }
159
+ // 6. Output to payTo.
160
+ const payToAddr = CSL.Address.from_bech32(requirements.payTo);
161
+ let payOut;
162
+ if (parsedAsset.isLovelace) {
163
+ const v = CSL.Value.new(CSL.BigNum.from_str(required.toString()));
164
+ payOut = CSL.TransactionOutput.new(payToAddr, v);
165
+ }
166
+ else {
167
+ const payOutMa = CSL.MultiAsset.new();
168
+ const payAssets = CSL.Assets.new();
169
+ const policyHash = CSL.ScriptHash.from_bytes(Buffer.from(parsedAsset.policyId, 'hex'));
170
+ payAssets.insert(CSL.AssetName.new(Buffer.from(parsedAsset.assetNameHex, 'hex')), CSL.BigNum.from_str(required.toString()));
171
+ payOutMa.insert(policyHash, payAssets);
172
+ const payOutV = CSL.Value.new(CSL.BigNum.from_str('0'));
173
+ payOutV.set_multiasset(payOutMa);
174
+ const provisional = CSL.TransactionOutput.new(payToAddr, payOutV);
175
+ const minAda = CSL.min_ada_for_output(provisional, CSL.DataCost.new_coins_per_byte(CSL.BigNum.from_str(String(params.coinsPerUtxoSize))));
176
+ payOutV.set_coin(minAda);
177
+ payOut = CSL.TransactionOutput.new(payToAddr, payOutV);
178
+ }
179
+ builder.add_output(payOut);
180
+ // 7. TTL (slot of upper bound). Default 1800 slots ≈ 30 min.
181
+ const ttlSlot = currentSlot + (args.ttlSlotsFromNow ?? 1800);
182
+ builder.set_ttl_bignum(CSL.BigNum.from_str(String(ttlSlot)));
183
+ // 8. Change to buyer.
184
+ builder.add_change_if_needed(buyerAddress);
185
+ // 9. Build body, compute hash, return unsigned tx.
186
+ const txBody = builder.build();
187
+ const txHash = CSL.FixedTransaction.new_from_body_bytes(txBody.to_bytes()).transaction_hash();
188
+ const emptyWits = CSL.TransactionWitnessSet.new();
189
+ const unsigned = CSL.Transaction.new(txBody, emptyWits);
190
+ // Pick the first input as the v2 nonce UTxO.
191
+ // It MUST appear in tx.inputs (which it does by construction) and be
192
+ // unspent (which it is — we just queried it from the buyer's UTxO set).
193
+ const nonceInput = inputs[0];
194
+ const nonceRef = `${nonceInput.txHash}#${nonceInput.outputIndex}`;
195
+ return {
196
+ unsignedTxCborHex: Buffer.from(unsigned.to_bytes()).toString('hex'),
197
+ txHashHex: Buffer.from(txHash.to_bytes()).toString('hex').toLowerCase(),
198
+ requiredSignerHex,
199
+ nonceRef,
200
+ inputs: inputs.map(i => ({ txHash: i.txHash, outputIndex: i.outputIndex, lovelace: i.lovelace })),
201
+ ttlSlot,
202
+ };
203
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Post-confirmed payment verifier.
3
+ *
4
+ * Use case: buyer pays out-of-band (CLI, hardware wallet, etc.), then
5
+ * presents a **txHash** to your server. This module fetches the tx
6
+ * from chain via the bridge and confirms it pays the right amount of
7
+ * the right asset to the right address on the right network.
8
+ *
9
+ * Differences from the middleware path (`facilitator/verify.ts`):
10
+ * - No envelope, no PAYMENT-SIGNATURE header — just a tx hash.
11
+ * - The tx is presumed already on-chain (the network already accepted
12
+ * witnesses), so we skip the witness-presence check.
13
+ * - No on-chain nonce check: replay defense for confirmed-payment
14
+ * flows is the consumer's job (e.g. "this txHash already redeemed
15
+ * a subscription" → consumer's DB). v2 still applies for the
16
+ * middleware path; this helper is for grants that outlive a
17
+ * single HTTP request.
18
+ *
19
+ * Asset-agnostic: pass either `'lovelace'` or `'<policy>.<nameHex>'`.
20
+ */
21
+ import { type X402Code } from '../core/errors';
22
+ import { type Network } from '../core/network';
23
+ export interface VerifyConfirmedArgs {
24
+ txHash: string;
25
+ /** Required asset amount in raw units (BigInt-safe). */
26
+ requiredAmount: string | number | bigint;
27
+ /** v2 asset string. */
28
+ asset: string;
29
+ /** Bech32 recipient. */
30
+ payTo: string;
31
+ network: Network | string;
32
+ }
33
+ export type VerifyConfirmedResult = {
34
+ ok: true;
35
+ txHash: string;
36
+ amountUnits: string;
37
+ } | {
38
+ ok: false;
39
+ code: X402Code;
40
+ reason: string;
41
+ };
42
+ export declare function verifyConfirmedPayment(args: VerifyConfirmedArgs): Promise<VerifyConfirmedResult>;
43
+ //# sourceMappingURL=verify-confirmed.d.ts.map
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ /**
3
+ * Post-confirmed payment verifier.
4
+ *
5
+ * Use case: buyer pays out-of-band (CLI, hardware wallet, etc.), then
6
+ * presents a **txHash** to your server. This module fetches the tx
7
+ * from chain via the bridge and confirms it pays the right amount of
8
+ * the right asset to the right address on the right network.
9
+ *
10
+ * Differences from the middleware path (`facilitator/verify.ts`):
11
+ * - No envelope, no PAYMENT-SIGNATURE header — just a tx hash.
12
+ * - The tx is presumed already on-chain (the network already accepted
13
+ * witnesses), so we skip the witness-presence check.
14
+ * - No on-chain nonce check: replay defense for confirmed-payment
15
+ * flows is the consumer's job (e.g. "this txHash already redeemed
16
+ * a subscription" → consumer's DB). v2 still applies for the
17
+ * middleware path; this helper is for grants that outlive a
18
+ * single HTTP request.
19
+ *
20
+ * Asset-agnostic: pass either `'lovelace'` or `'<policy>.<nameHex>'`.
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.verifyConfirmedPayment = verifyConfirmedPayment;
57
+ const bridge = __importStar(require("../bridge"));
58
+ const errors_1 = require("../core/errors");
59
+ const asset_1 = require("../core/asset");
60
+ const network_1 = require("../core/network");
61
+ function totalPaidToAddress(tx, payTo, isLovelace, unit) {
62
+ let total = 0n;
63
+ for (const o of tx.outputs ?? []) {
64
+ if (o.address !== payTo)
65
+ continue;
66
+ if (isLovelace) {
67
+ total += BigInt(o.lovelace ?? '0');
68
+ }
69
+ else {
70
+ for (const a of o.assets ?? []) {
71
+ if (String(a.unit ?? '').toLowerCase() === unit) {
72
+ total += BigInt(a.quantity ?? '0');
73
+ }
74
+ }
75
+ }
76
+ }
77
+ return total;
78
+ }
79
+ async function verifyConfirmedPayment(args) {
80
+ if (typeof args.txHash !== 'string' || !/^[0-9a-f]{64}$/i.test(args.txHash)) {
81
+ return { ok: false, code: errors_1.Codes.INVALID_CBOR, reason: 'txHash must be 64-char lowercase hex' };
82
+ }
83
+ // Parse / validate the user-supplied descriptors up front so we
84
+ // return precise diagnostics instead of failing later in the flow.
85
+ let network;
86
+ try {
87
+ network = (0, network_1.parseNetwork)(args.network);
88
+ }
89
+ catch (e) {
90
+ return { ok: false, code: errors_1.Codes.INVALID_NETWORK_FORMAT, reason: e.message };
91
+ }
92
+ let parsedAsset;
93
+ try {
94
+ parsedAsset = (0, asset_1.parseAsset)(args.asset);
95
+ }
96
+ catch (e) {
97
+ return { ok: false, code: errors_1.Codes.INVALID_ASSET_FORMAT, reason: e.message };
98
+ }
99
+ // 1. Fetch from chain.
100
+ let tx;
101
+ try {
102
+ tx = await bridge.getTransactionByHash(args.txHash);
103
+ }
104
+ catch (err) {
105
+ return {
106
+ ok: false,
107
+ code: errors_1.Codes.PENDING,
108
+ reason: `bridge.getTransactionByHash failed: ${err?.message ?? err}`,
109
+ };
110
+ }
111
+ if (!tx) {
112
+ return {
113
+ ok: false,
114
+ code: errors_1.Codes.PENDING,
115
+ reason: `tx ${args.txHash} not found on-chain (network=${network})`,
116
+ };
117
+ }
118
+ // 2. Quantity check, summed across all outputs to payTo.
119
+ const paid = totalPaidToAddress(tx, args.payTo, parsedAsset.isLovelace, parsedAsset.unit);
120
+ const required = BigInt(args.requiredAmount);
121
+ if (paid < required) {
122
+ return {
123
+ ok: false,
124
+ code: paid === 0n ? errors_1.Codes.WRONG_ASSET : errors_1.Codes.INSUFFICIENT_AMOUNT,
125
+ reason: `paid ${paid} < required ${required} of ${args.asset} to ${args.payTo}`,
126
+ };
127
+ }
128
+ return { ok: true, txHash: args.txHash, amountUnits: paid.toString() };
129
+ }
package/srv/index.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @odatano/x402 — public API barrel.
3
+ *
4
+ * Cardano-x402-v2 payment library for SAP CAP applications.
5
+ *
6
+ * Three usage shapes:
7
+ *
8
+ * // 1. Express middleware (mount under a path)
9
+ * import { x402Middleware } from '@odatano/x402';
10
+ * app.use('/api/premium', x402Middleware({
11
+ * payTo: 'addr_test1...',
12
+ * network: 'cardano:preprod',
13
+ * asset: '16a55b...ddde.0014df105553444d',
14
+ * priceUnits: '1000000',
15
+ * onAccepted: async (claim) => { ... },
16
+ * }));
17
+ *
18
+ * // 2. CAP service gate (registers a before-* handler)
19
+ * import { gateService } from '@odatano/x402';
20
+ * class MyService extends cds.ApplicationService {
21
+ * async init() {
22
+ * gateService(this, {
23
+ * payTo, network, asset,
24
+ * routePricing: { Prices: '10000', getBestPrice: '10000' },
25
+ * });
26
+ * return super.init();
27
+ * }
28
+ * }
29
+ *
30
+ * // 3. Programmatic — verify a confirmed payment by tx hash
31
+ * import { verifyConfirmedPayment } from '@odatano/x402';
32
+ * const r = await verifyConfirmedPayment({
33
+ * txHash, requiredAmount, asset, payTo, network,
34
+ * });
35
+ */
36
+ export { buildPaymentRequirements, buildEntry, flatRequirements, type BuildPaymentRequirementsArgs, } from './core/requirements';
37
+ export { decode } from './core/decode';
38
+ export { validatePayment, type ValidationResult, type ValidateOptions } from './core/validate';
39
+ export { parseAsset, buildAssetString, type ParsedAsset } from './core/asset';
40
+ export { parseNetwork, isNetwork, networksMatch, type Network } from './core/network';
41
+ export { X402Error, Codes, type X402Code } from './core/errors';
42
+ export type { AssetTransferMethod, ResourceDescriptor, PaymentRequirementEntry, PaymentRequirementsBody, PaymentEnvelope, PaymentClaim, DecodedPayment, DecodedOutput, DecodedAsset, DecodedInput, } from './core/types';
43
+ export { process as verifyPayment, type ProcessArgs, type ProcessResult, type ProcessKind, } from './facilitator/verify';
44
+ export { settle, type SettleArgs, type SettleResult } from './facilitator/settle';
45
+ export { checkNonceUnspent, type NonceCheckArgs, type NonceResult } from './facilitator/nonce';
46
+ export { verifyConfirmedPayment, type VerifyConfirmedArgs, type VerifyConfirmedResult, } from './helpers/verify-confirmed';
47
+ export { buildUnsignedPaymentTx, type BuildUnsignedTxArgs, type UnsignedTxResult, } from './helpers/build-unsigned-tx';
48
+ export { x402Middleware, type X402MiddlewareOptions } from './middleware/express';
49
+ export { gateService, type X402CapOptions } from './middleware/cap';
50
+ export * as bridge from './bridge';
51
+ //# sourceMappingURL=index.d.ts.map