11import type { Backend , BackendKind , CircuitKind } from '../backend/Backend.js' ;
22import { DryBackend , type SyncSimulator } from '../backend/DryBackend.js' ;
3+ import { PrivateStateMutator } from '../core/PrivateStateMutator.js' ;
34import type { LiveContext } from '../live/LiveContext.js' ;
45import { getRegisteredLiveBackend } from '../live/registry.js' ;
56import { Signers } from '../signers/Signers.js' ;
@@ -147,6 +148,12 @@ export function createSimulator<
147148 readonly _backend : Backend < P , L > ;
148149 readonly _signers : Signers ;
149150
151+ /**
152+ * Serializes private-state read-modify-write for this instance. Internal;
153+ * see {@link PrivateStateMutator} for the scope of the guarantee.
154+ */
155+ readonly _mutator : PrivateStateMutator < P > ;
156+
150157 /** Async circuit proxies; every call returns a promise. */
151158 readonly circuits : {
152159 pure : AsyncCircuits < ExtractPureCircuits < TContract > , P > ;
@@ -163,6 +170,12 @@ export function createSimulator<
163170 this . _backend = deps . backend ;
164171 this . backendKind = deps . backend . kind ;
165172 this . _signers = deps . signers ;
173+ // Constructed here (not as a field initializer) so the closures capture
174+ // the assigned `_backend` rather than an undefined one.
175+ this . _mutator = new PrivateStateMutator < P > (
176+ ( ) => this . _backend . getPrivateState ( ) ,
177+ ( next ) => this . _backend . setPrivateState ( next ) ,
178+ ) ;
166179 this . circuits = {
167180 pure : buildProxy (
168181 this . _backend ,
@@ -241,14 +254,37 @@ export function createSimulator<
241254 }
242255
243256 /**
244- * Replaces the private state (for per-module secret/nonce injection helpers).
245- * Dry mutates the in-memory context; live throws (mutation asymmetry) —
246- * guard such specs with `isLiveBackend()`.
257+ * Replaces the whole private state. Dry mutates the in-memory context; live
258+ * writes to the harness's private-state provider so the next impure call
259+ * proves against it (throws if the `LiveContext` opted out of mutation).
260+ * Serialized against other mutations via {@link _mutator}.
247261 *
248262 * @param privateState - The new private state.
249263 */
250- setPrivateState ( privateState : P ) : void {
251- this . _backend . setPrivateState ( privateState ) ;
264+ setPrivateState ( privateState : P ) : Promise < void > {
265+ return this . _mutator . set ( privateState ) ;
266+ }
267+
268+ /**
269+ * Ergonomic granular private-state mutation. Replaces the per-module
270+ * `injectSecretKey`/`injectSecretNonce` helpers: a plain object shallow-
271+ * merges onto the current state, a function receives the current state and
272+ * returns the next.
273+ *
274+ * The read-modify-write is serialized (see {@link _mutator}) and resolves to
275+ * the state that was written, so callers can `return sim.updatePrivateState(...)`
276+ * without a follow-up `getPrivateState()`. Works on both dry (in-memory) and
277+ * live (provider read then write); on live the current state must already
278+ * exist (it is seeded at deploy).
279+ *
280+ * @example sim.updatePrivateState({ secretKey });
281+ * @example sim.updatePrivateState((prev) => ({ ...prev, counter: prev.counter + 1n }));
282+ *
283+ * @param updater - A partial patch to merge, or an updater function.
284+ * @returns The private state that was written.
285+ */
286+ updatePrivateState ( updater : Partial < P > | ( ( prev : P ) => P ) ) : Promise < P > {
287+ return this . _mutator . update ( updater ) ;
252288 }
253289
254290 /** The raw contract state value. */
0 commit comments