@midnight-ntwrk/midnight-js-protocol 5.0.0-beta.7 → 5.0.0-beta.8-pre.765ca083

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 CHANGED
@@ -32,7 +32,11 @@ import { createPlatform } from '@midnight-ntwrk/midnight-js-protocol/platform-js
32
32
 
33
33
  | Sub-path | Re-exports | Description |
34
34
  | -------- | ---------- | ----------- |
35
+ | `./errors` | (own) | `PROTOCOL_ERROR_CODES` and every error class and error type this package raises — without pulling in the ledger/compact-js/onchain-runtime/platform namespaces. `protocol-acl.test.ts` pins the exact list |
36
+ | `./version` | (own) | `LEDGER_VERSIONS`, `protocolVersionToLedger`, `versionOfRecord`, `networkHeadVersion` and the types `LedgerVersion`, `ProtocolVersionSource`, `VersionedRecord` — same lightweight guarantee as `./errors` |
35
37
  | `./ledger` | `@midnightntwrk/ledger-v9` | Ledger types and transaction primitives |
38
+ | `./v8` | `@midnightntwrk/ledger-v8` | Previous-era (v8) ledger — do not import at runtime; use `loadLedger8()` |
39
+ | `./engine` | (own) | Retained pre-fork execution engine — do not import at runtime; use `loadLedger8Engine()` |
36
40
  | `./compact-runtime` | `@midnight-ntwrk/compact-runtime` | Compact contract runtime utilities |
37
41
  | `./compact-js` | `@midnight-ntwrk/compact-js` | Compact JS bindings |
38
42
  | `./compact-js/effect` | `@midnight-ntwrk/compact-js/effect` | Effect-based Compact bindings |
@@ -42,13 +46,208 @@ import { createPlatform } from '@midnight-ntwrk/midnight-js-protocol/platform-js
42
46
  | `./platform-js/effect/Configuration` | `@midnight-ntwrk/platform-js/effect/Configuration` | Effect-based configuration |
43
47
  | `./platform-js/effect/ContractAddress` | `@midnight-ntwrk/platform-js/effect/ContractAddress` | Effect-based contract address resolution |
44
48
 
49
+ ## Source Layout
50
+
51
+ Nothing under `src/lib/` is a build entry, so this layout is internal and no consumer import path depends on it. Its job is to make one question answerable from a path alone: *which ledger does this touch?*
52
+
53
+ | Directory | Holds | Ledger reference |
54
+ |---|---|---|
55
+ | `lib/v8/` | the retained pre-fork era: `load.ts`, `engine.ts`, `load-engine.ts`, `instance-guard.ts`, `down-convert.ts`, `execute.ts`, `compose.ts`, `deploy.ts`, `adapt.ts` | acquires `ledger-v8`, `onchain-runtime-v3` and the 0.16 glue, always through a dynamic import |
56
+ | `lib/v9/` | the current era's composition arms: `compose.ts`, `wrap.ts` | links `ledger-v9` statically |
57
+ | `lib/shared/` | what both arms run: `ledger-version.ts`, `verifier-keys.ts`, `compose-options.ts`, `assemble-call.ts`, `compose-types.ts`, `unshielded.ts`, `contract-state.ts` | era passed in as a `LedgerVersion` parameter, never chosen here |
58
+ | `lib/era/` | the facade and the dispatch: `load-era.ts`, `era.ts`, `envelope.ts` | reaches both, which is the point of the layer |
59
+
60
+ The directory names say what a module is **about**, not what it links. Three consequences a reader should not have to derive:
61
+
62
+ - **`lib/v8/compose.ts`, `deploy.ts` and `adapt.ts` link no v8 at all.** They take the acquired module as a `ProtocolV8` parameter, which is a type. That injection is what keeps the v8 WASM out of the eager graph, and the guarantee is enforced by `dist-laziness.test.ts` and `v8-surface.test.ts` — not by this layout. Read the tests, not the directory, for the bundle boundary.
63
+ - **`lib/shared/` is not free of vendors.** `assemble-call.ts` links `@midnight-ntwrk/compact-js`, a post-fork package, for `hashVerifierKey` and `encodeContractKeyLocation`; `contract-state.ts` links it for `hashVerifierKey`. Both are called by both arms, so their subject is shared even though their linkage is not. This is safe in one direction only: v9 is the eagerly-linked baseline, so a shared module reaching for it never wakes v8, while the reverse would.
64
+ - **`lib/v9/wrap.ts` type-imports `lib/v8/execute.ts`.** `TranscriptPojo` is the v8 engine's output and `wrapKeepStateCall` binds it onto v9. The cross-era edge is the operation's whole purpose, and it is type-only.
65
+
66
+ `compose-types.ts` and `era.ts` name their shared types through `@midnightntwrk/ledger-v9` because some vendor has to name them. `EncodedStateValue`, `Op`, `AlignedValue` and `Transcript` are pinned identical across `onchain-runtime-v3`, `ledger-v8` and `ledger-v9` by the compile-time assertions in `v8-down-convert.test.ts`; the import names one era, the type belongs to neither. Those assertions are evaluated by `yarn typecheck:tests` on the pre-push hook, not by CI — vitest transpiles test files without type-checking, so treat a failure there as the only signal you will get.
67
+
68
+ ## Architecture Documents
69
+
70
+ The reasoning behind this package's shape lives in `docs/`, not in the source
71
+ docstrings. Each file is registered with TypeDoc through `projectDocuments`, so
72
+ it is a page in the generated API reference and `@see {@link Title}` in a
73
+ docstring resolves to it.
74
+
75
+ | Document | What it explains |
76
+ |---|---|
77
+ | [Era seam](./docs/era-seam.md) | Why only bytes and plain objects cross between the eras, how the eight operations are split between `LedgerEra` and `Ledger8Engine`, and the memoisation rules |
78
+ | [Retained-era execution](./docs/retained-era-execution.md) | The down-conversion stages, the Merkle rehash requirement, the era pin, and what a `TranscriptPojo` carries |
79
+ | [Dual-instantiation guard](./docs/dual-instantiation-guard.md) | What a duplicate WASM install does in the argument position versus the receiver position, and why the guard is a correctness requirement rather than a diagnostic |
80
+ | [Fail-closed decoding](./docs/fail-closed-decoding.md) | Why the envelope is the only authority over the bytes, and the division of labour between the three decode failures |
81
+ | [Compose refusal order](./docs/compose-refusal-order.md) | The order in which both era arms refuse compose options, and the one deliberate difference between them |
82
+ | [Verifier keys](./docs/verifier-keys.md) | Registration rules, the refusals that stop a deploy landing at an address the caller's artifacts do not describe, and why the address cannot be recomputed |
83
+ | [Module graph and lazy loading](./docs/module-graph-and-lazy-loading.md) | Build entries, the `./v8` and `./engine` chunks, the import cycle avoided by a leaf module, and why a vendor's types are named with `import type` |
84
+ | [Injected vendor slices](./docs/injected-vendor-slices.md) | How a seam names the vendor class it takes by injection — derived from the vendor's own class, declared structurally, or narrowed — and what each choice buys |
85
+ | [Shared table discipline](./docs/shared-table-discipline.md) | Why the shared tables are frozen and null-prototyped, and why exhaustiveness is enforced at compile time as well as at run time |
86
+
87
+ Docstrings in `src/` carry the API contract: what a symbol does, its
88
+ parameters, what it returns and what it throws. Anything that answers "why is
89
+ it built this way" belongs in a document above, stated once.
90
+
91
+ ## Accessing the v8 Ledger Era
92
+
93
+ The `./v8` subpath re-exports the previous-era ledger (`@midnightntwrk/ledger-v8`), which carries its own WASM. To keep that WASM out of eagerly-loaded module graphs, runtime imports of `@midnight-ntwrk/midnight-js-protocol/v8` are blocked by ESLint everywhere outside this package. Use the lazy accessor instead:
94
+
95
+ ```typescript
96
+ import { loadLedger8 } from '@midnight-ntwrk/midnight-js-protocol';
97
+
98
+ const v8 = await loadLedger8();
99
+ const transaction = v8.Transaction.deserialize(rawTransaction);
100
+ ```
101
+
102
+ Type-only imports of the subpath are allowed:
103
+
104
+ ```typescript
105
+ import type { Transaction } from '@midnight-ntwrk/midnight-js-protocol/v8';
106
+ ```
107
+
108
+ If the v8 module cannot be loaded (usually a broken or partial install), `loadLedger8()` rejects with `Ledger8RuntimeMissingError` (code `MIDNIGHT_JS_P_LEDGER8_RUNTIME_MISSING`) carrying the original error as `cause`. Its `subpath` field is `'/v8'`, naming which chunk failed. The failed load is not memoised — the next call retries.
109
+
110
+ ## Running Contracts on the Retained Pre-Fork Engine
111
+
112
+ Contracts compiled against the pre-fork toolchain keep executing on `compact-runtime@0.16` after the fork. That toolchain and its `onchain-runtime-v3` WASM live behind the `./engine` subpath, gated the same way as `./v8` and for the same reason. `loadLedger8Engine()` is the only sanctioned runtime path to it:
113
+
114
+ ```typescript
115
+ import { loadLedger8Engine, loadLedgerEra, versionOfRecord } from '@midnight-ntwrk/midnight-js-protocol';
116
+
117
+ const engine = await loadLedger8Engine();
118
+ const era = await loadLedgerEra(versionOfRecord(indexerRecord));
119
+
120
+ const state = engine.downConvertForExecution(era.extractState(rawContractState));
121
+ const transcript = engine.executeCircuit({
122
+ contract,
123
+ circuitId: 'increment',
124
+ args: [],
125
+ state,
126
+ address: contractAddress,
127
+ coinPk: coinPublicKey,
128
+ privateState
129
+ });
130
+ const prototype = engine.wrapKeepStateCall({ transcript, contractAddress, contractState: migratedV9ContractState });
131
+ ```
132
+
133
+ The engine exposes `downConvertForExecution`, `executeCircuit`, `executeConstructor` and `wrapKeepStateCall`, and they form a pipeline, each result being the next call's input. Reading a contract state and composing a call or a deploy are **not** here: both eras do those, so they live on the [ledger-era facade](#ledger-era-facade) instead.
134
+
135
+ `migratedV9ContractState` passed to `wrapKeepStateCall` must be the migrated v9 state **as read from chain**, which is not `rawContractState` above: it is where the deployed operation and its verifier key come from, and the key location the prototype carries is derived from that key. A blank or constructor-built state throws `ComposeFailedError` (code `MIDNIGHT_JS_P_COMPOSE_FAILED`) with `stage` naming which lookup failed and `version` naming the ledger era it was composing for.
136
+
137
+ Circuits with Zswap coin effects run on this leg like any other. The transcript carries `zswapLocalState` — the post-call Zswap local state, decoded into the runtime's public shape — which is what you turn into the transaction's segmented Zswap offer (`zswapStateToSegmentedOffer` in `@midnight-ntwrk/midnight-js-contracts`) and pass to `composeCallTx` as `guaranteedZswapOffer` / `fallibleZswapOffer`. Dropping it is what would leave you composing a transaction missing the coin movements the circuit recorded.
138
+
139
+ The transcript also carries `partitionContext` — the block, the starting effects and the commitment indices the pre-fork query context recorded while the circuit ran. Pass it on unchanged: a transcript composed without it is partitioned against a context the circuit never ran on, and a circuit that RECEIVED a coin in-contract cannot be partitioned at all, because the index its commitment was registered at lives only in that context. `wrapKeepStateCall` carries it for you; a hand-built call entry has to supply it. A context the target era cannot read throws `ComposeFailedError` with `stage: 'call-partition-context'`.
140
+
141
+ A failure to load the chunk itself rejects with `Ledger8RuntimeMissingError` whose `subpath` is `'/engine'`; read `cause` for which module actually failed to resolve.
142
+
143
+ ## Ledger-Era Facade
144
+
145
+ Two ledger eras are live at once: `v8` backs the node 1.x line and `v9` the 2.x line. `loadLedgerEra` hands you one of them as a single object with the same methods on both, so code that has resolved which era a record belongs to does not then have to branch on it.
146
+
147
+ ```typescript
148
+ import { loadLedgerEra, versionOfRecord } from '@midnight-ntwrk/midnight-js-protocol';
149
+
150
+ const era = await loadLedgerEra(versionOfRecord(indexerRecord));
151
+
152
+ const state = era.extractState(rawContractState);
153
+ const decoded = era.decodeContractState(rawContractState);
154
+ const callTx = era.composeCallTx({ calls, networkId, ttl });
155
+ const deploy = era.composeDeployTx({ contractState, verifierKeys, networkId, ttl });
156
+ ```
157
+
158
+ | Method | What it does |
159
+ | ------ | ------------ |
160
+ | `version` | The era this object is bound to — the value that was passed in |
161
+ | `extractState` | Reads the primary state out of a raw contract-state envelope |
162
+ | `decodeContractState` | Reads an envelope into its state plus the entry points it declares, each with its verifier key and that key's hash |
163
+ | `composeCallTx` | Composes an UNPROVEN call transaction and serializes it |
164
+ | `composeDeployTx` | Composes an UNPROVEN deploy and returns it with the address it will have and the initial state that address came from |
165
+
166
+ Derive the era with `versionOfRecord` or `networkHeadVersion` (see [Version Module](#version-module)) rather than writing the string by hand. An era string that is not `'v8'` or `'v9'` rejects with `UnknownLedgerVersionError`; the offending value is on the error's `requestedVersion` field, not in its message.
167
+
168
+ Each era is memoised, so the retained pre-fork WASM is instantiated at most once per process. Asking for `'v8'` is what acquires it — a consumer that only ever asks for `'v9'` never loads it at all, which is gated by `dist-laziness.test.ts`. A **failed** v8 acquisition is not memoised: the rejection propagates unchanged as `Ledger8RuntimeMissingError` and the next call retries.
169
+
170
+ ### Only bytes and plain objects cross this boundary
171
+
172
+ Every value going in or coming out is plain data — `Uint8Array`s and plain objects — never a live WASM handle. A contract state goes in as the bytes it was serialized to and comes back as an `EncodedStateValue` plus plain entry-point records; a composed transaction comes back as bytes. So a result can be stored, compared across eras, or sent through a `structuredClone` or a worker boundary without the module that produced it. `era-parity.test.ts` asserts exactly that, on every method, for both eras.
173
+
174
+ Because the methods are synchronous, all the awaiting happens once, at `loadLedgerEra`.
175
+
176
+ ### Where the two eras differ
177
+
178
+ The same method names mostly mean the same capabilities. One thing the v8 arm refuses that the v9 arm accepts:
179
+
180
+ - **A call tree.** The v8 arm composes exactly one call. A cross-contract call is a ledger-9-only feature a pre-fork contract cannot emit, so that era has no call tree to express: a `calls` list longer than one throws `ComposeOptionError` with `option: 'calls'` rather than composing the first entry and dropping the rest.
181
+
182
+ **A Zswap offer is not one of them.** Both eras read `guaranteedZswapOffer` / `fallibleZswapOffer` and carry the resulting offer into the transaction; both throw `ComposeOptionError` with `option: 'zswapOffer'` for bytes their own decoder rejects, with the decoder's failure on `cause`. A coin-moving call composes on either era.
183
+
184
+ The v8 arm also *requires* `verifierKeys` on `composeDeployTx`, where the v9 arm accepts its omission in one case. The retained deploy leg registers the compiled contract's keys onto the initial state itself, so it always needs the map; omitting it throws `ComposeOptionError` with `option: 'verifierKeys'`. The v9 arm allows the omission only for a state that ALREADY carries its keys, and checks rather than assumes it: a state still declaring a blank-keyed entry point throws the same `ComposeOptionError` with the same `option`. So the two arms agree on every input except one — a pre-keyed state, which deploys as-is on v9 and needs its keys supplied again on v8.
185
+
186
+ What is *not* asymmetric: a call's user-addressed unshielded payouts are aggregated onto the transaction's guaranteed and fallible offers on **both** eras. Attaching them on one era only would leave the other composing an unbalanced transaction the node rejects on submission, with nothing having reported a problem at composition time — so `era-parity.test.ts` asserts the payout each segment carries, per era.
187
+
188
+ ### Errors
189
+
190
+ | Error | Code | Raised when |
191
+ | ----- | ---- | ----------- |
192
+ | `StateDecodeFailedError` | `MIDNIGHT_JS_P_STATE_DECODE_FAILED` | A contract-state envelope could not be read by the era it was requested for — most often a state written by the other era |
193
+ | `ComposeFailedError` | `MIDNIGHT_JS_P_COMPOSE_FAILED` | Something about a CALL or a DEPLOY could not be composed: an operation that is missing, unkeyed, or names a circuit the contract does not declare; an empty call list; a pre-call state or a recorded query context the era cannot bridge; a supplied transcript with neither half; a public transcript or a set of call inputs the ledger itself rejected; or a claimed payout the transaction cannot settle (dust, or a shielded token type). `stage` is a closed union naming which of those it was — see its own docs for the full list; `version` names the era |
194
+ | `ComposeOptionError` | `MIDNIGHT_JS_P_COMPOSE_OPTION_INVALID` | A transaction-wide OPTION cannot be used at all — an empty network id, an invalid ttl, a contract state whose envelope the era rejected, an offer the era's decoder rejected, a missing verifier-key map, or a call list longer than the era can compose. `option` names the field, `version` names the era |
195
+ | `UnknownLedgerVersionError` | `MIDNIGHT_JS_P_UNKNOWN_LEDGER_VERSION` | The requested era is not `'v8'` or `'v9'` |
196
+
197
+ Each names the era it was raised for — `version` on the first three, `requestedVersion` on `UnknownLedgerVersionError`, which also takes no `cause`. None renders hex or a byte dump of its own, and the first three preserve the underlying runtime failure on `cause` where there was one.
198
+
199
+ ### Planned follow-ups
200
+
201
+ Recorded here so the reasoning is not lost, and deliberately NOT done in the change that introduced this facade:
202
+
203
+ - **Collapse the version dispatch in `lib/era/envelope.ts`.** `extractEncodedStateValue` has one production caller, which passes the literal `'v8'`; the v9 arm calls `extractV9EncodedStateValue` directly. The decoder table, the unknown-version guard and the null-prototype defence are therefore only reachable from tests, and the per-file 100% floor keeps the tests that reach them alive. Collapsing it to a `extractV8EncodedStateValue` beside the v9 one deletes real tests, which belongs in its own change.
204
+ - **Give `StateDecodeFailedError` a `stage`.** `decodeContractStateWith` wraps the whole read, so a state that decoded fine but declares an entry point resolving to no operation is reported with the same code and the same "resolve the era and check the bytes" remediation as an envelope written by the other era. A discriminator would separate them; it is a public error-shape change.
205
+ - **Give `ComposeOptionError` a `circuitId`.** The v9 blank-key refusal knows which entry point was blank and cannot say so, because the field does not exist. Adding it would let that refusal name the slot without breaking the class parity the two arms currently have.
206
+
207
+ ## Version Module
208
+
209
+ Mapping a raw `protocolVersion` integer onto the ledger runtime it corresponds to. Reachable from the root barrel and, without loading the ledger/compact-js/onchain-runtime/platform namespaces, from the `./version` subpath. The error it throws is reachable from `./errors` on the same terms.
210
+
211
+ The `protocolVersion` integer encodes the **node** version as `major * 1_000_000 + minor * 1_000 + patch`, so a whole node major occupies a 1_000_000-wide range:
212
+
213
+ | protocolVersion range | node version | ledger |
214
+ | --------------------- | ------------ | ------ |
215
+ | 1_000_000 – 1_999_999 | 1.x | v8 |
216
+ | 2_000_000 – 2_999_999 | 2.x | v9 |
217
+
218
+ Anything outside those ranges throws rather than guessing. Node 0.x is deliberately absent: the indexer's own table does map it, but midnight-js meets only node 1.x or 2.x, so a 0.x `protocolVersion` is reported as unknown rather than silently resolved.
219
+
220
+ ```typescript
221
+ import {
222
+ LEDGER_VERSIONS, // readonly ['v8', 'v9']
223
+ protocolVersionToLedger, // (protocolVersion: number, path?: 'read' | 'construct') => 'v8' | 'v9' (path defaults to 'construct')
224
+ versionOfRecord, // (record: { protocolVersion: number }) => 'v8' | 'v9'
225
+ networkHeadVersion // (source: { queryLatestProtocolVersion(): Promise<number> }) => Promise<'v8' | 'v9'>
226
+ } from '@midnight-ntwrk/midnight-js-protocol/version';
227
+ import { UnknownProtocolVersionError } from '@midnight-ntwrk/midnight-js-protocol/errors';
228
+ ```
229
+
230
+ Prefer `versionOfRecord` for a `protocolVersion` already read off an indexer/node record, and `networkHeadVersion` for the network's current head version — both tag any error with the correct path automatically. `UnknownProtocolVersionError` carries a `reason` of `'malformed'` (the input was not a non-negative integer) or `'unknown'` (a well-formed integer outside every mapped range), so callers can distinguish "bad input" from "genuinely unsupported protocol version".
231
+
45
232
  ## ESLint Enforcement
46
233
 
47
- An ESLint `no-restricted-imports` rule prevents direct imports of the underlying protocol packages outside of this package. If you see an error like:
234
+ Three rules in the repo's `eslint.config.mjs` govern how other packages reach protocol internals.
235
+
236
+ **1. The protocol ACL** — a `no-restricted-imports` rule prevents direct imports of the underlying protocol packages outside of this package. If you see an error like:
48
237
 
49
238
  > Import from `@midnight-ntwrk/midnight-js-protocol/ledger` instead.
50
239
 
51
- Replace the direct protocol import with the corresponding subpath from this package.
240
+ Replace the direct protocol import with the corresponding subpath from this package. For the v8 era specifically, the error
241
+
242
+ > Runtime v8 access only via loadLedger8() from @midnight-ntwrk/midnight-js-protocol.
243
+
244
+ means: replace the direct `./v8` runtime import with `loadLedger8()` (type-only imports stay as they are).
245
+
246
+ **2. The v8 static-import gate** — `@typescript-eslint/no-restricted-imports` blocks runtime imports of `@midnight-ntwrk/midnight-js-protocol/v8` outside `packages/protocol/src/`. Type-only imports (`import type`) are allowed.
247
+
248
+ **3. The v8 dynamic-import gate** — `no-restricted-syntax` selectors block `import('@midnight-ntwrk/midnight-js-protocol/v8')` in the same scopes. An interpolated template literal cannot be matched statically and is not covered.
249
+
250
+ Both v8 gates point at `loadLedger8()`, the accessor on the root barrel — the only sanctioned runtime path to the v8 era.
52
251
 
53
252
  ## Resources
54
253
 
@@ -0,0 +1,367 @@
1
+ import * as ledgerV9 from '@midnightntwrk/ledger-v9';
2
+ import { CallContext, Effects, CoinCommitment, EncodedStateValue, ContractCallPrototype } from '@midnightntwrk/ledger-v9';
3
+ export { EncodedStateValue } from '@midnightntwrk/ledger-v9';
4
+ import * as OnchainRuntimeV3 from '@midnight-ntwrk/onchain-runtime-v3';
5
+ import { EncodedZswapLocalState, ProofData, ZswapLocalState } from 'compact-runtime-ledger8';
6
+
7
+ /**
8
+ * One entry point a contract state declares, with the verifier key registered
9
+ * against it if there is one.
10
+ *
11
+ * `verifierKey` and `verifierKeyHash` are both absent for a blank slot — the
12
+ * shape a constructor-built state has before a deploy fills it in.
13
+ *
14
+ * @see {@link FailClosedDecoding} for why they are absent rather than
15
+ * zero-length or a hash of nothing.
16
+ */
17
+ interface ContractEntryPointPojo {
18
+ readonly circuitId: string;
19
+ readonly verifierKey: Uint8Array | undefined;
20
+ readonly verifierKeyHash: string | undefined;
21
+ }
22
+
23
+ /**
24
+ * The query-context state a call recorded while it ran, which its pre-call
25
+ * state bytes do not carry.
26
+ *
27
+ * `block` and `effects` are the PRE-call values; `comIndices` is the POST-call
28
+ * map. Plain data on every member.
29
+ *
30
+ * @see {@link ComposeRefusalOrder} for why partitioning needs the context the
31
+ * circuit actually ran on, and why `CallContext` and `Effects` are declared
32
+ * once against ledger-v9.
33
+ * @see {@link RetainedEraExecution} for why each member is read off the
34
+ * context it is.
35
+ * @see {@link EraSeam}
36
+ */
37
+ interface PartitionContext {
38
+ readonly block: CallContext;
39
+ readonly effects: Effects;
40
+ /** Commitment -> the index the runtime recorded it at. Empty for a call that received no coin. */
41
+ readonly comIndices: ReadonlyMap<CoinCommitment, bigint>;
42
+ }
43
+ /**
44
+ * The sentinel that asks for the ledger's own INITIAL cost model instead of the chain's.
45
+ *
46
+ * Spelled out as a value a caller has to name, because the thing it selects is wrong for any chain
47
+ * that has been running: the initial parameters are the model the chain started with, and prices
48
+ * adjust per block. Partitioning against them draws the guaranteed/fallible boundary in the wrong
49
+ * place and the node then refuses the guaranteed segment with `Transcript(Execution(OutOfGas))` --
50
+ * after the caller has already paid to prove it.
51
+ */
52
+ declare const INITIAL_LEDGER_PARAMETERS = "initial";
53
+ /**
54
+ * What a composer accepts for the block's ledger parameters: the chain's own serialized parameters,
55
+ * or {@link INITIAL_LEDGER_PARAMETERS}.
56
+ *
57
+ * There is deliberately no third option. This used to be optional, and omitting it fell back to the
58
+ * initial parameters silently -- so a caller that simply forgot got the wrong cost model with no
59
+ * signal, and a `PublicDataProvider` that does not serve `ledgerParameters` (the field is optional)
60
+ * degraded every call built through it. Requiring the option keeps the compatibility path reachable
61
+ * only as a decision, never as an oversight.
62
+ *
63
+ * A caller that reads the chain must pass the bytes from the SAME read as the contract state: the
64
+ * parameters are era-tagged and dated per block, so a second read could answer for another block.
65
+ *
66
+ * @see {@link EraSeam}
67
+ */
68
+ type LedgerParametersOption = Uint8Array | typeof INITIAL_LEDGER_PARAMETERS;
69
+
70
+ type ChargedState = OnchainRuntimeV3.ChargedState;
71
+ type StateValue = OnchainRuntimeV3.StateValue;
72
+ /**
73
+ * The retained runtime's state handle types, under names a consumer can write.
74
+ *
75
+ * `DownConvertedState.data` is an `onchain-runtime-v3` `ChargedState` and
76
+ * `.data.state` a `StateValue`, and neither was nameable outside this package:
77
+ * the aliases above are local, and the vendor package publishes no subpath to
78
+ * import them from. Published because results now carry these handles.
79
+ *
80
+ * TYPE-ONLY, deliberately. `import type` is erased at compile time, so nothing
81
+ * here puts a second copy of the retained runtime on any module graph — the
82
+ * loaders remain the only runtime path to it.
83
+ */
84
+ type Ledger8ChargedState = ChargedState;
85
+ /** See {@link Ledger8ChargedState}. */
86
+ type Ledger8StateValue = StateValue;
87
+
88
+ /**
89
+ * The result of a down-convert: only the primary state data a pre-fork
90
+ * circuit reads during execution.
91
+ *
92
+ * Deliberately not a full pre-fork `ContractState`: it carries no
93
+ * `.operations`, `.maintenanceAuthority` or `.balance`, which remain the
94
+ * caller's to carry.
95
+ *
96
+ * @see {@link RetainedEraExecution}
97
+ */
98
+ interface DownConvertedState {
99
+ readonly data: ChargedState;
100
+ }
101
+
102
+ type AlignedValue = OnchainRuntimeV3.AlignedValue;
103
+ type Op<T> = OnchainRuntimeV3.Op<T>;
104
+
105
+ /**
106
+ * The `QueryContext` slice {@link executeCircuit} reads off a circuit's
107
+ * context: the primary state, ready to be wrapped back into a
108
+ * {@link DownConvertedState}, plus the three members a composition leg needs to
109
+ * partition the call's public transcript against the context it really ran on
110
+ * (see {@link PartitionContext}).
111
+ *
112
+ * A `Pick` of the vendor's own class, not a restatement of it: the member names
113
+ * and their types come from onchain-runtime-v3, so a rename there fails this
114
+ * build instead of leaving a mirror that describes a property the runtime no
115
+ * longer has. It stays a narrowing rather than the whole class because
116
+ * `QueryContext` is a WASM class with dozens of members, and this seam reads
117
+ * four — the narrowing is what lets the execution tests hand `executeCircuit` a
118
+ * plain object double instead of standing up real WASM.
119
+ *
120
+ * @see {@link RetainedEraExecution}
121
+ */
122
+ type Ledger8QueryContext = Pick<OnchainRuntimeV3.QueryContext, 'state' | 'block' | 'effects' | 'comIndices'>;
123
+ /**
124
+ * The circuit-context slice {@link executeCircuit} needs from a pre-fork
125
+ * (`compact-runtime@0.16`) `createCircuitContext` call and a circuit's
126
+ * updated context after it runs.
127
+ *
128
+ * Not the glue's `CircuitContext` itself: that one carries a real
129
+ * `QueryContext`, a WASM class no test double can satisfy, and this seam reads
130
+ * a handful of properties off it. The narrowing is the difference between
131
+ * execution tests that check plumbing with a literal and tests that must stand
132
+ * up WASM to do it — see {@link Ledger8QueryContext}, which derives those
133
+ * properties from the vendor's class so the narrowing cannot drift from it.
134
+ *
135
+ * @see {@link RetainedEraExecution}
136
+ */
137
+ interface Ledger8CircuitContext {
138
+ readonly currentQueryContext: Ledger8QueryContext;
139
+ readonly currentPrivateState: unknown;
140
+ readonly currentZswapLocalState: EncodedZswapLocalState;
141
+ }
142
+ /**
143
+ * The proof-data slice of a pre-fork {@link Ledger8CircuitResult}.
144
+ *
145
+ * The vendor's own `ProofData`, not a copy of its four members: it is already
146
+ * plain data with no WASM handle in it, so there was nothing for a mirror to
147
+ * narrow — only a shape to drift out of sync. `Readonly` is this package's own
148
+ * addition, and the only one.
149
+ *
150
+ * @see {@link EraSeam}
151
+ */
152
+ type Ledger8ProofData = Readonly<ProofData>;
153
+ /**
154
+ * What a pre-fork `impureCircuits[id](ctx, ...args)` call returns.
155
+ *
156
+ * Tracks the glue's `CircuitResults` but is not derived from it: `context` is
157
+ * the narrowed {@link Ledger8CircuitContext} for the reason given there, and
158
+ * `proofData` IS the vendor's own type — see {@link Ledger8ProofData}.
159
+ *
160
+ * @see {@link RetainedEraExecution}
161
+ */
162
+ interface Ledger8CircuitResult {
163
+ readonly result: unknown;
164
+ readonly proofData: Ledger8ProofData;
165
+ readonly context: Ledger8CircuitContext;
166
+ }
167
+ /** One callable entry point on a compiled pre-fork contract's `impureCircuits` map. */
168
+ type Ledger8ImpureCircuit = (ctx: Ledger8CircuitContext, ...args: readonly unknown[]) => Ledger8CircuitResult;
169
+ /**
170
+ * The subset of a compiled pre-fork (`compact-runtime@0.16`) contract module
171
+ * {@link executeCircuit} needs: only `impureCircuits`, the map every
172
+ * generated contract exposes its callable entry points under.
173
+ *
174
+ * @see {@link RetainedEraExecution}
175
+ */
176
+ interface Ledger8ContractLike {
177
+ readonly impureCircuits: Readonly<Record<string, Ledger8ImpureCircuit>>;
178
+ }
179
+ /**
180
+ * The result of one impure circuit's invocation on a pre-fork
181
+ * (`compact-runtime@0.16`) contract instance: the primary result plus every
182
+ * artifact {@link wrapKeepStateCall} (`../v9/wrap.ts`) needs to assemble a
183
+ * v9-native `ContractCallPrototype`.
184
+ *
185
+ * `preContractState`/`postContractState` are {@link DownConvertedState}s, not
186
+ * full pre-fork `ContractState`s: they carry only `.data`. They are also the
187
+ * only members here that are LIVE HANDLES -- every other member of this type is
188
+ * plain data. `postContractStateEncoded` is the post-state's encoded form,
189
+ * carried beside the handle so a caller has a value that outlives the runtime
190
+ * instance; the pre-state's encoded form is the caller's own input, which
191
+ * `downConvertForExecution` already proves round-trips to it.
192
+ *
193
+ * `partitionContext` is the query-context state the call ran with, which the
194
+ * carried state bytes do not hold — see {@link PartitionContext}. A composition
195
+ * leg needs it to partition the call's transcript.
196
+ *
197
+ * `zswapLocalState` is the post-call Zswap local state, DECODED into the
198
+ * runtime's public shape: the coins the circuit spent and produced. A caller
199
+ * turns it into the transaction's segmented Zswap offer
200
+ * (`zswapStateToSegmentedOffer`, `packages/contracts/src/internal/utils/zswap-utils.ts`)
201
+ * and hands that offer to whichever composition leg it targets.
202
+ *
203
+ * @see {@link RetainedEraExecution}
204
+ */
205
+ interface TranscriptPojo {
206
+ readonly circuitId: string;
207
+ readonly result: unknown;
208
+ readonly input: AlignedValue;
209
+ readonly output: AlignedValue;
210
+ readonly publicTranscript: Op<AlignedValue>[];
211
+ readonly privateTranscriptOutputs: AlignedValue[];
212
+ readonly preContractState: DownConvertedState;
213
+ readonly postContractState: DownConvertedState;
214
+ /**
215
+ * The post-call state as an {@link EncodedStateValue}: the same value
216
+ * {@link postContractState} holds, in the form that survives this process.
217
+ *
218
+ * `EncodedStateValue` is pinned identical across `onchain-runtime-v3`,
219
+ * `ledger-v8` and `ledger-v9` -- see this package's README and the
220
+ * cross-runtime assertions in `src/test/v8-down-convert.test.ts` -- so this is
221
+ * the member era-agnostic code reads, and the one that can be persisted,
222
+ * cloned or sent to a worker.
223
+ */
224
+ readonly postContractStateEncoded: EncodedStateValue;
225
+ readonly privateStateAfter: unknown;
226
+ readonly partitionContext: PartitionContext;
227
+ readonly zswapLocalState: ZswapLocalState;
228
+ }
229
+ /** Everything {@link executeCircuit} needs to run one impure circuit call. */
230
+ interface ExecuteCircuitOptions {
231
+ readonly contract: Ledger8ContractLike;
232
+ readonly circuitId: string;
233
+ readonly args: readonly unknown[];
234
+ readonly state: DownConvertedState;
235
+ readonly address: string;
236
+ readonly coinPk: string;
237
+ readonly privateState: unknown;
238
+ }
239
+
240
+ /**
241
+ * Everything {@link wrapKeepStateCall} needs to wrap one keep-state call.
242
+ * `contractState` is the migrated, post-fork v9 `ContractState` — read from
243
+ * chain, or otherwise carrying the contract's real registered operations —
244
+ * used only to look up the `ContractOperation` for `transcript.circuitId`
245
+ * (mirrors `ComposeV8CallOptions`'s `contractState` parameter in
246
+ * `../v8/compose.ts`).
247
+ */
248
+ interface WrapKeepStateCallOptions {
249
+ readonly transcript: TranscriptPojo;
250
+ readonly contractAddress: string;
251
+ readonly contractState: ledgerV9.ContractState;
252
+ /**
253
+ * The ledger parameters of the block this call is built against, or
254
+ * {@link INITIAL_LEDGER_PARAMETERS} to accept the initial cost model.
255
+ *
256
+ * Required rather than defaulted, and not hard-coded to the initial parameters here: the
257
+ * transcript crosses as `'unpartitioned'`, so the partitioner DOES run and the cost model it uses
258
+ * is whatever this supplies. Defaulting it would make this function the silent
259
+ * wrong-cost-model path the option exists to close. See {@link LedgerParametersOption}.
260
+ */
261
+ readonly ledgerParameters: LedgerParametersOption;
262
+ }
263
+
264
+ /**
265
+ * The minimal shape a pre-fork (`compact-runtime@0.16`) `ContractState` is used
266
+ * through here: just `.serialize()`. It is what {@link executeConstructor}
267
+ * returns on its result, and `.serialize()` is how a caller turns that handle
268
+ * into the bytes every deploy leg takes.
269
+ *
270
+ * Crosses the era boundary by bytes, not by handle.
271
+ *
272
+ * @see {@link DualInstantiationGuard} for why that crossing is the one a
273
+ * duplicate install cannot affect
274
+ * @see {@link EraSeam}
275
+ */
276
+ type Ledger8DeployableContractState = Pick<OnchainRuntimeV3.ContractState, 'serialize'>;
277
+ /** What a pre-fork `contract.initialState(constructorContext, ...args)` call returns. */
278
+ interface Ledger8ConstructorResult {
279
+ readonly currentContractState: Ledger8DeployableContractState;
280
+ readonly currentPrivateState: unknown;
281
+ /**
282
+ * The Zswap local state the constructor ended on, still ENCODED. A
283
+ * constructor that mints a coin records it here, and a deploy that drops it
284
+ * composes a transaction the ledger cannot balance.
285
+ */
286
+ readonly currentZswapLocalState: EncodedZswapLocalState;
287
+ }
288
+ /**
289
+ * The subset of a compiled pre-fork (`compact-runtime@0.16`) contract module
290
+ * {@link executeConstructor} needs: only `initialState`, the constructor
291
+ * every generated contract exposes to build its initial ledger state.
292
+ */
293
+ interface Ledger8ConstructorContractLike {
294
+ readonly initialState: (constructorContext: unknown, ...args: readonly unknown[]) => Ledger8ConstructorResult;
295
+ }
296
+ /** Everything {@link executeConstructor} needs to run one contract constructor. */
297
+ interface ExecuteConstructorOptions {
298
+ readonly contract: Ledger8ConstructorContractLike;
299
+ readonly args: readonly unknown[];
300
+ readonly privateState: unknown;
301
+ readonly coinPk: string;
302
+ }
303
+ /**
304
+ * The result of running a pre-fork constructor: the freshly built contract
305
+ * state (still carrying blank verifier keys on every operation slot — see
306
+ * {@link composeV8DeployTx}) and the resulting private state.
307
+ */
308
+ interface ConstructorResultPojo {
309
+ readonly contractState: Ledger8DeployableContractState;
310
+ readonly privateState: unknown;
311
+ /**
312
+ * The constructor's own Zswap local state, DECODED — plain data in both
313
+ * runtimes. Empty for the ordinary constructor that mints nothing; carries
314
+ * the outputs for one that does, which is what lets the deploy be balanced.
315
+ */
316
+ readonly zswapLocalState: ZswapLocalState;
317
+ }
318
+
319
+ /**
320
+ * The public surface {@link createLedger8Engine} builds: the retained pre-fork
321
+ * EXECUTION capabilities, with the 0.16 runtime instance already captured in
322
+ * closure — no method here takes a runtime or module parameter.
323
+ *
324
+ * Every method is synchronous: this object is handed over only after the
325
+ * retained toolchain has been acquired.
326
+ *
327
+ * @see {@link EraSeam}
328
+ */
329
+ interface Ledger8Engine {
330
+ downConvertForExecution(state: EncodedStateValue): DownConvertedState;
331
+ executeCircuit(options: ExecuteCircuitOptions): TranscriptPojo;
332
+ wrapKeepStateCall(options: WrapKeepStateCallOptions): ContractCallPrototype;
333
+ executeConstructor(options: ExecuteConstructorOptions): ConstructorResultPojo;
334
+ /**
335
+ * Re-expresses a retained-era contract's entry points as a current-era contract state, so a
336
+ * keep-state call has an operation registry the current composer can read.
337
+ *
338
+ * Fork-crossing work, which is why it sits here rather than on either era facade: the input is
339
+ * what the retained decoder read off the chain, and the output is for the current ledger.
340
+ */
341
+ reexpressOperationsForCurrentEra(entryPoints: readonly ContractEntryPointPojo[]): Uint8Array;
342
+ }
343
+ /**
344
+ * Acquires the retained pre-fork toolchain — the `compact-runtime@0.16` glue
345
+ * and `@midnight-ntwrk/onchain-runtime-v3` — and builds a {@link Ledger8Engine}
346
+ * bound to it.
347
+ *
348
+ * Runs {@link assertSharedLedger8Instance} exactly once, on the
349
+ * `onchain-runtime-v3` axis. Any acquisition failure surfaces through the
350
+ * facade (`lib/v8/load-engine.ts`) as `Ledger8RuntimeMissingError`
351
+ * (`../../errors.ts`).
352
+ *
353
+ * Does NOT acquire the v8 ledger module: a consumer that only executes
354
+ * circuits and binds them onto v9 never instantiates the multi-megabyte v8
355
+ * WASM, and never hard-depends on ledger-v8 resolving.
356
+ *
357
+ * @returns The engine surface, with the acquired runtime captured in closure.
358
+ * @throws Ledger8InstanceMismatchError If `onchain-runtime-v3` resolved to two
359
+ * physically distinct copies in this process.
360
+ * @see {@link EraSeam}
361
+ * @see {@link DualInstantiationGuard}
362
+ * @see {@link ModuleGraphAndLazyLoading}
363
+ */
364
+ declare const createLedger8Engine: () => Promise<Ledger8Engine>;
365
+
366
+ export { createLedger8Engine };
367
+ export type { ConstructorResultPojo, ContractEntryPointPojo, DownConvertedState, ExecuteCircuitOptions, ExecuteConstructorOptions, Ledger8ChargedState, Ledger8DeployableContractState, Ledger8Engine, Ledger8StateValue, TranscriptPojo, WrapKeepStateCallOptions };