@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.
Files changed (35) hide show
  1. package/README.md +72 -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 +5 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/standard/blueprint.d.ts +128 -29
  19. package/dist/standard/blueprint.d.ts.map +1 -1
  20. package/dist/standard/blueprint.js +244 -47
  21. package/dist/standard/blueprint.js.map +1 -1
  22. package/dist/standard/scripts.d.ts +143 -75
  23. package/dist/standard/scripts.d.ts.map +1 -1
  24. package/dist/standard/scripts.js +211 -116
  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 +131 -48
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +2 -1
@@ -7,17 +7,63 @@
7
7
  *
8
8
  * No compliance features, no blacklist.
9
9
  * Uses Evolution SDK directly — no adapter abstraction.
10
+ *
11
+ * ⛔ alpha.4 SPLIT ISSUANCE (#129), AND IT CHANGED WHAT `register` AND `mint`
12
+ * MUST CARRY. `issuance_mint` is frozen — its applied hash IS the policy id —
13
+ * so its redeemer is now `IssuanceRedeemer { params_idx }` and nothing else.
14
+ * Everything replaceable moved behind `issuance_logic_cred` in the
15
+ * protocol-params datum, which means EVERY mint and EVERY burn now carries a
16
+ * SECOND protocol withdrawal: `issuance_logic`'s withdraw-0, whose redeemer is
17
+ * the per-policy `MintingRegistryProof` map.
18
+ *
19
+ * ⚠ Omitting that withdrawal is SILENT. `issuance_mint` calls `covered_by`,
20
+ * which scans `tx.redeemers` for a `Withdraw(issuance_logic_cred)` purpose;
21
+ * with the withdrawal absent there is no such redeemer, the scan returns False,
22
+ * and the mint fails naming neither a withdrawal, nor a policy, nor an index.
23
+ * That is why `register` and `mint` route every index through `issuancePlan`
24
+ * and cross-check `plan.withdrawals.length` against their own `withdraw()`
25
+ * call count: a comment cannot prove a call site exists, a count can.
26
+ *
27
+ * `transfer` and `thirdPartyTransfer` neither mint nor burn, so neither needs
28
+ * `issuance_logic` and both still use `plbWithdrawalPlan`.
10
29
  */
11
30
  import { CIP171_METADATA_LABEL, buildCip171Metadatum } from "../../core/cip171.js";
12
- import { Address as EvoAddress, Assets as EvoAssets, Bytes, Data, Transaction, } from "@evolution-sdk/evolution";
31
+ import { Address as EvoAddress, Assets as EvoAssets, Bytes, Data, Transaction, TransactionHash as EvoTransactionHash, TransactionInput as EvoTransactionInput, } from "@evolution-sdk/evolution";
13
32
  import { getValidatorCode } from "../../standard/blueprint.js";
14
33
  import { findRegistryNode, findCoveringNode, utxoToTxInput, } from "../../core/registry.js";
15
- import { baseSpendRedeemer, transferRedeemer, thirdPartyRedeemer, referenceInputIndexOf, withdrawalIndexOf, compareTxInputs, } from "../../core/ledger-order.js";
16
- import { buildEvoScript, computeScriptHash, scriptAddress, baseAddress, stakingCredentialHash, stringToHex, voidData, registryNodeDatum, decodeRegistryNode, registryInsertRedeemer, mintingProofOutputIndex, mintingProofRefInput, mintAssetsFromMap, REGISTRY_NODE_MIN_ADA, getInlineDatum, utxoUnitQty, outputAssets, Credential, KeyHash, InlineDatum, } from "../../core/evo-utils.js";
34
+ import { baseSpendRedeemer, transferRedeemer, thirdPartyRedeemer, referenceInputIndexOf, plbWithdrawalPlan, issuancePlan, programmableLogicGlobalRedeemer, compareTxInputs, } from "../../core/ledger-order.js";
35
+ import { buildEvoScript, computeScriptHash, scriptAddress, baseAddress, stakingCredentialHash, stringToHex, voidData, registryNodeDatum, decodeRegistryNode, registryInsertRedeemer, mintAssetsFromMap, REGISTRY_NODE_MIN_ADA, getInlineDatum, assertProtocolParamsIssuanceLogic, utxoUnitQty, outputAssets, minUtxoAtLeast, Credential, KeyHash, InlineDatum, } from "../../core/evo-utils.js";
17
36
  const DUMMY_VALIDATORS = {
18
37
  ISSUE: "transfer.issue.withdraw",
19
38
  TRANSFER: "transfer.transfer.withdraw",
20
39
  };
40
+ /**
41
+ * The flat amount this plugin used to hardcode, kept as a FLOOR rather than an
42
+ * answer — see `minUtxoAtLeast`. All five token outputs share one shape (a
43
+ * programmable-logic-base address, one token, a `voidData()` datum), so the
44
+ * DATUM is fixed and they look like a constant is safe. It is not: min-UTxO
45
+ * scales with SERIALISED OUTPUT SIZE, and two of the three things that scale it
46
+ * come from the caller — the ASSET NAME (raw hex at every boundary here, 32
47
+ * bytes when CIP-67-labelled) and the QUANTITY (a CBOR integer that widens with
48
+ * magnitude).
49
+ *
50
+ * MEASURED against preview's live `coinsPerUtxoByte` of 4310 (2026-09-07):
51
+ *
52
+ * live "DUM"+suffix (6B), qty 1000 1,202,490 OK, 97,510 headroom
53
+ * live name (6B), qty 2^63-1 1,228,350 OK, 71,650 headroom
54
+ * CIP-67 labelled 12B, qty 1000 1,215,420 OK, 84,580 headroom
55
+ * max CIP-67 name (32B), qty 1000 1,318,860 SHORT by 18,860
56
+ * max name (32B), qty 2^63-1 1,344,720 SHORT by 44,720
57
+ *
58
+ * ⚠ IDENTICAL figures to `freeze-and-seize`, and that is the point: same output
59
+ * shape means this was ONE defect appearing in two places, not two defects. It
60
+ * was found in FES first and only then looked for here — the second sighting is
61
+ * what made it a pattern rather than a one-off.
62
+ *
63
+ * The floor keeps the change MONOTONE: ordinary names emit exactly what they
64
+ * always did, and only the cases that were genuinely short move.
65
+ */
66
+ const TOKEN_OUTPUT_FLOOR = 1300000n;
21
67
  export function dummySubstandard(config) {
22
68
  let ctx;
23
69
  let issueScript;
@@ -87,14 +133,16 @@ export function dummySubstandard(config) {
87
133
  */
88
134
  async function findParamsUtxo() {
89
135
  const unit = ctx.deployment.protocolParams.policyId + stringToHex("ProtocolParams");
90
- const addr = EvoAddress.fromBech32(scriptAddress(networkId, ctx.deployment.coordination.scriptHash));
136
+ const addr = EvoAddress.fromBech32(scriptAddress(networkId, ctx.deployment.protocolParams.policyId));
91
137
  const utxos = await ctx.client.getUtxosWithUnit(addr, unit);
92
138
  if (utxos.length === 0) {
93
139
  throw new Error(`Protocol params UTxO not found (unit ${unit}) at the coordination address. ` +
94
140
  `The params NFT is one-shot: zero results means it is locked elsewhere, not that ` +
95
141
  `the protocol is un-deployed.`);
96
142
  }
97
- return utxos[0];
143
+ const utxo = utxos[0];
144
+ assertProtocolParamsIssuanceLogic(utxo, ctx.deployment.issuanceLogic.scriptHash);
145
+ return utxo;
98
146
  }
99
147
  /**
100
148
  * The IssuanceCborHex UTxO.
@@ -115,6 +163,29 @@ export function dummySubstandard(config) {
115
163
  }
116
164
  return utxos[0];
117
165
  }
166
+ /**
167
+ * The `issuance_logic` reference-script UTxO.
168
+ *
169
+ * ⛔ Its withdraw-0 rides on EVERY mint and burn, which makes this the single
170
+ * most load-bearing reference-script output in the deployment. If an earlier
171
+ * transaction spent it, the deployment is broken and the script must be
172
+ * re-published; attaching the body here would hide that infrastructure loss.
173
+ */
174
+ async function findIssuanceLogicRefUtxo() {
175
+ const { txHash, outputIndex } = ctx.deployment.issuanceLogicRefInput;
176
+ const utxos = await ctx.client.getUtxosByOutRef([
177
+ new EvoTransactionInput.TransactionInput({
178
+ transactionId: EvoTransactionHash.fromHex(txHash),
179
+ index: BigInt(outputIndex),
180
+ }),
181
+ ]);
182
+ if (utxos.length === 0) {
183
+ throw new Error(`issuance_logic reference script not found on-chain at ${txHash}#${outputIndex}. ` +
184
+ `Deployment reference scripts are load-bearing: if an earlier transaction SPENT this ` +
185
+ `output, the deployment is broken and must be re-published.`);
186
+ }
187
+ return utxos[0];
188
+ }
118
189
  /** Build the transaction and shape it into an UnsignedTx. */
119
190
  async function finish(tx, changeAddress, extra = {}) {
120
191
  // ⛔ NEVER LET COIN SELECTION REACH A REFERENCE-SCRIPT UTxO.
@@ -170,22 +241,33 @@ export function dummySubstandard(config) {
170
241
  * node (the one whose key < ours and whose next > ours), spending it to
171
242
  * repoint its `next` at us, and minting a new node NFT for our own entry.
172
243
  *
173
- * Mint and registration are one transaction because `issuance_mint`'s
174
- * MintingRegistryProof can name the registry node as an OUTPUT of this very
175
- * transaction (ctor 1, OutputIndex) rather than as a reference input so
176
- * the token can be minted before its node exists anywhere else.
244
+ * Mint and registration are one transaction because the registry proof can
245
+ * name the new node as an OUTPUT of this very transaction (OutputIndex), so
246
+ * the token can be minted before its node exists anywhere else. In alpha.4
247
+ * that proof rides inside `issuance_logic`'s withdraw-0 map, keyed by policy
248
+ * id; `issuance_mint`'s own redeemer is only `IssuanceRedeemer { params_idx }`.
249
+ * Register IS a first mint, so it needs that withdrawal, the params reference
250
+ * input and its map entry exactly as `mint` does.
177
251
  */
178
252
  async register(params) {
179
253
  const { feePayerAddress, assetName, quantity } = params;
180
254
  const recipient = params.recipientAddress ?? feePayerAddress;
181
255
  const client = ctx.client;
256
+ // min-UTxO is sized from live protocol parameters, not guessed — the asset
257
+ // name and the quantity are both caller-supplied and both widen the output.
258
+ const coinsPerUtxoByte = (await client.getProtocolParameters()).coinsPerUtxoByte;
182
259
  // The token policy IS issuance_mint parameterised by our minting logic.
183
260
  const issuanceMint = ctx.standardScripts.buildIssuanceMint(issueScript.hash);
184
261
  const tokenPolicyId = issuanceMint.hash;
185
262
  const unit = tokenPolicyId + assetName;
186
- const registryMintPolicyId = ctx.standardScripts.registryMint.hash;
187
- const registrySpendAddr = scriptAddress(networkId, ctx.standardScripts.registrySpend.hash);
188
- const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registrySpendAddr));
263
+ const registryPolicyId = ctx.standardScripts.registry.hash;
264
+ // ONE HASH, TWO ROLES. registry_mint and registry_spend merged (#117), so
265
+ // the node NFT policy id and the node address's payment credential are the
266
+ // same value — the minting policy naming itself. The old names
267
+ // (registrySpendAddr / registryMintPolicyId) asserted a distinction that no
268
+ // longer exists and would tell a reader to look for two hashes.
269
+ const registryAddr = scriptAddress(networkId, ctx.standardScripts.registry.hash);
270
+ const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registryAddr));
189
271
  const covering = findCoveringNode(registryUtxos, tokenPolicyId);
190
272
  if (!covering) {
191
273
  throw new Error(`No covering registry node found for policy ${tokenPolicyId}. The registry origin ` +
@@ -220,14 +302,34 @@ export function dummySubstandard(config) {
220
302
  globalStateCs: "",
221
303
  });
222
304
  const updatedCoveringDatum = registryNodeDatum({ ...coveringNode, next: tokenPolicyId });
223
- const registryNftUnit = registryMintPolicyId + tokenPolicyId;
224
- const coveringNftUnit = registryMintPolicyId + coveringNode.key;
225
- // Output order is the contract: the MintingRegistryProof below names our
226
- // registry node by OUTPUT INDEX, so moving these outputs silently changes
227
- // which output the validator inspects.
228
- const OUT_TOKEN = 0;
229
- const OUT_NEW_NODE = 1;
230
- const OUT_COVERING = 2;
305
+ const registryNftUnit = registryPolicyId + tokenPolicyId;
306
+ const coveringNftUnit = registryPolicyId + coveringNode.key;
307
+ const paramsUtxo = await findParamsUtxo();
308
+ const issuanceCborUtxo = await findIssuanceCborUtxo();
309
+ const issuanceLogicRefUtxo = await findIssuanceLogicRefUtxo();
310
+ // ONE ARRAY, TWO USES, DECLARED ONCE. `params_idx` and every other
311
+ // reference-input index the plan computes are positions in THIS set, and
312
+ // `readFrom` below is what actually puts that set in the transaction. Two
313
+ // separately-built lists are two chances to disagree, and a disagreement
314
+ // here is a valid integer pointing at the wrong UTxO.
315
+ const refUtxos = [paramsUtxo, issuanceCborUtxo, issuanceLogicRefUtxo];
316
+ // Output order is the contract: `issuance_logic`'s map names our registry
317
+ // node by OUTPUT INDEX, so moving these outputs silently changes which
318
+ // output the validator inspects. Balancing change is appended after the
319
+ // explicit outputs — established by the literals this replaces having
320
+ // validated on chain, not by reasoning about Evolution.
321
+ const outputTags = ["user-token", "new-node", "covering-node"];
322
+ // ⛔ EVERY INDEX IN THIS TRANSACTION, COMPUTED IN ONE PLACE OVER THE
323
+ // COMPLETE SETS. No `plgHash`: register spends the covering registry node
324
+ // and wallet funds, never a `programmable_logic_base` input.
325
+ const plan = issuancePlan({
326
+ issuanceLogicHash: ctx.deployment.issuanceLogic.scriptHash,
327
+ otherWithdrawals: [{ hash: issueScript.hash, isScript: true }],
328
+ referenceInputs: refUtxos.map(utxoToTxInput),
329
+ paramsRefInput: utxoToTxInput(paramsUtxo),
330
+ outputs: outputTags,
331
+ issued: [{ policyId: tokenPolicyId, proof: { kind: "output", tag: "new-node" } }],
332
+ });
231
333
  let tx = client.newTx();
232
334
  // CIP-171 provenance, carried by the transaction that parameterises the
233
335
  // scripts it describes. Optional: absent, this behaves exactly as before.
@@ -238,24 +340,43 @@ export function dummySubstandard(config) {
238
340
  });
239
341
  }
240
342
  tx = tx.collectFrom({ inputs: [covering], redeemer: voidData() });
241
- // The minting-logic withdraw-0: dummy's `issue` validator, redeemer 100.
242
- tx = tx.withdraw({
243
- stakeCredential: Credential.makeScriptHash(hexToBytes(issueScript.hash)),
244
- amount: 0n,
245
- redeemer: Data.int(100n),
246
- });
343
+ // The minting-logic and protocol issuance-logic withdrawals. The latter's
344
+ // script witness comes from `issuanceLogicRefUtxo` in `readFrom` below.
345
+ const withdrawalSpecs = [
346
+ { hash: issueScript.hash, redeemer: Data.int(100n) },
347
+ {
348
+ hash: ctx.deployment.issuanceLogic.scriptHash,
349
+ redeemer: plan.issuanceLogicRedeemer,
350
+ },
351
+ ];
352
+ if (withdrawalSpecs.length !== plan.withdrawals.length) {
353
+ throw new Error(`register: issuance plan declares ${plan.withdrawals.length} withdrawals but ` +
354
+ `${withdrawalSpecs.length} are emitted`);
355
+ }
356
+ for (const withdrawal of withdrawalSpecs) {
357
+ tx = tx.withdraw({
358
+ stakeCredential: Credential.makeScriptHash(hexToBytes(withdrawal.hash)),
359
+ amount: 0n,
360
+ redeemer: withdrawal.redeemer,
361
+ });
362
+ }
247
363
  tx = tx.mintAssets({
248
364
  assets: mintAssetsFromMap(new Map([[unit, quantity]])),
249
- redeemer: mintingProofOutputIndex(OUT_NEW_NODE),
365
+ redeemer: plan.issuanceRedeemer,
250
366
  });
251
367
  tx = tx.mintAssets({
252
368
  assets: mintAssetsFromMap(new Map([[registryNftUnit, 1n]])),
253
369
  redeemer: registryInsertRedeemer(tokenPolicyId, { type: "script", hash: issueScript.hash }),
254
370
  });
371
+ // outputTags[0] — "user-token": the minted supply at the recipient's PLB.
255
372
  tx = tx.payToAddress({
256
373
  address: EvoAddress.fromBech32(recipientPlbAddr),
257
- assets: outputAssets(1300000n, new Map([[unit, quantity]])),
258
- datum: new InlineDatum.InlineDatum({ data: voidData() }),
374
+ assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
375
+ address: recipientPlbAddr,
376
+ assets: outputAssets(0n, new Map([[unit, quantity]])),
377
+ datum: voidData(),
378
+ coinsPerUtxoByte,
379
+ }), new Map([[unit, quantity]])), datum: new InlineDatum.InlineDatum({ data: voidData() }),
259
380
  });
260
381
  // ⚠ min-UTxO, not a round number pulled from the old code.
261
382
  //
@@ -267,24 +388,62 @@ export function dummySubstandard(config) {
267
388
  // Deliberately generous rather than exact: min-UTxO scales with
268
389
  // serialised size and with a protocol parameter that can rise.
269
390
  const REGISTRY_NODE_ADA = REGISTRY_NODE_MIN_ADA;
391
+ // outputTags[1] — "new-node": our registry node, named by the proof.
270
392
  tx = tx.payToAddress({
271
- address: EvoAddress.fromBech32(registrySpendAddr),
393
+ address: EvoAddress.fromBech32(registryAddr),
272
394
  assets: outputAssets(REGISTRY_NODE_ADA, new Map([[registryNftUnit, 1n]])),
273
395
  datum: new InlineDatum.InlineDatum({ data: newNodeDatum }),
274
396
  });
397
+ // outputTags[2] — "covering-node": the predecessor with its new link.
275
398
  tx = tx.payToAddress({
276
- address: EvoAddress.fromBech32(registrySpendAddr),
399
+ address: EvoAddress.fromBech32(registryAddr),
277
400
  assets: outputAssets(REGISTRY_NODE_ADA, new Map([[coveringNftUnit, 1n]])),
278
401
  datum: new InlineDatum.InlineDatum({ data: updatedCoveringDatum }),
279
402
  });
280
- const paramsUtxo = await findParamsUtxo();
281
- const issuanceCborUtxo = await findIssuanceCborUtxo();
282
- tx = tx.readFrom({ referenceInputs: [paramsUtxo, issuanceCborUtxo] });
403
+ // THE PROTOCOL-PARAMS REFERENCE INPUT STAYS — and as of S-11 that is a
404
+ // MEASURED decision, not a cautious one.
405
+ //
406
+ // alpha.3's registry no longer reads it (#117). `issuance_mint` does, to
407
+ // pull the LIVE delegate credentials from its datum, and that locate is
408
+ // DELIBERATELY NON-FAILING: absent params UTxO means "no delegation",
409
+ // falling back to local `no_escape` custody. So removing it raises no
410
+ // error — it silently selects a different custody path.
411
+ //
412
+ // MEASURED ON DEVNET (S-11): removing it from BOTH substandards' register
413
+ // paths, the full lifecycle still passes — dummy 2/2, FES 3/3. The two
414
+ // custody branches do converge here, as the reasoning predicted.
415
+ //
416
+ // ⇒ AND IT STAYS ANYWAY, for a reason the measurement itself supplies:
417
+ // the tests pass EITHER WAY. That is precisely what makes dropping it
418
+ // unsafe to keep. Nothing in this suite would notice if `issuance_mint`'s
419
+ // delegation semantics changed and the absent input started to matter —
420
+ // the guard against that is the input being there, not a test. The saving
421
+ // is one reference input on a once-per-token transaction; the exposure is
422
+ // a silent custody change nobody would see.
423
+ // alpha.4 makes it mandatory rather than an optional custody hint because
424
+ // `with_protocol_params_fields` opens with `list.expect_at`.
425
+ tx = tx.readFrom({ referenceInputs: refUtxos });
283
426
  tx = tx.attachScript({ script: buildEvoScript(issuanceMint.compiledCode) });
284
- tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.registryMint.compiledCode) });
285
- tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.registrySpend.compiledCode) });
427
+ // ONE attach, not two. registry_mint and registry_spend merged into a
428
+ // single validator (#117): this transaction both MINTS a node NFT and
429
+ // SPENDS the covering node, which in alpha.2 needed two scripts. It is
430
+ // now one script serving both purposes — attaching it twice would put a
431
+ // duplicate witness in the transaction.
432
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.registry.compiledCode) });
286
433
  tx = tx.attachScript({ script: buildEvoScript(issueScript.compiledCode) });
287
- return finish(tx, feePayerAddress, { tokenPolicyId, unit, outputIndices: { OUT_TOKEN, OUT_NEW_NODE, OUT_COVERING } });
434
+ // Register's withdrawal set is asserted on the decoded transaction at
435
+ // test/devnet/dummy-lifecycle.test.ts:142-155. It stays there as readable
436
+ // documentation of the invariant rather than as a guard, because the live
437
+ // evaluator is upstream of it.
438
+ return finish(tx, feePayerAddress, {
439
+ tokenPolicyId,
440
+ unit,
441
+ outputIndices: {
442
+ OUT_TOKEN: plan.outputIndexOf("user-token"),
443
+ OUT_NEW_NODE: plan.outputIndexOf("new-node"),
444
+ OUT_COVERING: plan.outputIndexOf("covering-node"),
445
+ },
446
+ });
288
447
  },
289
448
  /**
290
449
  * Mint more of an already-registered dummy token.
@@ -297,40 +456,77 @@ export function dummySubstandard(config) {
297
456
  const { feePayerAddress, tokenPolicyId, assetName, quantity } = params;
298
457
  const recipient = params.recipientAddress ?? feePayerAddress;
299
458
  const client = ctx.client;
459
+ // min-UTxO is sized from live protocol parameters, not guessed — the asset
460
+ // name and the quantity are both caller-supplied and both widen the output.
461
+ const coinsPerUtxoByte = (await client.getProtocolParameters()).coinsPerUtxoByte;
300
462
  const unit = tokenPolicyId + assetName;
301
463
  const issuanceMint = ctx.standardScripts.buildIssuanceMint(issueScript.hash);
302
464
  if (issuanceMint.hash !== tokenPolicyId) {
303
465
  throw new Error(`Policy ${tokenPolicyId} is not a dummy token: issuance_mint parameterised by ` +
304
466
  `dummy's issue logic hashes to ${issuanceMint.hash}.`);
305
467
  }
306
- const registrySpendAddr = scriptAddress(networkId, ctx.standardScripts.registrySpend.hash);
307
- const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registrySpendAddr));
468
+ const registryAddr = scriptAddress(networkId, ctx.standardScripts.registry.hash);
469
+ const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registryAddr));
308
470
  const node = findRegistryNode(registryUtxos, tokenPolicyId);
309
471
  if (!node)
310
472
  throw new Error(`Registry node not found for policy ${tokenPolicyId}`);
311
473
  const paramsUtxo = await findParamsUtxo();
312
- const refs = [paramsUtxo, node];
313
- const nodeIdx = referenceInputIndexOf(refs.map(utxoToTxInput), utxoToTxInput(node));
474
+ const issuanceLogicRefUtxo = await findIssuanceLogicRefUtxo();
475
+ // ONE ARRAY, TWO USES, DECLARED ONCE. The plan computes `params_idx`
476
+ // and the registry-node proof over this set; `readFrom` supplies this same
477
+ // set. A second list could disagree while remaining perfectly well typed.
478
+ const refUtxos = [paramsUtxo, node, issuanceLogicRefUtxo];
479
+ const plan = issuancePlan({
480
+ issuanceLogicHash: ctx.deployment.issuanceLogic.scriptHash,
481
+ otherWithdrawals: [{ hash: issueScript.hash, isScript: true }],
482
+ referenceInputs: refUtxos.map(utxoToTxInput),
483
+ paramsRefInput: utxoToTxInput(paramsUtxo),
484
+ issued: [
485
+ {
486
+ policyId: tokenPolicyId,
487
+ proof: { kind: "reference-input", input: utxoToTxInput(node) },
488
+ },
489
+ ],
490
+ });
314
491
  const plbHash = ctx.standardScripts.programmableLogicBase.hash;
315
492
  const recipientPlbAddr = baseAddress(networkId, plbHash, recipient);
316
493
  let tx = client.newTx();
317
- tx = tx.withdraw({
318
- stakeCredential: Credential.makeScriptHash(hexToBytes(issueScript.hash)),
319
- amount: 0n,
320
- redeemer: Data.int(100n),
321
- });
494
+ const withdrawalSpecs = [
495
+ { hash: issueScript.hash, redeemer: Data.int(100n) },
496
+ {
497
+ hash: ctx.deployment.issuanceLogic.scriptHash,
498
+ redeemer: plan.issuanceLogicRedeemer,
499
+ },
500
+ ];
501
+ if (withdrawalSpecs.length !== plan.withdrawals.length) {
502
+ throw new Error(`mint: issuance plan declares ${plan.withdrawals.length} withdrawals but ` +
503
+ `${withdrawalSpecs.length} are emitted`);
504
+ }
505
+ for (const withdrawal of withdrawalSpecs) {
506
+ tx = tx.withdraw({
507
+ stakeCredential: Credential.makeScriptHash(hexToBytes(withdrawal.hash)),
508
+ amount: 0n,
509
+ redeemer: withdrawal.redeemer,
510
+ });
511
+ }
322
512
  tx = tx.mintAssets({
323
513
  assets: mintAssetsFromMap(new Map([[unit, quantity]])),
324
- redeemer: mintingProofRefInput(nodeIdx),
514
+ redeemer: plan.issuanceRedeemer,
325
515
  });
326
516
  tx = tx.payToAddress({
327
517
  address: EvoAddress.fromBech32(recipientPlbAddr),
328
- assets: outputAssets(1300000n, new Map([[unit, quantity]])),
329
- datum: new InlineDatum.InlineDatum({ data: voidData() }),
518
+ assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
519
+ address: recipientPlbAddr,
520
+ assets: outputAssets(0n, new Map([[unit, quantity]])),
521
+ datum: voidData(),
522
+ coinsPerUtxoByte,
523
+ }), new Map([[unit, quantity]])), datum: new InlineDatum.InlineDatum({ data: voidData() }),
330
524
  });
331
- tx = tx.readFrom({ referenceInputs: refs });
525
+ tx = tx.readFrom({ referenceInputs: refUtxos });
332
526
  tx = tx.attachScript({ script: buildEvoScript(issuanceMint.compiledCode) });
333
527
  tx = tx.attachScript({ script: buildEvoScript(issueScript.compiledCode) });
528
+ // The executable count check above prevents the plan and emitted
529
+ // transaction silently diverging before evaluation.
334
530
  return finish(tx, feePayerAddress, { tokenPolicyId, unit });
335
531
  },
336
532
  async burn(_params) {
@@ -364,6 +560,9 @@ export function dummySubstandard(config) {
364
560
  const { holderAddress, recipientAddress, tokenPolicyId, assetName, quantity, feePayerAddress } = params;
365
561
  const unit = tokenPolicyId + assetName;
366
562
  const client = ctx.client;
563
+ // min-UTxO is sized from live protocol parameters, not guessed — the asset
564
+ // name and the quantity are both caller-supplied and both widen the output.
565
+ const coinsPerUtxoByte = (await client.getProtocolParameters()).coinsPerUtxoByte;
367
566
  const plbHash = ctx.standardScripts.programmableLogicBase.hash;
368
567
  const holderPlbAddr = baseAddress(networkId, plbHash, holderAddress);
369
568
  const recipientPlbAddr = baseAddress(networkId, plbHash, recipientAddress);
@@ -374,8 +573,8 @@ export function dummySubstandard(config) {
374
573
  }
375
574
  const { selected, totalTokenAmount } = selectUtxosForAmount(tokenUtxos, unit, quantity);
376
575
  const returningAmount = totalTokenAmount - quantity;
377
- const registrySpendAddr = scriptAddress(networkId, ctx.standardScripts.registrySpend.hash);
378
- const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registrySpendAddr));
576
+ const registryAddr = scriptAddress(networkId, ctx.standardScripts.registry.hash);
577
+ const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registryAddr));
379
578
  const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
380
579
  if (!registryUtxo)
381
580
  throw new Error(`Registry node not found for policy ${tokenPolicyId}`);
@@ -392,8 +591,12 @@ export function dummySubstandard(config) {
392
591
  isScript: true,
393
592
  };
394
593
  const issuerAuthorityKey = { hash: transferScript.hash, isScript: true };
395
- const allWithdrawals = [thirdPartyKey, issuerAuthorityKey];
396
- const thirdPartyWdrlIdx = withdrawalIndexOf(allWithdrawals, thirdPartyKey);
594
+ // alpha.3: the DISPATCHER withdraws too, and PLB's wdrl_idx points at IT.
595
+ const plan = plbWithdrawalPlan({
596
+ plgHash: ctx.standardScripts.programmableLogicGlobal.hash,
597
+ others: [thirdPartyKey, issuerAuthorityKey],
598
+ });
599
+ const allWithdrawals = plan.all;
397
600
  // ---- Output layout is the contract, and it is NOT the transfer layout --
398
601
  //
399
602
  // `third_party` PAIRS each programmable input with an output, in ledger
@@ -413,14 +616,20 @@ export function dummySubstandard(config) {
413
616
  // Getting this backwards — destination last, continuations first — encodes
414
617
  // and submits happily and fails at evaluation with an empty trace list.
415
618
  const OUTPUTS_START_IDX = 1;
416
- const spendRdmr = baseSpendRedeemer("THIRD_PARTY", paramsIdx, thirdPartyWdrlIdx);
619
+ // wdrl_idx targets the dispatcher; the ACT moves to the dispatcher's own
620
+ // redeemer. In alpha.2 both lived on this one redeemer.
621
+ const spendRdmr = baseSpendRedeemer(paramsIdx, plan.plgIdx);
417
622
  let tx = client.newTx();
418
623
  tx = tx.collectFrom({ inputs: selected, redeemer: spendRdmr });
419
624
  // Destination first.
420
625
  tx = tx.payToAddress({
421
626
  address: EvoAddress.fromBech32(recipientPlbAddr),
422
- assets: outputAssets(1300000n, new Map([[unit, quantity]])),
423
- datum: new InlineDatum.InlineDatum({ data: voidData() }),
627
+ assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
628
+ address: recipientPlbAddr,
629
+ assets: outputAssets(0n, new Map([[unit, quantity]])),
630
+ datum: voidData(),
631
+ coinsPerUtxoByte,
632
+ }), new Map([[unit, quantity]])), datum: new InlineDatum.InlineDatum({ data: voidData() }),
424
633
  });
425
634
  // Then one continuation per input, in LEDGER ORDER — the validator walks
426
635
  // inputs and outputs in lockstep, and the ledger sorts inputs by
@@ -448,10 +657,18 @@ export function dummySubstandard(config) {
448
657
  if (stillToSeize > 0n) {
449
658
  throw new Error(`Selected UTxOs hold ${quantity - stillToSeize} of ${quantity} ${unit} — selection is short`);
450
659
  }
660
+ // The dispatcher's own withdraw-0 — new in alpha.3, and required on every
661
+ // programmable transaction. Its redeemer carries the act that used to be
662
+ // BaseSpendRedeemer's constructor.
663
+ tx = tx.withdraw({
664
+ stakeCredential: Credential.makeScriptHash(hexToBytes(ctx.standardScripts.programmableLogicGlobal.hash)),
665
+ amount: 0n,
666
+ redeemer: programmableLogicGlobalRedeemer("THIRD_PARTY"),
667
+ });
451
668
  tx = tx.withdraw({
452
669
  stakeCredential: Credential.makeScriptHash(hexToBytes(ctx.standardScripts.thirdParty.hash)),
453
670
  amount: 0n,
454
- redeemer: thirdPartyRedeemer(paramsIdx, registryIdx, OUTPUTS_START_IDX),
671
+ redeemer: thirdPartyRedeemer(registryIdx, OUTPUTS_START_IDX),
455
672
  });
456
673
  tx = tx.withdraw({
457
674
  stakeCredential: Credential.makeScriptHash(hexToBytes(transferScript.hash)),
@@ -460,6 +677,12 @@ export function dummySubstandard(config) {
460
677
  });
461
678
  tx = tx.readFrom({ referenceInputs: refUtxos });
462
679
  tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.thirdParty.compiledCode) });
680
+ // ⛔ THE DISPATCHER'S OWN SCRIPT WITNESS. A withdraw-0 needs the script in
681
+ // full, not just a redeemer — alpha.3 added the dispatcher's withdrawal to
682
+ // every programmable transaction, and S-6 wired the withdrawal and the
683
+ // redeemer but not this. The ledger says "An associated script witness is
684
+ // missing" on purpose=withdraw, which names the shape but not the script.
685
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
463
686
  tx = tx.attachScript({ script: buildEvoScript(transferScript.compiledCode) });
464
687
  tx = tx.attachScript({
465
688
  script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode),
@@ -472,6 +695,9 @@ export function dummySubstandard(config) {
472
695
  const { senderAddress, recipientAddress, tokenPolicyId, assetName, quantity } = params;
473
696
  const unit = tokenPolicyId + assetName;
474
697
  const client = ctx.client;
698
+ // min-UTxO is sized from live protocol parameters, not guessed — the asset
699
+ // name and the quantity are both caller-supplied and both widen the output.
700
+ const coinsPerUtxoByte = (await client.getProtocolParameters()).coinsPerUtxoByte;
475
701
  const plbHash = ctx.standardScripts.programmableLogicBase.hash;
476
702
  // 1. Build PLB addresses
477
703
  const senderPlbAddr = baseAddress(networkId, plbHash, senderAddress);
@@ -486,8 +712,8 @@ export function dummySubstandard(config) {
486
712
  const { selected, totalTokenAmount } = selectUtxosForAmount(tokenUtxos, unit, quantity);
487
713
  const returningAmount = totalTokenAmount - quantity;
488
714
  // 4. Find registry node reference input
489
- const registrySpendAddr = scriptAddress(networkId, ctx.standardScripts.registrySpend.hash);
490
- const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registrySpendAddr));
715
+ const registryAddr = scriptAddress(networkId, ctx.standardScripts.registry.hash);
716
+ const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registryAddr));
491
717
  const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
492
718
  if (!registryUtxo) {
493
719
  throw new Error(`Registry node not found for policy ${tokenPolicyId}`);
@@ -510,18 +736,21 @@ export function dummySubstandard(config) {
510
736
  hash: ctx.standardScripts.transfer.hash,
511
737
  isScript: true,
512
738
  };
513
- const allWithdrawals = [
514
- { hash: transferScript.hash, isScript: true },
515
- coreTransferKey,
516
- ];
517
- const transferWdrlIdx = withdrawalIndexOf(allWithdrawals, coreTransferKey);
518
- const coreTransferRedeemer = transferRedeemer(paramsIdx, [
739
+ // alpha.3: the DISPATCHER withdraws too, and PLB's wdrl_idx points at IT.
740
+ const plan = plbWithdrawalPlan({
741
+ plgHash: ctx.standardScripts.programmableLogicGlobal.hash,
742
+ others: [{ hash: transferScript.hash, isScript: true }, coreTransferKey],
743
+ });
744
+ const allWithdrawals = plan.all;
745
+ // params_idx is gone from the delegate redeemer — delegates no longer
746
+ // read the params datum at all.
747
+ const coreTransferRedeemer = transferRedeemer([
519
748
  { type: "exists", nodeIdx: registryIdx },
520
749
  ]);
521
750
  const dummyTransferRedeemer = Data.int(200n);
522
- // programmable_logic_base no longer takes an untyped redeemer: it dispatches
523
- // on the constructor, and witnesses WHERE its delegate's withdrawal sits.
524
- const spendRdmr = baseSpendRedeemer("TRANSFER", paramsIdx, transferWdrlIdx);
751
+ // programmable_logic_base witnesses where the DISPATCHER's withdrawal
752
+ // sits; the dispatch choice itself is the dispatcher's own redeemer.
753
+ const spendRdmr = baseSpendRedeemer(paramsIdx, plan.plgIdx);
525
754
  const tokenDatum = voidData();
526
755
  // 8. Get sender's staking credential
527
756
  const senderStakingHash = stakingCredentialHash(senderAddress);
@@ -536,6 +765,12 @@ export function dummySubstandard(config) {
536
765
  amount: 0n,
537
766
  redeemer: dummyTransferRedeemer,
538
767
  });
768
+ // The dispatcher's own withdraw-0 — new in alpha.3.
769
+ tx = tx.withdraw({
770
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
771
+ amount: 0n,
772
+ redeemer: programmableLogicGlobalRedeemer("TRANSFER"),
773
+ });
539
774
  tx = tx.withdraw({
540
775
  stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.transfer.hash, "hex"))),
541
776
  amount: 0n,
@@ -544,17 +779,31 @@ export function dummySubstandard(config) {
544
779
  if (returningAmount > 0n) {
545
780
  tx = tx.payToAddress({
546
781
  address: EvoAddress.fromBech32(senderPlbAddr),
547
- assets: outputAssets(1300000n, new Map([[unit, returningAmount]])),
548
- datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
782
+ assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
783
+ address: senderPlbAddr,
784
+ assets: outputAssets(0n, new Map([[unit, returningAmount]])),
785
+ datum: voidData(),
786
+ coinsPerUtxoByte,
787
+ }), new Map([[unit, returningAmount]])), datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
549
788
  });
550
789
  }
551
790
  tx = tx.payToAddress({
552
791
  address: EvoAddress.fromBech32(recipientPlbAddr),
553
- assets: outputAssets(1300000n, new Map([[unit, quantity]])),
554
- datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
792
+ assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
793
+ address: recipientPlbAddr,
794
+ assets: outputAssets(0n, new Map([[unit, quantity]])),
795
+ datum: voidData(),
796
+ coinsPerUtxoByte,
797
+ }), new Map([[unit, quantity]])), datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
555
798
  });
556
799
  tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, registryUtxo] });
557
800
  tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.transfer.compiledCode) });
801
+ // ⛔ THE DISPATCHER'S OWN SCRIPT WITNESS. A withdraw-0 needs the script in
802
+ // full, not just a redeemer — alpha.3 added the dispatcher's withdrawal to
803
+ // every programmable transaction, and S-6 wired the withdrawal and the
804
+ // redeemer but not this. The ledger says "An associated script witness is
805
+ // missing" on purpose=withdraw, which names the shape but not the script.
806
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
558
807
  tx = tx.attachScript({ script: buildEvoScript(transferScript.compiledCode) });
559
808
  tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
560
809
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(senderStakingHash) });