Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions packages/simulator/src/core/AbstractSimulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export abstract class AbstractSimulator<P, L>
* @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;
}

Expand Down
38 changes: 38 additions & 0 deletions packages/simulator/src/create-contract.type-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -60,5 +62,41 @@ async function _callSiteSubtype(): Promise<void> {
void instance.marker();
}

// --- Circuit proxies resolve to promises ------------------------------------
// The wrapping proxies are async regardless of artifact era, so the mapped
// type must yield `Promise<R>` 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<GuardPrivateState>,
x: number,
) => Promise<{ result: boolean; context: CircuitContext<GuardPrivateState> }>;
legacy: (
ctx: CircuitContext<GuardPrivateState>,
x: number,
) => { result: boolean; context: CircuitContext<GuardPrivateState> };
};

declare const contextless: ContextlessCircuits<
GuardCircuits,
GuardPrivateState
>;

function _circuitsResolveToPromises(): void {
const modern: Promise<boolean> = contextless.modern(1);
const legacy: Promise<boolean> = 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<GuardCircuits, GuardPrivateState> = contextless;

void Guard;
void _callSiteSubtype;
void _circuitsResolveToPromises;
void _alias;
4 changes: 3 additions & 1 deletion packages/simulator/src/factory/createDrySimulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
};
Expand Down
44 changes: 36 additions & 8 deletions packages/simulator/src/factory/createSimulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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');
Expand All @@ -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<P, L>(
localSim as unknown as SyncSimulator<P, L>,
Expand Down
33 changes: 13 additions & 20 deletions packages/simulator/src/types/Circuit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,35 +30,28 @@ type CircuitResult<Ret> = Awaited<Ret> extends { result: infer R } ? R : never;
/**
* Transforms circuit functions by removing the explicit `CircuitContext` parameter.
*
* Each original circuit function has signature:
* `(ctx: CircuitContext<TState>, ...args) => Promise<{ result: R; context: CircuitContext<TState> }>`
*
* 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<Circuits, TState> = {
[K in keyof Circuits]: Circuits[K] extends (
ctx: CircuitContext<TState>,
...args: infer P
) => infer Ret
? (...args: P) => CircuitResult<Ret>
? (...args: P) => Promise<CircuitResult<Ret>>
: 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<R>` 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<Circuits, TState> = {
[K in keyof Circuits]: Circuits[K] extends (
ctx: CircuitContext<TState>,
...args: infer P
) => infer Ret
? (...args: P) => Promise<CircuitResult<Ret>>
: never;
};
export type AsyncCircuits<Circuits, TState> = ContextlessCircuits<
Circuits,
TState
>;
60 changes: 60 additions & 0 deletions packages/simulator/test/integration/LiveAddress.test.ts
Original file line number Diff line number Diff line change
@@ -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<WitnessPrivateState> => ({
contractAddress: DEPLOYED,
async handleFor(): Promise<DeployedTxHandle> {
return { callTx: {} };
},
async queryLedger(): Promise<StateValue> {
return {} as unknown as StateValue;
},
async queryPrivateState(): Promise<WitnessPrivateState> {
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/);
});
});
9 changes: 7 additions & 2 deletions packages/simulator/test/integration/LiveMutation.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -20,7 +23,9 @@ const makeWorld = (
): LiveContext<WitnessPrivateState> => {
let stored = initial;
const world: LiveContext<WitnessPrivateState> = {
contractAddress: '0200deadbeef',
// Runtime-parseable: the deployed address seeds the local evaluator's
// circuit context.
contractAddress: dummyContractAddress(),
async handleFor(): Promise<DeployedTxHandle> {
return { callTx: {} };
},
Expand Down
Loading