@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 ADDED
@@ -0,0 +1,164 @@
1
+ # @sm-lab/recipes
2
+
3
+ TypeScript recipes that prepare Lido SM on-chain state on an **anvil fork** — the rewritten,
4
+ Foundry-free successor to the contracts repo's `fork.just`. Recipes **prepare state and
5
+ return what they did**; they do not assert (verification is the calling test's job).
6
+
7
+ ## Install
8
+
9
+ pnpm add @sm-lab/recipes
10
+
11
+ Peer runtime: a running anvil fork with the target staking module already deployed
12
+ (`anvil --fork-url <RPC>`).
13
+
14
+ ## Quick start
15
+
16
+ ```js
17
+ import { connect, addKeys, operatorInfo } from '@sm-lab/recipes';
18
+
19
+ const ctx = await connect({ module: 'csm', rpcUrl: 'http://127.0.0.1:8545' });
20
+ const { publicKeys } = await addKeys(ctx, { noId: 0n, count: 3 });
21
+ const info = await operatorInfo(ctx, { noId: 0n });
22
+ ```
23
+
24
+ `connect()` reads protocol addresses (`stakingRouter`, `vebo`, `lido`, `withdrawalQueue`,
25
+ `burner`) from `LidoLocator` on-chain and merges them onto a module-suite snapshot from
26
+ `@sm-lab/receipts`. Override the snapshot per call: `connect({ module, rpcUrl, addresses })`.
27
+
28
+ ## CLI (`sm-recipes`)
29
+
30
+ A run-and-exit CLI over the recipe surface. Same `bin` underpins every route:
31
+
32
+ ```bash
33
+ npx @sm-lab/recipes cm seed --rpc-url http://127.0.0.1:8545 # published
34
+ npm i -g @sm-lab/recipes && sm-recipes --help # global install
35
+ node tools/recipes/dist/cli.mjs --help # built dist (repo dev)
36
+ ```
37
+
38
+ Global flags: `--rpc-url` (or `RPC_URL`, defaulting to anvil's `http://127.0.0.1:8545`),
39
+ `--module <csm|cm>`, `--cl-mock-url` (or `CL_MOCK_URL`), `--json`. Amounts (`--amount`,
40
+ `--exit-balance`, …) are in **ETH** (`0.000000000000000001` = 1 wei). `sm-recipes help [command]`
41
+ mirrors `--help`. `sm-recipes completion <bash|zsh|fish>` prints a static shell-completion
42
+ script (e.g. `sm-recipes completion fish | source`).
43
+
44
+ The `cm`/`csm` groups host their own recipes **and** mirror every shared recipe with the
45
+ module pre-bound — so a shared command works two ways: top-level with `--module`, or under
46
+ the group (no `--module` needed):
47
+
48
+ ```bash
49
+ sm-recipes --module csm operator-info --operator-id 0 # shared, module via flag
50
+ sm-recipes csm operator-info --operator-id 0 # same, module from the group
51
+ sm-recipes --module csm make-rewards --json
52
+ sm-recipes cm seed --seed 0x01 # cm-only recipe
53
+ sm-recipes csm set-gate --address 0xabc... --address 0xdef... # csm-only recipe
54
+ ```
55
+
56
+ Every **required, non-repeatable** option is also accepted **positionally**, in declaration
57
+ order — so the common case needs no flags. Flags still work and can be mixed with positionals;
58
+ optional flags (e.g. `--seed`, `--pair`) stay flag-only by default.
59
+
60
+ ```bash
61
+ sm-recipes csm operator-info 0 # == --operator-id 0
62
+ sm-recipes csm withdraw 0 1 32 # == --operator-id 0 --key-index 1 --exit-balance 32
63
+ sm-recipes csm add-keys 0 5 --seed 0x01 # positional id+count, optional seed via flag
64
+ ```
65
+
66
+ `set-gate` opts its `<selector>` (optional) and `<address...>` (repeatable, **variadic** —
67
+ must come last) into the positional form, so the selector leads and the addresses follow:
68
+
69
+ ```bash
70
+ sm-recipes csm set-gate idvtc 0xabc... 0xdef... # == --selector idvtc --address 0xabc... --address 0xdef...
71
+ ```
72
+
73
+ ## The `actAs` model
74
+
75
+ Every privileged write runs through `actAs(ctx, who, fn)` — it funds `who`
76
+ (`anvil_setBalance`), impersonates it (`anvil_impersonateAccount`), runs the body, and stops.
77
+ This replaces the Solidity `broadcast*` modifiers.
78
+
79
+ ## Subpaths & gate selectors
80
+
81
+ - `@sm-lab/recipes` — shared: `connect`, `actAs`, `addKeys`, `operatorInfo`, `warpBy`,
82
+ `snapshot`, `revert`, `clActivate`, plus the module-agnostic gate helpers `resolveGate`
83
+ and `setGateAddrs` (both dispatch on `ctx.module` — see gate selectors below).
84
+ - `@sm-lab/recipes/cm` — `createCuratedOperator` (cm gates `po/pto/pgo/do/eeo/iodc/iodcp` →
85
+ `CuratedGates[0..6]`), MetaRegistry group/curve recipes `createOperatorGroup`,
86
+ `resetOperatorGroup`, `setBondCurveWeight`, and `seedCm` — seed a realistic cm fork in one call
87
+ (3 gate operators, a 34/33/33 operator group, keyed/deposited/topped-up across rounds; pass `seed`
88
+ to make the operator addresses + keys deterministic).
89
+
90
+ **Gate selectors** (`resolveGate` / `setGateAddrs`, dispatched by `ctx.module`):
91
+
92
+ - csm — `ics` → `VettedGate` (default); `idvtc` → `IdentifiedDVTClusterGate` (v3-only, hoodi;
93
+ throws on mainnet/v2 snapshots that lack it).
94
+ - cm — `po/pto/pgo/do/eeo/iodc/iodcp` or a numeric index → `CuratedGates[0..6]` (`po` is the default).
95
+ - any — a raw `0x…` gate address is used as-is.
96
+
97
+ `setGateAddrs` builds the gate's address tree and installs it on whichever gate the selector
98
+ resolves to (VettedGate and CuratedGate share the same `setTreeParams` surface). It pins the tree to
99
+ IPFS (set `IPFS_API_URL` to a local `@sm-lab/ipfs`, or `PINATA_*`), or pass `cid` to skip pinning.
100
+
101
+ ## Top-up (`allocateDeposits` as the StakingRouter)
102
+
103
+ - `increaseAllocatedBalance(ctx, { noId, keyIndex, amountWei })` — top up one deposited key's
104
+ allocated balance (validates the key exists and is not withdrawn). Returns `{ amountWei }`.
105
+ - `topUpActiveKeys(ctx, { noId })` — top up every not-yet-allocated, not-withdrawn deposited key,
106
+ one at a time in ascending key-index order (FIFO `TopUpQueueOps`, capped at 2016 ETH per key).
107
+ Returns `{ toppedUp }` (a no-op `{ toppedUp: 0 }` when nothing needs it).
108
+
109
+ ## Rewards (`makeRewards` → `submitRewards`)
110
+
111
+ `makeRewards(ctx, opts?)` builds the cumulative FeeDistributor rewards tree off on-chain
112
+ operator state and a seeded mock reward per active key, pins the tree + report log to IPFS,
113
+ and returns a typed in-memory `RewardsReport` (`treeRoot`, `treeCid`, `logCid`, `distributed`,
114
+ `rebate`, `treeDump`, `cumulatives`). It builds the data half of an oracle report.
115
+
116
+ `submitRewards(ctx, report)` consumes that report and submits it on-chain: it funds the
117
+ FeeDistributor (if `pendingSharesToDistribute` can't cover the frame), warps to the next valid
118
+ consensus frame, builds the `ReportData` tuple, reaches consensus across the fast-lane members
119
+ (falling back to `getMembers` when the fast-lane set is empty), and submits the report data as
120
+ `members[0]`. A zero-root report is a graceful no-op (`{ submitted: false }`), so
121
+ `submitRewards(ctx, await makeRewards(ctx))` composes on an empty fork.
122
+
123
+ import { makeRewards, submitRewards } from '@sm-lab/recipes';
124
+ const report = await makeRewards(ctx, { seed: '0x…', treeCid: 'cid-t', logCid: 'cid-l' });
125
+ const { submitted, refSlot, reportHash } = await submitRewards(ctx, report);
126
+
127
+ - `seed` — deterministic per-key reward draw (keccak hash-chain, like `randomKeys`). Omit for
128
+ fresh randomness; pass a fixed `Hex` to pin `treeRoot`/`distributed` in tests.
129
+ - `previousCumulatives` — carry-forward input (`Map<bigint,bigint>` or `[bigint,bigint][]`).
130
+ Every prior leaf is carried forward (the `uint64`-max pad excluded) before this frame's deltas
131
+ are added. Chain frames by feeding a prior result's `cumulatives` back in.
132
+ - IPFS pin — same env switch as `setGateAddrs` (`IPFS_API_URL` → local mock, or `PINATA_*`).
133
+ The guard fails fast (before any network) when pinning is needed but unconfigured.
134
+ - Escape hatches — pass `treeCid` and/or `logCid` to skip that pin (hermetic tests do this).
135
+ An empty report (no active keys, no carry-forward) returns a zero root and pins nothing.
136
+ - `submitRewards` is fork-only in practice (it WRITES + warps over RPC). IPFS only matters via
137
+ `makeRewards` — `submitRewards` itself consumes the report's `treeCid`/`logCid` verbatim. The
138
+ consensus-frame wait uses `warpTo` (`setNextBlockTimestamp` + `mine`), the absolute counterpart
139
+ of `warpBy`.
140
+
141
+ ## CL bridge (cl-mock)
142
+
143
+ `clActivate(ctx, { noId, keyIndex })` is the only chain-aware `@sm-lab/cl` bridge in
144
+ recipes. It requires `ctx.clMockUrl` (pass it via `connect({ module, rpcUrl, clMockUrl })`),
145
+ reads the key's pubkey + allocated balance on-chain, then marks the validator `active_ongoing`
146
+ on the running cl-mock with effective balance = `32 ETH + allocated`, in gwei (full precision,
147
+ flooring sub-gwei dust — diverging from the source's integer-ETH truncation). Raw `cl-set` /
148
+ `cl-list` stay in cl-mock's own CLI.
149
+
150
+ ## Testing
151
+
152
+ Unit tests are **hermetic** — they inject a fake viem client (no network, no chain). One
153
+ opt-in integration smoke runs against a real fork:
154
+
155
+ anvil --fork-url <hoodi RPC> # in another shell
156
+ ANVIL_FORK_URL=http://127.0.0.1:8545 pnpm --filter @sm-lab/recipes test
157
+
158
+ Without `ANVIL_FORK_URL` the smoke is skipped and the suite stays offline-green.
159
+
160
+ ## Keeping ABIs/addresses fresh
161
+
162
+ ABIs + addresses are vendored via `@sm-lab/receipts` (`pnpm --filter @sm-lab/receipts
163
+ refresh`). The SDK maintains a parallel set via its `/update-abis`; a cross-repo parity
164
+ check is a planned follow-up (not yet wired). Recipes never import ABIs from the SDK.