@koralabs/kora-labs-common 6.13.4 → 6.14.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/chain/index.d.ts +1 -0
- package/chain/index.js +1 -0
- package/chain/transport/fetchProviderJson.d.ts +7 -3
- package/chain/transport/fetchProviderJson.js +38 -6
- package/chain/transport/rateLimit.d.ts +21 -0
- package/chain/transport/rateLimit.js +101 -0
- package/cronLock/cqlCronLock.d.ts +49 -0
- package/cronLock/cqlCronLock.js +199 -0
- package/cronLock/index.d.ts +1 -0
- package/cronLock/index.js +19 -0
- package/package.json +7 -2
- package/txBuild/blockfrost.d.ts +66 -0
- package/txBuild/blockfrost.js +209 -0
- package/txBuild/fees.d.ts +32 -0
- package/txBuild/fees.js +62 -0
- package/txBuild/index.d.ts +3 -0
- package/txBuild/index.js +21 -0
- package/txBuild/scriptTx.d.ts +79 -0
- package/txBuild/scriptTx.js +197 -0
- package/utils/cbor/index.d.ts +2 -0
- package/utils/cbor/index.js +27 -24
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BlockfrostTxClient = exports.toOgmiosUtxo = exports.valueFromBlockfrost = exports.BlockfrostEvaluationError = void 0;
|
|
4
|
+
// Blockfrost access for building/evaluating/submitting script transactions, returning @cardano-sdk/core
|
|
5
|
+
// types. Every call goes through the shared rate-limited transport (fetchProviderJson), which honors
|
|
6
|
+
// Retry-After and friends process-wide.
|
|
7
|
+
const core_1 = require("@cardano-sdk/core");
|
|
8
|
+
const fetchProviderJson_1 = require("../chain/transport/fetchProviderJson");
|
|
9
|
+
const fees_1 = require("./fees");
|
|
10
|
+
const PLUTUS_LANGUAGE = {
|
|
11
|
+
plutusV1: core_1.Cardano.PlutusLanguageVersion.V1,
|
|
12
|
+
plutusV2: core_1.Cardano.PlutusLanguageVersion.V2,
|
|
13
|
+
plutusV3: core_1.Cardano.PlutusLanguageVersion.V3
|
|
14
|
+
};
|
|
15
|
+
const OGMIOS_PURPOSE = {
|
|
16
|
+
spend: 'spend',
|
|
17
|
+
mint: 'mint',
|
|
18
|
+
certificate: 'certificate',
|
|
19
|
+
withdrawal: 'withdrawal',
|
|
20
|
+
withdraw: 'withdrawal',
|
|
21
|
+
reward: 'withdrawal'
|
|
22
|
+
};
|
|
23
|
+
class BlockfrostEvaluationError extends Error {
|
|
24
|
+
constructor(failure) {
|
|
25
|
+
super(`Transaction failed evaluation: ${JSON.stringify(failure)}`);
|
|
26
|
+
this.failure = failure;
|
|
27
|
+
this.name = 'BlockfrostEvaluationError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.BlockfrostEvaluationError = BlockfrostEvaluationError;
|
|
31
|
+
const valueFromBlockfrost = (amount) => {
|
|
32
|
+
let coins = BigInt(0);
|
|
33
|
+
const assets = new Map();
|
|
34
|
+
for (const { unit, quantity } of amount) {
|
|
35
|
+
if (unit === 'lovelace')
|
|
36
|
+
coins = BigInt(quantity);
|
|
37
|
+
else
|
|
38
|
+
assets.set(core_1.Cardano.AssetId.fromParts(core_1.Cardano.PolicyId(unit.slice(0, 56)), core_1.Cardano.AssetName(unit.slice(56))), BigInt(quantity));
|
|
39
|
+
}
|
|
40
|
+
return { coins, ...(assets.size ? { assets } : {}) };
|
|
41
|
+
};
|
|
42
|
+
exports.valueFromBlockfrost = valueFromBlockfrost;
|
|
43
|
+
/** Additional-UTxO shape Blockfrost's evaluator accepts: Ogmios value `{ ada: { lovelace }, [policy]: { [asset]: qty } }`. */
|
|
44
|
+
const toOgmiosUtxo = ([txIn, txOut]) => {
|
|
45
|
+
var _a, _b;
|
|
46
|
+
const value = { ada: { lovelace: Number(txOut.value.coins) } };
|
|
47
|
+
for (const [id, qty] of (_a = txOut.value.assets) !== null && _a !== void 0 ? _a : []) {
|
|
48
|
+
const policy = core_1.Cardano.AssetId.getPolicyId(id);
|
|
49
|
+
value[policy] = { ...((_b = value[policy]) !== null && _b !== void 0 ? _b : {}), [core_1.Cardano.AssetId.getAssetName(id)]: Number(qty) };
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
{ txId: txIn.txId, index: txIn.index },
|
|
53
|
+
{
|
|
54
|
+
address: txOut.address,
|
|
55
|
+
value,
|
|
56
|
+
...(txOut.datum ? { datum: core_1.Serialization.PlutusData.fromCore(txOut.datum).toCbor() } : {}),
|
|
57
|
+
...(txOut.datumHash ? { datumHash: txOut.datumHash } : {})
|
|
58
|
+
}
|
|
59
|
+
];
|
|
60
|
+
};
|
|
61
|
+
exports.toOgmiosUtxo = toOgmiosUtxo;
|
|
62
|
+
class BlockfrostTxClient {
|
|
63
|
+
constructor(config) {
|
|
64
|
+
this.config = config;
|
|
65
|
+
this.scripts = new Map();
|
|
66
|
+
this.host = `https://cardano-${config.network.toLowerCase()}.blockfrost.io/api/v0`;
|
|
67
|
+
}
|
|
68
|
+
request(path, init = {}) {
|
|
69
|
+
var _a, _b;
|
|
70
|
+
return (0, fetchProviderJson_1.fetchProviderJson)({
|
|
71
|
+
provider: 'Blockfrost',
|
|
72
|
+
url: `${this.host}/${path.replace(/^\//, '')}`,
|
|
73
|
+
method: (_a = init.method) !== null && _a !== void 0 ? _a : 'GET',
|
|
74
|
+
headers: { project_id: this.config.blockfrostApiKey, 'Content-Type': (_b = init.contentType) !== null && _b !== void 0 ? _b : 'application/json' },
|
|
75
|
+
body: init.body,
|
|
76
|
+
fetcher: this.config.fetcher,
|
|
77
|
+
// Submission/evaluation are not idempotent-safe to blind-retry on 5xx; rate limits are still honored.
|
|
78
|
+
...(init.method === 'POST' ? { maxRetries: 0 } : {})
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async getOrNull(path) {
|
|
82
|
+
try {
|
|
83
|
+
return await this.request(path);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if ((error === null || error === void 0 ? void 0 : error.status) === 404)
|
|
87
|
+
return null;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async getProtocolParameters() {
|
|
92
|
+
var _a;
|
|
93
|
+
const p = await this.request('epochs/latest/parameters');
|
|
94
|
+
const raw = p.cost_models_raw;
|
|
95
|
+
if (!raw)
|
|
96
|
+
throw new Error('Protocol parameters carry no cost_models_raw');
|
|
97
|
+
// cost_models_raw is the LEDGER-ordered array; the named `cost_models` object is not (its
|
|
98
|
+
// alphabetical order produced script_data_hash mismatches once the models were extended).
|
|
99
|
+
const costModels = new Map();
|
|
100
|
+
for (const [name, model] of Object.entries(raw)) {
|
|
101
|
+
const language = PLUTUS_LANGUAGE[name.charAt(0).toLowerCase() + name.slice(1)];
|
|
102
|
+
if (language !== undefined)
|
|
103
|
+
costModels.set(language, model);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
minFeeA: BigInt(p.min_fee_a),
|
|
107
|
+
minFeeB: BigInt(p.min_fee_b),
|
|
108
|
+
priceMemory: (0, fees_1.ratio)(p.price_mem),
|
|
109
|
+
priceSteps: (0, fees_1.ratio)(p.price_step),
|
|
110
|
+
minFeeRefScriptCostPerByte: (0, fees_1.ratio)((_a = p.min_fee_ref_script_cost_per_byte) !== null && _a !== void 0 ? _a : 0),
|
|
111
|
+
coinsPerUtxoByte: BigInt(p.coins_per_utxo_size),
|
|
112
|
+
maxTxSize: Number(p.max_tx_size),
|
|
113
|
+
maxTxExUnits: { memory: Number(p.max_tx_ex_mem), steps: Number(p.max_tx_ex_steps) },
|
|
114
|
+
costModels
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
getLatestBlock() {
|
|
118
|
+
return this.request('blocks/latest');
|
|
119
|
+
}
|
|
120
|
+
/** Slot of a POSIX time (ms), from the tip (1-second slots since Shelley on every network). */
|
|
121
|
+
async slotAt(posixMs) {
|
|
122
|
+
const tip = await this.getLatestBlock();
|
|
123
|
+
return tip.slot + Math.floor((posixMs - tip.time * 1000) / 1000);
|
|
124
|
+
}
|
|
125
|
+
getScript(hash) {
|
|
126
|
+
let cached = this.scripts.get(hash);
|
|
127
|
+
if (!cached) {
|
|
128
|
+
cached = (async () => {
|
|
129
|
+
var _a, _b;
|
|
130
|
+
const info = await this.request(`scripts/${hash}`);
|
|
131
|
+
if (info.type === 'timelock')
|
|
132
|
+
return { hash, language: 'native', size: Number((_a = info.serialised_size) !== null && _a !== void 0 ? _a : 0) };
|
|
133
|
+
const language = PLUTUS_LANGUAGE[info.type];
|
|
134
|
+
if (language === undefined)
|
|
135
|
+
throw new Error(`Unknown script type ${info.type} for ${hash}`);
|
|
136
|
+
const { cbor } = await this.request(`scripts/${hash}/cbor`);
|
|
137
|
+
// Blockfrost's `cbor` is the script as the ledger hashes it (a CBOR byte string of the flat program).
|
|
138
|
+
const script = { __type: core_1.Cardano.ScriptType.Plutus, version: language, bytes: cbor };
|
|
139
|
+
if (core_1.Serialization.Script.fromCore(script).hash() !== hash)
|
|
140
|
+
throw new Error(`Script ${hash} CBOR does not hash to ${hash}`);
|
|
141
|
+
return { hash, language, size: Number((_b = info.serialised_size) !== null && _b !== void 0 ? _b : cbor.length / 2), script };
|
|
142
|
+
})();
|
|
143
|
+
cached.catch(() => this.scripts.delete(hash));
|
|
144
|
+
this.scripts.set(hash, cached);
|
|
145
|
+
}
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
148
|
+
async toUtxo(item, txHash, address) {
|
|
149
|
+
const scriptReference = item.reference_script_hash ? (await this.getScript(item.reference_script_hash)).script : undefined;
|
|
150
|
+
return [
|
|
151
|
+
{ txId: core_1.Cardano.TransactionId(txHash), index: item.output_index, address: address },
|
|
152
|
+
{
|
|
153
|
+
address: address,
|
|
154
|
+
value: (0, exports.valueFromBlockfrost)(item.amount),
|
|
155
|
+
...(item.inline_datum ? { datum: core_1.Serialization.PlutusData.fromCbor(item.inline_datum).toCore() } : {}),
|
|
156
|
+
...(!item.inline_datum && item.data_hash ? { datumHash: item.data_hash } : {}),
|
|
157
|
+
...(scriptReference ? { scriptReference } : {})
|
|
158
|
+
}
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
/** Every UTxO at `address` (paginated). */
|
|
162
|
+
async getAddressUtxos(address) {
|
|
163
|
+
var _a;
|
|
164
|
+
const utxos = [];
|
|
165
|
+
for (let page = 1;; page++) {
|
|
166
|
+
const items = (_a = (await this.getOrNull(`addresses/${address}/utxos?page=${page}&count=100`))) !== null && _a !== void 0 ? _a : [];
|
|
167
|
+
for (const item of items)
|
|
168
|
+
utxos.push(await this.toUtxo(item, item.tx_hash, address));
|
|
169
|
+
if (items.length < 100)
|
|
170
|
+
return utxos;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** UTxOs at `address` holding `unit`. */
|
|
174
|
+
async getAddressAssetUtxos(address, unit) {
|
|
175
|
+
var _a;
|
|
176
|
+
const items = (_a = (await this.getOrNull(`addresses/${address}/utxos/${unit}`))) !== null && _a !== void 0 ? _a : [];
|
|
177
|
+
return Promise.all(items.map((item) => this.toUtxo(item, item.tx_hash, address)));
|
|
178
|
+
}
|
|
179
|
+
/** The UTxO `txHash#index`; throws `already spent` when it has been consumed. */
|
|
180
|
+
async getUtxo(ref) {
|
|
181
|
+
const [txHash, index] = ref.split('#');
|
|
182
|
+
const tx = await this.request(`txs/${txHash}/utxos`);
|
|
183
|
+
const output = tx.outputs.find((o) => o.output_index === Number(index));
|
|
184
|
+
if (!output)
|
|
185
|
+
throw new Error(`${ref} does not exist`);
|
|
186
|
+
if (output.consumed_by_tx)
|
|
187
|
+
throw new Error(`${ref} already spent by ${output.consumed_by_tx}`);
|
|
188
|
+
return this.toUtxo(output, txHash, output.address);
|
|
189
|
+
}
|
|
190
|
+
/** Node (Ogmios) evaluation of every redeemer; `additionalUtxos` supplies inputs not yet on chain. */
|
|
191
|
+
async evaluateTx(cbor, additionalUtxos = []) {
|
|
192
|
+
var _a, _b, _c, _d, _e;
|
|
193
|
+
const response = await this.request('utils/txs/evaluate/utxos', { method: 'POST', body: JSON.stringify({ cbor, additionalUtxoSet: additionalUtxos.map(exports.toOgmiosUtxo) }) });
|
|
194
|
+
const result = (_a = response.result) === null || _a === void 0 ? void 0 : _a.EvaluationResult;
|
|
195
|
+
if (!result)
|
|
196
|
+
throw new BlockfrostEvaluationError((_d = (_c = (_b = response.result) === null || _b === void 0 ? void 0 : _b.EvaluationFailure) !== null && _c !== void 0 ? _c : response.fault) !== null && _d !== void 0 ? _d : response);
|
|
197
|
+
const units = new Map();
|
|
198
|
+
for (const [pointer, { memory, steps }] of Object.entries(result)) {
|
|
199
|
+
const [purpose, index] = pointer.split(':');
|
|
200
|
+
units.set(`${(_e = OGMIOS_PURPOSE[purpose]) !== null && _e !== void 0 ? _e : purpose}:${index}`, { memory, steps });
|
|
201
|
+
}
|
|
202
|
+
return units;
|
|
203
|
+
}
|
|
204
|
+
/** Submit a signed tx; returns its hash. */
|
|
205
|
+
submitTx(cbor) {
|
|
206
|
+
return this.request('tx/submit', { method: 'POST', body: Buffer.from(cbor, 'hex'), contentType: 'application/cbor' });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
exports.BlockfrostTxClient = BlockfrostTxClient;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** A non-negative rational n/d. */
|
|
2
|
+
export interface Ratio {
|
|
3
|
+
n: bigint;
|
|
4
|
+
d: bigint;
|
|
5
|
+
}
|
|
6
|
+
/** Exact rational from a decimal number/string as providers report it (0.0577, "0.0000721", 7.21e-5). */
|
|
7
|
+
export declare const ratio: (value: number | string | bigint) => Ratio;
|
|
8
|
+
export interface FeeParameters {
|
|
9
|
+
/** min_fee_a (lovelace per byte) */
|
|
10
|
+
minFeeA: bigint;
|
|
11
|
+
/** min_fee_b (constant lovelace) */
|
|
12
|
+
minFeeB: bigint;
|
|
13
|
+
priceMemory: Ratio;
|
|
14
|
+
priceSteps: Ratio;
|
|
15
|
+
/** min_fee_ref_script_cost_per_byte (Conway) */
|
|
16
|
+
minFeeRefScriptCostPerByte: Ratio;
|
|
17
|
+
}
|
|
18
|
+
export interface ExUnits {
|
|
19
|
+
memory: number | bigint;
|
|
20
|
+
steps: number | bigint;
|
|
21
|
+
}
|
|
22
|
+
/** ceiling(priceMem * Σmem + priceSteps * Σsteps) — the ledger prices the TOTAL, then rounds once. */
|
|
23
|
+
export declare const scriptExecutionFee: (params: Pick<FeeParameters, "priceMemory" | "priceSteps">, exUnits: ExUnits[]) => bigint;
|
|
24
|
+
/**
|
|
25
|
+
* Conway's tiered reference-script fee: every 25,600 bytes the per-byte price is multiplied by 1.2;
|
|
26
|
+
* the sum is floored. `totalBytes` counts every reference script of every spent AND referenced input.
|
|
27
|
+
*/
|
|
28
|
+
export declare const referenceScriptFee: (costPerByte: Ratio, totalBytes: number | bigint) => bigint;
|
|
29
|
+
/** The ledger minimum fee for a tx of `sizeBytes` (as submitted, witnesses included). */
|
|
30
|
+
export declare const minFee: (params: FeeParameters, sizeBytes: number, exUnits: ExUnits[], referenceScriptBytes: number) => bigint;
|
|
31
|
+
/** Babbage/Conway min-UTxO: (160 + |serialized output|) * coinsPerUtxoByte. */
|
|
32
|
+
export declare const minAdaForOutputSize: (coinsPerUtxoByte: bigint, outputSizeBytes: number) => bigint;
|
package/txBuild/fees.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Exact Conway fee / deposit arithmetic (BigInt rationals — no floating point anywhere a ledger rule
|
|
3
|
+
// uses a ratio). Pure: no network, no cardano-sdk.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.minAdaForOutputSize = exports.minFee = exports.referenceScriptFee = exports.scriptExecutionFee = exports.ratio = void 0;
|
|
6
|
+
/** Exact rational from a decimal number/string as providers report it (0.0577, "0.0000721", 7.21e-5). */
|
|
7
|
+
const ratio = (value) => {
|
|
8
|
+
if (typeof value === 'bigint')
|
|
9
|
+
return { n: value, d: BigInt(1) };
|
|
10
|
+
const text = typeof value === 'number' ? value.toString() : value.trim();
|
|
11
|
+
const match = /^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(text);
|
|
12
|
+
if (!match)
|
|
13
|
+
throw new Error(`Not a non-negative decimal: ${value}`);
|
|
14
|
+
const [, whole, fraction = '', exponent = '0'] = match;
|
|
15
|
+
let n = BigInt(whole + fraction);
|
|
16
|
+
let d = BigInt(10) ** BigInt(fraction.length);
|
|
17
|
+
const exp = Number(exponent);
|
|
18
|
+
if (exp > 0)
|
|
19
|
+
n *= BigInt(10) ** BigInt(exp);
|
|
20
|
+
if (exp < 0)
|
|
21
|
+
d *= BigInt(10) ** BigInt(-exp);
|
|
22
|
+
return { n, d };
|
|
23
|
+
};
|
|
24
|
+
exports.ratio = ratio;
|
|
25
|
+
const ceilDiv = (n, d) => (n + d - BigInt(1)) / d;
|
|
26
|
+
/** ceiling(priceMem * Σmem + priceSteps * Σsteps) — the ledger prices the TOTAL, then rounds once. */
|
|
27
|
+
const scriptExecutionFee = (params, exUnits) => {
|
|
28
|
+
const memory = exUnits.reduce((sum, e) => sum + BigInt(e.memory), BigInt(0));
|
|
29
|
+
const steps = exUnits.reduce((sum, e) => sum + BigInt(e.steps), BigInt(0));
|
|
30
|
+
const { priceMemory: pm, priceSteps: ps } = params;
|
|
31
|
+
return ceilDiv(pm.n * memory * ps.d + ps.n * steps * pm.d, pm.d * ps.d);
|
|
32
|
+
};
|
|
33
|
+
exports.scriptExecutionFee = scriptExecutionFee;
|
|
34
|
+
const REF_SCRIPT_SIZE_INCREMENT = BigInt(25600);
|
|
35
|
+
const REF_SCRIPT_MULTIPLIER = { n: BigInt(6), d: BigInt(5) };
|
|
36
|
+
/**
|
|
37
|
+
* Conway's tiered reference-script fee: every 25,600 bytes the per-byte price is multiplied by 1.2;
|
|
38
|
+
* the sum is floored. `totalBytes` counts every reference script of every spent AND referenced input.
|
|
39
|
+
*/
|
|
40
|
+
const referenceScriptFee = (costPerByte, totalBytes) => {
|
|
41
|
+
let remaining = BigInt(totalBytes);
|
|
42
|
+
let price = costPerByte;
|
|
43
|
+
let acc = { n: BigInt(0), d: BigInt(1) };
|
|
44
|
+
const add = (a, b) => ({ n: a.n * b.d + b.n * a.d, d: a.d * b.d });
|
|
45
|
+
while (remaining >= REF_SCRIPT_SIZE_INCREMENT) {
|
|
46
|
+
acc = add(acc, { n: price.n * REF_SCRIPT_SIZE_INCREMENT, d: price.d });
|
|
47
|
+
price = { n: price.n * REF_SCRIPT_MULTIPLIER.n, d: price.d * REF_SCRIPT_MULTIPLIER.d };
|
|
48
|
+
remaining -= REF_SCRIPT_SIZE_INCREMENT;
|
|
49
|
+
}
|
|
50
|
+
acc = add(acc, { n: price.n * remaining, d: price.d });
|
|
51
|
+
return acc.n / acc.d;
|
|
52
|
+
};
|
|
53
|
+
exports.referenceScriptFee = referenceScriptFee;
|
|
54
|
+
/** The ledger minimum fee for a tx of `sizeBytes` (as submitted, witnesses included). */
|
|
55
|
+
const minFee = (params, sizeBytes, exUnits, referenceScriptBytes) => params.minFeeA * BigInt(sizeBytes) +
|
|
56
|
+
params.minFeeB +
|
|
57
|
+
(0, exports.scriptExecutionFee)(params, exUnits) +
|
|
58
|
+
(0, exports.referenceScriptFee)(params.minFeeRefScriptCostPerByte, referenceScriptBytes);
|
|
59
|
+
exports.minFee = minFee;
|
|
60
|
+
/** Babbage/Conway min-UTxO: (160 + |serialized output|) * coinsPerUtxoByte. */
|
|
61
|
+
const minAdaForOutputSize = (coinsPerUtxoByte, outputSizeBytes) => (BigInt(160) + BigInt(outputSizeBytes)) * coinsPerUtxoByte;
|
|
62
|
+
exports.minAdaForOutputSize = minAdaForOutputSize;
|
package/txBuild/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
// Subpath module: `@koralabs/kora-labs-common/txBuild`. Server-side Plutus transaction building on
|
|
18
|
+
// @cardano-sdk/core (optional peer dependency). NOT re-exported from the root index.
|
|
19
|
+
__exportStar(require("./fees"), exports);
|
|
20
|
+
__exportStar(require("./scriptTx"), exports);
|
|
21
|
+
__exportStar(require("./blockfrost"), exports);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Cardano, Serialization } from '@cardano-sdk/core';
|
|
2
|
+
import { ExUnits, FeeParameters } from './fees';
|
|
3
|
+
export type PlutusData = Serialization.PlutusData;
|
|
4
|
+
export interface ScriptTxProtocolParameters extends FeeParameters {
|
|
5
|
+
coinsPerUtxoByte: bigint;
|
|
6
|
+
maxTxSize: number;
|
|
7
|
+
maxTxExUnits: {
|
|
8
|
+
memory: number;
|
|
9
|
+
steps: number;
|
|
10
|
+
};
|
|
11
|
+
/** Ledger-ordered cost models (Blockfrost `cost_models_raw`), keyed by Plutus language. */
|
|
12
|
+
costModels: Map<Cardano.PlutusLanguageVersion, number[]>;
|
|
13
|
+
}
|
|
14
|
+
export interface ScriptTxPlan {
|
|
15
|
+
/** Every spent input, resolved. A redeemer marks a Plutus script spend. */
|
|
16
|
+
inputs: {
|
|
17
|
+
utxo: Cardano.Utxo;
|
|
18
|
+
redeemer?: PlutusData;
|
|
19
|
+
}[];
|
|
20
|
+
referenceInputs?: Cardano.TxIn[];
|
|
21
|
+
collateral?: Cardano.TxIn[];
|
|
22
|
+
/** Kept in this order. Outputs below min-UTxO are topped up to it. */
|
|
23
|
+
outputs: Cardano.TxOut[];
|
|
24
|
+
/** One entry per policy. A redeemer marks a Plutus policy (native policies need `nativeScripts`). */
|
|
25
|
+
mint?: {
|
|
26
|
+
policyId: string;
|
|
27
|
+
assets: Map<string, bigint>;
|
|
28
|
+
redeemer?: PlutusData;
|
|
29
|
+
}[];
|
|
30
|
+
withdrawals?: {
|
|
31
|
+
rewardAccount: string;
|
|
32
|
+
quantity: bigint;
|
|
33
|
+
redeemer?: PlutusData;
|
|
34
|
+
}[];
|
|
35
|
+
requiredSigners?: string[];
|
|
36
|
+
nativeScripts?: Cardano.NativeScript[];
|
|
37
|
+
validityInterval?: Cardano.ValidityInterval;
|
|
38
|
+
changeAddress: string;
|
|
39
|
+
/** vkey witnesses the signed tx will carry (sizes the fee). */
|
|
40
|
+
signerCount: number;
|
|
41
|
+
/** Languages of every Plutus script the tx runs (from their reference scripts). */
|
|
42
|
+
plutusLanguages: Cardano.PlutusLanguageVersion[];
|
|
43
|
+
/** Σ size of every reference script on spent + referenced inputs (Conway ref-script fee). */
|
|
44
|
+
referenceScriptBytes: number;
|
|
45
|
+
}
|
|
46
|
+
/** Redeemer pointer key: `${purpose}:${index}` (e.g. `spend:1`, `mint:0`, `withdrawal:0`). */
|
|
47
|
+
export type RedeemerKey = string;
|
|
48
|
+
export type Evaluator = (unsignedTxCbor: string) => Promise<Map<RedeemerKey, ExUnits>>;
|
|
49
|
+
export interface FinalizedScriptTx {
|
|
50
|
+
/** Unsigned tx CBOR (empty vkey witness set). */
|
|
51
|
+
cbor: string;
|
|
52
|
+
txId: string;
|
|
53
|
+
fee: bigint;
|
|
54
|
+
/** Final outputs, change last. */
|
|
55
|
+
outputs: Cardano.TxOut[];
|
|
56
|
+
redeemers: Cardano.Redeemer[];
|
|
57
|
+
}
|
|
58
|
+
/** Raise `output` to its min-UTxO (the coin's own CBOR width is part of the size, so iterate). */
|
|
59
|
+
export declare const withMinAda: (output: Cardano.TxOut, coinsPerUtxoByte: bigint) => Cardano.TxOut;
|
|
60
|
+
/**
|
|
61
|
+
* script_data_hash = blake2b-256(redeemers ‖ datums? ‖ language_views), over the exact bytes of the
|
|
62
|
+
* serialized witness-set fields. `datumsCbor` is only for witness-set datums (never inline datums).
|
|
63
|
+
*/
|
|
64
|
+
export declare const computeScriptDataHash: ({ redeemersCbor, datumsCbor, costModels, languages }: {
|
|
65
|
+
redeemersCbor?: string;
|
|
66
|
+
datumsCbor?: string;
|
|
67
|
+
costModels: Map<Cardano.PlutusLanguageVersion, number[]>;
|
|
68
|
+
languages: Cardano.PlutusLanguageVersion[];
|
|
69
|
+
}) => string | undefined;
|
|
70
|
+
export declare const finalizeScriptTx: (plan: ScriptTxPlan, params: ScriptTxProtocolParameters, evaluate: Evaluator) => Promise<FinalizedScriptTx>;
|
|
71
|
+
/**
|
|
72
|
+
* Add vkey witnesses to an unsigned tx by splicing them into its witness set byte-for-byte
|
|
73
|
+
* (`mergeWitnessSet`): the body (so the tx id and every signature) and the redeemer bytes the
|
|
74
|
+
* script_data_hash covers are untouched.
|
|
75
|
+
*/
|
|
76
|
+
export declare const addVkeyWitnesses: (unsignedCbor: string, witnesses: {
|
|
77
|
+
vkey: string;
|
|
78
|
+
signature: string;
|
|
79
|
+
}[]) => string;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addVkeyWitnesses = exports.finalizeScriptTx = exports.computeScriptDataHash = exports.withMinAda = void 0;
|
|
4
|
+
// Deterministic Plutus transaction finalizer on @cardano-sdk/core (no input selection, no Helios).
|
|
5
|
+
//
|
|
6
|
+
// The caller fixes every input; this module derives redeemer pointers from the ledger's canonical
|
|
7
|
+
// orderings, tops outputs up to min-UTxO, asks the NODE (via `evaluate`) for each redeemer's
|
|
8
|
+
// execution units, converges the fee to the exact ledger minimum, puts the remainder in one change
|
|
9
|
+
// output, and computes script_data_hash over the exact redeemer bytes it serializes.
|
|
10
|
+
//
|
|
11
|
+
// Lessons baked in (handle.me docs/spec/live-tx-lessons-learned.md):
|
|
12
|
+
// - Conway redeemers are map-encoded; script_data_hash must hash the bytes actually shipped (#1).
|
|
13
|
+
// - inline datums never enter script_data_hash (#2).
|
|
14
|
+
// - reference inputs / inputs / policies / withdrawals are indexed in ledger-sorted order (#3).
|
|
15
|
+
// - execution units come from the node's evaluator, re-checked after the fee settles (the fee and
|
|
16
|
+
// change are part of the ScriptContext, so a fee change can change the cost).
|
|
17
|
+
const core_1 = require("@cardano-sdk/core");
|
|
18
|
+
const blakejs_1 = require("blakejs");
|
|
19
|
+
const tx_1 = require("../tx");
|
|
20
|
+
const fees_1 = require("./fees");
|
|
21
|
+
(0, core_1.setInConwayEra)(true);
|
|
22
|
+
const byTxIn = (a, b) => (a.txId === b.txId ? a.index - b.index : a.txId < b.txId ? -1 : 1);
|
|
23
|
+
const hexCompare = (a, b) => (a === b ? 0 : a < b ? -1 : 1);
|
|
24
|
+
const rewardAccountBytes = (account) => core_1.Cardano.Address.fromBech32(account).toBytes();
|
|
25
|
+
const outputSize = (output) => core_1.Serialization.TransactionOutput.fromCore(output).toCbor().length / 2;
|
|
26
|
+
/** Raise `output` to its min-UTxO (the coin's own CBOR width is part of the size, so iterate). */
|
|
27
|
+
const withMinAda = (output, coinsPerUtxoByte) => {
|
|
28
|
+
let current = output;
|
|
29
|
+
for (let i = 0; i < 5; i++) {
|
|
30
|
+
const required = (0, fees_1.minAdaForOutputSize)(coinsPerUtxoByte, outputSize(current));
|
|
31
|
+
if (current.value.coins >= required)
|
|
32
|
+
return current;
|
|
33
|
+
current = { ...current, value: { ...current.value, coins: required } };
|
|
34
|
+
}
|
|
35
|
+
throw new Error('min-UTxO did not converge');
|
|
36
|
+
};
|
|
37
|
+
exports.withMinAda = withMinAda;
|
|
38
|
+
/**
|
|
39
|
+
* script_data_hash = blake2b-256(redeemers ‖ datums? ‖ language_views), over the exact bytes of the
|
|
40
|
+
* serialized witness-set fields. `datumsCbor` is only for witness-set datums (never inline datums).
|
|
41
|
+
*/
|
|
42
|
+
const computeScriptDataHash = ({ redeemersCbor, datumsCbor, costModels, languages }) => {
|
|
43
|
+
if (!redeemersCbor && !datumsCbor)
|
|
44
|
+
return undefined;
|
|
45
|
+
const views = new core_1.Serialization.Costmdls();
|
|
46
|
+
for (const language of new Set(languages)) {
|
|
47
|
+
const model = costModels.get(language);
|
|
48
|
+
if (!model)
|
|
49
|
+
throw new Error(`No cost model for Plutus language ${language}`);
|
|
50
|
+
views.insert(new core_1.Serialization.CostModel(language, model));
|
|
51
|
+
}
|
|
52
|
+
const EMPTY_MAP = 'a0';
|
|
53
|
+
const preimage = redeemersCbor ? redeemersCbor + (datumsCbor !== null && datumsCbor !== void 0 ? datumsCbor : '') + views.languageViewsEncoding() : EMPTY_MAP + datumsCbor + EMPTY_MAP;
|
|
54
|
+
return (0, blakejs_1.blake2bHex)(Buffer.from(preimage, 'hex'), undefined, 32);
|
|
55
|
+
};
|
|
56
|
+
exports.computeScriptDataHash = computeScriptDataHash;
|
|
57
|
+
const placeholderSignatures = (count) => {
|
|
58
|
+
const signatures = new Map();
|
|
59
|
+
for (let i = 0; i < count; i++)
|
|
60
|
+
signatures.set((i + 1).toString(16).padStart(64, '0'), '0'.repeat(128));
|
|
61
|
+
return signatures;
|
|
62
|
+
};
|
|
63
|
+
const sumValues = (values) => {
|
|
64
|
+
var _a, _b;
|
|
65
|
+
let coins = BigInt(0);
|
|
66
|
+
const assets = new Map();
|
|
67
|
+
for (const value of values) {
|
|
68
|
+
coins += value.coins;
|
|
69
|
+
for (const [id, qty] of (_a = value.assets) !== null && _a !== void 0 ? _a : [])
|
|
70
|
+
assets.set(id, ((_b = assets.get(id)) !== null && _b !== void 0 ? _b : BigInt(0)) + qty);
|
|
71
|
+
}
|
|
72
|
+
return { coins, assets };
|
|
73
|
+
};
|
|
74
|
+
const finalizeScriptTx = async (plan, params, evaluate) => {
|
|
75
|
+
var _a, _b;
|
|
76
|
+
const inputs = [...plan.inputs].sort((a, b) => byTxIn(a.utxo[0], b.utxo[0]));
|
|
77
|
+
const mint = [...((_a = plan.mint) !== null && _a !== void 0 ? _a : [])].sort((a, b) => hexCompare(a.policyId, b.policyId));
|
|
78
|
+
const withdrawals = [...((_b = plan.withdrawals) !== null && _b !== void 0 ? _b : [])].sort((a, b) => hexCompare(rewardAccountBytes(a.rewardAccount), rewardAccountBytes(b.rewardAccount)));
|
|
79
|
+
const outputs = plan.outputs.map((output) => (0, exports.withMinAda)(output, params.coinsPerUtxoByte));
|
|
80
|
+
const pointers = [
|
|
81
|
+
...inputs.flatMap(({ redeemer }, index) => (redeemer ? [{ purpose: core_1.Cardano.RedeemerPurpose.spend, index, data: redeemer }] : [])),
|
|
82
|
+
...mint.flatMap(({ redeemer }, index) => (redeemer ? [{ purpose: core_1.Cardano.RedeemerPurpose.mint, index, data: redeemer }] : [])),
|
|
83
|
+
...withdrawals.flatMap(({ redeemer }, index) => (redeemer ? [{ purpose: core_1.Cardano.RedeemerPurpose.withdrawal, index, data: redeemer }] : []))
|
|
84
|
+
];
|
|
85
|
+
const key = (purpose, index) => `${purpose}:${index}`;
|
|
86
|
+
const tokenMap = new Map();
|
|
87
|
+
for (const { policyId, assets } of mint) {
|
|
88
|
+
for (const [name, qty] of assets)
|
|
89
|
+
tokenMap.set(core_1.Cardano.AssetId.fromParts(core_1.Cardano.PolicyId(policyId), core_1.Cardano.AssetName(name)), qty);
|
|
90
|
+
}
|
|
91
|
+
const inValue = sumValues(inputs.map(({ utxo }) => utxo[1].value));
|
|
92
|
+
const withdrawn = withdrawals.reduce((sum, w) => sum + w.quantity, BigInt(0));
|
|
93
|
+
const produced = sumValues(outputs.map((o) => o.value));
|
|
94
|
+
const changeFor = (fee) => {
|
|
95
|
+
var _a, _b, _c;
|
|
96
|
+
const coins = inValue.coins + withdrawn - produced.coins - fee;
|
|
97
|
+
if (coins < BigInt(0))
|
|
98
|
+
throw new Error(`Inputs do not cover outputs + fee: ${-coins} lovelace short`);
|
|
99
|
+
const assets = new Map();
|
|
100
|
+
const ids = new Set([...inValue.assets.keys(), ...tokenMap.keys(), ...produced.assets.keys()]);
|
|
101
|
+
for (const id of ids) {
|
|
102
|
+
const qty = ((_a = inValue.assets.get(id)) !== null && _a !== void 0 ? _a : BigInt(0)) + ((_b = tokenMap.get(id)) !== null && _b !== void 0 ? _b : BigInt(0)) - ((_c = produced.assets.get(id)) !== null && _c !== void 0 ? _c : BigInt(0));
|
|
103
|
+
if (qty < BigInt(0))
|
|
104
|
+
throw new Error(`Outputs carry more ${id} than the inputs and mint provide`);
|
|
105
|
+
if (qty > BigInt(0))
|
|
106
|
+
assets.set(id, qty);
|
|
107
|
+
}
|
|
108
|
+
return { address: plan.changeAddress, value: { coins, ...(assets.size ? { assets } : {}) } };
|
|
109
|
+
};
|
|
110
|
+
const assemble = (fee, exUnits, signatures) => {
|
|
111
|
+
var _a, _b, _c, _d;
|
|
112
|
+
const change = changeFor(fee);
|
|
113
|
+
const redeemers = pointers.map(({ purpose, index, data }) => {
|
|
114
|
+
const units = exUnits.get(key(purpose, index));
|
|
115
|
+
if (!units)
|
|
116
|
+
throw new Error(`No execution units for redeemer ${key(purpose, index)}`);
|
|
117
|
+
return { purpose, index, data: data.toCore(), executionUnits: { memory: Number(units.memory), steps: Number(units.steps) } };
|
|
118
|
+
});
|
|
119
|
+
const redeemersCbor = redeemers.length ? core_1.Serialization.Redeemers.fromCore(redeemers).toCbor() : undefined;
|
|
120
|
+
const scriptIntegrityHash = (0, exports.computeScriptDataHash)({ redeemersCbor, costModels: params.costModels, languages: plan.plutusLanguages });
|
|
121
|
+
const body = {
|
|
122
|
+
inputs: inputs.map(({ utxo: [txIn] }) => ({ txId: txIn.txId, index: txIn.index })),
|
|
123
|
+
outputs: [...outputs, change],
|
|
124
|
+
fee,
|
|
125
|
+
...(((_a = plan.referenceInputs) === null || _a === void 0 ? void 0 : _a.length) ? { referenceInputs: [...plan.referenceInputs].sort(byTxIn) } : {}),
|
|
126
|
+
...(((_b = plan.collateral) === null || _b === void 0 ? void 0 : _b.length) ? { collaterals: [...plan.collateral].sort(byTxIn) } : {}),
|
|
127
|
+
...(tokenMap.size ? { mint: tokenMap } : {}),
|
|
128
|
+
...(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) } : {}),
|
|
130
|
+
...(plan.validityInterval ? { validityInterval: plan.validityInterval } : {}),
|
|
131
|
+
...(scriptIntegrityHash ? { scriptIntegrityHash: scriptIntegrityHash } : {})
|
|
132
|
+
};
|
|
133
|
+
const tx = core_1.Serialization.Transaction.fromCore({
|
|
134
|
+
id: '0'.repeat(64),
|
|
135
|
+
body,
|
|
136
|
+
witness: {
|
|
137
|
+
signatures: signatures,
|
|
138
|
+
...(redeemers.length ? { redeemers } : {}),
|
|
139
|
+
...(((_d = plan.nativeScripts) === null || _d === void 0 ? void 0 : _d.length) ? { scripts: plan.nativeScripts } : {})
|
|
140
|
+
},
|
|
141
|
+
isValid: true
|
|
142
|
+
});
|
|
143
|
+
return { tx, change, redeemers };
|
|
144
|
+
};
|
|
145
|
+
// Evaluation budget placeholder: the node evaluates against the protocol maximum, not the declared units.
|
|
146
|
+
const share = Math.max(1, pointers.length);
|
|
147
|
+
const placeholder = new Map(pointers.map(({ purpose, index }) => [key(purpose, index), {
|
|
148
|
+
memory: Math.floor(params.maxTxExUnits.memory / share),
|
|
149
|
+
steps: Math.floor(params.maxTxExUnits.steps / share)
|
|
150
|
+
}]));
|
|
151
|
+
const sizeOf = (fee, exUnits) => assemble(fee, exUnits, placeholderSignatures(plan.signerCount)).tx.toCbor().length / 2;
|
|
152
|
+
let exUnits = pointers.length ? await evaluate(assemble(BigInt(0), placeholder, new Map()).tx.toCbor()) : new Map();
|
|
153
|
+
let fee = BigInt(0);
|
|
154
|
+
for (let round = 0; round < 6; round++) {
|
|
155
|
+
// Converge the fee for these units: the fee's own CBOR width changes the size it pays for.
|
|
156
|
+
for (let i = 0; i < 6; i++) {
|
|
157
|
+
const needed = (0, fees_1.minFee)(params, sizeOf(fee, exUnits), [...exUnits.values()], plan.referenceScriptBytes);
|
|
158
|
+
if (needed <= fee)
|
|
159
|
+
break;
|
|
160
|
+
fee = needed;
|
|
161
|
+
}
|
|
162
|
+
if (!pointers.length)
|
|
163
|
+
break;
|
|
164
|
+
// The fee and change are part of the ScriptContext: re-evaluate the tx we would actually submit.
|
|
165
|
+
const reevaluated = await evaluate(assemble(fee, exUnits, new Map()).tx.toCbor());
|
|
166
|
+
const unchanged = [...reevaluated].every(([k, u]) => {
|
|
167
|
+
const prior = exUnits.get(k);
|
|
168
|
+
return prior && BigInt(prior.memory) === BigInt(u.memory) && BigInt(prior.steps) === BigInt(u.steps);
|
|
169
|
+
});
|
|
170
|
+
exUnits = reevaluated;
|
|
171
|
+
if (unchanged && (0, fees_1.minFee)(params, sizeOf(fee, exUnits), [...exUnits.values()], plan.referenceScriptBytes) <= fee)
|
|
172
|
+
break;
|
|
173
|
+
if (round === 5)
|
|
174
|
+
throw new Error('Execution units did not converge');
|
|
175
|
+
}
|
|
176
|
+
const { tx, change, redeemers } = assemble(fee, exUnits, new Map());
|
|
177
|
+
if (change.value.coins < (0, fees_1.minAdaForOutputSize)(params.coinsPerUtxoByte, outputSize(change))) {
|
|
178
|
+
throw new Error(`Inputs do not cover outputs + fee: change of ${change.value.coins} lovelace is below min-UTxO`);
|
|
179
|
+
}
|
|
180
|
+
const signedSize = sizeOf(fee, exUnits);
|
|
181
|
+
if (signedSize > params.maxTxSize)
|
|
182
|
+
throw new Error(`Transaction is ${signedSize} bytes, above the ${params.maxTxSize} byte limit`);
|
|
183
|
+
return { cbor: tx.toCbor(), txId: tx.getId(), fee, outputs: [...outputs, change], redeemers };
|
|
184
|
+
};
|
|
185
|
+
exports.finalizeScriptTx = finalizeScriptTx;
|
|
186
|
+
/**
|
|
187
|
+
* Add vkey witnesses to an unsigned tx by splicing them into its witness set byte-for-byte
|
|
188
|
+
* (`mergeWitnessSet`): the body (so the tx id and every signature) and the redeemer bytes the
|
|
189
|
+
* script_data_hash covers are untouched.
|
|
190
|
+
*/
|
|
191
|
+
const addVkeyWitnesses = (unsignedCbor, witnesses) => {
|
|
192
|
+
const witnessSet = core_1.Serialization.TransactionWitnessSet.fromCore({
|
|
193
|
+
signatures: new Map(witnesses.map(({ vkey, signature }) => [vkey, signature]))
|
|
194
|
+
});
|
|
195
|
+
return (0, tx_1.mergeWitnessSet)(unsignedCbor, witnessSet.toCbor());
|
|
196
|
+
};
|
|
197
|
+
exports.addVkeyWitnesses = addVkeyWitnesses;
|
package/utils/cbor/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as cbor from 'cbor';
|
|
1
2
|
export { designerSchema } from './schema/designer';
|
|
2
3
|
export { handleDatumSchema } from './schema/handleData';
|
|
3
4
|
export { marketplaceDatumSchema } from './schema/marketplaceDatum';
|
|
@@ -52,6 +53,7 @@ export declare class DupeKey extends JsonToDatumObject {
|
|
|
52
53
|
constructor(key: string);
|
|
53
54
|
}
|
|
54
55
|
export declare const encodeJsonToDatum: (json: any, options?: EncodeOptions) => Promise<string>;
|
|
56
|
+
export declare const constrToTagged: (index: number, fields: unknown) => cbor.Tagged;
|
|
55
57
|
export declare const decodeCborToJson: ({ cborString, schema, defaultKeyType, forJson }: {
|
|
56
58
|
cborString: string;
|
|
57
59
|
schema?: any;
|