@odatano/x402 0.3.0 → 0.4.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/.env +0 -0
- package/.github/workflows/test.yaml +10 -1
- package/CHANGELOG.md +27 -2
- package/LICENSE +169 -21
- package/README.md +11 -8
- package/datano-x402.png +0 -0
- package/package.json +5 -4
- package/release-0.4.0.md +58 -0
- package/srv/bridge.d.ts +85 -1
- package/srv/bridge.js +34 -8
- package/srv/client/axios.js +1 -3
- package/srv/client/errors.d.ts +0 -21
- package/srv/client/errors.js +0 -37
- package/srv/client/fetch.js +2 -7
- package/srv/core/decode.js +48 -137
- package/srv/helpers/address.d.ts +31 -0
- package/srv/helpers/address.js +57 -0
- package/srv/helpers/build-unsigned-tx.d.ts +24 -23
- package/srv/helpers/build-unsigned-tx.js +62 -147
- package/srv/middleware/cap.js +27 -2
package/srv/client/fetch.js
CHANGED
|
@@ -57,8 +57,7 @@ function x402Fetch(opts) {
|
|
|
57
57
|
let body = lastBody;
|
|
58
58
|
if (!body) {
|
|
59
59
|
try {
|
|
60
|
-
|
|
61
|
-
body = (0, errors_1.unwrapCapEnvelope)(raw);
|
|
60
|
+
body = await res.clone().json();
|
|
62
61
|
}
|
|
63
62
|
catch { /* fall through */ }
|
|
64
63
|
}
|
|
@@ -75,13 +74,9 @@ function x402Fetch(opts) {
|
|
|
75
74
|
}
|
|
76
75
|
// Parse 402 body. If it's not a v2 PaymentRequirementsBody we
|
|
77
76
|
// bail with the original response (or throw under errorOnFailure).
|
|
78
|
-
// Some servers (CAP-gated ones using req.reject) wrap the v2 body
|
|
79
|
-
// inside an OData error envelope, unwrap defensively before the
|
|
80
|
-
// x402Version check.
|
|
81
77
|
let body;
|
|
82
78
|
try {
|
|
83
|
-
|
|
84
|
-
body = (0, errors_1.unwrapCapEnvelope)(raw);
|
|
79
|
+
body = await res.clone().json();
|
|
85
80
|
}
|
|
86
81
|
catch {
|
|
87
82
|
if (opts.errorOnFailure) {
|
package/srv/core/decode.js
CHANGED
|
@@ -17,42 +17,9 @@
|
|
|
17
17
|
* `DecodedPayment` that downstream `validate.ts` checks against
|
|
18
18
|
* `PaymentRequirementEntry` (the 6 mandatory checks).
|
|
19
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
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
21
|
exports.decode = decode;
|
|
55
|
-
const
|
|
22
|
+
const bridge_1 = require("../bridge");
|
|
56
23
|
const errors_1 = require("./errors");
|
|
57
24
|
const SUPPORTED_VERSION = 2;
|
|
58
25
|
const SUPPORTED_SCHEME = 'exact';
|
|
@@ -68,90 +35,45 @@ function decodeBase64ToBuffer(s, errCode) {
|
|
|
68
35
|
}
|
|
69
36
|
return buf;
|
|
70
37
|
}
|
|
71
|
-
function extractOutputs(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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(),
|
|
38
|
+
function extractOutputs(outputs) {
|
|
39
|
+
return outputs.map((o, i) => {
|
|
40
|
+
const assets = o.assets.map(a => {
|
|
41
|
+
// core gives `unit` = policyId(56 hex) + assetNameHex; split it back
|
|
42
|
+
// into the (policyId, assetNameHex) pair x402's validate.ts expects.
|
|
43
|
+
const unit = a.unit.toLowerCase();
|
|
44
|
+
return {
|
|
45
|
+
unit,
|
|
46
|
+
policyId: unit.slice(0, 56),
|
|
47
|
+
assetNameHex: unit.slice(56),
|
|
48
|
+
quantity: a.quantity,
|
|
49
|
+
};
|
|
117
50
|
});
|
|
118
|
-
|
|
119
|
-
|
|
51
|
+
return { outputIndex: i, address: o.address, lovelace: o.lovelace, assets };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function extractInputs(inputs) {
|
|
55
|
+
return inputs.map(inp => ({
|
|
56
|
+
txHash: inp.txHash.toLowerCase(),
|
|
57
|
+
outputIndex: inp.outputIndex,
|
|
58
|
+
}));
|
|
120
59
|
}
|
|
121
60
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
61
|
+
* Convert core's decimal-string slot bounds to numbers. Both bounds can
|
|
62
|
+
* be null, in which case the downstream TTL check is skipped (per v2
|
|
63
|
+
* spec: only validate TTL if the buyer set one). Slots fit comfortably
|
|
64
|
+
* in a JS number (current preprod ~85M, max safe int 9e15).
|
|
126
65
|
*/
|
|
127
|
-
function extractValidityRange(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 };
|
|
66
|
+
function extractValidityRange(parsed) {
|
|
67
|
+
const toSlot = (s) => {
|
|
68
|
+
if (s == null)
|
|
69
|
+
return null;
|
|
70
|
+
const n = Number(s);
|
|
71
|
+
return Number.isFinite(n) ? n : null;
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
ttlSlot: toSlot(parsed.validityEnd),
|
|
75
|
+
validityStartSlot: toSlot(parsed.validityStart),
|
|
76
|
+
};
|
|
155
77
|
}
|
|
156
78
|
function parseNonceRef(nonce) {
|
|
157
79
|
const m = NONCE_RE.exec(nonce);
|
|
@@ -200,28 +122,17 @@ function decode(paymentHeader) {
|
|
|
200
122
|
if (typeof payload.nonce !== 'string' || payload.nonce.length === 0) {
|
|
201
123
|
throw new errors_1.X402Error(errors_1.Codes.MISSING_FIELD, 'payload.nonce is required (v2 UTxO-ref)');
|
|
202
124
|
}
|
|
203
|
-
// 3. Tx CBOR →
|
|
204
|
-
//
|
|
205
|
-
//
|
|
125
|
+
// 3. Tx CBOR → structured fields via @odatano/core's pure Buildooor
|
|
126
|
+
// parser. `parseTransaction` throws X402Error(INVALID_CBOR) on
|
|
127
|
+
// malformed input, and its `txHash` (= body.hash) is byte-preserving,
|
|
128
|
+
// the property the old CSL `FixedTransaction` path provided.
|
|
206
129
|
const txBuf = decodeBase64ToBuffer(payload.transaction, errors_1.Codes.INVALID_CBOR);
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
}
|
|
130
|
+
const txCborHex = txBuf.toString('hex');
|
|
131
|
+
const parsed = (0, bridge_1.parseTransaction)(txCborHex);
|
|
214
132
|
// 4. Diagnostics
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
const
|
|
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);
|
|
133
|
+
const txHash = parsed.txHash.toLowerCase();
|
|
134
|
+
const vkeyWitnessCount = parsed.witnesses.vkeyCount;
|
|
135
|
+
const validity = extractValidityRange(parsed);
|
|
225
136
|
const nonce = parseNonceRef(payload.nonce);
|
|
226
137
|
const envelope = {
|
|
227
138
|
x402Version: SUPPORTED_VERSION,
|
|
@@ -231,10 +142,10 @@ function decode(paymentHeader) {
|
|
|
231
142
|
};
|
|
232
143
|
return {
|
|
233
144
|
envelope,
|
|
234
|
-
txCborHex
|
|
145
|
+
txCborHex,
|
|
235
146
|
txHash,
|
|
236
|
-
outputs: extractOutputs(
|
|
237
|
-
inputs: extractInputs(
|
|
147
|
+
outputs: extractOutputs(parsed.outputs),
|
|
148
|
+
inputs: extractInputs(parsed.inputs),
|
|
238
149
|
vkeyWitnessCount,
|
|
239
150
|
ttlSlot: validity.ttlSlot,
|
|
240
151
|
validityStartSlot: validity.validityStartSlot,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, CSL-free Shelley address introspection.
|
|
3
|
+
*
|
|
4
|
+
* The unsigned-tx builder needs two things from the buyer's bech32
|
|
5
|
+
* address that the (Buildooor-based) @odatano/core builder doesn't hand
|
|
6
|
+
* back: the payment-credential VKey hash (returned to the wallet as the
|
|
7
|
+
* `requiredSignerHex`), and a guard that the address is a Base/Enterprise
|
|
8
|
+
* key-cred address (script-cred and reward/stake addresses can't sign a
|
|
9
|
+
* normal spend). Rather than pull in a CBOR/address library, we decode
|
|
10
|
+
* the bech32 payload and read the 1-byte Shelley header directly.
|
|
11
|
+
*
|
|
12
|
+
* Shelley address header (CIP-19): the top nibble is the address type,
|
|
13
|
+
* the bottom nibble the network id. Payment credential is the 28 bytes
|
|
14
|
+
* following the header. Payment-cred kind by type:
|
|
15
|
+
* 0,2 base / 6 enterprise → key hash (signable)
|
|
16
|
+
* 1,3 base / 7 enterprise → script hash (not signable)
|
|
17
|
+
* 4,5 pointer → unsupported here
|
|
18
|
+
* 14,15 reward/stake → not a payment address
|
|
19
|
+
*/
|
|
20
|
+
export interface ParsedPaymentAddress {
|
|
21
|
+
/** Buyer's payment-credential VKey hash (lowercase hex, 56 chars). */
|
|
22
|
+
paymentKeyHashHex: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Decode a bech32 Cardano address and extract its payment-credential
|
|
26
|
+
* VKey hash. Throws (with the same messages the old CSL path used) for
|
|
27
|
+
* malformed bech32, script-cred payment, or non-payment (reward/pointer)
|
|
28
|
+
* addresses.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parsePaymentAddress(bech32Addr: string): ParsedPaymentAddress;
|
|
31
|
+
//# sourceMappingURL=address.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal, CSL-free Shelley address introspection.
|
|
4
|
+
*
|
|
5
|
+
* The unsigned-tx builder needs two things from the buyer's bech32
|
|
6
|
+
* address that the (Buildooor-based) @odatano/core builder doesn't hand
|
|
7
|
+
* back: the payment-credential VKey hash (returned to the wallet as the
|
|
8
|
+
* `requiredSignerHex`), and a guard that the address is a Base/Enterprise
|
|
9
|
+
* key-cred address (script-cred and reward/stake addresses can't sign a
|
|
10
|
+
* normal spend). Rather than pull in a CBOR/address library, we decode
|
|
11
|
+
* the bech32 payload and read the 1-byte Shelley header directly.
|
|
12
|
+
*
|
|
13
|
+
* Shelley address header (CIP-19): the top nibble is the address type,
|
|
14
|
+
* the bottom nibble the network id. Payment credential is the 28 bytes
|
|
15
|
+
* following the header. Payment-cred kind by type:
|
|
16
|
+
* 0,2 base / 6 enterprise → key hash (signable)
|
|
17
|
+
* 1,3 base / 7 enterprise → script hash (not signable)
|
|
18
|
+
* 4,5 pointer → unsupported here
|
|
19
|
+
* 14,15 reward/stake → not a payment address
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.parsePaymentAddress = parsePaymentAddress;
|
|
23
|
+
const bech32_1 = require("bech32");
|
|
24
|
+
const PAYMENT_KEY_HASH_TYPES = new Set([0, 2, 6]); // base-key, base-key/script-stake, enterprise-key
|
|
25
|
+
const SCRIPT_PAYMENT_TYPES = new Set([1, 3, 7]); // base-script*, enterprise-script
|
|
26
|
+
/** Bech32 limit well above Cardano's longest (~103-char mainnet base addr). */
|
|
27
|
+
const BECH32_LIMIT = 1023;
|
|
28
|
+
/**
|
|
29
|
+
* Decode a bech32 Cardano address and extract its payment-credential
|
|
30
|
+
* VKey hash. Throws (with the same messages the old CSL path used) for
|
|
31
|
+
* malformed bech32, script-cred payment, or non-payment (reward/pointer)
|
|
32
|
+
* addresses.
|
|
33
|
+
*/
|
|
34
|
+
function parsePaymentAddress(bech32Addr) {
|
|
35
|
+
let bytes;
|
|
36
|
+
try {
|
|
37
|
+
const decoded = bech32_1.bech32.decode(bech32Addr, BECH32_LIMIT);
|
|
38
|
+
bytes = Uint8Array.from(bech32_1.bech32.fromWords(decoded.words));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error(`buildUnsignedPaymentTx: invalid bech32 address: ${bech32Addr}`);
|
|
42
|
+
}
|
|
43
|
+
// header (1 byte) + 28-byte payment credential.
|
|
44
|
+
if (bytes.length < 29) {
|
|
45
|
+
throw new Error(`buildUnsignedPaymentTx: invalid bech32 address: ${bech32Addr}`);
|
|
46
|
+
}
|
|
47
|
+
const addrType = bytes[0] >> 4;
|
|
48
|
+
if (SCRIPT_PAYMENT_TYPES.has(addrType)) {
|
|
49
|
+
throw new Error('buildUnsignedPaymentTx: payment credential must be a VKey hash, not a script');
|
|
50
|
+
}
|
|
51
|
+
if (!PAYMENT_KEY_HASH_TYPES.has(addrType)) {
|
|
52
|
+
// pointer (4,5), reward/stake (14,15), Byron-via-bech32, etc.
|
|
53
|
+
throw new Error('buildUnsignedPaymentTx: only Base / Enterprise addresses are supported');
|
|
54
|
+
}
|
|
55
|
+
const keyHash = bytes.slice(1, 29);
|
|
56
|
+
return { paymentKeyHashHex: Buffer.from(keyHash).toString('hex') };
|
|
57
|
+
}
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Server-side unsigned payment-tx builder for browser-buyer flows.
|
|
3
3
|
*
|
|
4
|
-
* The browser knows the buyer's bech32 (via CIP-30) but not the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* The browser knows the buyer's bech32 (via CIP-30) but not the signing
|
|
5
|
+
* keys, and shipping coin-selection + protocol-params logic to the
|
|
6
|
+
* browser would mean megabytes of WASM. So we build the unsigned tx
|
|
7
|
+
* server-side, return the CBOR for the wallet to sign, and let the
|
|
8
|
+
* browser submit the signed CBOR as `payload.transaction` in the
|
|
9
|
+
* PAYMENT-SIGNATURE envelope.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* The build itself (UTxO fetch, coin selection, change, min-ADA, fee) is
|
|
12
|
+
* delegated wholesale to `@odatano/core`'s Buildooor builder via
|
|
13
|
+
* `bridge.buildUnsignedTransfer`, x402 owns no tx-construction library.
|
|
14
|
+
* We add only the two x402-specific pieces core doesn't:
|
|
15
|
+
* - the `requiredSignerHex` (buyer's payment-cred VKey hash), parsed
|
|
16
|
+
* from the bech32 address (see `./address`);
|
|
17
|
+
* - the v2 `nonceRef`, read back from the built tx's first input so it
|
|
18
|
+
* is guaranteed to reference a UTxO the tx actually spends.
|
|
17
19
|
*
|
|
18
|
-
* **x402-spec deviation:** strict v2 has the buyer construct the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* browser ergonomics.
|
|
20
|
+
* **x402-spec deviation:** strict v2 has the buyer construct the tx
|
|
21
|
+
* end-to-end. This is the "self-facilitator" pattern: the server builds,
|
|
22
|
+
* the buyer signs, the server still validates the signed tx against
|
|
23
|
+
* requirements before settling. Same security model (the buyer's
|
|
24
|
+
* signature still authorises the spend), easier browser ergonomics.
|
|
24
25
|
*/
|
|
25
26
|
import type { PaymentRequirementEntry } from '../core/types';
|
|
26
27
|
export interface BuildUnsignedTxArgs {
|
|
@@ -29,7 +30,7 @@ export interface BuildUnsignedTxArgs {
|
|
|
29
30
|
/** A single accepts[] entry, call `flatRequirements(body)` to extract. */
|
|
30
31
|
requirements: PaymentRequirementEntry;
|
|
31
32
|
/**
|
|
32
|
-
* Optional TTL in slots from "now" (= current chain tip
|
|
33
|
+
* Optional TTL in slots from "now" (= current chain tip).
|
|
33
34
|
* Default 1800 (≈30 min on Cardano's 1s-slot networks).
|
|
34
35
|
*/
|
|
35
36
|
ttlSlotsFromNow?: number;
|
|
@@ -41,16 +42,16 @@ export interface UnsignedTxResult {
|
|
|
41
42
|
txHashHex: string;
|
|
42
43
|
/** Buyer's payment-cred VKey hash, wallet must sign for this. */
|
|
43
44
|
requiredSignerHex: string;
|
|
44
|
-
/** v2 nonce reference `<txHash>#<index>`,
|
|
45
|
+
/** v2 nonce reference `<txHash>#<index>`, the tx's first spent input. */
|
|
45
46
|
nonceRef: string;
|
|
46
|
-
/** Echo of the inputs
|
|
47
|
+
/** Echo of the inputs the builder selected so the buyer's UI can show "spends these UTxOs". */
|
|
47
48
|
inputs: Array<{
|
|
48
49
|
txHash: string;
|
|
49
50
|
outputIndex: number;
|
|
50
51
|
lovelace: string;
|
|
51
52
|
}>;
|
|
52
|
-
/** TTL slot used for the validity-range upper bound. */
|
|
53
|
-
ttlSlot: number;
|
|
53
|
+
/** TTL slot used for the validity-range upper bound (as set by the builder). */
|
|
54
|
+
ttlSlot: number | null;
|
|
54
55
|
}
|
|
55
56
|
export declare function buildUnsignedPaymentTx(args: BuildUnsignedTxArgs): Promise<UnsignedTxResult>;
|
|
56
57
|
//# sourceMappingURL=build-unsigned-tx.d.ts.map
|
|
@@ -2,26 +2,27 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Server-side unsigned payment-tx builder for browser-buyer flows.
|
|
4
4
|
*
|
|
5
|
-
* The browser knows the buyer's bech32 (via CIP-30) but not the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* The browser knows the buyer's bech32 (via CIP-30) but not the signing
|
|
6
|
+
* keys, and shipping coin-selection + protocol-params logic to the
|
|
7
|
+
* browser would mean megabytes of WASM. So we build the unsigned tx
|
|
8
|
+
* server-side, return the CBOR for the wallet to sign, and let the
|
|
9
|
+
* browser submit the signed CBOR as `payload.transaction` in the
|
|
10
|
+
* PAYMENT-SIGNATURE envelope.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* The build itself (UTxO fetch, coin selection, change, min-ADA, fee) is
|
|
13
|
+
* delegated wholesale to `@odatano/core`'s Buildooor builder via
|
|
14
|
+
* `bridge.buildUnsignedTransfer`, x402 owns no tx-construction library.
|
|
15
|
+
* We add only the two x402-specific pieces core doesn't:
|
|
16
|
+
* - the `requiredSignerHex` (buyer's payment-cred VKey hash), parsed
|
|
17
|
+
* from the bech32 address (see `./address`);
|
|
18
|
+
* - the v2 `nonceRef`, read back from the built tx's first input so it
|
|
19
|
+
* is guaranteed to reference a UTxO the tx actually spends.
|
|
18
20
|
*
|
|
19
|
-
* **x402-spec deviation:** strict v2 has the buyer construct the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* browser ergonomics.
|
|
21
|
+
* **x402-spec deviation:** strict v2 has the buyer construct the tx
|
|
22
|
+
* end-to-end. This is the "self-facilitator" pattern: the server builds,
|
|
23
|
+
* the buyer signs, the server still validates the signed tx against
|
|
24
|
+
* requirements before settling. Same security model (the buyer's
|
|
25
|
+
* signature still authorises the spend), easier browser ergonomics.
|
|
25
26
|
*/
|
|
26
27
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
28
|
if (k2 === undefined) k2 = k;
|
|
@@ -58,146 +59,60 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
58
59
|
})();
|
|
59
60
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
61
|
exports.buildUnsignedPaymentTx = buildUnsignedPaymentTx;
|
|
61
|
-
const CSL = __importStar(require("@emurgo/cardano-serialization-lib-nodejs"));
|
|
62
62
|
const bridge = __importStar(require("../bridge"));
|
|
63
63
|
const asset_1 = require("../core/asset");
|
|
64
|
+
const address_1 = require("./address");
|
|
65
|
+
/** ADA (lovelace) attached to a native-asset output to satisfy min-ADA. */
|
|
66
|
+
const TOKEN_OUTPUT_LOVELACE = 2000000n;
|
|
67
|
+
/** Cardano networks run 1-second slots, so TTL slots ≈ TTL seconds. */
|
|
68
|
+
const SLOT_MS = 1000;
|
|
64
69
|
async function buildUnsignedPaymentTx(args) {
|
|
65
70
|
const { buyerBech32, requirements } = args;
|
|
66
|
-
// 1.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
71
|
+
// 1. Validate the buyer address shape and derive the required signer.
|
|
72
|
+
// Throws for bad bech32 / script-cred / non-payment addresses.
|
|
73
|
+
const { paymentKeyHashHex } = (0, address_1.parsePaymentAddress)(buyerBech32);
|
|
74
|
+
// 2. Translate the v2 requirement into a core transfer request.
|
|
99
75
|
const parsedAsset = (0, asset_1.parseAsset)(requirements.asset);
|
|
100
76
|
const required = BigInt(requirements.amount);
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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}`);
|
|
77
|
+
const validityEndMs = Date.now() + (args.ttlSlotsFromNow ?? 1800) * SLOT_MS;
|
|
78
|
+
const req = parsedAsset.isLovelace
|
|
79
|
+
? {
|
|
80
|
+
senderAddress: buyerBech32,
|
|
81
|
+
recipientAddress: requirements.payTo,
|
|
82
|
+
changeAddress: buyerBech32,
|
|
83
|
+
lovelaceAmount: required.toString(),
|
|
84
|
+
validityEndMs,
|
|
117
85
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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);
|
|
86
|
+
: {
|
|
87
|
+
senderAddress: buyerBech32,
|
|
88
|
+
recipientAddress: requirements.payTo,
|
|
89
|
+
changeAddress: buyerBech32,
|
|
90
|
+
// Native-asset output rides a fixed min-ADA; change reconciles the rest.
|
|
91
|
+
lovelaceAmount: TOKEN_OUTPUT_LOVELACE.toString(),
|
|
92
|
+
assets: [{ unit: parsedAsset.unit, quantity: required.toString() }],
|
|
93
|
+
validityEndMs,
|
|
94
|
+
};
|
|
95
|
+
// 3. Delegate the build (UTxO fetch + coin selection + change + fee).
|
|
96
|
+
const result = await bridge.buildUnsignedTransfer(req);
|
|
97
|
+
// 4. Read the built tx back to recover the v2 nonce (first spent input,
|
|
98
|
+
// guaranteed present) and the TTL slot the builder actually set.
|
|
99
|
+
const parsed = bridge.parseTransaction(result.unsignedTxCbor);
|
|
100
|
+
const nonceInput = parsed.inputs[0];
|
|
101
|
+
if (!nonceInput) {
|
|
102
|
+
throw new Error('buildUnsignedPaymentTx: builder produced a tx with no inputs');
|
|
178
103
|
}
|
|
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
104
|
const nonceRef = `${nonceInput.txHash}#${nonceInput.outputIndex}`;
|
|
105
|
+
const ttlSlot = parsed.validityEnd != null ? Number(parsed.validityEnd) : null;
|
|
195
106
|
return {
|
|
196
|
-
unsignedTxCborHex:
|
|
197
|
-
txHashHex:
|
|
198
|
-
requiredSignerHex,
|
|
107
|
+
unsignedTxCborHex: result.unsignedTxCbor,
|
|
108
|
+
txHashHex: result.txBodyHash.toLowerCase(),
|
|
109
|
+
requiredSignerHex: paymentKeyHashHex,
|
|
199
110
|
nonceRef,
|
|
200
|
-
inputs: inputs.map(i => ({
|
|
111
|
+
inputs: result.inputs.map(i => ({
|
|
112
|
+
txHash: i.txHash,
|
|
113
|
+
outputIndex: i.index,
|
|
114
|
+
lovelace: i.lovelace,
|
|
115
|
+
})),
|
|
201
116
|
ttlSlot,
|
|
202
117
|
};
|
|
203
118
|
}
|