@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.
- package/README.md +164 -0
- package/dist/cl-activate-CPqr6v_D.mjs +653 -0
- package/dist/cl-activate-CPqr6v_D.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +1353 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/cm.d.mts +87 -0
- package/dist/cm.d.mts.map +1 -0
- package/dist/cm.mjs +305 -0
- package/dist/cm.mjs.map +1 -0
- package/dist/context-BiYdAnKn.d.mts +8397 -0
- package/dist/context-BiYdAnKn.d.mts.map +1 -0
- package/dist/index.d.mts +393 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -0
- package/dist/topup-B2NzJV7_.mjs +437 -0
- package/dist/topup-B2NzJV7_.mjs.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { concat, createTestClient, http, keccak256, maxUint256, parseEther, publicActions, size, toBytes, toHex, walletActions } from "viem";
|
|
2
|
+
import { accountingAbi, addresses, csModuleAbi, curatedGateAbi, lidoLocatorAbi, vettedGateAbi } from "@sm-lab/receipts";
|
|
3
|
+
import { makeDepositKeys } from "@sm-lab/keys";
|
|
4
|
+
//#region src/client.ts
|
|
5
|
+
/**
|
|
6
|
+
* The single viem client recipes use. An anvil test client (for setBalance /
|
|
7
|
+
* impersonateAccount / increaseTime / snapshot) extended with public actions
|
|
8
|
+
* (readContract / simulateContract / getChainId) and wallet actions (writeContract).
|
|
9
|
+
* No `chain` is set — anvil forks vary by chainId, so writes pass `chain: null`.
|
|
10
|
+
*/
|
|
11
|
+
function makeClient(rpcUrl) {
|
|
12
|
+
return createTestClient({
|
|
13
|
+
mode: "anvil",
|
|
14
|
+
transport: http(rpcUrl)
|
|
15
|
+
}).extend(publicActions).extend(walletActions);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/context.ts
|
|
19
|
+
const CM_SELECTORS = {
|
|
20
|
+
po: 0,
|
|
21
|
+
pto: 1,
|
|
22
|
+
pgo: 2,
|
|
23
|
+
do: 3,
|
|
24
|
+
eeo: 4,
|
|
25
|
+
iodc: 5,
|
|
26
|
+
iodcp: 6
|
|
27
|
+
};
|
|
28
|
+
function defaultSnapshot(chainId, module) {
|
|
29
|
+
for (const chainKey of Object.keys(addresses)) {
|
|
30
|
+
const book = addresses[chainKey][module];
|
|
31
|
+
if (book && book.ChainId === chainId) return book;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`@sm-lab/recipes: no default snapshot for chainId=${chainId}, module=${module} — pass addresses explicitly`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The only place chain / addresses / module resolve. Prefers the baked `protocol` block
|
|
37
|
+
* from the address book (zero on-chain reads) and falls back to reading the five protocol
|
|
38
|
+
* addresses from LidoLocator when the block is absent.
|
|
39
|
+
*/
|
|
40
|
+
async function connect(opts) {
|
|
41
|
+
const client = opts.client ?? makeClient(requireRpcUrl(opts));
|
|
42
|
+
const chainId = await client.getChainId();
|
|
43
|
+
const book = opts.addresses ?? defaultSnapshot(chainId, opts.module);
|
|
44
|
+
const protocol = book.protocol ? {
|
|
45
|
+
stakingRouter: book.protocol.stakingRouter,
|
|
46
|
+
vebo: book.protocol.validatorsExitBusOracle,
|
|
47
|
+
lido: book.protocol.lido,
|
|
48
|
+
withdrawalQueue: book.protocol.withdrawalQueue,
|
|
49
|
+
burner: book.protocol.burner
|
|
50
|
+
} : await resolveProtocolFromLocator(client, book.LidoLocator);
|
|
51
|
+
return {
|
|
52
|
+
client,
|
|
53
|
+
module: opts.module,
|
|
54
|
+
clMockUrl: opts.clMockUrl,
|
|
55
|
+
addresses: {
|
|
56
|
+
...book,
|
|
57
|
+
...protocol
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function resolveProtocolFromLocator(client, locator) {
|
|
62
|
+
const loc = {
|
|
63
|
+
address: locator,
|
|
64
|
+
abi: lidoLocatorAbi
|
|
65
|
+
};
|
|
66
|
+
const [stakingRouter, vebo, lido, withdrawalQueue, burner] = await Promise.all([
|
|
67
|
+
client.readContract({
|
|
68
|
+
...loc,
|
|
69
|
+
functionName: "stakingRouter"
|
|
70
|
+
}),
|
|
71
|
+
client.readContract({
|
|
72
|
+
...loc,
|
|
73
|
+
functionName: "validatorsExitBusOracle"
|
|
74
|
+
}),
|
|
75
|
+
client.readContract({
|
|
76
|
+
...loc,
|
|
77
|
+
functionName: "lido"
|
|
78
|
+
}),
|
|
79
|
+
client.readContract({
|
|
80
|
+
...loc,
|
|
81
|
+
functionName: "withdrawalQueue"
|
|
82
|
+
}),
|
|
83
|
+
client.readContract({
|
|
84
|
+
...loc,
|
|
85
|
+
functionName: "burner"
|
|
86
|
+
})
|
|
87
|
+
]);
|
|
88
|
+
return {
|
|
89
|
+
stakingRouter,
|
|
90
|
+
vebo,
|
|
91
|
+
lido,
|
|
92
|
+
withdrawalQueue,
|
|
93
|
+
burner
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function requireRpcUrl(opts) {
|
|
97
|
+
if (!opts.rpcUrl) throw new Error("@sm-lab/recipes: connect() needs rpcUrl (or an injected client)");
|
|
98
|
+
return opts.rpcUrl;
|
|
99
|
+
}
|
|
100
|
+
const STATIC = {
|
|
101
|
+
Accounting: accountingAbi,
|
|
102
|
+
VettedGate: vettedGateAbi,
|
|
103
|
+
CuratedGate: curatedGateAbi,
|
|
104
|
+
LidoLocator: lidoLocatorAbi
|
|
105
|
+
};
|
|
106
|
+
function contract(ctx, name) {
|
|
107
|
+
if (name === "module") return {
|
|
108
|
+
address: ctx.module === "cm" ? ctx.addresses.CuratedModule : ctx.addresses.CSModule,
|
|
109
|
+
abi: csModuleAbi
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
address: ctx.addresses[name],
|
|
113
|
+
abi: STATIC[name]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a gate selector to an address (the `_resolve-gate-addr` port). Accepted forms:
|
|
118
|
+
* a raw `0x…` 40-hex address (any module); for csm — `ics` → VettedGate, `idvtc` →
|
|
119
|
+
* IdentifiedDVTClusterGate (v3-only; throws on snapshots lacking it, e.g. mainnet/v2);
|
|
120
|
+
* for cm — `po|pto|pgo|do|eeo|iodc|iodcp` or a numeric index → `CuratedGates[0..6]`.
|
|
121
|
+
*/
|
|
122
|
+
function resolveGate(ctx, selector) {
|
|
123
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(selector)) return selector;
|
|
124
|
+
if (ctx.module === "cm") {
|
|
125
|
+
const idx = CM_SELECTORS[selector] ?? (/^\d+$/.test(selector) ? Number(selector) : void 0);
|
|
126
|
+
if (idx === void 0) throw new Error(`@sm-lab/recipes: unknown cm gate selector "${selector}"`);
|
|
127
|
+
const gate = ctx.addresses.CuratedGates[idx];
|
|
128
|
+
if (!gate) throw new Error(`@sm-lab/recipes: cm gate index ${idx} out of range`);
|
|
129
|
+
return gate;
|
|
130
|
+
}
|
|
131
|
+
if (selector === "ics") return ctx.addresses.VettedGate;
|
|
132
|
+
if (selector === "idvtc") {
|
|
133
|
+
const g = ctx.addresses.IdentifiedDVTClusterGate;
|
|
134
|
+
if (!g) throw new Error("@sm-lab/recipes: idvtc gate not in this snapshot (v3-only; absent on mainnet/v2)");
|
|
135
|
+
return g;
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`@sm-lab/recipes: unknown csm gate selector "${selector}"`);
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/act-as.ts
|
|
141
|
+
/**
|
|
142
|
+
* Run `fn` as a privileged account `who` on the fork. Funds it (anvil_setBalance),
|
|
143
|
+
* unlocks it (anvil_impersonateAccount), runs the body, and always stops impersonating.
|
|
144
|
+
* Replaces every Solidity `broadcast*` modifier. `who` is a RAW address — pass a contract
|
|
145
|
+
* address (e.g. the module, verifier, stakingRouter) or a role member resolved via roleMember.
|
|
146
|
+
*/
|
|
147
|
+
async function actAs(ctx, who, fn) {
|
|
148
|
+
await ctx.client.setBalance({
|
|
149
|
+
address: who,
|
|
150
|
+
value: parseEther("100")
|
|
151
|
+
});
|
|
152
|
+
await ctx.client.impersonateAccount({ address: who });
|
|
153
|
+
try {
|
|
154
|
+
return await fn(who);
|
|
155
|
+
} finally {
|
|
156
|
+
await ctx.client.stopImpersonatingAccount({ address: who });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Read `getRoleMember(role, 0)` — the canonical admin/governance member of an AccessControl role. */
|
|
160
|
+
async function roleMember(ctx, target, role) {
|
|
161
|
+
return await ctx.client.readContract({
|
|
162
|
+
address: target.address,
|
|
163
|
+
abi: target.abi,
|
|
164
|
+
functionName: "getRoleMember",
|
|
165
|
+
args: [role, 0n]
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/random.ts
|
|
170
|
+
/**
|
|
171
|
+
* A 32-byte cryptographically-random seed, hex-encoded. The shared origin for every recipe that
|
|
172
|
+
* needs fresh-but-reproducible-on-demand randomness (key material, reward draws, operator
|
|
173
|
+
* addresses) — pass a fixed seed for deterministic output, call this to mint a fresh one.
|
|
174
|
+
*/
|
|
175
|
+
function randomSeed() {
|
|
176
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
177
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
178
|
+
return toHex(bytes);
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/keys.ts
|
|
182
|
+
/**
|
|
183
|
+
* Fixed BIP-39 mnemonic for deterministic test/recipe key derivation. NOT a production
|
|
184
|
+
* key — exists only for hermetic reproducibility. The seed controls the startIndex,
|
|
185
|
+
* which determines the EIP-2334 derivation path slice used.
|
|
186
|
+
*/
|
|
187
|
+
const RECIPE_MNEMONIC = "impact exit example acquire drastic cement usage float mesh source private bulb twenty guitar neglect";
|
|
188
|
+
/**
|
|
189
|
+
* Deterministic real BLS validator keys derived from a fixed mnemonic at a seed-derived
|
|
190
|
+
* startIndex. Produces genuine 48-byte G1 pubkeys + 96-byte G2 BLS signatures — NOT
|
|
191
|
+
* keccak-expanded pseudo-keys. Pass a fixed `seed` for reproducible tests.
|
|
192
|
+
*
|
|
193
|
+
* The seed maps to a `startIndex` via `keccak256(seed) % 2^20` so different seeds yield
|
|
194
|
+
* non-overlapping key ranges (1M range >> any realistic `count`).
|
|
195
|
+
*/
|
|
196
|
+
async function randomKeys(count, seed) {
|
|
197
|
+
const root = seed ?? randomSeed();
|
|
198
|
+
const { keys } = await makeDepositKeys({
|
|
199
|
+
mnemonic: RECIPE_MNEMONIC,
|
|
200
|
+
count,
|
|
201
|
+
startIndex: Number(BigInt(keccak256(toBytes(root))) % 2n ** 20n),
|
|
202
|
+
chain: "hoodi"
|
|
203
|
+
});
|
|
204
|
+
const publicKeys = keys.map((k) => k.pubkey);
|
|
205
|
+
const signatures = keys.map((k) => k.signature);
|
|
206
|
+
return {
|
|
207
|
+
publicKeys,
|
|
208
|
+
signatures,
|
|
209
|
+
packedKeys: count === 0 ? "0x" : concat(publicKeys),
|
|
210
|
+
packedSignatures: count === 0 ? "0x" : concat(signatures)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/recipes/add-keys.ts
|
|
215
|
+
/**
|
|
216
|
+
* Add `count` fresh validator keys to operator `noId`, paying the required bond, as the
|
|
217
|
+
* operator's manager. Returns the generated pubkeys. (Port of `NodeOperators.addKeys`.)
|
|
218
|
+
*/
|
|
219
|
+
async function addKeys(ctx, opts) {
|
|
220
|
+
const m = contract(ctx, "module");
|
|
221
|
+
const acc = contract(ctx, "Accounting");
|
|
222
|
+
const manager = (await ctx.client.readContract({
|
|
223
|
+
...m,
|
|
224
|
+
functionName: "getNodeOperator",
|
|
225
|
+
args: [opts.noId]
|
|
226
|
+
})).managerAddress;
|
|
227
|
+
const value = await ctx.client.readContract({
|
|
228
|
+
...acc,
|
|
229
|
+
functionName: "getRequiredBondForNextKeys",
|
|
230
|
+
args: [opts.noId, BigInt(opts.count)]
|
|
231
|
+
});
|
|
232
|
+
const { publicKeys, packedKeys, packedSignatures } = await randomKeys(opts.count, opts.seed);
|
|
233
|
+
await actAs(ctx, manager, (from) => ctx.client.writeContract({
|
|
234
|
+
...m,
|
|
235
|
+
functionName: "addValidatorKeysETH",
|
|
236
|
+
args: [
|
|
237
|
+
manager,
|
|
238
|
+
opts.noId,
|
|
239
|
+
BigInt(opts.count),
|
|
240
|
+
packedKeys,
|
|
241
|
+
packedSignatures
|
|
242
|
+
],
|
|
243
|
+
account: from,
|
|
244
|
+
value,
|
|
245
|
+
chain: null
|
|
246
|
+
}));
|
|
247
|
+
return { publicKeys };
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/roles.ts
|
|
251
|
+
/** OZ AccessControl default admin role (all-zero bytes32). */
|
|
252
|
+
const DEFAULT_ADMIN_ROLE = `0x${"0".repeat(64)}`;
|
|
253
|
+
/** MerkleGate: keccak256("SET_TREE_ROLE"). */
|
|
254
|
+
const SET_TREE_ROLE = keccak256(toBytes("SET_TREE_ROLE"));
|
|
255
|
+
/** PausableWithRoles: keccak256("RESUME_ROLE"). */
|
|
256
|
+
const RESUME_ROLE = keccak256(toBytes("RESUME_ROLE"));
|
|
257
|
+
/** BaseModule: keccak256("REPORT_GENERAL_DELAYED_PENALTY_ROLE"). */
|
|
258
|
+
const REPORT_GENERAL_DELAYED_PENALTY_ROLE = keccak256(toBytes("REPORT_GENERAL_DELAYED_PENALTY_ROLE"));
|
|
259
|
+
/** BaseModule: keccak256("SETTLE_GENERAL_DELAYED_PENALTY_ROLE"). */
|
|
260
|
+
const SETTLE_GENERAL_DELAYED_PENALTY_ROLE = keccak256(toBytes("SETTLE_GENERAL_DELAYED_PENALTY_ROLE"));
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/recipes/deposit.ts
|
|
263
|
+
/**
|
|
264
|
+
* Deposit up to `count` of an operator's depositable keys (StakingRouter-gated). Flushes pending
|
|
265
|
+
* deposit-info, caps to the module's depositable count, and returns the number actually deposited
|
|
266
|
+
* (derived from the keys `obtainDepositData` hands back). Throws if a positive request finds nothing
|
|
267
|
+
* depositable — the Solidity helper silently no-ops there. (Port of `NodeOperators.deposit`.)
|
|
268
|
+
*/
|
|
269
|
+
async function deposit(ctx, opts) {
|
|
270
|
+
const m = contract(ctx, "module");
|
|
271
|
+
const requested = BigInt(opts.count);
|
|
272
|
+
return actAs(ctx, ctx.addresses.stakingRouter, async (from) => {
|
|
273
|
+
await ctx.client.writeContract({
|
|
274
|
+
...m,
|
|
275
|
+
functionName: "batchDepositInfoUpdate",
|
|
276
|
+
args: [maxUint256],
|
|
277
|
+
account: from,
|
|
278
|
+
chain: null
|
|
279
|
+
});
|
|
280
|
+
const [, , depositable] = await ctx.client.readContract({
|
|
281
|
+
...m,
|
|
282
|
+
functionName: "getStakingModuleSummary"
|
|
283
|
+
});
|
|
284
|
+
const capped = requested < depositable ? requested : depositable;
|
|
285
|
+
if (requested > 0n && capped === 0n) throw new Error("@sm-lab/recipes: deposit found nothing depositable for this module");
|
|
286
|
+
const { result, request } = await ctx.client.simulateContract({
|
|
287
|
+
...m,
|
|
288
|
+
functionName: "obtainDepositData",
|
|
289
|
+
args: [capped, "0x"],
|
|
290
|
+
account: from
|
|
291
|
+
});
|
|
292
|
+
await ctx.client.writeContract({
|
|
293
|
+
...request,
|
|
294
|
+
chain: null
|
|
295
|
+
});
|
|
296
|
+
const [pubkeys] = result;
|
|
297
|
+
return { deposited: BigInt(size(pubkeys) / 48) };
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/recipes/reads.ts
|
|
302
|
+
/** One key's 48-byte pubkey from on-chain storage. Throws if no key exists at `keyIndex`. */
|
|
303
|
+
async function getPubkey(ctx, opts) {
|
|
304
|
+
const m = contract(ctx, "module");
|
|
305
|
+
const keys = await ctx.client.readContract({
|
|
306
|
+
...m,
|
|
307
|
+
functionName: "getSigningKeys",
|
|
308
|
+
args: [
|
|
309
|
+
opts.noId,
|
|
310
|
+
opts.keyIndex,
|
|
311
|
+
1n
|
|
312
|
+
]
|
|
313
|
+
});
|
|
314
|
+
if (!keys || size(keys) !== 48) throw new Error(`@sm-lab/recipes: no key found for operator ${opts.noId} at index ${opts.keyIndex}`);
|
|
315
|
+
return keys;
|
|
316
|
+
}
|
|
317
|
+
/** Allocated balance (wei) for one key. */
|
|
318
|
+
async function getKeyBalance(ctx, opts) {
|
|
319
|
+
const m = contract(ctx, "module");
|
|
320
|
+
const wei = (await ctx.client.readContract({
|
|
321
|
+
...m,
|
|
322
|
+
functionName: "getKeyAllocatedBalances",
|
|
323
|
+
args: [
|
|
324
|
+
opts.noId,
|
|
325
|
+
opts.keyIndex,
|
|
326
|
+
1n
|
|
327
|
+
]
|
|
328
|
+
}))[0];
|
|
329
|
+
if (wei === void 0) throw new Error(`@sm-lab/recipes: no allocated balance for operator ${opts.noId} at index ${opts.keyIndex}`);
|
|
330
|
+
return wei;
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/recipes/topup.ts
|
|
334
|
+
/** Per-key top-up cap (2016 ether). Matches `NodeOperators.MAX_TOPUP_PER_KEY`. */
|
|
335
|
+
const MAX_TOPUP_PER_KEY = 2016n * 10n ** 18n;
|
|
336
|
+
/**
|
|
337
|
+
* Top up the allocated balance of a single deposited key (StakingRouter-gated). Validates the key
|
|
338
|
+
* exists and is not withdrawn, then writes a one-element `allocateDeposits` for it. Port of
|
|
339
|
+
* `NodeOperators.increaseAllocatedBalance` — returns the `amountWei` it allocated.
|
|
340
|
+
*/
|
|
341
|
+
async function increaseAllocatedBalance(ctx, opts) {
|
|
342
|
+
const m = contract(ctx, "module");
|
|
343
|
+
const { noId, keyIndex, amountWei } = opts;
|
|
344
|
+
const total = (await ctx.client.readContract({
|
|
345
|
+
...m,
|
|
346
|
+
functionName: "getNodeOperator",
|
|
347
|
+
args: [noId]
|
|
348
|
+
})).totalDepositedKeys;
|
|
349
|
+
if (keyIndex >= BigInt(total)) throw new Error(`@sm-lab/recipes: key index ${keyIndex} out of bounds (operator ${noId} has ${total} deposited keys)`);
|
|
350
|
+
if (await ctx.client.readContract({
|
|
351
|
+
...m,
|
|
352
|
+
functionName: "isValidatorWithdrawn",
|
|
353
|
+
args: [noId, keyIndex]
|
|
354
|
+
})) throw new Error(`@sm-lab/recipes: key ${keyIndex} of operator ${noId} is withdrawn`);
|
|
355
|
+
const key = await getPubkey(ctx, {
|
|
356
|
+
noId,
|
|
357
|
+
keyIndex
|
|
358
|
+
});
|
|
359
|
+
await actAs(ctx, ctx.addresses.stakingRouter, (from) => ctx.client.writeContract({
|
|
360
|
+
...m,
|
|
361
|
+
functionName: "allocateDeposits",
|
|
362
|
+
args: [
|
|
363
|
+
amountWei,
|
|
364
|
+
[key],
|
|
365
|
+
[keyIndex],
|
|
366
|
+
[noId],
|
|
367
|
+
[amountWei]
|
|
368
|
+
],
|
|
369
|
+
account: from,
|
|
370
|
+
chain: null
|
|
371
|
+
}));
|
|
372
|
+
return { amountWei };
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Top up every not-yet-allocated, not-withdrawn deposited key of an operator, one at a time in
|
|
376
|
+
* strict ascending key-index order — `TopUpQueueOps` enforces a global FIFO queue head, so the
|
|
377
|
+
* writes must be sequential and ordered (capped at `MAX_TOPUP_PER_KEY` per key). Port of
|
|
378
|
+
* `NodeOperators.topUpActiveKeys` — returns the count it topped up.
|
|
379
|
+
*/
|
|
380
|
+
async function topUpActiveKeys(ctx, opts) {
|
|
381
|
+
const m = contract(ctx, "module");
|
|
382
|
+
const { noId } = opts;
|
|
383
|
+
const total = (await ctx.client.readContract({
|
|
384
|
+
...m,
|
|
385
|
+
functionName: "getNodeOperator",
|
|
386
|
+
args: [noId]
|
|
387
|
+
})).totalDepositedKeys;
|
|
388
|
+
if (total === 0) throw new Error(`@sm-lab/recipes: operator ${noId} has no deposited keys`);
|
|
389
|
+
const allocated = await ctx.client.readContract({
|
|
390
|
+
...m,
|
|
391
|
+
functionName: "getKeyAllocatedBalances",
|
|
392
|
+
args: [
|
|
393
|
+
noId,
|
|
394
|
+
0n,
|
|
395
|
+
BigInt(total)
|
|
396
|
+
]
|
|
397
|
+
});
|
|
398
|
+
if (allocated.length !== total) throw new Error(`@sm-lab/recipes: getKeyAllocatedBalances returned ${allocated.length} entries (expected ${total})`);
|
|
399
|
+
const workList = (await Promise.all(Array.from({ length: total }, (_, i) => i).map(async (i) => ({
|
|
400
|
+
i,
|
|
401
|
+
pubkey: await ctx.client.readContract({
|
|
402
|
+
...m,
|
|
403
|
+
functionName: "getSigningKeys",
|
|
404
|
+
args: [
|
|
405
|
+
noId,
|
|
406
|
+
BigInt(i),
|
|
407
|
+
1n
|
|
408
|
+
]
|
|
409
|
+
}),
|
|
410
|
+
withdrawn: await ctx.client.readContract({
|
|
411
|
+
...m,
|
|
412
|
+
functionName: "isValidatorWithdrawn",
|
|
413
|
+
args: [noId, BigInt(i)]
|
|
414
|
+
})
|
|
415
|
+
})))).filter(({ i, withdrawn }) => allocated[i] === 0n && !withdrawn);
|
|
416
|
+
if (workList.length === 0) return { toppedUp: 0 };
|
|
417
|
+
await actAs(ctx, ctx.addresses.stakingRouter, async (from) => {
|
|
418
|
+
for (const { i, pubkey } of workList) await ctx.client.writeContract({
|
|
419
|
+
...m,
|
|
420
|
+
functionName: "allocateDeposits",
|
|
421
|
+
args: [
|
|
422
|
+
MAX_TOPUP_PER_KEY,
|
|
423
|
+
[pubkey],
|
|
424
|
+
[BigInt(i)],
|
|
425
|
+
[noId],
|
|
426
|
+
[MAX_TOPUP_PER_KEY]
|
|
427
|
+
],
|
|
428
|
+
account: from,
|
|
429
|
+
chain: null
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
return { toppedUp: workList.length };
|
|
433
|
+
}
|
|
434
|
+
//#endregion
|
|
435
|
+
export { contract as _, deposit as a, RESUME_ROLE as c, addKeys as d, randomKeys as f, connect as g, roleMember as h, getPubkey as i, SETTLE_GENERAL_DELAYED_PENALTY_ROLE as l, actAs as m, topUpActiveKeys as n, DEFAULT_ADMIN_ROLE as o, randomSeed as p, getKeyBalance as r, REPORT_GENERAL_DELAYED_PENALTY_ROLE as s, increaseAllocatedBalance as t, SET_TREE_ROLE as u, resolveGate as v, makeClient as y };
|
|
436
|
+
|
|
437
|
+
//# sourceMappingURL=topup-B2NzJV7_.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topup-B2NzJV7_.mjs","names":["DEFAULTS"],"sources":["../src/client.ts","../src/context.ts","../src/act-as.ts","../src/random.ts","../src/keys.ts","../src/recipes/add-keys.ts","../src/roles.ts","../src/recipes/deposit.ts","../src/recipes/reads.ts","../src/recipes/topup.ts"],"sourcesContent":["import { createTestClient, http, publicActions, walletActions } from 'viem';\n\n/**\n * The single viem client recipes use. An anvil test client (for setBalance /\n * impersonateAccount / increaseTime / snapshot) extended with public actions\n * (readContract / simulateContract / getChainId) and wallet actions (writeContract).\n * No `chain` is set — anvil forks vary by chainId, so writes pass `chain: null`.\n */\nexport function makeClient(rpcUrl: string) {\n return createTestClient({ mode: 'anvil', transport: http(rpcUrl) })\n .extend(publicActions)\n .extend(walletActions);\n}\n\n/** Structural type recipes depend on — satisfied by makeClient's return or a test fake. */\nexport type RecipeClient = ReturnType<typeof makeClient>;\n","import {\n accountingAbi,\n addresses as DEFAULTS,\n csModuleAbi,\n curatedGateAbi,\n lidoLocatorAbi,\n vettedGateAbi,\n} from '@sm-lab/receipts';\nimport type {\n AddressBook,\n ChainName,\n CmAddressBook,\n CsmAddressBook,\n Hex,\n ModuleName,\n} from '@sm-lab/receipts';\nimport { makeClient, type RecipeClient } from './client';\n\n/** Module-suite snapshot + the protocol addresses resolved on-chain by connect(). */\nexport type ResolvedAddresses = AddressBook & {\n stakingRouter: Hex;\n vebo: Hex;\n lido: Hex;\n withdrawalQueue: Hex;\n burner: Hex;\n};\n\nexport interface Ctx {\n client: RecipeClient;\n module: ModuleName;\n addresses: ResolvedAddresses;\n clMockUrl?: string;\n}\n\nexport interface ConnectOptions {\n module: ModuleName;\n /** Required unless `client` is injected. */\n rpcUrl?: string;\n /** Inject a prebuilt client (tests, or a shared client). */\n client?: RecipeClient;\n /** Override the module-suite snapshot; defaults to @sm-lab/receipts by chainId. */\n addresses?: AddressBook;\n clMockUrl?: string;\n}\n\nexport type CsmGateSelector = 'ics' | 'idvtc';\nexport type CmGateSelector = 'po' | 'pto' | 'pgo' | 'do' | 'eeo' | 'iodc' | 'iodcp';\nexport type GateSelector = CsmGateSelector | CmGateSelector;\n\nconst CM_SELECTORS: Record<string, number> = {\n po: 0,\n pto: 1,\n pgo: 2,\n do: 3,\n eeo: 4,\n iodc: 5,\n iodcp: 6,\n};\n\nfunction defaultSnapshot(chainId: number, module: ModuleName): AddressBook {\n for (const chainKey of Object.keys(DEFAULTS) as ChainName[]) {\n const byModule = DEFAULTS[chainKey] as Partial<Record<ModuleName, AddressBook>>;\n const book = byModule[module];\n if (book && (book as { ChainId: number }).ChainId === chainId) return book;\n }\n throw new Error(\n `@sm-lab/recipes: no default snapshot for chainId=${chainId}, module=${module} — pass addresses explicitly`,\n );\n}\n\n/**\n * The only place chain / addresses / module resolve. Prefers the baked `protocol` block\n * from the address book (zero on-chain reads) and falls back to reading the five protocol\n * addresses from LidoLocator when the block is absent.\n */\nexport async function connect(opts: ConnectOptions): Promise<Ctx> {\n const client = opts.client ?? makeClient(requireRpcUrl(opts));\n const chainId = await client.getChainId();\n const book = opts.addresses ?? defaultSnapshot(chainId, opts.module);\n\n const protocol = book.protocol\n ? {\n stakingRouter: book.protocol.stakingRouter,\n vebo: book.protocol.validatorsExitBusOracle,\n lido: book.protocol.lido,\n withdrawalQueue: book.protocol.withdrawalQueue,\n burner: book.protocol.burner,\n }\n : await resolveProtocolFromLocator(client, book.LidoLocator as Hex);\n\n return {\n client,\n module: opts.module,\n clMockUrl: opts.clMockUrl,\n addresses: { ...book, ...protocol } as ResolvedAddresses,\n };\n}\n\nasync function resolveProtocolFromLocator(\n client: RecipeClient,\n locator: Hex,\n): Promise<{ stakingRouter: Hex; vebo: Hex; lido: Hex; withdrawalQueue: Hex; burner: Hex }> {\n const loc = { address: locator, abi: lidoLocatorAbi } as const;\n const [stakingRouter, vebo, lido, withdrawalQueue, burner] = await Promise.all([\n client.readContract({ ...loc, functionName: 'stakingRouter' }),\n client.readContract({ ...loc, functionName: 'validatorsExitBusOracle' }),\n client.readContract({ ...loc, functionName: 'lido' }),\n client.readContract({ ...loc, functionName: 'withdrawalQueue' }),\n client.readContract({ ...loc, functionName: 'burner' }),\n ]);\n return { stakingRouter, vebo, lido, withdrawalQueue, burner };\n}\n\nfunction requireRpcUrl(opts: ConnectOptions): string {\n if (!opts.rpcUrl)\n throw new Error('@sm-lab/recipes: connect() needs rpcUrl (or an injected client)');\n return opts.rpcUrl;\n}\n\nconst STATIC = {\n Accounting: accountingAbi,\n VettedGate: vettedGateAbi,\n CuratedGate: curatedGateAbi,\n LidoLocator: lidoLocatorAbi,\n} as const;\ntype StaticName = keyof typeof STATIC;\n\nexport function contract(ctx: Ctx, name: 'module'): { address: Hex; abi: typeof csModuleAbi };\nexport function contract<N extends StaticName>(\n ctx: Ctx,\n name: N,\n): { address: Hex; abi: (typeof STATIC)[N] };\nexport function contract(ctx: Ctx, name: StaticName | 'module') {\n if (name === 'module') {\n // csModuleAbi anchors the shared IBaseModule surface (getNodeOperator,\n // addValidatorKeysETH, …) for BOTH modules — those fragments are byte-identical\n // across CSModule/CuratedModule, so selectors and decoding match. Only the ADDRESS\n // switches by ctx.module.\n const address = (\n ctx.module === 'cm'\n ? (ctx.addresses as CmAddressBook).CuratedModule\n : (ctx.addresses as CsmAddressBook).CSModule\n ) as Hex;\n return { address, abi: csModuleAbi };\n }\n return { address: (ctx.addresses as unknown as Record<string, Hex>)[name], abi: STATIC[name] };\n}\n\n/**\n * Resolve a gate selector to an address (the `_resolve-gate-addr` port). Accepted forms:\n * a raw `0x…` 40-hex address (any module); for csm — `ics` → VettedGate, `idvtc` →\n * IdentifiedDVTClusterGate (v3-only; throws on snapshots lacking it, e.g. mainnet/v2);\n * for cm — `po|pto|pgo|do|eeo|iodc|iodcp` or a numeric index → `CuratedGates[0..6]`.\n */\nexport function resolveGate(ctx: Ctx, selector: string): Hex {\n if (/^0x[0-9a-fA-F]{40}$/.test(selector)) return selector as Hex;\n if (ctx.module === 'cm') {\n const idx = CM_SELECTORS[selector] ?? (/^\\d+$/.test(selector) ? Number(selector) : undefined);\n if (idx === undefined)\n throw new Error(`@sm-lab/recipes: unknown cm gate selector \"${selector}\"`);\n const gate = (ctx.addresses as CmAddressBook).CuratedGates[idx];\n if (!gate) throw new Error(`@sm-lab/recipes: cm gate index ${idx} out of range`);\n return gate;\n }\n if (selector === 'ics') return (ctx.addresses as CsmAddressBook).VettedGate;\n if (selector === 'idvtc') {\n const g = (ctx.addresses as CsmAddressBook).IdentifiedDVTClusterGate;\n if (!g)\n throw new Error(\n '@sm-lab/recipes: idvtc gate not in this snapshot (v3-only; absent on mainnet/v2)',\n );\n return g;\n }\n throw new Error(`@sm-lab/recipes: unknown csm gate selector \"${selector}\"`);\n}\n","import { parseEther } from 'viem';\nimport type { Abi } from 'viem';\nimport type { Hex } from '@sm-lab/receipts';\nimport type { Ctx } from './context';\n\n/**\n * Run `fn` as a privileged account `who` on the fork. Funds it (anvil_setBalance),\n * unlocks it (anvil_impersonateAccount), runs the body, and always stops impersonating.\n * Replaces every Solidity `broadcast*` modifier. `who` is a RAW address — pass a contract\n * address (e.g. the module, verifier, stakingRouter) or a role member resolved via roleMember.\n */\nexport async function actAs<T>(ctx: Ctx, who: Hex, fn: (from: Hex) => Promise<T>): Promise<T> {\n await ctx.client.setBalance({ address: who, value: parseEther('100') });\n await ctx.client.impersonateAccount({ address: who });\n try {\n return await fn(who);\n } finally {\n await ctx.client.stopImpersonatingAccount({ address: who });\n }\n}\n\n/** Read `getRoleMember(role, 0)` — the canonical admin/governance member of an AccessControl role. */\nexport async function roleMember(\n ctx: Ctx,\n target: { address: Hex; abi: Abi },\n role: Hex,\n): Promise<Hex> {\n const member = await ctx.client.readContract({\n address: target.address,\n abi: target.abi,\n functionName: 'getRoleMember',\n args: [role, 0n],\n });\n return member as Hex;\n}\n","import { toHex } from 'viem';\nimport type { Hex } from '@sm-lab/receipts';\n\n/**\n * A 32-byte cryptographically-random seed, hex-encoded. The shared origin for every recipe that\n * needs fresh-but-reproducible-on-demand randomness (key material, reward draws, operator\n * addresses) — pass a fixed seed for deterministic output, call this to mint a fresh one.\n */\nexport function randomSeed(): Hex {\n const bytes = new Uint8Array(32);\n globalThis.crypto.getRandomValues(bytes);\n return toHex(bytes);\n}\n","import { makeDepositKeys } from '@sm-lab/keys';\nimport { concat, keccak256, toBytes } from 'viem';\nimport type { Hex } from '@sm-lab/receipts';\nimport { randomSeed } from './random';\n\n/**\n * Fixed BIP-39 mnemonic for deterministic test/recipe key derivation. NOT a production\n * key — exists only for hermetic reproducibility. The seed controls the startIndex,\n * which determines the EIP-2334 derivation path slice used.\n */\nconst RECIPE_MNEMONIC =\n 'impact exit example acquire drastic cement usage float mesh source private bulb twenty guitar neglect';\n\n/**\n * Deterministic real BLS validator keys derived from a fixed mnemonic at a seed-derived\n * startIndex. Produces genuine 48-byte G1 pubkeys + 96-byte G2 BLS signatures — NOT\n * keccak-expanded pseudo-keys. Pass a fixed `seed` for reproducible tests.\n *\n * The seed maps to a `startIndex` via `keccak256(seed) % 2^20` so different seeds yield\n * non-overlapping key ranges (1M range >> any realistic `count`).\n */\nexport async function randomKeys(\n count: number,\n seed?: Hex,\n): Promise<{ publicKeys: Hex[]; signatures: Hex[]; packedKeys: Hex; packedSignatures: Hex }> {\n const root = seed ?? randomSeed();\n const startIndex = Number(BigInt(keccak256(toBytes(root))) % 2n ** 20n);\n\n const { keys } = await makeDepositKeys({\n mnemonic: RECIPE_MNEMONIC,\n count,\n startIndex,\n // NOTE: withdrawal_credentials + fork-version DO sign into the BLS signature (the sig covers\n // a DepositMessage hash under a domain derived from the fork version). But this path never\n // verifies it: CSModule.addValidatorKeysETH → SigningKeys.saveKeysSigs only checks length +\n // non-empty (no BLS verify), and the deposit recipe uses obtainDepositData (StakingRouter-\n // impersonated), not the beacon DepositContract. So the chosen chain is arbitrary-but-valid here.\n chain: 'hoodi',\n });\n\n const publicKeys = keys.map((k) => k.pubkey as Hex);\n const signatures = keys.map((k) => k.signature as Hex);\n\n return {\n publicKeys,\n signatures,\n packedKeys: count === 0 ? '0x' : (concat(publicKeys) as Hex),\n packedSignatures: count === 0 ? '0x' : (concat(signatures) as Hex),\n };\n}\n","import type { Hex } from '@sm-lab/receipts';\nimport { actAs } from '../act-as';\nimport { contract, type Ctx } from '../context';\nimport { randomKeys } from '../keys';\n\nexport interface AddKeysOptions {\n noId: bigint;\n count: number;\n /** Injectable seed for reproducible keys. */\n seed?: Hex;\n}\n\nexport interface AddKeysResult {\n publicKeys: Hex[];\n}\n\n/**\n * Add `count` fresh validator keys to operator `noId`, paying the required bond, as the\n * operator's manager. Returns the generated pubkeys. (Port of `NodeOperators.addKeys`.)\n */\nexport async function addKeys(ctx: Ctx, opts: AddKeysOptions): Promise<AddKeysResult> {\n const m = contract(ctx, 'module');\n const acc = contract(ctx, 'Accounting');\n\n const op = await ctx.client.readContract({\n ...m,\n functionName: 'getNodeOperator',\n args: [opts.noId],\n });\n const manager = (op as { managerAddress: Hex }).managerAddress;\n\n const value = await ctx.client.readContract({\n ...acc,\n functionName: 'getRequiredBondForNextKeys',\n args: [opts.noId, BigInt(opts.count)],\n });\n\n const { publicKeys, packedKeys, packedSignatures } = await randomKeys(opts.count, opts.seed);\n\n await actAs(ctx, manager, (from) =>\n ctx.client.writeContract({\n ...m,\n functionName: 'addValidatorKeysETH',\n args: [manager, opts.noId, BigInt(opts.count), packedKeys, packedSignatures],\n account: from,\n value: value as bigint,\n chain: null,\n }),\n );\n\n return { publicKeys };\n}\n","import { keccak256, toBytes } from 'viem';\nimport type { Hex } from '@sm-lab/receipts';\n\n/** OZ AccessControl default admin role (all-zero bytes32). */\nexport const DEFAULT_ADMIN_ROLE = `0x${'0'.repeat(64)}` as Hex;\n/** MerkleGate: keccak256(\"SET_TREE_ROLE\"). */\nexport const SET_TREE_ROLE = keccak256(toBytes('SET_TREE_ROLE'));\n/** PausableWithRoles: keccak256(\"RESUME_ROLE\"). */\nexport const RESUME_ROLE = keccak256(toBytes('RESUME_ROLE'));\n/** BaseModule: keccak256(\"REPORT_GENERAL_DELAYED_PENALTY_ROLE\"). */\nexport const REPORT_GENERAL_DELAYED_PENALTY_ROLE = keccak256(\n toBytes('REPORT_GENERAL_DELAYED_PENALTY_ROLE'),\n);\n/** BaseModule: keccak256(\"SETTLE_GENERAL_DELAYED_PENALTY_ROLE\"). */\nexport const SETTLE_GENERAL_DELAYED_PENALTY_ROLE = keccak256(\n toBytes('SETTLE_GENERAL_DELAYED_PENALTY_ROLE'),\n);\n","import { maxUint256, size } from 'viem';\nimport { actAs } from '../act-as';\nimport { contract, type Ctx } from '../context';\n\n/**\n * Deposit up to `count` of an operator's depositable keys (StakingRouter-gated). Flushes pending\n * deposit-info, caps to the module's depositable count, and returns the number actually deposited\n * (derived from the keys `obtainDepositData` hands back). Throws if a positive request finds nothing\n * depositable — the Solidity helper silently no-ops there. (Port of `NodeOperators.deposit`.)\n */\nexport async function deposit(\n ctx: Ctx,\n opts: { count: number | bigint },\n): Promise<{ deposited: bigint }> {\n const m = contract(ctx, 'module');\n const requested = BigInt(opts.count);\n\n return actAs(ctx, ctx.addresses.stakingRouter, async (from) => {\n await ctx.client.writeContract({\n ...m,\n functionName: 'batchDepositInfoUpdate',\n args: [maxUint256],\n account: from,\n chain: null,\n });\n\n const summary = await ctx.client.readContract({\n ...m,\n functionName: 'getStakingModuleSummary',\n });\n const [, , depositable] = summary;\n const capped = requested < depositable ? requested : depositable;\n if (requested > 0n && capped === 0n) {\n throw new Error('@sm-lab/recipes: deposit found nothing depositable for this module');\n }\n\n const { result, request } = await ctx.client.simulateContract({\n ...m,\n functionName: 'obtainDepositData',\n args: [capped, '0x'],\n account: from,\n });\n await ctx.client.writeContract({ ...request, chain: null });\n\n const [pubkeys] = result;\n return { deposited: BigInt(size(pubkeys) / 48) };\n });\n}\n","import { size } from 'viem';\nimport type { Hex } from '@sm-lab/receipts';\nimport { contract, type Ctx } from '../context';\n\n/** One key's 48-byte pubkey from on-chain storage. Throws if no key exists at `keyIndex`. */\nexport async function getPubkey(ctx: Ctx, opts: { noId: bigint; keyIndex: bigint }): Promise<Hex> {\n const m = contract(ctx, 'module');\n const keys = (await ctx.client.readContract({\n ...m,\n functionName: 'getSigningKeys',\n args: [opts.noId, opts.keyIndex, 1n],\n })) as Hex;\n // count=1 → a single packed 48-byte pubkey; no per-48 slice needed. Guard `undefined`\n // (unscripted fake reads) before `size()` so it throws the clean error, not a viem internal.\n if (!keys || size(keys) !== 48) {\n throw new Error(\n `@sm-lab/recipes: no key found for operator ${opts.noId} at index ${opts.keyIndex}`,\n );\n }\n return keys;\n}\n\n/** Allocated balance (wei) for one key. */\nexport async function getKeyBalance(\n ctx: Ctx,\n opts: { noId: bigint; keyIndex: bigint },\n): Promise<bigint> {\n const m = contract(ctx, 'module');\n const balances = (await ctx.client.readContract({\n ...m,\n functionName: 'getKeyAllocatedBalances',\n args: [opts.noId, opts.keyIndex, 1n],\n })) as readonly bigint[];\n const wei = balances[0]; // noUncheckedIndexedAccess: guard\n if (wei === undefined) {\n throw new Error(\n `@sm-lab/recipes: no allocated balance for operator ${opts.noId} at index ${opts.keyIndex}`,\n );\n }\n return wei;\n}\n","import type { Hex } from '@sm-lab/receipts';\nimport { actAs } from '../act-as';\nimport { contract, type Ctx } from '../context';\nimport { getPubkey } from './reads';\n\n/** Per-key top-up cap (2016 ether). Matches `NodeOperators.MAX_TOPUP_PER_KEY`. */\nconst MAX_TOPUP_PER_KEY = 2016n * 10n ** 18n;\n\n/**\n * Top up the allocated balance of a single deposited key (StakingRouter-gated). Validates the key\n * exists and is not withdrawn, then writes a one-element `allocateDeposits` for it. Port of\n * `NodeOperators.increaseAllocatedBalance` — returns the `amountWei` it allocated.\n */\nexport async function increaseAllocatedBalance(\n ctx: Ctx,\n opts: { noId: bigint; keyIndex: bigint; amountWei: bigint },\n): Promise<{ amountWei: bigint }> {\n const m = contract(ctx, 'module');\n const { noId, keyIndex, amountWei } = opts;\n\n // Validate in the source's order — bounds, then withdrawn, THEN the pubkey read. The two checks\n // gate the pubkey lookup deliberately: on a real fork `getSigningKeys` reverts on an out-of-range\n // index, so reading it before the bounds check (e.g. batched in a parallel read) surfaces an\n // opaque revert instead of these precise errors (mirrors NodeOperators.s.sol:311-315).\n const op = await ctx.client.readContract({ ...m, functionName: 'getNodeOperator', args: [noId] });\n const total = (op as { totalDepositedKeys: number }).totalDepositedKeys;\n if (keyIndex >= BigInt(total)) {\n throw new Error(\n `@sm-lab/recipes: key index ${keyIndex} out of bounds (operator ${noId} has ${total} deposited keys)`,\n );\n }\n const withdrawn = await ctx.client.readContract({\n ...m,\n functionName: 'isValidatorWithdrawn',\n args: [noId, keyIndex],\n });\n if (withdrawn) {\n throw new Error(`@sm-lab/recipes: key ${keyIndex} of operator ${noId} is withdrawn`);\n }\n // count=1 → a single packed 48-byte pubkey; reuse reads.ts (same read + 48-byte guard + error).\n const key = await getPubkey(ctx, { noId, keyIndex });\n\n await actAs(ctx, ctx.addresses.stakingRouter, (from) =>\n ctx.client.writeContract({\n ...m,\n functionName: 'allocateDeposits',\n args: [amountWei, [key], [keyIndex], [noId], [amountWei]],\n account: from,\n chain: null,\n }),\n );\n\n return { amountWei };\n}\n\n/**\n * Top up every not-yet-allocated, not-withdrawn deposited key of an operator, one at a time in\n * strict ascending key-index order — `TopUpQueueOps` enforces a global FIFO queue head, so the\n * writes must be sequential and ordered (capped at `MAX_TOPUP_PER_KEY` per key). Port of\n * `NodeOperators.topUpActiveKeys` — returns the count it topped up.\n */\nexport async function topUpActiveKeys(\n ctx: Ctx,\n opts: { noId: bigint },\n): Promise<{ toppedUp: number }> {\n const m = contract(ctx, 'module');\n const { noId } = opts;\n\n const op = await ctx.client.readContract({ ...m, functionName: 'getNodeOperator', args: [noId] });\n const total = (op as { totalDepositedKeys: number }).totalDepositedKeys;\n if (total === 0) {\n throw new Error(`@sm-lab/recipes: operator ${noId} has no deposited keys`);\n }\n\n const allocated = (await ctx.client.readContract({\n ...m,\n functionName: 'getKeyAllocatedBalances',\n args: [noId, 0n, BigInt(total)],\n })) as readonly bigint[];\n // Guard the read against noUncheckedIndexedAccess: a too-short array would otherwise make\n // `allocated[i] === 0n` silently false-y for the missing tail.\n if (allocated.length !== total) {\n throw new Error(\n `@sm-lab/recipes: getKeyAllocatedBalances returned ${allocated.length} entries (expected ${total})`,\n );\n }\n\n // Reads are pulled up-front in parallel for every key — a harmless divergence from the source's\n // lazy per-key short-circuit (the extra `isValidatorWithdrawn`/`getSigningKeys` reads are free on\n // a fork and order-independent).\n const keys = await Promise.all(\n Array.from({ length: total }, (_, i) => i).map(async (i) => ({\n i,\n pubkey: (await ctx.client.readContract({\n ...m,\n functionName: 'getSigningKeys',\n args: [noId, BigInt(i), 1n],\n })) as Hex,\n withdrawn: (await ctx.client.readContract({\n ...m,\n functionName: 'isValidatorWithdrawn',\n args: [noId, BigInt(i)],\n })) as boolean,\n })),\n );\n\n // Skip already-allocated (`allocated[i] !== 0`) and withdrawn keys — the source's two `continue`s.\n const workList = keys.filter(({ i, withdrawn }) => allocated[i] === 0n && !withdrawn);\n\n // No-op without entering impersonation when nothing needs topping up (matches the\n // createOperatorGroup \"no chain call on no-op\" discipline).\n if (workList.length === 0) {\n return { toppedUp: 0 };\n }\n\n await actAs(ctx, ctx.addresses.stakingRouter, async (from) => {\n for (const { i, pubkey } of workList) {\n // eslint-disable-next-line no-await-in-loop -- sequential by necessity (TopUpQueueOps FIFO queue head; impersonation is global state)\n await ctx.client.writeContract({\n ...m,\n functionName: 'allocateDeposits',\n args: [MAX_TOPUP_PER_KEY, [pubkey], [BigInt(i)], [noId], [MAX_TOPUP_PER_KEY]],\n account: from,\n chain: null,\n });\n }\n });\n\n return { toppedUp: workList.length };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sm-lab/recipes",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "TypeScript recipes to prepare Lido SM on-chain state on an anvil fork (rewritten fork.just; no Foundry)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/exromany/sm-lab.git",
|
|
10
|
+
"directory": "tools/recipes"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"csm",
|
|
14
|
+
"lido",
|
|
15
|
+
"anvil",
|
|
16
|
+
"fork",
|
|
17
|
+
"recipes",
|
|
18
|
+
"viem",
|
|
19
|
+
"testing"
|
|
20
|
+
],
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"import": "./dist/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./cm": {
|
|
28
|
+
"types": "./dist/cm.d.mts",
|
|
29
|
+
"import": "./dist/cm.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"bin": {
|
|
37
|
+
"sm-recipes": "dist/cli.mjs"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"commander": "^15.0.0",
|
|
41
|
+
"dotenv": "^17.4.2",
|
|
42
|
+
"viem": "~2.53.1",
|
|
43
|
+
"@sm-lab/merkle": "1.2.0",
|
|
44
|
+
"@sm-lab/keys": "0.2.0",
|
|
45
|
+
"@sm-lab/receipts": "0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"tsdown": "^0.22.3",
|
|
49
|
+
"typescript": "^6.0.3",
|
|
50
|
+
"vitest": "^4.1.9",
|
|
51
|
+
"@types/node": "^26.1.0",
|
|
52
|
+
"@sm-lab/core": "0.0.0",
|
|
53
|
+
"@sm-lab/config": "0.0.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=24"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsdown",
|
|
60
|
+
"dev": "tsdown --watch",
|
|
61
|
+
"start": "node dist/cli.mjs",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"types": "tsc --noEmit"
|
|
64
|
+
}
|
|
65
|
+
}
|