@midnight-ntwrk/midnight-js-protocol 5.0.0-beta.8 → 5.0.0-rc.1

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
@@ -52,7 +52,7 @@ Nothing under `src/lib/` is a build entry, so this layout is internal and no con
52
52
 
53
53
  | Directory | Holds | Ledger reference |
54
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 |
55
+ | `lib/v8/` | the retained pre-fork era: `load.ts`, `engine.ts`, `load-engine.ts`, `executable.ts`, `compose.ts`, `deploy.ts`, `prove.ts`, `adapt.ts` | acquires `ledger-v8`, `onchain-runtime-v3` and the 0.16 glue, always through a dynamic import |
56
56
  | `lib/v9/` | the current era's composition arms: `compose.ts`, `wrap.ts` | links `ledger-v9` statically |
57
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
58
  | `lib/era/` | the facade and the dispatch: `load-era.ts`, `era.ts`, `envelope.ts` | reaches both, which is the point of the layer |
@@ -61,9 +61,9 @@ The directory names say what a module is **about**, not what it links. Three con
61
61
 
62
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
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.
64
+ - **`lib/v9/wrap.ts` type-imports `lib/v8/executable.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
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.
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`, `Transcript`, `CallContext` and `Effects` are pinned identical across `onchain-runtime-v3`, `ledger-v8` and `ledger-v9` by the compile-time assertions at the bottom of `v8-executable.test.ts`; the import names one era, the type belongs to neither. Those assertions are evaluated by `yarn typecheck:tests` — run by the pre-push hook AND by CI's `typecheck:tests:core` job. The test run itself does not evaluate them, because vitest transpiles test files without type-checking.
67
67
 
68
68
  ## Architecture Documents
69
69
 
@@ -82,6 +82,7 @@ docstring resolves to it.
82
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
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
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
+ | [Cross-era operation registry](./docs/cross-era-operation-registry.md) | What actually crosses the fork boundary when a dormant retained-era contract is called, why re-expressing its operations is faithful rather than a substitution, and why the primary state is left at its default |
85
86
  | [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
 
87
88
  Docstrings in `src/` carry the API contract: what a symbol does, its
@@ -117,12 +118,16 @@ import { loadLedger8Engine, loadLedgerEra, versionOfRecord } from '@midnight-ntw
117
118
  const engine = await loadLedger8Engine();
118
119
  const era = await loadLedgerEra(versionOfRecord(indexerRecord));
119
120
 
120
- const state = engine.downConvertForExecution(era.extractState(rawContractState));
121
- const transcript = engine.executeCircuit({
121
+ // The DECODED state, not the extracted one: the balances a circuit reads are
122
+ // not part of the primary state, and this value carries them alongside it.
123
+ // One read in, one value out -- there is no second argument that could
124
+ // describe a different block.
125
+ const contractState = era.decodeContractState(rawContractState);
126
+ const transcript = await engine.executeCircuit({
122
127
  contract,
123
128
  circuitId: 'increment',
124
129
  args: [],
125
- state,
130
+ contractState,
126
131
  address: contractAddress,
127
132
  coinPk: coinPublicKey,
128
133
  privateState
@@ -130,11 +135,17 @@ const transcript = engine.executeCircuit({
130
135
  const prototype = engine.wrapKeepStateCall({ transcript, contractAddress, contractState: migratedV9ContractState });
131
136
  ```
132
137
 
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.
138
+ `executeCircuit` and `executeConstructor` are **asynchronous** — `await` them. There is no separate down-convert step: pass the decoded contract state straight in, and the engine builds the state the retained runtime executes against.
139
+
140
+ Read it with the era the state's own envelope names, not with a fixed one. A contract an earlier post-fork call has MIGRATED carries a current-era envelope while its artifacts stay the retained toolchain's, so `era` above is the head's era for that contract and the retained era for one that has not migrated. Handing the retained reader a current-era envelope is refused on the tag.
141
+
142
+ `contractState` must carry a usable `balance`. It is required rather than defaulted, because an empty balance is a legitimate value — a contract holding nothing has one — and defaulting would make "holds nothing" indistinguishable from "the caller forgot", which is a circuit reading zero where the chain says otherwise. `decodeContractState` produces both halves for you; assembling the value by hand is what the refusal is there for.
143
+
144
+ The engine exposes `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
145
 
135
146
  `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
147
 
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.
148
+ 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`). You do not hand that offer to `composeCallTx` as ready-made bytes: you hand it a `zswapOffer` factory, which the composer calls back with each call's guaranteed/fallible split once it has drawn it. That split is the fourth argument `zswapStateToSegmentedOffer` routes by — without it every movement lands in the guaranteed segment, and a circuit whose transcript is wholly fallible produces an offer the wallet cannot balance. Omitting the factory altogether leaves you composing a transaction missing the coin movements the circuit recorded.
138
149
 
139
150
  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
151
 
@@ -151,7 +162,17 @@ const era = await loadLedgerEra(versionOfRecord(indexerRecord));
151
162
 
152
163
  const state = era.extractState(rawContractState);
153
164
  const decoded = era.decodeContractState(rawContractState);
154
- const callTx = era.composeCallTx({ calls, networkId, ttl });
165
+ const { transaction, partitions } = era.composeCallTx({
166
+ calls,
167
+ networkId,
168
+ ttl,
169
+ // Called back ONCE, with one `[guaranteed, fallible]` pair per call, in
170
+ // `calls` order -- cross-contract callees first, the root call last. Route
171
+ // against all of them: a transaction carries one offer per segment, so
172
+ // destructuring the first pair alone would route the whole transaction
173
+ // against a callee's split and drop the root call's.
174
+ zswapOffer: (partitions) => buildSegmentedOfferBytes(partitions)
175
+ });
155
176
  const deploy = era.composeDeployTx({ contractState, verifierKeys, networkId, ttl });
156
177
  ```
157
178
 
@@ -160,7 +181,7 @@ const deploy = era.composeDeployTx({ contractState, verifierKeys, networkId, ttl
160
181
  | `version` | The era this object is bound to — the value that was passed in |
161
182
  | `extractState` | Reads the primary state out of a raw contract-state envelope |
162
183
  | `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 |
184
+ | `composeCallTx` | Composes an UNPROVEN call transaction and answers with its bytes plus each call's guaranteed/fallible split |
164
185
  | `composeDeployTx` | Composes an UNPROVEN deploy and returns it with the address it will have and the initial state that address came from |
165
186
 
166
187
  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.
@@ -179,7 +200,7 @@ The same method names mostly mean the same capabilities. One thing the v8 arm re
179
200
 
180
201
  - **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
202
 
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.
203
+ **A Zswap offer is not one of them.** Both eras call the `zswapOffer` factory back with the split they resolved and carry the offer it answers with into the transaction; both throw `ComposeOptionError` with `option: 'zswapOffer'` for bytes their own decoder rejects, with the decoder's failure on `cause`. Both read that offer LAST, after the call's unshielded payout has been aggregated, so a call with two faults is refused the same way on either era. A coin-moving call composes on either era.
183
204
 
184
205
  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
206
 
@@ -0,0 +1 @@
1
+ export * from '@midnight-ntwrk/compact-js/v8/effect';
@@ -0,0 +1,2 @@
1
+ export * from '@midnight-ntwrk/compact-js/v8/effect';
2
+ //# sourceMappingURL=compact-js-v8-effect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compact-js-v8-effect.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/dist/engine.d.ts CHANGED
@@ -1,9 +1,28 @@
1
1
  import * as ledgerV9 from '@midnightntwrk/ledger-v9';
2
- import { CallContext, Effects, CoinCommitment, EncodedStateValue, ContractCallPrototype } from '@midnightntwrk/ledger-v9';
2
+ import { TokenType, EncodedStateValue, CallContext, Effects, CoinCommitment, ContractCallPrototype } from '@midnightntwrk/ledger-v9';
3
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';
4
+ import { ContractExecutable } from '@midnight-ntwrk/compact-js/v8/effect';
6
5
 
6
+ /**
7
+ * The public balances a contract holds, keyed by token type.
8
+ *
9
+ * Derived from the vendor's own `TokenType` rather than restated, so a rename
10
+ * fails this build instead of leaving a mirror describing a shape neither
11
+ * runtime has.
12
+ *
13
+ * `shared-contract-state.test.ts` pins all three `TokenType` declarations —
14
+ * both eras' and the execution runtime's, which is where the map actually
15
+ * lands — mutually assignable. That pin is STRUCTURAL and can be no more:
16
+ * `raw` is an opaque `string` everywhere, so assignability says the handoff
17
+ * type-checks and says nothing about two eras reading a given key as the same
18
+ * colour.
19
+ *
20
+ * Plain data end to end — string-tagged objects and `bigint`s — so it crosses
21
+ * an era boundary like every other member of {@link ContractStatePojo}. That is
22
+ * a statement about `structuredClone`, which carries a `Map`; a JSON round trip
23
+ * does NOT, and leaves the plain object the balance guards refuse.
24
+ */
25
+ type ContractBalance = ReadonlyMap<TokenType, bigint>;
7
26
  /**
8
27
  * One entry point a contract state declares, with the verifier key registered
9
28
  * against it if there is one.
@@ -19,6 +38,27 @@ interface ContractEntryPointPojo {
19
38
  readonly verifierKey: Uint8Array | undefined;
20
39
  readonly verifierKeyHash: string | undefined;
21
40
  }
41
+ /**
42
+ * A contract state as plain data: the primary state in its encoded form, the
43
+ * balances the contract holds, and the entry points the state declares.
44
+ *
45
+ * `entryPoints` is an ARRAY, not a map keyed by circuit id: two distinct byte
46
+ * entry points can decode to the same name, and a caller has to reconcile
47
+ * them.
48
+ *
49
+ * @see {@link FailClosedDecoding}
50
+ */
51
+ interface ContractStatePojo {
52
+ readonly state: EncodedStateValue;
53
+ /**
54
+ * The balances the contract holds, which are NOT part of the primary state:
55
+ * the ledger keeps them beside it, so a caller reading only `state` cannot
56
+ * reach them. A retained-era call that executes without them runs every
57
+ * circuit against an empty balance — see {@link RetainedEraExecution}.
58
+ */
59
+ readonly balance: ContractBalance;
60
+ readonly entryPoints: readonly ContractEntryPointPojo[];
61
+ }
22
62
 
23
63
  /**
24
64
  * The query-context state a call recorded while it ran, which its pre-call
@@ -67,174 +107,150 @@ declare const INITIAL_LEDGER_PARAMETERS = "initial";
67
107
  */
68
108
  type LedgerParametersOption = Uint8Array | typeof INITIAL_LEDGER_PARAMETERS;
69
109
 
70
- type ChargedState = OnchainRuntimeV3.ChargedState;
71
- type StateValue = OnchainRuntimeV3.StateValue;
110
+ /** A retained-era signing key: 32 bytes of hex. @see {@link LEDGER8_SIGNING_KEY_PATTERN} */
111
+ type Ledger8SigningKey = string;
72
112
  /**
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.
113
+ * A retained-era contract, as the caller holds it: the CONSTRUCTED artifact the
114
+ * previous toolchain generates.
115
+ *
116
+ * Deliberately not compact-js's `CompiledContract` container. That container is
117
+ * a recipe — a class plus witnesses, instantiated fresh per operation — and the
118
+ * retained era's callers hold an instance whose witnesses are already bound.
119
+ * {@link containerFor} adapts one to the other at this seam, which keeps the
120
+ * adaptation in one place instead of on every consumer.
83
121
  */
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;
122
+ interface RetainedContract {
123
+ /** The map compact-js indexes to find the circuit to run. */
124
+ readonly provableCircuits: Readonly<Record<string, unknown>>;
125
+ /** The constructor a deployment runs. */
126
+ initialState(...args: never[]): unknown;
100
127
  }
101
128
 
102
- type AlignedValue = OnchainRuntimeV3.AlignedValue;
103
- type Op<T> = OnchainRuntimeV3.Op<T>;
104
-
105
129
  /**
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}).
130
+ * The result of one retained-era circuit call: every artifact
131
+ * {@link wrapKeepStateCall} (`../v9/wrap.ts`) needs to assemble a v9-native
132
+ * `ContractCallPrototype`.
111
133
  *
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.
134
+ * `preContractState` and `postContractState` are LIVE HANDLES from the retained
135
+ * runtime; every other member is plain data. `postContractStateEncoded` is the
136
+ * post-state's encoded form, carried beside the handle so a caller has a value
137
+ * that outlives the runtime instance.
202
138
  *
203
139
  * @see {@link RetainedEraExecution}
204
140
  */
205
141
  interface TranscriptPojo {
206
142
  readonly circuitId: string;
207
143
  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;
144
+ readonly input: ContractExecutable.ContractExecutable.ContractCallPrivate['input'];
145
+ readonly output: ContractExecutable.ContractExecutable.ContractCallPrivate['output'];
146
+ readonly publicTranscript: ContractExecutable.ContractExecutable.ContractCallPublic['publicTranscript'];
147
+ readonly privateTranscriptOutputs: ContractExecutable.ContractExecutable.ContractCallPrivate['privateTranscriptOutputs'];
148
+ /**
149
+ * The state the call BOUND to, i.e. `partitionInputs.state`.
150
+ *
151
+ * Indistinguishable from {@link TranscriptPojo.postContractState} BY TYPE:
152
+ * compact-js resolves both to the same declaration, so swapping the two is
153
+ * not a compile error. Partitioning against the wrong one rejects a state the
154
+ * transcript's reads do not fit, or silently mis-charges one that merely
155
+ * differs in value, so the distinction rests on the member name alone.
156
+ */
157
+ readonly preContractState: ContractExecutable.ContractExecutable.CallPartitionInputs['state'];
158
+ /**
159
+ * The state the call LEFT, i.e. compact-js's own `contractState`.
160
+ *
161
+ * See {@link TranscriptPojo.preContractState}: the two are the same type.
162
+ */
163
+ readonly postContractState: ContractExecutable.ContractExecutable.ContractCallPublic['contractState'];
214
164
  /**
215
165
  * The post-call state as an {@link EncodedStateValue}: the same value
216
166
  * {@link postContractState} holds, in the form that survives this process.
217
167
  *
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.
168
+ * `EncodedStateValue` is pinned identical across the retained and current
169
+ * runtimes, so this is the member era-agnostic code reads, and the one that
170
+ * can be persisted, cloned or sent to a worker.
223
171
  */
224
172
  readonly postContractStateEncoded: EncodedStateValue;
225
173
  readonly privateStateAfter: unknown;
226
174
  readonly partitionContext: PartitionContext;
227
- readonly zswapLocalState: ZswapLocalState;
175
+ readonly zswapLocalState: ContractExecutable.ContractExecutable.DeployResultPrivate<unknown>['zswapLocalState'];
176
+ }
177
+ /** The freshly built state a retained-era constructor produced. */
178
+ interface ConstructorResultPojo {
179
+ readonly contractState: ContractExecutable.ContractExecutable.DeployResultPublic['contractState'];
180
+ readonly privateState: unknown;
181
+ readonly zswapLocalState: ContractExecutable.ContractExecutable.DeployResultPrivate<unknown>['zswapLocalState'];
182
+ /**
183
+ * The key the state's maintenance authority was built from: the caller's own
184
+ * when one was named, otherwise the one compact-js sampled.
185
+ *
186
+ * REQUIRED, unlike the request's, and that asymmetry is the point — a sampled
187
+ * key exists nowhere else, so a result that did not report it would leave the
188
+ * deployment as unmaintainable as the empty committee the retained
189
+ * constructor writes on its own.
190
+ */
191
+ readonly signingKey: Ledger8SigningKey;
228
192
  }
229
- /** Everything {@link executeCircuit} needs to run one impure circuit call. */
230
- interface ExecuteCircuitOptions {
231
- readonly contract: Ledger8ContractLike;
193
+ /**
194
+ * Reads verifier keys for the entry points a contract declares.
195
+ *
196
+ * A plain function rather than compact-js's `ZKConfiguration` service, so no
197
+ * Effect type reaches this package's callers; {@link zkConfigurationLayer}
198
+ * adapts it at the seam.
199
+ */
200
+ type VerifierKeyReader = (provableCircuitId: string) => Promise<Uint8Array | undefined>;
201
+ /** Everything {@link runRetainedCircuit} needs to run one circuit. */
202
+ interface RunRetainedCircuitOptions<C extends RetainedContract, PS> {
203
+ readonly contract: C;
232
204
  readonly circuitId: string;
233
205
  readonly args: readonly unknown[];
234
- readonly state: DownConvertedState;
206
+ /**
207
+ * The contract state the call runs against, DECODED — the primary state in
208
+ * its era-neutral encoded form, with the balances the contract holds beside
209
+ * it.
210
+ *
211
+ * Era-neutral rather than one era's serialized bytes, because a contract that
212
+ * an earlier post-fork call has already migrated carries a CURRENT-era
213
+ * envelope while still executing on the retained runtime. Chain bytes would
214
+ * have to be decoded by the era that wrote them; this shape is what both
215
+ * envelopes' readers produce, so one path serves a pre-fork contract and a
216
+ * migrated one alike.
217
+ *
218
+ * The balance travels on the same value, so the two halves cannot come off
219
+ * different reads. {@link executableStateFrom} writes it onto a whole
220
+ * `ContractState`, which is the only form the retained runtime reads a
221
+ * balance from — handed a bare state value it substitutes an empty map, and a
222
+ * circuit reading a balance then executes against nothing with every guard
223
+ * green. That substitution is what #1345 was.
224
+ */
225
+ readonly contractState: ContractStatePojo;
235
226
  readonly address: string;
236
227
  readonly coinPk: string;
237
- readonly privateState: unknown;
228
+ readonly privateState: PS;
229
+ /**
230
+ * The execution clock, in SECONDS since the epoch. Omitted, the wall clock is
231
+ * used.
232
+ *
233
+ * Lands on the query context's `block.secondsSinceEpoch`, which
234
+ * {@link TranscriptPojo.partitionContext} reports — so without pinning it a
235
+ * recorded fixture differs on every run
236
+ * (midnightntwrk/midnight-sdk#403).
237
+ */
238
+ readonly nowSeconds?: number;
239
+ }
240
+ /** Everything {@link runRetainedConstructor} needs to run one constructor. */
241
+ interface RunRetainedConstructorOptions<C extends RetainedContract, PS> {
242
+ readonly contract: C;
243
+ readonly args: readonly unknown[];
244
+ readonly privateState: PS;
245
+ readonly coinPk: string;
246
+ /**
247
+ * The key the contract's maintenance authority is built from. Optional:
248
+ * compact-js samples one when it is absent, and the result reports whichever
249
+ * was used.
250
+ */
251
+ readonly signingKey?: Ledger8SigningKey;
252
+ /** Reads the verifier key for each entry point the constructor registers. */
253
+ readonly verifierKeys: VerifierKeyReader;
238
254
  }
239
255
 
240
256
  /**
@@ -262,75 +278,48 @@ interface WrapKeepStateCallOptions {
262
278
  }
263
279
 
264
280
  /**
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.
281
+ * The public surface {@link createLedger8Engine} builds: the retained pre-fork
282
+ * EXECUTION capabilities.
269
283
  *
270
- * Crosses the era boundary by bytes, not by handle.
284
+ * The two execution members are ASYNCHRONOUS. They used to be synchronous, and
285
+ * the docblock here used to promise it: compact-js builds a circuit call on
286
+ * `Effect.tryPromise`, so `runSync` cannot discharge it and the promise is not
287
+ * satisfiable. Nothing else about the surface changed shape -- every value
288
+ * crossing it is still plain data or an era handle, and no `Effect` reaches a
289
+ * caller.
271
290
  *
272
- * @see {@link DualInstantiationGuard} for why that crossing is the one a
273
- * duplicate install cannot affect
274
291
  * @see {@link EraSeam}
275
292
  */
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;
293
+ interface Ledger8Engine {
281
294
  /**
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.
295
+ * Runs one circuit against the decoded contract state the chain serves.
296
+ *
297
+ * Takes the state and the balances beside it as ONE value -- see
298
+ * {@link RunRetainedCircuitOptions.contractState} -- because the balances a
299
+ * circuit reads do not live inside the primary state, and two separate
300
+ * options could describe two different blocks.
301
+ *
302
+ * @throws DownConvertFailedError At stage `'state down-convert'` when the
303
+ * state cannot be decoded, does not re-encode to its source, or carries a
304
+ * balance the retained runtime cannot read.
305
+ * @throws Error When the contract declares no circuit of that name.
285
306
  */
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;
307
+ executeCircuit<C extends RetainedContract, PS>(options: RunRetainedCircuitOptions<C, PS>): Promise<TranscriptPojo>;
311
308
  /**
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.
309
+ * Runs one retained-era constructor and returns the state it built.
310
+ *
311
+ * ASYNCHRONOUS for the same reason {@link Ledger8Engine.executeCircuit} is.
312
+ * Stricter than the leg it replaced: compact-js registers a verifier key
313
+ * against every declared entry point and refuses a missing one, where the
314
+ * hand-written constructor left every slot blank.
315
+ *
316
+ * @throws ComposeOptionError Naming option `'signingKey'` when a supplied key
317
+ * is not the 32 bytes of hex the retained runtime reads.
318
+ * @throws ComposeFailedError At stage `'deploy-verifier-key-blob'` when the
319
+ * ledger refuses the bytes served for a circuit.
315
320
  */
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;
321
+ executeConstructor<C extends RetainedContract, PS>(options: RunRetainedConstructorOptions<C, PS>): Promise<ConstructorResultPojo>;
332
322
  wrapKeepStateCall(options: WrapKeepStateCallOptions): ContractCallPrototype;
333
- executeConstructor(options: ExecuteConstructorOptions): ConstructorResultPojo;
334
323
  /**
335
324
  * Re-expresses a retained-era contract's entry points as a current-era contract state, so a
336
325
  * keep-state call has an operation registry the current composer can read.
@@ -341,27 +330,28 @@ interface Ledger8Engine {
341
330
  reexpressOperationsForCurrentEra(entryPoints: readonly ContractEntryPointPojo[]): Uint8Array;
342
331
  }
343
332
  /**
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.
333
+ * Builds a {@link Ledger8Engine}.
334
+ *
335
+ * The retained toolchain is no longer acquired here: compact-js's era-pinned
336
+ * ledger-8 entries own it, and `lib/v8/executable.ts` reaches them. What this
337
+ * function still does is sit behind the dynamic `import('../../engine.js')` in
338
+ * `lib/v8/load-engine.ts`, so importing the package root never pulls the
339
+ * multi-megabyte retained WASM onto the module graph.
340
+ *
341
+ * It no longer runs a dual-instantiation guard either. That guard compared
342
+ * `onchain-runtime-v3` against the 0.16 glue at construction time. Both
343
+ * acquisition paths are still live — `lib/v8/executable.ts` imports the glue
344
+ * directly, and compact-js resolves the same specifier for itself — so the axis
345
+ * did not go away; only the runtime check did. The invariant is held at install
346
+ * time instead, by `src/test/single-instance.test.ts`, which pins one resolved
347
+ * copy of `onchain-runtime-v3` and of each compact-runtime era line. That is a
348
+ * check on THIS repo's lockfile, not on a consumer's process.
349
+ *
350
+ * @returns The engine surface.
360
351
  * @see {@link EraSeam}
361
- * @see {@link DualInstantiationGuard}
362
352
  * @see {@link ModuleGraphAndLazyLoading}
363
353
  */
364
354
  declare const createLedger8Engine: () => Promise<Ledger8Engine>;
365
355
 
366
356
  export { createLedger8Engine };
367
- export type { ConstructorResultPojo, ContractEntryPointPojo, DownConvertedState, ExecuteCircuitOptions, ExecuteConstructorOptions, Ledger8ChargedState, Ledger8DeployableContractState, Ledger8Engine, Ledger8StateValue, TranscriptPojo, WrapKeepStateCallOptions };
357
+ export type { ConstructorResultPojo, ContractBalance, ContractEntryPointPojo, ContractStatePojo, Ledger8Engine, Ledger8SigningKey, RetainedContract, RunRetainedCircuitOptions, RunRetainedConstructorOptions, TranscriptPojo, VerifierKeyReader, WrapKeepStateCallOptions };