diff --git a/packages/simulator/package.json b/packages/simulator/package.json
index 822fb3f..2a53117 100644
--- a/packages/simulator/package.json
+++ b/packages/simulator/package.json
@@ -36,10 +36,13 @@
"scripts": {
"build": "tsc -p .",
"types": "tsc -p tsconfig.json --noEmit",
- "test": "yarn vitest run",
+ "test": "MIDNIGHT_BACKEND=dry vitest run",
+ "test:live": "MIDNIGHT_BACKEND=live vitest run",
"clean": "git clean -fXd"
},
"devDependencies": {
+ "@midnight-ntwrk/midnight-js-contracts": "^4.1.0",
+ "@midnight-ntwrk/midnight-js-types": "^4.1.0",
"@tsconfig/node24": "^24.0.3",
"@types/node": "25.9.1",
"fast-check": "^4.5.2",
@@ -49,5 +52,17 @@
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@midnight-ntwrk/ledger-v8": "8.1.0"
+ },
+ "peerDependencies": {
+ "@midnight-ntwrk/midnight-js-contracts": "^4.1.0",
+ "@midnight-ntwrk/midnight-js-types": "^4.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@midnight-ntwrk/midnight-js-contracts": {
+ "optional": true
+ },
+ "@midnight-ntwrk/midnight-js-types": {
+ "optional": true
+ }
}
}
diff --git a/packages/simulator/src/backend/Backend.ts b/packages/simulator/src/backend/Backend.ts
new file mode 100644
index 0000000..869c3e6
--- /dev/null
+++ b/packages/simulator/src/backend/Backend.ts
@@ -0,0 +1,117 @@
+import type { StateValue } from '@midnight-ntwrk/compact-runtime';
+
+/**
+ * Identifies which execution backend a simulator is bound to.
+ *
+ * Resolved once at construction time and fixed for the simulator's lifetime.
+ * There is no runtime toggle: a `'dry'` simulator never becomes
+ * `'live'` or vice versa.
+ */
+export type BackendKind = 'dry' | 'live';
+
+/**
+ * Whether a circuit runs locally on the JS artifact (`'pure'`) or, in live mode,
+ * is submitted as a transaction to the node (`'impure'`).
+ *
+ * Locality follows the pure/impure distinction, NOT read/write (D2):
+ * a read implemented as an impure circuit (e.g. `owner()`) still hits the node
+ * in live mode.
+ */
+export type CircuitKind = 'pure' | 'impure';
+
+/**
+ * The execution seam that genuinely differs between the in-memory simulator and
+ * a live Midnight node.
+ *
+ * `createBackendSimulator` builds the async circuit proxies, caller helpers, and
+ * state getters on top of this interface; the backend itself stays dumb. Every
+ * operation is async so spec code is uniform `await` across both backends:
+ * {@link DryBackend} wraps its synchronous results in `Promise.resolve`,
+ * the live adapter awaits the network.
+ *
+ * @template P - Private state type.
+ * @template L - Public ledger state type.
+ */
+export interface Backend
{
+ /** The backend this instance is bound to. */
+ readonly kind: BackendKind;
+
+ /** The deployed contract's address. */
+ readonly contractAddress: string;
+
+ /**
+ * Invokes a circuit. Pure circuits run locally on the JS artifact in both
+ * modes; impure circuits run locally in dry and submit a tx in live (D2).
+ * The live adapter normalizes the result to the bare `R` that dry
+ * returns, so an assertion on the return value is identical across
+ * backends.
+ *
+ * @param kind - Whether the circuit is pure or impure.
+ * @param name - The circuit name.
+ * @param args - The circuit arguments.
+ * @returns The bare circuit result `R`, normalized to match dry.
+ */
+ call(kind: CircuitKind, name: string, args: unknown[]): Promise;
+
+ /**
+ * Extracts the public ledger state. Both backends apply the same
+ * `ledgerExtractor` — over the in-memory context in dry, over the
+ * indexer-sourced state in live.
+ */
+ getPublicState(): Promise;
+
+ /**
+ * Reads the private state `P`. Read parity holds across backends;
+ * mutation parity does not (see {@link overrideWitness} and the private-state
+ * mutation asymmetry documented on the live adapter).
+ */
+ getPrivateState(): Promise;
+
+ /** Returns the raw contract `StateValue` (the input to `ledgerExtractor`). */
+ getContractState(): Promise;
+
+ /**
+ * Replaces the private state. Dry mutates the in-memory context (used by
+ * per-module helpers like secret/nonce injection); live throws, because
+ * mid-test private-state mutation is the documented dry↔live asymmetry.
+ * Guard such specs with `isLiveBackend()`.
+ *
+ * @param privateState - The new private state `P`.
+ */
+ setPrivateState(privateState: P): void;
+
+ /**
+ * Sets the caller identity for subsequent circuit calls.
+ *
+ * The mode lifecycle matches across backends: `'single'` applies the
+ * caller to the next call then reverts to the default signer; `'persistent'`
+ * keeps it until changed. `null` clears the override (default signer).
+ *
+ * @param alias - The caller alias (e.g. `'OWNER'`), or `null` for the default signer.
+ * @param mode - `'single'` (one call) or `'persistent'` (until changed).
+ */
+ setCaller(alias: string | null, mode: 'single' | 'persistent'): void;
+
+ /**
+ * Replaces a single witness implementation.
+ *
+ * Dry recreates the contract with the new witness; the live adapter throws
+ * `"witness override unsupported on live backend"` because witnesses bind at
+ * deploy and cannot be swapped mid-test.
+ *
+ * @param key - The witness key to override.
+ * @param fn - The new witness implementation.
+ */
+ overrideWitness(key: PropertyKey, fn: unknown): void;
+
+ /**
+ * Replaces the whole witness set. Dry recreates the contract; the live adapter
+ * throws the same message as {@link overrideWitness}.
+ *
+ * @param witnesses - The new witness set.
+ */
+ setWitnesses(witnesses: unknown): void;
+
+ /** Returns the current witness set (read parity; live reads the local set). */
+ getWitnesses(): unknown;
+}
diff --git a/packages/simulator/src/backend/DryBackend.ts b/packages/simulator/src/backend/DryBackend.ts
new file mode 100644
index 0000000..772758e
--- /dev/null
+++ b/packages/simulator/src/backend/DryBackend.ts
@@ -0,0 +1,129 @@
+import type {
+ CoinPublicKey,
+ StateValue,
+} from '@midnight-ntwrk/compact-runtime';
+import type { Signers } from '../signers/Signers.js';
+import type { Backend, BackendKind, CircuitKind } from './Backend.js';
+
+/**
+ * The slice of a synchronous `createSimulator` instance that {@link DryBackend}
+ * drives. Kept structural so the dry backend reuses the existing simulator
+ * machinery without coupling to its concrete (anonymous) class.
+ *
+ * @template P - Private state type.
+ * @template L - Public ledger state type.
+ */
+export interface SyncSimulator {
+ readonly contractAddress: string;
+ callerOverride: CoinPublicKey | null;
+ persistentCallerOverride: CoinPublicKey | null;
+ readonly circuits: {
+ pure: Record unknown>;
+ impure: Record unknown>;
+ };
+ getPublicState(): L;
+ getPrivateState(): P;
+ getContractState(): StateValue;
+ overrideWitness(key: PropertyKey, fn: unknown): void;
+ witnesses: unknown;
+ readonly circuitContextManager: { updatePrivateState(privateState: P): void };
+}
+
+/**
+ * The in-memory backend: a thin async facade over the existing synchronous
+ * `createSimulator` instance.
+ *
+ * Every operation delegates to the wrapped simulator and wraps the synchronous
+ * result in a resolved promise, so a circuit never returns a bare value
+ * on dry but a `Promise` on live. Because all real work routes through the
+ * unchanged synchronous path, dry behavior is preserved byte-for-byte
+ * and is the parity reference the live backend is measured against.
+ *
+ * @template P - Private state type.
+ * @template L - Public ledger state type.
+ */
+export class DryBackend implements Backend
{
+ readonly kind: BackendKind = 'dry';
+
+ private readonly sim: SyncSimulator
;
+ private readonly signers: Signers;
+
+ /**
+ * @param sim - The wrapped synchronous simulator instance.
+ * @param signers - Resolver used to turn caller aliases into deterministic keys.
+ */
+ constructor(sim: SyncSimulator
, signers: Signers) {
+ this.sim = sim;
+ this.signers = signers;
+ }
+
+ get contractAddress(): string {
+ return this.sim.contractAddress;
+ }
+
+ /**
+ * Runs a circuit on the in-memory contract. Accessing `circuits.{pure,impure}`
+ * fresh on each call means a witness override (which rebuilds the wrapped
+ * simulator's proxies) is picked up transparently.
+ */
+ async call(
+ kind: CircuitKind,
+ name: string,
+ args: unknown[],
+ ): Promise {
+ const proxy =
+ kind === 'pure' ? this.sim.circuits.pure : this.sim.circuits.impure;
+ const fn = proxy[name];
+ if (typeof fn !== 'function') {
+ throw new Error(`unknown ${kind} circuit "${name}"`);
+ }
+ return fn(...args);
+ }
+
+ async getPublicState(): Promise {
+ return this.sim.getPublicState();
+ }
+
+ async getPrivateState(): Promise {
+ return this.sim.getPrivateState();
+ }
+
+ async getContractState(): Promise {
+ return this.sim.getContractState();
+ }
+
+ /** Mutates the in-memory private state (dry supports mid-test mutation). */
+ setPrivateState(privateState: P): void {
+ this.sim.circuitContextManager.updatePrivateState(privateState);
+ }
+
+ /**
+ * Resolves the alias to a deterministic key and applies it to the wrapped
+ * simulator's override fields. `'single'` uses `callerOverride` (the existing
+ * proxy auto-resets it after one call); `'persistent'` uses
+ * `persistentCallerOverride`.
+ */
+ setCaller(alias: string | null, mode: 'single' | 'persistent'): void {
+ const key = alias === null ? null : this.signers.resolveDryKey(alias);
+ if (mode === 'persistent') {
+ this.sim.persistentCallerOverride = key;
+ } else {
+ this.sim.callerOverride = key;
+ }
+ }
+
+ /** Delegates to the wrapped simulator, which recreates the contract (dry supports this). */
+ overrideWitness(key: PropertyKey, fn: unknown): void {
+ this.sim.overrideWitness(key, fn);
+ }
+
+ /** Delegates to the wrapped simulator's witness setter (dry supports this). */
+ setWitnesses(witnesses: unknown): void {
+ this.sim.witnesses = witnesses;
+ }
+
+ /** Returns the wrapped simulator's current witnesses. */
+ getWitnesses(): unknown {
+ return this.sim.witnesses;
+ }
+}
diff --git a/packages/simulator/src/factory/SimulatorConfig.ts b/packages/simulator/src/factory/SimulatorConfig.ts
index bd11aaa..6186ee4 100644
--- a/packages/simulator/src/factory/SimulatorConfig.ts
+++ b/packages/simulator/src/factory/SimulatorConfig.ts
@@ -36,4 +36,11 @@ export interface SimulatorConfig<
ledgerExtractor: (state: StateValue) => L;
/** Factory function to create default witnesses */
witnessesFactory: () => W;
+ /**
+ * Optional artifact name (the `artifacts//` directory) for the compiled
+ * contract. Dry ignores it; the live backend's registered harness uses it to
+ * locate the compiled assets + ZK keys and to build the deployable contract.
+ * Only needed for modules that run on the live backend.
+ */
+ artifactName?: string;
}
diff --git a/packages/simulator/src/factory/createDrySimulator.ts b/packages/simulator/src/factory/createDrySimulator.ts
new file mode 100644
index 0000000..5e65dd5
--- /dev/null
+++ b/packages/simulator/src/factory/createDrySimulator.ts
@@ -0,0 +1,199 @@
+import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
+import { dummyContractAddress } from '@midnight-ntwrk/compact-runtime';
+import { CircuitContextManager } from '../core/CircuitContextManager.js';
+import { ContractSimulator } from '../core/ContractSimulator.js';
+import type { IMinimalContract } from '../types/Contract.js';
+import type {
+ ContextlessCircuits,
+ ExtractImpureCircuits,
+ ExtractPureCircuits,
+} from '../types/index.js';
+import type { BaseSimulatorOptions } from '../types/Options.js';
+import type { SimulatorConfig } from './SimulatorConfig.js';
+
+/**
+ * Internal synchronous simulator primitive.
+ *
+ * This is the in-memory engine the public async {@link createSimulator} builds on:
+ * the dry backend wraps an instance of this class, and the live backend uses one
+ * locally to evaluate pure circuits. It is not the public testing API —
+ * use {@link createSimulator} instead.
+ *
+ * Creates a class extending ContractSimulator with witness management, state
+ * management, circuit proxy creation, and options handling.
+ *
+ * @param config - Configuration object defining how to create and manage the simulator
+ * @returns A class constructor that can be extended to create specific simulators
+ */
+export function createDrySimulator<
+ P,
+ L,
+ W,
+ TContract extends IMinimalContract,
+ TArgs extends readonly any[] = readonly any[],
+>(config: SimulatorConfig) {
+ return class GeneratedSimulator extends ContractSimulator
{
+ contract: TContract;
+ readonly contractAddress: string;
+ public _witnesses: W;
+
+ /**
+ * Creates a new simulator instance with explicit contract args and options
+ */
+ constructor(
+ contractArgs: TArgs = [] as any,
+ options: BaseSimulatorOptions
= {},
+ ) {
+ super();
+
+ const {
+ privateState = config.defaultPrivateState(),
+ witnesses = config.witnessesFactory(),
+ coinPK = '0'.repeat(64),
+ contractAddress = dummyContractAddress(),
+ } = options;
+
+ this._witnesses = witnesses;
+ this.contract = config.contractFactory(this._witnesses);
+
+ const processedArgs = config.contractArgs(...contractArgs);
+
+ this.circuitContextManager = new CircuitContextManager(
+ this.contract,
+ privateState,
+ coinPK,
+ contractAddress,
+ ...processedArgs,
+ );
+
+ this.contractAddress = this.circuitContext.currentQueryContext.address;
+ }
+
+ public _pureCircuitProxy?: ContextlessCircuits<
+ ExtractPureCircuits,
+ P
+ >;
+ public _impureCircuitProxy?: ContextlessCircuits<
+ ExtractImpureCircuits,
+ P
+ >;
+
+ /**
+ * Gets the pure circuit proxy, creating it lazily if it doesn't exist.
+ *
+ * @returns The pure circuit proxy for executing read-only contract methods
+ */
+ public get pureCircuit(): ContextlessCircuits<
+ ExtractPureCircuits,
+ P
+ > {
+ if (!this._pureCircuitProxy) {
+ this._pureCircuitProxy = this.createPureCircuitProxy(
+ this.contract.circuits as ExtractPureCircuits,
+ () => this.circuitContext,
+ );
+ }
+ return this._pureCircuitProxy;
+ }
+
+ /**
+ * Gets the impure circuit proxy, creating it lazily if it doesn't exist.
+ *
+ * @returns The impure circuit proxy for executing state-modifying contract methods
+ */
+ public get impureCircuit(): ContextlessCircuits<
+ ExtractImpureCircuits,
+ P
+ > {
+ if (!this._impureCircuitProxy) {
+ this._impureCircuitProxy = this.createImpureCircuitProxy(
+ this.contract.impureCircuits as ExtractImpureCircuits,
+ () => this.getCallerContext(),
+ (ctx) => {
+ this.circuitContext = ctx;
+ },
+ );
+ }
+ return this._impureCircuitProxy;
+ }
+
+ /**
+ * Gets both pure and impure circuit proxies.
+ *
+ * @returns Object containing both pure and impure circuit proxies
+ */
+ public get circuits() {
+ return {
+ pure: this.pureCircuit,
+ impure: this.impureCircuit,
+ };
+ }
+
+ /**
+ * Resets cached circuit proxies, forcing re-initialization on next access.
+ */
+ public resetCircuitProxies(): void {
+ this._pureCircuitProxy = undefined;
+ this._impureCircuitProxy = undefined;
+ }
+
+ /**
+ * Extracts the public ledger state from the current contract state.
+ *
+ * @returns The current public state of the contract
+ */
+ getPublicState(): L {
+ return config.ledgerExtractor(
+ this.circuitContext.currentQueryContext.state.state,
+ );
+ }
+
+ // Common witness management methods
+ /**
+ * Gets the current witness functions.
+ *
+ * @returns The current witness function implementations
+ */
+ public get witnesses(): W {
+ return this._witnesses;
+ }
+
+ /**
+ * Sets new witness functions and recreates the contract with them.
+ *
+ * @param newWitnesses - The new witness function implementations to use
+ */
+ public set witnesses(newWitnesses: W) {
+ this._witnesses = newWitnesses;
+ this.contract = config.contractFactory(this._witnesses);
+ this.resetCircuitProxies();
+ }
+
+ /**
+ * Overrides a specific witness function while keeping others unchanged.
+ *
+ * @param key - The key of the witness function to override
+ * @param fn - The new implementation for the witness function
+ */
+ public overrideWitness(key: K, fn: W[K]) {
+ this.witnesses = {
+ ...this._witnesses,
+ [key]: fn,
+ } as W;
+ }
+
+ /**
+ * Gets the current witness context with the proper structure for witness function calls.
+ *
+ * @returns The current witness context that can be passed to witness functions
+ */
+ public getWitnessContext(): WitnessContext {
+ const circuitCtx = this.circuitContext;
+ return {
+ ledger: this.getPublicState(),
+ privateState: circuitCtx.currentPrivateState,
+ contractAddress: circuitCtx.currentQueryContext.address,
+ };
+ }
+ };
+}
diff --git a/packages/simulator/src/factory/createSimulator.ts b/packages/simulator/src/factory/createSimulator.ts
index 98c83f0..379408b 100644
--- a/packages/simulator/src/factory/createSimulator.ts
+++ b/packages/simulator/src/factory/createSimulator.ts
@@ -1,28 +1,50 @@
-import type { WitnessContext } from '@midnight-ntwrk/compact-runtime';
-import { dummyContractAddress } from '@midnight-ntwrk/compact-runtime';
-import { CircuitContextManager } from '../core/CircuitContextManager.js';
-import { ContractSimulator } from '../core/ContractSimulator.js';
+import type { Backend, BackendKind, CircuitKind } from '../backend/Backend.js';
+import { DryBackend, type SyncSimulator } from '../backend/DryBackend.js';
+import type { LiveContext } from '../live/LiveContext.js';
+import { getRegisteredLiveBackend } from '../live/registry.js';
+import { Signers } from '../signers/Signers.js';
import type { IMinimalContract } from '../types/Contract.js';
import type {
- ContextlessCircuits,
+ AsyncCircuits,
ExtractImpureCircuits,
ExtractPureCircuits,
} from '../types/index.js';
-import type { BaseSimulatorOptions } from '../types/Options.js';
+import type { SimulatorOptions } from '../types/Options.js';
+import { createDrySimulator } from './createDrySimulator.js';
import type { SimulatorConfig } from './SimulatorConfig.js';
+/** Prepared backend wiring handed to the simulator constructor. */
+interface BackendDeps {
+ backend: Backend
;
+ signers: Signers;
+ pureNames: string[];
+ impureNames: string[];
+}
+
+/**
+ * Resolves the backend kind once: an explicit override wins, otherwise
+ * `MIDNIGHT_BACKEND=live` selects live and anything else (unset or `dry`)
+ * selects dry.
+ */
+const resolveBackendKind = (override?: BackendKind): BackendKind =>
+ override ?? (process.env.MIDNIGHT_BACKEND === 'live' ? 'live' : 'dry');
+
/**
- * Factory function to create simulator classes with consistent boilerplate elimination.
+ * Creates a backend-aware simulator class for a contract.
+ *
+ * One factory, two backends: the produced class runs against the in-memory path
+ * ({@link DryBackend}) or a live Midnight node (`LiveBackend`), selected by
+ * `MIDNIGHT_BACKEND=dry|live` at construction. `create` is async and
+ * circuits return promises ({@link AsyncCircuits}) so a single spec file runs on
+ * both backends with uniform `await`.
*
- * This factory creates a class that extends ContractSimulator with all the common
- * functionality needed for contract simulation, including:
- * - Witness management
- * - State management
- * - Circuit proxy creation
- * - Options handling
+ * The live adapter is reached only through a runtime dynamic import: a static
+ * `import { createSimulator }` never pulls midnight-js into the
+ * dependency graph. In live mode the {@link LiveContext} comes from `options.live`
+ * or the globally registered live backend (`registerLiveBackend`).
*
- * @param config - Configuration object defining how to create and manage the simulator
- * @returns A class constructor that can be extended to create specific simulators
+ * @param config - The shared simulator configuration (same shape both backends).
+ * @returns A class to extend with per-circuit delegating methods.
*/
export function createSimulator<
P,
@@ -31,168 +53,239 @@ export function createSimulator<
TContract extends IMinimalContract,
TArgs extends readonly any[] = readonly any[],
>(config: SimulatorConfig
) {
- return class GeneratedSimulator extends ContractSimulator
{
- contract: TContract;
- readonly contractAddress: string;
- public _witnesses: W;
+ // Built once per factory; instances per `create()`. The synchronous primitive
+ // is the whole dry path and the local JS artifact for pure-circuit eval.
+ const DrySimClass = createDrySimulator
(config);
- /**
- * Creates a new simulator instance with explicit contract args and options
- */
- constructor(
- contractArgs: TArgs = [] as any,
- options: BaseSimulatorOptions
= {},
- ) {
- super();
-
- const {
- privateState = config.defaultPrivateState(),
- witnesses = config.witnessesFactory(),
- coinPK = '0'.repeat(64),
- contractAddress = dummyContractAddress(),
- } = options;
-
- this._witnesses = witnesses;
- this.contract = config.contractFactory(this._witnesses);
-
- const processedArgs = config.contractArgs(...contractArgs);
-
- this.circuitContextManager = new CircuitContextManager(
- this.contract,
- privateState,
- coinPK,
- contractAddress,
- ...processedArgs,
- );
-
- this.contractAddress = this.circuitContext.currentQueryContext.address;
- }
-
- public _pureCircuitProxy?: ContextlessCircuits<
- ExtractPureCircuits,
- P
- >;
- public _impureCircuitProxy?: ContextlessCircuits<
- ExtractImpureCircuits,
- P
- >;
+ /**
+ * Builds an async circuit proxy: each name becomes a function that routes to
+ * `backend.call(kind, name, args)`, returning the bare `R` as a promise.
+ */
+ const buildProxy = (
+ backend: Backend,
+ kind: CircuitKind,
+ names: string[],
+ ): Record Promise> => {
+ const proxy: Record Promise> = {};
+ for (const name of names) {
+ proxy[name] = (...args: unknown[]) => backend.call(kind, name, args);
+ }
+ return proxy;
+ };
- /**
- * Gets the pure circuit proxy, creating it lazily if it doesn't exist.
- *
- * @returns The pure circuit proxy for executing read-only contract methods
- */
- public get pureCircuit(): ContextlessCircuits<
- ExtractPureCircuits,
- P
- > {
- if (!this._pureCircuitProxy) {
- this._pureCircuitProxy = this.createPureCircuitProxy(
- this.contract.circuits as ExtractPureCircuits,
- () => this.circuitContext,
+ /** Resolves backend selection, builds the backend, and derives circuit names. */
+ const prepareBackend = async (
+ contractArgs: TArgs,
+ options: SimulatorOptions,
+ ): Promise> => {
+ const kind = resolveBackendKind(options.backend);
+
+ // The local synchronous simulator: the whole dry path, and the pure-circuit
+ // evaluator in live (D2). In live this runs `initialState` in memory only —
+ // it is never deployed on-chain.
+ const localSim = new DrySimClass(contractArgs, options);
+ const contract = localSim.contract;
+ const impureNames = Object.keys(contract.impureCircuits);
+ const impureSet = new Set(impureNames);
+ const pureNames = Object.keys(contract.circuits).filter(
+ (name) => !impureSet.has(name),
+ );
+
+ if (kind === 'live') {
+ const signers = new Signers({
+ mode: 'live',
+ liveAliases: options.liveAliases,
+ resolveLiveKey: options.resolveLiveKey,
+ });
+
+ // Prefer an explicit ctx; otherwise the globally registered live backend.
+ let liveCtx = options.live;
+ if (!liveCtx) {
+ const factory = getRegisteredLiveBackend();
+ if (factory) {
+ liveCtx = (await factory({
+ config,
+ contractArgs,
+ options,
+ })) as LiveContext;
+ }
+ }
+ if (!liveCtx) {
+ throw new Error(
+ 'live backend selected (MIDNIGHT_BACKEND=live) but no LiveContext available. ' +
+ 'Pass `{ live }` to create(), or call registerLiveBackend(...) in your ' +
+ 'test:live setup. The harness owns deploy/providers/wallets.',
);
}
- return this._pureCircuitProxy;
+
+ // The live adapter value is reached only via dynamic import,
+ // so a dry import never statically links it (and any future heavy deps).
+ const { LiveBackend } = await import('../live/LiveBackend.js');
+ const backend = new LiveBackend
({
+ ctx: liveCtx,
+ pureSim: localSim as unknown as SyncSimulator
,
+ signers,
+ ledgerExtractor: config.ledgerExtractor,
+ });
+ return { backend, signers, pureNames, impureNames };
}
+ const signers = new Signers({ mode: 'dry', dryKeys: options.signerKeys });
+ const backend = new DryBackend
(
+ localSim as unknown as SyncSimulator
,
+ signers,
+ );
+ return { backend, signers, pureNames, impureNames };
+ };
+
+ return class Simulator {
+ /** The backend this instance resolved to at construction. */
+ readonly backendKind: BackendKind;
+
+ // Public (underscore-prefixed) to satisfy declaration emit for the returned
+ // anonymous class; treat as internal.
+ readonly _backend: Backend
;
+ readonly _signers: Signers;
+
+ /** Async circuit proxies; every call returns a promise. */
+ readonly circuits: {
+ pure: AsyncCircuits, P>;
+ impure: AsyncCircuits, P>;
+ };
+
/**
- * Gets the impure circuit proxy, creating it lazily if it doesn't exist.
+ * Internal constructor. Use the async static {@link create} instead — it
+ * resolves the backend (including the live dynamic import) before construction.
*
- * @returns The impure circuit proxy for executing state-modifying contract methods
+ * @param deps - Prepared backend wiring from {@link prepareBackend}.
*/
- public get impureCircuit(): ContextlessCircuits<
- ExtractImpureCircuits,
- P
- > {
- if (!this._impureCircuitProxy) {
- this._impureCircuitProxy = this.createImpureCircuitProxy(
- this.contract.impureCircuits as ExtractImpureCircuits,
- () => this.getCallerContext(),
- (ctx) => {
- this.circuitContext = ctx;
- },
- );
- }
- return this._impureCircuitProxy;
+ constructor(deps: BackendDeps) {
+ this._backend = deps.backend;
+ this.backendKind = deps.backend.kind;
+ this._signers = deps.signers;
+ this.circuits = {
+ pure: buildProxy(
+ this._backend,
+ 'pure',
+ deps.pureNames,
+ ) as unknown as AsyncCircuits, P>,
+ impure: buildProxy(
+ this._backend,
+ 'impure',
+ deps.impureNames,
+ ) as unknown as AsyncCircuits, P>,
+ };
}
/**
- * Gets both pure and impure circuit proxies.
+ * Constructs a simulator. In dry, deploys from `contractArgs` to fresh
+ * in-memory state. In live, the caller already deployed; the args seed only
+ * the local pure-eval context, never an on-chain deploy.
*
- * @returns Object containing both pure and impure circuit proxies
+ * @param contractArgs - Constructor args for the contract.
+ * @param options - Backend selection, witnesses, private state, live world.
+ * @returns The constructed simulator (subclass-aware via `this`).
*/
- public get circuits() {
- return {
- pure: this.pureCircuit,
- impure: this.impureCircuit,
- };
+ static async create(
+ this: new (
+ deps: BackendDeps,
+ ) => T,
+ contractArgs: TArgs = [] as unknown as TArgs,
+ options: SimulatorOptions
= {},
+ ): Promise {
+ const deps = await prepareBackend(contractArgs, options);
+ return new this(deps);
+ }
+
+ /** The alias resolver for circuit-arg keys (`signers.eitherFor('OWNER')`). */
+ get signers(): Signers {
+ return this._signers;
}
/**
- * Resets cached circuit proxies, forcing re-initialization on next access.
+ * Sets the caller for the next call only, then reverts.
+ *
+ * @param alias - The caller alias, or `null` for the default signer.
+ * @returns This instance, for chaining (`sim.as('OWNER').transfer(...)`).
*/
- public resetCircuitProxies(): void {
- this._pureCircuitProxy = undefined;
- this._impureCircuitProxy = undefined;
+ as(alias: string | null): this {
+ this._backend.setCaller(alias, 'single');
+ return this;
}
/**
- * Extracts the public ledger state from the current contract state.
+ * Sets a persistent caller for all subsequent calls until changed.
*
- * @returns The current public state of the contract
+ * @param alias - The caller alias, or `null` to clear.
+ * @returns This instance, for chaining.
*/
- getPublicState(): L {
- return config.ledgerExtractor(
- this.circuitContext.currentQueryContext.state.state,
- );
+ setPersistentCaller(alias: string | null): this {
+ this._backend.setCaller(alias, 'persistent');
+ return this;
+ }
+
+ /** Clears the persistent caller (the single-shot caller self-resets per call). */
+ resetCaller(): this {
+ this._backend.setCaller(null, 'persistent');
+ return this;
+ }
+
+ /** The public ledger state, via the shared extractor. */
+ getPublicState(): Promise {
+ return this._backend.getPublicState();
+ }
+
+ /** The private state (read parity across backends). */
+ getPrivateState(): Promise {
+ return this._backend.getPrivateState();
}
- // Common witness management methods
/**
- * Gets the current witness functions.
+ * Replaces the private state (for per-module secret/nonce injection helpers).
+ * Dry mutates the in-memory context; live throws (mutation asymmetry) —
+ * guard such specs with `isLiveBackend()`.
*
- * @returns The current witness function implementations
+ * @param privateState - The new private state.
*/
- public get witnesses(): W {
- return this._witnesses;
+ setPrivateState(privateState: P): void {
+ this._backend.setPrivateState(privateState);
+ }
+
+ /** The raw contract state value. */
+ getContractState() {
+ return this._backend.getContractState();
+ }
+
+ /** The current witness set. */
+ get witnesses(): W {
+ return this._backend.getWitnesses() as W;
}
/**
- * Sets new witness functions and recreates the contract with them.
- *
- * @param newWitnesses - The new witness function implementations to use
+ * Replaces the whole witness set. Dry recreates the contract; live throws.
+ * Equivalent to {@link setWitnesses}; kept for API compatibility.
*/
- public set witnesses(newWitnesses: W) {
- this._witnesses = newWitnesses;
- this.contract = config.contractFactory(this._witnesses);
- this.resetCircuitProxies();
+ set witnesses(newWitnesses: W) {
+ this._backend.setWitnesses(newWitnesses);
}
/**
- * Overrides a specific witness function while keeping others unchanged.
+ * Overrides a single witness. Dry recreates the contract; live throws.
*
- * @param key - The key of the witness function to override
- * @param fn - The new implementation for the witness function
+ * @param key - The witness key.
+ * @param fn - The replacement implementation.
*/
- public overrideWitness(key: K, fn: W[K]) {
- this.witnesses = {
- ...this._witnesses,
- [key]: fn,
- } as W;
+ overrideWitness(key: K, fn: W[K]): void {
+ this._backend.overrideWitness(key as PropertyKey, fn);
}
/**
- * Gets the current witness context with the proper structure for witness function calls.
+ * Replaces the whole witness set. Dry recreates the contract; live throws.
*
- * @returns The current witness context that can be passed to witness functions
+ * @param witnesses - The new witness set.
*/
- public getWitnessContext(): WitnessContext {
- const circuitCtx = this.circuitContext;
- return {
- ledger: this.getPublicState(),
- privateState: circuitCtx.currentPrivateState,
- contractAddress: circuitCtx.currentQueryContext.address,
- };
+ setWitnesses(witnesses: W): void {
+ this._backend.setWitnesses(witnesses);
}
};
}
diff --git a/packages/simulator/src/index.ts b/packages/simulator/src/index.ts
index 59523ed..8f02c5f 100644
--- a/packages/simulator/src/index.ts
+++ b/packages/simulator/src/index.ts
@@ -1,14 +1,68 @@
-// biome-ignore lint/performance/noBarrelFile: entrypoint module
+// biome-ignore-all lint/performance/noBarrelFile: package entrypoint
+
+// --- Backend seam ----------------------------------------------------------
+export type { Backend, BackendKind, CircuitKind } from './backend/Backend.js';
+export type { SyncSimulator } from './backend/DryBackend.js';
+export { DryBackend } from './backend/DryBackend.js';
export { AbstractSimulator } from './core/AbstractSimulator.js';
export { CircuitContextManager } from './core/CircuitContextManager.js';
export { ContractSimulator } from './core/ContractSimulator.js';
+// --- Core simulator (one factory, two backends) ----------------------------
+// A dry import pulls zero midnight-js: the live adapter `LiveBackend`
+// is type-only here and reached at runtime only via the dynamic import inside
+// `createSimulator`. `createLiveContext`/`registerLiveBackend` are
+// values, but their static graph is midnight-js-free (type-only + dynamic
+// import), so exporting them from the main barrel keeps the wall up.
export { createSimulator } from './factory/createSimulator.js';
export type { SimulatorConfig } from './factory/SimulatorConfig.js';
export type {
+ CreateLiveContextOptions,
+ IndexerLagPolicy,
+} from './live/createLiveContext.js';
+// --- Live wiring (harness-facing) ------------------------------------------
+export {
+ createLiveContext,
+ DEFAULT_INDEXER_LAG,
+} from './live/createLiveContext.js';
+export type { LiveBackend } from './live/LiveBackend.js';
+export { WITNESS_OVERRIDE_UNSUPPORTED } from './live/LiveBackend.js';
+export type {
+ DeployedTxHandle,
+ FinalizedCallResult,
+ LiveContext,
+} from './live/LiveContext.js';
+export type {
+ LiveBackendFactory,
+ LiveBackendRequest,
+} from './live/registry.js';
+export {
+ clearLiveBackend,
+ getRegisteredLiveBackend,
+ isLiveBackend,
+ registerLiveBackend,
+} from './live/registry.js';
+export type {
+ ContractAddress,
+ Either,
+ ZswapCoinPublicKey,
+} from './signers/Signers.js';
+// --- Signers ---------------------------------------------------------------
+export {
+ MAX_LIVE_SIGNERS,
+ Signers,
+ type SignersOptions,
+} from './signers/Signers.js';
+
+// --- Types -----------------------------------------------------------------
+export type {
+ AsyncCircuits,
ContextlessCircuits,
ExtractImpureCircuits,
ExtractPureCircuits,
IContractSimulator,
IMinimalContract,
} from './types/index.js';
-export type { BaseSimulatorOptions } from './types/Options.js';
+export type {
+ BaseSimulatorOptions,
+ SimulatorOptions,
+} from './types/Options.js';
diff --git a/packages/simulator/src/live/LiveBackend.ts b/packages/simulator/src/live/LiveBackend.ts
new file mode 100644
index 0000000..7014391
--- /dev/null
+++ b/packages/simulator/src/live/LiveBackend.ts
@@ -0,0 +1,173 @@
+import type { StateValue } from '@midnight-ntwrk/compact-runtime';
+import type { Backend, BackendKind, CircuitKind } from '../backend/Backend.js';
+import type { SyncSimulator } from '../backend/DryBackend.js';
+import type { Signers } from '../signers/Signers.js';
+import type { LiveContext } from './LiveContext.js';
+
+/** The error thrown when witnesses are swapped on the live backend. */
+export const WITNESS_OVERRIDE_UNSUPPORTED =
+ 'witness override unsupported on live backend';
+
+/** The error thrown when private state is mutated on the live backend. */
+export const PRIVATE_STATE_MUTATION_UNSUPPORTED =
+ 'private-state mutation unsupported on live backend';
+
+/**
+ * Dependencies the live adapter is constructed with by `createBackendSimulator`.
+ *
+ * @template P - Private state type.
+ * @template L - Public ledger state type.
+ */
+export interface LiveBackendDeps {
+ /** The caller-supplied live world (handles + readers). */
+ ctx: LiveContext
;
+ /**
+ * A local in-memory simulator used solely to evaluate pure circuits (D2).
+ * Never deployed on-chain; pure circuits are state- and
+ * caller-independent, so its seed state does not affect results.
+ */
+ pureSim: SyncSimulator
;
+ /** Alias resolver, used here for the live signer cap. */
+ signers: Signers;
+ /** The shared ledger extractor, applied to indexer state for parity. */
+ ledgerExtractor: (state: StateValue) => L;
+}
+
+/**
+ * The live backend: a thin adapter that routes operations to an injected
+ * {@link LiveContext} and normalizes results to match dry.
+ *
+ * It imports no midnight-js — all node wiring lives behind the
+ * {@link LiveContext} seam. Routing follows pure/impure, not read/write
+ * (D2): pure circuits run locally on the JS artifact, impure circuits
+ * submit a tx.
+ *
+ * @template P - Private state type.
+ * @template L - Public ledger state type.
+ */
+export class LiveBackend
implements Backend
{
+ readonly kind: BackendKind = 'live';
+
+ private readonly ctx: LiveContext
;
+ private readonly pureSim: SyncSimulator
;
+ private readonly signers: Signers;
+ private readonly ledgerExtractor: (state: StateValue) => L;
+
+ /** Caller active for all subsequent calls until changed. */
+ private persistentAlias: string | null = null;
+ /** Caller active for the next call only, then reverts. */
+ private singleAlias: string | null = null;
+ private hasSingle = false;
+
+ constructor(deps: LiveBackendDeps
) {
+ this.ctx = deps.ctx;
+ this.pureSim = deps.pureSim;
+ this.signers = deps.signers;
+ this.ledgerExtractor = deps.ledgerExtractor;
+ }
+
+ get contractAddress(): string {
+ return this.ctx.contractAddress;
+ }
+
+ /**
+ * Pure circuits evaluate locally on the JS artifact (no tx); impure circuits
+ * submit a tx via the per-alias handle and the result is normalized from
+ * `FinalizedCallTxData.private.result` to the bare `R` dry returns.
+ */
+ async call(
+ kind: CircuitKind,
+ name: string,
+ args: unknown[],
+ ): Promise {
+ if (kind === 'pure') {
+ const fn = this.pureSim.circuits.pure[name];
+ if (typeof fn !== 'function') {
+ throw new Error(`unknown pure circuit "${name}"`);
+ }
+ const result = fn(...args);
+ this.consumeSingle();
+ return result;
+ }
+
+ const alias = this.activeAlias();
+ const handle = await this.ctx.handleFor(alias);
+ const txFn = handle.callTx[name];
+ if (typeof txFn !== 'function') {
+ throw new Error(`unknown impure circuit "${name}"`);
+ }
+ // Unwrap callTx's { public, private: { result } } to the bare R.
+ // The assert message inside any rejection is preserved verbatim —
+ // we await directly and never catch/rewrite it.
+ const finalized = await txFn(...args);
+ this.consumeSingle();
+ return finalized.private.result;
+ }
+
+ /** Same extractor as dry, applied to indexer-sourced state. */
+ async getPublicState(): Promise {
+ return this.ledgerExtractor(await this.ctx.queryLedger());
+ }
+
+ /** Read parity via the private-state provider. */
+ async getPrivateState(): Promise {
+ return this.ctx.queryPrivateState();
+ }
+
+ async getContractState(): Promise {
+ return this.ctx.queryLedger();
+ }
+
+ /**
+ * Mid-test private-state mutation does not faithfully reproduce on live.
+ * Throws so such specs are explicitly guarded with `isLiveBackend()`
+ * rather than silently passing against unchanged state.
+ */
+ setPrivateState(_privateState: P): void {
+ throw new Error(PRIVATE_STATE_MUTATION_UNSUPPORTED);
+ }
+
+ /**
+ * Validates the alias against the prefunded pool and records it.
+ * `'single'` applies to the next call then reverts; `'persistent'` holds until
+ * changed. The lifecycle mirrors dry's `callerOverride` /
+ * `persistentCallerOverride`.
+ */
+ setCaller(alias: string | null, mode: 'single' | 'persistent'): void {
+ if (alias !== null) this.signers.assertLiveAliasAllowed(alias);
+ if (mode === 'persistent') {
+ this.persistentAlias = alias;
+ } else {
+ this.singleAlias = alias;
+ this.hasSingle = true;
+ }
+ }
+
+ /** Witnesses bind at deploy and cannot be swapped mid-test. */
+ overrideWitness(_key: PropertyKey, _fn: unknown): void {
+ throw new Error(WITNESS_OVERRIDE_UNSUPPORTED);
+ }
+
+ /** Witnesses bind at deploy and cannot be swapped mid-test. */
+ setWitnesses(_witnesses: unknown): void {
+ throw new Error(WITNESS_OVERRIDE_UNSUPPORTED);
+ }
+
+ /** Reads the local witness set (used for pure-circuit evaluation). */
+ getWitnesses(): unknown {
+ return this.pureSim.witnesses;
+ }
+
+ /** The alias active for the next call: single-shot takes priority over persistent. */
+ private activeAlias(): string | null {
+ return this.hasSingle ? this.singleAlias : this.persistentAlias;
+ }
+
+ /** Clears the single-shot caller after a call, reverting to persistent/default. */
+ private consumeSingle(): void {
+ if (this.hasSingle) {
+ this.hasSingle = false;
+ this.singleAlias = null;
+ }
+ }
+}
diff --git a/packages/simulator/src/live/LiveContext.ts b/packages/simulator/src/live/LiveContext.ts
new file mode 100644
index 0000000..c16e45c
--- /dev/null
+++ b/packages/simulator/src/live/LiveContext.ts
@@ -0,0 +1,69 @@
+import type { StateValue } from '@midnight-ntwrk/compact-runtime';
+
+/**
+ * The result shape the live adapter reads from a `callTx` invocation.
+ *
+ * Pinned against `@midnight-ntwrk/midnight-js-contracts`: the default
+ * `handle.callTx[name](...args)` resolves to a `FinalizedCallTxData` whose
+ * circuit return value lives at `.private.result`. A harness's
+ * `FoundContract['callTx']` is structurally assignable to this.
+ */
+export interface FinalizedCallResult {
+ readonly private: { readonly result: unknown };
+}
+
+/**
+ * The minimal slice of a deployed-contract handle the live adapter needs:
+ * a `callTx` map from circuit name to an async tx submission.
+ *
+ * Kept structural so the package's runtime graph never imports midnight-js.
+ * A harness's `FoundContract` satisfies this without any cast.
+ */
+export interface DeployedTxHandle {
+ readonly callTx: Record<
+ string,
+ (...args: unknown[]) => Promise
+ >;
+}
+
+/**
+ * The injection seam between the package and a live Midnight node.
+ *
+ * Defined by the package, implemented by the caller's harness, which owns all
+ * live infra (deploy, providers, wallet pool). The package's adapter
+ * ({@link LiveBackend}) is a pure consumer of this interface and imports no
+ * midnight-js itself.
+ *
+ * Parameterized only by `P` (private state): the contract type `C` and ledger
+ * type `L` are erased into structural types ({@link DeployedTxHandle}) and the
+ * shared `ledgerExtractor`, so a harness can implement this without fighting
+ * midnight-js generics.
+ *
+ * @template P - Private state type.
+ */
+export interface LiveContext {
+ /** The address of the contract the harness already deployed. */
+ readonly contractAddress: string;
+
+ /**
+ * Resolves a per-alias deployed-contract handle, signing with that alias's
+ * prefunded wallet. `null` selects the default signer. Implementations should
+ * cache handles per alias.
+ *
+ * @param alias - The caller alias, or `null` for the default signer.
+ */
+ handleFor(alias: string | null): Promise;
+
+ /**
+ * Reads the current public contract state from the indexer as a `StateValue`,
+ * ready to feed the shared `ledgerExtractor`. Implementations should
+ * absorb bounded indexer lag so read-after-write is stable.
+ */
+ queryLedger(): Promise;
+
+ /**
+ * Reads the contract's private state from the private-state provider (read
+ * parity).
+ */
+ queryPrivateState(): Promise;
+}
diff --git a/packages/simulator/src/live/createLiveContext.ts b/packages/simulator/src/live/createLiveContext.ts
new file mode 100644
index 0000000..1132dbf
--- /dev/null
+++ b/packages/simulator/src/live/createLiveContext.ts
@@ -0,0 +1,183 @@
+import type { StateValue } from '@midnight-ntwrk/compact-runtime';
+// Type-only imports — erased at build, so they create no runtime edge to
+// midnight-js. The only runtime midnight-js edge in this file is the
+// lazy dynamic import inside `loadFindDeployedContract`.
+import type {
+ PrivateStateProvider,
+ PublicDataProvider,
+} from '@midnight-ntwrk/midnight-js-types';
+import type { DeployedTxHandle, LiveContext } from './LiveContext.js';
+
+/**
+ * Bounded retry policy for absorbing indexer block-lag on public-state reads.
+ * Always finite: a genuinely missing write fails the suite rather than
+ * hanging it. Defaults concretize OQ2 and should be tuned against a real node.
+ */
+export interface IndexerLagPolicy {
+ /** Max poll attempts before giving up. */
+ retries: number;
+ /** Initial backoff between attempts, in ms. */
+ baseDelayMs: number;
+ /** Backoff ceiling, in ms. */
+ maxDelayMs: number;
+}
+
+/** Default indexer-lag policy (OQ2 — provisional, tune against a live node). */
+export const DEFAULT_INDEXER_LAG: IndexerLagPolicy = {
+ retries: 8,
+ baseDelayMs: 150,
+ maxDelayMs: 2000,
+};
+
+/**
+ * Options for {@link createLiveContext}.
+ *
+ * The package only assembles already-provided pieces; deploy, provider
+ * construction, and wallet funding are the caller's harness. Provider
+ * and contract specifics are threaded through opaquely — the harness owns their
+ * construction and types.
+ *
+ * @template P - Private state type.
+ */
+export interface CreateLiveContextOptions
{
+ /** The address of the already-deployed contract. */
+ contractAddress: string;
+ /**
+ * Per-alias `ContractProviders`, supplied by the harness. The wallet/signing
+ * differs per alias; `null` is the default signer. Threaded into
+ * `findDeployedContract`.
+ */
+ providersFor: (alias: string | null) => unknown;
+ /** The compiled contract, from the harness. Threaded into `findDeployedContract`. */
+ compiledContract: unknown;
+ /** Identifier under which the contract's private state is stored. */
+ privateStateId: string;
+ /** Provider for reading on-chain public state. */
+ publicDataProvider: PublicDataProvider;
+ /** Provider for reading private state (read parity). */
+ privateStateProvider: PrivateStateProvider;
+ /** Optional override of the indexer-lag policy. */
+ indexerLag?: Partial;
+}
+
+type FindDeployedContractFn = (
+ providers: unknown,
+ options: unknown,
+) => Promise<{ callTx: DeployedTxHandle['callTx'] }>;
+
+let cachedFindDeployedContract: FindDeployedContractFn | undefined;
+
+/**
+ * Lazily loads `findDeployedContract`. The dynamic import is the sole runtime
+ * edge to midnight-js in the package's graph; a failure to resolve it (the
+ * optional peers are absent) is rewrapped into an actionable message
+ * rather than a raw `ERR_MODULE_NOT_FOUND`.
+ */
+const loadFindDeployedContract = async (): Promise => {
+ if (cachedFindDeployedContract) return cachedFindDeployedContract;
+ try {
+ const mod = await import('@midnight-ntwrk/midnight-js-contracts');
+ cachedFindDeployedContract =
+ mod.findDeployedContract as unknown as FindDeployedContractFn;
+ } catch (cause) {
+ throw new Error(
+ 'install @midnight-ntwrk/midnight-js-contracts (and the midnight-js peers) ' +
+ 'to use live mode',
+ { cause },
+ );
+ }
+ return cachedFindDeployedContract;
+};
+
+const sleep = (ms: number): Promise =>
+ new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+
+/**
+ * Assembles a {@link LiveContext} from harness-provided pieces.
+ *
+ * Provides three things the harness would otherwise hand-roll: a per-alias
+ * deployed-handle cache (via `findDeployedContract`), a public-state reader that
+ * absorbs bounded indexer lag, and a private-state reader. The
+ * adapter ({@link LiveContext}) stays thin; this helper is separate and
+ * imported only by live consumers (who already depend on midnight-js).
+ *
+ * @param options - Harness-provided providers, contract, and address.
+ * @returns A {@link LiveContext} ready to pass to `create(args, { live })`.
+ */
+export function createLiveContext(
+ options: CreateLiveContextOptions
,
+): LiveContext
{
+ const lag: IndexerLagPolicy = {
+ ...DEFAULT_INDEXER_LAG,
+ ...options.indexerLag,
+ };
+ const handleCache = new Map>();
+
+ const resolveHandle = (alias: string | null): Promise => {
+ const key = alias ?? '\u0000default';
+ const cached = handleCache.get(key);
+ if (cached) return cached;
+ const built = loadFindDeployedContract().then((findDeployedContract) =>
+ findDeployedContract(options.providersFor(alias), {
+ compiledContract: options.compiledContract,
+ contractAddress: options.contractAddress,
+ privateStateId: options.privateStateId,
+ }),
+ );
+ handleCache.set(key, built);
+ return built;
+ };
+
+ return {
+ contractAddress: options.contractAddress,
+
+ handleFor: resolveHandle,
+
+ /**
+ * Polls the indexer for the contract state, retrying a bounded number of
+ * times with capped exponential backoff to absorb block-lag after a
+ * confirmed write. Returns the `StateValue` the shared
+ * `ledgerExtractor` consumes.
+ */
+ async queryLedger(): Promise {
+ let delay = lag.baseDelayMs;
+ let lastErr: unknown;
+ for (let attempt = 0; attempt <= lag.retries; attempt++) {
+ try {
+ const state = await options.publicDataProvider.queryContractState(
+ options.contractAddress,
+ );
+ if (state != null) {
+ // `ContractState.data` is the StateValue the dry path also extracts.
+ return (state as unknown as { data: StateValue }).data;
+ }
+ } catch (err) {
+ lastErr = err;
+ }
+ if (attempt < lag.retries) {
+ await sleep(delay);
+ delay = Math.min(delay * 2, lag.maxDelayMs);
+ }
+ }
+ throw new Error(
+ `no contract state at ${options.contractAddress} after ${lag.retries + 1} ` +
+ 'attempts — the write may be missing, or indexer lag exceeds the budget',
+ lastErr === undefined ? undefined : { cause: lastErr },
+ );
+ },
+
+ async queryPrivateState(): Promise {
+ const state = await options.privateStateProvider.get(
+ options.privateStateId,
+ );
+ if (state == null) {
+ throw new Error(
+ `no private state stored at "${options.privateStateId}"`,
+ );
+ }
+ return state;
+ },
+ };
+}
diff --git a/packages/simulator/src/live/registry.ts b/packages/simulator/src/live/registry.ts
new file mode 100644
index 0000000..9cc33ef
--- /dev/null
+++ b/packages/simulator/src/live/registry.ts
@@ -0,0 +1,77 @@
+import type { SimulatorConfig } from '../factory/SimulatorConfig.js';
+import type { IMinimalContract } from '../types/Contract.js';
+import type { SimulatorOptions } from '../types/Options.js';
+import type { LiveContext } from './LiveContext.js';
+
+/**
+ * What the registered live backend receives in order to deploy/attach the right
+ * contract and return a {@link LiveContext} for it. The harness owns all infra;
+ * this just hands it the same config + args the test used.
+ */
+export interface LiveBackendRequest<
+ P = unknown,
+ L = unknown,
+ W = unknown,
+ TContract extends IMinimalContract = IMinimalContract,
+ TArgs extends readonly unknown[] = readonly unknown[],
+> {
+ /** The simulator config (contract factory, witnesses, ledger extractor, …). */
+ config: SimulatorConfig
;
+ /** The constructor args the test passed to `create`. */
+ contractArgs: TArgs;
+ /** The options the test passed to `create`. */
+ options: SimulatorOptions
;
+}
+
+/**
+ * Produces a {@link LiveContext} for a given request. Registered once by the
+ * consuming harness (typically in a `test:live` setup file).
+ */
+export type LiveBackendFactory = (
+ // The registry is contract-agnostic; each call is concretely typed at the create() site.
+ req: LiveBackendRequest,
+) => Promise>;
+
+let registeredFactory: LiveBackendFactory | undefined;
+
+/**
+ * Registers the live backend the simulator attaches to when
+ * `MIDNIGHT_BACKEND=live` and no explicit `{ live }` is passed to `create`.
+ *
+ * Call this once from your `test:live` setup. It keeps the per-module test files
+ * backend-agnostic: `await Sim.create()` works on both backends.
+ *
+ * Throws if a different factory is already registered: in a shared test process
+ * a silent replacement would switch harness context and cause non-deterministic
+ * live behavior. Call {@link clearLiveBackend} before re-registering.
+ *
+ * @param factory - Builds a {@link LiveContext} per `create` call.
+ */
+export function registerLiveBackend(factory: LiveBackendFactory): void {
+ if (registeredFactory && registeredFactory !== factory) {
+ throw new Error(
+ 'live backend is already registered. Call clearLiveBackend() before replacing it.',
+ );
+ }
+ registeredFactory = factory;
+}
+
+/** Clears the registered live backend (mainly for test teardown). */
+export function clearLiveBackend(): void {
+ registeredFactory = undefined;
+}
+
+/** Returns the registered live backend factory, if any. */
+export function getRegisteredLiveBackend(): LiveBackendFactory | undefined {
+ return registeredFactory;
+}
+
+/**
+ * Whether the live backend is selected via `MIDNIGHT_BACKEND=live`.
+ *
+ * Use it in specs to guard the documented dry↔live asymmetries, e.g.
+ * `it.skipIf(isLiveBackend())('rejects witness override', …)`.
+ */
+export function isLiveBackend(): boolean {
+ return process.env.MIDNIGHT_BACKEND === 'live';
+}
diff --git a/packages/simulator/src/signers/Signers.ts b/packages/simulator/src/signers/Signers.ts
new file mode 100644
index 0000000..e913340
--- /dev/null
+++ b/packages/simulator/src/signers/Signers.ts
@@ -0,0 +1,173 @@
+import {
+ type CoinPublicKey,
+ convertFieldToBytes,
+ encodeCoinPublicKey,
+} from '@midnight-ntwrk/compact-runtime';
+import type { BackendKind } from '../backend/Backend.js';
+
+/**
+ * The number of prefunded wallets available on the dev-preset live node:
+ * the deployer plus three named aliases (D1). Requesting more requires
+ * the deferred derive-and-fund flow.
+ */
+export const MAX_LIVE_SIGNERS = 4;
+
+/** Structural mirror of a generated artifact's `ZswapCoinPublicKey`. */
+export type ZswapCoinPublicKey = { bytes: Uint8Array };
+
+/** Structural mirror of a generated artifact's `ContractAddress`. */
+export type ContractAddress = { bytes: Uint8Array };
+
+/** Structural mirror of a generated artifact's `Either`. */
+export type Either = {
+ is_left: boolean;
+ left: L;
+ right: R;
+};
+
+/**
+ * Converts an ASCII alias to a 64-char zero-padded hex string.
+ *
+ * This is the exact derivation the existing test harness uses
+ * (`generatePubKeyPair` / `encodeToPK`), so a backend-aware simulator resolves
+ * an alias to the same key the current synchronous specs do — preserving dry
+ * parity for migrated modules.
+ *
+ * @param alias - The caller alias.
+ * @returns A 64-char hex `CoinPublicKey`.
+ */
+const aliasToHex = (alias: string): CoinPublicKey =>
+ Buffer.from(alias, 'ascii').toString('hex').padStart(64, '0');
+
+const zeroBytes = (): Uint8Array => convertFieldToBytes(32, 0n, '');
+
+/**
+ * Configuration for {@link Signers}.
+ */
+export interface SignersOptions {
+ /** The backend this resolver serves. */
+ mode: BackendKind;
+ /**
+ * Dry only: override the default deterministic alias derivation with an
+ * explicit alias→key map. Aliases not present fall back to the default
+ * derivation (OQ4).
+ */
+ dryKeys?: Readonly>;
+ /**
+ * Live only: the aliases backed by a prefunded wallet on the node. Capped at
+ * {@link MAX_LIVE_SIGNERS}. Requesting an alias outside this set
+ * fails with a clear error rather than silently reusing a wallet.
+ */
+ liveAliases?: readonly string[];
+ /**
+ * Live only: resolve an alias to its wallet's coin public key. Supplied by the
+ * caller's harness, which owns wallet provisioning.
+ */
+ resolveLiveKey?: (alias: string) => CoinPublicKey | Promise;
+}
+
+/**
+ * Resolves caller-identity aliases to keys, uniformly across backends.
+ *
+ * Alias strings are the common currency for caller identity (D1): `as('OWNER')`
+ * denotes the same logical actor in both modes. Dry derives a
+ * deterministic key from the alias label; live resolves the alias to a pooled,
+ * prefunded wallet, enforcing the {@link MAX_LIVE_SIGNERS} cap.
+ *
+ * The public resolvers ({@link keyFor}, {@link eitherFor}) are async so spec
+ * code is uniform `await` across backends, even though dry resolves
+ * synchronously.
+ */
+export class Signers {
+ readonly mode: BackendKind;
+ private readonly dryKeys: Readonly>;
+ private readonly liveAliases: ReadonlySet;
+ private readonly resolveLiveKey?: (
+ alias: string,
+ ) => CoinPublicKey | Promise;
+
+ constructor(options: SignersOptions) {
+ this.mode = options.mode;
+ this.dryKeys = options.dryKeys ?? {};
+ this.liveAliases = new Set(options.liveAliases ?? []);
+ this.resolveLiveKey = options.resolveLiveKey;
+
+ if (this.mode === 'live' && this.liveAliases.size > MAX_LIVE_SIGNERS) {
+ throw new Error(
+ `live backend supports at most ${MAX_LIVE_SIGNERS} prefunded signers; ` +
+ `got ${this.liveAliases.size}. The derive-and-fund flow for more is deferred.`,
+ );
+ }
+ }
+
+ /**
+ * Synchronous dry-mode alias→key resolution.
+ *
+ * Used by the dry backend's `setCaller`, which is synchronous. Not used in
+ * live mode (live resolution is deferred to the async handle cache).
+ *
+ * @param alias - The caller alias.
+ * @returns The deterministic dry key for the alias.
+ */
+ public resolveDryKey(alias: string): CoinPublicKey {
+ return this.dryKeys[alias] ?? aliasToHex(alias);
+ }
+
+ /**
+ * Asserts an alias is backed by a prefunded wallet on the live node.
+ *
+ * Throws the cap error rather than silently reusing a wallet or proceeding
+ * with an unfunded one. A no-op in dry mode.
+ *
+ * @param alias - The caller alias to validate.
+ */
+ public assertLiveAliasAllowed(alias: string): void {
+ if (this.mode !== 'live') return;
+ if (this.liveAliases.size > 0 && !this.liveAliases.has(alias)) {
+ throw new Error(
+ `live signer "${alias}" is not in the prefunded pool ` +
+ `[${[...this.liveAliases].join(', ')}] (max ${MAX_LIVE_SIGNERS}). ` +
+ 'Add it to the wallet pool or use the deferred derive-and-fund flow.',
+ );
+ }
+ }
+
+ /**
+ * Resolves an alias to a raw {@link CoinPublicKey}, for use as a circuit arg.
+ *
+ * @param alias - The caller alias.
+ * @returns The key for the alias.
+ */
+ public async keyFor(alias: string): Promise {
+ if (this.mode === 'live') {
+ this.assertLiveAliasAllowed(alias);
+ if (!this.resolveLiveKey) {
+ throw new Error(
+ `cannot resolve live key for "${alias}": no resolveLiveKey supplied. ` +
+ 'The caller harness must provide one.',
+ );
+ }
+ return this.resolveLiveKey(alias);
+ }
+ return this.resolveDryKey(alias);
+ }
+
+ /**
+ * Resolves an alias to an `Either`,
+ * the shape circuits expect for an owner/user argument. Always the left
+ * (coin-public-key) variant; contract-address owners are out of scope here.
+ *
+ * @param alias - The caller alias.
+ * @returns The `Either` wrapping the alias's coin public key.
+ */
+ public async eitherFor(
+ alias: string,
+ ): Promise> {
+ const key = await this.keyFor(alias);
+ return {
+ is_left: true,
+ left: { bytes: encodeCoinPublicKey(key) },
+ right: { bytes: zeroBytes() },
+ };
+ }
+}
diff --git a/packages/simulator/src/types/Circuit.ts b/packages/simulator/src/types/Circuit.ts
index 232b28c..5a20a03 100644
--- a/packages/simulator/src/types/Circuit.ts
+++ b/packages/simulator/src/types/Circuit.ts
@@ -38,3 +38,22 @@ export type ContextlessCircuits = {
? (...args: P) => R
: never;
};
+
+/**
+ * Async sibling of {@link ContextlessCircuits}, used by `createBackendSimulator`.
+ *
+ * Identical to {@link ContextlessCircuits} except every circuit returns
+ * `Promise` instead of `R`. This is the type-level half of dry↔live parity:
+ * the dry backend wraps its synchronous result in `Promise.resolve`,
+ * the live backend awaits the network, and spec code is uniform `await` across
+ * both. A circuit can never return a bare value on one backend and a `Promise`
+ * on the other.
+ */
+export type AsyncCircuits = {
+ [K in keyof Circuits]: Circuits[K] extends (
+ ctx: CircuitContext,
+ ...args: infer P
+ ) => { result: infer R; context?: CircuitContext }
+ ? (...args: P) => Promise
+ : never;
+};
diff --git a/packages/simulator/src/types/Options.ts b/packages/simulator/src/types/Options.ts
index e876530..66666cf 100644
--- a/packages/simulator/src/types/Options.ts
+++ b/packages/simulator/src/types/Options.ts
@@ -2,6 +2,8 @@ import type {
CoinPublicKey,
ContractAddress,
} from '@midnight-ntwrk/compact-runtime';
+import type { BackendKind } from '../backend/Backend.js';
+import type { LiveContext } from '../live/LiveContext.js';
/**
* Base configuration options for simulator constructors.
@@ -19,3 +21,30 @@ export type BaseSimulatorOptions = {
/** Contract deployment address */
contractAddress?: ContractAddress;
};
+
+/**
+ * Options for `createSimulator`'s async `create`. Extends the base construction
+ * options with backend selection and the live-world injection seam.
+ *
+ * @template P - Private state type.
+ * @template W - Witnesses type.
+ */
+export interface SimulatorOptions
extends BaseSimulatorOptions
{
+ /**
+ * Force a backend instead of reading `MIDNIGHT_BACKEND`. Mainly for tests that
+ * pin a backend regardless of the environment.
+ */
+ backend?: BackendKind;
+ /**
+ * The live world, supplied by the caller's harness. In live mode this
+ * is used if provided; otherwise the globally registered live backend (see
+ * `registerLiveBackend`) is used. Ignored in dry mode.
+ */
+ live?: LiveContext
;
+ /** Dry only: override the deterministic alias→key derivation (OQ4). */
+ signerKeys?: Readonly>;
+ /** Live only: the prefunded alias pool (max `MAX_LIVE_SIGNERS`). */
+ liveAliases?: readonly string[];
+ /** Live only: resolve an alias to its wallet's coin public key. */
+ resolveLiveKey?: (alias: string) => CoinPublicKey | Promise;
+}
diff --git a/packages/simulator/src/types/index.ts b/packages/simulator/src/types/index.ts
index c4fb050..f94da8e 100644
--- a/packages/simulator/src/types/index.ts
+++ b/packages/simulator/src/types/index.ts
@@ -1,6 +1,7 @@
// Re-export all types from type modules
export type {
+ AsyncCircuits,
ContextlessCircuits,
ExtractImpureCircuits,
ExtractPureCircuits,
diff --git a/packages/simulator/test/integration/SampleZOwnable.test.ts b/packages/simulator/test/integration/SampleZOwnable.test.ts
index 1a21bb9..02681d2 100644
--- a/packages/simulator/test/integration/SampleZOwnable.test.ts
+++ b/packages/simulator/test/integration/SampleZOwnable.test.ts
@@ -10,10 +10,10 @@ import { SampleZOwnablePrivateState } from '../fixtures/sample-contracts/witness
import * as utils from '../fixtures/utils/address.js';
import { SampleZOwnableSimulator } from './SampleZOwnableSimulator.js';
-// PKs
-const [OWNER, Z_OWNER] = utils.generatePubKeyPair('OWNER');
-const [NEW_OWNER, Z_NEW_OWNER] = utils.generatePubKeyPair('NEW_OWNER');
-const [UNAUTHORIZED, _] = utils.generatePubKeyPair('UNAUTHORIZED');
+// PKs — the caller identity now travels as an alias string (`as('OWNER')`); the
+// alias resolves to the same deterministic key these encoded PKs are built from.
+const [, Z_OWNER] = utils.generatePubKeyPair('OWNER');
+const [, Z_NEW_OWNER] = utils.generatePubKeyPair('NEW_OWNER');
const INSTANCE_SALT = new Uint8Array(32).fill(8675309);
const BAD_NONCE = Buffer.from(Buffer.alloc(32, 'BAD_NONCE'));
@@ -98,30 +98,30 @@ const buildCommitment = (
describe('SampleZOwnable', () => {
describe('before initialize', () => {
- it('should fail when setting owner commitment as 0', () => {
- expect(() => {
- const badId = new Uint8Array(32).fill(0);
- new SampleZOwnableSimulator(badId, INSTANCE_SALT);
- }).toThrow('SampleZOwnable: invalid id');
+ it('should fail when setting owner commitment as 0', async () => {
+ const badId = new Uint8Array(32).fill(0);
+ await expect(
+ SampleZOwnableSimulator.create(badId, INSTANCE_SALT),
+ ).rejects.toThrow('SampleZOwnable: invalid id');
});
- it('should initialize with non-zero commitment', () => {
+ it('should initialize with non-zero commitment', async () => {
const notZeroPK = utils.encodeToPK('NOT_ZERO');
const notZeroNonce = new Uint8Array(32).fill(1);
const nonZeroId = createIdHash(notZeroPK, notZeroNonce);
- ownable = new SampleZOwnableSimulator(nonZeroId, INSTANCE_SALT);
+ ownable = await SampleZOwnableSimulator.create(nonZeroId, INSTANCE_SALT);
const nonZeroCommitment = buildCommitmentFromId(
nonZeroId,
INSTANCE_SALT,
INIT_COUNTER,
);
- expect(ownable.owner()).toEqual(nonZeroCommitment);
+ expect(await ownable.owner()).toEqual(nonZeroCommitment);
});
});
describe('after initialization', () => {
- beforeEach(() => {
+ beforeEach(async () => {
// Create private state object and generate nonce
const PS = SampleZOwnablePrivateState.generate();
// Bind nonce for convenience
@@ -129,13 +129,13 @@ describe('SampleZOwnable', () => {
// Prepare owner ID with gen nonce
const ownerId = createIdHash(Z_OWNER, secretNonce);
// Deploy contract with derived owner commitment and PS
- ownable = new SampleZOwnableSimulator(ownerId, INSTANCE_SALT, {
+ ownable = await SampleZOwnableSimulator.create(ownerId, INSTANCE_SALT, {
privateState: PS,
});
});
describe('owner', () => {
- it('should return the correct owner commitment', () => {
+ it('should return the correct owner commitment', async () => {
const expCommitment = buildCommitment(
Z_OWNER,
secretNonce,
@@ -143,7 +143,7 @@ describe('SampleZOwnable', () => {
INIT_COUNTER,
DOMAIN,
);
- expect(ownable.owner()).toEqual(expCommitment);
+ expect(await ownable.owner()).toEqual(expCommitment);
});
});
@@ -167,56 +167,58 @@ describe('SampleZOwnable', () => {
);
});
- it('should transfer ownership', () => {
- ownable.as(OWNER).transferOwnership(newIdHash);
- expect(ownable.owner()).toEqual(newOwnerCommitment);
+ it('should transfer ownership', async () => {
+ await ownable.as('OWNER').transferOwnership(newIdHash);
+ expect(await ownable.owner()).toEqual(newOwnerCommitment);
// Old owner
- expect(() => {
- ownable.as(OWNER).assertOnlyOwner();
- }).toThrow('SampleZOwnable: caller is not the owner');
+ await expect(ownable.as('OWNER').assertOnlyOwner()).rejects.toThrow(
+ 'SampleZOwnable: caller is not the owner',
+ );
// Unauthorized
- expect(() => {
- ownable.as(UNAUTHORIZED).assertOnlyOwner();
- }).toThrow('SampleZOwnable: caller is not the owner');
+ await expect(
+ ownable.as('UNAUTHORIZED').assertOnlyOwner(),
+ ).rejects.toThrow('SampleZOwnable: caller is not the owner');
// New owner
- ownable.privateState.injectSecretNonce(Buffer.from(newOwnerNonce));
- expect(ownable.as(NEW_OWNER).assertOnlyOwner()).not.to.throw;
+ await ownable.privateState.injectSecretNonce(
+ Buffer.from(newOwnerNonce),
+ );
+ await ownable.as('NEW_OWNER').assertOnlyOwner();
});
- it('should fail when transferring to id zero', () => {
+ it('should fail when transferring to id zero', async () => {
const badId = new Uint8Array(32).fill(0);
- expect(() => {
- ownable.as(OWNER).transferOwnership(badId);
- }).toThrow('SampleZOwnable: invalid id');
+ await expect(
+ ownable.as('OWNER').transferOwnership(badId),
+ ).rejects.toThrow('SampleZOwnable: invalid id');
});
- it('should fail when unauthorized transfers ownership', () => {
- expect(() => {
- ownable.as(UNAUTHORIZED).transferOwnership(newOwnerCommitment);
- }).toThrow('SampleZOwnable: caller is not the owner');
+ it('should fail when unauthorized transfers ownership', async () => {
+ await expect(
+ ownable.as('UNAUTHORIZED').transferOwnership(newOwnerCommitment),
+ ).rejects.toThrow('SampleZOwnable: caller is not the owner');
});
/**
* @description More thoroughly tested in `_transferOwnership`
* */
- it('should bump instance after transfer', () => {
- const beforeInstance = ownable.getPublicState()._counter;
+ it('should bump instance after transfer', async () => {
+ const beforeInstance = (await ownable.getPublicState())._counter;
// Transfer
- ownable.as(OWNER).transferOwnership(newOwnerCommitment);
+ await ownable.as('OWNER').transferOwnership(newOwnerCommitment);
// Check counter
- const afterInstance = ownable.getPublicState()._counter;
+ const afterInstance = (await ownable.getPublicState())._counter;
expect(afterInstance).toEqual(beforeInstance + 1n);
});
- it('should change commitment when transferring ownership to self with same pk + nonce)', () => {
+ it('should change commitment when transferring ownership to self with same pk + nonce)', async () => {
// Confirm current commitment
const repeatedId = createIdHash(Z_OWNER, secretNonce);
- const initCommitment = ownable.owner();
+ const initCommitment = await ownable.owner();
const expInitCommitment = buildCommitmentFromId(
repeatedId,
INSTANCE_SALT,
@@ -225,10 +227,10 @@ describe('SampleZOwnable', () => {
expect(initCommitment).toEqual(expInitCommitment);
// Transfer ownership to self with the same id -> `H(pk, nonce)`
- ownable.as(OWNER).transferOwnership(repeatedId);
+ await ownable.as('OWNER').transferOwnership(repeatedId);
// Check commitments don't match
- const newCommitment = ownable.owner();
+ const newCommitment = await ownable.owner();
expect(initCommitment).not.toEqual(newCommitment);
// Build commitment locally and validate new commitment == expected
@@ -241,93 +243,93 @@ describe('SampleZOwnable', () => {
expect(newCommitment).toEqual(expNewCommitment);
// Check same owner maintains permissions after transfer
- expect(ownable.as(OWNER).assertOnlyOwner()).not.to.throw;
+ await ownable.as('OWNER').assertOnlyOwner();
});
});
describe('renounceOwnership', () => {
- it('should renounce ownership', () => {
- ownable.as(OWNER).renounceOwnership();
+ it('should renounce ownership', async () => {
+ await ownable.as('OWNER').renounceOwnership();
// Check owner is reset
- expect(ownable.owner()).toEqual(new Uint8Array(32).fill(0));
+ expect(await ownable.owner()).toEqual(new Uint8Array(32).fill(0));
// Check revoked permissions
- expect(() => {
- ownable.assertOnlyOwner();
- }).toThrow('SampleZOwnable: caller is not the owner');
+ await expect(ownable.assertOnlyOwner()).rejects.toThrow(
+ 'SampleZOwnable: caller is not the owner',
+ );
});
- it('should fail when renouncing from unauthorized', () => {
- expect(() => {
- ownable.as(UNAUTHORIZED).renounceOwnership();
- }).toThrow('SampleZOwnable: caller is not the owner');
+ it('should fail when renouncing from unauthorized', async () => {
+ await expect(
+ ownable.as('UNAUTHORIZED').renounceOwnership(),
+ ).rejects.toThrow('SampleZOwnable: caller is not the owner');
});
- it('should fail when renouncing from authorized with bad nonce', () => {
- ownable.privateState.injectSecretNonce(BAD_NONCE);
- expect(() => {
- ownable.as(OWNER).renounceOwnership();
- }).toThrow('SampleZOwnable: caller is not the owner');
+ it('should fail when renouncing from authorized with bad nonce', async () => {
+ await ownable.privateState.injectSecretNonce(BAD_NONCE);
+ await expect(ownable.as('OWNER').renounceOwnership()).rejects.toThrow(
+ 'SampleZOwnable: caller is not the owner',
+ );
});
- it('should fail when renouncing from unauthorized with bad nonce', () => {
- ownable.privateState.injectSecretNonce(BAD_NONCE);
- expect(() => {
- ownable.as(UNAUTHORIZED).renounceOwnership();
- });
+ it('should fail when renouncing from unauthorized with bad nonce', async () => {
+ await ownable.privateState.injectSecretNonce(BAD_NONCE);
+ await expect(
+ ownable.as('UNAUTHORIZED').renounceOwnership(),
+ ).rejects.toThrow('SampleZOwnable: caller is not the owner');
});
});
describe('assertOnlyOwner', () => {
- it('should allow authorized caller with correct nonce to call', () => {
+ it('should allow authorized caller with correct nonce to call', async () => {
// Check nonce is correct
- expect(ownable.privateState.getCurrentSecretNonce()).toEqual(
+ expect(await ownable.privateState.getCurrentSecretNonce()).toEqual(
secretNonce,
);
- expect(ownable.as(OWNER).assertOnlyOwner()).to.not.throw;
+ await ownable.as('OWNER').assertOnlyOwner();
});
- it('should fail when the authorized caller has the wrong nonce', () => {
+ it('should fail when the authorized caller has the wrong nonce', async () => {
// Inject bad nonce
- ownable.privateState.injectSecretNonce(BAD_NONCE);
+ await ownable.privateState.injectSecretNonce(BAD_NONCE);
// Check nonce does not match
- expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual(
+ expect(await ownable.privateState.getCurrentSecretNonce()).not.toEqual(
secretNonce,
);
// Set caller and call circuit
- expect(() => {
- ownable.as(OWNER).assertOnlyOwner();
- }).toThrow('SampleZOwnable: caller is not the owner');
+ await expect(ownable.as('OWNER').assertOnlyOwner()).rejects.toThrow(
+ 'SampleZOwnable: caller is not the owner',
+ );
});
- it('should fail when unauthorized caller has the correct nonce', () => {
+ it('should fail when unauthorized caller has the correct nonce', async () => {
// Check nonce is correct
- expect(ownable.privateState.getCurrentSecretNonce()).toEqual(
+ expect(await ownable.privateState.getCurrentSecretNonce()).toEqual(
secretNonce,
);
- expect(() => {
- ownable.as(UNAUTHORIZED).assertOnlyOwner();
- }).toThrow('SampleZOwnable: caller is not the owner');
+ await expect(
+ ownable.as('UNAUTHORIZED').assertOnlyOwner(),
+ ).rejects.toThrow('SampleZOwnable: caller is not the owner');
});
- it('should fail when unauthorized caller has the wrong nonce', () => {
+ it('should fail when unauthorized caller has the wrong nonce', async () => {
// Inject bad nonce
- ownable.privateState.injectSecretNonce(BAD_NONCE);
+ await ownable.privateState.injectSecretNonce(BAD_NONCE);
// Check nonce does not match
- expect(ownable.privateState.getCurrentSecretNonce()).not.toEqual(
+ expect(await ownable.privateState.getCurrentSecretNonce()).not.toEqual(
secretNonce,
);
// Set unauthorized caller and call circuit
- expect(() => {
- ownable.as(UNAUTHORIZED).assertOnlyOwner();
- }).toThrow('SampleZOwnable: caller is not the owner');
+ await expect(
+ ownable.as('UNAUTHORIZED').assertOnlyOwner(),
+ ).rejects.toThrow('SampleZOwnable: caller is not the owner');
});
});
@@ -352,14 +354,17 @@ describe('SampleZOwnable', () => {
];
it.each(
testCases,
- )('should match commitment for $label with counter $counter', ({
+ )('should match commitment for $label with counter $counter', async ({
ownerPK,
counter,
}) => {
const id = createIdHash(ownerPK, secretNonce);
// Check buildCommitmentFromId
- const hashFromContract = ownable._computeOwnerCommitment(id, counter);
+ const hashFromContract = await ownable._computeOwnerCommitment(
+ id,
+ counter,
+ );
const hashFromHelper1 = buildCommitmentFromId(
id,
INSTANCE_SALT,
@@ -400,21 +405,21 @@ describe('SampleZOwnable', () => {
it.each(
testCases,
- )('should match local and contract owner id for $label', ({
+ )('should match local and contract owner id for $label', async ({
eitherOwner,
nonce,
}) => {
- const ownerId = ownable._computeOwnerId(eitherOwner, nonce);
+ const ownerId = await ownable._computeOwnerId(eitherOwner, nonce);
const expId = createIdHash(eitherOwner.left, nonce);
expect(ownerId).toEqual(expId);
});
- it('should fail to compute ContractAddress id', () => {
+ it('should fail to compute ContractAddress id', async () => {
const eitherContract =
utils.createEitherTestContractAddress('CONTRACT');
- expect(() => {
- ownable._computeOwnerId(eitherContract, secretNonce);
- }).toThrow(
+ await expect(
+ ownable._computeOwnerId(eitherContract, secretNonce),
+ ).rejects.toThrow(
'SampleZOwnable: contract address owners are not yet supported',
);
});
diff --git a/packages/simulator/test/integration/SampleZOwnableSimulator.ts b/packages/simulator/test/integration/SampleZOwnableSimulator.ts
index e2e26f2..415feaf 100644
--- a/packages/simulator/test/integration/SampleZOwnableSimulator.ts
+++ b/packages/simulator/test/integration/SampleZOwnableSimulator.ts
@@ -1,4 +1,4 @@
-import { type BaseSimulatorOptions, createSimulator } from '../../src/index';
+import { createSimulator, type SimulatorOptions } from '../../src/index';
import {
type ContractAddress,
type Either,
@@ -44,15 +44,19 @@ const SampleZOwnableSimulatorBase = createSimulator<
* SampleZOwnable Simulator
*/
export class SampleZOwnableSimulator extends SampleZOwnableSimulatorBase {
- constructor(
+ static async create(
ownerId: Uint8Array,
instanceSalt: Uint8Array,
- options: BaseSimulatorOptions<
+ options: SimulatorOptions<
SampleZOwnablePrivateState,
ReturnType
> = {},
- ) {
- super([ownerId, instanceSalt], options);
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this`
+ return super.create(
+ [ownerId, instanceSalt],
+ options,
+ ) as Promise;
}
/**
@@ -60,7 +64,7 @@ export class SampleZOwnableSimulator extends SampleZOwnableSimulatorBase {
* The full commitment is: `SHA256(SHA256(pk, nonce), instanceSalt, counter, domain)`.
* @returns The current owner's commitment.
*/
- public owner(): Uint8Array {
+ public owner(): Promise {
return this.circuits.impure.owner();
}
@@ -69,8 +73,8 @@ export class SampleZOwnableSimulator extends SampleZOwnableSimulatorBase {
* `newOwnerId` must be precalculated and given to the current owner off chain.
* @param newOwnerId The new owner's unique identifier (`SHA256(pk, nonce)`).
*/
- public transferOwnership(newOwnerId: Uint8Array) {
- this.circuits.impure.transferOwnership(newOwnerId);
+ public transferOwnership(newOwnerId: Uint8Array): Promise<[]> {
+ return this.circuits.impure.transferOwnership(newOwnerId);
}
/**
@@ -78,26 +82,28 @@ export class SampleZOwnableSimulator extends SampleZOwnableSimulatorBase {
* It will not be possible to call `assertOnlyOnwer` circuits anymore.
* Can only be called by the current owner.
*/
- public renounceOwnership() {
- this.circuits.impure.renounceOwnership();
+ public renounceOwnership(): Promise<[]> {
+ return this.circuits.impure.renounceOwnership();
}
/**
* @description Throws if called by any account whose id hash `SHA256(pk, nonce)` does not match
* the stored owner commitment. Use this to only allow the owner to call specific circuits.
*/
- public assertOnlyOwner() {
- this.circuits.impure.assertOnlyOwner();
+ public assertOnlyOwner(): Promise<[]> {
+ return this.circuits.impure.assertOnlyOwner();
}
/**
* @description Computes the owner commitment from the given `id` and `counter`.
* @param id - The unique identifier of the owner calculated by `SHA256(pk, nonce)`.
- * @param counter - The current counter or round. This increments by `1`
- * after every transfer to prevent duplicate commitments given the same `id`.
+ * @param counter - The current counter or round.
* @returns The commitment derived from `id` and `counter`.
*/
- public _computeOwnerCommitment(id: Uint8Array, counter: bigint): Uint8Array {
+ public _computeOwnerCommitment(
+ id: Uint8Array,
+ counter: bigint,
+ ): Promise {
return this.circuits.impure._computeOwnerCommitment(id, counter);
}
@@ -111,7 +117,7 @@ export class SampleZOwnableSimulator extends SampleZOwnableSimulatorBase {
public _computeOwnerId(
pk: Either,
nonce: Uint8Array,
- ): Uint8Array {
+ ): Promise {
return this.circuits.pure._computeOwnerId(pk, nonce);
}
@@ -121,13 +127,12 @@ export class SampleZOwnableSimulator extends SampleZOwnableSimulatorBase {
* @param newNonce The secret nonce.
* @returns The SampleZOwnable private state after setting the new nonce.
*/
- injectSecretNonce: (
+ injectSecretNonce: async (
newNonce: Buffer,
- ): SampleZOwnablePrivateState => {
- const currentState =
- this.circuitContextManager.getContext().currentPrivateState;
+ ): Promise => {
+ const currentState = await this.getPrivateState();
const updatedState = { ...currentState, secretNonce: newNonce };
- this.circuitContextManager.updatePrivateState(updatedState);
+ this.setPrivateState(updatedState);
return updatedState;
},
@@ -135,9 +140,8 @@ export class SampleZOwnableSimulator extends SampleZOwnableSimulatorBase {
* @description Returns the secret nonce given the context.
* @returns The secret nonce.
*/
- getCurrentSecretNonce: (): Uint8Array => {
- return this.circuitContextManager.getContext().currentPrivateState
- .secretNonce;
+ getCurrentSecretNonce: async (): Promise => {
+ return (await this.getPrivateState()).secretNonce;
},
};
}
diff --git a/packages/simulator/test/integration/Simple.test.ts b/packages/simulator/test/integration/Simple.test.ts
index d174d45..c1513d3 100644
--- a/packages/simulator/test/integration/Simple.test.ts
+++ b/packages/simulator/test/integration/Simple.test.ts
@@ -4,17 +4,17 @@ import { SimpleSimulator } from './SimpleSimulator';
let simple: SimpleSimulator;
describe('Simple test', () => {
- beforeEach(() => {
- simple = new SimpleSimulator();
+ beforeEach(async () => {
+ simple = await SimpleSimulator.create();
});
it('sanity check', () => {
expect(1).toEqual(1);
});
- it('should set val', () => {
+ it('should set val', async () => {
const VAL = 123n;
- simple.setVal(VAL);
- expect(simple.getVal()).toEqual(VAL);
+ await simple.setVal(VAL);
+ expect(await simple.getVal()).toEqual(VAL);
});
});
diff --git a/packages/simulator/test/integration/SimpleSimulator.ts b/packages/simulator/test/integration/SimpleSimulator.ts
index 411c5e9..6a2bf4c 100644
--- a/packages/simulator/test/integration/SimpleSimulator.ts
+++ b/packages/simulator/test/integration/SimpleSimulator.ts
@@ -1,4 +1,4 @@
-import { type BaseSimulatorOptions, createSimulator } from '../../src/index';
+import { createSimulator, type SimulatorOptions } from '../../src/index';
import {
ledger,
Contract as SimpleContract,
@@ -29,20 +29,21 @@ const SimpleSimulatorBase = createSimulator<
* Simple Simulator
*/
export class SimpleSimulator extends SimpleSimulatorBase {
- constructor(
- options: BaseSimulatorOptions<
+ static async create(
+ options: SimulatorOptions<
SimplePrivateState,
ReturnType
> = {},
- ) {
- super([], options);
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this`
+ return super.create([], options) as Promise;
}
- public setVal(n: bigint) {
- this.circuits.impure.setVal(n);
+ public setVal(n: bigint): Promise<[]> {
+ return this.circuits.impure.setVal(n);
}
- public getVal(): bigint {
+ public getVal(): Promise {
return this.circuits.impure.getVal();
}
}
diff --git a/packages/simulator/test/integration/Witness.test.ts b/packages/simulator/test/integration/Witness.test.ts
index c2e0519..147ee11 100644
--- a/packages/simulator/test/integration/Witness.test.ts
+++ b/packages/simulator/test/integration/Witness.test.ts
@@ -26,162 +26,187 @@ const overrideWitnesses = (): IWitnessWitnesses => ({
let contract: WitnessSimulator;
describe('witness/private state overrides', () => {
- beforeEach(() => {
- contract = new WitnessSimulator();
+ beforeEach(async () => {
+ contract = await WitnessSimulator.create();
});
describe('witness overrides', () => {
- it('should have default public state values', () => {
- expect(contract.getPublicState()._valBytes).toEqual(
+ it('should have default public state values', async () => {
+ expect((await contract.getPublicState())._valBytes).toEqual(
new Uint8Array(32).fill(0),
);
- expect(contract.getPublicState()._valField).toEqual(0n);
- expect(contract.getPublicState()._valUint).toEqual(0n);
+ expect((await contract.getPublicState())._valField).toEqual(0n);
+ expect((await contract.getPublicState())._valUint).toEqual(0n);
});
- it('should set values according to witness logic', () => {
+ it('should set values according to witness logic', async () => {
// Private state
- const psBytes = contract.getPrivateState().secretBytes;
- const psField = contract.getPrivateState().secretField;
- const psUint = contract.getPrivateState().secretUint;
+ const ps = await contract.getPrivateState();
+ const psBytes = ps.secretBytes;
+ const psField = ps.secretField;
+ const psUint = ps.secretUint;
// Set values
- contract.setBytes();
- contract.setField(VAL1);
- contract.setUint(VAL1, VAL2);
+ await contract.setBytes();
+ await contract.setField(VAL1);
+ await contract.setUint(VAL1, VAL2);
// Check values
- expect(contract.getPublicState()._valBytes).toEqual(
+ expect((await contract.getPublicState())._valBytes).toEqual(
new Uint8Array(psBytes),
);
- expect(contract.getPublicState()._valField).toEqual(psField + VAL1);
- expect(contract.getPublicState()._valUint).toEqual(psUint + VAL1 + VAL2);
+ expect((await contract.getPublicState())._valField).toEqual(
+ psField + VAL1,
+ );
+ expect((await contract.getPublicState())._valUint).toEqual(
+ psUint + VAL1 + VAL2,
+ );
});
- it('should override all witnesses', () => {
+ it('should override all witnesses', async () => {
// Private state
- const psBytes = contract.getPrivateState().secretBytes;
- const psField = contract.getPrivateState().secretField;
- const psUint = contract.getPrivateState().secretUint;
+ const ps = await contract.getPrivateState();
+ const psBytes = ps.secretBytes;
+ const psField = ps.secretField;
+ const psUint = ps.secretUint;
// Override entire object
contract.witnesses = overrideWitnesses();
// Set values
- contract.setBytes();
- contract.setField(VAL1);
- contract.setUint(VAL1, VAL2);
+ await contract.setBytes();
+ await contract.setField(VAL1);
+ await contract.setUint(VAL1, VAL2);
// Check bytes
- expect(contract.getPublicState()._valBytes).toEqual(BYTES_OVERRIDE);
- expect(contract.getPublicState()._valBytes).not.toEqual(
+ expect((await contract.getPublicState())._valBytes).toEqual(
+ BYTES_OVERRIDE,
+ );
+ expect((await contract.getPublicState())._valBytes).not.toEqual(
new Uint8Array(psBytes),
);
// Check field
- expect(contract.getPublicState()._valField).toEqual(FIELD_OVERRIDE);
- expect(contract.getPublicState()._valField).not.toEqual(psField + VAL1);
+ expect((await contract.getPublicState())._valField).toEqual(
+ FIELD_OVERRIDE,
+ );
+ expect((await contract.getPublicState())._valField).not.toEqual(
+ psField + VAL1,
+ );
// Check uint
- expect(contract.getPublicState()._valUint).toEqual(UINT_OVERRIDE);
- expect(contract.getPublicState()._valUint).not.toEqual(
+ expect((await contract.getPublicState())._valUint).toEqual(UINT_OVERRIDE);
+ expect((await contract.getPublicState())._valUint).not.toEqual(
psUint + VAL1 + VAL2,
);
});
describe('when overriding individual witnesses', () => {
- it('should override wit_secretBytes', () => {
+ it('should override wit_secretBytes', async () => {
// Private state
- const psBytes = contract.getPrivateState().secretBytes;
- const psField = contract.getPrivateState().secretField;
- const psUint = contract.getPrivateState().secretUint;
+ const ps = await contract.getPrivateState();
+ const psBytes = ps.secretBytes;
+ const psField = ps.secretField;
+ const psUint = ps.secretUint;
contract.overrideWitness('wit_secretBytes', (ctx) => {
return [ctx.privateState, BYTES_OVERRIDE];
});
// Set all values
- contract.setBytes();
- contract.setField(VAL1);
- contract.setUint(VAL1, VAL2);
+ await contract.setBytes();
+ await contract.setField(VAL1);
+ await contract.setUint(VAL1, VAL2);
// Check bytes override
- expect(contract.getPublicState()._valBytes).toEqual(BYTES_OVERRIDE);
- expect(contract.getPublicState()._valBytes).not.toEqual(
+ expect((await contract.getPublicState())._valBytes).toEqual(
+ BYTES_OVERRIDE,
+ );
+ expect((await contract.getPublicState())._valBytes).not.toEqual(
new Uint8Array(psBytes),
);
// Check other witnesses remain unchanged
- expect(contract.getPublicState()._valField).toEqual(psField + VAL1);
- expect(contract.getPublicState()._valUint).toEqual(
+ expect((await contract.getPublicState())._valField).toEqual(
+ psField + VAL1,
+ );
+ expect((await contract.getPublicState())._valUint).toEqual(
psUint + VAL1 + VAL2,
);
});
- it('should override wit_secretFieldPlusArg', () => {
+ it('should override wit_secretFieldPlusArg', async () => {
// Private state
- const psBytes = contract.getPrivateState().secretBytes;
- const _psField = contract.getPrivateState().secretField;
- const psUint = contract.getPrivateState().secretUint;
+ const ps = await contract.getPrivateState();
+ const psBytes = ps.secretBytes;
+ const psUint = ps.secretUint;
contract.overrideWitness('wit_secretFieldPlusArg', (ctx) => {
return [ctx.privateState, FIELD_OVERRIDE];
});
// Set all values
- contract.setBytes();
- contract.setField(VAL1);
- contract.setUint(VAL1, VAL2);
+ await contract.setBytes();
+ await contract.setField(VAL1);
+ await contract.setUint(VAL1, VAL2);
// Check field override
- expect(contract.getPublicState()._valField).toEqual(FIELD_OVERRIDE);
- expect(contract.getPublicState()._valField).not.toEqual(VAL1);
+ expect((await contract.getPublicState())._valField).toEqual(
+ FIELD_OVERRIDE,
+ );
+ expect((await contract.getPublicState())._valField).not.toEqual(VAL1);
// Check other witnesses remain unchanged
- expect(contract.getPublicState()._valBytes).toEqual(
+ expect((await contract.getPublicState())._valBytes).toEqual(
new Uint8Array(psBytes),
);
- expect(contract.getPublicState()._valUint).toEqual(
+ expect((await contract.getPublicState())._valUint).toEqual(
psUint + VAL1 + VAL2,
);
});
- it('should override wit_secretUintPlusArgs', () => {
+ it('should override wit_secretUintPlusArgs', async () => {
// Private state
- const psBytes = contract.getPrivateState().secretBytes;
- const psField = contract.getPrivateState().secretField;
- const psUint = contract.getPrivateState().secretUint;
+ const ps = await contract.getPrivateState();
+ const psBytes = ps.secretBytes;
+ const psField = ps.secretField;
+ const psUint = ps.secretUint;
contract.overrideWitness('wit_secretUintPlusArgs', (ctx) => {
return [ctx.privateState, UINT_OVERRIDE];
});
// Set all values
- contract.setBytes();
- contract.setField(VAL1);
- contract.setUint(VAL1, VAL2);
+ await contract.setBytes();
+ await contract.setField(VAL1);
+ await contract.setUint(VAL1, VAL2);
// Check uint override
- expect(contract.getPublicState()._valUint).toEqual(UINT_OVERRIDE);
- expect(contract.getPublicState()._valUint).not.toEqual(
+ expect((await contract.getPublicState())._valUint).toEqual(
+ UINT_OVERRIDE,
+ );
+ expect((await contract.getPublicState())._valUint).not.toEqual(
psUint + VAL1 + VAL2,
);
// Check other witnesses remain unchanged
- expect(contract.getPublicState()._valBytes).toEqual(
+ expect((await contract.getPublicState())._valBytes).toEqual(
new Uint8Array(psBytes),
);
- expect(contract.getPublicState()._valField).toEqual(psField + VAL1);
+ expect((await contract.getPublicState())._valField).toEqual(
+ psField + VAL1,
+ );
});
});
});
describe('private state overrides', () => {
- it('should match ps ', () => {
+ it('should match ps ', async () => {
// Private state
- const _psBytes = contract.getPrivateState().secretBytes;
- const _psField = contract.getPrivateState().secretField;
- const _psUint = contract.getPrivateState().secretUint;
+ const ps = await contract.getPrivateState();
+ void ps.secretBytes;
+ void ps.secretField;
+ void ps.secretUint;
});
it('should override the entire private state', () => {});
diff --git a/packages/simulator/test/integration/WitnessSimulator.ts b/packages/simulator/test/integration/WitnessSimulator.ts
index e03ac46..04e3026 100644
--- a/packages/simulator/test/integration/WitnessSimulator.ts
+++ b/packages/simulator/test/integration/WitnessSimulator.ts
@@ -1,4 +1,4 @@
-import { type BaseSimulatorOptions, createSimulator } from '../../src/index';
+import { createSimulator, type SimulatorOptions } from '../../src/index';
import {
ledger,
Contract as WitnessContract,
@@ -38,49 +38,49 @@ const WitnessSimulatorBase = createSimulator<
* Witness Simulator
*/
export class WitnessSimulator extends WitnessSimulatorBase {
- constructor(
- options: BaseSimulatorOptions<
+ static async create(
+ options: SimulatorOptions<
WitnessPrivateState,
ReturnType
> = {},
- ) {
- super([], options);
+ ): Promise {
+ // biome-ignore lint/complexity/noThisInStatic: super.create must keep the subclass `this`
+ return super.create([], options) as Promise;
}
- public setBytes() {
- this.circuits.impure.setBytes();
+ public setBytes(): Promise<[]> {
+ return this.circuits.impure.setBytes();
}
- public setField(arg: bigint) {
- this.circuits.impure.setField(arg);
+ public setField(arg: bigint): Promise<[]> {
+ return this.circuits.impure.setField(arg);
}
- public setUint(arg1: bigint, arg2: bigint) {
- this.circuits.impure.setUint(arg1, arg2);
+ public setUint(arg1: bigint, arg2: bigint): Promise<[]> {
+ return this.circuits.impure.setUint(arg1, arg2);
}
public readonly privateState = {
- injectSecretBytes: (
+ injectSecretBytes: async (
newBytes: Buffer,
- ): WitnessPrivateState => {
- const currentState =
- this.circuitContextManager.getContext().currentPrivateState;
+ ): Promise => {
+ const currentState = await this.getPrivateState();
const updatedState = { ...currentState, secretBytes: newBytes };
- this.circuitContextManager.updatePrivateState(updatedState);
+ this.setPrivateState(updatedState);
return updatedState;
},
- injectSecretField: (newField: bigint): WitnessPrivateState => {
- const currentState =
- this.circuitContextManager.getContext().currentPrivateState;
+ injectSecretField: async (
+ newField: bigint,
+ ): Promise => {
+ const currentState = await this.getPrivateState();
const updatedState = { ...currentState, secretField: newField };
- this.circuitContextManager.updatePrivateState(updatedState);
+ this.setPrivateState(updatedState);
return updatedState;
},
- injectSecretUint: (newUint: bigint): WitnessPrivateState => {
- const currentState =
- this.circuitContextManager.getContext().currentPrivateState;
+ injectSecretUint: async (newUint: bigint): Promise => {
+ const currentState = await this.getPrivateState();
const updatedState = { ...currentState, secretUint: newUint };
- this.circuitContextManager.updatePrivateState(updatedState);
+ this.setPrivateState(updatedState);
return updatedState;
},
};
diff --git a/packages/simulator/test/unit/LiveBackendAdapter.test.ts b/packages/simulator/test/unit/LiveBackendAdapter.test.ts
new file mode 100644
index 0000000..3951fb1
--- /dev/null
+++ b/packages/simulator/test/unit/LiveBackendAdapter.test.ts
@@ -0,0 +1,137 @@
+import type { StateValue } from '@midnight-ntwrk/compact-runtime';
+import { describe, expect, it } from 'vitest';
+import type { SyncSimulator } from '../../src/backend/DryBackend.js';
+import { LiveBackend } from '../../src/live/LiveBackend.js';
+import type {
+ DeployedTxHandle,
+ LiveContext,
+} from '../../src/live/LiveContext.js';
+import { Signers } from '../../src/signers/Signers.js';
+
+type Ledger = { tag: string };
+
+/** Records the alias passed to handleFor and serves scripted callTx results. */
+class FakeWorld implements LiveContext<{ secret: number }> {
+ readonly contractAddress = '0200cafef00d';
+ lastAlias: string | null | undefined;
+ private readonly callTx: DeployedTxHandle['callTx'];
+
+ constructor(callTx: DeployedTxHandle['callTx']) {
+ this.callTx = callTx;
+ }
+
+ async handleFor(alias: string | null): Promise {
+ this.lastAlias = alias;
+ return { callTx: this.callTx };
+ }
+
+ async queryLedger(): Promise {
+ return { tag: 'ledger-state' } as unknown as StateValue;
+ }
+
+ async queryPrivateState() {
+ return { secret: 7 };
+ }
+}
+
+/** Minimal pure-circuit evaluator: only `circuits.pure` is exercised by LiveBackend. */
+const fakePureSim = (
+ pure: Record unknown>,
+): SyncSimulator<{ secret: number }, Ledger> =>
+ ({ circuits: { pure, impure: {} } }) as unknown as SyncSimulator<
+ { secret: number },
+ Ledger
+ >;
+
+const makeBackend = (
+ callTx: DeployedTxHandle['callTx'],
+ pure: Record unknown> = {},
+ liveAliases: string[] = ['OWNER', 'ALICE'],
+) => {
+ const world = new FakeWorld(callTx);
+ const backend = new LiveBackend<{ secret: number }, Ledger>({
+ ctx: world,
+ pureSim: fakePureSim(pure),
+ signers: new Signers({ mode: 'live', liveAliases }),
+ ledgerExtractor: (state) => state as unknown as Ledger,
+ });
+ return { backend, world };
+};
+
+describe('LiveBackend adapter', () => {
+ it('runs pure circuits locally without touching the node', async () => {
+ const { backend, world } = makeBackend(
+ {},
+ { double: (n) => (n as bigint) * 2n },
+ );
+ expect(await backend.call('pure', 'double', [21n])).toEqual(42n);
+ // No impure handle was ever requested.
+ expect(world.lastAlias).toBeUndefined();
+ });
+
+ it('normalizes impure results from .private.result to bare R', async () => {
+ const { backend } = makeBackend({
+ owner: async () => ({ private: { result: 'OWNER_COMMITMENT' } }),
+ });
+ expect(await backend.call('impure', 'owner', [])).toEqual(
+ 'OWNER_COMMITMENT',
+ );
+ });
+
+ it('propagates the contract assert message as a substring', async () => {
+ const { backend } = makeBackend({
+ guarded: async () => {
+ throw new Error(
+ 'AccessControl: unauthorized account (+ proof/tx framing)',
+ );
+ },
+ });
+ await expect(backend.call('impure', 'guarded', [])).rejects.toThrow(
+ 'AccessControl: unauthorized account',
+ );
+ });
+
+ it('applies single-shot caller for one call, then reverts', async () => {
+ const { backend, world } = makeBackend({
+ noop: async () => ({ private: { result: undefined } }),
+ });
+ backend.setCaller('OWNER', 'single');
+ await backend.call('impure', 'noop', []);
+ expect(world.lastAlias).toBe('OWNER');
+
+ await backend.call('impure', 'noop', []);
+ expect(world.lastAlias).toBeNull();
+ });
+
+ it('keeps a persistent caller across calls', async () => {
+ const { backend, world } = makeBackend({
+ noop: async () => ({ private: { result: undefined } }),
+ });
+ backend.setCaller('ALICE', 'persistent');
+ await backend.call('impure', 'noop', []);
+ await backend.call('impure', 'noop', []);
+ expect(world.lastAlias).toBe('ALICE');
+ });
+
+ it('rejects callers outside the prefunded pool', () => {
+ const { backend } = makeBackend({});
+ expect(() => backend.setCaller('STRANGER', 'single')).toThrow(
+ 'not in the prefunded pool',
+ );
+ });
+
+ it('hard-errors on witness override / setWitnesses', () => {
+ const { backend } = makeBackend({});
+ expect(() => backend.overrideWitness('w', () => {})).toThrow(
+ 'witness override unsupported on live backend',
+ );
+ expect(() => backend.setWitnesses({})).toThrow(
+ 'witness override unsupported on live backend',
+ );
+ });
+
+ it('reads private state through the provider', async () => {
+ const { backend } = makeBackend({});
+ expect(await backend.getPrivateState()).toEqual({ secret: 7 });
+ });
+});
diff --git a/packages/simulator/test/unit/Signers.test.ts b/packages/simulator/test/unit/Signers.test.ts
new file mode 100644
index 0000000..a774c29
--- /dev/null
+++ b/packages/simulator/test/unit/Signers.test.ts
@@ -0,0 +1,66 @@
+import { describe, expect, it } from 'vitest';
+import { MAX_LIVE_SIGNERS, Signers } from '../../src/signers/Signers.js';
+
+/** The existing harness's alias derivation, reproduced for the parity check. */
+const expectedDryKey = (alias: string): string =>
+ Buffer.from(alias, 'ascii').toString('hex').padStart(64, '0');
+
+describe('Signers — dry derivation', () => {
+ const signers = new Signers({ mode: 'dry' });
+
+ it('derives the same key the existing test harness uses', async () => {
+ expect(await signers.keyFor('OWNER')).toEqual(expectedDryKey('OWNER'));
+ expect(signers.resolveDryKey('OWNER')).toEqual(expectedDryKey('OWNER'));
+ });
+
+ it('honors an explicit alias→key override', async () => {
+ const custom = new Signers({
+ mode: 'dry',
+ dryKeys: { OWNER: 'ff'.repeat(32) },
+ });
+ expect(await custom.keyFor('OWNER')).toEqual('ff'.repeat(32));
+ // Unmapped aliases fall back to the default derivation.
+ expect(await custom.keyFor('ALICE')).toEqual(expectedDryKey('ALICE'));
+ });
+
+ it('wraps the key in a left-variant Either for circuit args', async () => {
+ const either = await signers.eitherFor('OWNER');
+ expect(either.is_left).toBe(true);
+ expect(either.left.bytes).toBeInstanceOf(Uint8Array);
+ });
+});
+
+describe('Signers — live cap', () => {
+ it(`allows up to ${MAX_LIVE_SIGNERS} prefunded aliases`, () => {
+ expect(
+ () =>
+ new Signers({
+ mode: 'live',
+ liveAliases: ['DEPLOYER', 'OWNER', 'ALICE', 'BOB'],
+ }),
+ ).not.toThrow();
+ });
+
+ it('rejects a pool larger than the cap at construction', () => {
+ expect(
+ () =>
+ new Signers({
+ mode: 'live',
+ liveAliases: ['DEPLOYER', 'OWNER', 'ALICE', 'BOB', 'CAROL'],
+ }),
+ ).toThrow(`at most ${MAX_LIVE_SIGNERS}`);
+ });
+
+ it('rejects an alias outside the pool, never silently reusing a wallet', () => {
+ const signers = new Signers({ mode: 'live', liveAliases: ['OWNER'] });
+ expect(() => signers.assertLiveAliasAllowed('STRANGER')).toThrow(
+ 'not in the prefunded pool',
+ );
+ expect(() => signers.assertLiveAliasAllowed('OWNER')).not.toThrow();
+ });
+
+ it('is a no-op in dry mode', () => {
+ const signers = new Signers({ mode: 'dry' });
+ expect(() => signers.assertLiveAliasAllowed('ANYONE')).not.toThrow();
+ });
+});
diff --git a/packages/simulator/test/unit/dependency-wall.test.ts b/packages/simulator/test/unit/dependency-wall.test.ts
new file mode 100644
index 0000000..9d0bb71
--- /dev/null
+++ b/packages/simulator/test/unit/dependency-wall.test.ts
@@ -0,0 +1,56 @@
+import { readdirSync, readFileSync } from 'node:fs';
+import { dirname, join, relative, sep } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, expect, it } from 'vitest';
+
+const here = dirname(fileURLToPath(import.meta.url));
+const SRC_DIR = join(here, '..', '..', 'src');
+const LIVE_DIR = join(SRC_DIR, 'live');
+// Trailing separator so the prefix match can't leak to a sibling like `live2/`.
+const LIVE_PREFIX = `${LIVE_DIR}${sep}`;
+
+/** All `.ts` files under a directory, recursively. */
+function tsFiles(dir: string): string[] {
+ const out: string[] = [];
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
+ const full = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ out.push(...tsFiles(full));
+ } else if (entry.name.endsWith('.ts')) {
+ out.push(full);
+ }
+ }
+ return out;
+}
+
+const MIDNIGHT_JS = '@midnight-ntwrk/midnight-js';
+
+/**
+ * Dependency-wall enforcement (the CI guard from OQ6).
+ *
+ * The dry dependency graph must pull zero midnight-js. We enforce the structural
+ * precondition: every midnight-js import is physically confined to `src/live/`.
+ * Any reference elsewhere — even a `type` import — is flagged, since a stray
+ * value re-export is exactly how the wall silently falls.
+ *
+ * A stronger bundle/dependency-graph analysis (the other OQ6 option) can layer
+ * on top; this source-level guard is the fast, deterministic floor.
+ */
+describe('dependency wall', () => {
+ it('confines every midnight-js import to src/live/', () => {
+ const offenders = tsFiles(SRC_DIR)
+ .filter((file) => !file.startsWith(LIVE_PREFIX))
+ .filter((file) => readFileSync(file, 'utf8').includes(MIDNIGHT_JS))
+ .map((file) => relative(SRC_DIR, file));
+
+ expect(offenders).toEqual([]);
+ });
+
+ it('keeps midnight-js out of the main barrel', () => {
+ const barrel = readFileSync(join(SRC_DIR, 'index.ts'), 'utf8');
+ expect(barrel.includes(MIDNIGHT_JS)).toBe(false);
+ // The live adapter must be a type-only re-export, never a value re-export.
+ expect(barrel).toContain('export type { LiveBackend }');
+ expect(barrel).not.toMatch(/export\s*\{\s*LiveBackend\b/);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index 1777295..5941fc6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -105,6 +105,19 @@ __metadata:
languageName: node
linkType: hard
+"@effect/platform@npm:^0.95.0":
+ version: 0.95.0
+ resolution: "@effect/platform@npm:0.95.0"
+ dependencies:
+ find-my-way-ts: "npm:^0.1.6"
+ msgpackr: "npm:^1.11.4"
+ multipasta: "npm:^0.2.7"
+ peerDependencies:
+ effect: ^3.20.0
+ checksum: 10/ae3f3bd441f77bb0f3bb71f954d3a06be2565e4d924eba8c7d5c898da32d893f42c4af0e5c6fee5a1ba087ab7d2d1dae8734a4b1e830baeb654fcccd63c996bb
+ languageName: node
+ linkType: hard
+
"@emnapi/core@npm:1.10.0":
version: 1.10.0
resolution: "@emnapi/core@npm:1.10.0"
@@ -166,6 +179,20 @@ __metadata:
languageName: node
linkType: hard
+"@midnight-ntwrk/compact-js@npm:2.5.1":
+ version: 2.5.1
+ resolution: "@midnight-ntwrk/compact-js@npm:2.5.1"
+ dependencies:
+ "@effect/platform": "npm:^0.95.0"
+ "@midnight-ntwrk/compact-runtime": "npm:0.16.0"
+ "@midnight-ntwrk/ledger-v8": "npm:^8.0.3"
+ "@midnight-ntwrk/platform-js": "npm:^2.2.4"
+ effect: "npm:^3.20.0"
+ tslib: "npm:^2.8.1"
+ checksum: 10/ee041b88d8fd43dc63f8cbb6b02f2eb0d6445921633b032e1dd3e909c75be8ca8f311cee70d16a9d06795bbb94172d2a6797799ee3870f29a8aa42e7e3e153b6
+ languageName: node
+ linkType: hard
+
"@midnight-ntwrk/compact-runtime@npm:0.16.0":
version: 0.16.0
resolution: "@midnight-ntwrk/compact-runtime@npm:0.16.0"
@@ -177,20 +204,139 @@ __metadata:
languageName: node
linkType: hard
-"@midnight-ntwrk/ledger-v8@npm:8.1.0":
+"@midnight-ntwrk/ledger-v8@npm:8.1.0, @midnight-ntwrk/ledger-v8@npm:^8.0.3, @midnight-ntwrk/ledger-v8@npm:^8.1.0":
version: 8.1.0
resolution: "@midnight-ntwrk/ledger-v8@npm:8.1.0"
checksum: 10/10d56076b0333a502f157c816f8cfebefc8d50221cb20c6db15abcbf2d0092bdaf7e9bc1bd19a6d9f51455547c713c916cb16d4a7d18e83cba0e172ad6e2a507
languageName: node
linkType: hard
-"@midnight-ntwrk/onchain-runtime-v3@npm:^3.0.0":
+"@midnight-ntwrk/midnight-js-contracts@npm:^4.1.0":
+ version: 4.1.1
+ resolution: "@midnight-ntwrk/midnight-js-contracts@npm:4.1.1"
+ dependencies:
+ "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.1"
+ "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1"
+ "@midnight-ntwrk/midnight-js-types": "npm:4.1.1"
+ "@midnight-ntwrk/midnight-js-utils": "npm:4.1.1"
+ checksum: 10/93791613419dd914cbd4db6c0bc75cebb093962e4162a391d298e2ae705e53c2e87a40fa923fd9a9a6a31cdb91bede0836673beea14d4a594e15da250a1b4feb
+ languageName: node
+ linkType: hard
+
+"@midnight-ntwrk/midnight-js-network-id@npm:4.1.1":
+ version: 4.1.1
+ resolution: "@midnight-ntwrk/midnight-js-network-id@npm:4.1.1"
+ checksum: 10/ac2f06da0d3bdec6ee83fe84312d8d012b398dad8e23896727c8de10df51eefeeff8eff2de29079c534e090f7cc8520f6e0763a324b560dfe1a0f1d55f2ca2a3
+ languageName: node
+ linkType: hard
+
+"@midnight-ntwrk/midnight-js-protocol@npm:4.1.1":
+ version: 4.1.1
+ resolution: "@midnight-ntwrk/midnight-js-protocol@npm:4.1.1"
+ dependencies:
+ "@midnight-ntwrk/compact-js": "npm:2.5.1"
+ "@midnight-ntwrk/compact-runtime": "npm:0.16.0"
+ "@midnight-ntwrk/ledger-v8": "npm:8.1.0"
+ "@midnight-ntwrk/onchain-runtime-v3": "npm:3.0.0"
+ "@midnight-ntwrk/platform-js": "npm:2.2.4"
+ checksum: 10/bfd6195e90b8c0fbc178b32ff9f8223f377d48a55a4a6fdedba424ede77c2234eac75555263b73a61f3ca14d2cc27ed935b3efae15daa6a475a00db4a696e7a1
+ languageName: node
+ linkType: hard
+
+"@midnight-ntwrk/midnight-js-types@npm:4.1.1, @midnight-ntwrk/midnight-js-types@npm:^4.1.0":
+ version: 4.1.1
+ resolution: "@midnight-ntwrk/midnight-js-types@npm:4.1.1"
+ dependencies:
+ "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1"
+ effect: "npm:^3.20.0"
+ pino: "npm:^10.3.1"
+ rxjs: "npm:^7.8.2"
+ checksum: 10/a11a994f0838968954f01146c9f5f65f29c8e30fe305401a3f0cef15b6a75802078d2c8c4abe6ef743585a12a05b4d9d30dbe1bb7177699cd2623302fb2b619e
+ languageName: node
+ linkType: hard
+
+"@midnight-ntwrk/midnight-js-utils@npm:4.1.1":
+ version: 4.1.1
+ resolution: "@midnight-ntwrk/midnight-js-utils@npm:4.1.1"
+ dependencies:
+ "@midnight-ntwrk/midnight-js-network-id": "npm:4.1.1"
+ "@midnight-ntwrk/midnight-js-protocol": "npm:4.1.1"
+ "@midnight-ntwrk/wallet-sdk-address-format": "npm:^3.1.0"
+ checksum: 10/73f3ab682bd0ea37b988bc34e8036bc03d748b1df534ea57764c7fc656a235ea3287ca55481d57be956455ff4a8334e805c99d87d95c998f1879b5b7743dc18c
+ languageName: node
+ linkType: hard
+
+"@midnight-ntwrk/onchain-runtime-v3@npm:3.0.0, @midnight-ntwrk/onchain-runtime-v3@npm:^3.0.0":
version: 3.0.0
resolution: "@midnight-ntwrk/onchain-runtime-v3@npm:3.0.0"
checksum: 10/873aeb9e631c3678373c62b5aef847de454de94427028fb3d3f28bfdc8b2c02a3c770bd79d9bfef183eb9db6fb8c23e6826636f2e512ffd6eacbcf7cc0651c5d
languageName: node
linkType: hard
+"@midnight-ntwrk/platform-js@npm:2.2.4, @midnight-ntwrk/platform-js@npm:^2.2.4":
+ version: 2.2.4
+ resolution: "@midnight-ntwrk/platform-js@npm:2.2.4"
+ dependencies:
+ "@effect/platform": "npm:^0.95.0"
+ effect: "npm:^3.20.0"
+ tslib: "npm:^2.8.1"
+ checksum: 10/1650bb7e54a64740aaaf27f7e84b7bffdb08611c994bbf54208db43a0a11d10ea8994f05d82e848d60d6fcee8a9b3a5db770d306262b99547e71185d52614825
+ languageName: node
+ linkType: hard
+
+"@midnight-ntwrk/wallet-sdk-address-format@npm:^3.1.0":
+ version: 3.1.2
+ resolution: "@midnight-ntwrk/wallet-sdk-address-format@npm:3.1.2"
+ dependencies:
+ "@midnight-ntwrk/ledger-v8": "npm:^8.1.0"
+ "@scure/base": "npm:^2.0.0"
+ "@subsquid/scale-codec": "npm:^4.0.1"
+ checksum: 10/f3e2374c1dd8e31310aa464fa2afecca3cca92b923a999bfcba2922225b907e5387b94a70e6a8c06e8fb9d51fd9140a08c827c13f8a9191fd18d597cb5ab7b0c
+ languageName: node
+ linkType: hard
+
+"@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.4":
+ version: 3.0.4
+ resolution: "@msgpackr-extract/msgpackr-extract-darwin-arm64@npm:3.0.4"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.4":
+ version: 3.0.4
+ resolution: "@msgpackr-extract/msgpackr-extract-darwin-x64@npm:3.0.4"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
+"@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.4":
+ version: 3.0.4
+ resolution: "@msgpackr-extract/msgpackr-extract-linux-arm64@npm:3.0.4"
+ conditions: os=linux & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.4":
+ version: 3.0.4
+ resolution: "@msgpackr-extract/msgpackr-extract-linux-arm@npm:3.0.4"
+ conditions: os=linux & cpu=arm
+ languageName: node
+ linkType: hard
+
+"@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.4":
+ version: 3.0.4
+ resolution: "@msgpackr-extract/msgpackr-extract-linux-x64@npm:3.0.4"
+ conditions: os=linux & cpu=x64
+ languageName: node
+ linkType: hard
+
+"@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.4":
+ version: 3.0.4
+ resolution: "@msgpackr-extract/msgpackr-extract-win32-x64@npm:3.0.4"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
"@napi-rs/wasm-runtime@npm:^1.1.4":
version: 1.1.4
resolution: "@napi-rs/wasm-runtime@npm:1.1.4"
@@ -242,11 +388,21 @@ __metadata:
dependencies:
"@midnight-ntwrk/compact-runtime": "npm:0.16.0"
"@midnight-ntwrk/ledger-v8": "npm:8.1.0"
+ "@midnight-ntwrk/midnight-js-contracts": "npm:^4.1.0"
+ "@midnight-ntwrk/midnight-js-types": "npm:^4.1.0"
"@tsconfig/node24": "npm:^24.0.3"
"@types/node": "npm:25.9.1"
fast-check: "npm:^4.5.2"
typescript: "npm:^6.0.3"
vitest: "npm:^4.1.6"
+ peerDependencies:
+ "@midnight-ntwrk/midnight-js-contracts": ^4.1.0
+ "@midnight-ntwrk/midnight-js-types": ^4.1.0
+ peerDependenciesMeta:
+ "@midnight-ntwrk/midnight-js-contracts":
+ optional: true
+ "@midnight-ntwrk/midnight-js-types":
+ optional: true
languageName: unknown
linkType: soft
@@ -257,6 +413,13 @@ __metadata:
languageName: node
linkType: hard
+"@pinojs/redact@npm:^0.4.0":
+ version: 0.4.0
+ resolution: "@pinojs/redact@npm:0.4.0"
+ checksum: 10/2210ffb6b38357853d47239fd0532cc9edb406325270a81c440a35cece22090127c30c2ead3eefa3e608f2244087485308e515c431f4f69b6bd2e16cbd32812b
+ languageName: node
+ linkType: hard
+
"@rolldown/binding-android-arm64@npm:1.0.2":
version: 1.0.2
resolution: "@rolldown/binding-android-arm64@npm:1.0.2"
@@ -373,13 +536,46 @@ __metadata:
languageName: node
linkType: hard
-"@standard-schema/spec@npm:^1.1.0":
+"@scure/base@npm:^2.0.0":
+ version: 2.2.0
+ resolution: "@scure/base@npm:2.2.0"
+ checksum: 10/b52ec9cd54bad77e22f881b6924ccab692dc1c6dd10287d1787bf263e9f1e560d6d2bda906538fb9a39615d61a1b5c2f53f57a511667fd10e93b9cdaa6fb5d2a
+ languageName: node
+ linkType: hard
+
+"@standard-schema/spec@npm:^1.0.0, @standard-schema/spec@npm:^1.1.0":
version: 1.1.0
resolution: "@standard-schema/spec@npm:1.1.0"
checksum: 10/a209615c9e8b2ea535d7db0a5f6aa0f962fd4ab73ee86a46c100fb78116964af1f55a27c1794d4801e534a196794223daa25ff5135021e03c7828aa3d95e1763
languageName: node
linkType: hard
+"@subsquid/scale-codec@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "@subsquid/scale-codec@npm:4.0.1"
+ dependencies:
+ "@subsquid/util-internal-hex": "npm:^1.2.2"
+ "@subsquid/util-internal-json": "npm:^1.2.2"
+ checksum: 10/d0c81f43c6c93d6885baa0992dd170c94e8259b2eb500694b62b8ca25624c78bb7e4815b1120bbb7f3ed0e7eda02cd02233e1d8b5bac903322731ff3c9fb42bc
+ languageName: node
+ linkType: hard
+
+"@subsquid/util-internal-hex@npm:^1.2.2":
+ version: 1.2.3
+ resolution: "@subsquid/util-internal-hex@npm:1.2.3"
+ checksum: 10/d3feeb16e130d7a5281bbd98c0ddc9a44d3c49f2655766d4e97d16407c8466b3b246bbefecfb397580f2402dc62b45065c8e62ce986b14935246b1252e66d347
+ languageName: node
+ linkType: hard
+
+"@subsquid/util-internal-json@npm:^1.2.2":
+ version: 1.2.3
+ resolution: "@subsquid/util-internal-json@npm:1.2.3"
+ dependencies:
+ "@subsquid/util-internal-hex": "npm:^1.2.2"
+ checksum: 10/9a518c8fc56066778b0535ed243024e17f958d9020d99d5444657fd877d7da3adc1f34b3f0e621cb8365729bc9e10aeb63bb24b91e579eb413ef8cbbab66c81d
+ languageName: node
+ linkType: hard
+
"@tsconfig/node10@npm:^1.0.7":
version: 1.0.12
resolution: "@tsconfig/node10@npm:1.0.12"
@@ -641,6 +837,13 @@ __metadata:
languageName: node
linkType: hard
+"atomic-sleep@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "atomic-sleep@npm:1.0.0"
+ checksum: 10/3ab6d2cf46b31394b4607e935ec5c1c3c4f60f3e30f0913d35ea74b51b3585e84f590d09e58067f11762eec71c87d25314ce859030983dc0e4397eed21daa12e
+ languageName: node
+ linkType: hard
+
"chai@npm:^6.2.2":
version: 6.2.2
resolution: "chai@npm:6.2.2"
@@ -705,7 +908,7 @@ __metadata:
languageName: node
linkType: hard
-"detect-libc@npm:^2.0.3":
+"detect-libc@npm:^2.0.1, detect-libc@npm:^2.0.3":
version: 2.1.2
resolution: "detect-libc@npm:2.1.2"
checksum: 10/b736c8d97d5d46164c0d1bed53eb4e6a3b1d8530d460211e2d52f1c552875e706c58a5376854e4e54f8b828c9cada58c855288c968522eb93ac7696d65970766
@@ -719,6 +922,16 @@ __metadata:
languageName: node
linkType: hard
+"effect@npm:^3.20.0":
+ version: 3.21.4
+ resolution: "effect@npm:3.21.4"
+ dependencies:
+ "@standard-schema/spec": "npm:^1.0.0"
+ fast-check: "npm:^3.23.1"
+ checksum: 10/c95506043e070662af85963af779b3d4e9ec501e03069532888831cc268e0fd9f448dcd2a4c89809e05471e64dcd270313b196056f878d1c028037870694af11
+ languageName: node
+ linkType: hard
+
"env-paths@npm:^2.2.0":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
@@ -756,6 +969,15 @@ __metadata:
languageName: node
linkType: hard
+"fast-check@npm:^3.23.1":
+ version: 3.23.2
+ resolution: "fast-check@npm:3.23.2"
+ dependencies:
+ pure-rand: "npm:^6.1.0"
+ checksum: 10/dab344146b778e8bc2973366ea55528d1b58d3e3037270262b877c54241e800c4d744957722c24705c787020d702aece11e57c9e3dbd5ea19c3e10926bf1f3fe
+ languageName: node
+ linkType: hard
+
"fast-check@npm:^4.5.2":
version: 4.8.0
resolution: "fast-check@npm:4.8.0"
@@ -777,6 +999,13 @@ __metadata:
languageName: node
linkType: hard
+"find-my-way-ts@npm:^0.1.6":
+ version: 0.1.6
+ resolution: "find-my-way-ts@npm:0.1.6"
+ checksum: 10/b95bf644011f0d341e5963aa4cac55b2ee59e2435d3f65ae5cf9ee80e52f0fc7db0cee9a55e7420a62a2cec7d8bec7538399dada45e024c05488daa754451bcc
+ languageName: node
+ linkType: hard
+
"fsevents@npm:~2.3.3":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
@@ -1000,6 +1229,56 @@ __metadata:
languageName: node
linkType: hard
+"msgpackr-extract@npm:^3.0.2":
+ version: 3.0.4
+ resolution: "msgpackr-extract@npm:3.0.4"
+ dependencies:
+ "@msgpackr-extract/msgpackr-extract-darwin-arm64": "npm:3.0.4"
+ "@msgpackr-extract/msgpackr-extract-darwin-x64": "npm:3.0.4"
+ "@msgpackr-extract/msgpackr-extract-linux-arm": "npm:3.0.4"
+ "@msgpackr-extract/msgpackr-extract-linux-arm64": "npm:3.0.4"
+ "@msgpackr-extract/msgpackr-extract-linux-x64": "npm:3.0.4"
+ "@msgpackr-extract/msgpackr-extract-win32-x64": "npm:3.0.4"
+ node-gyp: "npm:latest"
+ node-gyp-build-optional-packages: "npm:5.2.2"
+ dependenciesMeta:
+ "@msgpackr-extract/msgpackr-extract-darwin-arm64":
+ optional: true
+ "@msgpackr-extract/msgpackr-extract-darwin-x64":
+ optional: true
+ "@msgpackr-extract/msgpackr-extract-linux-arm":
+ optional: true
+ "@msgpackr-extract/msgpackr-extract-linux-arm64":
+ optional: true
+ "@msgpackr-extract/msgpackr-extract-linux-x64":
+ optional: true
+ "@msgpackr-extract/msgpackr-extract-win32-x64":
+ optional: true
+ bin:
+ download-msgpackr-prebuilds: bin/download-prebuilds.js
+ checksum: 10/05a66482eca3c7932afef4300abc0cccfbb002185506d85d77fd2cff63870d58ef6903fef879ea09ff76ed0c18c9282a0dceb0d621a58e3c02adc9e0bfb8eb33
+ languageName: node
+ linkType: hard
+
+"msgpackr@npm:^1.11.4":
+ version: 1.12.1
+ resolution: "msgpackr@npm:1.12.1"
+ dependencies:
+ msgpackr-extract: "npm:^3.0.2"
+ dependenciesMeta:
+ msgpackr-extract:
+ optional: true
+ checksum: 10/90f5eabb2b059f441714ec0b09cfccea589caf36604598ae44a023e6c113f97889ca6c899beb318bc1a6bb7d6b4d6bef75a6fc13a01aa49c3111ae08da2c6db5
+ languageName: node
+ linkType: hard
+
+"multipasta@npm:^0.2.7":
+ version: 0.2.7
+ resolution: "multipasta@npm:0.2.7"
+ checksum: 10/244a7194ff508b3c5c1724f11c303f1c446cf6142cdbe82e57d5e59c44abb4942b1b983dd8c0d9c63080e684b2a8fa10f511df70d42dbef4d215ed7d41e76fcc
+ languageName: node
+ linkType: hard
+
"nanoid@npm:^3.3.12":
version: 3.3.12
resolution: "nanoid@npm:3.3.12"
@@ -1009,6 +1288,19 @@ __metadata:
languageName: node
linkType: hard
+"node-gyp-build-optional-packages@npm:5.2.2":
+ version: 5.2.2
+ resolution: "node-gyp-build-optional-packages@npm:5.2.2"
+ dependencies:
+ detect-libc: "npm:^2.0.1"
+ bin:
+ node-gyp-build-optional-packages: bin.js
+ node-gyp-build-optional-packages-optional: optional.js
+ node-gyp-build-optional-packages-test: build-test.js
+ checksum: 10/f448a328cf608071dc8cc4426ac5be0daec4788e4e1759e9f7ffcd286822cc799384edce17a8c79e610c4bbfc8e3aff788f3681f1d88290e0ca7aaa5342a090f
+ languageName: node
+ linkType: hard
+
"node-gyp@npm:latest":
version: 12.3.0
resolution: "node-gyp@npm:12.3.0"
@@ -1054,6 +1346,13 @@ __metadata:
languageName: node
linkType: hard
+"on-exit-leak-free@npm:^2.1.0":
+ version: 2.1.2
+ resolution: "on-exit-leak-free@npm:2.1.2"
+ checksum: 10/f7b4b7200026a08f6e4a17ba6d72e6c5cbb41789ed9cf7deaf9d9e322872c7dc5a7898549a894651ee0ee9ae635d34a678115bf8acdfba8ebd2ba2af688b563c
+ languageName: node
+ linkType: hard
+
"onetime@npm:^7.0.0":
version: 7.0.0
resolution: "onetime@npm:7.0.0"
@@ -1100,6 +1399,43 @@ __metadata:
languageName: node
linkType: hard
+"pino-abstract-transport@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "pino-abstract-transport@npm:3.0.0"
+ dependencies:
+ split2: "npm:^4.0.0"
+ checksum: 10/f42b85b2663c8520839124a55b27801e88c89c65e9569384b49bb4c81b022ae24860020c2375b92a03db699113969007cc155e1fb2dfe53754403920c1cbe18c
+ languageName: node
+ linkType: hard
+
+"pino-std-serializers@npm:^7.0.0":
+ version: 7.1.0
+ resolution: "pino-std-serializers@npm:7.1.0"
+ checksum: 10/6e27f6f885927b6df3b424ddb8a9e0e9854f3b59f4abd51afa74e1c2cf33436a505277b004bb00ce61884a962c8fdfd977391205c7baab885d6afb35fce7396a
+ languageName: node
+ linkType: hard
+
+"pino@npm:^10.3.1":
+ version: 10.3.1
+ resolution: "pino@npm:10.3.1"
+ dependencies:
+ "@pinojs/redact": "npm:^0.4.0"
+ atomic-sleep: "npm:^1.0.0"
+ on-exit-leak-free: "npm:^2.1.0"
+ pino-abstract-transport: "npm:^3.0.0"
+ pino-std-serializers: "npm:^7.0.0"
+ process-warning: "npm:^5.0.0"
+ quick-format-unescaped: "npm:^4.0.3"
+ real-require: "npm:^0.2.0"
+ safe-stable-stringify: "npm:^2.3.1"
+ sonic-boom: "npm:^4.0.1"
+ thread-stream: "npm:^4.0.0"
+ bin:
+ pino: bin.js
+ checksum: 10/46cad7bf1859c83a8a9c43af764e5165f44057ce76d44b1b2b4390f2abccb8a579f42abfe742d88b4d8e1d339213afb46ea50fc39c50095dd1f0f9fe26ea1342
+ languageName: node
+ linkType: hard
+
"postcss@npm:^8.5.15":
version: 8.5.15
resolution: "postcss@npm:8.5.15"
@@ -1118,6 +1454,20 @@ __metadata:
languageName: node
linkType: hard
+"process-warning@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "process-warning@npm:5.0.0"
+ checksum: 10/10f3e00ac9fc1943ec4566ff41fff2b964e660f853c283e622257719839d340b4616e707d62a02d6aa0038761bb1fa7c56bc7308d602d51bd96f05f9cd305dcd
+ languageName: node
+ linkType: hard
+
+"pure-rand@npm:^6.1.0":
+ version: 6.1.0
+ resolution: "pure-rand@npm:6.1.0"
+ checksum: 10/256aa4bcaf9297256f552914e03cbdb0039c8fe1db11fa1e6d3f80790e16e563eb0a859a1e61082a95e224fc0c608661839439f8ecc6a3db4e48d46d99216ee4
+ languageName: node
+ linkType: hard
+
"pure-rand@npm:^8.0.0":
version: 8.4.0
resolution: "pure-rand@npm:8.4.0"
@@ -1125,6 +1475,27 @@ __metadata:
languageName: node
linkType: hard
+"quick-format-unescaped@npm:^4.0.3":
+ version: 4.0.4
+ resolution: "quick-format-unescaped@npm:4.0.4"
+ checksum: 10/591eca457509a99368b623db05248c1193aa3cedafc9a077d7acab09495db1231017ba3ad1b5386e5633271edd0a03b312d8640a59ee585b8516a42e15438aa7
+ languageName: node
+ linkType: hard
+
+"real-require@npm:^0.2.0":
+ version: 0.2.0
+ resolution: "real-require@npm:0.2.0"
+ checksum: 10/ddf44ee76301c774e9c9f2826da8a3c5c9f8fc87310f4a364e803ef003aa1a43c378b4323051ced212097fff1af459070f4499338b36a7469df1d4f7e8c0ba4c
+ languageName: node
+ linkType: hard
+
+"real-require@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "real-require@npm:1.0.0"
+ checksum: 10/ac2ae7681e20c92be45a5f49110414af1576c7b4512869c2260076a69fc7c336335ef354f466a3be92e779c55b8df0b0043d191797d82d7f18e6310958e5a890
+ languageName: node
+ linkType: hard
+
"restore-cursor@npm:^5.0.0":
version: 5.1.0
resolution: "restore-cursor@npm:5.1.0"
@@ -1193,6 +1564,22 @@ __metadata:
languageName: node
linkType: hard
+"rxjs@npm:^7.8.2":
+ version: 7.8.2
+ resolution: "rxjs@npm:7.8.2"
+ dependencies:
+ tslib: "npm:^2.1.0"
+ checksum: 10/03dff09191356b2b87d94fbc1e97c4e9eb3c09d4452399dddd451b09c2f1ba8d56925a40af114282d7bc0c6fe7514a2236ca09f903cf70e4bbf156650dddb49d
+ languageName: node
+ linkType: hard
+
+"safe-stable-stringify@npm:^2.3.1":
+ version: 2.5.0
+ resolution: "safe-stable-stringify@npm:2.5.0"
+ checksum: 10/2697fa186c17c38c3ca5309637b4ac6de2f1c3d282da27cd5e1e3c88eca0fb1f9aea568a6aabdf284111592c8782b94ee07176f17126031be72ab1313ed46c5c
+ languageName: node
+ linkType: hard
+
"semver@npm:^7.3.5":
version: 7.8.0
resolution: "semver@npm:7.8.0"
@@ -1223,6 +1610,15 @@ __metadata:
languageName: node
linkType: hard
+"sonic-boom@npm:^4.0.1":
+ version: 4.2.1
+ resolution: "sonic-boom@npm:4.2.1"
+ dependencies:
+ atomic-sleep: "npm:^1.0.0"
+ checksum: 10/161af46b3e6debc4ad3865b0db47f37289741a0b3005b8cf056f93a4e0e1a347e24ca1a2d8ccc864f7f19caa6185a766797f8382cdbfd2f3d046a0323d73a542
+ languageName: node
+ linkType: hard
+
"source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
@@ -1230,6 +1626,13 @@ __metadata:
languageName: node
linkType: hard
+"split2@npm:^4.0.0":
+ version: 4.2.0
+ resolution: "split2@npm:4.2.0"
+ checksum: 10/09bbefc11bcf03f044584c9764cd31a252d8e52cea29130950b26161287c11f519807c5e54bd9e5804c713b79c02cefe6a98f4688630993386be353e03f534ab
+ languageName: node
+ linkType: hard
+
"stackback@npm:0.0.2":
version: 0.0.2
resolution: "stackback@npm:0.0.2"
@@ -1283,6 +1686,15 @@ __metadata:
languageName: node
linkType: hard
+"thread-stream@npm:^4.0.0":
+ version: 4.2.0
+ resolution: "thread-stream@npm:4.2.0"
+ dependencies:
+ real-require: "npm:^1.0.0"
+ checksum: 10/040d1f5284806a28d12ac83fc0a1f0b32547c6773b5fab9494c664798fa7fb14197bede695ec61221c5f93f64a32b0f1850408d0ad5730af03ee1d1efe6c1b05
+ languageName: node
+ linkType: hard
+
"tinybench@npm:^2.9.0":
version: 2.9.0
resolution: "tinybench@npm:2.9.0"
@@ -1352,7 +1764,7 @@ __metadata:
languageName: node
linkType: hard
-"tslib@npm:^2.4.0":
+"tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7