@sm-lab/recipes 0.2.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.
@@ -0,0 +1,653 @@
1
+ import { _ as contract, h as roleMember, i as getPubkey, l as SETTLE_GENERAL_DELAYED_PENALTY_ROLE, m as actAs, o as DEFAULT_ADMIN_ROLE, p as randomSeed, r as getKeyBalance, s as REPORT_GENERAL_DELAYED_PENALTY_ROLE, u as SET_TREE_ROLE, v as resolveGate } from "./topup-B2NzJV7_.mjs";
2
+ import { encodeAbiParameters, keccak256, maxUint256, parseAbiParameters, parseEther, toBytes, toHex } from "viem";
3
+ import { curatedGateAbi, feeDistributorAbi, feeOracleAbi, hashConsensusAbi, lidoAbi, vettedGateAbi } from "@sm-lab/receipts";
4
+ import { buildAddressesTree, ipfsOptionsFromEnv, makeRewards, pinJsonToIpfs, shouldAttemptPin } from "@sm-lab/merkle";
5
+ //#region src/recipes/operator-info.ts
6
+ /** Typed read of a node operator's on-chain record. (Port of `NodeOperators.operatorInfo`.) */
7
+ async function operatorInfo(ctx, opts) {
8
+ const m = contract(ctx, "module");
9
+ return await ctx.client.readContract({
10
+ ...m,
11
+ functionName: "getNodeOperator",
12
+ args: [opts.noId]
13
+ });
14
+ }
15
+ //#endregion
16
+ //#region src/recipes/chain.ts
17
+ /** Advance fork time by `seconds` and mine a block (evm_increaseTime + evm_mine). */
18
+ async function warpBy(ctx, seconds) {
19
+ await ctx.client.increaseTime({ seconds: Number(seconds) });
20
+ await ctx.client.mine({ blocks: 1 });
21
+ }
22
+ /**
23
+ * Warp fork time to an absolute unix timestamp and mine a block
24
+ * (evm_setNextBlockTimestamp + evm_mine). The absolute counterpart of `warpBy` — used by
25
+ * `submitRewards`'s consensus-frame wait. Over RPC, `setNextBlockTimestamp` + `mine` reproduces
26
+ * Foundry's `vm.warp(ts)` (the post-warp `block.timestamp` settles automatically after mining).
27
+ */
28
+ async function warpTo(ctx, timestamp) {
29
+ await ctx.client.setNextBlockTimestamp({ timestamp: BigInt(timestamp) });
30
+ await ctx.client.mine({ blocks: 1 });
31
+ }
32
+ /** Take an anvil state snapshot; returns the snapshot id (evm_snapshot). */
33
+ function snapshot(ctx) {
34
+ return ctx.client.snapshot();
35
+ }
36
+ /** Revert the fork to a snapshot id (evm_revert). */
37
+ async function revert(ctx, id) {
38
+ await ctx.client.revert({ id });
39
+ }
40
+ //#endregion
41
+ //#region src/recipes/address-changes.ts
42
+ /** Propose a new manager address for an operator (signed by the current manager). */
43
+ async function proposeManager(ctx, opts) {
44
+ const m = contract(ctx, "module");
45
+ await actAs(ctx, (await ctx.client.readContract({
46
+ ...m,
47
+ functionName: "getNodeOperator",
48
+ args: [opts.noId]
49
+ })).managerAddress, (from) => ctx.client.writeContract({
50
+ ...m,
51
+ functionName: "proposeNodeOperatorManagerAddressChange",
52
+ args: [opts.noId, opts.proposed],
53
+ account: from,
54
+ chain: null
55
+ }));
56
+ }
57
+ /** Confirm the proposed manager address (signed by the proposed manager). */
58
+ async function confirmManager(ctx, opts) {
59
+ const m = contract(ctx, "module");
60
+ await actAs(ctx, (await ctx.client.readContract({
61
+ ...m,
62
+ functionName: "getNodeOperator",
63
+ args: [opts.noId]
64
+ })).proposedManagerAddress, (from) => ctx.client.writeContract({
65
+ ...m,
66
+ functionName: "confirmNodeOperatorManagerAddressChange",
67
+ args: [opts.noId],
68
+ account: from,
69
+ chain: null
70
+ }));
71
+ }
72
+ /** Propose a new reward address (signed by the MANAGER, per the on-chain access rule). */
73
+ async function proposeReward(ctx, opts) {
74
+ const m = contract(ctx, "module");
75
+ await actAs(ctx, (await ctx.client.readContract({
76
+ ...m,
77
+ functionName: "getNodeOperator",
78
+ args: [opts.noId]
79
+ })).managerAddress, (from) => ctx.client.writeContract({
80
+ ...m,
81
+ functionName: "proposeNodeOperatorRewardAddressChange",
82
+ args: [opts.noId, opts.proposed],
83
+ account: from,
84
+ chain: null
85
+ }));
86
+ }
87
+ /** Confirm the proposed reward address (signed by the proposed reward address). */
88
+ async function confirmReward(ctx, opts) {
89
+ const m = contract(ctx, "module");
90
+ await actAs(ctx, (await ctx.client.readContract({
91
+ ...m,
92
+ functionName: "getNodeOperator",
93
+ args: [opts.noId]
94
+ })).proposedRewardAddress, (from) => ctx.client.writeContract({
95
+ ...m,
96
+ functionName: "confirmNodeOperatorRewardAddressChange",
97
+ args: [opts.noId],
98
+ account: from,
99
+ chain: null
100
+ }));
101
+ }
102
+ //#endregion
103
+ //#region src/encode.ts
104
+ /** A node-operator id packed as the on-chain `bytes8(uint64)` (big-endian). */
105
+ function nodeOperatorIdBytes(noId) {
106
+ return toHex(noId, { size: 8 });
107
+ }
108
+ /** A key count packed as the on-chain `bytes16(uint128)` (big-endian). */
109
+ function keyCountBytes(count) {
110
+ return toHex(count, { size: 16 });
111
+ }
112
+ //#endregion
113
+ //#region src/recipes/vetting.ts
114
+ /** Set an operator's vetted-keys count (StakingRouter-gated). `vettedKeys` is the new absolute count. */
115
+ async function unvet(ctx, opts) {
116
+ const m = contract(ctx, "module");
117
+ await actAs(ctx, ctx.addresses.stakingRouter, (from) => ctx.client.writeContract({
118
+ ...m,
119
+ functionName: "decreaseVettedSigningKeysCount",
120
+ args: [nodeOperatorIdBytes(opts.noId), keyCountBytes(opts.vettedKeys)],
121
+ account: from,
122
+ chain: null
123
+ }));
124
+ }
125
+ /** Set an operator's exited-keys count (StakingRouter-gated). `exitedKeys` is the new absolute total. */
126
+ async function exit(ctx, opts) {
127
+ const m = contract(ctx, "module");
128
+ await actAs(ctx, ctx.addresses.stakingRouter, (from) => ctx.client.writeContract({
129
+ ...m,
130
+ functionName: "updateExitedValidatorsCount",
131
+ args: [nodeOperatorIdBytes(opts.noId), keyCountBytes(opts.exitedKeys)],
132
+ account: from,
133
+ chain: null
134
+ }));
135
+ }
136
+ //#endregion
137
+ //#region src/recipes/validators.ts
138
+ /** Report a validator slashing for an operator's key (Verifier-gated). `keyIndex` is the storage index. */
139
+ async function slash(ctx, opts) {
140
+ const m = contract(ctx, "module");
141
+ await actAs(ctx, ctx.addresses.Verifier, (from) => ctx.client.writeContract({
142
+ ...m,
143
+ functionName: "reportValidatorSlashing",
144
+ args: [opts.noId, opts.keyIndex],
145
+ account: from,
146
+ chain: null
147
+ }));
148
+ }
149
+ /** Report a regular validator withdrawal for one key (Verifier-gated). isSlashed = slashingPenalty > 0. */
150
+ async function withdraw(ctx, opts) {
151
+ const m = contract(ctx, "module");
152
+ const slashingPenalty = opts.slashingPenalty ?? 0n;
153
+ const info = {
154
+ nodeOperatorId: opts.noId,
155
+ keyIndex: opts.keyIndex,
156
+ exitBalance: opts.exitBalance,
157
+ slashingPenalty,
158
+ isSlashed: slashingPenalty > 0n
159
+ };
160
+ await actAs(ctx, ctx.addresses.Verifier, (from) => ctx.client.writeContract({
161
+ ...m,
162
+ functionName: "reportRegularWithdrawnValidators",
163
+ args: [[info]],
164
+ account: from,
165
+ chain: null
166
+ }));
167
+ }
168
+ //#endregion
169
+ //#region src/recipes/penalties.ts
170
+ /** Report a general delayed penalty against an operator (penalty-reporter role). */
171
+ async function reportPenalty(ctx, opts) {
172
+ const m = contract(ctx, "module");
173
+ const reporter = await roleMember(ctx, m, REPORT_GENERAL_DELAYED_PENALTY_ROLE);
174
+ const penaltyType = opts.penaltyType ?? toHex(1n, { size: 32 });
175
+ const details = opts.details ?? "fork-penalty";
176
+ await actAs(ctx, reporter, (from) => ctx.client.writeContract({
177
+ ...m,
178
+ functionName: "reportGeneralDelayedPenalty",
179
+ args: [
180
+ opts.noId,
181
+ penaltyType,
182
+ opts.amount,
183
+ details
184
+ ],
185
+ account: from,
186
+ chain: null
187
+ }));
188
+ }
189
+ /** Cancel a previously-reported general delayed penalty (penalty-reporter role). */
190
+ async function cancelPenalty(ctx, opts) {
191
+ const m = contract(ctx, "module");
192
+ await actAs(ctx, await roleMember(ctx, m, REPORT_GENERAL_DELAYED_PENALTY_ROLE), (from) => ctx.client.writeContract({
193
+ ...m,
194
+ functionName: "cancelGeneralDelayedPenalty",
195
+ args: [opts.noId, opts.amount],
196
+ account: from,
197
+ chain: null
198
+ }));
199
+ }
200
+ /**
201
+ * Settle (process) an operator's general delayed penalty (penalty-settler role).
202
+ * The `maxAmount` option is the per-operator settlement cap passed as the contract's 2nd array arg (confusingly named `bondLockNonces` in the ABI); the default `maxUint256` settles the penalty fully.
203
+ */
204
+ async function settlePenalty(ctx, opts) {
205
+ const m = contract(ctx, "module");
206
+ await actAs(ctx, await roleMember(ctx, m, SETTLE_GENERAL_DELAYED_PENALTY_ROLE), (from) => ctx.client.writeContract({
207
+ ...m,
208
+ functionName: "settleGeneralDelayedPenalty",
209
+ args: [[opts.noId], [opts.maxAmount ?? maxUint256]],
210
+ account: from,
211
+ chain: null
212
+ }));
213
+ }
214
+ /** Compensate (pay off) an operator's general delayed penalty — signed by the operator manager. */
215
+ async function compensatePenalty(ctx, opts) {
216
+ const m = contract(ctx, "module");
217
+ await actAs(ctx, (await ctx.client.readContract({
218
+ ...m,
219
+ functionName: "getNodeOperator",
220
+ args: [opts.noId]
221
+ })).managerAddress, (from) => ctx.client.writeContract({
222
+ ...m,
223
+ functionName: "compensateGeneralDelayedPenalty",
224
+ args: [opts.noId],
225
+ account: from,
226
+ chain: null
227
+ }));
228
+ }
229
+ //#endregion
230
+ //#region src/recipes/bond.ts
231
+ /** Top up an operator's bond with ETH (permissionless `depositETH`), signed by the manager. */
232
+ async function addBond(ctx, opts) {
233
+ const m = contract(ctx, "module");
234
+ const acc = contract(ctx, "Accounting");
235
+ await actAs(ctx, (await ctx.client.readContract({
236
+ ...m,
237
+ functionName: "getNodeOperator",
238
+ args: [opts.noId]
239
+ })).managerAddress, (from) => ctx.client.writeContract({
240
+ ...acc,
241
+ functionName: "depositETH",
242
+ args: [opts.noId],
243
+ account: from,
244
+ value: opts.amount,
245
+ chain: null
246
+ }));
247
+ }
248
+ /**
249
+ * Charge a penalty against an operator's bond (creating bond debt if uncovered). `Accounting.penalize`
250
+ * is module-only, so we impersonate the module address. Returns whether the penalty was fully covered.
251
+ */
252
+ async function createBondDebt(ctx, opts) {
253
+ const moduleAddress = contract(ctx, "module").address;
254
+ const acc = contract(ctx, "Accounting");
255
+ return actAs(ctx, moduleAddress, async (from) => {
256
+ const { result, request } = await ctx.client.simulateContract({
257
+ ...acc,
258
+ functionName: "penalize",
259
+ args: [opts.noId, opts.amount],
260
+ account: from
261
+ });
262
+ await ctx.client.writeContract({
263
+ ...request,
264
+ chain: null
265
+ });
266
+ return { penaltyCovered: result };
267
+ });
268
+ }
269
+ //#endregion
270
+ //#region src/recipes/set-gate.ts
271
+ /** Default gate selector per module: cm → 'po' (CuratedGates[0]); csm → 'ics' (VettedGate). */
272
+ function defaultSelector(ctx) {
273
+ return ctx.module === "cm" ? "po" : "ics";
274
+ }
275
+ /**
276
+ * Build a gate addresses tree (OZ ['address'], via @sm-lab/merkle) and install it on the gate
277
+ * (`setTreeParams(root, cid)`), impersonating the gate admin. The cid is pinned to IPFS
278
+ * (env-configured: a local @sm-lab/ipfs or Pinata) unless `cid` is supplied.
279
+ *
280
+ * Module-agnostic (port of fork.just `set-gate-addrs` + `update-gate-tree`, both selector-driven):
281
+ * the csm VettedGate and cm CuratedGate share a byte-identical grantRole/setTreeParams/getRoleMember
282
+ * surface, so the only per-module differences are the resolved address, the default selector, and
283
+ * which gate ABI carries the fragments.
284
+ */
285
+ async function setGateAddrs(ctx, opts) {
286
+ const selector = opts.selector ?? defaultSelector(ctx);
287
+ const abi = ctx.module === "cm" ? curatedGateAbi : vettedGateAbi;
288
+ const gate = {
289
+ address: resolveGate(ctx, selector),
290
+ abi
291
+ };
292
+ const tree = buildAddressesTree(opts.addresses);
293
+ const treeRoot = tree.root;
294
+ const treeCid = opts.cid ?? await pinTree(tree.dump(), selector);
295
+ const admin = await roleMember(ctx, gate, DEFAULT_ADMIN_ROLE);
296
+ await actAs(ctx, admin, async (from) => {
297
+ await ctx.client.writeContract({
298
+ ...gate,
299
+ functionName: "grantRole",
300
+ args: [SET_TREE_ROLE, admin],
301
+ account: from,
302
+ chain: null
303
+ });
304
+ await ctx.client.writeContract({
305
+ ...gate,
306
+ functionName: "setTreeParams",
307
+ args: [treeRoot, treeCid],
308
+ account: from,
309
+ chain: null
310
+ });
311
+ });
312
+ return {
313
+ treeRoot,
314
+ treeCid
315
+ };
316
+ }
317
+ async function pinTree(dump, selector) {
318
+ if (!shouldAttemptPin()) throw new Error("@sm-lab/recipes: could not pin the gate tree — set IPFS_API_URL (a local @sm-lab/ipfs) or PINATA_* credentials, or pass opts.cid");
319
+ return pinJsonToIpfs(dump, `gate-${selector}`, ipfsOptionsFromEnv());
320
+ }
321
+ //#endregion
322
+ //#region src/recipes/rewards.ts
323
+ /** Reward draw bounds (mirrors `mock-rewards.mjs`): per active key, in wei. */
324
+ const REWARD_MIN_WEI = 100000000000000000n;
325
+ const REWARD_SPAN = 200000000000000000n - REWARD_MIN_WEI;
326
+ /** type(uint64).max — padding id for a lone real operator (FeeDistributor non-empty-proof rule). */
327
+ const PAD_NO_ID = (1n << 64n) - 1n;
328
+ /** Empty-tree sentinel root: 32 zero bytes. */
329
+ const ZERO_ROOT = `0x${"00".repeat(32)}`;
330
+ /**
331
+ * Build the cumulative FeeDistributor rewards tree off on-chain operator state and a seeded mock
332
+ * reward per active key, pin the tree + report log to IPFS (guarded), and return a typed in-memory
333
+ * `RewardsReport`. Port of `mock-rewards.mjs` — but the OUTPUT path stays bigint-typed (no bare
334
+ * `JSON.stringify` of bigints), and the per-key draw is fully seeded (no `Math.random`) so tests can
335
+ * pin `treeRoot`/`distributed`.
336
+ *
337
+ * Carry-forward is via `opts.previousCumulatives` only (option (a) of the design). The pure
338
+ * carry-forward logic — carry every prior leaf forward except the pad, then add this frame's deltas —
339
+ * is identical no matter where the prior Map came from, so the fork path chains calls:
340
+ * `makeRewards(ctx, { previousCumulatives: prev.cumulatives })`.
341
+ * TODO(6f?): on-chain `treeRoot()` → `treeCid()` → IPFS fetch of the prior tree as a default source.
342
+ */
343
+ async function makeRewards$1(ctx, opts = {}) {
344
+ const seed = opts.seed ?? randomSeed();
345
+ const m = contract(ctx, "module");
346
+ const count = await ctx.client.readContract({
347
+ ...m,
348
+ functionName: "getNodeOperatorsCount"
349
+ });
350
+ const ids = Array.from({ length: Number(count) }, (_, i) => BigInt(i));
351
+ const infos = await Promise.all(ids.map((noId) => operatorInfo(ctx, { noId })));
352
+ const reportOperators = {};
353
+ const frameDeltas = /* @__PURE__ */ new Map();
354
+ let distributed = 0n;
355
+ for (const [i, noId] of ids.entries()) {
356
+ const info = infos[i];
357
+ const activeKeys = info.totalDepositedKeys - info.totalWithdrawnKeys;
358
+ if (activeKeys <= 0) continue;
359
+ let opDistributed = 0n;
360
+ for (let k = 0; k < activeKeys; k++) opDistributed += REWARD_MIN_WEI + seededUint(seed, `reward:${noId}:${k}`, REWARD_SPAN);
361
+ reportOperators[noId.toString()] = { distributed_rewards: opDistributed };
362
+ frameDeltas.set(noId, opDistributed);
363
+ distributed += opDistributed;
364
+ }
365
+ const rebate = 0n;
366
+ const cumulatives = /* @__PURE__ */ new Map();
367
+ for (const [noId, shares] of normalizePrevious(opts.previousCumulatives)) if (noId !== PAD_NO_ID) cumulatives.set(noId, shares);
368
+ for (const [noId, delta] of frameDeltas) cumulatives.set(noId, (cumulatives.get(noId) ?? 0n) + delta);
369
+ const leaves = [...cumulatives.entries()].map(([noId, shares]) => [noId, shares]);
370
+ if (leaves.length === 1) leaves.push([PAD_NO_ID, 0n]);
371
+ if (leaves.length === 0) return {
372
+ treeRoot: ZERO_ROOT,
373
+ treeCid: "",
374
+ logCid: "",
375
+ distributed,
376
+ rebate,
377
+ cumulatives
378
+ };
379
+ if ((opts.treeCid === void 0 || opts.logCid === void 0) && !shouldAttemptPin()) throw new Error("@sm-lab/recipes: could not pin the rewards tree/log — set IPFS_API_URL (a local @sm-lab/ipfs) or PINATA_* credentials, or pass opts.treeCid + opts.logCid");
380
+ const log = {
381
+ blockstamp: { block_timestamp: opts.now ?? Math.floor(Date.now() / 1e3) },
382
+ distributable: distributed,
383
+ distributed_rewards: distributed,
384
+ rebate_to_protocol: rebate,
385
+ operators: reportOperators
386
+ };
387
+ let treeRoot;
388
+ let treeCid;
389
+ let logCid;
390
+ let treeDump;
391
+ if (opts.treeCid !== void 0 && opts.logCid !== void 0) {
392
+ const result = await makeRewards(leaves, { noUpload: true });
393
+ treeRoot = result.treeRoot;
394
+ treeDump = result.treeDump;
395
+ treeCid = opts.treeCid;
396
+ logCid = opts.logCid;
397
+ } else if (opts.treeCid !== void 0) {
398
+ const result = await makeRewards(leaves, {
399
+ log,
400
+ noUpload: false
401
+ });
402
+ treeRoot = result.treeRoot;
403
+ treeDump = result.treeDump;
404
+ treeCid = opts.treeCid;
405
+ logCid = result.logCid ?? "";
406
+ } else if (opts.logCid !== void 0) {
407
+ const result = await makeRewards(leaves, { noUpload: false });
408
+ treeRoot = result.treeRoot;
409
+ treeDump = result.treeDump;
410
+ treeCid = result.treeCid ?? "";
411
+ logCid = opts.logCid;
412
+ } else {
413
+ const result = await makeRewards(leaves, { log });
414
+ treeRoot = result.treeRoot;
415
+ treeDump = result.treeDump;
416
+ treeCid = result.treeCid ?? "";
417
+ logCid = result.logCid ?? "";
418
+ }
419
+ return {
420
+ treeRoot,
421
+ treeCid,
422
+ logCid,
423
+ distributed,
424
+ rebate,
425
+ treeDump,
426
+ cumulatives
427
+ };
428
+ }
429
+ /** Deterministic uint in `[0, span)` from `keccak256(toBytes(\`${seed}:${label}\`)) % span`. */
430
+ function seededUint(seed, label, span) {
431
+ return BigInt(keccak256(toBytes(`${seed}:${label}`))) % span;
432
+ }
433
+ /** Normalize the carry-forward input (Map or entries) to an iterable of [noId, shares]. */
434
+ function normalizePrevious(prev) {
435
+ if (!prev) return [];
436
+ return prev instanceof Map ? prev.entries() : prev;
437
+ }
438
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
439
+ /**
440
+ * The `IFeeOracle.ReportData` struct as ONE tuple parameter — components in declaration order
441
+ * (verified against `fixtures/receipts/src/abi/FeeOracle.ts`). `abi.encode(data)` for a single
442
+ * struct == ABI-encoding one tuple parameter (head/tail offsets for the dynamic `string` fields);
443
+ * passing the tuple type as a single param to `encodeAbiParameters` reproduces it. Do NOT flatten
444
+ * into 9 top-level params — that drops the tuple offset and changes the hash. This is the exact
445
+ * reportHash transcription trap.
446
+ */
447
+ const REPORT_DATA_PARAMS = parseAbiParameters("(uint256 consensusVersion, uint256 refSlot, bytes32 treeRoot, string treeCid, string logCid, uint256 distributed, uint256 rebate, bytes32 strikesTreeRoot, string strikesTreeCid)");
448
+ /**
449
+ * Submit a `RewardsReport` (from `makeRewards`) on-chain: fund the FeeDistributor, warp to the next
450
+ * consensus frame, build the `ReportData` tuple, reach consensus across the fast-lane members (with
451
+ * a `getMembers` fallback), and submit the report data. Port of `OracleReport.s.sol:submitRewards`.
452
+ *
453
+ * Fork-only in practice (it WRITES + warps); the hermetic tests assert the call sequence, the funding
454
+ * literals, the warp arguments, and the reportHash, not real frame advancement.
455
+ *
456
+ * A zero-root report (no active keys this frame) is a graceful no-op: returns `{ submitted: false }`
457
+ * with no reads/writes, so `submitRewards(ctx, await makeRewards(ctx))` composes on an empty fork.
458
+ */
459
+ async function submitRewards(ctx, report) {
460
+ if (report.treeRoot === ZERO_ROOT) return { submitted: false };
461
+ const feeDistributor = {
462
+ address: ctx.addresses.FeeDistributor,
463
+ abi: feeDistributorAbi
464
+ };
465
+ const oracle = {
466
+ address: ctx.addresses.FeeOracle,
467
+ abi: feeOracleAbi
468
+ };
469
+ const hashConsensus = {
470
+ address: ctx.addresses.HashConsensus,
471
+ abi: hashConsensusAbi
472
+ };
473
+ const lido = {
474
+ address: ctx.addresses.lido,
475
+ abi: lidoAbi
476
+ };
477
+ const pending = await ctx.client.readContract({
478
+ ...feeDistributor,
479
+ functionName: "pendingSharesToDistribute"
480
+ });
481
+ if (pending < report.distributed + report.rebate) {
482
+ const neededShares = report.distributed + report.rebate - pending;
483
+ const x = await ctx.client.readContract({
484
+ ...lido,
485
+ functionName: "getPooledEthByShares",
486
+ args: [neededShares]
487
+ });
488
+ const submitValue = x + parseEther("1");
489
+ const fd = feeDistributor.address;
490
+ await ctx.client.setBalance({
491
+ address: fd,
492
+ value: x + parseEther("2")
493
+ });
494
+ await ctx.client.impersonateAccount({ address: fd });
495
+ try {
496
+ await ctx.client.writeContract({
497
+ ...lido,
498
+ functionName: "submit",
499
+ args: [ZERO_ADDRESS],
500
+ value: submitValue,
501
+ account: fd,
502
+ chain: null
503
+ });
504
+ } finally {
505
+ await ctx.client.stopImpersonatingAccount({ address: fd });
506
+ }
507
+ }
508
+ const [slotsPerEpoch, secondsPerSlot, genesisTime] = await ctx.client.readContract({
509
+ ...hashConsensus,
510
+ functionName: "getChainConfig"
511
+ });
512
+ const [initialEpoch, epochsPerFrame] = await ctx.client.readContract({
513
+ ...hashConsensus,
514
+ functionName: "getFrameConfig"
515
+ });
516
+ if (((await ctx.client.getBlock()).timestamp - genesisTime) / secondsPerSlot / slotsPerEpoch < initialEpoch) await warpTo(ctx, genesisTime + 1n + initialEpoch * slotsPerEpoch * secondsPerSlot);
517
+ const [frameRefSlot] = await ctx.client.readContract({
518
+ ...hashConsensus,
519
+ functionName: "getCurrentFrame"
520
+ });
521
+ const frameStart = genesisTime + (frameRefSlot + slotsPerEpoch * epochsPerFrame + 1n) * secondsPerSlot;
522
+ if (frameStart > (await ctx.client.getBlock()).timestamp) await warpTo(ctx, frameStart);
523
+ const [refSlot] = await ctx.client.readContract({
524
+ ...hashConsensus,
525
+ functionName: "getCurrentFrame"
526
+ });
527
+ const consensusVersion = await ctx.client.readContract({
528
+ ...oracle,
529
+ functionName: "getConsensusVersion"
530
+ });
531
+ const data = {
532
+ consensusVersion,
533
+ refSlot,
534
+ treeRoot: report.treeRoot,
535
+ treeCid: report.treeCid,
536
+ logCid: report.logCid,
537
+ distributed: report.distributed,
538
+ rebate: report.rebate,
539
+ strikesTreeRoot: strikesTreeRoot(refSlot),
540
+ strikesTreeCid: `mock-strikes-${refSlot}`
541
+ };
542
+ const hash = reportHash(data);
543
+ let [members] = await ctx.client.readContract({
544
+ ...hashConsensus,
545
+ functionName: "getFastLaneMembers"
546
+ });
547
+ if (members.length === 0) [members] = await ctx.client.readContract({
548
+ ...hashConsensus,
549
+ functionName: "getMembers"
550
+ });
551
+ if (members.length === 0) throw new Error("@sm-lab/recipes: no consensus members to reach quorum");
552
+ for (const member of members) await actAs(ctx, member, (from) => ctx.client.writeContract({
553
+ ...hashConsensus,
554
+ functionName: "submitReport",
555
+ args: [
556
+ refSlot,
557
+ hash,
558
+ consensusVersion
559
+ ],
560
+ account: from,
561
+ chain: null
562
+ }));
563
+ const contractVersion = await ctx.client.readContract({
564
+ ...oracle,
565
+ functionName: "getContractVersion"
566
+ });
567
+ await actAs(ctx, members[0], (from) => ctx.client.writeContract({
568
+ ...oracle,
569
+ functionName: "submitReportData",
570
+ args: [data, contractVersion],
571
+ account: from,
572
+ chain: null
573
+ }));
574
+ return {
575
+ submitted: true,
576
+ refSlot,
577
+ treeRoot: report.treeRoot,
578
+ reportHash: hash,
579
+ members
580
+ };
581
+ }
582
+ /** keccak256(abi.encode(data)) — encode the 9-field struct as ONE tuple param (see REPORT_DATA_PARAMS). */
583
+ function reportHash(data) {
584
+ return keccak256(encodeAbiParameters(REPORT_DATA_PARAMS, [data]));
585
+ }
586
+ /**
587
+ * Mock strikes root: `keccak256(abi.encode("mock-strikes", refSlot))` — TWO top-level params
588
+ * (`string`, `uint256`), NOT a tuple. Matches the source's `abi.encode(a, b)`.
589
+ */
590
+ function strikesTreeRoot(refSlot) {
591
+ return keccak256(encodeAbiParameters(parseAbiParameters("string, uint256"), ["mock-strikes", refSlot]));
592
+ }
593
+ //#endregion
594
+ //#region src/cl-mock.ts
595
+ /**
596
+ * POST one validator to a running `@sm-lab/cl` `/admin/validators`. Surfaces the mock's
597
+ * `errors[]` and throws unless the single item was accepted.
598
+ *
599
+ * cl-mock returns 200 (clean), 207 (partial — only when `errors.length > 0` AND `accepted > 0`,
600
+ * which is impossible for a single item), or 400 (all rejected, no `accepted` field). For one
601
+ * item the realistic failure is 400, caught by the `!res.ok` clause. The `(accepted ?? 0) < 1`
602
+ * guard is defensive belt-and-braces for a synthetic accepted:0 / missing-`accepted` body the mock
603
+ * never actually emits — the `?? 0` is load-bearing, since a bare `accepted < 1` is `false` for
604
+ * `undefined` and would let such a body through as a silent success.
605
+ */
606
+ async function setClValidator(clMockUrl, input) {
607
+ const url = `${clMockUrl.replace(/\/+$/, "")}/admin/validators`;
608
+ const body = JSON.stringify({
609
+ pubkey: input.pubkey,
610
+ status: input.status,
611
+ ...input.effectiveBalanceGwei !== void 0 ? { effective_balance: input.effectiveBalanceGwei.toString() } : {}
612
+ });
613
+ const res = await fetch(url, {
614
+ method: "POST",
615
+ headers: { "Content-Type": "application/json" },
616
+ body
617
+ });
618
+ const json = await res.json().catch(() => void 0);
619
+ if (!res.ok || !json || (json.accepted ?? 0) < 1) {
620
+ const errs = json?.errors?.length ? ` — ${json.errors.join("; ")}` : "";
621
+ throw new Error(`@sm-lab/recipes: cl-mock rejected validator (${res.status} ${res.statusText})${errs}`);
622
+ }
623
+ }
624
+ //#endregion
625
+ //#region src/recipes/cl-activate.ts
626
+ const GWEI_PER_ETH = 1000000000n;
627
+ const BASE_ETH_GWEI = 32n * GWEI_PER_ETH;
628
+ /**
629
+ * Read a key's pubkey + allocated balance on-chain, then mark it `active_ongoing` on a running
630
+ * cl-mock with effective balance = 32 ETH + allocated balance, in gwei. Port of
631
+ * `cl-mock.just:cl-activate`. Diverges from the Solidity helper's integer-ETH truncation
632
+ * (`balances[0] / 1 ether`) by keeping full gwei precision (cl-mock stores `effective_balance`
633
+ * in gwei) — the spec-blessed divergence.
634
+ */
635
+ async function clActivate(ctx, opts) {
636
+ if (!ctx.clMockUrl) throw new Error("@sm-lab/recipes: clActivate needs ctx.clMockUrl (a running cl-mock)");
637
+ const pubkey = await getPubkey(ctx, opts);
638
+ const effectiveBalanceGwei = BASE_ETH_GWEI + await getKeyBalance(ctx, opts) / GWEI_PER_ETH;
639
+ await setClValidator(ctx.clMockUrl, {
640
+ pubkey,
641
+ status: "active_ongoing",
642
+ effectiveBalanceGwei
643
+ });
644
+ return {
645
+ pubkey,
646
+ status: "active_ongoing",
647
+ effectiveBalanceGwei
648
+ };
649
+ }
650
+ //#endregion
651
+ export { snapshot as C, operatorInfo as E, revert as S, warpTo as T, nodeOperatorIdBytes as _, setGateAddrs as a, proposeManager as b, cancelPenalty as c, settlePenalty as d, slash as f, keyCountBytes as g, unvet as h, submitRewards as i, compensatePenalty as l, exit as m, setClValidator as n, addBond as o, withdraw as p, makeRewards$1 as r, createBondDebt as s, clActivate as t, reportPenalty as u, confirmManager as v, warpBy as w, proposeReward as x, confirmReward as y };
652
+
653
+ //# sourceMappingURL=cl-activate-CPqr6v_D.mjs.map