diff --git a/RELEASING.md b/RELEASING.md
index e97f7fa..535200f 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -21,7 +21,7 @@ runs only from `beta`, `patch`/`minor`/`major` only from `main`.
4. Choose the package to release and the version bump type.
Following [SemVer](https://semver.org/):
- **Patch** - Backward-compatible bug fixes.
- - **Minor** - New functionality in a backward compatible way.
+ - **Minor** - New functionality in a backward-compatible way.
- **Major** - Breaking API changes.
- **Prepatch / preminor / premajor** - Open a new beta cycle at the
corresponding bump (`0.3.1` + preminor -> `0.4.0-beta.0`).
@@ -36,7 +36,7 @@ runs only from `beta`, `patch`/`minor`/`major` only from `main`.
- Create a git tag.
- Publish the package to npm under the channel's dist-tag.
7. Once published, go to "Releases" and create a GitHub release using the
- generated tag. Mark beta tags as pre-releases.
+ generated tag. Mark beta tags as prereleases.
## Graduating a beta to stable
diff --git a/packages/simulator/src/core/AbstractSimulator.ts b/packages/simulator/src/core/AbstractSimulator.ts
index c179fe8..7721224 100644
--- a/packages/simulator/src/core/AbstractSimulator.ts
+++ b/packages/simulator/src/core/AbstractSimulator.ts
@@ -93,6 +93,8 @@ export abstract class AbstractSimulator
* @returns The current private state of type P
*/
public getPrivateState(): P {
+ // The runtime types this `P | undefined`; the simulator always seeds a
+ // private state, so it is set after init.
return this.circuitContext.callContext.currentPrivateState as P;
}
diff --git a/packages/simulator/src/create-contract.type-test.ts b/packages/simulator/src/create-contract.type-test.ts
index 0b40d5d..00d49ab 100644
--- a/packages/simulator/src/create-contract.type-test.ts
+++ b/packages/simulator/src/create-contract.type-test.ts
@@ -8,8 +8,10 @@
* Exports nothing and is imported by nothing. The build config excludes
* `*.type-test.ts` from emit so it never reaches `dist`; `yarn types` compiles it.
*/
+import type { CircuitContext } from '@midnight-ntwrk/compact-runtime';
import { createSimulator } from './factory/createSimulator.js';
import type { SimulatorConfig } from './factory/SimulatorConfig.js';
+import type { AsyncCircuits, ContextlessCircuits } from './types/Circuit.js';
import type { IMinimalContract } from './types/Contract.js';
type GuardPrivateState = { readonly value: number };
@@ -60,5 +62,41 @@ async function _callSiteSubtype(): Promise {
void instance.marker();
}
+// --- Circuit proxies resolve to promises ------------------------------------
+// The wrapping proxies are async regardless of artifact era, so the mapped
+// type must yield `Promise` for the 0.18 promise shape and the 0.16 sync
+// shape alike. A bare-`R` mapping reintroduces the un-awaited-result footgun.
+
+type GuardCircuits = {
+ modern: (
+ ctx: CircuitContext,
+ x: number,
+ ) => Promise<{ result: boolean; context: CircuitContext }>;
+ legacy: (
+ ctx: CircuitContext,
+ x: number,
+ ) => { result: boolean; context: CircuitContext };
+};
+
+declare const contextless: ContextlessCircuits<
+ GuardCircuits,
+ GuardPrivateState
+>;
+
+function _circuitsResolveToPromises(): void {
+ const modern: Promise = contextless.modern(1);
+ const legacy: Promise = contextless.legacy(1);
+ // @ts-expect-error the mapped circuit yields a promise, not a bare boolean.
+ const sync: boolean = contextless.modern(1);
+ void modern;
+ void legacy;
+ void sync;
+}
+
+// `AsyncCircuits` must stay interchangeable with `ContextlessCircuits`.
+const _alias: AsyncCircuits = contextless;
+
void Guard;
void _callSiteSubtype;
+void _circuitsResolveToPromises;
+void _alias;
diff --git a/packages/simulator/src/factory/createDrySimulator.ts b/packages/simulator/src/factory/createDrySimulator.ts
index 844a71f..f1db530 100644
--- a/packages/simulator/src/factory/createDrySimulator.ts
+++ b/packages/simulator/src/factory/createDrySimulator.ts
@@ -44,7 +44,7 @@ export function createDrySimulator<
* before {@link init} has been awaited.
*/
get contractAddress(): string {
- if (!this._contractAddress) {
+ if (this._contractAddress === undefined) {
throw new Error('Simulator: await init() before use');
}
return this._contractAddress;
@@ -218,6 +218,8 @@ export function createDrySimulator<
const circuitCtx = this.circuitContext;
return {
ledger: this.getPublicState(),
+ // Runtime-typed `P | undefined`; always set after init (see
+ // `AbstractSimulator.getPrivateState`).
privateState: circuitCtx.callContext.currentPrivateState as P,
contractAddress: circuitCtx.callContext.currentQueryContext.address,
};
diff --git a/packages/simulator/src/factory/createSimulator.ts b/packages/simulator/src/factory/createSimulator.ts
index 5403b23..a31f1a6 100644
--- a/packages/simulator/src/factory/createSimulator.ts
+++ b/packages/simulator/src/factory/createSimulator.ts
@@ -89,16 +89,25 @@ export function createSimulator<
// The local in-memory 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);
// 0.18: `initialState` is async, so the constructor defers the constructor
// run to `init()`. Await it before deriving names / wiring any backend.
- await localSim.init();
- 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),
- );
+ const buildLocalSim = async (contractAddress?: string) => {
+ const sim = new DrySimClass(
+ contractArgs,
+ contractAddress ? { ...options, contractAddress } : options,
+ );
+ await sim.init();
+ return sim;
+ };
+
+ const circuitNames = (contract: IMinimalContract) => {
+ const impureNames = Object.keys(contract.impureCircuits);
+ const impureSet = new Set(impureNames);
+ const pureNames = Object.keys(contract.circuits).filter(
+ (name) => !impureSet.has(name),
+ );
+ return { pureNames, impureNames };
+ };
if (kind === 'live') {
const signers = new Signers({
@@ -127,6 +136,23 @@ export function createSimulator<
);
}
+ // The deployed address is the contract's identity; an explicit override
+ // that disagrees with it can only produce wrong results.
+ if (
+ options.contractAddress &&
+ options.contractAddress !== liveCtx.contractAddress
+ ) {
+ throw new Error(
+ `contractAddress ${options.contractAddress} does not match the ` +
+ `deployed contract at ${liveCtx.contractAddress}`,
+ );
+ }
+
+ // Bind the local evaluator to the deployed address so pure circuits
+ // never observe a dummy address.
+ const localSim = await buildLocalSim(liveCtx.contractAddress);
+ const { pureNames, impureNames } = circuitNames(localSim.contract);
+
// 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');
@@ -139,6 +165,8 @@ export function createSimulator<
return { backend, signers, pureNames, impureNames };
}
+ const localSim = await buildLocalSim();
+ const { pureNames, impureNames } = circuitNames(localSim.contract);
const signers = new Signers({ mode: 'dry', dryKeys: options.signerKeys });
const backend = new DryBackend(
localSim as unknown as SyncSimulator
,
diff --git a/packages/simulator/src/types/Circuit.ts b/packages/simulator/src/types/Circuit.ts
index b3f961d..17444b7 100644
--- a/packages/simulator/src/types/Circuit.ts
+++ b/packages/simulator/src/types/Circuit.ts
@@ -30,35 +30,28 @@ type CircuitResult = Awaited extends { result: infer R } ? R : never;
/**
* Transforms circuit functions by removing the explicit `CircuitContext` parameter.
*
- * Each original circuit function has signature:
- * `(ctx: CircuitContext, ...args) => Promise<{ result: R; context: CircuitContext }>`
- *
- * The transformed function takes the same parameters except the context,
- * and returns the `result` directly.
+ * Each transformed function takes the original parameters minus the context and
+ * resolves to the `result`. Always a `Promise`: the wrapping proxies are async
+ * whether or not the underlying artifact is.
*/
export type ContextlessCircuits = {
[K in keyof Circuits]: Circuits[K] extends (
ctx: CircuitContext,
...args: infer P
) => infer Ret
- ? (...args: P) => CircuitResult
+ ? (...args: P) => Promise>
: never;
};
/**
- * Async sibling of {@link ContextlessCircuits}, used by `createBackendSimulator`.
+ * Alias 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 resolves in memory, 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.
+ * The name states the dry↔live parity contract: the dry backend resolves in
+ * memory, 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
- ) => infer Ret
- ? (...args: P) => Promise>
- : never;
-};
+export type AsyncCircuits = ContextlessCircuits<
+ Circuits,
+ TState
+>;
diff --git a/packages/simulator/test/integration/LiveAddress.test.ts b/packages/simulator/test/integration/LiveAddress.test.ts
new file mode 100644
index 0000000..d84e737
--- /dev/null
+++ b/packages/simulator/test/integration/LiveAddress.test.ts
@@ -0,0 +1,60 @@
+import {
+ dummyContractAddress,
+ type StateValue,
+} from '@midnight-ntwrk/compact-runtime';
+import { describe, expect, it } from 'vitest';
+import type {
+ DeployedTxHandle,
+ LiveContext,
+} from '../../src/live/LiveContext.js';
+import { WitnessPrivateState } from '../fixtures/sample-contracts/witnesses/WitnessWitnesses.js';
+import { WitnessSimulator } from './WitnessSimulator.js';
+
+// Runtime-parseable: the deployed address now seeds the local evaluator's
+// circuit context, so a made-up string no longer works here.
+const DEPLOYED = dummyContractAddress();
+
+/** An inert live world; only the address-binding paths are exercised. */
+const makeWorld = (): LiveContext => ({
+ contractAddress: DEPLOYED,
+ async handleFor(): Promise {
+ return { callTx: {} };
+ },
+ async queryLedger(): Promise {
+ return {} as unknown as StateValue;
+ },
+ async queryPrivateState(): Promise {
+ return WitnessPrivateState.generate();
+ },
+});
+
+describe('live contract-address binding', () => {
+ it('exposes the deployed address', async () => {
+ const sim = await WitnessSimulator.create({
+ backend: 'live',
+ live: makeWorld(),
+ });
+
+ expect(sim.contractAddress).toBe(DEPLOYED);
+ });
+
+ it('accepts an explicit contractAddress equal to the deployed one', async () => {
+ const sim = await WitnessSimulator.create({
+ backend: 'live',
+ live: makeWorld(),
+ contractAddress: DEPLOYED,
+ });
+
+ expect(sim.contractAddress).toBe(DEPLOYED);
+ });
+
+ it('rejects an explicit contractAddress that differs from the deployed one', async () => {
+ await expect(
+ WitnessSimulator.create({
+ backend: 'live',
+ live: makeWorld(),
+ contractAddress: '0200aaaa',
+ }),
+ ).rejects.toThrow(/does not match the deployed contract/);
+ });
+});
diff --git a/packages/simulator/test/integration/LiveMutation.test.ts b/packages/simulator/test/integration/LiveMutation.test.ts
index d6b200e..5ccadd7 100644
--- a/packages/simulator/test/integration/LiveMutation.test.ts
+++ b/packages/simulator/test/integration/LiveMutation.test.ts
@@ -1,4 +1,7 @@
-import type { StateValue } from '@midnight-ntwrk/compact-runtime';
+import {
+ dummyContractAddress,
+ type StateValue,
+} from '@midnight-ntwrk/compact-runtime';
import { describe, expect, it } from 'vitest';
import { PRIVATE_STATE_MUTATION_UNSUPPORTED } from '../../src/index.js';
import type {
@@ -20,7 +23,9 @@ const makeWorld = (
): LiveContext => {
let stored = initial;
const world: LiveContext = {
- contractAddress: '0200deadbeef',
+ // Runtime-parseable: the deployed address seeds the local evaluator's
+ // circuit context.
+ contractAddress: dummyContractAddress(),
async handleFor(): Promise {
return { callTx: {} };
},