@easy1staking/cip113-sdk-ts 0.3.1 → 0.5.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 (51) hide show
  1. package/README.md +22 -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 +38 -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 +36 -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 +134 -17
  14. package/dist/core/evo-utils.d.ts.map +1 -1
  15. package/dist/core/evo-utils.js +162 -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 +58 -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 +124 -22
  34. package/dist/standard/scripts.d.ts.map +1 -1
  35. package/dist/standard/scripts.js +182 -55
  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 +470 -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 +314 -41
  42. package/dist/substandards/freeze-and-seize/index.js.map +1 -1
  43. package/dist/substandards/freeze-and-seize/scripts.d.ts +2 -1
  44. package/dist/substandards/freeze-and-seize/scripts.d.ts.map +1 -1
  45. package/dist/substandards/freeze-and-seize/scripts.js +7 -6
  46. package/dist/substandards/freeze-and-seize/scripts.js.map +1 -1
  47. package/dist/substandards/interface.d.ts +111 -1
  48. package/dist/substandards/interface.d.ts.map +1 -1
  49. package/dist/types.d.ts +87 -4
  50. package/dist/types.d.ts.map +1 -1
  51. package/package.json +4 -1
@@ -5,9 +5,11 @@
5
5
  *
6
6
  * Capabilities: register, mint, burn, transfer, freeze, unfreeze, seize.
7
7
  */
8
+ import { CIP171_METADATA_LABEL, buildCip171Metadatum } from "../../core/cip171.js";
8
9
  import { Address as EvoAddress, Assets, Data, Transaction, TransactionHash as EvoTransactionHash, TransactionInput as EvoTransactionInput, } from "@evolution-sdk/evolution";
9
10
  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";
11
+ 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";
12
+ import { baseSpendRedeemer, transferRedeemer, thirdPartyRedeemer, withdrawalIndexOf, } from "../../core/ledger-order.js";
11
13
  import { createFESScripts } from "./scripts.js";
12
14
  // ---------------------------------------------------------------------------
13
15
  // Helpers
@@ -16,6 +18,20 @@ import { createFESScripts } from "./scripts.js";
16
18
  * Build the transaction and extract unsigned CBOR + txHash.
17
19
  * Works with both ReadOnlyClient (TransactionResultBase) and SigningClient (SignBuilder).
18
20
  */
21
+ /**
22
+ * The evaluator this plugin builds with, set by `init()`.
23
+ *
24
+ * Module-scoped rather than threaded through nine call sites. Without it every
25
+ * script failure in this substandard surfaces as Kupmios's bare
26
+ * "evaluateTx failed", naming neither the failing validator nor the reason —
27
+ * which is the difference between a diagnosis and a guess, and it cost a cycle
28
+ * here exactly as it did in `dummy` before the same fix.
29
+ *
30
+ * ⚠ Shared across instances of this plugin in one process. Acceptable because
31
+ * it only affects DIAGNOSTICS, never the transaction built; if that ever stops
32
+ * being true, thread it through instead.
33
+ */
34
+ let sharedEvaluator;
19
35
  async function buildAndSerialize(
20
36
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
37
  builder, changeAddress, availableUtxos, passAdditionalUtxos = false) {
@@ -23,6 +39,9 @@ builder, changeAddress, availableUtxos, passAdditionalUtxos = false) {
23
39
  const buildOpts = {
24
40
  changeAddress: EvoAddress.fromBech32(changeAddress),
25
41
  };
42
+ if (sharedEvaluator) {
43
+ buildOpts.evaluator = sharedEvaluator;
44
+ }
26
45
  if (availableUtxos) {
27
46
  buildOpts.availableUtxos = availableUtxos;
28
47
  }
@@ -42,6 +61,57 @@ builder, changeAddress, availableUtxos, passAdditionalUtxos = false) {
42
61
  }
43
62
  return { cbor, txHash, chainAvailable, _signBuilder: result };
44
63
  }
64
+ /**
65
+ * Wallet UTxOs that are SAFE TO SPEND.
66
+ *
67
+ * A deployment's reference scripts live in outputs somewhere on chain, and if
68
+ * that somewhere is the operator's own wallet — which is exactly what the
69
+ * devnet harness does — then `getUtxos(wallet)` hands them back alongside
70
+ * ordinary funds. Spending one DESTROYS protocol infrastructure: the
71
+ * `script_ref` is not carried into the change output, so the reference input
72
+ * every later operation depends on simply ceases to exist.
73
+ *
74
+ * MEASURED, not theorised: `seize` consumed the deployment's `third_party`
75
+ * reference-script UTxO this way, which is why it appeared to work at all —
76
+ * a spent input's reference script counts as supplied (CIP-33) — and why the
77
+ * next run failed with 3011.
78
+ */
79
+ function spendableWalletUtxos(walletUtxos, deployment) {
80
+ const reserved = new Set([
81
+ deployment.programmableBaseRefInput,
82
+ deployment.transferRefInput,
83
+ deployment.thirdPartyRefInput,
84
+ deployment.unfrackingRefInput,
85
+ ]
86
+ .filter(Boolean)
87
+ .map((r) => `${r.txHash}#${r.outputIndex}`));
88
+ return walletUtxos.filter((u) => {
89
+ // ⛔ ANY UTxO CARRYING A REFERENCE SCRIPT IS OFF LIMITS, not merely the four
90
+ // this deployment names.
91
+ //
92
+ // MEASURED, THE EXPENSIVE WAY: the named-four filter was not enough. A
93
+ // long-lived wallet accumulates reference scripts from EVERY deployment it
94
+ // has ever made, and coin selection is free to spend any of them. On
95
+ // preview it consumed two of the LIVE deployment's own four during a
96
+ // failed retry — the deployment two VERIFIED CIP-171 records describe —
97
+ // because those four were reached through a path that did not consult this
98
+ // filter at all.
99
+ //
100
+ // Spending one destroys protocol infrastructure silently: the script_ref is
101
+ // not carried into the change output, nothing errors, and the damage
102
+ // surfaces only when a later operation needs the script. On mainnet this is
103
+ // spending live reference scripts that running contracts depend on, and it
104
+ // would look like a successful transaction.
105
+ //
106
+ // The safe rule needs no knowledge of WHOSE deployment a script belongs to:
107
+ // if a wallet UTxO carries a reference script, it is infrastructure, not
108
+ // funds.
109
+ if (u.scriptRef)
110
+ return false;
111
+ const i = utxoToTxInput(u);
112
+ return !reserved.has(`${i.txHash}#${i.outputIndex}`);
113
+ });
114
+ }
45
115
  function selectUtxosForAmount(utxos, unit, requiredAmount) {
46
116
  const selected = [];
47
117
  let total = 0n;
@@ -62,7 +132,9 @@ function selectUtxosForAmount(utxos, unit, requiredAmount) {
62
132
  /** Find protocol params UTxO by searching for the protocol params NFT */
63
133
  async function findProtocolParamsUtxo(client, networkId, deployment) {
64
134
  const ppUnit = deployment.protocolParams.policyId + stringToHex("ProtocolParams");
65
- const addr = EvoAddress.fromBech32(scriptAddress(networkId, deployment.protocolParams.alwaysFailScriptHash));
135
+ const addr = EvoAddress.fromBech32(
136
+ // 0.5.x: params NFT lives at coordination_spend, not always_fail.
137
+ scriptAddress(networkId, deployment.coordination.scriptHash));
66
138
  const utxos = await client.getUtxosWithUnit(addr, ppUnit);
67
139
  if (utxos.length > 0)
68
140
  return utxos[0];
@@ -114,7 +186,27 @@ export function freezeAndSeizeSubstandard(config) {
114
186
  version: "0.1.0",
115
187
  blueprint: config.blueprint,
116
188
  init(context) {
189
+ // ---------------------------------------------------------------------
190
+ // MIGRATION STATE (W-E). Read this before assuming an operation works.
191
+ // ---------------------------------------------------------------------
192
+ //
193
+ // The blanket refusal that D-14 installed is GONE: `register`, `mint`,
194
+ // `transfer`, `initCompliance`, `freeze` and `unfreeze` are migrated to
195
+ // CIP-113 0.5.0-alpha.2 and this plugin initialises.
196
+ //
197
+ // `seize` and `burn` are migrated too (W-E S-5): both take the
198
+ // THIRD-PARTY route, which #110 moved off the deleted `ThirdPartyAct` and
199
+ // onto the standalone `third_party` validator's withdraw-0 with a
200
+ // ThirdPartyRedeemer, plus BaseSpendRedeemer(SpendViaThirdParty) on every
201
+ // programmable input.
202
+ //
203
+ // ⚠ The seize/burn paths still need re-validating against upstream #79
204
+ // (UTxO contamination), #80 (issuance delegation scope) and #115 (datum
205
+ // hashes and reference scripts banned on programmable outputs). Those are
206
+ // SEMANTIC changes with no signature to catch them, so a green devnet run
207
+ // is necessary and not sufficient.
117
208
  ctx = context;
209
+ sharedEvaluator = ctx.evaluator;
118
210
  networkId = ctx.client.chain.id;
119
211
  const { adminPkh, assetName, blacklistNodePolicyId, blacklistInitTxInput } = config.deployment;
120
212
  const fes = createFESScripts(config.blueprint);
@@ -140,7 +232,12 @@ export function freezeAndSeizeSubstandard(config) {
140
232
  const { feePayerAddress, assetName, quantity, recipientAddress } = params;
141
233
  const recipient = recipientAddress || feePayerAddress;
142
234
  const chainedUtxos = (params.chainedUtxos ?? []);
143
- const assetNameHex = stringToHex(assetName);
235
+ // Already hex: `assetName` is raw asset-name HEX at every API boundary in
236
+ // this SDK. This previously ran stringToHex over it, double-encoding any
237
+ // caller who followed the convention — and silently producing a DIFFERENT
238
+ // token, under a policy derived from a different issuer_admin, so nothing
239
+ // failed; the tokens simply were not where anyone looked for them.
240
+ const assetNameHex = assetName;
144
241
  const hasCIP68 = !!params.cip68Metadata;
145
242
  const client = ctx.client;
146
243
  // When CIP-68 is enabled, prefix asset names with CIP-67 labels
@@ -162,26 +259,41 @@ export function freezeAndSeizeSubstandard(config) {
162
259
  const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
163
260
  const issuanceCborHexUtxo = await findIssuanceCborHexUtxo(client, networkId, ctx.deployment);
164
261
  // 3. Build datums
165
- const coveringTransferCred = extractCredentialField(coveringDatum, 2) ?? { type: "key", hash: "" };
166
- const coveringThirdPartyCred = extractCredentialField(coveringDatum, 3) ?? { type: "key", hash: "" };
262
+ // UNREACHABLE this plugin refuses at init (T-D09). Retained, and made to
263
+ // typecheck against the 7-field layout, so the freeze-and-seize epic gets a
264
+ // readable diff instead of a rewrite from memory.
265
+ //
266
+ // The index-based reads that used to be here (2/3/4) were silently wrong
267
+ // under the 7-field layout. Replaced with decodeRegistryNode, which is
268
+ // positional-safe and fails loudly on a short record.
269
+ if (!coveringDatum) {
270
+ throw new Error("register: the covering registry node has no inline datum. The directory " +
271
+ "cannot be traversed without it — a node with no datum is malformed, not empty.");
272
+ }
273
+ const covering = decodeRegistryNode(coveringDatum);
167
274
  const updatedCoveringDatum = registryNodeDatum({
275
+ ...covering,
168
276
  key: coveringKey,
169
277
  next: scripts.tokenPolicyId,
170
- transferLogicScript: coveringTransferCred,
171
- thirdPartyTransferLogicScript: coveringThirdPartyCred,
172
- globalStateCs: extractConstrBytesField(coveringDatum, 4) ?? "",
173
278
  });
174
279
  const newRegistryNodeDatum = registryNodeDatum({
175
280
  key: scripts.tokenPolicyId,
176
281
  next: coveringNext,
282
+ mintingLogicScript: { type: "script", hash: scripts.issuerAdmin.hash },
177
283
  transferLogicScript: { type: "script", hash: scripts.transfer.hash },
178
284
  thirdPartyTransferLogicScript: { type: "script", hash: scripts.issuerAdmin.hash },
285
+ // ⚠ UNASSIGNED, NOT DECIDED. Upstream added an unfracking delegate per
286
+ // registry node; freeze-and-seize has no unfracking concept and the
287
+ // choice belongs to its own epic. Pointing it at issuerAdmin keeps the
288
+ // shape valid; it is NOT a design decision and is never emitted, because
289
+ // init() throws before any of this runs.
290
+ unfrackingLogicScript: { type: "script", hash: scripts.issuerAdmin.hash },
179
291
  globalStateCs: "",
180
292
  });
181
293
  // 4. Build redeemers — registry output index shifts when CIP-68 adds an extra output
182
294
  const registryOutputIndex = hasCIP68 ? 3 : 2;
183
- const issuanceRedeemer = issuanceRedeemerFirstMint(scripts.issuerAdmin.hash, registryOutputIndex);
184
- const registryMintRedeemer = registryInsertRedeemer(scripts.issuanceMint.hash, scripts.issuerAdmin.hash);
295
+ const issuanceRedeemer = mintingProofOutputIndex(registryOutputIndex);
296
+ const registryMintRedeemer = registryInsertRedeemer(scripts.issuanceMint.hash, { type: "script", hash: scripts.issuerAdmin.hash });
185
297
  const tokenDatum = voidData();
186
298
  // 5. Determine if chaining from initCompliance
187
299
  const useChaining = chainedUtxos.length > 0;
@@ -200,6 +312,14 @@ export function freezeAndSeizeSubstandard(config) {
200
312
  const coveringNftUnit = findCoveringNodeNftUnit(coveringNodeUtxo, registryMintPolicyId);
201
313
  // 8. Build transaction
202
314
  let tx = client.newTx();
315
+ // CIP-171 provenance, carried by the transaction that parameterises the
316
+ // scripts it describes. Optional: absent, this behaves exactly as before.
317
+ if (params.cip171Record) {
318
+ tx = tx.attachMetadata({
319
+ label: CIP171_METADATA_LABEL,
320
+ metadata: buildCip171Metadatum(params.cip171Record),
321
+ });
322
+ }
203
323
  tx = tx.collectFrom({ inputs: [coveringNodeUtxo], redeemer: voidData() });
204
324
  tx = tx.withdraw({
205
325
  stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
@@ -233,10 +353,19 @@ export function freezeAndSeizeSubstandard(config) {
233
353
  assets: outputAssets(utxoLovelace(coveringNodeUtxo), coveringNodeTokenMap),
234
354
  datum: new InlineDatum.InlineDatum({ data: updatedCoveringDatum }),
235
355
  });
236
- // New registry node (output 2 or 3)
356
+ // New registry node (output 2 or 3).
357
+ //
358
+ // 3 ADA, not 2: the RegistryNode datum is SEVEN fields in 0.5.x and
359
+ // min-UTxO scales with serialised output size. MEASURED at 2,038,630 for
360
+ // a node of this shape — the inherited 2,000,000 was sized for the
361
+ // five-field datum, and the ledger reports the shortfall as
362
+ // "insufficient Ada" with a number, never as "your datum grew".
363
+ // (The covering-node output above needs no change: it carries the
364
+ // covering UTxO's OWN lovelace forward, so it inherits whatever the
365
+ // bootstrap funded the origin with.)
237
366
  tx = tx.payToAddress({
238
367
  address: EvoAddress.fromBech32(registrySpendAddr),
239
- assets: outputAssets(2000000n, new Map([[registryNftUnit, 1n]])),
368
+ assets: outputAssets(REGISTRY_NODE_MIN_ADA, new Map([[registryNftUnit, 1n]])),
240
369
  datum: new InlineDatum.InlineDatum({ data: newRegistryNodeDatum }),
241
370
  });
242
371
  // Reference inputs
@@ -287,7 +416,7 @@ export function freezeAndSeizeSubstandard(config) {
287
416
  const sortedRefInputs = sortTxInputs([regRef]);
288
417
  const registryRefIdx = findRefInputIndex(sortedRefInputs, regRef);
289
418
  // 3. Build redeemers
290
- const issuanceRedeemer = issuanceRedeemerRefInput(scripts.issuerAdmin.hash, registryRefIdx);
419
+ const issuanceRedeemer = mintingProofRefInput(registryRefIdx);
291
420
  const tokenDatum = voidData();
292
421
  // 4. Build PLB address
293
422
  const plbHash = ctx.standardScripts.programmableLogicBase.hash;
@@ -297,7 +426,8 @@ export function freezeAndSeizeSubstandard(config) {
297
426
  // 6. Build transaction
298
427
  const tokenAssets = mintAssetsFromMap(new Map([[unit, quantity]]));
299
428
  let tx = client.newTx();
300
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
429
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
430
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
301
431
  tx = tx.withdraw({
302
432
  stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
303
433
  amount: 0n,
@@ -313,7 +443,7 @@ export function freezeAndSeizeSubstandard(config) {
313
443
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
314
444
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
315
445
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
316
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
446
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
317
447
  return { cbor, txHash, tokenPolicyId, _signBuilder };
318
448
  },
319
449
  // ====================================================================
@@ -349,8 +479,33 @@ export function freezeAndSeizeSubstandard(config) {
349
479
  const sortedRefInputs = sortTxInputs(allRefInputRefs);
350
480
  const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
351
481
  // 4. Build redeemers
352
- const issuanceRedeemer = issuanceRedeemerRefInput(scripts.issuerAdmin.hash, registryIdx);
353
- const plgRedeemer = thirdPartyActRedeemer(registryIdx, 0);
482
+ const issuanceRedeemer = mintingProofRefInput(registryIdx);
483
+ const paramsIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(protocolParamsUtxo));
484
+ // 0.5.x third-party route. `ThirdPartyAct` was DELETED by #110: the
485
+ // administrative path is now the standalone `third_party` validator, and
486
+ // programmable_logic_base dispatches to it via SpendViaThirdParty — so
487
+ // this transaction never loads the `transfer` reference script at all.
488
+ //
489
+ // The withdrawal set is BOTH scripts: the framework delegate, and the
490
+ // issuer authority the registry node names in
491
+ // `third_party_transfer_logic_script`. `third_party` requires the latter
492
+ // by name — it is the only thing standing between a holder's tokens and
493
+ // anyone who wants them, and for FES it is a real issuer-admin check
494
+ // rather than dummy's unconditional one.
495
+ const thirdPartyKey = {
496
+ hash: ctx.standardScripts.thirdParty.hash,
497
+ isScript: true,
498
+ };
499
+ const issuerAuthorityKey = { hash: scripts.issuerAdmin.hash, isScript: true };
500
+ const thirdPartyWdrlIdx = withdrawalIndexOf([thirdPartyKey, issuerAuthorityKey], thirdPartyKey);
501
+ // outputs_start_idx = 0: `third_party` PAIRS each programmable input with
502
+ // the NEXT output (same address, datum and reference script, lovelace
503
+ // ratcheting up) and reads the seized amount as the DELTA. Destination
504
+ // outputs must therefore sit AMONG THE LEADING ones it skips.
505
+ // See docs/api-reference.md — getting this backwards fails with an EMPTY
506
+ // TRACE LIST, because it is a structural expect and not a traced check.
507
+ const plgRedeemer = thirdPartyRedeemer(paramsIdx, registryIdx, 0);
508
+ const plbSpendRedeemer = baseSpendRedeemer("THIRD_PARTY", paramsIdx, thirdPartyWdrlIdx);
354
509
  const tokenDatum = voidData();
355
510
  // 5. Compute remaining assets (remove burned token's policy)
356
511
  const remainingTokens = new Map();
@@ -370,31 +525,37 @@ export function freezeAndSeizeSubstandard(config) {
370
525
  const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
371
526
  // 8. Build transaction
372
527
  let tx = client.newTx();
373
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
374
- tx = tx.collectFrom({ inputs: [utxoToBurn], redeemer: voidData() });
528
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
529
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
530
+ tx = tx.collectFrom({ inputs: [utxoToBurn], redeemer: plbSpendRedeemer });
375
531
  tx = tx.withdraw({
376
532
  stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
377
533
  amount: 0n,
378
534
  redeemer: voidData(),
379
535
  });
380
536
  tx = tx.withdraw({
381
- stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
537
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.thirdParty.hash, "hex"))),
382
538
  amount: 0n,
383
539
  redeemer: plgRedeemer,
384
540
  });
385
541
  tx = tx.payToAddress({
386
542
  address: utxoToBurn.address,
387
543
  assets: outputAssets(utxoLovelace(utxoToBurn), remainingTokens.size > 0 ? remainingTokens : undefined),
388
- datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
544
+ // The PAIRED continuation: `third_party` requires it to preserve the
545
+ // input's ADDRESS, DATUM and REFERENCE SCRIPT exactly. Carry the input's
546
+ // own datum rather than a fresh void one — they happen to be equal today
547
+ // because every programmable output here is void-datumed, and that is a
548
+ // coincidence rather than a guarantee.
549
+ datum: new InlineDatum.InlineDatum({ data: getInlineDatum(utxoToBurn) ?? tokenDatum }),
389
550
  });
390
551
  tx = tx.mintAssets({ assets: burnAssets, redeemer: issuanceRedeemer });
391
552
  tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, registryUtxo] });
392
553
  tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
393
- tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
554
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.thirdParty.compiledCode) });
394
555
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
395
556
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
396
557
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
397
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
558
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
398
559
  return { cbor, txHash, _signBuilder };
399
560
  },
400
561
  // ====================================================================
@@ -451,18 +612,56 @@ export function freezeAndSeizeSubstandard(config) {
451
612
  const sortedRefInputs = sortTxInputs(allRefInputRefs);
452
613
  const proofIndices = selected.map(() => findRefInputIndex(sortedRefInputs, utxoToTxInput(proofUtxos[0])));
453
614
  const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
615
+ const paramsIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(protocolParamsUtxo));
454
616
  // 8. Get sender's wallet UTxOs
455
617
  const senderWalletUtxos = await client.getUtxos(EvoAddress.fromBech32(senderAddress));
456
- // 9. Build redeemers
618
+ // 9. Build redeemers — 0.5.x shapes.
619
+ //
620
+ // The withdrawal set must be COMPLETE and in LEDGER order: script
621
+ // credentials before key credentials, bytewise within each. This
622
+ // transaction carries two, both scripts — the framework's `transfer`
623
+ // delegate and this substandard's own transfer logic.
624
+ const coreTransferKey = {
625
+ hash: ctx.standardScripts.transfer.hash,
626
+ isScript: true,
627
+ };
628
+ const fesLogicKey = { hash: scripts.transfer.hash, isScript: true };
629
+ const transferWdrlIdx = withdrawalIndexOf([coreTransferKey, fesLogicKey], coreTransferKey);
630
+ if (process.env.DUMP_TX_STRUCTURE) {
631
+ // Computed indices, printed beside the artefact's own order so the two
632
+ // can be COMPARED rather than reasoned about. A structural expect in
633
+ // programmable_logic_base fails with an empty trace list, so this is the
634
+ // only way to see which index is wrong.
635
+ // eslint-disable-next-line no-console
636
+ console.error("=== FES transfer COMPUTED ===\n" +
637
+ ` refs (sorted): ${sortedRefInputs.map((r) => `${r.txHash.slice(0, 8)}#${r.outputIndex}`).join(" ")}\n` +
638
+ ` paramsIdx=${paramsIdx} -> ${sortedRefInputs[paramsIdx]?.txHash?.slice(0, 8)}#${sortedRefInputs[paramsIdx]?.outputIndex}\n` +
639
+ ` registryIdx=${registryIdx}\n` +
640
+ ` coreTransfer=${ctx.standardScripts.transfer.hash}\n` +
641
+ ` fesLogic =${scripts.transfer.hash}\n` +
642
+ ` transferWdrlIdx=${transferWdrlIdx}\n` +
643
+ "=== END COMPUTED ===");
644
+ }
457
645
  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();
646
+ // TransferRedeemer { params_idx, proofs } — params_idx was PREPENDED by
647
+ // #109; an old TransferAct encoder's bytes are not compatible.
648
+ const plgRedeemer = transferRedeemer(paramsIdx, [
649
+ { type: "exists", nodeIdx: registryIdx },
650
+ ]);
651
+ // programmable_logic_base no longer takes an untyped redeemer: it
652
+ // dispatches on the constructor and witnesses where its delegate's
653
+ // withdrawal sits.
654
+ const spendRdmr = baseSpendRedeemer("TRANSFER", paramsIdx, transferWdrlIdx);
460
655
  const tokenDatum = voidData();
461
656
  // 10. Build transaction
462
657
  let tx = client.newTx();
463
658
  tx = tx.collectFrom({ inputs: selected, redeemer: spendRdmr });
659
+ // The TRANSFER delegate — not third_party. programmable_logic_base
660
+ // resolves the withdrawal at `wdrl_idx` and requires it to equal the
661
+ // credential the params datum names for THIS dispatch arm, so withdrawing
662
+ // the wrong delegate fails the spend with an empty trace list.
464
663
  tx = tx.withdraw({
465
- stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
664
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.transfer.hash, "hex"))),
466
665
  amount: 0n,
467
666
  redeemer: plgRedeemer,
468
667
  });
@@ -485,7 +684,7 @@ export function freezeAndSeizeSubstandard(config) {
485
684
  });
486
685
  tx = tx.readFrom({ referenceInputs: [...proofUtxos, protocolParamsUtxo, registryUtxo] });
487
686
  tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
488
- tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
687
+ tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.transfer.compiledCode) });
489
688
  tx = tx.attachScript({ script: buildEvoScript(scripts.transfer.compiledCode) });
490
689
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(senderStakingHash) });
491
690
  const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, senderAddress, senderWalletUtxos);
@@ -592,7 +791,8 @@ export function freezeAndSeizeSubstandard(config) {
592
791
  const nftAssets = mintAssetsFromMap(new Map([[nftUnit, 1n]]));
593
792
  const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
594
793
  let tx = client.newTx();
595
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
794
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
795
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
596
796
  tx = tx.collectFrom({ inputs: [coveringNode], redeemer: voidData() });
597
797
  tx = tx.mintAssets({ assets: nftAssets, redeemer: blacklistAddRedeemer(targetStakingHash) });
598
798
  // Output 0: updated covering node
@@ -615,7 +815,7 @@ export function freezeAndSeizeSubstandard(config) {
615
815
  tx = tx.attachScript({ script: buildEvoScript(scripts.blacklistMint.compiledCode) });
616
816
  const managerPkh = paymentCredentialHash(feePayerAddress);
617
817
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(managerPkh) });
618
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
818
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
619
819
  return { cbor, txHash, _signBuilder };
620
820
  },
621
821
  // ====================================================================
@@ -648,7 +848,8 @@ export function freezeAndSeizeSubstandard(config) {
648
848
  const burnAssets = mintAssetsFromMap(new Map([[nftUnit, -1n]]));
649
849
  const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
650
850
  let tx = client.newTx();
651
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
851
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
852
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
652
853
  tx = tx.collectFrom({ inputs: [nodeToRemove], redeemer: voidData() });
653
854
  tx = tx.collectFrom({ inputs: [precedingNode], redeemer: voidData() });
654
855
  tx = tx.mintAssets({ assets: burnAssets, redeemer: blacklistRemoveRedeemer(targetStakingHash) });
@@ -666,7 +867,7 @@ export function freezeAndSeizeSubstandard(config) {
666
867
  tx = tx.attachScript({ script: buildEvoScript(scripts.blacklistMint.compiledCode) });
667
868
  const managerPkh = paymentCredentialHash(feePayerAddress);
668
869
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(managerPkh) });
669
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
870
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
670
871
  return { cbor, txHash, _signBuilder };
671
872
  },
672
873
  // ====================================================================
@@ -709,11 +910,64 @@ export function freezeAndSeizeSubstandard(config) {
709
910
  if (!registryUtxo)
710
911
  throw new Error(`Registry node not found for ${tokenPolicyId}`);
711
912
  // 3. Sort reference inputs
712
- const allRefInputRefs = [utxoToTxInput(protocolParamsUtxo), utxoToTxInput(registryUtxo)];
913
+ // `third_party`'s script is supplied EXPLICITLY, as a reference input.
914
+ //
915
+ // ⚠ It used to arrive BY ACCIDENT: the bootstrap publishes reference
916
+ // scripts to the fee payer's own address, so `getUtxos(wallet)` returns
917
+ // them and `slice(0, 2)` sometimes SPENT one. A spent input's reference
918
+ // script counts as supplied (CIP-33), so the transaction validated —
919
+ // and CONSUMED the deployment's reference-script UTxO in the process.
920
+ // When the draw went the other way the script was absent entirely and
921
+ // the ledger rejected with 3011 (missing script witness). One root
922
+ // cause, two opposite symptoms; the earlier 3104 was its third face,
923
+ // an explicit attach duplicating what the spent input already supplied.
924
+ const thirdPartyRefUtxos = await client.getUtxosByOutRef([
925
+ new EvoTransactionInput.TransactionInput({
926
+ transactionId: EvoTransactionHash.fromHex(ctx.deployment.thirdPartyRefInput.txHash),
927
+ index: BigInt(ctx.deployment.thirdPartyRefInput.outputIndex),
928
+ }),
929
+ ]);
930
+ if (thirdPartyRefUtxos.length === 0) {
931
+ throw new Error(`third_party reference script not found on-chain at ` +
932
+ `${ctx.deployment.thirdPartyRefInput.txHash}#${ctx.deployment.thirdPartyRefInput.outputIndex}. ` +
933
+ `Deployment reference scripts are load-bearing: if an earlier transaction SPENT this ` +
934
+ `output, the deployment is broken and must be re-published.`);
935
+ }
936
+ const thirdPartyRefUtxo = thirdPartyRefUtxos[0];
937
+ const allRefInputRefs = [
938
+ utxoToTxInput(protocolParamsUtxo),
939
+ utxoToTxInput(registryUtxo),
940
+ utxoToTxInput(thirdPartyRefUtxo),
941
+ ];
713
942
  const sortedRefInputs = sortTxInputs(allRefInputRefs);
714
943
  const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
715
944
  // 4. Build redeemers
716
- const plgRedeemer = thirdPartyActRedeemer(registryIdx, 1);
945
+ const paramsIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(protocolParamsUtxo));
946
+ // 0.5.x third-party route. `ThirdPartyAct` was DELETED by #110: the
947
+ // administrative path is now the standalone `third_party` validator, and
948
+ // programmable_logic_base dispatches to it via SpendViaThirdParty — so
949
+ // this transaction never loads the `transfer` reference script at all.
950
+ //
951
+ // The withdrawal set is BOTH scripts: the framework delegate, and the
952
+ // issuer authority the registry node names in
953
+ // `third_party_transfer_logic_script`. `third_party` requires the latter
954
+ // by name — it is the only thing standing between a holder's tokens and
955
+ // anyone who wants them, and for FES it is a real issuer-admin check
956
+ // rather than dummy's unconditional one.
957
+ const thirdPartyKey = {
958
+ hash: ctx.standardScripts.thirdParty.hash,
959
+ isScript: true,
960
+ };
961
+ const issuerAuthorityKey = { hash: scripts.issuerAdmin.hash, isScript: true };
962
+ const thirdPartyWdrlIdx = withdrawalIndexOf([thirdPartyKey, issuerAuthorityKey], thirdPartyKey);
963
+ // outputs_start_idx = 1: `third_party` PAIRS each programmable input with
964
+ // the NEXT output (same address, datum and reference script, lovelace
965
+ // ratcheting up) and reads the seized amount as the DELTA. Destination
966
+ // outputs must therefore sit AMONG THE LEADING ones it skips.
967
+ // See docs/api-reference.md — getting this backwards fails with an EMPTY
968
+ // TRACE LIST, because it is a structural expect and not a traced check.
969
+ const plgRedeemer = thirdPartyRedeemer(paramsIdx, registryIdx, 1);
970
+ const plbSpendRedeemer = baseSpendRedeemer("THIRD_PARTY", paramsIdx, thirdPartyWdrlIdx);
717
971
  const tokenDatum = voidData();
718
972
  // 5. Build recipient PLB address
719
973
  const recipientPlbAddr = baseAddress(networkId, plbHash, destinationAddress);
@@ -729,17 +983,19 @@ export function freezeAndSeizeSubstandard(config) {
729
983
  }
730
984
  // 7. Get wallet UTxOs
731
985
  const walletUtxos = await client.getUtxos(EvoAddress.fromBech32(feePayerAddress));
986
+ // NEVER spend a deployment reference-script UTxO — see spendableWalletUtxos.
987
+ const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
732
988
  // 8. Build transaction
733
989
  let tx = client.newTx();
734
- tx = tx.collectFrom({ inputs: walletUtxos.slice(0, 2) });
735
- tx = tx.collectFrom({ inputs: [utxoToSeize], redeemer: voidData() });
990
+ tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
991
+ tx = tx.collectFrom({ inputs: [utxoToSeize], redeemer: plbSpendRedeemer });
736
992
  tx = tx.withdraw({
737
993
  stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(scripts.issuerAdmin.hash, "hex"))),
738
994
  amount: 0n,
739
995
  redeemer: voidData(),
740
996
  });
741
997
  tx = tx.withdraw({
742
- stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
998
+ stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.thirdParty.hash, "hex"))),
743
999
  amount: 0n,
744
1000
  redeemer: plgRedeemer,
745
1001
  });
@@ -753,14 +1009,31 @@ export function freezeAndSeizeSubstandard(config) {
753
1009
  tx = tx.payToAddress({
754
1010
  address: utxoToSeize.address,
755
1011
  assets: outputAssets(utxoLovelace(utxoToSeize), remainingTokens.size > 0 ? remainingTokens : undefined),
756
- datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
1012
+ // The PAIRED continuation: `third_party` requires it to preserve the
1013
+ // input's ADDRESS, DATUM and REFERENCE SCRIPT exactly. Carry the input's
1014
+ // own datum rather than a fresh void one — they happen to be equal today
1015
+ // because every programmable output here is void-datumed, and that is a
1016
+ // coincidence rather than a guarantee.
1017
+ datum: new InlineDatum.InlineDatum({ data: getInlineDatum(utxoToSeize) ?? tokenDatum }),
757
1018
  });
758
- tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, registryUtxo] });
1019
+ tx = tx.readFrom({ referenceInputs: [protocolParamsUtxo, registryUtxo, thirdPartyRefUtxo] });
1020
+ if (process.env.DUMP_TX_STRUCTURE) {
1021
+ // eslint-disable-next-line no-console
1022
+ console.error("=== SEIZE attached scripts ===\n" +
1023
+ ` programmableLogicBase=${ctx.standardScripts.programmableLogicBase.hash}\n` +
1024
+ ` thirdParty =${ctx.standardScripts.thirdParty.hash}\n` +
1025
+ ` issuerAdmin =${scripts.issuerAdmin.hash}\n` +
1026
+ "=== END SEIZE SCRIPTS ===");
1027
+ }
759
1028
  tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
760
- tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
1029
+ // `third_party` is NOT attached here. Its withdrawal already carries the
1030
+ // script witness, and attaching it again makes the duplicate extraneous —
1031
+ // the ledger rejects the whole transaction with code 3104. MEASURED: the
1032
+ // hash it names is third_party's, while the withdrawal for that same
1033
+ // credential is present and correct.
761
1034
  tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
762
1035
  tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
763
- const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, walletUtxos);
1036
+ const { cbor, txHash, _signBuilder } = await buildAndSerialize(tx, feePayerAddress, spendableUtxos);
764
1037
  return { cbor, txHash, _signBuilder };
765
1038
  },
766
1039
  };