@easy1staking/cip113-sdk-ts 0.7.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -2
- package/blueprints/standard/v0.5.0-alpha.3/UPSTREAM_PIN.json +35 -0
- package/blueprints/standard/v0.5.0-alpha.3/plutus.json +1126 -0
- package/blueprints/standard/v0.5.0-alpha.4/UPSTREAM_PIN.json +36 -0
- package/blueprints/standard/v0.5.0-alpha.4/plutus.json +1434 -0
- package/dist/core/evo-utils.d.ts +357 -39
- package/dist/core/evo-utils.d.ts.map +1 -1
- package/dist/core/evo-utils.js +513 -27
- package/dist/core/evo-utils.js.map +1 -1
- package/dist/core/ledger-order.d.ts +204 -18
- package/dist/core/ledger-order.d.ts.map +1 -1
- package/dist/core/ledger-order.js +385 -28
- package/dist/core/ledger-order.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/standard/blueprint.d.ts +129 -29
- package/dist/standard/blueprint.d.ts.map +1 -1
- package/dist/standard/blueprint.js +245 -47
- package/dist/standard/blueprint.js.map +1 -1
- package/dist/standard/scripts.d.ts +189 -75
- package/dist/standard/scripts.d.ts.map +1 -1
- package/dist/standard/scripts.js +454 -123
- package/dist/standard/scripts.js.map +1 -1
- package/dist/substandards/dummy/index.d.ts +19 -0
- package/dist/substandards/dummy/index.d.ts.map +1 -1
- package/dist/substandards/dummy/index.js +322 -73
- package/dist/substandards/dummy/index.js.map +1 -1
- package/dist/substandards/freeze-and-seize/index.d.ts.map +1 -1
- package/dist/substandards/freeze-and-seize/index.js +455 -117
- package/dist/substandards/freeze-and-seize/index.js.map +1 -1
- package/dist/types.d.ts +161 -48
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -8,8 +8,28 @@
|
|
|
8
8
|
import { CIP171_METADATA_LABEL, buildCip171Metadatum } from "../../core/cip171.js";
|
|
9
9
|
import { Address as EvoAddress, Assets, Data, Transaction, TransactionHash as EvoTransactionHash, TransactionInput as EvoTransactionInput, } from "@evolution-sdk/evolution";
|
|
10
10
|
import { sortTxInputs, findRefInputIndex, findRegistryNode, findCoveringNode, utxoToTxInput, } from "../../core/registry.js";
|
|
11
|
-
import { buildEvoScript, scriptAddress, rewardAddress, baseAddress, stakingCredentialHash, paymentCredentialHash, stringToHex, MAX_NEXT, voidData, registryNodeDatum, decodeRegistryNode, blacklistNodeDatum,
|
|
12
|
-
import { baseSpendRedeemer, transferRedeemer, thirdPartyRedeemer,
|
|
11
|
+
import { buildEvoScript, scriptAddress, rewardAddress, baseAddress, stakingCredentialHash, paymentCredentialHash, stringToHex, MAX_NEXT, voidData, registryNodeDatum, decodeRegistryNode, blacklistNodeDatum, registryInsertRedeemer, blacklistInitRedeemer, blacklistAddRedeemer, blacklistRemoveRedeemer, extractConstrBytesField, getInlineDatum, assertProtocolParamsIssuanceLogic, utxoUnitQty, utxoLovelace, utxoTxHash, utxoOutputIndex, outputAssets, mintAssetsFromMap, REGISTRY_NODE_MIN_ADA, Credential, KeyHash, InlineDatum, labeledAssetName, buildCIP68FTDatum, inlineDatumBytes, assertInlineDatumWithinBound, minUtxoAtLeast, ceilToWholeAda, } from "../../core/evo-utils.js";
|
|
12
|
+
import { baseSpendRedeemer, transferRedeemer, thirdPartyRedeemer, plbWithdrawalPlan, issuancePlan, programmableLogicGlobalRedeemer, } from "../../core/ledger-order.js";
|
|
13
|
+
/**
|
|
14
|
+
* The flat amounts this plugin used to hardcode, kept as FLOORS rather than as
|
|
15
|
+
* answers — see `minUtxoAtLeast`. Every output that carries caller-controlled
|
|
16
|
+
* bytes now computes its requirement from live protocol parameters and takes
|
|
17
|
+
* the larger of the two, so ordinary inputs emit exactly what they always did
|
|
18
|
+
* and only the cases that were genuinely short move.
|
|
19
|
+
*
|
|
20
|
+
* MEASURED 2026-09-01 against preview (`coinsPerUtxoByte` 4310), which is how
|
|
21
|
+
* we know these were not merely theoretical:
|
|
22
|
+
* - the (100) reference output needed 3,021,310 with every CIP-68 field at a
|
|
23
|
+
* consumer's own documented caps and a 12-byte asset name — the flat
|
|
24
|
+
* 3,000,000 was SHORT by 21,310. With a maximal 32-byte CIP-67 name the
|
|
25
|
+
* same datum needs 3,111,820. Both axes are caller-controlled and this
|
|
26
|
+
* package caps NEITHER, so the true ceiling is unbounded.
|
|
27
|
+
* - a token output with a 32-byte CIP-67 asset name needed 1,318,860 — the
|
|
28
|
+
* flat 1,300,000 was SHORT by 18,860, rising to 44,720 at a maximal
|
|
29
|
+
* quantity.
|
|
30
|
+
*/
|
|
31
|
+
const TOKEN_OUTPUT_FLOOR = 1300000n;
|
|
32
|
+
const CIP68_REFERENCE_OUTPUT_FLOOR = 3000000n;
|
|
13
33
|
import { createFESScripts } from "./scripts.js";
|
|
14
34
|
// ---------------------------------------------------------------------------
|
|
15
35
|
// Helpers
|
|
@@ -77,14 +97,27 @@ builder, changeAddress, availableUtxos, passAdditionalUtxos = false) {
|
|
|
77
97
|
* next run failed with 3011.
|
|
78
98
|
*/
|
|
79
99
|
function spendableWalletUtxos(walletUtxos, deployment) {
|
|
80
|
-
const
|
|
100
|
+
const reservedRefs = [
|
|
81
101
|
deployment.programmableBaseRefInput,
|
|
102
|
+
// alpha.3: the dispatcher's reference script joined the set the bootstrap
|
|
103
|
+
// publishes. Naming it here keeps the explicit list complete — the general
|
|
104
|
+
// scriptRef rule below already covers it, but a named list that silently
|
|
105
|
+
// omits a live deployment's script invites the next reader to trust it.
|
|
106
|
+
// alpha.4 adds issuance_logic and upgrade_multisig; the newest member is
|
|
107
|
+
// exactly the one least likely to be covered anywhere else.
|
|
108
|
+
deployment.programmableLogicGlobalRefInput,
|
|
82
109
|
deployment.transferRefInput,
|
|
83
110
|
deployment.thirdPartyRefInput,
|
|
84
111
|
deployment.unfrackingRefInput,
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
-
|
|
112
|
+
deployment.issuanceLogicRefInput,
|
|
113
|
+
deployment.upgradeMultisigRefInput,
|
|
114
|
+
].filter(Boolean);
|
|
115
|
+
if (reservedRefs.length !== 7) {
|
|
116
|
+
throw new Error(`spendableWalletUtxos: the named deployment reference-script reservation list must ` +
|
|
117
|
+
`contain exactly 7 entries; got ${reservedRefs.length}. A membership-only list ` +
|
|
118
|
+
`decays into a stale subset and silently stops protecting its newest member.`);
|
|
119
|
+
}
|
|
120
|
+
const reserved = new Set(reservedRefs.map((r) => `${r.txHash}#${r.outputIndex}`));
|
|
88
121
|
return walletUtxos.filter((u) => {
|
|
89
122
|
// ⛔ ANY UTxO CARRYING A REFERENCE SCRIPT IS OFF LIMITS, not merely the four
|
|
90
123
|
// this deployment names.
|
|
@@ -133,11 +166,15 @@ function selectUtxosForAmount(utxos, unit, requiredAmount) {
|
|
|
133
166
|
async function findProtocolParamsUtxo(client, networkId, deployment) {
|
|
134
167
|
const ppUnit = deployment.protocolParams.policyId + stringToHex("ProtocolParams");
|
|
135
168
|
const addr = EvoAddress.fromBech32(
|
|
136
|
-
//
|
|
137
|
-
|
|
169
|
+
// alpha.3: protocol_params is one validator whose hash is BOTH the NFT
|
|
170
|
+
// policy and the address holding it — the minting policy naming itself.
|
|
171
|
+
scriptAddress(networkId, deployment.protocolParams.policyId));
|
|
138
172
|
const utxos = await client.getUtxosWithUnit(addr, ppUnit);
|
|
139
|
-
if (utxos.length > 0)
|
|
140
|
-
|
|
173
|
+
if (utxos.length > 0) {
|
|
174
|
+
const utxo = utxos[0];
|
|
175
|
+
assertProtocolParamsIssuanceLogic(utxo, deployment.issuanceLogic.scriptHash);
|
|
176
|
+
return utxo;
|
|
177
|
+
}
|
|
141
178
|
throw new Error(`Protocol params UTxO not found (unit: ${ppUnit})`);
|
|
142
179
|
}
|
|
143
180
|
/** Find issuance CBOR hex UTxO */
|
|
@@ -149,6 +186,28 @@ async function findIssuanceCborHexUtxo(client, networkId, deployment) {
|
|
|
149
186
|
return utxos[0];
|
|
150
187
|
throw new Error(`Issuance CBOR hex UTxO not found (unit: ${icUnit})`);
|
|
151
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* The `issuance_logic` reference-script UTxO.
|
|
191
|
+
*
|
|
192
|
+
* Its withdraw-0 rides on every mint and burn, making this the most
|
|
193
|
+
* load-bearing reference-script output in the deployment. Attaching the body
|
|
194
|
+
* here would hide a spent deployment output instead of naming the breakage.
|
|
195
|
+
*/
|
|
196
|
+
async function findIssuanceLogicRefUtxo(client, deployment) {
|
|
197
|
+
const { txHash, outputIndex } = deployment.issuanceLogicRefInput;
|
|
198
|
+
const utxos = await client.getUtxosByOutRef([
|
|
199
|
+
new EvoTransactionInput.TransactionInput({
|
|
200
|
+
transactionId: EvoTransactionHash.fromHex(txHash),
|
|
201
|
+
index: BigInt(outputIndex),
|
|
202
|
+
}),
|
|
203
|
+
]);
|
|
204
|
+
if (utxos.length === 0) {
|
|
205
|
+
throw new Error(`issuance_logic reference script not found on-chain at ${txHash}#${outputIndex}. ` +
|
|
206
|
+
`Deployment reference scripts are load-bearing: if an earlier transaction SPENT this ` +
|
|
207
|
+
`output, the deployment is broken and must be re-published.`);
|
|
208
|
+
}
|
|
209
|
+
return utxos[0];
|
|
210
|
+
}
|
|
152
211
|
/** Find the NFT unit in a covering node's value that belongs to a given policy */
|
|
153
212
|
function findCoveringNodeNftUnit(utxo, policyId) {
|
|
154
213
|
const units = Assets.getUnits(utxo.assets);
|
|
@@ -191,14 +250,18 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
191
250
|
// ---------------------------------------------------------------------
|
|
192
251
|
//
|
|
193
252
|
// The blanket refusal that D-14 installed is GONE: `register`, `mint`,
|
|
194
|
-
//
|
|
195
|
-
//
|
|
253
|
+
// Every operation is migrated to CIP-113 0.5.0-alpha.3.
|
|
254
|
+
//
|
|
255
|
+
// `seize` and `burn` take the THIRD-PARTY route: the standalone
|
|
256
|
+
// `third_party` validator's withdraw-0 with a ThirdPartyRedeemer, reached
|
|
257
|
+
// through the `programmable_logic_global` dispatcher, plus a
|
|
258
|
+
// BaseSpendRedeemer on every programmable input.
|
|
196
259
|
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
260
|
+
// ⚠ In alpha.2 the dispatch choice lived on that BaseSpendRedeemer as
|
|
261
|
+
// `SpendViaThirdParty`. alpha.3 made the redeemer a single-constructor
|
|
262
|
+
// record and moved the choice to the dispatcher's own redeemer. The two
|
|
263
|
+
// encodings are BYTE-IDENTICAL for the transfer arm, so a stale builder
|
|
264
|
+
// is not rejected — see the guards in core/ledger-order.ts.
|
|
202
265
|
//
|
|
203
266
|
// ⚠ The seize/burn paths still need re-validating against upstream #79
|
|
204
267
|
// (UTxO contamination), #80 (issuance delegation scope) and #115 (datum
|
|
@@ -246,9 +309,27 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
246
309
|
// Reference token (only when CIP-68)
|
|
247
310
|
const refAssetNameHex = hasCIP68 ? labeledAssetName(100, assetNameHex) : null;
|
|
248
311
|
const refUnit = refAssetNameHex ? scripts.tokenPolicyId + refAssetNameHex : null;
|
|
312
|
+
let cip68Datum;
|
|
313
|
+
let cip68DatumBytes;
|
|
314
|
+
if (hasCIP68) {
|
|
315
|
+
cip68Datum = buildCIP68FTDatum(params.cip68Metadata);
|
|
316
|
+
// alpha.4's issuance_logic `no_escape` now applies
|
|
317
|
+
// is_seizable_output_shape_bounded to PLB outputs carrying the minted
|
|
318
|
+
// policy. lib/assets.ak marks CIP-68's former exemption "(expired)": a
|
|
319
|
+
// data-URI logo over the bound fails on chain with an empty trace.
|
|
320
|
+
// Refuse before selecting UTxOs or constructing the transaction so the
|
|
321
|
+
// caller gets the measured, named diagnosis instead.
|
|
322
|
+
assertInlineDatumWithinBound(cip68Datum, ctx.deployment.maxInlineDatumBytes, "the CIP-68 (100) reference datum");
|
|
323
|
+
cip68DatumBytes = inlineDatumBytes(cip68Datum);
|
|
324
|
+
}
|
|
249
325
|
// 1. Find covering registry node
|
|
250
|
-
|
|
251
|
-
|
|
326
|
+
// ⚑ ONE HASH, TWO ROLES. registry_mint and registry_spend merged (#117), so
|
|
327
|
+
// the node NFT policy id and the node address's payment credential are the
|
|
328
|
+
// same value — the minting policy naming itself. The old names
|
|
329
|
+
// (registrySpendAddr / registryMintPolicyId) asserted a distinction that no
|
|
330
|
+
// longer exists and would tell a reader to look for two hashes.
|
|
331
|
+
const registryAddr = scriptAddress(networkId, ctx.standardScripts.registry.hash);
|
|
332
|
+
const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registryAddr));
|
|
252
333
|
const coveringNodeUtxo = findCoveringNode(registryUtxos, scripts.tokenPolicyId);
|
|
253
334
|
if (!coveringNodeUtxo)
|
|
254
335
|
throw new Error("Could not find covering registry node for insertion");
|
|
@@ -258,14 +339,19 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
258
339
|
// 2. Get reference inputs
|
|
259
340
|
const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
|
|
260
341
|
const issuanceCborHexUtxo = await findIssuanceCborHexUtxo(client, networkId, ctx.deployment);
|
|
342
|
+
const issuanceLogicRefUtxo = await findIssuanceLogicRefUtxo(client, ctx.deployment);
|
|
343
|
+
// One array supplies both the plan's index arithmetic and the transaction.
|
|
344
|
+
// Constructing either set twice would let two valid integer indices drift.
|
|
345
|
+
const refUtxos = [protocolParamsUtxo, issuanceCborHexUtxo, issuanceLogicRefUtxo];
|
|
261
346
|
// 3. Build datums
|
|
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
347
|
// The index-based reads that used to be here (2/3/4) were silently wrong
|
|
267
|
-
//
|
|
348
|
+
// when RegistryNode grew. Replaced with decodeRegistryNode, which is
|
|
268
349
|
// positional-safe and fails loudly on a short record.
|
|
350
|
+
//
|
|
351
|
+
// ⚑ RegistryNode is STILL SEVEN FIELDS in alpha.3 — it is the PARAMS datum
|
|
352
|
+
// that went 7 -> 4, not this one. Two adjacent records, one changed and one
|
|
353
|
+
// did not, is exactly the pairing that invites a "consistency" edit; the
|
|
354
|
+
// decoder's arity check is what stops it.
|
|
269
355
|
if (!coveringDatum) {
|
|
270
356
|
throw new Error("register: the covering registry node has no inline datum. The directory " +
|
|
271
357
|
"cannot be traversed without it — a node with no datum is malformed, not empty.");
|
|
@@ -290,9 +376,32 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
290
376
|
unfrackingLogicScript: { type: "script", hash: scripts.issuerAdmin.hash },
|
|
291
377
|
globalStateCs: "",
|
|
292
378
|
});
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
379
|
+
// The former branch-dependent registry-output literal is gone. The index
|
|
380
|
+
// now derives from the declared output set, so adding an output shifts it
|
|
381
|
+
// automatically instead of requiring a second edit in step. Its CIP-68
|
|
382
|
+
// branch has never executed on chain: no test has passed
|
|
383
|
+
// `cip68Metadata` yet; T-F04-4 owns that first execution.
|
|
384
|
+
const outputTags = [
|
|
385
|
+
"user-token",
|
|
386
|
+
...(hasCIP68 ? ["cip68-reference"] : []),
|
|
387
|
+
"covering-node",
|
|
388
|
+
"new-node",
|
|
389
|
+
];
|
|
390
|
+
// No plgHash: register spends a registry node and wallet funds, never a
|
|
391
|
+
// programmable_logic_base input.
|
|
392
|
+
const plan = issuancePlan({
|
|
393
|
+
issuanceLogicHash: ctx.deployment.issuanceLogic.scriptHash,
|
|
394
|
+
otherWithdrawals: [{ hash: scripts.issuerAdmin.hash, isScript: true }],
|
|
395
|
+
referenceInputs: refUtxos.map(utxoToTxInput),
|
|
396
|
+
paramsRefInput: utxoToTxInput(protocolParamsUtxo),
|
|
397
|
+
outputs: outputTags,
|
|
398
|
+
issued: [
|
|
399
|
+
{
|
|
400
|
+
policyId: scripts.tokenPolicyId,
|
|
401
|
+
proof: { kind: "output", tag: "new-node" },
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
});
|
|
296
405
|
const registryMintRedeemer = registryInsertRedeemer(scripts.issuanceMint.hash, { type: "script", hash: scripts.issuerAdmin.hash });
|
|
297
406
|
const tokenDatum = voidData();
|
|
298
407
|
// 5. Determine if chaining from initCompliance
|
|
@@ -300,16 +409,16 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
300
409
|
// 6. Build addresses
|
|
301
410
|
const plbHash = ctx.standardScripts.programmableLogicBase.hash;
|
|
302
411
|
const recipientPlbAddr = baseAddress(networkId, plbHash, recipient);
|
|
303
|
-
const
|
|
412
|
+
const registryPolicyId = ctx.standardScripts.registry.hash;
|
|
304
413
|
// 7. Build asset maps — include CIP-68 ref token in the same mint (same policy + redeemer)
|
|
305
414
|
const mintEntries = new Map([[unit, quantity]]);
|
|
306
415
|
if (hasCIP68 && refUnit) {
|
|
307
416
|
mintEntries.set(refUnit, 1n);
|
|
308
417
|
}
|
|
309
418
|
const tokenAssets = mintAssetsFromMap(mintEntries);
|
|
310
|
-
const registryNftUnit =
|
|
419
|
+
const registryNftUnit = registryPolicyId + scripts.tokenPolicyId;
|
|
311
420
|
const registryNftAssets = mintAssetsFromMap(new Map([[registryNftUnit, 1n]]));
|
|
312
|
-
const coveringNftUnit = findCoveringNodeNftUnit(coveringNodeUtxo,
|
|
421
|
+
const coveringNftUnit = findCoveringNodeNftUnit(coveringNodeUtxo, registryPolicyId);
|
|
313
422
|
// 8. Build transaction
|
|
314
423
|
let tx = client.newTx();
|
|
315
424
|
// CIP-171 provenance, carried by the transaction that parameterises the
|
|
@@ -321,39 +430,70 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
321
430
|
});
|
|
322
431
|
}
|
|
323
432
|
tx = tx.collectFrom({ inputs: [coveringNodeUtxo], redeemer: voidData() });
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
433
|
+
const withdrawalSpecs = [
|
|
434
|
+
{ hash: scripts.issuerAdmin.hash, redeemer: voidData() },
|
|
435
|
+
{
|
|
436
|
+
hash: ctx.deployment.issuanceLogic.scriptHash,
|
|
437
|
+
redeemer: plan.issuanceLogicRedeemer,
|
|
438
|
+
},
|
|
439
|
+
];
|
|
440
|
+
if (withdrawalSpecs.length !== plan.withdrawals.length) {
|
|
441
|
+
throw new Error(`register: issuance plan declares ${plan.withdrawals.length} withdrawals but ` +
|
|
442
|
+
`${withdrawalSpecs.length} are emitted`);
|
|
443
|
+
}
|
|
444
|
+
for (const withdrawal of withdrawalSpecs) {
|
|
445
|
+
tx = tx.withdraw({
|
|
446
|
+
stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(withdrawal.hash, "hex"))),
|
|
447
|
+
amount: 0n,
|
|
448
|
+
redeemer: withdrawal.redeemer,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
tx = tx.mintAssets({ assets: tokenAssets, redeemer: plan.issuanceRedeemer });
|
|
330
452
|
tx = tx.mintAssets({ assets: registryNftAssets, redeemer: registryMintRedeemer });
|
|
331
|
-
//
|
|
453
|
+
// min-UTxO is sized from live protocol parameters, not guessed: `unit`
|
|
454
|
+
// carries a caller-supplied asset name and `quantity` a caller-supplied
|
|
455
|
+
// magnitude, and both widen the serialised output.
|
|
456
|
+
const coinsPerUtxoByte = (await client.getProtocolParameters()).coinsPerUtxoByte;
|
|
457
|
+
// outputTags: "user-token" — minted supply at the recipient's PLB.
|
|
332
458
|
tx = tx.payToAddress({
|
|
333
459
|
address: EvoAddress.fromBech32(recipientPlbAddr),
|
|
334
|
-
assets: outputAssets(
|
|
460
|
+
assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
|
|
461
|
+
address: recipientPlbAddr,
|
|
462
|
+
assets: outputAssets(0n, new Map([[unit, quantity]])),
|
|
463
|
+
datum: tokenDatum,
|
|
464
|
+
coinsPerUtxoByte,
|
|
465
|
+
}), new Map([[unit, quantity]])),
|
|
335
466
|
datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
|
|
336
467
|
});
|
|
337
|
-
//
|
|
468
|
+
// outputTags: "cip68-reference" — optional reference token and metadata.
|
|
338
469
|
if (hasCIP68 && refUnit) {
|
|
339
470
|
const issuerPlbAddr = baseAddress(networkId, plbHash, feePayerAddress);
|
|
340
|
-
|
|
471
|
+
// ⛔ THE ONE OUTPUT SIZED BY USER-SUPPLIED STRINGS. Rounded UP TO A
|
|
472
|
+
// WHOLE ADA deliberately: the reference implementation this SDK is
|
|
473
|
+
// diffed against does the same, and a builder-equivalence check is only
|
|
474
|
+
// useful while both sides agree. The cost is at most ~1 ADA, once, on an
|
|
475
|
+
// output that exists for the life of the token.
|
|
341
476
|
tx = tx.payToAddress({
|
|
342
477
|
address: EvoAddress.fromBech32(issuerPlbAddr),
|
|
343
|
-
assets: outputAssets(
|
|
478
|
+
assets: outputAssets(ceilToWholeAda(minUtxoAtLeast(CIP68_REFERENCE_OUTPUT_FLOOR, {
|
|
479
|
+
address: issuerPlbAddr,
|
|
480
|
+
assets: outputAssets(0n, new Map([[refUnit, 1n]])),
|
|
481
|
+
datum: cip68Datum,
|
|
482
|
+
coinsPerUtxoByte,
|
|
483
|
+
})), new Map([[refUnit, 1n]])),
|
|
344
484
|
datum: new InlineDatum.InlineDatum({ data: cip68Datum }),
|
|
345
485
|
});
|
|
346
486
|
}
|
|
347
|
-
//
|
|
487
|
+
// outputTags: "covering-node" — predecessor with its updated link.
|
|
348
488
|
const coveringNodeTokenMap = new Map();
|
|
349
489
|
if (coveringNftUnit)
|
|
350
490
|
coveringNodeTokenMap.set(coveringNftUnit, 1n);
|
|
351
491
|
tx = tx.payToAddress({
|
|
352
|
-
address: EvoAddress.fromBech32(
|
|
492
|
+
address: EvoAddress.fromBech32(registryAddr),
|
|
353
493
|
assets: outputAssets(utxoLovelace(coveringNodeUtxo), coveringNodeTokenMap),
|
|
354
494
|
datum: new InlineDatum.InlineDatum({ data: updatedCoveringDatum }),
|
|
355
495
|
});
|
|
356
|
-
//
|
|
496
|
+
// outputTags: "new-node" — the node named by the issuance proof.
|
|
357
497
|
//
|
|
358
498
|
// 3 ADA, not 2: the RegistryNode datum is SEVEN fields in 0.5.x and
|
|
359
499
|
// min-UTxO scales with serialised output size. MEASURED at 2,038,630 for
|
|
@@ -364,17 +504,43 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
364
504
|
// covering UTxO's OWN lovelace forward, so it inherits whatever the
|
|
365
505
|
// bootstrap funded the origin with.)
|
|
366
506
|
tx = tx.payToAddress({
|
|
367
|
-
address: EvoAddress.fromBech32(
|
|
507
|
+
address: EvoAddress.fromBech32(registryAddr),
|
|
368
508
|
assets: outputAssets(REGISTRY_NODE_MIN_ADA, new Map([[registryNftUnit, 1n]])),
|
|
369
509
|
datum: new InlineDatum.InlineDatum({ data: newRegistryNodeDatum }),
|
|
370
510
|
});
|
|
371
511
|
// Reference inputs
|
|
372
|
-
|
|
512
|
+
// ⛔ THE PROTOCOL-PARAMS REFERENCE INPUT STAYS — and as of S-11 that is a
|
|
513
|
+
// MEASURED decision, not a cautious one.
|
|
514
|
+
//
|
|
515
|
+
// alpha.3's registry no longer reads it (#117). `issuance_mint` does, to
|
|
516
|
+
// pull the LIVE delegate credentials from its datum, and that locate is
|
|
517
|
+
// DELIBERATELY NON-FAILING: absent params UTxO means "no delegation",
|
|
518
|
+
// falling back to local `no_escape` custody. So removing it raises no
|
|
519
|
+
// error — it silently selects a different custody path.
|
|
520
|
+
//
|
|
521
|
+
// MEASURED ON DEVNET (S-11): removing it from BOTH substandards' register
|
|
522
|
+
// paths, the full lifecycle still passes — dummy 2/2, FES 3/3. The two
|
|
523
|
+
// custody branches do converge here, as the reasoning predicted.
|
|
524
|
+
//
|
|
525
|
+
// ⇒ AND IT STAYS ANYWAY, for a reason the measurement itself supplies:
|
|
526
|
+
// the tests pass EITHER WAY. That is precisely what makes dropping it
|
|
527
|
+
// unsafe to keep. Nothing in this suite would notice if `issuance_mint`'s
|
|
528
|
+
// delegation semantics changed and the absent input started to matter —
|
|
529
|
+
// the guard against that is the input being there, not a test. The saving
|
|
530
|
+
// is one reference input on a once-per-token transaction; the exposure is
|
|
531
|
+
// a silent custody change nobody would see.
|
|
532
|
+
// alpha.4 makes the params reference input mandatory rather than an
|
|
533
|
+
// optional custody hint: with_protocol_params_fields uses expect_at.
|
|
534
|
+
tx = tx.readFrom({ referenceInputs: refUtxos });
|
|
373
535
|
// Attach scripts
|
|
374
|
-
|
|
536
|
+
// ⚑ ONE attach, not two. registry_mint and registry_spend merged into a
|
|
537
|
+
// single validator (#117): this transaction both MINTS a node NFT and
|
|
538
|
+
// SPENDS the covering node, which in alpha.2 needed two scripts. It is
|
|
539
|
+
// now one script serving both purposes — attaching it twice would put a
|
|
540
|
+
// duplicate witness in the transaction.
|
|
541
|
+
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.registry.compiledCode) });
|
|
375
542
|
tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
|
|
376
543
|
tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
|
|
377
|
-
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.registryMint.compiledCode) });
|
|
378
544
|
// Required signer
|
|
379
545
|
tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
|
|
380
546
|
const built = await buildAndSerialize(tx, feePayerAddress, useChaining ? chainedUtxos : undefined, useChaining);
|
|
@@ -386,8 +552,12 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
386
552
|
metadata: {
|
|
387
553
|
issuerAdminScriptHash: scripts.issuerAdmin.hash,
|
|
388
554
|
transferScriptHash: scripts.transfer.hash,
|
|
555
|
+
outputIndices: {
|
|
556
|
+
OUT_NEW_NODE: plan.outputIndexOf("new-node"),
|
|
557
|
+
},
|
|
389
558
|
...(hasCIP68 && {
|
|
390
559
|
cip68Enabled: true,
|
|
560
|
+
cip68DatumBytes,
|
|
391
561
|
userAssetNameHex,
|
|
392
562
|
refAssetNameHex,
|
|
393
563
|
}),
|
|
@@ -406,17 +576,30 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
406
576
|
throw new Error(`Token policy ${tokenPolicyId} does not match this FES instance (${scripts.tokenPolicyId})`);
|
|
407
577
|
}
|
|
408
578
|
// 1. Find registry node as RefInput proof
|
|
409
|
-
const
|
|
410
|
-
const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(
|
|
579
|
+
const registryAddr = scriptAddress(networkId, ctx.standardScripts.registry.hash);
|
|
580
|
+
const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registryAddr));
|
|
411
581
|
const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
|
|
412
582
|
if (!registryUtxo)
|
|
413
583
|
throw new Error(`Registry node not found for ${tokenPolicyId}`);
|
|
414
|
-
// 2.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
584
|
+
// 2. Find the two alpha.4 protocol reference inputs. The params input is
|
|
585
|
+
// NEW here: with_protocol_params_fields begins with a hard expect_at,
|
|
586
|
+
// and S-7's open mint-vs-burn inconsistency is answered at this site.
|
|
587
|
+
const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
|
|
588
|
+
const issuanceLogicRefUtxo = await findIssuanceLogicRefUtxo(client, ctx.deployment);
|
|
589
|
+
const refUtxos = [protocolParamsUtxo, registryUtxo, issuanceLogicRefUtxo];
|
|
590
|
+
const plan = issuancePlan({
|
|
591
|
+
issuanceLogicHash: ctx.deployment.issuanceLogic.scriptHash,
|
|
592
|
+
otherWithdrawals: [{ hash: scripts.issuerAdmin.hash, isScript: true }],
|
|
593
|
+
referenceInputs: refUtxos.map(utxoToTxInput),
|
|
594
|
+
paramsRefInput: utxoToTxInput(protocolParamsUtxo),
|
|
595
|
+
issued: [
|
|
596
|
+
{
|
|
597
|
+
policyId: tokenPolicyId,
|
|
598
|
+
proof: { kind: "reference-input", input: utxoToTxInput(registryUtxo) },
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
});
|
|
418
602
|
// 3. Build redeemers
|
|
419
|
-
const issuanceRedeemer = mintingProofRefInput(registryRefIdx);
|
|
420
603
|
const tokenDatum = voidData();
|
|
421
604
|
// 4. Build PLB address
|
|
422
605
|
const plbHash = ctx.standardScripts.programmableLogicBase.hash;
|
|
@@ -428,18 +611,37 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
428
611
|
let tx = client.newTx();
|
|
429
612
|
const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
|
|
430
613
|
tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
614
|
+
const withdrawalSpecs = [
|
|
615
|
+
{ hash: scripts.issuerAdmin.hash, redeemer: voidData() },
|
|
616
|
+
{
|
|
617
|
+
hash: ctx.deployment.issuanceLogic.scriptHash,
|
|
618
|
+
redeemer: plan.issuanceLogicRedeemer,
|
|
619
|
+
},
|
|
620
|
+
];
|
|
621
|
+
if (withdrawalSpecs.length !== plan.withdrawals.length) {
|
|
622
|
+
throw new Error(`mint: issuance plan declares ${plan.withdrawals.length} withdrawals but ` +
|
|
623
|
+
`${withdrawalSpecs.length} are emitted`);
|
|
624
|
+
}
|
|
625
|
+
for (const withdrawal of withdrawalSpecs) {
|
|
626
|
+
tx = tx.withdraw({
|
|
627
|
+
stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(withdrawal.hash, "hex"))),
|
|
628
|
+
amount: 0n,
|
|
629
|
+
redeemer: withdrawal.redeemer,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
tx = tx.mintAssets({ assets: tokenAssets, redeemer: plan.issuanceRedeemer });
|
|
633
|
+
const coinsPerUtxoByte = (await client.getProtocolParameters()).coinsPerUtxoByte;
|
|
437
634
|
tx = tx.payToAddress({
|
|
438
635
|
address: EvoAddress.fromBech32(recipientPlbAddr),
|
|
439
|
-
assets: outputAssets(
|
|
636
|
+
assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
|
|
637
|
+
address: recipientPlbAddr,
|
|
638
|
+
assets: outputAssets(0n, new Map([[unit, quantity]])),
|
|
639
|
+
datum: tokenDatum,
|
|
640
|
+
coinsPerUtxoByte,
|
|
641
|
+
}), new Map([[unit, quantity]])),
|
|
440
642
|
datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
|
|
441
643
|
});
|
|
442
|
-
tx = tx.readFrom({ referenceInputs:
|
|
644
|
+
tx = tx.readFrom({ referenceInputs: refUtxos });
|
|
443
645
|
tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
|
|
444
646
|
tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
|
|
445
647
|
tx = tx.addSigner({ keyHash: KeyHash.fromHex(config.deployment.adminPkh) });
|
|
@@ -469,22 +671,18 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
469
671
|
throw new Error(`No tokens of ${unit} in UTxO`);
|
|
470
672
|
// 2. Find reference inputs
|
|
471
673
|
const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
|
|
472
|
-
const
|
|
473
|
-
const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(
|
|
674
|
+
const registryAddr = scriptAddress(networkId, ctx.standardScripts.registry.hash);
|
|
675
|
+
const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registryAddr));
|
|
474
676
|
const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
|
|
475
677
|
if (!registryUtxo)
|
|
476
678
|
throw new Error(`Registry node not found for ${tokenPolicyId}`);
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
const sortedRefInputs = sortTxInputs(allRefInputRefs);
|
|
480
|
-
const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
|
|
481
|
-
// 4. Build redeemers
|
|
482
|
-
const issuanceRedeemer = mintingProofRefInput(registryIdx);
|
|
483
|
-
const paramsIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(protocolParamsUtxo));
|
|
679
|
+
const issuanceLogicRefUtxo = await findIssuanceLogicRefUtxo(client, ctx.deployment);
|
|
680
|
+
const refUtxos = [protocolParamsUtxo, registryUtxo, issuanceLogicRefUtxo];
|
|
484
681
|
// 0.5.x third-party route. `ThirdPartyAct` was DELETED by #110: the
|
|
485
682
|
// administrative path is now the standalone `third_party` validator, and
|
|
486
|
-
// programmable_logic_base
|
|
487
|
-
// this transaction never loads the `transfer`
|
|
683
|
+
// programmable_logic_base withdraws through the dispatcher, whose redeemer
|
|
684
|
+
// carries ThirdPartyAct — so this transaction never loads the `transfer`
|
|
685
|
+
// reference script at all.
|
|
488
686
|
//
|
|
489
687
|
// The withdrawal set is BOTH scripts: the framework delegate, and the
|
|
490
688
|
// issuer authority the registry node names in
|
|
@@ -497,15 +695,36 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
497
695
|
isScript: true,
|
|
498
696
|
};
|
|
499
697
|
const issuerAuthorityKey = { hash: scripts.issuerAdmin.hash, isScript: true };
|
|
500
|
-
|
|
698
|
+
// One plan owns the COMPLETE four-withdrawal and three-reference-input
|
|
699
|
+
// sets. In particular, adding issuance_logic's reference script can move
|
|
700
|
+
// every reference-input index that sorts after it.
|
|
701
|
+
const plan = issuancePlan({
|
|
702
|
+
issuanceLogicHash: ctx.deployment.issuanceLogic.scriptHash,
|
|
703
|
+
plgHash: ctx.standardScripts.programmableLogicGlobal.hash,
|
|
704
|
+
otherWithdrawals: [thirdPartyKey, issuerAuthorityKey],
|
|
705
|
+
referenceInputs: refUtxos.map(utxoToTxInput),
|
|
706
|
+
paramsRefInput: utxoToTxInput(protocolParamsUtxo),
|
|
707
|
+
issued: [
|
|
708
|
+
{
|
|
709
|
+
policyId: tokenPolicyId,
|
|
710
|
+
proof: { kind: "reference-input", input: utxoToTxInput(registryUtxo) },
|
|
711
|
+
},
|
|
712
|
+
],
|
|
713
|
+
});
|
|
714
|
+
const registryIdx = plan.referenceInputIndexOf(utxoToTxInput(registryUtxo));
|
|
501
715
|
// outputs_start_idx = 0: `third_party` PAIRS each programmable input with
|
|
502
716
|
// the NEXT output (same address, datum and reference script, lovelace
|
|
503
717
|
// ratcheting up) and reads the seized amount as the DELTA. Destination
|
|
504
718
|
// outputs must therefore sit AMONG THE LEADING ones it skips.
|
|
505
719
|
// See docs/api-reference.md — getting this backwards fails with an EMPTY
|
|
506
720
|
// TRACE LIST, because it is a structural expect and not a traced check.
|
|
507
|
-
|
|
508
|
-
|
|
721
|
+
// params_idx dropped: delegates no longer read the params datum.
|
|
722
|
+
// ThirdPartyRedeemer.registry_node_idx and the issuance map's
|
|
723
|
+
// RefInput{index} MUST be the same integer: upstream
|
|
724
|
+
// third_party_covers_own_registry_node compares them. One plan computes
|
|
725
|
+
// both, so the pair cannot drift when a reference input is added.
|
|
726
|
+
const plgRedeemer = thirdPartyRedeemer(registryIdx, 0);
|
|
727
|
+
const plbSpendRedeemer = baseSpendRedeemer(plan.paramsIdx, plan.plgIdx());
|
|
509
728
|
const tokenDatum = voidData();
|
|
510
729
|
// 5. Compute remaining assets (remove burned token's policy)
|
|
511
730
|
const remainingTokens = new Map();
|
|
@@ -528,16 +747,38 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
528
747
|
const spendableUtxos = spendableWalletUtxos(walletUtxos, ctx.deployment);
|
|
529
748
|
tx = tx.collectFrom({ inputs: spendableUtxos.slice(0, 2) });
|
|
530
749
|
tx = tx.collectFrom({ inputs: [utxoToBurn], redeemer: plbSpendRedeemer });
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
750
|
+
const withdrawalSpecs = [
|
|
751
|
+
{ hash: scripts.issuerAdmin.hash, redeemer: voidData() },
|
|
752
|
+
{
|
|
753
|
+
hash: ctx.standardScripts.programmableLogicGlobal.hash,
|
|
754
|
+
redeemer: programmableLogicGlobalRedeemer("THIRD_PARTY"),
|
|
755
|
+
},
|
|
756
|
+
{ hash: ctx.standardScripts.thirdParty.hash, redeemer: plgRedeemer },
|
|
757
|
+
{
|
|
758
|
+
hash: ctx.deployment.issuanceLogic.scriptHash,
|
|
759
|
+
redeemer: plan.issuanceLogicRedeemer,
|
|
760
|
+
},
|
|
761
|
+
];
|
|
762
|
+
if (withdrawalSpecs.length !== plan.withdrawals.length) {
|
|
763
|
+
throw new Error(`burn: issuance plan declares ${plan.withdrawals.length} withdrawals but ` +
|
|
764
|
+
`${withdrawalSpecs.length} are emitted`);
|
|
765
|
+
}
|
|
766
|
+
for (const withdrawal of withdrawalSpecs) {
|
|
767
|
+
tx = tx.withdraw({
|
|
768
|
+
stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(withdrawal.hash, "hex"))),
|
|
769
|
+
amount: 0n,
|
|
770
|
+
redeemer: withdrawal.redeemer,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
// ⚑ NO min-UTxO COMPUTATION HERE, AND THAT IS CORRECT — do not "fix" it to
|
|
774
|
+
// match the other outputs. This is the PAIRED CONTINUATION: `third_party`
|
|
775
|
+
// requires it to preserve the input's address, datum and reference script
|
|
776
|
+
// with lovelace only ratcheting UP, so it carries the INPUT'S OWN lovelace
|
|
777
|
+
// forward. That figure was already min-UTxO-valid when the input was
|
|
778
|
+
// created, and burning REMOVES tokens — a smaller output has a LOWER
|
|
779
|
+
// requirement. Carrying it forward is therefore always sufficient, and
|
|
780
|
+
// recomputing could only produce a smaller number, which the ratchet rule
|
|
781
|
+
// forbids.
|
|
541
782
|
tx = tx.payToAddress({
|
|
542
783
|
address: utxoToBurn.address,
|
|
543
784
|
assets: outputAssets(utxoLovelace(utxoToBurn), remainingTokens.size > 0 ? remainingTokens : undefined),
|
|
@@ -548,9 +789,15 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
548
789
|
// coincidence rather than a guarantee.
|
|
549
790
|
datum: new InlineDatum.InlineDatum({ data: getInlineDatum(utxoToBurn) ?? tokenDatum }),
|
|
550
791
|
});
|
|
551
|
-
tx = tx.mintAssets({ assets: burnAssets, redeemer: issuanceRedeemer });
|
|
552
|
-
tx = tx.readFrom({ referenceInputs:
|
|
792
|
+
tx = tx.mintAssets({ assets: burnAssets, redeemer: plan.issuanceRedeemer });
|
|
793
|
+
tx = tx.readFrom({ referenceInputs: refUtxos });
|
|
553
794
|
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
|
|
795
|
+
// ⛔ THE DISPATCHER'S OWN SCRIPT WITNESS. A withdraw-0 needs the script in
|
|
796
|
+
// full, not just a redeemer — alpha.3 added the dispatcher's withdrawal to
|
|
797
|
+
// every programmable transaction, and S-6 wired the withdrawal and the
|
|
798
|
+
// redeemer but not this. The ledger says "An associated script witness is
|
|
799
|
+
// missing" on purpose=withdraw, which names the shape but not the script.
|
|
800
|
+
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
|
|
554
801
|
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.thirdParty.compiledCode) });
|
|
555
802
|
tx = tx.attachScript({ script: buildEvoScript(scripts.issuerAdmin.compiledCode) });
|
|
556
803
|
tx = tx.attachScript({ script: buildEvoScript(scripts.issuanceMint.compiledCode) });
|
|
@@ -582,8 +829,8 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
582
829
|
const { selected, totalTokenAmount } = selectUtxosForAmount(tokenUtxos, unit, quantity);
|
|
583
830
|
const returningAmount = totalTokenAmount - quantity;
|
|
584
831
|
// 4. Find registry node reference input
|
|
585
|
-
const
|
|
586
|
-
const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(
|
|
832
|
+
const registryAddr = scriptAddress(networkId, ctx.standardScripts.registry.hash);
|
|
833
|
+
const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registryAddr));
|
|
587
834
|
const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
|
|
588
835
|
if (!registryUtxo)
|
|
589
836
|
throw new Error(`Registry node not found for ${tokenPolicyId}`);
|
|
@@ -610,6 +857,27 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
610
857
|
utxoToTxInput(registryUtxo),
|
|
611
858
|
];
|
|
612
859
|
const sortedRefInputs = sortTxInputs(allRefInputRefs);
|
|
860
|
+
// ⚠ NOTE THE ASYMMETRY WITH `register`, WHICH S-11 MEASURED: there,
|
|
861
|
+
// removing the params reference input is behaviour-preserving. HERE it is
|
|
862
|
+
// not, and the reason has nothing to do with custody — see below. Two
|
|
863
|
+
// transactions, one shared input, two unrelated dependencies on it.
|
|
864
|
+
//
|
|
865
|
+
// ⛔ A SECOND REASON THE PROTOCOL-PARAMS REFERENCE INPUT MUST STAY, beyond
|
|
866
|
+
// the custody one recorded at `register`.
|
|
867
|
+
//
|
|
868
|
+
// Every index below — the blacklist proof indices, registryIdx, paramsIdx
|
|
869
|
+
// — is a position in THIS sorted set. FES's own transfer validator reads
|
|
870
|
+
// its blacklist nodes with `list.at(reference_inputs, node_idx)`, an
|
|
871
|
+
// INDEX, not a search. Removing the params UTxO from the set therefore
|
|
872
|
+
// shifts every proof index that sorts after it, and the failure is a
|
|
873
|
+
// blacklist proof resolving to the WRONG NODE — a well-formed proof about
|
|
874
|
+
// something else.
|
|
875
|
+
//
|
|
876
|
+
// So "the registry no longer needs this input" (#117) is true and still
|
|
877
|
+
// does not license dropping it: two unrelated consumers in this same
|
|
878
|
+
// transaction depend on it, one for custody semantics and one for index
|
|
879
|
+
// arithmetic. Requirements belong to the TRANSACTION, not to the
|
|
880
|
+
// component being edited.
|
|
613
881
|
const proofIndices = selected.map(() => findRefInputIndex(sortedRefInputs, utxoToTxInput(proofUtxos[0])));
|
|
614
882
|
const registryIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(registryUtxo));
|
|
615
883
|
const paramsIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(protocolParamsUtxo));
|
|
@@ -618,15 +886,19 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
618
886
|
// 9. Build redeemers — 0.5.x shapes.
|
|
619
887
|
//
|
|
620
888
|
// The withdrawal set must be COMPLETE and in LEDGER order: script
|
|
621
|
-
// credentials before key credentials, bytewise within each.
|
|
622
|
-
//
|
|
623
|
-
// delegate and this substandard's own transfer logic.
|
|
889
|
+
// credentials before key credentials, bytewise within each. alpha.3 makes
|
|
890
|
+
// it THREE, all scripts — the dispatcher, the framework's `transfer`
|
|
891
|
+
// delegate, and this substandard's own transfer logic. Every index in the
|
|
892
|
+
// transaction shifted when the dispatcher joined.
|
|
624
893
|
const coreTransferKey = {
|
|
625
894
|
hash: ctx.standardScripts.transfer.hash,
|
|
626
895
|
isScript: true,
|
|
627
896
|
};
|
|
628
897
|
const fesLogicKey = { hash: scripts.transfer.hash, isScript: true };
|
|
629
|
-
const
|
|
898
|
+
const transferPlan = plbWithdrawalPlan({
|
|
899
|
+
plgHash: ctx.standardScripts.programmableLogicGlobal.hash,
|
|
900
|
+
others: [coreTransferKey, fesLogicKey],
|
|
901
|
+
});
|
|
630
902
|
if (process.env.DUMP_TX_STRUCTURE) {
|
|
631
903
|
// Computed indices, printed beside the artefact's own order so the two
|
|
632
904
|
// can be COMPARED rather than reasoned about. A structural expect in
|
|
@@ -639,19 +911,16 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
639
911
|
` registryIdx=${registryIdx}\n` +
|
|
640
912
|
` coreTransfer=${ctx.standardScripts.transfer.hash}\n` +
|
|
641
913
|
` fesLogic =${scripts.transfer.hash}\n` +
|
|
642
|
-
`
|
|
914
|
+
` plgWdrlIdx=${transferPlan.plgIdx}\n` +
|
|
643
915
|
"=== END COMPUTED ===");
|
|
644
916
|
}
|
|
645
917
|
const fesTransferRedeemer = Data.list(proofIndices.map((idx) => Data.constr(0n, [Data.int(BigInt(idx))])));
|
|
646
|
-
// TransferRedeemer {
|
|
647
|
-
//
|
|
648
|
-
const plgRedeemer = transferRedeemer(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
// dispatches on the constructor and witnesses where its delegate's
|
|
653
|
-
// withdrawal sits.
|
|
654
|
-
const spendRdmr = baseSpendRedeemer("TRANSFER", paramsIdx, transferWdrlIdx);
|
|
918
|
+
// TransferRedeemer { proofs } — params_idx was dropped in alpha.3; the
|
|
919
|
+
// delegates stop reading the params datum entirely.
|
|
920
|
+
const plgRedeemer = transferRedeemer([{ type: "exists", nodeIdx: registryIdx }]);
|
|
921
|
+
// PLB witnesses where the DISPATCHER's withdrawal sits; the dispatch
|
|
922
|
+
// choice itself moved to the dispatcher's own redeemer.
|
|
923
|
+
const spendRdmr = baseSpendRedeemer(paramsIdx, transferPlan.plgIdx);
|
|
655
924
|
const tokenDatum = voidData();
|
|
656
925
|
// 10. Build transaction
|
|
657
926
|
let tx = client.newTx();
|
|
@@ -660,6 +929,13 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
660
929
|
// resolves the withdrawal at `wdrl_idx` and requires it to equal the
|
|
661
930
|
// credential the params datum names for THIS dispatch arm, so withdrawing
|
|
662
931
|
// the wrong delegate fails the spend with an empty trace list.
|
|
932
|
+
// The DISPATCHER's own withdraw-0 — new in alpha.3, required on every
|
|
933
|
+
// programmable transaction, and the entry PLB's wdrl_idx resolves to.
|
|
934
|
+
tx = tx.withdraw({
|
|
935
|
+
stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
|
|
936
|
+
amount: 0n,
|
|
937
|
+
redeemer: programmableLogicGlobalRedeemer("TRANSFER"),
|
|
938
|
+
});
|
|
663
939
|
tx = tx.withdraw({
|
|
664
940
|
stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.transfer.hash, "hex"))),
|
|
665
941
|
amount: 0n,
|
|
@@ -670,20 +946,37 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
670
946
|
amount: 0n,
|
|
671
947
|
redeemer: fesTransferRedeemer,
|
|
672
948
|
});
|
|
949
|
+
const coinsPerUtxoByte = (await client.getProtocolParameters()).coinsPerUtxoByte;
|
|
673
950
|
if (returningAmount > 0n) {
|
|
674
951
|
tx = tx.payToAddress({
|
|
675
952
|
address: EvoAddress.fromBech32(senderPlbAddr),
|
|
676
|
-
assets: outputAssets(
|
|
953
|
+
assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
|
|
954
|
+
address: senderPlbAddr,
|
|
955
|
+
assets: outputAssets(0n, new Map([[unit, returningAmount]])),
|
|
956
|
+
datum: tokenDatum,
|
|
957
|
+
coinsPerUtxoByte,
|
|
958
|
+
}), new Map([[unit, returningAmount]])),
|
|
677
959
|
datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
|
|
678
960
|
});
|
|
679
961
|
}
|
|
680
962
|
tx = tx.payToAddress({
|
|
681
963
|
address: EvoAddress.fromBech32(recipientPlbAddr),
|
|
682
|
-
assets: outputAssets(
|
|
964
|
+
assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
|
|
965
|
+
address: recipientPlbAddr,
|
|
966
|
+
assets: outputAssets(0n, new Map([[unit, quantity]])),
|
|
967
|
+
datum: tokenDatum,
|
|
968
|
+
coinsPerUtxoByte,
|
|
969
|
+
}), new Map([[unit, quantity]])),
|
|
683
970
|
datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
|
|
684
971
|
});
|
|
685
972
|
tx = tx.readFrom({ referenceInputs: [...proofUtxos, protocolParamsUtxo, registryUtxo] });
|
|
686
973
|
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
|
|
974
|
+
// ⛔ THE DISPATCHER'S OWN SCRIPT WITNESS. A withdraw-0 needs the script in
|
|
975
|
+
// full, not just a redeemer — alpha.3 added the dispatcher's withdrawal to
|
|
976
|
+
// every programmable transaction, and S-6 wired the withdrawal and the
|
|
977
|
+
// redeemer but not this. The ledger says "An associated script witness is
|
|
978
|
+
// missing" on purpose=withdraw, which names the shape but not the script.
|
|
979
|
+
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
|
|
687
980
|
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.transfer.compiledCode) });
|
|
688
981
|
tx = tx.attachScript({ script: buildEvoScript(scripts.transfer.compiledCode) });
|
|
689
982
|
tx = tx.addSigner({ keyHash: KeyHash.fromHex(senderStakingHash) });
|
|
@@ -729,7 +1022,16 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
729
1022
|
address: EvoAddress.fromBech32(feePayerAddress),
|
|
730
1023
|
assets: outputAssets(40000000n),
|
|
731
1024
|
});
|
|
732
|
-
// Output: blacklist origin node
|
|
1025
|
+
// Output: blacklist origin node.
|
|
1026
|
+
//
|
|
1027
|
+
// ✅ FLAT CONSTANT IS CORRECT HERE, AND IT IS PROVABLE RATHER THAN
|
|
1028
|
+
// ASSUMED — this output carries NO caller-controlled bytes. The unit is
|
|
1029
|
+
// `blacklistMint.hash + ""` (an EMPTY asset name, fixed above), and the
|
|
1030
|
+
// datum is `blacklistNodeDatum("", MAX_NEXT)` — an empty key and a
|
|
1031
|
+
// 30-byte sentinel, both constants. MEASURED on preview
|
|
1032
|
+
// (coinsPerUtxoByte 4310): 1,198,180 required against 1,300,000 supplied,
|
|
1033
|
+
// 101,820 of headroom, and the live origin node on preview sits at
|
|
1034
|
+
// exactly 1,300,000. Nothing a caller types can move it.
|
|
733
1035
|
tx = tx.payToAddress({
|
|
734
1036
|
address: EvoAddress.fromBech32(blacklistSpendAddr),
|
|
735
1037
|
assets: outputAssets(1300000n, new Map([[blacklistOriginUnit, 1n]])),
|
|
@@ -805,7 +1107,17 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
805
1107
|
assets: outputAssets(utxoLovelace(coveringNode), coveringTokenMap),
|
|
806
1108
|
datum: new InlineDatum.InlineDatum({ data: updatedCoveringDatum }),
|
|
807
1109
|
});
|
|
808
|
-
// Output 1: new blacklist node
|
|
1110
|
+
// Output 1: new blacklist node.
|
|
1111
|
+
//
|
|
1112
|
+
// ✅ FLAT CONSTANT IS CORRECT HERE TOO, for the same provable reason. The
|
|
1113
|
+
// unit is `blacklistMint.hash + targetStakingHash` — a 28-byte staking
|
|
1114
|
+
// credential, fixed width — and the datum is two hashes. A blacklist entry
|
|
1115
|
+
// has no user-supplied strings in it at all. MEASURED: 1,448,160 required
|
|
1116
|
+
// against 2,000,000 supplied, 551,840 of headroom.
|
|
1117
|
+
//
|
|
1118
|
+
// ⚠ If a future node datum gains a field, this stops being true silently.
|
|
1119
|
+
// That is exactly how REGISTRY_NODE_MIN_ADA went wrong when the registry
|
|
1120
|
+
// datum grew from five fields to seven.
|
|
809
1121
|
tx = tx.payToAddress({
|
|
810
1122
|
address: EvoAddress.fromBech32(blacklistSpendAddr),
|
|
811
1123
|
assets: outputAssets(2000000n, new Map([[nftUnit, 1n]])),
|
|
@@ -904,8 +1216,8 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
904
1216
|
throw new Error(`No tokens of ${unit} in UTxO`);
|
|
905
1217
|
// 2. Find reference inputs
|
|
906
1218
|
const protocolParamsUtxo = await findProtocolParamsUtxo(client, networkId, ctx.deployment);
|
|
907
|
-
const
|
|
908
|
-
const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(
|
|
1219
|
+
const registryAddr = scriptAddress(networkId, ctx.standardScripts.registry.hash);
|
|
1220
|
+
const registryUtxos = await client.getUtxos(EvoAddress.fromBech32(registryAddr));
|
|
909
1221
|
const registryUtxo = findRegistryNode(registryUtxos, tokenPolicyId);
|
|
910
1222
|
if (!registryUtxo)
|
|
911
1223
|
throw new Error(`Registry node not found for ${tokenPolicyId}`);
|
|
@@ -945,8 +1257,9 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
945
1257
|
const paramsIdx = findRefInputIndex(sortedRefInputs, utxoToTxInput(protocolParamsUtxo));
|
|
946
1258
|
// 0.5.x third-party route. `ThirdPartyAct` was DELETED by #110: the
|
|
947
1259
|
// administrative path is now the standalone `third_party` validator, and
|
|
948
|
-
// programmable_logic_base
|
|
949
|
-
// this transaction never loads the `transfer`
|
|
1260
|
+
// programmable_logic_base withdraws through the dispatcher, whose redeemer
|
|
1261
|
+
// carries ThirdPartyAct — so this transaction never loads the `transfer`
|
|
1262
|
+
// reference script at all.
|
|
950
1263
|
//
|
|
951
1264
|
// The withdrawal set is BOTH scripts: the framework delegate, and the
|
|
952
1265
|
// issuer authority the registry node names in
|
|
@@ -959,15 +1272,21 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
959
1272
|
isScript: true,
|
|
960
1273
|
};
|
|
961
1274
|
const issuerAuthorityKey = { hash: scripts.issuerAdmin.hash, isScript: true };
|
|
962
|
-
|
|
1275
|
+
// alpha.3: the DISPATCHER withdraws on every programmable transaction and
|
|
1276
|
+
// PLB's wdrl_idx points at IT, not at the delegate.
|
|
1277
|
+
const plan = plbWithdrawalPlan({
|
|
1278
|
+
plgHash: ctx.standardScripts.programmableLogicGlobal.hash,
|
|
1279
|
+
others: [thirdPartyKey, issuerAuthorityKey],
|
|
1280
|
+
});
|
|
963
1281
|
// outputs_start_idx = 1: `third_party` PAIRS each programmable input with
|
|
964
1282
|
// the NEXT output (same address, datum and reference script, lovelace
|
|
965
1283
|
// ratcheting up) and reads the seized amount as the DELTA. Destination
|
|
966
1284
|
// outputs must therefore sit AMONG THE LEADING ones it skips.
|
|
967
1285
|
// See docs/api-reference.md — getting this backwards fails with an EMPTY
|
|
968
1286
|
// TRACE LIST, because it is a structural expect and not a traced check.
|
|
969
|
-
|
|
970
|
-
const
|
|
1287
|
+
// params_idx dropped: delegates no longer read the params datum.
|
|
1288
|
+
const plgRedeemer = thirdPartyRedeemer(registryIdx, 1);
|
|
1289
|
+
const plbSpendRedeemer = baseSpendRedeemer(paramsIdx, plan.plgIdx);
|
|
971
1290
|
const tokenDatum = voidData();
|
|
972
1291
|
// 5. Build recipient PLB address
|
|
973
1292
|
const recipientPlbAddr = baseAddress(networkId, plbHash, destinationAddress);
|
|
@@ -994,15 +1313,28 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
994
1313
|
amount: 0n,
|
|
995
1314
|
redeemer: voidData(),
|
|
996
1315
|
});
|
|
1316
|
+
// The DISPATCHER's own withdraw-0 — new in alpha.3, required on every
|
|
1317
|
+
// programmable transaction, and the entry PLB's wdrl_idx resolves to.
|
|
1318
|
+
tx = tx.withdraw({
|
|
1319
|
+
stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.programmableLogicGlobal.hash, "hex"))),
|
|
1320
|
+
amount: 0n,
|
|
1321
|
+
redeemer: programmableLogicGlobalRedeemer("THIRD_PARTY"),
|
|
1322
|
+
});
|
|
997
1323
|
tx = tx.withdraw({
|
|
998
1324
|
stakeCredential: Credential.makeScriptHash(new Uint8Array(Buffer.from(ctx.standardScripts.thirdParty.hash, "hex"))),
|
|
999
1325
|
amount: 0n,
|
|
1000
1326
|
redeemer: plgRedeemer,
|
|
1001
1327
|
});
|
|
1328
|
+
const coinsPerUtxoByte = (await client.getProtocolParameters()).coinsPerUtxoByte;
|
|
1002
1329
|
// Output 0: seized tokens to recipient
|
|
1003
1330
|
tx = tx.payToAddress({
|
|
1004
1331
|
address: EvoAddress.fromBech32(recipientPlbAddr),
|
|
1005
|
-
assets: outputAssets(
|
|
1332
|
+
assets: outputAssets(minUtxoAtLeast(TOKEN_OUTPUT_FLOOR, {
|
|
1333
|
+
address: recipientPlbAddr,
|
|
1334
|
+
assets: outputAssets(0n, new Map([[unit, seizedAmount]])),
|
|
1335
|
+
datum: tokenDatum,
|
|
1336
|
+
coinsPerUtxoByte,
|
|
1337
|
+
}), new Map([[unit, seizedAmount]])),
|
|
1006
1338
|
datum: new InlineDatum.InlineDatum({ data: tokenDatum }),
|
|
1007
1339
|
});
|
|
1008
1340
|
// Output 1: remaining value to original address
|
|
@@ -1026,6 +1358,12 @@ export function freezeAndSeizeSubstandard(config) {
|
|
|
1026
1358
|
"=== END SEIZE SCRIPTS ===");
|
|
1027
1359
|
}
|
|
1028
1360
|
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicBase.compiledCode) });
|
|
1361
|
+
// ⛔ THE DISPATCHER'S OWN SCRIPT WITNESS. A withdraw-0 needs the script in
|
|
1362
|
+
// full, not just a redeemer — alpha.3 added the dispatcher's withdrawal to
|
|
1363
|
+
// every programmable transaction, and S-6 wired the withdrawal and the
|
|
1364
|
+
// redeemer but not this. The ledger says "An associated script witness is
|
|
1365
|
+
// missing" on purpose=withdraw, which names the shape but not the script.
|
|
1366
|
+
tx = tx.attachScript({ script: buildEvoScript(ctx.standardScripts.programmableLogicGlobal.compiledCode) });
|
|
1029
1367
|
// ⚠ `third_party` is NOT attached here. Its withdrawal already carries the
|
|
1030
1368
|
// script witness, and attaching it again makes the duplicate extraneous —
|
|
1031
1369
|
// the ledger rejects the whole transaction with code 3104. MEASURED: the
|