@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.
@@ -57,8 +57,7 @@ function x402Fetch(opts) {
57
57
  let body = lastBody;
58
58
  if (!body) {
59
59
  try {
60
- const raw = await res.clone().json();
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
- const raw = await res.clone().json();
84
- body = (0, errors_1.unwrapCapEnvelope)(raw);
79
+ body = await res.clone().json();
85
80
  }
86
81
  catch {
87
82
  if (opts.errorOnFailure) {
@@ -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 CSL = __importStar(require("@emurgo/cardano-serialization-lib-nodejs"));
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(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(),
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
- return result;
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
- * 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).
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(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 };
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 → CSL Transaction (parses both `transaction` and `fixed`
204
- // representation; we need both, Transaction for body access,
205
- // FixedTransaction for byte-stable hash).
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
- 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
- }
130
+ const txCborHex = txBuf.toString('hex');
131
+ const parsed = (0, bridge_1.parseTransaction)(txCborHex);
214
132
  // 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);
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: Buffer.from(txBuf).toString('hex'),
145
+ txCborHex,
235
146
  txHash,
236
- outputs: extractOutputs(txBody),
237
- inputs: extractInputs(txBody),
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
- * 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.
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
- * 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.
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
- * 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.
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 slot).
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>`, picked from the buyer's chosen inputs. */
45
+ /** v2 nonce reference `<txHash>#<index>`, the tx's first spent input. */
45
46
  nonceRef: string;
46
- /** Echo of the inputs chosen so the buyer's UI can show "spends these UTxOs". */
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
- * 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.
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
- * 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.
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
- * 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.
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. 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.
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 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}`);
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
- 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);
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: Buffer.from(unsigned.to_bytes()).toString('hex'),
197
- txHashHex: Buffer.from(txHash.to_bytes()).toString('hex').toLowerCase(),
198
- requiredSignerHex,
107
+ unsignedTxCborHex: result.unsignedTxCbor,
108
+ txHashHex: result.txBodyHash.toLowerCase(),
109
+ requiredSignerHex: paymentKeyHashHex,
199
110
  nonceRef,
200
- inputs: inputs.map(i => ({ txHash: i.txHash, outputIndex: i.outputIndex, lovelace: i.lovelace })),
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
  }