@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.
- package/cronLock/cqlCronLock.d.ts +18 -1
- package/cronLock/cqlCronLock.js +49 -12
- package/package.json +7 -2
- package/txBuild/blockfrost.js +1 -0
- package/txBuild/index.d.ts +1 -0
- package/txBuild/index.js +1 -0
- package/txBuild/scalus.d.ts +26 -0
- package/txBuild/scalus.js +119 -0
- package/txBuild/scriptTx.d.ts +12 -0
- package/txBuild/scriptTx.js +54 -6
|
@@ -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
|
-
|
|
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 {};
|
package/cronLock/cqlCronLock.js
CHANGED
|
@@ -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.
|
|
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
|
-
/**
|
|
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
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
234
|
-
event: 'cqlCronLock.release.
|
|
235
|
-
category: logger_1.LogCategory.
|
|
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.
|
|
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
|
}
|
package/txBuild/blockfrost.js
CHANGED
|
@@ -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
|
package/txBuild/index.d.ts
CHANGED
package/txBuild/index.js
CHANGED
|
@@ -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;
|
package/txBuild/scriptTx.d.ts
CHANGED
|
@@ -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
|
package/txBuild/scriptTx.js
CHANGED
|
@@ -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
|
-
...(((
|
|
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
|
-
...(((
|
|
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
|