@easy1staking/cip113-sdk-ts 0.7.0 → 0.10.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.
Files changed (35) hide show
  1. package/README.md +118 -2
  2. package/blueprints/standard/v0.5.0-alpha.3/UPSTREAM_PIN.json +35 -0
  3. package/blueprints/standard/v0.5.0-alpha.3/plutus.json +1126 -0
  4. package/blueprints/standard/v0.5.0-alpha.4/UPSTREAM_PIN.json +36 -0
  5. package/blueprints/standard/v0.5.0-alpha.4/plutus.json +1434 -0
  6. package/dist/core/evo-utils.d.ts +357 -39
  7. package/dist/core/evo-utils.d.ts.map +1 -1
  8. package/dist/core/evo-utils.js +513 -27
  9. package/dist/core/evo-utils.js.map +1 -1
  10. package/dist/core/ledger-order.d.ts +204 -18
  11. package/dist/core/ledger-order.d.ts.map +1 -1
  12. package/dist/core/ledger-order.js +385 -28
  13. package/dist/core/ledger-order.js.map +1 -1
  14. package/dist/index.d.ts +6 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +15 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/standard/blueprint.d.ts +129 -29
  19. package/dist/standard/blueprint.d.ts.map +1 -1
  20. package/dist/standard/blueprint.js +245 -47
  21. package/dist/standard/blueprint.js.map +1 -1
  22. package/dist/standard/scripts.d.ts +189 -75
  23. package/dist/standard/scripts.d.ts.map +1 -1
  24. package/dist/standard/scripts.js +454 -123
  25. package/dist/standard/scripts.js.map +1 -1
  26. package/dist/substandards/dummy/index.d.ts +19 -0
  27. package/dist/substandards/dummy/index.d.ts.map +1 -1
  28. package/dist/substandards/dummy/index.js +322 -73
  29. package/dist/substandards/dummy/index.js.map +1 -1
  30. package/dist/substandards/freeze-and-seize/index.d.ts.map +1 -1
  31. package/dist/substandards/freeze-and-seize/index.js +455 -117
  32. package/dist/substandards/freeze-and-seize/index.js.map +1 -1
  33. package/dist/types.d.ts +161 -48
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +2 -1
@@ -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
- /** Build the coordination datum. Field order is the on-chain contract. */
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
- Data.bytearray(p.registryNodeCs),
268
- credToData(p.progLogicCred),
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
- Data.int(p.maxInlineDatumBytes),
356
+ pending,
274
357
  ]);
275
358
  }
276
- /** Parse the coordination datum. */
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 !== 7) {
282
- throw new Error(`ProgrammableLogicGlobalParams: expected exactly 7 fields, got ${d.fields.length}. ` +
283
- `If you read 6 somewhere, that source predates #115max_inline_datum_bytes ` +
284
- `is field 6. A 6-field datum is malformed for every programmable_logic_base spend.`);
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
- registryNodeCs: expectBytes(f[0], "ProtocolParams.registryNodeCs"),
293
- progLogicCred: dataToCred(f[1], "ProtocolParams.progLogicCred"),
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
- unfrackingCred: dataToCred(f[4], "ProtocolParams.unfrackingCred"),
297
- upgradeCred: dataToCred(f[5], "ProtocolParams.upgradeCred"),
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
- * `issuance_mint`'s redeemer a BARE `MintingRegistryProof`.
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
- * Verified against the blueprint's own redeemer schema, not upstream's prose.
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, };