@easy1staking/cip113-sdk-ts 0.7.0 → 0.9.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/README.md +72 -2
- package/blueprints/standard/v0.5.0-alpha.3/UPSTREAM_PIN.json +35 -0
- package/blueprints/standard/v0.5.0-alpha.3/plutus.json +1126 -0
- package/blueprints/standard/v0.5.0-alpha.4/UPSTREAM_PIN.json +36 -0
- package/blueprints/standard/v0.5.0-alpha.4/plutus.json +1434 -0
- package/dist/core/evo-utils.d.ts +357 -39
- package/dist/core/evo-utils.d.ts.map +1 -1
- package/dist/core/evo-utils.js +513 -27
- package/dist/core/evo-utils.js.map +1 -1
- package/dist/core/ledger-order.d.ts +204 -18
- package/dist/core/ledger-order.d.ts.map +1 -1
- package/dist/core/ledger-order.js +385 -28
- package/dist/core/ledger-order.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/standard/blueprint.d.ts +128 -29
- package/dist/standard/blueprint.d.ts.map +1 -1
- package/dist/standard/blueprint.js +244 -47
- package/dist/standard/blueprint.js.map +1 -1
- package/dist/standard/scripts.d.ts +143 -75
- package/dist/standard/scripts.d.ts.map +1 -1
- package/dist/standard/scripts.js +211 -116
- package/dist/standard/scripts.js.map +1 -1
- package/dist/substandards/dummy/index.d.ts +19 -0
- package/dist/substandards/dummy/index.d.ts.map +1 -1
- package/dist/substandards/dummy/index.js +322 -73
- package/dist/substandards/dummy/index.js.map +1 -1
- package/dist/substandards/freeze-and-seize/index.d.ts.map +1 -1
- package/dist/substandards/freeze-and-seize/index.js +455 -117
- package/dist/substandards/freeze-and-seize/index.js.map +1 -1
- package/dist/types.d.ts +131 -48
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/core/evo-utils.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* These are used across the SDK to build scripts, addresses, datums, and
|
|
5
5
|
* perform common conversions. All use Evolution SDK types directly.
|
|
6
6
|
*/
|
|
7
|
-
import { Data, Bytes, ScriptHash as EvoScriptHash, Script, UPLC, Address as EvoAddress, AddressEras, BaseAddress, EnterpriseAddress, RewardAccount, Assets, Credential, InlineDatum, KeyHash, TransactionHash, Transaction, } from "@evolution-sdk/evolution";
|
|
7
|
+
import { Data, Bytes, ScriptHash as EvoScriptHash, Script, UPLC, Address as EvoAddress, AddressEras, BaseAddress, EnterpriseAddress, RewardAccount, Assets, Credential, InlineDatum, KeyHash, TransactionHash, Transaction, TxOut, } from "@evolution-sdk/evolution";
|
|
8
8
|
import * as Label from "@evolution-sdk/evolution/Label";
|
|
9
9
|
const PlutusV3 = Script.Script.members[3];
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
@@ -261,43 +261,162 @@ export function decodeRegistryNode(d) {
|
|
|
261
261
|
* CHANGED. Deliberately generous: min-UTxO also moves with protocol parameters.
|
|
262
262
|
*/
|
|
263
263
|
export const REGISTRY_NODE_MIN_ADA = 3000000n;
|
|
264
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* The ledger's per-UTxO byte overhead, from the Babbage min-UTxO rule
|
|
266
|
+
* (`utxoEntrySize` = serialised output size + a fixed constant). Evolution uses
|
|
267
|
+
* the same value internally; it is restated here rather than imported because
|
|
268
|
+
* its home is `sdk/builders/internal/`, and this package does not reach into a
|
|
269
|
+
* dependency's internals.
|
|
270
|
+
*/
|
|
271
|
+
const UTXO_ENTRY_OVERHEAD_BYTES = 160n;
|
|
272
|
+
/**
|
|
273
|
+
* The minimum lovelace an output must carry, computed from PROTOCOL PARAMETERS
|
|
274
|
+
* and the output's own serialised size.
|
|
275
|
+
*
|
|
276
|
+
* ⚠ WHY THIS EXISTS RATHER THAN ANOTHER CONSTANT. min-UTxO scales with
|
|
277
|
+
* SERIALISED OUTPUT SIZE, and three things that scale it are supplied by the
|
|
278
|
+
* CALLER, not by us:
|
|
279
|
+
* - the CIP-68 metadata datum (name/description/ticker/url/logo). This
|
|
280
|
+
* package imposes no length caps at all, so the datum is UNBOUNDED.
|
|
281
|
+
* - the ASSET NAME. CIP-67-labelled names run to 32 bytes, and this package's
|
|
282
|
+
* API takes raw hex names at every boundary.
|
|
283
|
+
* - the QUANTITY, which is a CBOR integer and widens with magnitude.
|
|
284
|
+
*
|
|
285
|
+
* ⛔ AND EVOLUTION DOES NOT RESCUE AN UNDER-FUNDED OUTPUT. MEASURED on preview
|
|
286
|
+
* (2026-09-01): `calculateMinimumUtxoLovelace` is applied ONLY to CHANGE and
|
|
287
|
+
* unfracking outputs. An explicit `payToAddress` amount is passed through
|
|
288
|
+
* verbatim — a build with a deliberately short datum-bearing output produced a
|
|
289
|
+
* transaction carrying exactly the requested lovelace. The shortfall therefore
|
|
290
|
+
* survives to submission, where the ledger rejects it as "insufficient Ada"
|
|
291
|
+
* with a number and NEVER as "your datum grew".
|
|
292
|
+
*
|
|
293
|
+
* A flat constant cannot be right for an input the caller controls. This is the
|
|
294
|
+
* successor to {@link REGISTRY_NODE_MIN_ADA}'s "deliberately generous" habit:
|
|
295
|
+
* generous is a guess, and it was already wrong at 3 ADA for a CIP-68 datum
|
|
296
|
+
* whose fields sit within a consumer's own documented caps.
|
|
297
|
+
*
|
|
298
|
+
* Built from PUBLIC Evolution API only, and solved as a fixed point because the
|
|
299
|
+
* lovelace figure is itself part of what gets serialised.
|
|
300
|
+
*/
|
|
301
|
+
export function minUtxoForOutput(params) {
|
|
302
|
+
const address = EvoAddress.fromBech32(params.address);
|
|
303
|
+
const datumOption = params.datum
|
|
304
|
+
? new InlineDatum.InlineDatum({ data: params.datum })
|
|
305
|
+
: undefined;
|
|
306
|
+
const required = (lovelace) => {
|
|
307
|
+
const output = new TxOut.TransactionOutput({
|
|
308
|
+
address,
|
|
309
|
+
assets: Assets.withLovelace(params.assets, lovelace),
|
|
310
|
+
datumOption,
|
|
311
|
+
});
|
|
312
|
+
const size = BigInt(TxOut.toCBORBytes(output).length);
|
|
313
|
+
return params.coinsPerUtxoByte * (UTXO_ENTRY_OVERHEAD_BYTES + size);
|
|
314
|
+
};
|
|
315
|
+
// Writing a larger number widens the CBOR, which raises the requirement. Two
|
|
316
|
+
// or three rounds converge; the cap only stops a pathological non-convergence
|
|
317
|
+
// from hanging a transaction build.
|
|
318
|
+
let current = 0n;
|
|
319
|
+
for (let i = 0; i < 10; i++) {
|
|
320
|
+
const next = required(current);
|
|
321
|
+
if (next === current)
|
|
322
|
+
return next;
|
|
323
|
+
current = next;
|
|
324
|
+
}
|
|
325
|
+
throw new Error(`min-UTxO did not converge after 10 iterations (last ${current} lovelace). ` +
|
|
326
|
+
`This should not happen for a well-formed output; report it with the datum.`);
|
|
327
|
+
}
|
|
328
|
+
/** Round up to a whole ADA. */
|
|
329
|
+
export function ceilToWholeAda(lovelace) {
|
|
330
|
+
return ((lovelace + 999999n) / 1000000n) * 1000000n;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* min-UTxO for an output, never returning less than `floor`.
|
|
334
|
+
*
|
|
335
|
+
* The floor keeps this change MONOTONE: every value this package used to emit
|
|
336
|
+
* is preserved for ordinary inputs, and the figure only ever rises — where it
|
|
337
|
+
* had to. A fix that lowered an amount would be a behaviour change smuggled in
|
|
338
|
+
* beside a bug fix.
|
|
339
|
+
*/
|
|
340
|
+
export function minUtxoAtLeast(floor, params) {
|
|
341
|
+
const computed = minUtxoForOutput(params);
|
|
342
|
+
return computed > floor ? computed : floor;
|
|
343
|
+
}
|
|
344
|
+
/** Build the protocol-params datum. Field order is the on-chain contract. */
|
|
265
345
|
export function protocolParamsDatum(p) {
|
|
346
|
+
// `Option<Credential>`: Some(cred) = Constr(0, [Credential]), None = Constr(1, []).
|
|
347
|
+
const pending = p.pendingUpgradeCred == null
|
|
348
|
+
? Data.constr(1n, [])
|
|
349
|
+
: Data.constr(0n, [credToData(p.pendingUpgradeCred)]);
|
|
266
350
|
return Data.constr(0n, [
|
|
267
|
-
|
|
268
|
-
credToData(p.
|
|
351
|
+
credToData(p.plgCred),
|
|
352
|
+
credToData(p.issuanceLogicCred),
|
|
269
353
|
credToData(p.transferCred),
|
|
270
354
|
credToData(p.thirdPartyCred),
|
|
271
|
-
credToData(p.unfrackingCred),
|
|
272
355
|
credToData(p.upgradeCred),
|
|
273
|
-
|
|
356
|
+
pending,
|
|
274
357
|
]);
|
|
275
358
|
}
|
|
276
|
-
/**
|
|
359
|
+
/** Decode index 5's `Option<Credential>`. */
|
|
360
|
+
function dataToPendingCred(d) {
|
|
361
|
+
if (d instanceof Data.Constr) {
|
|
362
|
+
if (d.index === 1n && d.fields.length === 0)
|
|
363
|
+
return null;
|
|
364
|
+
if (d.index === 0n && d.fields.length === 1) {
|
|
365
|
+
return dataToCred(d.fields[0], "ProtocolParams.pendingUpgradeCred");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
throw new Error(`ProtocolParams.pendingUpgradeCred: field 5 is Option<Credential> — ` +
|
|
369
|
+
`Constr(0, [Credential]) for a standing nomination, Constr(1, []) for none. ` +
|
|
370
|
+
`pending_upgrade_cred was added in 0.5.0-alpha.4; a datum without it is not a ` +
|
|
371
|
+
`six-field datum at all.`);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Parse the protocol-params datum. STRICT on arity — see the block above for
|
|
375
|
+
* why that strictness is load-bearing rather than defensive, AND for the one
|
|
376
|
+
* misread it CANNOT catch.
|
|
377
|
+
*/
|
|
277
378
|
export function decodeProtocolParams(d) {
|
|
278
379
|
if (!(d instanceof Data.Constr) || d.index !== 0n) {
|
|
279
380
|
throw new Error("ProgrammableLogicGlobalParams: expected Constr(0, ...)");
|
|
280
381
|
}
|
|
281
|
-
if (d.fields.length !==
|
|
282
|
-
throw new Error(`ProgrammableLogicGlobalParams: expected exactly
|
|
283
|
-
`
|
|
284
|
-
`
|
|
382
|
+
if (d.fields.length !== 6) {
|
|
383
|
+
throw new Error(`ProgrammableLogicGlobalParams: expected exactly 6 fields, got ${d.fields.length}. ` +
|
|
384
|
+
`alpha.4 INSERTED issuance_logic_cred at index 1 — it was not appended — so ` +
|
|
385
|
+
`transfer_cred, which was index 1 in alpha.3, is now index 2. Both are Credentials, ` +
|
|
386
|
+
`so a shifted positional read returns a well-formed value naming the wrong authority. ` +
|
|
387
|
+
`A 4-field datum belongs to a 0.5.0-alpha.3 protocol instance and a 7-field one to ` +
|
|
388
|
+
`0.5.0-alpha.2; point at that instance's SDK, do not relax this check.`);
|
|
285
389
|
}
|
|
286
390
|
const f = d.fields;
|
|
287
|
-
const n = f[6];
|
|
288
|
-
if (typeof n !== "bigint") {
|
|
289
|
-
throw new Error("ProgrammableLogicGlobalParams.maxInlineDatumBytes: expected an integer");
|
|
290
|
-
}
|
|
291
391
|
return {
|
|
292
|
-
|
|
293
|
-
|
|
392
|
+
plgCred: dataToCred(f[0], "ProtocolParams.plgCred"),
|
|
393
|
+
issuanceLogicCred: dataToCred(f[1], "ProtocolParams.issuanceLogicCred"),
|
|
294
394
|
transferCred: dataToCred(f[2], "ProtocolParams.transferCred"),
|
|
295
395
|
thirdPartyCred: dataToCred(f[3], "ProtocolParams.thirdPartyCred"),
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
maxInlineDatumBytes: n,
|
|
396
|
+
upgradeCred: dataToCred(f[4], "ProtocolParams.upgradeCred"),
|
|
397
|
+
pendingUpgradeCred: dataToPendingCred(f[5]),
|
|
299
398
|
};
|
|
300
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Verify that the deployment record and live protocol-params datum name the
|
|
402
|
+
* same issuance-logic script. They are independent sources: the record drives
|
|
403
|
+
* the withdrawal emitted here, while the datum drives the permanent issuance
|
|
404
|
+
* policy on chain.
|
|
405
|
+
*/
|
|
406
|
+
export function assertProtocolParamsIssuanceLogic(utxo, expectedScriptHash) {
|
|
407
|
+
const datum = getInlineDatum(utxo);
|
|
408
|
+
if (datum === undefined) {
|
|
409
|
+
throw new Error(`Protocol params UTxO has no inline datum; cannot verify deployment ` +
|
|
410
|
+
`issuanceLogic.scriptHash ${expectedScriptHash}.`);
|
|
411
|
+
}
|
|
412
|
+
const actual = decodeProtocolParams(datum).issuanceLogicCred;
|
|
413
|
+
if (actual.type !== "script" || actual.hash.toLowerCase() !== expectedScriptHash.toLowerCase()) {
|
|
414
|
+
throw new Error(`Deployment issuanceLogic.scriptHash ${expectedScriptHash} does not match the live ` +
|
|
415
|
+
`protocol-params datum's issuance_logic_cred ${actual.type}/${actual.hash}. ` +
|
|
416
|
+
`Refusing before transaction construction; use the DeploymentParams recorded for ` +
|
|
417
|
+
`this protocol instance.`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
301
420
|
/** Build a BlacklistNode datum */
|
|
302
421
|
export function blacklistNodeDatum(key, next) {
|
|
303
422
|
return Data.constr(0n, [Data.bytearray(key), Data.bytearray(next)]);
|
|
@@ -306,25 +425,172 @@ export function blacklistNodeDatum(key, next) {
|
|
|
306
425
|
// Redeemer builders (CIP-113 validators)
|
|
307
426
|
// ---------------------------------------------------------------------------
|
|
308
427
|
/**
|
|
309
|
-
* `
|
|
310
|
-
*
|
|
311
|
-
* Upstream #68 REMOVED the `SmartTokenMintingAction { minting_logic_cred,
|
|
312
|
-
* minting_registry_proof }` wrapper these builders used to emit. The redeemer is
|
|
313
|
-
* now the proof itself, and the minting-logic credential is no longer carried in
|
|
314
|
-
* it at all — it comes from the registry node.
|
|
428
|
+
* A `MintingRegistryProof` — where the token's registry node is in this
|
|
429
|
+
* transaction.
|
|
315
430
|
*
|
|
316
431
|
* ctor 0 RefInput { index } — the registry node is a REFERENCE input
|
|
317
432
|
* ctor 1 OutputIndex { index } — the registry node is an OUTPUT of this tx
|
|
318
433
|
* (registration and first mint in one)
|
|
319
434
|
*
|
|
320
|
-
*
|
|
435
|
+
* ⛔ THIS IS NO LONGER `issuance_mint`'s REDEEMER, AND THE OLD COMMENT HERE WAS
|
|
436
|
+
* AN INSTRUCTION FOR BUILDING A TRANSACTION THE LEDGER REFUSES. In 0.5.0-alpha.3
|
|
437
|
+
* `issuance_mint` took a BARE `MintingRegistryProof`. In alpha.4 issuance was
|
|
438
|
+
* split (upstream #129): the permanent per-token policy's redeemer is now
|
|
439
|
+
* `IssuanceRedeemer { params_idx }` — see {@link issuanceRedeemer} — and the
|
|
440
|
+
* proof travels as a VALUE inside the `issuance_logic` withdraw-0 redeemer's
|
|
441
|
+
* map, keyed by policy id. See {@link issuanceLogicRedeemer}, which is where a
|
|
442
|
+
* proof built here now goes.
|
|
443
|
+
*
|
|
444
|
+
* Signatures and encodings are UNCHANGED on purpose: `src/substandards/**`
|
|
445
|
+
* still calls both, and rewiring those call sites is a separate slice.
|
|
446
|
+
*
|
|
447
|
+
* Verified against the blueprint's own `types/MintingRegistryProof` definition,
|
|
448
|
+
* not upstream's prose.
|
|
321
449
|
*/
|
|
322
450
|
export function mintingProofRefInput(registryRefInputIndex) {
|
|
323
451
|
return Data.constr(0n, [Data.int(BigInt(registryRefInputIndex))]);
|
|
324
452
|
}
|
|
453
|
+
/** The other arm of {@link mintingProofRefInput} — see its block comment. */
|
|
325
454
|
export function mintingProofOutputIndex(registryOutputIndex) {
|
|
326
455
|
return Data.constr(1n, [Data.int(BigInt(registryOutputIndex))]);
|
|
327
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* `issuance_mint`'s redeemer — `IssuanceRedeemer { params_idx }`.
|
|
459
|
+
*
|
|
460
|
+
* The PERMANENT half of issuance (upstream #129). This script's applied hash IS
|
|
461
|
+
* a token's policy id, so its redeemer is frozen for as long as any token
|
|
462
|
+
* exists: nothing but an index hint locating the protocol-params UTxO among the
|
|
463
|
+
* reference inputs. Everything that could ever change moved to
|
|
464
|
+
* {@link issuanceLogicRedeemer}.
|
|
465
|
+
*
|
|
466
|
+
* ⚠ `params_idx` INDEXES THE COMPLETE, LEDGER-SORTED REFERENCE-INPUT SET, and
|
|
467
|
+
* the validator jumps straight to `list.at(reference_inputs, params_idx)` and
|
|
468
|
+
* authenticates what it finds by the params NFT. Computing this index before
|
|
469
|
+
* the builder has added every reference input is a builder bug, and it does not
|
|
470
|
+
* report as one — it resolves to some other UTxO, fails the NFT check, and dies
|
|
471
|
+
* naming nothing. Use `referenceInputIndexOf` in `src/core/ledger-order.ts`,
|
|
472
|
+
* over the complete set, last.
|
|
473
|
+
*/
|
|
474
|
+
export function issuanceRedeemer(paramsIdx) {
|
|
475
|
+
if (!Number.isInteger(paramsIdx) || paramsIdx < 0) {
|
|
476
|
+
throw new Error(`issuanceRedeemer: params_idx must be a non-negative integer, got ${paramsIdx}`);
|
|
477
|
+
}
|
|
478
|
+
return Data.constr(0n, [Data.int(BigInt(paramsIdx))]);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* `issuance_logic`'s withdraw-0 redeemer — a MAP from policy id to that
|
|
482
|
+
* policy's `MintingRegistryProof`.
|
|
483
|
+
*
|
|
484
|
+
* The REPLACEABLE half of issuance (upstream #129). This is a Plutus `Pairs`
|
|
485
|
+
* association list, NOT a Constr. The two consumers read disjoint halves of it:
|
|
486
|
+
*
|
|
487
|
+
* - `issuance_mint` reads only the KEYS — `has_key(covered, own_policy)`. It
|
|
488
|
+
* never decodes a value.
|
|
489
|
+
* - `issuance_logic` reads only the VALUES — `list.all` over the entries,
|
|
490
|
+
* running the per-policy rule set on each proof.
|
|
491
|
+
*
|
|
492
|
+
* Entry ORDER is not significant to either, which is why this takes a list and
|
|
493
|
+
* imposes no sort.
|
|
494
|
+
*
|
|
495
|
+
* Three refusals, each closing a builder bug that reports as something else:
|
|
496
|
+
*
|
|
497
|
+
* ⛔ AN EMPTY MAP. `list.all([])` is VACUOUSLY TRUE on chain, so an empty
|
|
498
|
+
* redeemer sails through `issuance_logic` while every `issuance_mint` in the
|
|
499
|
+
* transaction fails its `has_key`. The failure names the mint, not the empty
|
|
500
|
+
* map, and nothing points at the omission.
|
|
501
|
+
*
|
|
502
|
+
* ⛔ A DUPLICATE POLICY ID. Compared as LOWER-CASED HEX STRINGS, not as encoded
|
|
503
|
+
* keys. MEASURED: `Data.map` builds a JS `Map` keyed by `Uint8Array` IDENTITY,
|
|
504
|
+
* so two distinct arrays holding identical bytes BOTH survive and the map is not
|
|
505
|
+
* deduplicated for you. Which of the two proofs governs is then a property of
|
|
506
|
+
* the ledger's map handling rather than of anything you wrote. The keys are
|
|
507
|
+
* lower-cased first because hex case is presentation and two spellings of one
|
|
508
|
+
* policy encode to identical bytes — the same normalisation `cip171.ts` applies
|
|
509
|
+
* to raw script hashes, and the hazard `ledger-order.ts` measures for ordering.
|
|
510
|
+
*
|
|
511
|
+
* ⛔ A VALUE THAT IS NOT A `MintingRegistryProof`. The keys are the frozen
|
|
512
|
+
* interface `issuance_mint` depends on; the values are what `issuance_logic`
|
|
513
|
+
* decodes, and a value it cannot decode aborts the whole withdrawal.
|
|
514
|
+
*/
|
|
515
|
+
export function issuanceLogicRedeemer(entries) {
|
|
516
|
+
if (entries.length === 0) {
|
|
517
|
+
throw new Error(`issuanceLogicRedeemer: the entry list is EMPTY. On chain issuance_logic runs ` +
|
|
518
|
+
`list.all over these entries, which is vacuously TRUE for an empty map — so an ` +
|
|
519
|
+
`empty redeemer passes issuance_logic while every issuance_mint in the transaction ` +
|
|
520
|
+
`fails its has_key check, and the error names the mint rather than the omission. ` +
|
|
521
|
+
`Every policy this transaction issues must appear here.`);
|
|
522
|
+
}
|
|
523
|
+
const seen = new Set();
|
|
524
|
+
for (const e of entries) {
|
|
525
|
+
const k = e.policyId.toLowerCase();
|
|
526
|
+
if (seen.has(k)) {
|
|
527
|
+
throw new Error(`issuanceLogicRedeemer: duplicate policy id ${e.policyId}. A policy occupies exactly ` +
|
|
528
|
+
`one entry in the map issuance_mint's has_key runs against. MEASURED: Data.map is a ` +
|
|
529
|
+
`JS Map keyed by Uint8Array IDENTITY, so two byte-identical keys BOTH survive and the ` +
|
|
530
|
+
`map is not deduplicated — which of the two proofs governs is not something this ` +
|
|
531
|
+
`builder decides.`);
|
|
532
|
+
}
|
|
533
|
+
seen.add(k);
|
|
534
|
+
const p = e.proof;
|
|
535
|
+
const ok = p instanceof Data.Constr && (p.index === 0n || p.index === 1n) && p.fields.length === 1;
|
|
536
|
+
if (!ok) {
|
|
537
|
+
throw new Error(`issuanceLogicRedeemer: the value for policy ${e.policyId} is not a ` +
|
|
538
|
+
`MintingRegistryProof. issuance_logic decodes every value, and one it cannot decode ` +
|
|
539
|
+
`aborts the whole withdrawal. Build it with mintingProofRefInput() when the registry ` +
|
|
540
|
+
`node is a reference input, or mintingProofOutputIndex() when it is an output of this ` +
|
|
541
|
+
`transaction.`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return Data.map(entries.map((e) => [Data.bytearray(e.policyId), e.proof]));
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Which of the three upgrade-path shapes a `protocol_params` SPEND is.
|
|
548
|
+
* Field-less constructors; the index is the whole payload. Mirrors `PlgAct` in
|
|
549
|
+
* `src/core/ledger-order.ts`.
|
|
550
|
+
*/
|
|
551
|
+
export const ProtocolParamsAct = {
|
|
552
|
+
PROTOCOL_UPGRADE: 0n,
|
|
553
|
+
NOMINATE_AUTHORITY: 1n,
|
|
554
|
+
PROMOTE_AUTHORITY: 2n,
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Build a `ProtocolParamsRedeemer`.
|
|
558
|
+
*
|
|
559
|
+
* The arms carry no payload: every value a branch needs is already in the
|
|
560
|
+
* continuing datum, which is validated regardless. They exist to make each
|
|
561
|
+
* transaction DECLARE its intent, so `ProtocolUpgrade` freezes the nomination
|
|
562
|
+
* and `NominateAuthority` freezes everything else — an authority handover can
|
|
563
|
+
* never ride along inside a parameter change.
|
|
564
|
+
*
|
|
565
|
+
* ⛔ HAZARD: `ProtocolUpgrade` IS BYTE-IDENTICAL TO {@link voidData}, which is
|
|
566
|
+
* `Constr(0, [])`. Every existing caller that passes `voidData()` as the params
|
|
567
|
+
* spend redeemer therefore keeps working BY ACCIDENT, and no decoder anywhere —
|
|
568
|
+
* on chain or off — can distinguish a migrated caller from a stale one, because
|
|
569
|
+
* the bytes are the same bytes. Same shape as the `SpendViaTransfer` /
|
|
570
|
+
* `BaseSpendRedeemer` collision recorded in WORKLOG S-6.
|
|
571
|
+
*
|
|
572
|
+
* ⇒ The silence is ASYMMETRIC, and that is the risk profile: the other two arms
|
|
573
|
+
* are constructors 1 and 2, which `voidData()` cannot represent, so they fail
|
|
574
|
+
* loudly. Only the upgrade path is quiet — and it is the one every deployment
|
|
575
|
+
* exercises first.
|
|
576
|
+
*
|
|
577
|
+
* ⇒ Because no offline instrument can settle it, the proof that this redeemer
|
|
578
|
+
* was ever really implemented is a devnet mutation: submit `Constr(1, [])` where
|
|
579
|
+
* the validator expects `ProtocolUpgrade` and require it to go RED on chain. The
|
|
580
|
+
* ledger is the only witness with jurisdiction.
|
|
581
|
+
*/
|
|
582
|
+
export function protocolParamsRedeemer(arm) {
|
|
583
|
+
// `Object.hasOwn`, not `=== undefined`: `ProtocolParamsAct["valueOf"]` inherits
|
|
584
|
+
// a FUNCTION from Object.prototype, so a plain lookup sails past an undefined
|
|
585
|
+
// check and dies inside the encoder as a Data.Constr index type error naming
|
|
586
|
+
// nothing the caller can act on.
|
|
587
|
+
const idx = Object.hasOwn(ProtocolParamsAct, arm) ? ProtocolParamsAct[arm] : undefined;
|
|
588
|
+
if (idx === undefined) {
|
|
589
|
+
throw new Error(`protocolParamsRedeemer: unknown act ${JSON.stringify(arm)}. ` +
|
|
590
|
+
`Expected one of: ${Object.keys(ProtocolParamsAct).join(", ")}.`);
|
|
591
|
+
}
|
|
592
|
+
return Data.constr(idx, []);
|
|
593
|
+
}
|
|
328
594
|
/** Build a TransferAct redeemer for PLGlobal. */
|
|
329
595
|
export function transferActRedeemer(proofs) {
|
|
330
596
|
return Data.constr(0n, [
|
|
@@ -371,6 +637,161 @@ export function blacklistRemoveRedeemer(stakingPkh) {
|
|
|
371
637
|
return Data.constr(2n, [Data.bytearray(stakingPkh)]);
|
|
372
638
|
}
|
|
373
639
|
// ---------------------------------------------------------------------------
|
|
640
|
+
// MultisigScript — the upgrade authority's tree
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
/**
|
|
643
|
+
* Upper bound on the number of nodes (inner and leaf) a `MultisigScript` may
|
|
644
|
+
* hold, from upstream `lib/multisig.ak`'s `max_size`. Chosen there on MEASURED
|
|
645
|
+
* execution budget, not on feel, and baked into the validator's bytes — so a
|
|
646
|
+
* tree above it is refused on chain and changing the cap is a redeployment.
|
|
647
|
+
*/
|
|
648
|
+
export const MULTISIG_MAX_SIZE = 20;
|
|
649
|
+
/** Node count, leaves included — upstream `multisig.size`. */
|
|
650
|
+
function multisigSize(t) {
|
|
651
|
+
switch (t.type) {
|
|
652
|
+
case "all-of":
|
|
653
|
+
case "any-of":
|
|
654
|
+
case "at-least":
|
|
655
|
+
return 1 + t.scripts.reduce((acc, c) => acc + multisigSize(c), 0);
|
|
656
|
+
default:
|
|
657
|
+
return 1;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function expect28ByteHash(hash, where) {
|
|
661
|
+
if (!/^[0-9a-fA-F]{56}$/.test(hash)) {
|
|
662
|
+
throw new Error(`multisigScriptDatum: ${where} must be exactly 28 bytes (56 hex chars), got ` +
|
|
663
|
+
`${hash.length} chars. A hash of any other length can never match a signatory or a ` +
|
|
664
|
+
`withdrawal credential, so the leaf is permanently unsatisfiable — and an authority ` +
|
|
665
|
+
`nobody can satisfy is a permanent brick with no repair path.`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/** Encode one node, enforcing upstream `shape_ok` as it goes. */
|
|
669
|
+
function encodeMultisigNode(t) {
|
|
670
|
+
switch (t.type) {
|
|
671
|
+
case "signature":
|
|
672
|
+
expect28ByteHash(t.keyHash, "Signature.key_hash");
|
|
673
|
+
return Data.constr(0n, [Data.bytearray(t.keyHash)]);
|
|
674
|
+
case "script":
|
|
675
|
+
expect28ByteHash(t.scriptHash, "Script.script_hash");
|
|
676
|
+
return Data.constr(6n, [Data.bytearray(t.scriptHash)]);
|
|
677
|
+
case "before":
|
|
678
|
+
return Data.constr(4n, [Data.int(t.time)]);
|
|
679
|
+
case "after":
|
|
680
|
+
return Data.constr(5n, [Data.int(t.time)]);
|
|
681
|
+
case "all-of":
|
|
682
|
+
return Data.constr(1n, [Data.list(encodeChildren(t.scripts, "AllOf"))]);
|
|
683
|
+
case "any-of":
|
|
684
|
+
return Data.constr(2n, [Data.list(encodeChildren(t.scripts, "AnyOf"))]);
|
|
685
|
+
case "at-least": {
|
|
686
|
+
if (!Number.isInteger(t.required) || t.required < 1 || t.required > t.scripts.length) {
|
|
687
|
+
throw new Error(`multisigScriptDatum: AtLeast.required must satisfy 1 <= required <= ` +
|
|
688
|
+
`${t.scripts.length} (the child count), got ${t.required}. A threshold of 0 or ` +
|
|
689
|
+
`below authorises with no evidence at all; one above the child count can never be ` +
|
|
690
|
+
`met. Upstream MINR-054 / audit-3 finding 05.`);
|
|
691
|
+
}
|
|
692
|
+
return Data.constr(3n, [
|
|
693
|
+
Data.int(BigInt(t.required)),
|
|
694
|
+
Data.list(encodeChildren(t.scripts, "AtLeast")),
|
|
695
|
+
]);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function encodeChildren(scripts, where) {
|
|
700
|
+
if (scripts.length === 0) {
|
|
701
|
+
throw new Error(`multisigScriptDatum: ${where} has an EMPTY child list. AllOf [] is VACUOUSLY TRUE on ` +
|
|
702
|
+
`chain — a permissionless authority, the sharpest edge in the whole type — and AnyOf [] ` +
|
|
703
|
+
`can never be satisfied at all. One rule for all three list nodes, as upstream does.`);
|
|
704
|
+
}
|
|
705
|
+
const encoded = scripts.map(encodeMultisigNode);
|
|
706
|
+
// Structural equality, matching Aiken's `list.unique` over the children:
|
|
707
|
+
// compare the ENCODED CBOR rather than the JS objects, which are distinct
|
|
708
|
+
// references even when they describe the same node.
|
|
709
|
+
const seen = new Set();
|
|
710
|
+
for (const child of encoded) {
|
|
711
|
+
const key = bytesToHex(Data.toCBORBytes(child));
|
|
712
|
+
if (seen.has(key)) {
|
|
713
|
+
throw new Error(`multisigScriptDatum: ${where} has DUPLICATE children. A duplicate distorts AtLeast — ` +
|
|
714
|
+
`[A, A, B] at threshold 2 is met by A alone — and upstream refuses it on every list ` +
|
|
715
|
+
`node rather than only where it bites. Upstream MINR-054 / audit-3 finding 05.`);
|
|
716
|
+
}
|
|
717
|
+
seen.add(key);
|
|
718
|
+
}
|
|
719
|
+
return encoded;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Build a `MultisigScript` datum, enforcing upstream `lib/multisig.ak`'s
|
|
723
|
+
* `well_formed` — 28-byte hashes, non-empty and duplicate-free child lists,
|
|
724
|
+
* `1 <= required <= |scripts|`, and at most {@link MULTISIG_MAX_SIZE} nodes.
|
|
725
|
+
*
|
|
726
|
+
* ⛔ THE RAIL LIVES ON THE ENCODER ONLY, AND {@link decodeMultisigScript}
|
|
727
|
+
* DELIBERATELY ENFORCES NONE OF IT. The two functions answer different
|
|
728
|
+
* questions: the encoder decides what may be WRITTEN, the decoder reads what is
|
|
729
|
+
* ON CHAIN. A decoder that refused an ill-formed tree could not be used to
|
|
730
|
+
* inspect one — and inspecting a live authority somebody managed to write is
|
|
731
|
+
* exactly the moment you need to read it. Do not "tidy" the check into the
|
|
732
|
+
* decoder for symmetry.
|
|
733
|
+
*/
|
|
734
|
+
export function multisigScriptDatum(tree) {
|
|
735
|
+
const size = multisigSize(tree);
|
|
736
|
+
if (size > MULTISIG_MAX_SIZE) {
|
|
737
|
+
throw new Error(`multisigScriptDatum: the tree has ${size} nodes, above MULTISIG_MAX_SIZE = ` +
|
|
738
|
+
`${MULTISIG_MAX_SIZE}. The cap is baked into the validator's bytes and was chosen on ` +
|
|
739
|
+
`MEASURED execution budget; an authority that writes itself a tree too expensive to ` +
|
|
740
|
+
`evaluate has bricked the upgrade path.`);
|
|
741
|
+
}
|
|
742
|
+
return encodeMultisigNode(tree);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Parse a `MultisigScript` datum. Reads what is on chain and enforces NO
|
|
746
|
+
* well-formedness — see {@link multisigScriptDatum} for why the asymmetry is
|
|
747
|
+
* deliberate.
|
|
748
|
+
*/
|
|
749
|
+
export function decodeMultisigScript(d) {
|
|
750
|
+
if (!(d instanceof Data.Constr)) {
|
|
751
|
+
throw new Error("MultisigScript: expected a Constr");
|
|
752
|
+
}
|
|
753
|
+
const f = d.fields;
|
|
754
|
+
const children = (where) => {
|
|
755
|
+
const list = f[0];
|
|
756
|
+
if (!Array.isArray(list)) {
|
|
757
|
+
throw new Error(`MultisigScript.${where}: expected a list of scripts`);
|
|
758
|
+
}
|
|
759
|
+
return list.map(decodeMultisigScript);
|
|
760
|
+
};
|
|
761
|
+
switch (d.index) {
|
|
762
|
+
case 0n:
|
|
763
|
+
return { type: "signature", keyHash: expectBytes(f[0], "MultisigScript.Signature.keyHash") };
|
|
764
|
+
case 1n:
|
|
765
|
+
return { type: "all-of", scripts: children("AllOf") };
|
|
766
|
+
case 2n:
|
|
767
|
+
return { type: "any-of", scripts: children("AnyOf") };
|
|
768
|
+
case 3n: {
|
|
769
|
+
const required = f[0];
|
|
770
|
+
if (typeof required !== "bigint") {
|
|
771
|
+
throw new Error("MultisigScript.AtLeast: expected required to be an Int");
|
|
772
|
+
}
|
|
773
|
+
const list = f[1];
|
|
774
|
+
if (!Array.isArray(list)) {
|
|
775
|
+
throw new Error("MultisigScript.AtLeast: expected a list of scripts");
|
|
776
|
+
}
|
|
777
|
+
return { type: "at-least", required: Number(required), scripts: list.map(decodeMultisigScript) };
|
|
778
|
+
}
|
|
779
|
+
case 4n:
|
|
780
|
+
case 5n: {
|
|
781
|
+
const time = f[0];
|
|
782
|
+
if (typeof time !== "bigint") {
|
|
783
|
+
throw new Error("MultisigScript.Before/After: expected time to be an Int");
|
|
784
|
+
}
|
|
785
|
+
return d.index === 4n ? { type: "before", time } : { type: "after", time };
|
|
786
|
+
}
|
|
787
|
+
case 6n:
|
|
788
|
+
return { type: "script", scriptHash: expectBytes(f[0], "MultisigScript.Script.scriptHash") };
|
|
789
|
+
default:
|
|
790
|
+
throw new Error(`MultisigScript: unknown constructor index ${d.index}. Valid indices are 0..6 ` +
|
|
791
|
+
`(Signature, AllOf, AnyOf, AtLeast, Before, After, Script).`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// ---------------------------------------------------------------------------
|
|
374
795
|
// Asset helpers
|
|
375
796
|
// ---------------------------------------------------------------------------
|
|
376
797
|
/** Build an Assets object with a single native token unit + min lovelace */
|
|
@@ -536,6 +957,71 @@ export function buildCIP68FTDatum(meta) {
|
|
|
536
957
|
]);
|
|
537
958
|
}
|
|
538
959
|
// ---------------------------------------------------------------------------
|
|
960
|
+
// The inline-datum bound
|
|
961
|
+
// ---------------------------------------------------------------------------
|
|
962
|
+
/**
|
|
963
|
+
* Serialised size, in bytes, of an inline datum.
|
|
964
|
+
*
|
|
965
|
+
* The on-chain counterpart is `bytearray.length(builtin.serialise_data(d))`
|
|
966
|
+
* inside `is_seizable_output_shape_bounded` (upstream `lib/assets.ak`), which
|
|
967
|
+
* every script that creates a programmable-logic-base output applies as
|
|
968
|
+
* `... <= max_inline_datum_bytes`. Since alpha.4 that includes `issuance_logic`,
|
|
969
|
+
* so a CIP-68 mint datum is bounded at BIRTH — the old issuance exemption is
|
|
970
|
+
* expired, and CIP-68 reference tokens carrying data-URI logos were the
|
|
971
|
+
* standing example of the kilobyte datum it now refuses.
|
|
972
|
+
*
|
|
973
|
+
* TWO ENCODERS, AND ONLY ONE DIRECTION IS SAFE. This measures with Evolution's
|
|
974
|
+
* `Data.toCBORBytes`; the chain measures with Plutus's `serialise_data`. They
|
|
975
|
+
* are different implementations and no offline instrument can prove they agree.
|
|
976
|
+
* The SDK must measure GREATER THAN OR EQUAL TO the chain: if it ever measures
|
|
977
|
+
* fewer bytes than the ledger does, it accepts a record the ledger then
|
|
978
|
+
* refuses, which is precisely the failure the bound exists to prevent.
|
|
979
|
+
*
|
|
980
|
+
* That direction is NOT settled here, and nothing in `test/cip68-bound.test.mjs`
|
|
981
|
+
* establishes it — every figure in that file comes from this same encoder. It
|
|
982
|
+
* is settled by the devnet at-bound SUCCESS in T-F04-4: a datum measuring
|
|
983
|
+
* exactly `maxInlineDatumBytes` by this function, accepted by a real ledger.
|
|
984
|
+
* Until that test is green, the agreement of the two encoders is an ASSUMPTION.
|
|
985
|
+
*/
|
|
986
|
+
export function inlineDatumBytes(datum) {
|
|
987
|
+
return Data.toCBORBytes(datum).length;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Refuse an inline datum larger than `maxInlineDatumBytes`, naming the bound,
|
|
991
|
+
* the measured size and the parameter the caller would have to change.
|
|
992
|
+
*
|
|
993
|
+
* THE COMPARISON IS `<=`, MATCHING THE CHAIN. A datum measuring EXACTLY
|
|
994
|
+
* `maxInlineDatumBytes` is legal and MUST be accepted. A `<` here would read as
|
|
995
|
+
* prudence and would instead remove CIP-68 registration from this SDK for the
|
|
996
|
+
* boundary case: the caller sees a refusal, concludes the feature does not
|
|
997
|
+
* work, and there is no incident, no log and no corruption for anyone to
|
|
998
|
+
* investigate. `test/cip68-bound.test.mjs` pins both directions one byte apart
|
|
999
|
+
* for that reason.
|
|
1000
|
+
*
|
|
1001
|
+
* `datum` is a `Data.Data` and NOT a `CIP68MetadataInput` on purpose: the thing
|
|
1002
|
+
* the chain bounds is the serialised datum, so a helper taking metadata would
|
|
1003
|
+
* be measuring a different object one step removed from the rule it mirrors.
|
|
1004
|
+
*
|
|
1005
|
+
* @param datum the inline datum that will be attached to the output
|
|
1006
|
+
* @param maxInlineDatumBytes the deployment's bound (`DeploymentParams`)
|
|
1007
|
+
* @param what what is being measured, for the refusal message
|
|
1008
|
+
*/
|
|
1009
|
+
export function assertInlineDatumWithinBound(datum, maxInlineDatumBytes, what) {
|
|
1010
|
+
const size = inlineDatumBytes(datum);
|
|
1011
|
+
if (size <= maxInlineDatumBytes)
|
|
1012
|
+
return;
|
|
1013
|
+
throw new Error(`${what}: inline datum is ${size} bytes, which exceeds this deployment's ` +
|
|
1014
|
+
`maxInlineDatumBytes of ${maxInlineDatumBytes}. The scripts that create ` +
|
|
1015
|
+
`programmable-token outputs refuse a datum over the bound, so this ` +
|
|
1016
|
+
`transaction would be rejected on chain. Remedy: shorten the metadata — ` +
|
|
1017
|
+
`an embedded data-URI logo is almost always the cause, since a record ` +
|
|
1018
|
+
`with every field at a documented cap is only 419 bytes — or use a ` +
|
|
1019
|
+
`deployment whose maxInlineDatumBytes is larger. Note that the bound is ` +
|
|
1020
|
+
`baked into four script hashes (transfer, third_party, unfracking, ` +
|
|
1021
|
+
`issuance_logic), so raising it produces a DIFFERENT protocol instance ` +
|
|
1022
|
+
`rather than a configuration change.`);
|
|
1023
|
+
}
|
|
1024
|
+
// ---------------------------------------------------------------------------
|
|
539
1025
|
// Re-exports for convenience
|
|
540
1026
|
// ---------------------------------------------------------------------------
|
|
541
1027
|
export { Data, Bytes, Assets, Credential, InlineDatum, KeyHash, EvoAddress, EvoScriptHash, TransactionHash, Transaction, UPLC, };
|