@easy1staking/cip113-sdk-ts 0.3.1 → 0.4.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 (47) hide show
  1. package/README.md +20 -0
  2. package/blueprints/standard/v0.3.0/UPSTREAM_PIN.json +30 -0
  3. package/blueprints/standard/v0.5.0-alpha.2/UPSTREAM_PIN.json +33 -0
  4. package/blueprints/standard/v0.5.0-alpha.2/plutus.json +1010 -0
  5. package/blueprints/substandards/dummy/v0.1.0/UPSTREAM_PIN.json +29 -0
  6. package/blueprints/substandards/dummy/v0.2.0/UPSTREAM_PIN.json +36 -0
  7. package/blueprints/substandards/dummy/v0.2.0/plutus.json +84 -0
  8. package/blueprints/substandards/freeze-and-seize/v0.1.0/UPSTREAM_PIN.json +35 -0
  9. package/dist/core/cip171.d.ts +149 -0
  10. package/dist/core/cip171.d.ts.map +1 -0
  11. package/dist/core/cip171.js +284 -0
  12. package/dist/core/cip171.js.map +1 -0
  13. package/dist/core/evo-utils.d.ts +125 -17
  14. package/dist/core/evo-utils.d.ts.map +1 -1
  15. package/dist/core/evo-utils.js +149 -21
  16. package/dist/core/evo-utils.js.map +1 -1
  17. package/dist/core/ledger-order.d.ts +109 -0
  18. package/dist/core/ledger-order.d.ts.map +1 -0
  19. package/dist/core/ledger-order.js +185 -0
  20. package/dist/core/ledger-order.js.map +1 -0
  21. package/dist/core/registry.d.ts +16 -3
  22. package/dist/core/registry.d.ts.map +1 -1
  23. package/dist/core/registry.js +18 -9
  24. package/dist/core/registry.js.map +1 -1
  25. package/dist/index.d.ts +26 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +54 -16
  28. package/dist/index.js.map +1 -1
  29. package/dist/standard/blueprint.d.ts +48 -1
  30. package/dist/standard/blueprint.d.ts.map +1 -1
  31. package/dist/standard/blueprint.js +71 -5
  32. package/dist/standard/blueprint.js.map +1 -1
  33. package/dist/standard/scripts.d.ts +76 -27
  34. package/dist/standard/scripts.d.ts.map +1 -1
  35. package/dist/standard/scripts.js +177 -49
  36. package/dist/standard/scripts.js.map +1 -1
  37. package/dist/substandards/dummy/index.d.ts.map +1 -1
  38. package/dist/substandards/dummy/index.js +443 -26
  39. package/dist/substandards/dummy/index.js.map +1 -1
  40. package/dist/substandards/freeze-and-seize/index.d.ts.map +1 -1
  41. package/dist/substandards/freeze-and-seize/index.js +283 -41
  42. package/dist/substandards/freeze-and-seize/index.js.map +1 -1
  43. package/dist/substandards/interface.d.ts +93 -1
  44. package/dist/substandards/interface.d.ts.map +1 -1
  45. package/dist/types.d.ts +87 -4
  46. package/dist/types.d.ts.map +1 -1
  47. package/package.json +4 -1
@@ -7,7 +7,8 @@
7
7
  */
8
8
  import { Address as EvoAddress, Assets, Data, Transaction, TransactionHash as EvoTransactionHash, TransactionInput as EvoTransactionInput, } from "@evolution-sdk/evolution";
9
9
  import { sortTxInputs, findRefInputIndex, findRegistryNode, findCoveringNode, utxoToTxInput, } from "../../core/registry.js";
10
- import { buildEvoScript, scriptAddress, rewardAddress, baseAddress, stakingCredentialHash, paymentCredentialHash, stringToHex, MAX_NEXT, voidData, registryNodeDatum, blacklistNodeDatum, issuanceRedeemerFirstMint, issuanceRedeemerRefInput, registryInsertRedeemer, transferActRedeemer, thirdPartyActRedeemer, blacklistInitRedeemer, blacklistAddRedeemer, blacklistRemoveRedeemer, extractConstrBytesField, extractCredentialField, getInlineDatum, utxoUnitQty, utxoLovelace, utxoTxHash, utxoOutputIndex, outputAssets, mintAssetsFromMap, Credential, KeyHash, InlineDatum, labeledAssetName, buildCIP68FTDatum, } from "../../core/evo-utils.js";
10
+ import { buildEvoScript, scriptAddress, rewardAddress, baseAddress, stakingCredentialHash, paymentCredentialHash, stringToHex, MAX_NEXT, voidData, registryNodeDatum, decodeRegistryNode, blacklistNodeDatum, mintingProofOutputIndex, mintingProofRefInput, registryInsertRedeemer, blacklistInitRedeemer, blacklistAddRedeemer, blacklistRemoveRedeemer, extractConstrBytesField, getInlineDatum, utxoUnitQty, utxoLovelace, utxoTxHash, utxoOutputIndex, outputAssets, mintAssetsFromMap, REGISTRY_NODE_MIN_ADA, Credential, KeyHash, InlineDatum, labeledAssetName, buildCIP68FTDatum, } from "../../core/evo-utils.js";
11
+ import { baseSpendRedeemer, transferRedeemer, thirdPartyRedeemer, withdrawalIndexOf, } from "../../core/ledger-order.js";
11
12
  import { createFESScripts } from "./scripts.js";
12
13
  // ---------------------------------------------------------------------------
13
14
  // Helpers
@@ -16,6 +17,20 @@ import { createFESScripts } from "./scripts.js";
16
17
  * Build the transaction and extract unsigned CBOR + txHash.
17
18
  * Works with both ReadOnlyClient (TransactionResultBase) and SigningClient (SignBuilder).
18
19
  */
20
+ /**
21
+ * The evaluator this plugin builds with, set by `init()`.
22
+ *
23
+ * Module-scoped rather than threaded through nine call sites. Without it every
24
+ * script failure in this substandard surfaces as Kupmios's bare
25
+ * "evaluateTx failed", naming neither the failing validator nor the reason —
26
+ * which is the difference between a diagnosis and a guess, and it cost a cycle
27
+ * here exactly as it did in `dummy` before the same fix.
28
+ *
29
+ * ⚠ Shared across instances of this plugin in one process. Acceptable because
30
+ * it only affects DIAGNOSTICS, never the transaction built; if that ever stops
31
+ * being true, thread it through instead.
32
+ */
33
+ let sharedEvaluator;
19
34
  async function buildAndSerialize(
20
35
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
36
  builder, changeAddress, availableUtxos, passAdditionalUtxos = false) {
@@ -23,6 +38,9 @@ builder, changeAddress, availableUtxos, passAdditionalUtxos = false) {
23
38
  const buildOpts = {
24
39
  changeAddress: EvoAddress.fromBech32(changeAddress),
25
40
  };
41
+ if (sharedEvaluator) {
42
+ buildOpts.evaluator = sharedEvaluator;
43
+ }
26
44
  if (availableUtxos) {
27
45
  buildOpts.availableUtxos = availableUtxos;
28
46
  }
@@ -42,6 +60,35 @@ builder, changeAddress, availableUtxos, passAdditionalUtxos = false) {
42
60
  }
43
61
  return { cbor, txHash, chainAvailable, _signBuilder: result };
44
62
  }
63
+ /**
64
+ * Wallet UTxOs that are SAFE TO SPEND.
65
+ *
66
+ * A deployment's reference scripts live in outputs somewhere on chain, and if
67
+ * that somewhere is the operator's own wallet — which is exactly what the
68
+ * devnet harness does — then `getUtxos(wallet)` hands them back alongside
69
+ * ordinary funds. Spending one DESTROYS protocol infrastructure: the
70
+ * `script_ref` is not carried into the change output, so the reference input
71
+ * every later operation depends on simply ceases to exist.
72
+ *
73
+ * MEASURED, not theorised: `seize` consumed the deployment's `third_party`
74
+ * reference-script UTxO this way, which is why it appeared to work at all —
75
+ * a spent input's reference script counts as supplied (CIP-33) — and why the
76
+ * next run failed with 3011.
77
+ */
78
+ function spendableWalletUtxos(walletUtxos, deployment) {
79
+ const reserved = new Set([
80
+ deployment.programmableBaseRefInput,
81
+ deployment.transferRefInput,
82
+ deployment.thirdPartyRefInput,
83
+ deployment.unfrackingRefInput,
84
+ ]
85
+ .filter(Boolean)
86
+ .map((r) => `${r.txHash}#${r.outputIndex}`));
87
+ return walletUtxos.filter((u) => {
88
+ const i = utxoToTxInput(u);
89
+ return !reserved.has(`${i.txHash}#${i.outputIndex}`);
90
+ });
91
+ }
45
92
  function selectUtxosForAmount(utxos, unit, requiredAmount) {
46
93
  const selected = [];
47
94
  let total = 0n;
@@ -62,7 +109,9 @@ function selectUtxosForAmount(utxos, unit, requiredAmount) {
62
109
  /** Find protocol params UTxO by searching for the protocol params NFT */
63
110
  async function findProtocolParamsUtxo(client, networkId, deployment) {
64
111
  const ppUnit = deployment.protocolParams.policyId + stringToHex("ProtocolParams");
65
- const addr = EvoAddress.fromBech32(scriptAddress(networkId, deployment.protocolParams.alwaysFailScriptHash));
112
+ const addr = EvoAddress.fromBech32(
113
+ // 0.5.x: params NFT lives at coordination_spend, not always_fail.
114
+ scriptAddress(networkId, deployment.coordination.scriptHash));
66
115
  const utxos = await client.getUtxosWithUnit(addr, ppUnit);
67
116
  if (utxos.length > 0)
68
117
  return utxos[0];
@@ -114,7 +163,27 @@ export function freezeAndSeizeSubstandard(config) {
114
163
  version: "0.1.0",
115
164
  blueprint: config.blueprint,
116
165
  init(context) {
166
+ // ---------------------------------------------------------------------
167
+ // MIGRATION STATE (W-E). Read this before assuming an operation works.
168
+ // ---------------------------------------------------------------------
169
+ //
170
+ // The blanket refusal that D-14 installed is GONE: `register`, `mint`,
171
+ // `transfer`, `initCompliance`, `freeze` and `unfreeze` are migrated to
172
+ // CIP-113 0.5.0-alpha.2 and this plugin initialises.
173
+ //
174
+ // `seize` and `burn` are migrated too (W-E S-5): both take the
175
+ // THIRD-PARTY route, which #110 moved off the deleted `ThirdPartyAct` and
176
+ // onto the standalone `third_party` validator's withdraw-0 with a
177
+ // ThirdPartyRedeemer, plus BaseSpendRedeemer(SpendViaThirdParty) on every
178
+ // programmable input.
179
+ //
180
+ // ⚠ The seize/burn paths still need re-validating against upstream #79
181
+ // (UTxO contamination), #80 (issuance delegation scope) and #115 (datum
182
+ // hashes and reference scripts banned on programmable outputs). Those are
183
+ // SEMANTIC changes with no signature to catch them, so a green devnet run
184
+ // is necessary and not sufficient.
117
185
  ctx = context;
186
+ sharedEvaluator = ctx.evaluator;
118
187
  networkId = ctx.client.chain.id;
119
188
  const { adminPkh, assetName, blacklistNodePolicyId, blacklistInitTxInput } = config.deployment;
120
189
  const fes = createFESScripts(config.blueprint);
@@ -140,7 +209,12 @@ export function freezeAndSeizeSubstandard(config) {
140
209
  const { feePayerAddress, assetName, quantity, recipientAddress } = params;
141
210
  const recipient = recipientAddress || feePayerAddress;
142
211
  const chainedUtxos = (params.chainedUtxos ?? []);
143
- const assetNameHex = stringToHex(assetName);
212
+ // Already hex: `assetName` is raw asset-name HEX at every API boundary in
213
+ // this SDK. This previously ran stringToHex over it, double-encoding any
214
+ // caller who followed the convention — and silently producing a DIFFERENT
215
+ // token, under a policy derived from a different issuer_admin, so nothing
216
+ // failed; the tokens simply were not where anyone looked for them.
217
+ const assetNameHex = assetName;
144
218
  const hasCIP68 = !!params.cip68Metadata;
145
219
  const client = ctx.client;
146
220
  // When CIP-68 is enabled, prefix asset names with CIP-67 labels
@@ -162,26 +236,41 @@ export function freezeAndSeizeSubstandard(config) {
162
236
  const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
163
237
  const issuanceCborHexUtxo = await findIssuanceCborHexUtxo(client, networkId, ctx.deployment);
164
238
  // 3. Build datums
165
- const coveringTransferCred = extractCredentialField(coveringDatum, 2) ?? { type: "key", hash: "" };
166
- const coveringThirdPartyCred = extractCredentialField(coveringDatum, 3) ?? { type: "key", hash: "" };
239
+ // UNREACHABLE this plugin refuses at init (T-D09). Retained, and made to
240
+ // typecheck against the 7-field layout, so the freeze-and-seize epic gets a
241
+ // readable diff instead of a rewrite from memory.
242
+ //
243
+ // The index-based reads that used to be here (2/3/4) were silently wrong
244
+ // under the 7-field layout. Replaced with decodeRegistryNode, which is
245
+ // positional-safe and fails loudly on a short record.
246
+ if (!coveringDatum) {
247
+ throw new Error("register: the covering registry node has no inline datum. The directory " +
248
+ "cannot be traversed without it — a node with no datum is malformed, not empty.");
249
+ }
250
+ const covering = decodeRegistryNode(coveringDatum);
167
251
  const updatedCoveringDatum = registryNodeDatum({
252
+ ...covering,
168
253
  key: coveringKey,
169
254
  next: scripts.tokenPolicyId,
170
- transferLogicScript: coveringTransferCred,
171
- thirdPartyTransferLogicScript: coveringThirdPartyCred,
172
- globalStateCs: extractConstrBytesField(coveringDatum, 4) ?? "",
173
255
  });
174
256
  const newRegistryNodeDatum = registryNodeDatum({
175
257
  key: scripts.tokenPolicyId,
176
258
  next: coveringNext,
259
+ mintingLogicScript: { type: "script", hash: scripts.issuerAdmin.hash },
177
260
  transferLogicScript: { type: "script", hash: scripts.transfer.hash },
178
261
  thirdPartyTransferLogicScript: { type: "script", hash: scripts.issuerAdmin.hash },
262
+ // ⚠ UNASSIGNED, NOT DECIDED. Upstream added an unfracking delegate per
263
+ // registry node; freeze-and-seize has no unfracking concept and the
264
+ // choice belongs to its own epic. Pointing it at issuerAdmin keeps the
265
+ // shape valid; it is NOT a design decision and is never emitted, because
266
+ // init() throws before any of this runs.
267
+ unfrackingLogicScript: { type: "script", hash: scripts.issuerAdmin.hash },
179
268
  globalStateCs: "",
180
269
  });
181
270
  // 4. Build redeemers — registry output index shifts when CIP-68 adds an extra output
182
271
  const registryOutputIndex = hasCIP68 ? 3 : 2;
183
- const issuanceRedeemer = issuanceRedeemerFirstMint(scripts.issuerAdmin.hash, registryOutputIndex);
184
- const registryMintRedeemer = registryInsertRedeemer(scripts.issuanceMint.hash, scripts.issuerAdmin.hash);
272
+ const issuanceRedeemer = mintingProofOutputIndex(registryOutputIndex);
273
+ const registryMintRedeemer = registryInsertRedeemer(scripts.issuanceMint.hash, { type: "script", hash: scripts.issuerAdmin.hash });
185
274
  const tokenDatum = voidData();
186
275
  // 5. Determine if chaining from initCompliance
187
276
  const useChaining = chainedUtxos.length > 0;
@@ -233,10 +322,19 @@ export function freezeAndSeizeSubstandard(config) {
233
322
  assets: outputAssets(utxoLovelace(coveringNodeUtxo), coveringNodeTokenMap),
234
323
  datum: new InlineDatum.InlineDatum({ data: updatedCoveringDatum }),
235
324
  });
236
- // New registry node (output 2 or 3)
325
+ // New registry node (output 2 or 3).
326
+ //
327
+ // 3 ADA, not 2: the RegistryNode datum is SEVEN fields in 0.5.x and
328
+ // min-UTxO scales with serialised output size. MEASURED at 2,038,630 for
329
+ // a node of this shape — the inherited 2,000,000 was sized for the
330
+ // five-field datum, and the ledger reports the shortfall as
331
+ // "insufficient Ada" with a number, never as "your datum grew".
332
+ // (The covering-node output above needs no change: it carries the
333
+ // covering UTxO's OWN lovelace forward, so it inherits whatever the
334
+ // bootstrap funded the origin with.)
237
335
  tx = tx.payToAddress({
238
336
  address: EvoAddress.fromBech32(registrySpendAddr),
239
- assets: outputAssets(2000000n, new Map([[registryNftUnit, 1n]])),
337
+ assets: outputAssets(REGISTRY_NODE_MIN_ADA, new Map([[registryNftUnit, 1n]])),
240
338
  datum: new InlineDatum.InlineDatum({ data: newRegistryNodeDatum }),
241
339
  });
242
340
  // Reference inputs
@@ -287,7 +385,7 @@ export function freezeAndSeizeSubstandard(config) {
287
385
  const sortedRefInputs = sortTxInputs([regRef]);
288
386
  const registryRefIdx = findRefInputIndex(sortedRefInputs, regRef);
289
387
  // 3. Build redeemers
290
- const issuanceRedeemer = issuanceRedeemerRefInput(scripts.issuerAdmin.hash, registryRefIdx);
388
+ const issuanceRedeemer = mintingProofRefInput(registryRefIdx);
291
389
  const tokenDatum = voidData();
292
390
  // 4. Build PLB address
293
391
  const plbHash = ctx.standardScripts.programmableLogicBase.hash;
@@ -297,7 +395,8 @@ export function freezeAndSeizeSubstandard(config) {
297
395
  // 6. Build transaction
298
396
  const tokenAssets = mintAssetsFromMap(new Map([[unit, quantity]]));
299
397
  let tx = client.newTx();
300
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
398
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
399
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
301
400
  tx = tx.withdraw({
302
401
  stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
303
402
  amount: 0n,
@@ -313,7 +412,7 @@ export function freezeAndSeizeSubstandard(config) {
313
412
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
314
413
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
315
414
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
316
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
415
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
317
416
  return { cbor, txHash, tokenPolicyId, _signBuilder };
318
417
  },
319
418
  // ====================================================================
@@ -349,8 +448,33 @@ export function freezeAndSeizeSubstandard(config) {
349
448
  const sortedRefInputs = sortTxInputs(allRefInputRefs);
350
449
  const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
351
450
  // 4. Build redeemers
352
- const issuanceRedeemer = issuanceRedeemerRefInput(scripts.issuerAdmin.hash, registryIdx);
353
- const plgRedeemer = thirdPartyActRedeemer(registryIdx, 0);
451
+ const issuanceRedeemer = mintingProofRefInput(registryIdx);
452
+ const paramsIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(protocolParamsUtxo));
453
+ // 0.5.x third-party route. `ThirdPartyAct` was DELETED by #110: the
454
+ // administrative path is now the standalone `third_party` validator, and
455
+ // programmable_logic_base dispatches to it via SpendViaThirdParty — so
456
+ // this transaction never loads the `transfer` reference script at all.
457
+ //
458
+ // The withdrawal set is BOTH scripts: the framework delegate, and the
459
+ // issuer authority the registry node names in
460
+ // `third_party_transfer_logic_script`. `third_party` requires the latter
461
+ // by name — it is the only thing standing between a holder's tokens and
462
+ // anyone who wants them, and for FES it is a real issuer-admin check
463
+ // rather than dummy's unconditional one.
464
+ const thirdPartyKey = {
465
+ hash: ctx.standardScripts.thirdParty.hash,
466
+ isScript: true,
467
+ };
468
+ const issuerAuthorityKey = { hash: scripts.issuerAdmin.hash, isScript: true };
469
+ const thirdPartyWdrlIdx = withdrawalIndexOf([thirdPartyKey, issuerAuthorityKey], thirdPartyKey);
470
+ // outputs_start_idx = 0: `third_party` PAIRS each programmable input with
471
+ // the NEXT output (same address, datum and reference script, lovelace
472
+ // ratcheting up) and reads the seized amount as the DELTA. Destination
473
+ // outputs must therefore sit AMONG THE LEADING ones it skips.
474
+ // See docs/api-reference.md — getting this backwards fails with an EMPTY
475
+ // TRACE LIST, because it is a structural expect and not a traced check.
476
+ const plgRedeemer = thirdPartyRedeemer(paramsIdx, registryIdx, 0);
477
+ const plbSpendRedeemer = baseSpendRedeemer("THIRD_PARTY", paramsIdx, thirdPartyWdrlIdx);
354
478
  const tokenDatum = voidData();
355
479
  // 5. Compute remaining assets (remove burned token's policy)
356
480
  const remainingTokens = new Map();
@@ -370,31 +494,37 @@ export function freezeAndSeizeSubstandard(config) {
370
494
  const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
371
495
  // 8. Build transaction
372
496
  let tx = client.newTx();
373
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
374
- tx = tx.collectFrom({ inputs: [utxoToBurn], redeemer: voidData() });
497
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
498
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
499
+ tx = tx.collectFrom({ inputs: [utxoToBurn], redeemer: plbSpendRedeemer });
375
500
  tx = tx.withdraw({
376
501
  stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
377
502
  amount: 0n,
378
503
  redeemer: voidData(),
379
504
  });
380
505
  tx = tx.withdraw({
381
- stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
506
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.thirdParty.hash, "hex"))),
382
507
  amount: 0n,
383
508
  redeemer: plgRedeemer,
384
509
  });
385
510
  tx = tx.payToAddress({
386
511
  address: utxoToBurn.address,
387
512
  assets: outputAssets(utxoLovelace(utxoToBurn), remainingTokens.size > 0 ? remainingTokens : undefined),
388
- datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
513
+ // The PAIRED continuation: `third_party` requires it to preserve the
514
+ // input's ADDRESS, DATUM and REFERENCE SCRIPT exactly. Carry the input's
515
+ // own datum rather than a fresh void one — they happen to be equal today
516
+ // because every programmable output here is void-datumed, and that is a
517
+ // coincidence rather than a guarantee.
518
+ datum: new InlineDatum.InlineDatum({ data: getInlineDatum(utxoToBurn) ?? tokenDatum }),
389
519
  });
390
520
  tx = tx.mintAssets({ assets: burnAssets, redeemer: issuanceRedeemer });
391
521
  tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, registryUtxo] });
392
522
  tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
393
- tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
523
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.thirdParty.compiledCode) });
394
524
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
395
525
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
396
526
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
397
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
527
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
398
528
  return { cbor, txHash, _signBuilder };
399
529
  },
400
530
  // ====================================================================
@@ -451,18 +581,56 @@ export function freezeAndSeizeSubstandard(config) {
451
581
  const sortedRefInputs = sortTxInputs(allRefInputRefs);
452
582
  const proofIndices = selected.map(() => findRefInputIndex(sortedRefInputs, utxoToTxInput(proofUtxos[0])));
453
583
  const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
584
+ const paramsIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(protocolParamsUtxo));
454
585
  // 8. Get sender's wallet UTxOs
455
586
  const senderWalletUtxos = await client.getUtxos(EvoAddress.fromBech32(senderAddress));
456
- // 9. Build redeemers
587
+ // 9. Build redeemers — 0.5.x shapes.
588
+ //
589
+ // The withdrawal set must be COMPLETE and in LEDGER order: script
590
+ // credentials before key credentials, bytewise within each. This
591
+ // transaction carries two, both scripts — the framework's `transfer`
592
+ // delegate and this substandard's own transfer logic.
593
+ const coreTransferKey = {
594
+ hash: ctx.standardScripts.transfer.hash,
595
+ isScript: true,
596
+ };
597
+ const fesLogicKey = { hash: scripts.transfer.hash, isScript: true };
598
+ const transferWdrlIdx = withdrawalIndexOf([coreTransferKey, fesLogicKey], coreTransferKey);
599
+ if (process.env.DUMP_TX_STRUCTURE) {
600
+ // Computed indices, printed beside the artefact's own order so the two
601
+ // can be COMPARED rather than reasoned about. A structural expect in
602
+ // programmable_logic_base fails with an empty trace list, so this is the
603
+ // only way to see which index is wrong.
604
+ // eslint-disable-next-line no-console
605
+ console.error("=== FES transfer COMPUTED ===\n" +
606
+ ` refs (sorted): ${sortedRefInputs.map((r) => `${r.txHash.slice(0, 8)}#${r.outputIndex}`).join(" ")}\n` +
607
+ ` paramsIdx=${paramsIdx} -> ${sortedRefInputs[paramsIdx]?.txHash?.slice(0, 8)}#${sortedRefInputs[paramsIdx]?.outputIndex}\n` +
608
+ ` registryIdx=${registryIdx}\n` +
609
+ ` coreTransfer=${ctx.standardScripts.transfer.hash}\n` +
610
+ ` fesLogic =${scripts.transfer.hash}\n` +
611
+ ` transferWdrlIdx=${transferWdrlIdx}\n` +
612
+ "=== END COMPUTED ===");
613
+ }
457
614
  const fesTransferRedeemer = Data.list(proofIndices.map((idx) => Data.constr(0n, [Data.int(BigInt(idx))])));
458
- const plgRedeemer = transferActRedeemer([{ type: "exists", nodeIdx: registryIdx }]);
459
- const spendRdmr = voidData();
615
+ // TransferRedeemer { params_idx, proofs } — params_idx was PREPENDED by
616
+ // #109; an old TransferAct encoder's bytes are not compatible.
617
+ const plgRedeemer = transferRedeemer(paramsIdx, [
618
+ { type: "exists", nodeIdx: registryIdx },
619
+ ]);
620
+ // programmable_logic_base no longer takes an untyped redeemer: it
621
+ // dispatches on the constructor and witnesses where its delegate's
622
+ // withdrawal sits.
623
+ const spendRdmr = baseSpendRedeemer("TRANSFER", paramsIdx, transferWdrlIdx);
460
624
  const tokenDatum = voidData();
461
625
  // 10. Build transaction
462
626
  let tx = client.newTx();
463
627
  tx = tx.collectFrom({ inputs: selected, redeemer: spendRdmr });
628
+ // The TRANSFER delegate — not third_party. programmable_logic_base
629
+ // resolves the withdrawal at `wdrl_idx` and requires it to equal the
630
+ // credential the params datum names for THIS dispatch arm, so withdrawing
631
+ // the wrong delegate fails the spend with an empty trace list.
464
632
  tx = tx.withdraw({
465
- stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
633
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.transfer.hash, "hex"))),
466
634
  amount: 0n,
467
635
  redeemer: plgRedeemer,
468
636
  });
@@ -485,7 +653,7 @@ export function freezeAndSeizeSubstandard(config) {
485
653
  });
486
654
  tx = tx.readFrom({ referenceInputs: [...proofUtxos, protocolParamsUtxo, registryUtxo] });
487
655
  tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
488
- tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
656
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.transfer.compiledCode) });
489
657
  tx = tx.attachScript({ script: buildEvoScript(scripts.transfer.compiledCode) });
490
658
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(senderStakingHash) });
491
659
  const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, senderAddress, senderWalletUtxos);
@@ -592,7 +760,8 @@ export function freezeAndSeizeSubstandard(config) {
592
760
  const nftAssets = mintAssetsFromMap(new Map([[nftUnit, 1n]]));
593
761
  const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
594
762
  let tx = client.newTx();
595
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
763
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
764
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
596
765
  tx = tx.collectFrom({ inputs: [coveringNode], redeemer: voidData() });
597
766
  tx = tx.mintAssets({ assets: nftAssets, redeemer: blacklistAddRedeemer(targetStakingHash) });
598
767
  // Output 0: updated covering node
@@ -615,7 +784,7 @@ export function freezeAndSeizeSubstandard(config) {
615
784
  tx = tx.attachScript({ script: buildEvoScript(scripts.blacklistMint.compiledCode) });
616
785
  const managerPkh = paymentCredentialHash(feePayerAddress);
617
786
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(managerPkh) });
618
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
787
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
619
788
  return { cbor, txHash, _signBuilder };
620
789
  },
621
790
  // ====================================================================
@@ -648,7 +817,8 @@ export function freezeAndSeizeSubstandard(config) {
648
817
  const burnAssets = mintAssetsFromMap(new Map([[nftUnit, -1n]]));
649
818
  const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
650
819
  let tx = client.newTx();
651
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
820
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
821
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
652
822
  tx = tx.collectFrom({ inputs: [nodeToRemove], redeemer: voidData() });
653
823
  tx = tx.collectFrom({ inputs: [precedingNode], redeemer: voidData() });
654
824
  tx = tx.mintAssets({ assets: burnAssets, redeemer: blacklistRemoveRedeemer(targetStakingHash) });
@@ -666,7 +836,7 @@ export function freezeAndSeizeSubstandard(config) {
666
836
  tx = tx.attachScript({ script: buildEvoScript(scripts.blacklistMint.compiledCode) });
667
837
  const managerPkh = paymentCredentialHash(feePayerAddress);
668
838
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(managerPkh) });
669
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
839
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
670
840
  return { cbor, txHash, _signBuilder };
671
841
  },
672
842
  // ====================================================================
@@ -709,11 +879,64 @@ export function freezeAndSeizeSubstandard(config) {
709
879
  if (!registryUtxo)
710
880
  throw new Error(`Registry node not found for ${tokenPolicyId}`);
711
881
  // 3. Sort reference inputs
712
- const allRefInputRefs = [utxoToTxInput(protocolParamsUtxo), utxoToTxInput(registryUtxo)];
882
+ // `third_party`'s script is supplied EXPLICITLY, as a reference input.
883
+ //
884
+ // ⚠ It used to arrive BY ACCIDENT: the bootstrap publishes reference
885
+ // scripts to the fee payer's own address, so `getUtxos(wallet)` returns
886
+ // them and `slice(0, 2)` sometimes SPENT one. A spent input's reference
887
+ // script counts as supplied (CIP-33), so the transaction validated —
888
+ // and CONSUMED the deployment's reference-script UTxO in the process.
889
+ // When the draw went the other way the script was absent entirely and
890
+ // the ledger rejected with 3011 (missing script witness). One root
891
+ // cause, two opposite symptoms; the earlier 3104 was its third face,
892
+ // an explicit attach duplicating what the spent input already supplied.
893
+ const thirdPartyRefUtxos = await client.getUtxosByOutRef([
894
+ new EvoTransactionInput.TransactionInput({
895
+ transactionId: EvoTransactionHash.fromHex(ctx.deployment.thirdPartyRefInput.txHash),
896
+ index: BigInt(ctx.deployment.thirdPartyRefInput.outputIndex),
897
+ }),
898
+ ]);
899
+ if (thirdPartyRefUtxos.length === 0) {
900
+ throw new Error(`third_party reference script not found on-chain at ` +
901
+ `${ctx.deployment.thirdPartyRefInput.txHash}#${ctx.deployment.thirdPartyRefInput.outputIndex}. ` +
902
+ `Deployment reference scripts are load-bearing: if an earlier transaction SPENT this ` +
903
+ `output, the deployment is broken and must be re-published.`);
904
+ }
905
+ const thirdPartyRefUtxo = thirdPartyRefUtxos[0];
906
+ const allRefInputRefs = [
907
+ utxoToTxInput(protocolParamsUtxo),
908
+ utxoToTxInput(registryUtxo),
909
+ utxoToTxInput(thirdPartyRefUtxo),
910
+ ];
713
911
  const sortedRefInputs = sortTxInputs(allRefInputRefs);
714
912
  const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
715
913
  // 4. Build redeemers
716
- const plgRedeemer = thirdPartyActRedeemer(registryIdx, 1);
914
+ const paramsIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(protocolParamsUtxo));
915
+ // 0.5.x third-party route. `ThirdPartyAct` was DELETED by #110: the
916
+ // administrative path is now the standalone `third_party` validator, and
917
+ // programmable_logic_base dispatches to it via SpendViaThirdParty — so
918
+ // this transaction never loads the `transfer` reference script at all.
919
+ //
920
+ // The withdrawal set is BOTH scripts: the framework delegate, and the
921
+ // issuer authority the registry node names in
922
+ // `third_party_transfer_logic_script`. `third_party` requires the latter
923
+ // by name — it is the only thing standing between a holder's tokens and
924
+ // anyone who wants them, and for FES it is a real issuer-admin check
925
+ // rather than dummy's unconditional one.
926
+ const thirdPartyKey = {
927
+ hash: ctx.standardScripts.thirdParty.hash,
928
+ isScript: true,
929
+ };
930
+ const issuerAuthorityKey = { hash: scripts.issuerAdmin.hash, isScript: true };
931
+ const thirdPartyWdrlIdx = withdrawalIndexOf([thirdPartyKey, issuerAuthorityKey], thirdPartyKey);
932
+ // outputs_start_idx = 1: `third_party` PAIRS each programmable input with
933
+ // the NEXT output (same address, datum and reference script, lovelace
934
+ // ratcheting up) and reads the seized amount as the DELTA. Destination
935
+ // outputs must therefore sit AMONG THE LEADING ones it skips.
936
+ // See docs/api-reference.md — getting this backwards fails with an EMPTY
937
+ // TRACE LIST, because it is a structural expect and not a traced check.
938
+ const plgRedeemer = thirdPartyRedeemer(paramsIdx, registryIdx, 1);
939
+ const plbSpendRedeemer = baseSpendRedeemer("THIRD_PARTY", paramsIdx, thirdPartyWdrlIdx);
717
940
  const tokenDatum = voidData();
718
941
  // 5. Build recipient PLB address
719
942
  const recipientPlbAddr = baseAddress(networkId, plbHash, destinationAddress);
@@ -729,17 +952,19 @@ export function freezeAndSeizeSubstandard(config) {
729
952
  }
730
953
  // 7. Get wallet UTxOs
731
954
  const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
955
+ // NEVER spend a deployment reference-script UTxO — see spendableWalletUtxos.
956
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
732
957
  // 8. Build transaction
733
958
  let tx = client.newTx();
734
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
735
- tx = tx.collectFrom({ inputs: [utxoToSeize], redeemer: voidData() });
959
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
960
+ tx = tx.collectFrom({ inputs: [utxoToSeize], redeemer: plbSpendRedeemer });
736
961
  tx = tx.withdraw({
737
962
  stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
738
963
  amount: 0n,
739
964
  redeemer: voidData(),
740
965
  });
741
966
  tx = tx.withdraw({
742
- stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
967
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.thirdParty.hash, "hex"))),
743
968
  amount: 0n,
744
969
  redeemer: plgRedeemer,
745
970
  });
@@ -753,14 +978,31 @@ export function freezeAndSeizeSubstandard(config) {
753
978
  tx = tx.payToAddress({
754
979
  address: utxoToSeize.address,
755
980
  assets: outputAssets(utxoLovelace(utxoToSeize), remainingTokens.size > 0 ? remainingTokens : undefined),
756
- datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
981
+ // The PAIRED continuation: `third_party` requires it to preserve the
982
+ // input's ADDRESS, DATUM and REFERENCE SCRIPT exactly. Carry the input's
983
+ // own datum rather than a fresh void one — they happen to be equal today
984
+ // because every programmable output here is void-datumed, and that is a
985
+ // coincidence rather than a guarantee.
986
+ datum: new InlineDatum.InlineDatum({ data: getInlineDatum(utxoToSeize) ?? tokenDatum }),
757
987
  });
758
- tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, registryUtxo] });
988
+ tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, registryUtxo, thirdPartyRefUtxo] });
989
+ if (process.env.DUMP_TX_STRUCTURE) {
990
+ // eslint-disable-next-line no-console
991
+ console.error("=== SEIZE attached scripts ===\n" +
992
+ ` programmableLogicBase=${ctx.standardScripts.programmableLogicBase.hash}\n` +
993
+ ` thirdParty =${ctx.standardScripts.thirdParty.hash}\n` +
994
+ ` issuerAdmin =${scripts.issuerAdmin.hash}\n` +
995
+ "=== END SEIZE SCRIPTS ===");
996
+ }
759
997
  tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
760
- tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
998
+ // `third_party` is NOT attached here. Its withdrawal already carries the
999
+ // script witness, and attaching it again makes the duplicate extraneous —
1000
+ // the ledger rejects the whole transaction with code 3104. MEASURED: the
1001
+ // hash it names is third_party's, while the withdrawal for that same
1002
+ // credential is present and correct.
761
1003
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
762
1004
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
763
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
1005
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
764
1006
  return { cbor, txHash, _signBuilder };
765
1007
  },
766
1008
  };