@koralabs/kora-labs-common 6.14.3 → 6.15.1

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.
@@ -38,6 +38,11 @@ export declare const resolveMode: (offline: string[], topology: CronLockTopology
38
38
  export declare const isPeerUnavailableError: (error: unknown) => boolean;
39
39
  /** A write timeout leaves an LWT's outcome unknown: the proposal may or may not have been applied. */
40
40
  export declare const isWriteTimeoutError: (error: unknown) => boolean;
41
+ /**
42
+ * An LWT whose outcome is unknown: the coordinator reported a write timeout, or it never replied within
43
+ * the driver's readTimeout ("The host … did not reply before timeout") — either way it may have committed.
44
+ */
45
+ export declare const isUnknownOutcomeError: (error: unknown) => boolean;
41
46
  /**
42
47
  * The directly testable LWT operation. Each invocation owns a unique token, so
43
48
  * concurrent ticks on one node cannot both treat the node identity as ownership.
@@ -46,6 +51,18 @@ export declare const acquireWithExecutor: (executor: CqlExecutor, name: string,
46
51
  export declare const acquire: (name: string, offline?: string | null, options?: {
47
52
  leaseMs?: number;
48
53
  }) => Promise<AcquireResult>;
49
- /** Release only the exact acquisition token owned by this invocation. */
54
+ export type ReleaseResult = {
55
+ status: 'released';
56
+ } | {
57
+ status: 'unknown';
58
+ message: string;
59
+ };
60
+ /**
61
+ * Release only the exact acquisition token owned by this invocation. The conditional DELETE is an LWT
62
+ * too: when it fails its outcome is unknown, and a surviving row would idle the job on every box for
63
+ * the whole lease. A serial read completes any in-flight Paxos round and tells whether the row is still
64
+ * ours; if it is, the (token-conditional) delete is issued once more.
65
+ */
66
+ export declare const releaseWithExecutor: (executor: CqlExecutor, name: string, token: string, offline: string | null | undefined, topology: CronLockTopology) => Promise<ReleaseResult>;
50
67
  export declare const release: (name: string, token: string, offline?: string | null) => Promise<void>;
51
68
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.release = exports.acquire = exports.acquireWithExecutor = exports.isWriteTimeoutError = exports.isPeerUnavailableError = exports.resolveMode = exports.parseOffline = exports.leaseMs = exports.resolveTopology = void 0;
3
+ exports.release = exports.releaseWithExecutor = exports.acquire = exports.acquireWithExecutor = exports.isUnknownOutcomeError = exports.isWriteTimeoutError = exports.isPeerUnavailableError = exports.resolveMode = exports.parseOffline = exports.leaseMs = exports.resolveTopology = void 0;
4
4
  /**
5
5
  * Cross-datacenter cron mutex for the self-hosted, multi-DC Scylla topology.
6
6
  * (Moved from minting.handle.me src/helpers/cronLock/cqlCronLock.ts @ 6ecb9c3 so every box cron
@@ -117,6 +117,12 @@ exports.isPeerUnavailableError = isPeerUnavailableError;
117
117
  /** A write timeout leaves an LWT's outcome unknown: the proposal may or may not have been applied. */
118
118
  const isWriteTimeoutError = (error) => error instanceof cassandra_driver_1.errors.ResponseError && error.code === cassandra_driver_1.types.responseErrorCodes.writeTimeout;
119
119
  exports.isWriteTimeoutError = isWriteTimeoutError;
120
+ /**
121
+ * An LWT whose outcome is unknown: the coordinator reported a write timeout, or it never replied within
122
+ * the driver's readTimeout ("The host … did not reply before timeout") — either way it may have committed.
123
+ */
124
+ const isUnknownOutcomeError = (error) => (0, exports.isWriteTimeoutError)(error) || error instanceof cassandra_driver_1.errors.OperationTimedOutError;
125
+ exports.isUnknownOutcomeError = isUnknownOutcomeError;
120
126
  /**
121
127
  * The directly testable LWT operation. Each invocation owns a unique token, so
122
128
  * concurrent ticks on one node cannot both treat the node identity as ownership.
@@ -144,7 +150,7 @@ const acquireWithExecutor = async (executor, name, offline, topology, token = cr
144
150
  if (mode === 'serial' && (0, exports.isPeerUnavailableError)(error)) {
145
151
  return { status: 'peerUnavailable', token, message };
146
152
  }
147
- if ((0, exports.isWriteTimeoutError)(error)) {
153
+ if ((0, exports.isUnknownOutcomeError)(error)) {
148
154
  // The LWT timed out: it may have been applied WITH OUR TOKEN, in which case nobody (not even
149
155
  // us) could run until the lease expired. A read at the same serial consistency completes any
150
156
  // in-flight Paxos round, so it tells us the real owner.
@@ -217,22 +223,53 @@ const acquire = async (name, offline, options = {}) => {
217
223
  }
218
224
  };
219
225
  exports.acquire = acquire;
220
- /** Release only the exact acquisition token owned by this invocation. */
226
+ /**
227
+ * Release only the exact acquisition token owned by this invocation. The conditional DELETE is an LWT
228
+ * too: when it fails its outcome is unknown, and a surviving row would idle the job on every box for
229
+ * the whole lease. A serial read completes any in-flight Paxos round and tells whether the row is still
230
+ * ours; if it is, the (token-conditional) delete is issued once more.
231
+ */
232
+ const releaseWithExecutor = async (executor, name, token, offline, topology) => {
233
+ const mode = (0, exports.resolveMode)((0, exports.parseOffline)(offline), topology);
234
+ if (mode === 'standDown')
235
+ return { status: 'released' };
236
+ const serialConsistency = mode === 'localSerial' ? cassandra_driver_1.types.consistencies.localSerial : cassandra_driver_1.types.consistencies.serial;
237
+ const consistency = mode === 'localSerial' ? cassandra_driver_1.types.consistencies.localQuorum : cassandra_driver_1.types.consistencies.quorum;
238
+ const key = scopeName(name, topology);
239
+ try {
240
+ await executor.execute(`DELETE FROM ${KEYSPACE}.${LOCK_TABLE} WHERE name = ? IF owner = ?`, [key, token], { prepare: true, serialConsistency, consistency });
241
+ return { status: 'released' };
242
+ }
243
+ catch (error) {
244
+ const message = error instanceof Error ? error.message : String(error);
245
+ const read = await readOwner(executor, key, serialConsistency);
246
+ if (read.error)
247
+ return { status: 'unknown', message: `${message}; owner read failed (${read.error})` };
248
+ if (read.owner !== token)
249
+ return { status: 'released' };
250
+ return (await deleteIfOwner(executor, key, token, serialConsistency, consistency))
251
+ ? { status: 'released' }
252
+ : { status: 'unknown', message: `${message}; the row is still ours and the second delete failed` };
253
+ }
254
+ };
255
+ exports.releaseWithExecutor = releaseWithExecutor;
221
256
  const release = async (name, token, offline) => {
222
257
  try {
223
258
  const topology = (0, exports.resolveTopology)();
224
- const mode = (0, exports.resolveMode)((0, exports.parseOffline)(offline), topology);
225
- if (mode === 'standDown')
226
- return;
227
- const serialConsistency = mode === 'localSerial' ? cassandra_driver_1.types.consistencies.localSerial : cassandra_driver_1.types.consistencies.serial;
228
- const consistency = mode === 'localSerial' ? cassandra_driver_1.types.consistencies.localQuorum : cassandra_driver_1.types.consistencies.quorum;
229
- await getClient(topology).execute(`DELETE FROM ${KEYSPACE}.${LOCK_TABLE} WHERE name = ? IF owner = ?`, [scopeName(name, topology), token], { prepare: true, serialConsistency, consistency });
259
+ const result = await (0, exports.releaseWithExecutor)(getClient(topology), name, token, offline, topology);
260
+ if (result.status === 'unknown') {
261
+ logger_1.Logger.log({
262
+ message: `cron lock '${name}' release failed; if the row survived, '${name}' idles on every box until its lease expires: ${result.message}`,
263
+ event: 'cqlCronLock.release.unknown',
264
+ category: logger_1.LogCategory.WARN
265
+ });
266
+ }
230
267
  }
231
268
  catch (error) {
232
269
  logger_1.Logger.log({
233
- message: `cron lock '${name}' release best-effort failed (TTL will reclaim): ${error.message}`,
234
- event: 'cqlCronLock.release.softfail',
235
- category: logger_1.LogCategory.INFO
270
+ message: `cron lock '${name}' release failed (TTL will reclaim): ${error.message}`,
271
+ event: 'cqlCronLock.release.unknown',
272
+ category: logger_1.LogCategory.WARN
236
273
  });
237
274
  }
238
275
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koralabs/kora-labs-common",
3
- "version": "6.14.3",
3
+ "version": "6.15.1",
4
4
  "description": "Kora Labs Common Utilities",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -52,6 +52,7 @@
52
52
  "cassandra-driver": "^4.10.0",
53
53
  "eslint": "^8.57.1",
54
54
  "jest": "^30.4.2",
55
+ "scalus": "^0.15.0",
55
56
  "ts-jest": "^29.4.11",
56
57
  "typescript": "^5.9.3"
57
58
  },
@@ -77,7 +78,8 @@
77
78
  "@cardano-sdk/crypto": "^0.4.5",
78
79
  "@emurgo/cardano-message-signing-nodejs": "^1.1.0",
79
80
  "bip39": "^3.1.0",
80
- "cassandra-driver": "^4.9.0"
81
+ "cassandra-driver": "^4.9.0",
82
+ "scalus": "^0.15.0"
81
83
  },
82
84
  "peerDependenciesMeta": {
83
85
  "@aiken-lang/merkle-patricia-forestry": {
@@ -97,6 +99,9 @@
97
99
  },
98
100
  "cassandra-driver": {
99
101
  "optional": true
102
+ },
103
+ "scalus": {
104
+ "optional": true
100
105
  }
101
106
  }
102
107
  }
@@ -109,6 +109,7 @@ class BlockfrostTxClient {
109
109
  priceSteps: (0, fees_1.ratio)(p.price_step),
110
110
  minFeeRefScriptCostPerByte: (0, fees_1.ratio)((_a = p.min_fee_ref_script_cost_per_byte) !== null && _a !== void 0 ? _a : 0),
111
111
  coinsPerUtxoByte: BigInt(p.coins_per_utxo_size),
112
+ stakeKeyDeposit: BigInt(p.key_deposit),
112
113
  maxTxSize: Number(p.max_tx_size),
113
114
  maxTxExUnits: { memory: Number(p.max_tx_ex_mem), steps: Number(p.max_tx_ex_steps) },
114
115
  costModels
@@ -1,3 +1,4 @@
1
1
  export * from './fees';
2
2
  export * from './scriptTx';
3
3
  export * from './blockfrost';
4
+ export * from './scalus';
package/txBuild/index.js CHANGED
@@ -19,3 +19,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  __exportStar(require("./fees"), exports);
20
20
  __exportStar(require("./scriptTx"), exports);
21
21
  __exportStar(require("./blockfrost"), exports);
22
+ __exportStar(require("./scalus"), exports);
@@ -0,0 +1,26 @@
1
+ import { Cardano, Serialization } from '@cardano-sdk/core';
2
+ import { Evaluator } from './scriptTx';
3
+ /**
4
+ * Scripts come in two forms: "single CBOR" — a CBOR byte string of the flat program, as Aiken
5
+ * blueprints, Blockfrost `/scripts/{hash}/cbor` and cardano-sdk's `PlutusScript.bytes` hold it — and
6
+ * "double CBOR" — that wrapped once more, as scalus reads and writes it.
7
+ */
8
+ export declare const toDoubleCbor: (cborHex: string) => string;
9
+ export declare const toSingleCbor: (cborHex: string) => string;
10
+ /** Plutus data in the node's JSON form (what scalus.applyDataArgToScript reads). */
11
+ export declare const plutusDataToJson: (data: Serialization.PlutusData) => unknown;
12
+ /** Apply parameters (in order) to a compiled validator; returns the script in single-CBOR form. */
13
+ export declare const applyParamsToScript: (compiledCode: string, params: Serialization.PlutusData[]) => string;
14
+ /** A Plutus script (single- or double-CBOR) as a cardano-sdk Script. */
15
+ export declare const plutusScript: (cborHex: string, version: Cardano.PlutusLanguageVersion) => Cardano.PlutusScript;
16
+ /** The script hash of a (single- or double-CBOR) Plutus script of `version`. */
17
+ export declare const plutusScriptHash: (cborHex: string, version: Cardano.PlutusLanguageVersion) => string;
18
+ /**
19
+ * An Evaluator that runs the tx's scripts locally (scalus) against `utxos` — every spent AND
20
+ * referenced UTxO, reference scripts included. Offline stand-in for the node's evaluation.
21
+ */
22
+ export declare const localEvaluator: ({ utxos, costModels, network }: {
23
+ utxos: Cardano.Utxo[];
24
+ costModels: Map<Cardano.PlutusLanguageVersion, number[]>;
25
+ network: "mainnet" | "preview" | "preprod";
26
+ }) => Evaluator;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.localEvaluator = exports.plutusScriptHash = exports.plutusScript = exports.applyParamsToScript = exports.plutusDataToJson = exports.toSingleCbor = exports.toDoubleCbor = void 0;
4
+ // Local UPLC work on scalus (optional peer dependency, loaded on first use): applying parameters to a
5
+ // compiled validator (replaces Helios' UplcProgram.apply) and evaluating a tx's scripts offline.
6
+ // Production code evaluates with the node (BlockfrostTxClient.evaluateTx); the local evaluator is for
7
+ // tests and tooling that must run without a network.
8
+ const core_1 = require("@cardano-sdk/core");
9
+ let scalusModule;
10
+ // Lazy: scalus is an optional peer, needed only by callers of these helpers.
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const scalus = () => (scalusModule !== null && scalusModule !== void 0 ? scalusModule : (scalusModule = require('scalus')));
13
+ const cborByteStringHeader = (length) => {
14
+ if (length <= 23)
15
+ return Buffer.from([0x40 | length]);
16
+ if (length <= 0xff)
17
+ return Buffer.from([0x58, length]);
18
+ if (length <= 0xffff)
19
+ return Buffer.from([0x59, length >> 8, length & 0xff]);
20
+ return Buffer.from([0x5a, (length >>> 24) & 0xff, (length >> 16) & 0xff, (length >> 8) & 0xff, length & 0xff]);
21
+ };
22
+ /** [header length, content length] of the CBOR byte string at the start of `bytes`. */
23
+ const byteStringHeader = (bytes) => {
24
+ if (bytes.length === 0 || bytes[0] >> 5 !== 2)
25
+ throw new Error('expected a CBOR byte string');
26
+ const info = bytes[0] & 0x1f;
27
+ if (info <= 23)
28
+ return [1, info];
29
+ if (info === 24)
30
+ return [2, bytes[1]];
31
+ if (info === 25)
32
+ return [3, bytes.readUInt16BE(1)];
33
+ if (info === 26)
34
+ return [5, bytes.readUInt32BE(1)];
35
+ throw new Error(`unsupported CBOR byte string header 0x${bytes[0].toString(16)}`);
36
+ };
37
+ const isDoubleCbor = (bytes) => {
38
+ const [headerLength] = byteStringHeader(bytes);
39
+ return bytes[headerLength] >> 5 === 2;
40
+ };
41
+ /**
42
+ * Scripts come in two forms: "single CBOR" — a CBOR byte string of the flat program, as Aiken
43
+ * blueprints, Blockfrost `/scripts/{hash}/cbor` and cardano-sdk's `PlutusScript.bytes` hold it — and
44
+ * "double CBOR" — that wrapped once more, as scalus reads and writes it.
45
+ */
46
+ const toDoubleCbor = (cborHex) => {
47
+ const bytes = Buffer.from(cborHex, 'hex');
48
+ return isDoubleCbor(bytes) ? cborHex : Buffer.concat([cborByteStringHeader(bytes.length), bytes]).toString('hex');
49
+ };
50
+ exports.toDoubleCbor = toDoubleCbor;
51
+ const toSingleCbor = (cborHex) => {
52
+ const bytes = Buffer.from(cborHex, 'hex');
53
+ if (!isDoubleCbor(bytes))
54
+ return cborHex;
55
+ const [headerLength, length] = byteStringHeader(bytes);
56
+ return bytes.subarray(headerLength, headerLength + length).toString('hex');
57
+ };
58
+ exports.toSingleCbor = toSingleCbor;
59
+ /** Plutus data in the node's JSON form (what scalus.applyDataArgToScript reads). */
60
+ const plutusDataToJson = (data) => {
61
+ const c = data.asConstrPlutusData();
62
+ if (c) {
63
+ const fields = c.getData();
64
+ return { constructor: Number(c.getAlternative()), fields: Array.from({ length: fields.getLength() }, (_, i) => (0, exports.plutusDataToJson)(fields.get(i))) };
65
+ }
66
+ const bytes = data.asBoundedBytes();
67
+ if (bytes)
68
+ return { bytes: Buffer.from(bytes).toString('hex') };
69
+ const integer = data.asInteger();
70
+ if (integer !== undefined) {
71
+ if (integer > BigInt(Number.MAX_SAFE_INTEGER) || integer < BigInt(Number.MIN_SAFE_INTEGER))
72
+ throw new Error(`integer parameter ${integer} is outside the safe range scalus accepts`);
73
+ return { int: Number(integer) };
74
+ }
75
+ const items = data.asList();
76
+ if (items)
77
+ return { list: Array.from({ length: items.getLength() }, (_, i) => (0, exports.plutusDataToJson)(items.get(i))) };
78
+ const map = data.asMap();
79
+ if (map) {
80
+ const keys = map.getKeys();
81
+ return { map: Array.from({ length: keys.getLength() }, (_, i) => ({ k: (0, exports.plutusDataToJson)(keys.get(i)), v: (0, exports.plutusDataToJson)(map.get(keys.get(i))) })) };
82
+ }
83
+ throw new Error('unsupported Plutus data');
84
+ };
85
+ exports.plutusDataToJson = plutusDataToJson;
86
+ /** Apply parameters (in order) to a compiled validator; returns the script in single-CBOR form. */
87
+ const applyParamsToScript = (compiledCode, params) => (0, exports.toSingleCbor)(params.reduce((script, param) => scalus().Scalus.applyDataArgToScript(script, JSON.stringify((0, exports.plutusDataToJson)(param))), (0, exports.toDoubleCbor)(compiledCode)));
88
+ exports.applyParamsToScript = applyParamsToScript;
89
+ /** A Plutus script (single- or double-CBOR) as a cardano-sdk Script. */
90
+ const plutusScript = (cborHex, version) => ({
91
+ __type: core_1.Cardano.ScriptType.Plutus,
92
+ version,
93
+ bytes: (0, exports.toSingleCbor)(cborHex)
94
+ });
95
+ exports.plutusScript = plutusScript;
96
+ /** The script hash of a (single- or double-CBOR) Plutus script of `version`. */
97
+ const plutusScriptHash = (cborHex, version) => core_1.Serialization.Script.fromCore((0, exports.plutusScript)(cborHex, version)).hash();
98
+ exports.plutusScriptHash = plutusScriptHash;
99
+ const PURPOSE = { spend: 'spend', mint: 'mint', cert: 'certificate', certificate: 'certificate', reward: 'withdrawal', withdraw: 'withdrawal', withdrawal: 'withdrawal' };
100
+ /**
101
+ * An Evaluator that runs the tx's scripts locally (scalus) against `utxos` — every spent AND
102
+ * referenced UTxO, reference scripts included. Offline stand-in for the node's evaluation.
103
+ */
104
+ const localEvaluator = ({ utxos, costModels, network }) => {
105
+ const writer = new core_1.Serialization.CborWriter();
106
+ writer.writeStartMap(utxos.length);
107
+ for (const [txIn, txOut] of utxos) {
108
+ writer.writeEncodedValue(Buffer.from(core_1.Serialization.TransactionInput.fromCore(txIn).toCbor(), 'hex'));
109
+ writer.writeEncodedValue(Buffer.from(core_1.Serialization.TransactionOutput.fromCore(txOut).toCbor(), 'hex'));
110
+ }
111
+ const utxoCbor = writer.encode();
112
+ const models = [core_1.Cardano.PlutusLanguageVersion.V1, core_1.Cardano.PlutusLanguageVersion.V2, core_1.Cardano.PlutusLanguageVersion.V3].map((v) => { var _a; return (_a = costModels.get(v)) !== null && _a !== void 0 ? _a : []; });
113
+ return async (txCbor) => {
114
+ const { Scalus, SlotConfig } = scalus();
115
+ const results = Scalus.evalPlutusScripts(Buffer.from(txCbor, 'hex'), utxoCbor, SlotConfig[network], models);
116
+ return new Map(results.map((r) => { var _a; return [`${(_a = PURPOSE[String(r.tag).toLowerCase()]) !== null && _a !== void 0 ? _a : String(r.tag).toLowerCase()}:${r.index}`, { memory: Number(r.budget.memory), steps: Number(r.budget.steps) }]; }));
117
+ };
118
+ };
119
+ exports.localEvaluator = localEvaluator;
@@ -3,6 +3,8 @@ import { ExUnits, FeeParameters } from './fees';
3
3
  export type PlutusData = Serialization.PlutusData;
4
4
  export interface ScriptTxProtocolParameters extends FeeParameters {
5
5
  coinsPerUtxoByte: bigint;
6
+ /** key_deposit: what a (pre-Conway-style) stake registration certificate locks. */
7
+ stakeKeyDeposit: bigint;
6
8
  maxTxSize: number;
7
9
  maxTxExUnits: {
8
10
  memory: number;
@@ -32,6 +34,8 @@ export interface ScriptTxPlan {
32
34
  quantity: bigint;
33
35
  redeemer?: PlutusData;
34
36
  }[];
37
+ /** Stake (de)registrations; their deposits/refunds are balanced into the change. No certificate redeemers. */
38
+ certificates?: Cardano.Certificate[];
35
39
  requiredSigners?: string[];
36
40
  nativeScripts?: Cardano.NativeScript[];
37
41
  validityInterval?: Cardano.ValidityInterval;
@@ -67,7 +71,15 @@ export declare const computeScriptDataHash: ({ redeemersCbor, datumsCbor, costMo
67
71
  costModels: Map<Cardano.PlutusLanguageVersion, number[]>;
68
72
  languages: Cardano.PlutusLanguageVersion[];
69
73
  }) => string | undefined;
74
+ /** Lovelace a certificate locks (positive) or releases (negative). */
75
+ export declare const certificateDeposit: (certificate: Cardano.Certificate, stakeKeyDeposit: bigint) => bigint;
70
76
  export declare const finalizeScriptTx: (plan: ScriptTxPlan, params: ScriptTxProtocolParameters, evaluate: Evaluator) => Promise<FinalizedScriptTx>;
77
+ /**
78
+ * Wallet inputs for a tx whose other inputs are fixed: ADA-only UTxOs first, largest first, until they
79
+ * hold `lovelace` (callers include a fee/min-UTxO allowance; the finalizer returns the excess as change).
80
+ * UTxOs carrying tokens are only used when ADA-only ones do not suffice (their tokens go to change).
81
+ */
82
+ export declare const selectWalletInputs: (utxos: Cardano.Utxo[], lovelace: bigint, exclude?: Set<string>) => Cardano.Utxo[];
71
83
  /**
72
84
  * Add vkey witnesses to an unsigned tx by splicing them into its witness set byte-for-byte
73
85
  * (`mergeWitnessSet`): the body (so the tx id and every signature) and the redeemer bytes the
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.addVkeyWitnesses = exports.finalizeScriptTx = exports.computeScriptDataHash = exports.withMinAda = void 0;
3
+ exports.addVkeyWitnesses = exports.selectWalletInputs = exports.finalizeScriptTx = exports.certificateDeposit = exports.computeScriptDataHash = exports.withMinAda = void 0;
4
4
  // Deterministic Plutus transaction finalizer on @cardano-sdk/core (no input selection, no Helios).
5
5
  //
6
6
  // The caller fixes every input; this module derives redeemer pointers from the ledger's canonical
@@ -60,6 +60,22 @@ const placeholderSignatures = (count) => {
60
60
  signatures.set((i + 1).toString(16).padStart(64, '0'), '0'.repeat(128));
61
61
  return signatures;
62
62
  };
63
+ /** Lovelace a certificate locks (positive) or releases (negative). */
64
+ const certificateDeposit = (certificate, stakeKeyDeposit) => {
65
+ switch (certificate.__typename) {
66
+ case core_1.Cardano.CertificateType.StakeRegistration:
67
+ return stakeKeyDeposit;
68
+ case core_1.Cardano.CertificateType.StakeDeregistration:
69
+ return -stakeKeyDeposit;
70
+ case core_1.Cardano.CertificateType.Registration:
71
+ return certificate.deposit;
72
+ case core_1.Cardano.CertificateType.Unregistration:
73
+ return -certificate.deposit;
74
+ default:
75
+ throw new Error(`Certificate ${certificate.__typename} is not supported by finalizeScriptTx`);
76
+ }
77
+ };
78
+ exports.certificateDeposit = certificateDeposit;
63
79
  const sumValues = (values) => {
64
80
  var _a, _b;
65
81
  let coins = BigInt(0);
@@ -72,7 +88,7 @@ const sumValues = (values) => {
72
88
  return { coins, assets };
73
89
  };
74
90
  const finalizeScriptTx = async (plan, params, evaluate) => {
75
- var _a, _b;
91
+ var _a, _b, _c;
76
92
  const inputs = [...plan.inputs].sort((a, b) => byTxIn(a.utxo[0], b.utxo[0]));
77
93
  const mint = [...((_a = plan.mint) !== null && _a !== void 0 ? _a : [])].sort((a, b) => hexCompare(a.policyId, b.policyId));
78
94
  const withdrawals = [...((_b = plan.withdrawals) !== null && _b !== void 0 ? _b : [])].sort((a, b) => hexCompare(rewardAccountBytes(a.rewardAccount), rewardAccountBytes(b.rewardAccount)));
@@ -90,10 +106,11 @@ const finalizeScriptTx = async (plan, params, evaluate) => {
90
106
  }
91
107
  const inValue = sumValues(inputs.map(({ utxo }) => utxo[1].value));
92
108
  const withdrawn = withdrawals.reduce((sum, w) => sum + w.quantity, BigInt(0));
109
+ const deposited = ((_c = plan.certificates) !== null && _c !== void 0 ? _c : []).reduce((sum, c) => sum + (0, exports.certificateDeposit)(c, params.stakeKeyDeposit), BigInt(0));
93
110
  const produced = sumValues(outputs.map((o) => o.value));
94
111
  const changeFor = (fee) => {
95
112
  var _a, _b, _c;
96
- const coins = inValue.coins + withdrawn - produced.coins - fee;
113
+ const coins = inValue.coins + withdrawn - deposited - produced.coins - fee;
97
114
  if (coins < BigInt(0))
98
115
  throw new Error(`Inputs do not cover outputs + fee: ${-coins} lovelace short`);
99
116
  const assets = new Map();
@@ -108,7 +125,7 @@ const finalizeScriptTx = async (plan, params, evaluate) => {
108
125
  return { address: plan.changeAddress, value: { coins, ...(assets.size ? { assets } : {}) } };
109
126
  };
110
127
  const assemble = (fee, exUnits, signatures) => {
111
- var _a, _b, _c, _d;
128
+ var _a, _b, _c, _d, _e;
112
129
  const change = changeFor(fee);
113
130
  const redeemers = pointers.map(({ purpose, index, data }) => {
114
131
  const units = exUnits.get(key(purpose, index));
@@ -125,8 +142,9 @@ const finalizeScriptTx = async (plan, params, evaluate) => {
125
142
  ...(((_a = plan.referenceInputs) === null || _a === void 0 ? void 0 : _a.length) ? { referenceInputs: [...plan.referenceInputs].sort(byTxIn) } : {}),
126
143
  ...(((_b = plan.collateral) === null || _b === void 0 ? void 0 : _b.length) ? { collaterals: [...plan.collateral].sort(byTxIn) } : {}),
127
144
  ...(tokenMap.size ? { mint: tokenMap } : {}),
145
+ ...(((_c = plan.certificates) === null || _c === void 0 ? void 0 : _c.length) ? { certificates: plan.certificates } : {}),
128
146
  ...(withdrawals.length ? { withdrawals: withdrawals.map((w) => ({ stakeAddress: w.rewardAccount, quantity: w.quantity })) } : {}),
129
- ...(((_c = plan.requiredSigners) === null || _c === void 0 ? void 0 : _c.length) ? { requiredExtraSignatures: [...new Set(plan.requiredSigners)].sort(hexCompare) } : {}),
147
+ ...(((_d = plan.requiredSigners) === null || _d === void 0 ? void 0 : _d.length) ? { requiredExtraSignatures: [...new Set(plan.requiredSigners)].sort(hexCompare) } : {}),
130
148
  ...(plan.validityInterval ? { validityInterval: plan.validityInterval } : {}),
131
149
  ...(scriptIntegrityHash ? { scriptIntegrityHash: scriptIntegrityHash } : {})
132
150
  };
@@ -136,7 +154,7 @@ const finalizeScriptTx = async (plan, params, evaluate) => {
136
154
  witness: {
137
155
  signatures: signatures,
138
156
  ...(redeemers.length ? { redeemers } : {}),
139
- ...(((_d = plan.nativeScripts) === null || _d === void 0 ? void 0 : _d.length) ? { scripts: plan.nativeScripts } : {})
157
+ ...(((_e = plan.nativeScripts) === null || _e === void 0 ? void 0 : _e.length) ? { scripts: plan.nativeScripts } : {})
140
158
  },
141
159
  isValid: true
142
160
  });
@@ -183,6 +201,36 @@ const finalizeScriptTx = async (plan, params, evaluate) => {
183
201
  return { cbor: tx.toCbor(), txId: tx.getId(), fee, outputs: [...outputs, change], redeemers };
184
202
  };
185
203
  exports.finalizeScriptTx = finalizeScriptTx;
204
+ /**
205
+ * Wallet inputs for a tx whose other inputs are fixed: ADA-only UTxOs first, largest first, until they
206
+ * hold `lovelace` (callers include a fee/min-UTxO allowance; the finalizer returns the excess as change).
207
+ * UTxOs carrying tokens are only used when ADA-only ones do not suffice (their tokens go to change).
208
+ */
209
+ const selectWalletInputs = (utxos, lovelace, exclude = new Set()) => {
210
+ const key = ([txIn]) => `${txIn.txId}#${txIn.index}`;
211
+ const candidates = utxos
212
+ .filter((u) => !exclude.has(key(u)))
213
+ .sort((a, b) => {
214
+ var _a, _b;
215
+ const tokensA = ((_a = a[1].value.assets) === null || _a === void 0 ? void 0 : _a.size) ? 1 : 0;
216
+ const tokensB = ((_b = b[1].value.assets) === null || _b === void 0 ? void 0 : _b.size) ? 1 : 0;
217
+ if (tokensA !== tokensB)
218
+ return tokensA - tokensB;
219
+ return a[1].value.coins === b[1].value.coins ? 0 : a[1].value.coins > b[1].value.coins ? -1 : 1;
220
+ });
221
+ const selected = [];
222
+ let total = BigInt(0);
223
+ for (const utxo of candidates) {
224
+ if (total >= lovelace)
225
+ break;
226
+ selected.push(utxo);
227
+ total += utxo[1].value.coins;
228
+ }
229
+ if (total < lovelace)
230
+ throw new Error(`Wallet holds ${total} spendable lovelace; ${lovelace} needed`);
231
+ return selected;
232
+ };
233
+ exports.selectWalletInputs = selectWalletInputs;
186
234
  /**
187
235
  * Add vkey witnesses to an unsigned tx by splicing them into its witness set byte-for-byte
188
236
  * (`mergeWitnessSet`): the body (so the tx id and every signature) and the redeemer bytes the