@easy1staking/cip113-sdk-ts 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +152 -0
  2. package/blueprints/standard/v0.5.0-alpha.5/UPSTREAM_PIN.json +36 -0
  3. package/blueprints/standard/v0.5.0-alpha.5/plutus.json +1434 -0
  4. package/dist/core/evo-utils.d.ts +6 -1
  5. package/dist/core/evo-utils.d.ts.map +1 -1
  6. package/dist/core/evo-utils.js.map +1 -1
  7. package/dist/index.d.ts +15 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +81 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/standard/blueprint.d.ts +27 -3
  12. package/dist/standard/blueprint.d.ts.map +1 -1
  13. package/dist/standard/blueprint.js +27 -3
  14. package/dist/standard/blueprint.js.map +1 -1
  15. package/dist/standard/bootstrap.d.ts +662 -0
  16. package/dist/standard/bootstrap.d.ts.map +1 -0
  17. package/dist/standard/bootstrap.js +1221 -0
  18. package/dist/standard/bootstrap.js.map +1 -0
  19. package/dist/standard/scripts.d.ts +13 -2
  20. package/dist/standard/scripts.d.ts.map +1 -1
  21. package/dist/standard/scripts.js +13 -2
  22. package/dist/standard/scripts.js.map +1 -1
  23. package/dist/substandards/address-guard.d.ts +60 -0
  24. package/dist/substandards/address-guard.d.ts.map +1 -0
  25. package/dist/substandards/address-guard.js +159 -0
  26. package/dist/substandards/address-guard.js.map +1 -0
  27. package/dist/substandards/dummy/index.d.ts.map +1 -1
  28. package/dist/substandards/dummy/index.js +32 -5
  29. package/dist/substandards/dummy/index.js.map +1 -1
  30. package/dist/substandards/freeze-and-seize/index.d.ts.map +1 -1
  31. package/dist/substandards/freeze-and-seize/index.js +45 -9
  32. package/dist/substandards/freeze-and-seize/index.js.map +1 -1
  33. package/dist/substandards/interface.d.ts +20 -2
  34. package/dist/substandards/interface.d.ts.map +1 -1
  35. package/package.json +1 -1
@@ -0,0 +1,1221 @@
1
+ /**
2
+ * Protocol bootstrap — the transactions that stand up a CIP-113 instance.
3
+ *
4
+ * TARGETS CIP-113 0.5.0-alpha.5 (upstream b83a041eaa053625c502f8ee64b607a787cf5f79).
5
+ *
6
+ * ⚑ WHY THIS IS EXPORTED AT ALL. Until 2026-09-17 this sequence lived only in
7
+ * `test/harness/bootstrap.ts`, which `files: ["dist","blueprints"]` does not
8
+ * ship — so the platform maintained ITS OWN PORT of a protocol-critical
9
+ * sequence. That port had already diverged (it reserves the seed UTxOs from
10
+ * coin selection where the harness did not) and the divergence was CORRECT.
11
+ * Two implementations, one right and the other not yet wrong, is worse than one
12
+ * supported export. See CLAUDE.md, "Protocol bootstrap — AMENDED 2026-09-17",
13
+ * and `design/bootstrap-export.md` for the step boundaries and their reasons.
14
+ *
15
+ * ⛔ WHAT THIS MODULE DOES NOT DO, AND WILL NOT. It does not sign, submit, await,
16
+ * poll, fund, retry, or hold a key. It builds unsigned transactions and returns
17
+ * them. Orchestration is the caller's, and that is not a gap — it is what lets
18
+ * the platform keep its own UTxO reservation and its own confirmation strategy.
19
+ *
20
+ * ⛔ AND IT SHIPS NO FIXTURE VALUES. No mnemonic, no endpoint, no nonce, no
21
+ * security parameter, no placeholder policy id, no `any`. A value the caller
22
+ * must decide is a REQUIRED INPUT, never a default. That rule is the whole
23
+ * reason this export is safe to make: everything the devnet fixture chose for
24
+ * itself is now something its CALL passes in.
25
+ *
26
+ * ---------------------------------------------------------------------------
27
+ * THE SEQUENCE IS DEPENDENT, SO THE EXPORT IS STEPWISE
28
+ * ---------------------------------------------------------------------------
29
+ *
30
+ * The one-shot minting policies are parameterised by SPECIFIC OUTPUT REFERENCES
31
+ * of an earlier transaction, so step N+1 cannot be built until step N has been
32
+ * submitted and its outputs observed. There is no "give me five transactions"
33
+ * shape available; asking for one would mean signing, and signing is out.
34
+ *
35
+ * 1 buildSeedTx -> three distinct seed UTxOs
36
+ * 2 buildMultisigGenesisTx -> the upgrade authority's config UTxO
37
+ * 3 buildStakeRegistrationTx -> the six withdraw-0 registrations
38
+ * 4 buildProtocolGenesisTx -> params, registry origin, issuance CBOR
39
+ * 5 buildReferenceScriptsTx -> the seven reference scripts
40
+ * assembleDeploymentParams -> DeploymentParams (pure)
41
+ *
42
+ * ⛔ STEPS 3 AND 4 SWAPPED AT 0.12.0, AND THE ORDER IS A LEDGER RULE RATHER
43
+ * THAN A PREFERENCE. alpha.5's `protocol_params.mint` requires a withdraw-0
44
+ * from `upgrade_cred`, and a reward account cannot be withdrawn from in the
45
+ * same transaction that registers it — the ledger applies withdrawals against
46
+ * the state BEFORE certificates. So `upgrade_multisig`'s stake credential must
47
+ * be registered in an EARLIER transaction than the genesis. The registrations
48
+ * were last because nothing needed them yet; now one does. The other five
49
+ * registrations are dragged along only because they ride the same transaction,
50
+ * and none of the six `publish` handlers reads any chain state the genesis
51
+ * creates — they admit `RegisterCredential` and nothing else — so moving the
52
+ * whole step earlier is safe.
53
+ *
54
+ * ⚑ RESUMPTION. `planBootstrap(config)` is pure and deterministic: persist the
55
+ * CONFIG and every hash, address, datum and asset unit can be re-derived on any
56
+ * machine with no chain access. No step needs an earlier step's CONSUMED
57
+ * inputs, which is the property that makes resuming at step N work at all — by
58
+ * the time you resume at step 5 (reference scripts) the seeds are spent and
59
+ * unfetchable.
60
+ */
61
+ import { Address as EvoAddress, Assets as EvoAssets, Bytes, Credential, Data, InlineDatum, KeyHash, Transaction as EvoTransaction, TransactionHash as EvoTransactionHash, UPLC, } from "@evolution-sdk/evolution";
62
+ import { buildCip171RecordFromPin } from "../core/provenance.js";
63
+ import { buildCip171Metadatum, CIP171_METADATA_LABEL } from "../core/cip171.js";
64
+ import { buildEvoScript, ceilToWholeAda, decodeMultisigScript, getInlineDatum, mintAssetsFromMap, minUtxoAtLeast, minUtxoForOutput, multisigScriptDatum, outputAssets, protocolParamsDatum, registryInitRedeemer, registryNodeDatum, REGISTRY_NODE_MIN_ADA, scriptAddress, stringToHex, voidData, } from "../core/evo-utils.js";
65
+ import { STANDARD_VALIDATORS, validateStandardBlueprint } from "./blueprint.js";
66
+ import { createStandardScripts, UNFRACKING_DISABLED, } from "./scripts.js";
67
+ // ---------------------------------------------------------------------------
68
+ // Constants that are PROTOCOL FACTS, not caller choices
69
+ // ---------------------------------------------------------------------------
70
+ /**
71
+ * Three seed UTxOs, and the count is not negotiable.
72
+ *
73
+ * utxo1 -> protocol_params AND registry, utxo2 -> issuance_cbor_hex_mint,
74
+ * utxo3 -> upgrade_multisig.
75
+ *
76
+ * ⛔ THREE DISTINCT ONES, NOT ONE REUSED. Reusing utxo1 for upgrade_multisig
77
+ * deploys perfectly well — a one-shot outref is consumed once, and the two
78
+ * policies consume it in different transactions, so nothing on chain objects.
79
+ * The reason it must be distinct is OFF chain: `DeploymentParams` carries TWO
80
+ * same-typed one-shot outrefs, `protocolParams.txInput` and
81
+ * `upgradeMultisig.txInput`, and `assertDeploymentScripts` derives a hash from
82
+ * each. A record that gives them ONE value makes the upgrade_multisig check
83
+ * pass whichever field the code reads, so the check cannot fail and a real
84
+ * defect in which field is read is invisible. That is not hypothetical: it hid
85
+ * a wrong derivation for an entire migration once already.
86
+ */
87
+ export const BOOTSTRAP_SEED_COUNT = 3;
88
+ /**
89
+ * The reference-script publication order, which is ONE FACT WRITTEN ONCE.
90
+ *
91
+ * ⛔ APPENDED, NEVER INSERTED. `buildReferenceScriptsTx` pays them in this
92
+ * order and `assembleDeploymentParams` derives every `…RefInput` index from the
93
+ * same array. alpha.3 inserted the dispatcher at index 1 and shifted all three
94
+ * delegates down — and a mismatch here does not fail loudly. It hands out a
95
+ * reference input carrying the WRONG script, and the transaction dies at
96
+ * evaluation naming neither.
97
+ *
98
+ * ⛔ AND APPENDING IS NOT FREE — THERE IS A CAP AND THIS LIST IS NEAR IT (F-9).
99
+ * MEASURED on devnet, 2026-09-17: the transaction publishing these seven comes
100
+ * to **12,496 bytes against a 16,384-byte protocol maximum — 76%**. Two more
101
+ * scripts of the size already here would burst it, and the failure arrives at
102
+ * SUBMISSION rather than at build. This is the same cap that forced the
103
+ * mint/publish/register split in the first place: the 0.3.x single-transaction
104
+ * fixture measured 21,816 bytes. `buildReferenceScriptsTx` now refuses over the
105
+ * chain's own `maxTxSize` rather than letting the ledger say it; when that
106
+ * refusal fires, the answer is a SECOND publish transaction and a second
107
+ * recorded tx hash, not a shorter list.
108
+ */
109
+ export const REFERENCE_SCRIPT_ORDER = [
110
+ "programmableLogicBase",
111
+ "programmableLogicGlobal",
112
+ "transfer",
113
+ "thirdParty",
114
+ "unfracking",
115
+ "issuanceLogic",
116
+ "upgradeMultisig",
117
+ ];
118
+ /**
119
+ * The six withdraw-0 credentials a bootstrap must register.
120
+ *
121
+ * ⛔ SIX, AND EVERY OMISSION IS UNDIAGNOSABLE. An unregistered stake credential
122
+ * does not fail with "not registered": the ledger answers code 3141, "rewards
123
+ * withdrawals must consume rewards in full", which reads as a balance problem
124
+ * and sends the reader to the wrong subsystem entirely. MEASURED on devnet —
125
+ * every programmable operation failed that way until the dispatcher joined this
126
+ * list, and `issuance_logic` rides EVERY mint and EVERY burn.
127
+ */
128
+ export const STAKE_REGISTRATION_ORDER = [
129
+ "programmableLogicGlobal",
130
+ "transfer",
131
+ "thirdParty",
132
+ "unfracking",
133
+ "issuanceLogic",
134
+ "upgradeMultisig",
135
+ ];
136
+ /**
137
+ * The five steps, in order. Exported so a caller can name where it resumed.
138
+ *
139
+ * ⛔ THE ORDER CHANGED AT 0.12.0 AND THE STRINGS DID NOT, which is the shape a
140
+ * caller can miss. `stake-registrations` moved from last to THIRD, ahead of
141
+ * `protocol-genesis`: alpha.5's genesis carries a withdraw-0 from
142
+ * `upgrade_cred`, and a reward account cannot be withdrawn from in the same
143
+ * transaction that registers it. A caller that drives the steps off this array
144
+ * follows automatically; one that hard-coded the old sequence builds a genesis
145
+ * the ledger refuses as code 3141, "rewards withdrawals must consume rewards in
146
+ * full" — a message that names a balance problem and not a missing certificate.
147
+ */
148
+ export const BOOTSTRAP_STEPS = [
149
+ "seed",
150
+ "multisig-genesis",
151
+ "stake-registrations",
152
+ "protocol-genesis",
153
+ "reference-scripts",
154
+ ];
155
+ /**
156
+ * A FLOOR for solved min-UTxO amounts, not a chosen output size.
157
+ *
158
+ * ⚠ Every datum-bearing output below is SOLVED with `minUtxoAtLeast`, never set
159
+ * flat: min-UTxO scales with serialised output size, and Evolution does NOT
160
+ * rescue an under-funded `payToAddress`. The shortfall survives to submission
161
+ * and is reported as "insufficient Ada", which sends the reader to the wallet
162
+ * balance rather than to the datum that grew. The floor only keeps the figure
163
+ * monotone against what this package already emitted; the solved value governs.
164
+ */
165
+ const MIN_UTXO_FLOOR = 2000000n;
166
+ /**
167
+ * Solve min-UTxO for a genesis output, then ROUND UP TO A WHOLE ADA.
168
+ *
169
+ * ⛔ THE ROUNDING IS DELIBERATE AND IT IS NOT TIDINESS (F-7). `minUtxoAtLeast`
170
+ * returns the EXACT minimum, and its own docstring promises only that the
171
+ * figure "only ever rises" relative to what this package used to emit — it
172
+ * makes no promise about the ENCODER. The figure is
173
+ * `coinsPerUtxoByte × (160 + serialised output size)`, and the serialised size
174
+ * comes from Evolution's `TxOut` encoder, which this package does not own.
175
+ *
176
+ * ⚠ THE FAILURE THAT BUYS: a one-byte widening in that encoder — a version
177
+ * bump, a CBOR canonicalisation change — under-funds an output by
178
+ * `coinsPerUtxoByte` lovelace. Evolution does NOT rescue an under-funded
179
+ * `payToAddress`: the shortfall survives to submission and the ledger rejects
180
+ * it as "insufficient Ada", which sends the reader to the wallet balance. On
181
+ * the protocol genesis that rejection lands on an IRREVERSIBLE step, after the
182
+ * multisig config UTxO already exists on chain.
183
+ *
184
+ * ⇒ A whole-ADA ceiling is a RULE, not a guessed constant, and at
185
+ * `coinsPerUtxoByte` 4310 it absorbs on the order of a hundred bytes of encoder
186
+ * drift on the largest output here. It is still an order of magnitude below the
187
+ * flat 15 ADA this sequence used to write.
188
+ */
189
+ function genesisOutputLovelace(params) {
190
+ return ceilToWholeAda(minUtxoAtLeast(MIN_UTXO_FLOOR, params));
191
+ }
192
+ /**
193
+ * The lowest lovelace an output of this shape can legally carry — a FLOOR to
194
+ * refuse below, never a sufficiency check.
195
+ *
196
+ * ⚠ Clearing min-UTxO is necessary and not sufficient: a seed output also has
197
+ * to fund the fee of the transaction that consumes it, and this says nothing
198
+ * about that.
199
+ */
200
+ function minLovelaceForPlainOutput(address, coinsPerUtxoByte) {
201
+ return minUtxoForOutput({ address, assets: outputAssets(0n), coinsPerUtxoByte });
202
+ }
203
+ /**
204
+ * The 28-byte marker `issuance_mint` is parameterised against so the stored
205
+ * CBOR can be cut either side of its minting-logic argument.
206
+ *
207
+ * ⛔ NOT A PARAMETER, NOT A DEFAULT, AND NEVER DEPLOYED. It is applied and
208
+ * immediately cut back out; what reaches the chain is `pre` and `post` with the
209
+ * marker gone from between them, so that a later registration can splice a REAL
210
+ * minting-logic hash in without re-deriving the script. Passing this value
211
+ * anywhere a minting-logic hash is expected would produce a policy nobody can
212
+ * mint under — it is a cutting guide, not an identifier.
213
+ *
214
+ * ⚑ EXPORTED SO THE OCCURRENCE GUARD CAN BE EXERCISED, and for no other reason.
215
+ * `splitParts.length !== 2` below is the entire reason shipping a fixed hex
216
+ * marker is tolerable — and an unmutated guard is a claim rather than a check.
217
+ * A test cannot construct a body that collides with a marker it cannot see, so
218
+ * the marker is visible. (MEASURED: the `pre`/`post` halves do not depend on
219
+ * this VALUE — two different markers yield byte-identical halves, because the
220
+ * argument occupies the same bit positions either way.)
221
+ */
222
+ export const ISSUANCE_SPLICE_MARKER = "deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeef";
223
+ /**
224
+ * The three withdraw-0 delegates whose registration runs a `publish` handler.
225
+ *
226
+ * Registering emits a Conway RegCert, which executes the script under the
227
+ * PUBLISH purpose — so each needs a publish handler or the transaction dies at
228
+ * evaluation with a bare "machine terminated" and an EMPTY trace list. The
229
+ * check is kept because the failure it diagnoses is undiagnosable without it.
230
+ */
231
+ const REQUIRED_PUBLISH_HANDLERS = [
232
+ "transfer.transfer.publish",
233
+ "third_party.third_party.publish",
234
+ "unfracking.unfracking.publish",
235
+ ];
236
+ // ---------------------------------------------------------------------------
237
+ // Required-input refusals — by name, and before anything is derived
238
+ // ---------------------------------------------------------------------------
239
+ function requireHex(value, field, bytes) {
240
+ if (typeof value !== "string" || value.length === 0) {
241
+ throw new Error(`bootstrap: ${field} is required and must be a non-empty hex string, got ` +
242
+ `${value === undefined ? "undefined" : JSON.stringify(value)}. This SDK ships no ` +
243
+ `default for it — it is a value the caller must decide.`);
244
+ }
245
+ if (!/^[0-9a-fA-F]+$/.test(value) || value.length % 2 !== 0) {
246
+ throw new Error(`bootstrap: ${field} must be an even-length hex string with no 0x prefix, got ` +
247
+ `${JSON.stringify(value)}.`);
248
+ }
249
+ if (bytes !== undefined && value.length !== bytes * 2) {
250
+ throw new Error(`bootstrap: ${field} must be exactly ${bytes} bytes (${bytes * 2} hex chars), got ` +
251
+ `${value.length / 2}.`);
252
+ }
253
+ return value;
254
+ }
255
+ function requireTxInput(value, field) {
256
+ const ref = value;
257
+ if (!ref || typeof ref !== "object") {
258
+ throw new Error(`bootstrap: ${field} is required — a { txHash, outputIndex } naming the one-shot UTxO ` +
259
+ `this policy is parameterised by. Got ${JSON.stringify(value)}.`);
260
+ }
261
+ requireHex(ref.txHash, `${field}.txHash`, 32);
262
+ if (!Number.isInteger(ref.outputIndex) || ref.outputIndex < 0) {
263
+ throw new Error(`bootstrap: ${field}.outputIndex must be a non-negative integer, got ` +
264
+ `${JSON.stringify(ref.outputIndex)}.`);
265
+ }
266
+ return { txHash: ref.txHash.toLowerCase(), outputIndex: ref.outputIndex };
267
+ }
268
+ const refKey = (ref) => `${ref.txHash.toLowerCase()}#${ref.outputIndex}`;
269
+ function requireSeeds(seeds) {
270
+ const s = seeds;
271
+ if (!s || typeof s !== "object") {
272
+ throw new Error(`bootstrap: seeds is required — three DISTINCT one-shot output references, as ` +
273
+ `{ protocolParams, issuance, upgradeMultisig }. Got ${JSON.stringify(seeds)}.`);
274
+ }
275
+ const resolved = {
276
+ protocolParams: requireTxInput(s.protocolParams, "seeds.protocolParams"),
277
+ issuance: requireTxInput(s.issuance, "seeds.issuance"),
278
+ upgradeMultisig: requireTxInput(s.upgradeMultisig, "seeds.upgradeMultisig"),
279
+ };
280
+ // ⛔ DISTINCTNESS IS A REFUSAL, NOT A WARNING. See BOOTSTRAP_SEED_COUNT: two
281
+ // roles sharing one outref deploy fine and make the off-chain derivation
282
+ // check for the shared pair VACUOUS. A check that cannot fail is worse than
283
+ // an absent one, because it reads as coverage.
284
+ const seen = new Map();
285
+ for (const [role, ref] of Object.entries(resolved)) {
286
+ const key = refKey(ref);
287
+ const previous = seen.get(key);
288
+ if (previous) {
289
+ throw new Error(`bootstrap: seeds.${previous} and seeds.${role} are the SAME output reference ` +
290
+ `(${key}). The three one-shot policies must be parameterised by three DISTINCT ` +
291
+ `UTxOs: DeploymentParams carries protocolParams.txInput and upgradeMultisig.txInput ` +
292
+ `as two same-typed fields, and a record that gives them one value makes ` +
293
+ `assertDeploymentScripts pass whichever field the code reads — so the check cannot ` +
294
+ `fail and a real defect in which field is read becomes invisible.`);
295
+ }
296
+ seen.set(key, role);
297
+ }
298
+ return resolved;
299
+ }
300
+ function requirePublishHandlers(blueprint) {
301
+ const titles = blueprint.validators.map((v) => v.title);
302
+ const missing = REQUIRED_PUBLISH_HANDLERS.filter((t) => !titles.includes(t));
303
+ if (missing.length > 0) {
304
+ throw new Error(`This blueprint cannot bootstrap a protocol: it lacks publish handler(s) ` +
305
+ `${missing.join(", ")}, so registering the corresponding stake credential fails at ` +
306
+ `script evaluation (purpose "publish") with no diagnostic.\n` +
307
+ `Blueprint: "${blueprint.preamble.title}" v${blueprint.preamble.version}.\n` +
308
+ `Present handlers: ${titles.filter((t) => t.endsWith(".publish")).join(", ") || "(none)"}`);
309
+ }
310
+ }
311
+ /** Extract the PlutusV3 script body hex (inner UPLC, no outer CBOR wrap). */
312
+ function scriptBodyHex(compiledCode) {
313
+ const level = UPLC.getCborEncodingLevel(compiledCode);
314
+ if (level !== "double")
315
+ return compiledCode;
316
+ const raw = Bytes.fromHex(compiledCode);
317
+ const additionalInfo = raw[0] & 0x1f;
318
+ const headerLen = additionalInfo < 24 ? 1 : additionalInfo === 24 ? 2 : additionalInfo === 25 ? 3 : 5;
319
+ return Bytes.toHex(raw.slice(headerLen));
320
+ }
321
+ // ---------------------------------------------------------------------------
322
+ // planBootstrap
323
+ // ---------------------------------------------------------------------------
324
+ /**
325
+ * Derive a whole protocol instance from a config. Pure: no client, no network,
326
+ * no clock, no filesystem.
327
+ *
328
+ * ⚠ THE PARAMETERISATION ORDER IS LOAD-BEARING and each script's hash feeds the
329
+ * next. It is NOT a single chain — `programmable_logic_base` hangs off the
330
+ * params-NFT policy and everything downstream fans out — but the edges that do
331
+ * exist cannot be reordered. See `scripts.ts` for the full graph.
332
+ *
333
+ * ⛔ PARAMETER TYPES ARE NOT INTERCHANGEABLE AND TYPESCRIPT CANNOT TELL THEM
334
+ * APART: every one is a hex string at the call site. `issuance_logic` in
335
+ * particular takes TWO ADJACENT PolicyIds — `registry_node_cs` then
336
+ * `params_policy` — and swapping them yields a script that builds, hashes,
337
+ * deploys and is registered without a murmur. It simply looks for the registry
338
+ * under the params policy for the rest of its life.
339
+ */
340
+ export function planBootstrap(config) {
341
+ if (!config || typeof config !== "object") {
342
+ throw new Error("planBootstrap: a BootstrapConfig is required.");
343
+ }
344
+ const { blueprint } = config;
345
+ if (!blueprint || typeof blueprint !== "object" || !Array.isArray(blueprint.validators)) {
346
+ throw new Error("planBootstrap: blueprint is required — the CIP-57 standard blueprint this instance is " +
347
+ "built from. Import it via the package's \"./blueprints/*\" export path.");
348
+ }
349
+ validateStandardBlueprint(blueprint);
350
+ requirePublishHandlers(blueprint);
351
+ if (typeof config.networkId !== "number" || !Number.isInteger(config.networkId)) {
352
+ throw new Error(`planBootstrap: networkId is required and must be an integer (0 for any testnet, 1 for ` +
353
+ `mainnet), got ${JSON.stringify(config.networkId)}. Every address this plan derives ` +
354
+ `depends on it, and a wrong one yields addresses that are valid and unreachable.`);
355
+ }
356
+ const seeds = requireSeeds(config.seeds);
357
+ const alwaysFailNonce = requireHex(config.alwaysFailNonce, "alwaysFailNonce");
358
+ if (typeof config.maxInlineDatumBytes !== "bigint" || config.maxInlineDatumBytes <= 0n) {
359
+ throw new Error(`planBootstrap: maxInlineDatumBytes is required and must be a positive bigint, got ` +
360
+ `${JSON.stringify(String(config.maxInlineDatumBytes))}. It is a SECURITY PARAMETER ` +
361
+ `baked into the hashes of transfer, third_party, unfracking and issuance_logic, and ` +
362
+ `this package deliberately ships no default for it — upstream gives no guidance and ` +
363
+ `the devnet fixture's 1024 is explicitly not a recommendation.`);
364
+ }
365
+ if (config.unfracking !== "enabled" && config.unfracking !== "disabled") {
366
+ throw new Error(`planBootstrap: unfracking is required and must be "enabled" or "disabled", got ` +
367
+ `${JSON.stringify(config.unfracking)}. The choice is baked into the dispatcher's hash, ` +
368
+ `so two deployments differing only here are DIFFERENT PROTOCOLS — it cannot be defaulted.`);
369
+ }
370
+ const events = [];
371
+ const builders = createStandardScripts(blueprint, (e) => events.push(e));
372
+ const max = config.maxInlineDatumBytes;
373
+ const alwaysFail = builders.alwaysFail(alwaysFailNonce);
374
+ // Depends on nothing but its own seed, so it can be built first — and its
375
+ // config UTxO must be minted BEFORE the protocol genesis names it. See
376
+ // buildMultisigGenesisTx for why that ordering is not a style choice.
377
+ const upgradeMultisig = builders.upgradeMultisig(seeds.upgradeMultisig);
378
+ // NO NONCE AND NO ORDERING EDGE. protocol_params takes only its one-shot
379
+ // utxo_ref: the mint side no longer depends on a spend-side address, so the
380
+ // cycle that once forced a coordination script to be built first is
381
+ // dissolved. Its hash is BOTH the NFT policy id and the address payment
382
+ // credential — the minting policy naming itself.
383
+ const protocolParams = builders.protocolParams(seeds.protocolParams);
384
+ const paramsPolicy = protocolParams.hash;
385
+ const programmableLogicBase = builders.programmableLogicBase(paramsPolicy);
386
+ // The registry does not touch the params chain at all — it reads its own
387
+ // policy off its own input's payment credential — so it can be built as soon
388
+ // as issuance_cbor_hex_mint exists. Its hash is also BOTH policy and address.
389
+ const issuanceCborHexMint = builders.issuanceCborHexMint(seeds.issuance, alwaysFail.hash);
390
+ const registry = builders.registry(seeds.protocolParams, issuanceCborHexMint.hash);
391
+ // ⚠ progLogicCred is programmable_logic_base's hash, NOT the dispatcher's.
392
+ // The dispatcher is parameterised BY these three, so the reverse would be a
393
+ // parameter cycle: a value derived from your own script hash can never be
394
+ // your own parameter.
395
+ const transfer = builders.transfer(programmableLogicBase.hash, registry.hash, max);
396
+ const thirdParty = builders.thirdParty(programmableLogicBase.hash, registry.hash, max);
397
+ const unfracking = builders.unfracking(programmableLogicBase.hash, registry.hash, max);
398
+ const unfrackingParameter = config.unfracking === "enabled" ? unfracking.hash : UNFRACKING_DISABLED;
399
+ // Built LAST of the delegate family: it names all three at compile time, so
400
+ // replacing ONE delegate requires deploying a new dispatcher too.
401
+ const programmableLogicGlobal = builders.programmableLogicGlobal(transfer.hash, thirdParty.hash, unfrackingParameter);
402
+ // ⛔ PARAMETERS 2 AND 3 ARE TWO ADJACENT PolicyIds AND BOTH ARE `string`:
403
+ // 1 progLogicCred = programmable_logic_base (NOT the dispatcher)
404
+ // 2 registryPolicy <- registry_node_cs
405
+ // 3 paramsPolicy <- params_policy
406
+ // 4 maxInlineDatumBytes
407
+ // Nothing — not the compiler, not the parameteriser, not the hash — can tell
408
+ // a swap from the intended order.
409
+ const issuanceLogic = builders.issuanceLogic(programmableLogicBase.hash, registry.hash, paramsPolicy, max);
410
+ // issuance_mint is parameterised per minting logic, which is not known until
411
+ // a token is registered. Build it once against a marker and store the CBOR
412
+ // either side of it, so registration can splice in the real hash without
413
+ // re-deriving the whole script.
414
+ //
415
+ // ⚑ TWO ARGUMENTS, CREDENTIAL FIRST: `(mintingLogicHash, paramsPolicy)`. The
416
+ // delegate credentials it used to be compiled against now reach it through
417
+ // the params datum instead — which is the whole point of the alpha.4 split.
418
+ const issuanceMarker = builders.issuanceMint(ISSUANCE_SPLICE_MARKER, paramsPolicy);
419
+ const markerBody = scriptBodyHex(issuanceMarker.compiledCode);
420
+ const splitParts = markerBody.split(ISSUANCE_SPLICE_MARKER);
421
+ /**
422
+ * ⛔ A HARD FAILURE, AND IT MUST NOT BECOME A WARNING. The splice reassembles
423
+ * the script from `pre + <real minting logic hash> + post`, so it is correct
424
+ * only while the marker occurs EXACTLY ONCE. Zero means the parameter is no
425
+ * longer inlined where we think; two means the splice would silently rewrite
426
+ * an unrelated byte run.
427
+ *
428
+ * ⚠ And flat UPLC is BIT-packed: a parameter is findable as a whole number of
429
+ * hex bytes only when it happens to land on a byte boundary. That alignment
430
+ * is a property to be MEASURED per artefact, never assumed — this check is
431
+ * what measures it.
432
+ */
433
+ if (splitParts.length !== 2) {
434
+ throw new Error(`planBootstrap: the issuance_mint splice marker appeared ${splitParts.length - 1} times ` +
435
+ `in the compiled body — expected exactly once. The stored IssuanceCborHex datum is ` +
436
+ `reassembled around this cut, so zero occurrences means the parameter is no longer ` +
437
+ `inlined where this code expects, and more than one means the splice would rewrite an ` +
438
+ `unrelated byte run. Blueprint: "${blueprint.preamble.title}" v${blueprint.preamble.version}.`);
439
+ }
440
+ const [cborPre, cborPost] = splitParts;
441
+ const scripts = {
442
+ alwaysFail,
443
+ protocolParams,
444
+ programmableLogicBase,
445
+ issuanceCborHexMint,
446
+ registry,
447
+ transfer,
448
+ thirdParty,
449
+ unfracking,
450
+ programmableLogicGlobal,
451
+ issuanceLogic,
452
+ upgradeMultisig,
453
+ };
454
+ /**
455
+ * SIX fields, written BY NAME and never as a positional spread.
456
+ *
457
+ * ⛔ INDEX 1 CHANGED MEANING BETWEEN alpha.3 AND alpha.4. `issuance_logic_cred`
458
+ * was INSERTED at index 1, displacing `transfer_cred` to index 2. Both are
459
+ * `Credential` — same constructor, same 28 bytes — so a datum written in
460
+ * alpha.3's order with two fields appended is SIX fields long, passes the
461
+ * arity check, passes `params_wellformed` (which only asserts each credential
462
+ * is 28 bytes), decodes into a well-formed record, and hands `issuance_mint`
463
+ * the TRANSFER credential as its issuance authority. Nothing at deploy time
464
+ * catches it. Keying the record is what makes the compiler an ally.
465
+ *
466
+ * ⛔ `pendingUpgradeCred` MUST BE null AT GENESIS. `protocol_params` runs
467
+ * `params_wellformed(genesis_params, is_init: True)`, and `is_init: True` is
468
+ * exactly what forbids a nomination baked into the genesis datum.
469
+ */
470
+ const paramsDatum = protocolParamsDatum({
471
+ plgCred: { type: "script", hash: programmableLogicGlobal.hash },
472
+ issuanceLogicCred: { type: "script", hash: issuanceLogic.hash },
473
+ transferCred: { type: "script", hash: transfer.hash },
474
+ thirdPartyCred: { type: "script", hash: thirdParty.hash },
475
+ upgradeCred: { type: "script", hash: upgradeMultisig.hash },
476
+ pendingUpgradeCred: null,
477
+ });
478
+ // Sentinel head of the registry linked list: key "", next 0xff*30, every
479
+ // delegate slot empty.
480
+ const EMPTY_CRED = { type: "key", hash: "" };
481
+ const originDatum = registryNodeDatum({
482
+ key: "",
483
+ next: "ff".repeat(30),
484
+ mintingLogicScript: EMPTY_CRED,
485
+ transferLogicScript: EMPTY_CRED,
486
+ thirdPartyTransferLogicScript: EMPTY_CRED,
487
+ unfrackingLogicScript: EMPTY_CRED,
488
+ globalStateCs: "",
489
+ });
490
+ const issuanceDatum = Data.constr(0n, [Data.bytearray(cborPre), Data.bytearray(cborPost)]);
491
+ const byName = {
492
+ programmableLogicBase,
493
+ programmableLogicGlobal,
494
+ transfer,
495
+ thirdParty,
496
+ unfracking,
497
+ issuanceLogic,
498
+ upgradeMultisig,
499
+ };
500
+ return {
501
+ config: { ...config, seeds, alwaysFailNonce },
502
+ scripts,
503
+ unfrackingParameter,
504
+ addresses: {
505
+ protocolParams: scriptAddress(config.networkId, paramsPolicy),
506
+ registry: scriptAddress(config.networkId, registry.hash),
507
+ issuanceCborHex: scriptAddress(config.networkId, alwaysFail.hash),
508
+ upgradeMultisig: scriptAddress(config.networkId, upgradeMultisig.hash),
509
+ },
510
+ assetUnits: {
511
+ protocolParamsNft: paramsPolicy + stringToHex("ProtocolParams"),
512
+ // Empty asset name, so the unit IS the policy.
513
+ registryNode: registry.hash,
514
+ issuanceCborHexNft: issuanceCborHexMint.hash + stringToHex("IssuanceCborHex"),
515
+ upgradeMultisigNft: upgradeMultisig.hash + stringToHex("UpgradeMultisig"),
516
+ },
517
+ datums: {
518
+ protocolParams: paramsDatum,
519
+ registryOrigin: originDatum,
520
+ issuanceCborHex: issuanceDatum,
521
+ },
522
+ issuanceCbor: { pre: cborPre, post: cborPost },
523
+ referenceScripts: REFERENCE_SCRIPT_ORDER.map((n) => byName[n]),
524
+ stakeCredentialScripts: STAKE_REGISTRATION_ORDER.map((n) => byName[n]),
525
+ parameterizations: events
526
+ .filter((e) => e.title !== STANDARD_VALIDATORS.ISSUANCE_MINT)
527
+ .map((e) => ({ rawScriptHash: e.rawScriptHash, params: e.params })),
528
+ };
529
+ }
530
+ const isSignBuilder = (built) => "chainResult" in built;
531
+ function requireBuildContext(ctx, step) {
532
+ if (!ctx || typeof ctx !== "object") {
533
+ throw new Error(`bootstrap ${step}: a build context is required.`);
534
+ }
535
+ if (!ctx.client || typeof ctx.client.newTx !== "function") {
536
+ throw new Error(`bootstrap ${step}: client is required — an Evolution SDK ReadOnlyClient or SigningClient. ` +
537
+ `This SDK does not construct clients, hold keys, or name endpoints.`);
538
+ }
539
+ if (typeof ctx.changeAddress !== "string" || ctx.changeAddress.length === 0) {
540
+ throw new Error(`bootstrap ${step}: changeAddress is required (bech32).`);
541
+ }
542
+ if (!Array.isArray(ctx.availableUtxos)) {
543
+ throw new Error(`bootstrap ${step}: availableUtxos is required — exactly the UTxOs this transaction may ` +
544
+ `spend. There is deliberately no default: without it coin selection is free to spend ` +
545
+ `reference-script UTxOs and seed UTxOs a later step still needs.`);
546
+ }
547
+ if (ctx.availableUtxos.length === 0) {
548
+ throw new Error(`bootstrap ${step}: availableUtxos is empty. Nothing can fund this transaction. If you ` +
549
+ `filtered a wallet view, check the filter before checking the wallet.`);
550
+ }
551
+ }
552
+ function buildOptions(ctx) {
553
+ return {
554
+ changeAddress: EvoAddress.fromBech32(ctx.changeAddress),
555
+ availableUtxos: ctx.availableUtxos,
556
+ ...(ctx.evaluator ? { evaluator: ctx.evaluator } : {}),
557
+ };
558
+ }
559
+ /**
560
+ * Build and shape into the SDK's one result type.
561
+ *
562
+ * ⚠ `_signBuilder` is the SAME field every other operation in this package
563
+ * returns, not a new escape hatch: `UnsignedTx` already carries it and the
564
+ * README's quick start already signs through it. Omitting it here would make
565
+ * the bootstrap the only operation whose result cannot be submitted — the
566
+ * defect `dummy.transfer` already recorded once. `cbor` is the supported path
567
+ * and the one a key-holding caller should prefer; this SDK signs nothing.
568
+ */
569
+ async function finish(tx, ctx, step, metadata) {
570
+ const built = await tx.build(buildOptions(ctx));
571
+ const cbor = EvoTransaction.toCBORHex(await built.toTransaction());
572
+ const txHash = isSignBuilder(built) ? built.chainResult().txHash : "";
573
+ return { cbor, txHash, metadata: { step, ...metadata }, _signBuilder: built };
574
+ }
575
+ function requireSeedMatches(utxo, expected, role) {
576
+ if (!utxo || typeof utxo !== "object" || utxo.transactionId === undefined) {
577
+ throw new Error(`bootstrap: the ${role} seed UTxO is required — the unspent output this plan's one-shot ` +
578
+ `policy is parameterised by (${refKey(expected)}).`);
579
+ }
580
+ const actual = {
581
+ txHash: EvoTransactionHash.toHex(utxo.transactionId),
582
+ outputIndex: Number(utxo.index),
583
+ };
584
+ if (refKey(actual) !== refKey(expected)) {
585
+ throw new Error(`bootstrap: the ${role} seed UTxO is ${refKey(actual)}, but this plan was parameterised ` +
586
+ `by ${refKey(expected)}. A one-shot policy's hash is a function of its outref, so ` +
587
+ `spending a different UTxO builds a transaction against a script this deployment does ` +
588
+ `not own — and it fails on chain naming neither. Rebuild the plan from the seeds you ` +
589
+ `actually hold, or pass the seeds this plan names.`);
590
+ }
591
+ }
592
+ /**
593
+ * Step 1 — fragment the wallet into {@link BOOTSTRAP_SEED_COUNT} distinct UTxOs.
594
+ *
595
+ * ⚑ THIS STEP IS OPTIONAL. A caller that already holds three distinct unspent
596
+ * UTxOs may skip it and call {@link planBootstrap} with their outrefs directly.
597
+ * It exists because most callers do not.
598
+ *
599
+ * ⚠ Its outputs cannot be predicted: the plan needs the OBSERVED outrefs. Submit
600
+ * this, wait for it, read the outputs back, then {@link selectBootstrapSeeds}.
601
+ */
602
+ export async function buildSeedTx(params) {
603
+ requireBuildContext(params, "seed");
604
+ if (typeof params.ownerAddress !== "string" || params.ownerAddress.length === 0) {
605
+ throw new Error("bootstrap seed: ownerAddress is required (bech32).");
606
+ }
607
+ if (typeof params.seedLovelace !== "bigint" || params.seedLovelace <= 0n) {
608
+ throw new Error(`bootstrap seed: seedLovelace is required and must be a positive bigint, got ` +
609
+ `${JSON.stringify(String(params.seedLovelace))}. Each seed funds the transaction that ` +
610
+ `consumes it, so it must clear min-UTxO with room for a fee — a figure this package ` +
611
+ `cannot choose for a chain it does not know.`);
612
+ }
613
+ // ⛔ REQUIRED IS NOT THE SAME AS VALID (F-11). A caller passing `1n` built
614
+ // happily and failed only at submission, as "insufficient Ada" — the exact
615
+ // failure min-UTxO solving exists to prevent, arriving from the one figure
616
+ // this builder does NOT solve because the caller chose it.
617
+ const { coinsPerUtxoByte } = await params.client.getProtocolParameters();
618
+ const seedFloor = minLovelaceForPlainOutput(params.ownerAddress, coinsPerUtxoByte);
619
+ if (params.seedLovelace < seedFloor) {
620
+ throw new Error(`bootstrap seed: seedLovelace is ${params.seedLovelace} lovelace, below the min-UTxO ` +
621
+ `floor of ${seedFloor} for a plain output at ${params.ownerAddress} ` +
622
+ `(coinsPerUtxoByte ${coinsPerUtxoByte}). The ledger would reject the seed transaction ` +
623
+ `as "insufficient Ada" at submission. ⚠ This floor is NECESSARY, NOT SUFFICIENT: each ` +
624
+ `seed also has to fund the fee of the transaction that consumes it, and nothing here ` +
625
+ `checks that.`);
626
+ }
627
+ const owner = EvoAddress.fromBech32(params.ownerAddress);
628
+ let tx = params.client.newTx();
629
+ for (let i = 0; i < BOOTSTRAP_SEED_COUNT; i++) {
630
+ tx = tx.payToAddress({ address: owner, assets: EvoAssets.fromLovelace(params.seedLovelace) });
631
+ }
632
+ return finish(tx, params, "seed", { seedCount: BOOTSTRAP_SEED_COUNT });
633
+ }
634
+ /**
635
+ * Pick the three seeds out of a seed transaction's observed outputs.
636
+ *
637
+ * ⚠ REFUSES FEWER THAN THREE, LOUDLY. An earlier version of this logic asked
638
+ * for three and tolerated two, which handed `undefined` to the third consumer.
639
+ *
640
+ * The change output of the seed transaction lands at the same address and is a
641
+ * legitimate candidate; ordering by output index keeps the assignment stable
642
+ * across reads of an indexer that does not preserve order.
643
+ */
644
+ export function selectBootstrapSeeds(utxos, seedTxHash) {
645
+ const wanted = requireHex(seedTxHash, "seedTxHash", 32).toLowerCase();
646
+ const mine = utxos
647
+ .filter((u) => EvoTransactionHash.toHex(u.transactionId).toLowerCase() === wanted)
648
+ .sort((a, b) => Number(a.index) - Number(b.index));
649
+ if (mine.length < BOOTSTRAP_SEED_COUNT) {
650
+ throw new Error(`bootstrap: the seed transaction ${wanted} shows ${mine.length} output(s) in the UTxO set ` +
651
+ `supplied, need ≥${BOOTSTRAP_SEED_COUNT}. If the transaction was submitted, the ` +
652
+ `provider's view has probably not settled yet — wait for it rather than proceeding ` +
653
+ `with fewer, which hands a later step an undefined seed.`);
654
+ }
655
+ const [one, two, three] = mine;
656
+ const ref = (u) => ({
657
+ txHash: EvoTransactionHash.toHex(u.transactionId).toLowerCase(),
658
+ outputIndex: Number(u.index),
659
+ });
660
+ return {
661
+ seeds: {
662
+ protocolParams: ref(one),
663
+ issuance: ref(two),
664
+ upgradeMultisig: ref(three),
665
+ },
666
+ utxos: { protocolParams: one, issuance: two, upgradeMultisig: three },
667
+ };
668
+ }
669
+ /**
670
+ * Step 2 — mint the `upgrade_multisig` one-shot NFT and lock it with the signer
671
+ * tree, BEFORE the protocol genesis names this authority.
672
+ *
673
+ * ⛔ THE ORDER IS THE POINT, AND IT IS NOT A STYLE CHOICE. The protocol
674
+ * genesis (step 4) writes a genesis datum naming
675
+ * `upgrade_cred = Script(upgrade_multisig)`. That authority
676
+ * is usable only while its config UTxO exists — the tree lives there, not in the
677
+ * script's parameters. Running this AFTER the protocol genesis and failing
678
+ * leaves a protocol on chain naming an authority whose config UTxO does not
679
+ * exist: upstream's documented ONE-WAY BRICK, manufactured by transaction
680
+ * ordering rather than by any defect in the validators. Failing BEFORE the
681
+ * irreversible step costs one transaction and nothing else.
682
+ *
683
+ * The four rails `upgrade_multisig.mint` enforces, all satisfied here:
684
+ * 1. the named UTxO is consumed -> collectFrom
685
+ * 2. exactly one "UpgradeMultisig" token of this policy -> mintAssets
686
+ * 3. an output found by `has_nft_strict` -> the output below
687
+ * 4. well_formed(tree), NO reference script, address == from_script(policy)
688
+ *
689
+ * ⛔ THE NFT AND NOTHING ELSE in that output. `has_nft_strict` is strict about
690
+ * the WHOLE value: bundling any other asset with the config NFT means the output
691
+ * is simply NOT FOUND by `list.expect_find`, and the genesis fails naming
692
+ * nothing about bundling. Change is a separate output.
693
+ *
694
+ * ⚠ NO `script:` ON THE OUTPUT — rail 4 requires `reference_script == None`.
695
+ * The reference script is published in step 5 like every other one.
696
+ *
697
+ * ⚑ Additional outputs are the CALLER's to add in their own transaction if they
698
+ * want them. Rail 3 uses `list.expect_find`, which SKIPS a non-matching output
699
+ * rather than rejecting it, so a junk UTxO parked at this address is harmless —
700
+ * but it is not protocol state and this step will not mint one.
701
+ */
702
+ export async function buildMultisigGenesisTx(params) {
703
+ requireBuildContext(params, "multisig-genesis");
704
+ const { plan } = params;
705
+ if (!plan || typeof plan !== "object" || !plan.scripts) {
706
+ throw new Error("bootstrap multisig-genesis: plan is required — see planBootstrap().");
707
+ }
708
+ if (!params.upgradeMultisigTree || typeof params.upgradeMultisigTree !== "object") {
709
+ throw new Error(`bootstrap multisig-genesis: upgradeMultisigTree is required — the MultisigScript tree ` +
710
+ `that IS this protocol's upgrade authority. There is no default: shipping one would ` +
711
+ `mean shipping a decision about who controls every deployment made with this package.`);
712
+ }
713
+ requireSeedMatches(params.seedUtxo, plan.config.seeds.upgradeMultisig, "upgradeMultisig");
714
+ // Enforces upstream's `well_formed`; throws by name on an unsatisfiable leaf.
715
+ const datum = multisigScriptDatum(params.upgradeMultisigTree);
716
+ const nftUnit = plan.assetUnits.upgradeMultisigNft;
717
+ const assets = new Map([[nftUnit, 1n]]);
718
+ const coinsPerUtxoByte = (await params.client.getProtocolParameters()).coinsPerUtxoByte;
719
+ const lovelace = genesisOutputLovelace({
720
+ address: plan.addresses.upgradeMultisig,
721
+ assets: outputAssets(0n, assets),
722
+ datum,
723
+ coinsPerUtxoByte,
724
+ });
725
+ let tx = params.client.newTx();
726
+ tx = tx.collectFrom({ inputs: [params.seedUtxo] });
727
+ tx = tx.mintAssets({ assets: mintAssetsFromMap(assets), redeemer: voidData() });
728
+ tx = tx.payToAddress({
729
+ address: EvoAddress.fromBech32(plan.addresses.upgradeMultisig),
730
+ assets: outputAssets(lovelace, assets),
731
+ datum: new InlineDatum.InlineDatum({ data: datum }),
732
+ });
733
+ tx = tx.attachScript({ script: buildEvoScript(plan.scripts.upgradeMultisig.compiledCode) });
734
+ return finish(tx, params, "multisig-genesis", {
735
+ configUtxoOutputIndex: 0,
736
+ nftUnit,
737
+ address: plan.addresses.upgradeMultisig,
738
+ });
739
+ }
740
+ /**
741
+ * Assert the multisig config UTxO on chain is the one the genesis datum is
742
+ * about to name. PURE — the caller fetches the UTxOs, this decides.
743
+ *
744
+ * ⛔ AN OPERABILITY GATE, NOT DECORATION. The measured instance of "correct and
745
+ * useless" in this repo is a deployment naming an authority credential that
746
+ * could not be registered at all: every hash reproduced, every read-back
747
+ * matched, every test passed, and the protocol's upgrade path was permanently
748
+ * unsatisfiable. "It exists and is well-formed" left "and can be used" untested.
749
+ * Run this between step 2 and the protocol genesis (step 4), so it is
750
+ * structurally impossible for the genesis datum to name an authority that is
751
+ * not there. ⚠ "Between step 2 and step 3" is what this said before 0.12.0 and
752
+ * step 3 is now the stake registrations — the sentence stayed true-looking
753
+ * while naming the wrong transaction.
754
+ *
755
+ * ⚑ FILTERS STRUCTURALLY, BY POLICY, EXACTLY AS THE VALIDATOR DOES — not by
756
+ * equality against a unit string we ourselves built. A lookup keyed on our own
757
+ * constructed unit shares a blind spot with the code that constructed it: get
758
+ * the asset name wrong in both places and the check agrees with itself.
759
+ */
760
+ export function assertMultisigConfigUtxo(params) {
761
+ const { plan } = params;
762
+ const policy = plan.scripts.upgradeMultisig.hash;
763
+ const address = plan.addresses.upgradeMultisig;
764
+ const candidates = (params.utxosAtAddress ?? []).filter((u) => EvoAssets.getUnits(u.assets).some((unit) => unit !== "lovelace" && unit.slice(0, 56) === policy));
765
+ if (candidates.length !== 1) {
766
+ throw new Error(`upgrade_multisig config UTxO: expected exactly 1 UTxO at ${address} carrying an asset ` +
767
+ `of policy ${policy}, found ${candidates.length}. The NFT is one-shot, so zero means ` +
768
+ `the genesis output is not where the validator locks it, and more than one means this ` +
769
+ `address is not what we think it is. Refusing to let a genesis datum name an authority ` +
770
+ `whose config UTxO is not exactly one well-formed UTxO.`);
771
+ }
772
+ const utxo = candidates[0];
773
+ const onChain = getInlineDatum(utxo);
774
+ if (!onChain) {
775
+ throw new Error(`upgrade_multisig config UTxO at ${address} carries no inline datum. The tree IS the ` +
776
+ `authority; without it the credential is unsatisfiable and there is no repair path.`);
777
+ }
778
+ // ⚠ `decodeMultisigScript` deliberately enforces none of upstream's
779
+ // `well_formed` rules — that asymmetry with the encoder is intentional — so
780
+ // the SHAPE is asserted here, against what the caller said it minted.
781
+ const tree = decodeMultisigScript(onChain);
782
+ if (!sameTree(tree, params.expectedTree)) {
783
+ throw new Error(`upgrade_multisig config UTxO holds a different authority tree than the one this ` +
784
+ `bootstrap minted.\n expected: ${JSON.stringify(params.expectedTree, replacer)}\n ` +
785
+ `on chain: ${JSON.stringify(tree, replacer)}\nAn authority nobody can satisfy is a ` +
786
+ `permanent brick with no repair path, so this refuses before the genesis rather than ` +
787
+ `after it.`);
788
+ }
789
+ return {
790
+ utxo,
791
+ ref: {
792
+ txHash: EvoTransactionHash.toHex(utxo.transactionId).toLowerCase(),
793
+ outputIndex: Number(utxo.index),
794
+ },
795
+ };
796
+ }
797
+ const replacer = (_k, v) => (typeof v === "bigint" ? `${v}` : v);
798
+ /** Structural equality for a MultisigScript tree. Order-sensitive, as the encoding is. */
799
+ function sameTree(a, b) {
800
+ if (a.type !== b.type)
801
+ return false;
802
+ switch (a.type) {
803
+ case "signature":
804
+ return a.keyHash.toLowerCase() === b.keyHash.toLowerCase();
805
+ case "script":
806
+ return a.scriptHash.toLowerCase() === b.scriptHash.toLowerCase();
807
+ case "before":
808
+ case "after":
809
+ return a.time === b.time;
810
+ case "at-least":
811
+ if (a.required !== b.required)
812
+ return false;
813
+ // falls through — the child list is compared the same way
814
+ case "all-of":
815
+ case "any-of": {
816
+ const bs = b.scripts;
817
+ const as = a.scripts;
818
+ return as.length === bs.length && as.every((c, i) => sameTree(c, bs[i]));
819
+ }
820
+ }
821
+ }
822
+ /**
823
+ * Step 4 — the protocol genesis: three one-shot mints, the three UTxOs that
824
+ * hold this instance's state, and THE ACTIVATION OF THE UPGRADE AUTHORITY.
825
+ *
826
+ * Outputs, and the indices are POSITIONAL — `assembleDeploymentParams` reads
827
+ * output 0 and nothing checks the rest but the chain:
828
+ * 0 the params UTxO (NFT + the six-field params datum)
829
+ * 1 the registry origin node (NFT + the sentinel head of the linked list)
830
+ * 2 the issuance CBOR UTxO (NFT + the spliced issuance_mint body)
831
+ *
832
+ * ⛔ THE ACTIVATION, NEW IN alpha.5 AND THE REASON THIS STEP'S SIGNATURE MOVED.
833
+ * Upstream added one line to `protocol_params.mint`:
834
+ *
835
+ * pairs.has_key(self.withdrawals, genesis_params.upgrade_cred)?
836
+ *
837
+ * The transaction that creates the protocol-params NFT must carry a withdraw-0
838
+ * from the credential its OWN datum names as the upgrade authority. The point
839
+ * is that a typo, or the hash of a script nobody deployed, can never become the
840
+ * authority: the authority has to RUN, in the genesis transaction, before it is
841
+ * canonical. It is the same trampoline every later upgrade uses, so nothing is
842
+ * being proven here that is not proven again on every upgrade.
843
+ *
844
+ * Four things go in together, and three of them are consequences of the fourth:
845
+ * 1. `withdraw({ stakeCredential: Script(upgrade_multisig), amount: 0n })`;
846
+ * 2. the `upgrade_multisig` SCRIPT BODY, attached — the reference scripts are
847
+ * published in the NEXT step, so there is nothing to reference yet;
848
+ * 3. the config UTxO as a REFERENCE INPUT, because that is where
849
+ * `upgrade_multisig.withdraw` reads the authority tree from;
850
+ * 4. the `extra_signatories` the tree asks for — a caller input.
851
+ *
852
+ * ⛔ AND THE CREDENTIAL MUST ALREADY BE REGISTERED, IN AN EARLIER TRANSACTION.
853
+ * `buildStakeRegistrationTx` now runs BEFORE this step. Nothing here can check
854
+ * that it did: an unregistered credential does not fail with "not registered",
855
+ * it fails as ledger code 3141, *"rewards withdrawals must consume rewards in
856
+ * full"*, which reads as a balance problem and sends the reader to the wallet.
857
+ */
858
+ export async function buildProtocolGenesisTx(params) {
859
+ requireBuildContext(params, "protocol-genesis");
860
+ const { plan } = params;
861
+ if (!plan || typeof plan !== "object" || !plan.scripts) {
862
+ throw new Error("bootstrap protocol-genesis: plan is required — see planBootstrap().");
863
+ }
864
+ requireSeedMatches(params.protocolParamsSeedUtxo, plan.config.seeds.protocolParams, "protocolParams");
865
+ requireSeedMatches(params.issuanceSeedUtxo, plan.config.seeds.issuance, "issuance");
866
+ // ⛔ THE ACTIVATION'S TWO CALLER INPUTS, CHECKED BY NAME BEFORE ANYTHING IS
867
+ // BUILT. Both were added at 0.12.0 for alpha.5; a caller still passing the
868
+ // 0.11.x argument object reaches this and is told which field is missing and
869
+ // why, rather than building a transaction the ledger refuses for a reason
870
+ // that names neither.
871
+ const configUtxo = params.upgradeMultisigConfigUtxo;
872
+ if (!configUtxo || typeof configUtxo !== "object" || configUtxo.transactionId === undefined) {
873
+ throw new Error(`bootstrap protocol-genesis: upgradeMultisigConfigUtxo is required — the upgrade_multisig ` +
874
+ `config UTxO, carrying the config NFT and the authority tree as an inline datum, read ` +
875
+ `back off the chain. alpha.5's protocol_params.mint demands a withdraw-0 from ` +
876
+ `upgrade_cred, and upgrade_multisig.withdraw reads its tree from this UTxO among the ` +
877
+ `reference inputs. Get it from assertMultisigConfigUtxo(), which also refuses a decoy ` +
878
+ `parked at the same address.`);
879
+ }
880
+ const configAddress = EvoAddress.toBech32(configUtxo.address);
881
+ if (configAddress !== plan.addresses.upgradeMultisig) {
882
+ throw new Error(`bootstrap protocol-genesis: upgradeMultisigConfigUtxo sits at ${configAddress}, but this ` +
883
+ `plan's upgrade_multisig address is ${plan.addresses.upgradeMultisig}. A UTxO at any ` +
884
+ `other address cannot be this deployment's config: the address payment credential IS ` +
885
+ `the config NFT's policy id IS the withdraw-0 credential, one value in three roles.`);
886
+ }
887
+ if (!Array.isArray(params.upgradeAuthoritySigners)) {
888
+ throw new Error(`bootstrap protocol-genesis: upgradeAuthoritySigners is required and must be an array of ` +
889
+ `28-byte key hashes (hex), got ${JSON.stringify(params.upgradeAuthoritySigners)}. It ` +
890
+ `may be EMPTY — a tree of Script or time leaves alone needs no signature — but it may ` +
891
+ `not be omitted. It cannot be derived from the tree: only a Signature leaf names a key ` +
892
+ `hash, and AnyOf/AtLeast leave a genuine choice of which branch to satisfy. Which ` +
893
+ `branch you are satisfying is your decision, not this package's.`);
894
+ }
895
+ const signers = params.upgradeAuthoritySigners.map((k, i) => requireHex(k, `upgradeAuthoritySigners[${i}]`, 28));
896
+ const { coinsPerUtxoByte, maxTxSize } = await params.client.getProtocolParameters();
897
+ const paramsNft = new Map([[plan.assetUnits.protocolParamsNft, 1n]]);
898
+ const registryNft = new Map([[plan.assetUnits.registryNode, 1n]]);
899
+ const issuanceNft = new Map([[plan.assetUnits.issuanceCborHexNft, 1n]]);
900
+ let tx = params.client.newTx();
901
+ if (params.provenancePin) {
902
+ const record = buildCip171RecordFromPin(plan.config.blueprint, params.provenancePin, plan.parameterizations);
903
+ tx = tx.attachMetadata({
904
+ label: CIP171_METADATA_LABEL,
905
+ metadata: buildCip171Metadatum(record),
906
+ });
907
+ }
908
+ tx = tx.collectFrom({ inputs: [params.protocolParamsSeedUtxo, params.issuanceSeedUtxo] });
909
+ // --- The upgrade authority activates itself (alpha.5) --------------------
910
+ //
911
+ // ⚠ ORDER OF THE THREE CALLS DOES NOT MATTER — the builder defers and
912
+ // canonicalises — but the SET does. Drop any one and the mint handler,
913
+ // upgrade_multisig.withdraw, or the ledger refuses, each for a different
914
+ // reason and none of them naming the other two.
915
+ tx = tx.readFrom({ referenceInputs: [configUtxo] });
916
+ tx = tx.withdraw({
917
+ stakeCredential: Credential.makeScriptHash(Bytes.fromHex(plan.scripts.upgradeMultisig.hash)),
918
+ amount: 0n,
919
+ // `upgrade_multisig.withdraw` binds its redeemer as `_redeemer: Data` and
920
+ // never reads it. Void is the smallest thing that satisfies the ledger's
921
+ // "a script witness needs a redeemer" rule.
922
+ redeemer: voidData(),
923
+ });
924
+ // Attached, not referenced: step 5 has not published the reference scripts
925
+ // yet, and this transaction is the one that must run before they exist.
926
+ tx = tx.attachScript({ script: buildEvoScript(plan.scripts.upgradeMultisig.compiledCode) });
927
+ for (const keyHash of signers) {
928
+ tx = tx.addSigner({ keyHash: KeyHash.fromHex(keyHash) });
929
+ }
930
+ // ⚠ THREE POLICIES, THREE REDEEMERS, EACH THE GENESIS ARM OF ITS OWN
931
+ // VALIDATOR. The indices are not a shared enum and do not correspond to each
932
+ // other; they are copied verbatim from the sequence proven on chain.
933
+ tx = tx.mintAssets({
934
+ assets: mintAssetsFromMap(registryNft),
935
+ redeemer: registryInitRedeemer(),
936
+ });
937
+ tx = tx.mintAssets({
938
+ assets: mintAssetsFromMap(paramsNft),
939
+ redeemer: Data.constr(1n, []),
940
+ });
941
+ tx = tx.mintAssets({
942
+ assets: mintAssetsFromMap(issuanceNft),
943
+ redeemer: Data.constr(2n, []),
944
+ });
945
+ // ⚠ SOLVED, NOT FLAT, for every datum-bearing output here. The params datum
946
+ // grew from four fields to six and the origin node from five to seven;
947
+ // min-UTxO scales with serialised output size, and an under-funded output is
948
+ // reported as "insufficient Ada" — which sends the reader to the wallet
949
+ // balance rather than to the datum.
950
+ tx = tx.payToAddress({
951
+ address: EvoAddress.fromBech32(plan.addresses.protocolParams),
952
+ assets: outputAssets(genesisOutputLovelace({
953
+ address: plan.addresses.protocolParams,
954
+ assets: outputAssets(0n, paramsNft),
955
+ datum: plan.datums.protocolParams,
956
+ coinsPerUtxoByte,
957
+ }), paramsNft),
958
+ datum: new InlineDatum.InlineDatum({ data: plan.datums.protocolParams }),
959
+ });
960
+ tx = tx.payToAddress({
961
+ address: EvoAddress.fromBech32(plan.addresses.registry),
962
+ assets: outputAssets(ceilToWholeAda(minUtxoAtLeast(REGISTRY_NODE_MIN_ADA, {
963
+ address: plan.addresses.registry,
964
+ assets: outputAssets(0n, registryNft),
965
+ datum: plan.datums.registryOrigin,
966
+ coinsPerUtxoByte,
967
+ })), registryNft),
968
+ datum: new InlineDatum.InlineDatum({ data: plan.datums.registryOrigin }),
969
+ });
970
+ // The issuance datum carries the whole spliced script body, so its min-UTxO
971
+ // is an order of magnitude above the others. Solved for the same reason.
972
+ tx = tx.payToAddress({
973
+ address: EvoAddress.fromBech32(plan.addresses.issuanceCborHex),
974
+ assets: outputAssets(genesisOutputLovelace({
975
+ address: plan.addresses.issuanceCborHex,
976
+ assets: outputAssets(0n, issuanceNft),
977
+ datum: plan.datums.issuanceCborHex,
978
+ coinsPerUtxoByte,
979
+ }), issuanceNft),
980
+ datum: new InlineDatum.InlineDatum({ data: plan.datums.issuanceCborHex }),
981
+ });
982
+ tx = tx.attachScript({ script: buildEvoScript(plan.scripts.registry.compiledCode) });
983
+ tx = tx.attachScript({ script: buildEvoScript(plan.scripts.protocolParams.compiledCode) });
984
+ tx = tx.attachScript({ script: buildEvoScript(plan.scripts.issuanceCborHexMint.compiledCode) });
985
+ const unsigned = await finish(tx, params, "protocol-genesis", {
986
+ outputIndices: { protocolParams: 0, registryOrigin: 1, issuanceCborHex: 2 },
987
+ cip171: Boolean(params.provenancePin),
988
+ // ⚑ EMITTED SO A CALLER CAN ASSERT ON IT. The activation is invisible in
989
+ // the output list and in the datums; without this, "did the genesis carry
990
+ // the withdraw-0?" is answerable only by decoding the CBOR.
991
+ upgradeActivation: {
992
+ withdrawal: plan.scripts.upgradeMultisig.hash,
993
+ configUtxo: refKey({
994
+ txHash: EvoTransactionHash.toHex(configUtxo.transactionId),
995
+ outputIndex: Number(configUtxo.index),
996
+ }),
997
+ signers,
998
+ },
999
+ });
1000
+ // ⚑ THE SIZE, REPORTED RATHER THAN ASSUMED — and alpha.5 is why it is worth
1001
+ // reporting. This transaction already carried three script bodies, the
1002
+ // six-field params datum and the issuance datum, which holds the whole
1003
+ // spliced `issuance_mint` body; the activation adds a FOURTH script, and at
1004
+ // this blueprint `upgrade_multisig` is the largest of the four. The repo has
1005
+ // burst the 16,384-byte cap here before — MEASURED at 21,816 when the stake
1006
+ // registrations rode along — and the failure arrives at SUBMISSION, naming a
1007
+ // size and not a cause.
1008
+ //
1009
+ // ⚠ REPORTED, NOT REFUSED, and the asymmetry with `buildReferenceScriptsTx`
1010
+ // is deliberate. That step's content is this package's own fixed list, so a
1011
+ // refusal there names something the caller can act on. This step's size is
1012
+ // dominated by a caller-chosen datum, and a hard refusal here would reject a
1013
+ // transaction the ledger might still take. The figure is a LOWER BOUND
1014
+ // either way: witnesses are not added yet.
1015
+ const bytes = unsigned.cbor.length / 2;
1016
+ return { ...unsigned, metadata: { ...unsigned.metadata, unsignedBytes: bytes, maxTxSize } };
1017
+ }
1018
+ /**
1019
+ * Step 5 — publish the seven reference scripts, in {@link REFERENCE_SCRIPT_ORDER}.
1020
+ *
1021
+ * Cannot be folded into the genesis: a transaction cannot reference a script it
1022
+ * is itself creating. That is also why the genesis ATTACHES the
1023
+ * `upgrade_multisig` body rather than referencing it — at genesis time there is
1024
+ * no published reference script to reach for.
1025
+ */
1026
+ export async function buildReferenceScriptsTx(params) {
1027
+ requireBuildContext(params, "reference-scripts");
1028
+ const { plan } = params;
1029
+ if (!plan || typeof plan !== "object" || !plan.referenceScripts) {
1030
+ throw new Error("bootstrap reference-scripts: plan is required — see planBootstrap().");
1031
+ }
1032
+ if (typeof params.referenceScriptAddress !== "string" ||
1033
+ params.referenceScriptAddress.length === 0) {
1034
+ throw new Error(`bootstrap reference-scripts: referenceScriptAddress is required (bech32). These outputs ` +
1035
+ `are protocol infrastructure for the life of the deployment; where they live is a ` +
1036
+ `decision, not a default.`);
1037
+ }
1038
+ if (typeof params.referenceScriptLovelace !== "bigint" || params.referenceScriptLovelace <= 0n) {
1039
+ throw new Error(`bootstrap reference-scripts: referenceScriptLovelace is required and must be a positive ` +
1040
+ `bigint, got ${JSON.stringify(String(params.referenceScriptLovelace))}. min-UTxO for a ` +
1041
+ `script-bearing output scales with the script's size.`);
1042
+ }
1043
+ // ⛔ REQUIRED IS NOT THE SAME AS VALID (F-11), and a script-bearing output is
1044
+ // where the gap bites hardest: the script's own bytes are part of the
1045
+ // serialised output, so its min-UTxO is an order of magnitude above a plain
1046
+ // one. A caller passing a plain-output figure builds happily and is refused
1047
+ // at submission as "insufficient Ada".
1048
+ //
1049
+ // ⚠ A SOUND LOWER BOUND, NOT THE EXACT FIGURE. `minUtxoForOutput` takes no
1050
+ // script, so this adds the largest script's own bytes at `coinsPerUtxoByte`
1051
+ // each. The true requirement is higher by the script ref's CBOR wrapping —
1052
+ // which is why this REFUSES BELOW the bound and never reports it as
1053
+ // sufficient.
1054
+ const { coinsPerUtxoByte, maxTxSize } = await params.client.getProtocolParameters();
1055
+ let largest = { name: "(none)", bytes: 0 };
1056
+ plan.referenceScripts.forEach((script, i) => {
1057
+ const bytes = scriptBodyHex(script.compiledCode).length / 2;
1058
+ if (bytes > largest.bytes)
1059
+ largest = { name: REFERENCE_SCRIPT_ORDER[i], bytes };
1060
+ });
1061
+ const refFloor = minLovelaceForPlainOutput(params.referenceScriptAddress, coinsPerUtxoByte) +
1062
+ coinsPerUtxoByte * BigInt(largest.bytes);
1063
+ if (params.referenceScriptLovelace < refFloor) {
1064
+ throw new Error(`bootstrap reference-scripts: referenceScriptLovelace is ` +
1065
+ `${params.referenceScriptLovelace} lovelace, below the ${refFloor} a script-bearing ` +
1066
+ `output needs at coinsPerUtxoByte ${coinsPerUtxoByte} — the largest script here is ` +
1067
+ `${largest.name} at ${largest.bytes} bytes, and a script's own bytes count toward ` +
1068
+ `min-UTxO. The ledger would reject this as "insufficient Ada" at submission. ⚠ The ` +
1069
+ `figure quoted is a LOWER BOUND: the true requirement is higher by the script ref's ` +
1070
+ `CBOR wrapping.`);
1071
+ }
1072
+ const to = EvoAddress.fromBech32(params.referenceScriptAddress);
1073
+ let tx = params.client.newTx();
1074
+ for (const script of plan.referenceScripts) {
1075
+ tx = tx.payToAddress({
1076
+ address: to,
1077
+ assets: outputAssets(params.referenceScriptLovelace),
1078
+ script: buildEvoScript(script.compiledCode),
1079
+ });
1080
+ }
1081
+ const unsigned = await finish(tx, params, "reference-scripts", {
1082
+ order: [...REFERENCE_SCRIPT_ORDER],
1083
+ outputIndices: Object.fromEntries(REFERENCE_SCRIPT_ORDER.map((n, i) => [n, i])),
1084
+ });
1085
+ // ⛔ THE SIZE CAP, CHECKED HERE RATHER THAN DISCOVERED AT SUBMISSION (F-9).
1086
+ // This transaction carries every published script body at once and is the
1087
+ // one that grows when REFERENCE_SCRIPT_ORDER grows — MEASURED at 12,496 of
1088
+ // 16,384 bytes for the current seven. Unsigned, so this is a LOWER BOUND on
1089
+ // the signed size; it refuses the certain failures and cannot promise the
1090
+ // marginal ones.
1091
+ const bytes = unsigned.cbor.length / 2;
1092
+ if (bytes >= maxTxSize) {
1093
+ throw new Error(`bootstrap reference-scripts: the unsigned transaction publishing ` +
1094
+ `${plan.referenceScripts.length} reference scripts is ${bytes} bytes, at or over this ` +
1095
+ `chain's ${maxTxSize}-byte maximum — and witnesses have not been added yet. Split the ` +
1096
+ `publication across two transactions and record both hashes; do not shorten ` +
1097
+ `REFERENCE_SCRIPT_ORDER, whose indices a deployment record already depends on.`);
1098
+ }
1099
+ return { ...unsigned, metadata: { ...unsigned.metadata, unsignedBytes: bytes, maxTxSize } };
1100
+ }
1101
+ /**
1102
+ * Step 3 — the six Conway `RegCert`s, in one transaction.
1103
+ *
1104
+ * ⛔ THIS RAN LAST UNTIL 0.12.0 AND NOW RUNS BEFORE THE GENESIS. alpha.5's
1105
+ * `protocol_params.mint` requires a withdraw-0 from `upgrade_cred`, and the
1106
+ * ledger applies withdrawals against the reward-account state BEFORE it applies
1107
+ * certificates — so registering and withdrawing in one transaction is not a
1108
+ * transaction, it is two. Only `upgradeMultisig` is forced earlier by that
1109
+ * rule; the other five ride along because they share this transaction, and
1110
+ * every one of the six `publish` handlers admits `RegisterCredential` and reads
1111
+ * no chain state, so none of them cares that the protocol does not exist yet.
1112
+ *
1113
+ * ⛔ `registerStake` + `attachScript` + a void redeemer, NEVER
1114
+ * `registerAndDelegateTo`, FOR A SCRIPT CREDENTIAL. MEASURED: a combined
1115
+ * certificate is a Conway `vote_reg_deleg_cert`, which arrives at the `publish`
1116
+ * handler as a DIFFERENT `Certificate` constructor, and both
1117
+ * `upgrade_multisig.publish` and `issuance_logic.publish` admit
1118
+ * `RegisterCredential` and nothing else.
1119
+ *
1120
+ * ⚠ The DRep delegation a KEY credential needs is not missing here by oversight.
1121
+ * A script withdraw-0 needs three things — a script witness, a registration, and
1122
+ * the withdrawal itself. The DRep delegation is the FOURTH thing a key
1123
+ * credential needs, and a script cannot have one.
1124
+ *
1125
+ * ⚠ Kept separate from the genesis because each certificate executes its script
1126
+ * under the PUBLISH purpose: carrying all six script bodies alongside the mint
1127
+ * witnesses is what breaks the 16,384-byte size cap. MEASURED at 21,816 bytes
1128
+ * when this was one transaction.
1129
+ */
1130
+ export async function buildStakeRegistrationTx(params) {
1131
+ requireBuildContext(params, "stake-registrations");
1132
+ const { plan } = params;
1133
+ if (!plan || typeof plan !== "object" || !plan.stakeCredentialScripts) {
1134
+ throw new Error("bootstrap stake-registrations: plan is required — see planBootstrap().");
1135
+ }
1136
+ let tx = params.client.newTx();
1137
+ for (const script of plan.stakeCredentialScripts) {
1138
+ tx = tx.registerStake({
1139
+ stakeCredential: Credential.makeScriptHash(Bytes.fromHex(script.hash)),
1140
+ redeemer: voidData(),
1141
+ });
1142
+ tx = tx.attachScript({ script: buildEvoScript(script.compiledCode) });
1143
+ }
1144
+ return finish(tx, params, "stake-registrations", {
1145
+ order: [...STAKE_REGISTRATION_ORDER],
1146
+ credentials: plan.stakeCredentialScripts.map((s) => s.hash),
1147
+ });
1148
+ }
1149
+ /**
1150
+ * Assemble `DeploymentParams` from the plan and what was observed. Pure.
1151
+ *
1152
+ * ⛔ THE RECORD MUST SAY WHAT THE DATUM SAYS. `upgradeAuthority` is the
1153
+ * deployment's account of the ACTIVE authority and the genesis datum writes
1154
+ * `upgradeCred: Script(upgradeMultisig)`. Nothing derives one from the other and
1155
+ * nothing may check them against each other on chain — which is exactly why
1156
+ * both come from the same plan here, rather than being written twice.
1157
+ */
1158
+ export function assembleDeploymentParams(plan, observed) {
1159
+ if (!plan || typeof plan !== "object" || !plan.scripts) {
1160
+ throw new Error("assembleDeploymentParams: plan is required — see planBootstrap().");
1161
+ }
1162
+ const genesisTxHash = requireHex(observed?.protocolGenesisTxHash, "observed.protocolGenesisTxHash", 32).toLowerCase();
1163
+ const refTxHash = requireHex(observed?.referenceScriptsTxHash, "observed.referenceScriptsTxHash", 32).toLowerCase();
1164
+ const multisigUtxo = requireTxInput(observed?.multisigConfigUtxo, "observed.multisigConfigUtxo");
1165
+ const refIdx = (name) => ({
1166
+ txHash: refTxHash,
1167
+ outputIndex: REFERENCE_SCRIPT_ORDER.indexOf(name),
1168
+ });
1169
+ const s = plan.scripts;
1170
+ return {
1171
+ txHash: genesisTxHash,
1172
+ protocolParams: {
1173
+ txInput: plan.config.seeds.protocolParams,
1174
+ // ONE value: policy id AND address payment credential.
1175
+ policyId: s.protocolParams.hash,
1176
+ utxo: { txHash: genesisTxHash, outputIndex: 0 },
1177
+ },
1178
+ programmableLogicBase: { scriptHash: s.programmableLogicBase.hash },
1179
+ transfer: { scriptHash: s.transfer.hash },
1180
+ thirdParty: { scriptHash: s.thirdParty.hash },
1181
+ unfracking: { scriptHash: s.unfracking.hash },
1182
+ programmableLogicGlobal: {
1183
+ scriptHash: s.programmableLogicGlobal.hash,
1184
+ // ⛔ WHAT THE DISPATCHER WAS COMPILED AGAINST, and it is NOT derivable
1185
+ // from `unfracking.scriptHash` above: a deployment may have unfracking
1186
+ // fully deployed, published and recorded while compiling this dispatcher
1187
+ // against the sentinel, so the arm can never be satisfied. Two fields,
1188
+ // two different facts, and neither may be defaulted from the other.
1189
+ unfrackingParameter: plan.unfrackingParameter,
1190
+ },
1191
+ maxInlineDatumBytes: Number(plan.config.maxInlineDatumBytes),
1192
+ issuanceLogic: { scriptHash: s.issuanceLogic.hash },
1193
+ upgradeMultisig: {
1194
+ scriptHash: s.upgradeMultisig.hash,
1195
+ // ⚠ The upgradeMultisig seed, and NOT the protocolParams one. Same type,
1196
+ // not interchangeable — see BOOTSTRAP_SEED_COUNT.
1197
+ txInput: plan.config.seeds.upgradeMultisig,
1198
+ utxo: multisigUtxo,
1199
+ },
1200
+ upgradeAuthority: { type: "script", hash: s.upgradeMultisig.hash },
1201
+ issuance: {
1202
+ txInput: plan.config.seeds.issuance,
1203
+ policyId: s.issuanceCborHexMint.hash,
1204
+ alwaysFailScriptHash: s.alwaysFail.hash,
1205
+ },
1206
+ registry: {
1207
+ txInput: plan.config.seeds.protocolParams,
1208
+ issuanceScriptHash: s.issuanceCborHexMint.hash,
1209
+ // ONE value again: node NFT policy AND node address payment credential.
1210
+ scriptHash: s.registry.hash,
1211
+ },
1212
+ programmableBaseRefInput: refIdx("programmableLogicBase"),
1213
+ programmableLogicGlobalRefInput: refIdx("programmableLogicGlobal"),
1214
+ transferRefInput: refIdx("transfer"),
1215
+ thirdPartyRefInput: refIdx("thirdParty"),
1216
+ unfrackingRefInput: refIdx("unfracking"),
1217
+ issuanceLogicRefInput: refIdx("issuanceLogic"),
1218
+ upgradeMultisigRefInput: refIdx("upgradeMultisig"),
1219
+ };
1220
+ }
1221
+ //# sourceMappingURL=bootstrap.js.map