Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/simulator/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Releases before `0.4.0` predate this file; see the `compact-simulator/v*` tags.

### Changed

- **Breaking:** `createLiveContext` takes midnight-js types. `findOptions` replaces `contractAddress`, `compiledContract` and `privateStateId`. `providersFor` returns `ContractProviders`, and `providersFor(null)` replaces `publicDataProvider` and `privateStateProvider`. `@midnight-ntwrk/compact-js` joins the optional peers (#196)
- **Breaking:** `@midnight-ntwrk/compact-runtime` moves to `0.19.0`, which made `initialState` and every circuit async. The `@midnight-ntwrk/midnight-js-contracts` and `@midnight-ntwrk/midnight-js-types` peers move from `^4.1.0` to `^5.0.0-beta.7`. Both peers stay optional, so a dry-only consumer needs neither (#152)
- **Breaking:** construction is async. Replace `new MySimulator(args, options)` with `await MySimulator.create(args, options)`. Subclass `create` overrides return `Promise<MySimulator>` and delegate to `super._create([...args], options)` (#145, #147)
- **Breaking:** circuit proxies return promises. `ContextlessCircuits` maps to `Promise<R>`, so an un-awaited call that used to pass as a truthy promise, such as `if (sim.isOwner(x))`, no longer typechecks (#145, #154)
Expand Down
3 changes: 2 additions & 1 deletion packages/simulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ wallet's coin and encryption public keys (its `WalletProvider`'s

```typescript
createLiveContext({
// ...
providersFor, // alias => ContractProviders
findOptions: { compiledContract, contractAddress, privateStateId },
scopedTransactionOptions: {
additionalCoinEncPublicKeyMappings: new Map([
[bob.getCoinPublicKey(), bob.getEncryptionPublicKey()],
Expand Down
5 changes: 5 additions & 0 deletions packages/simulator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"clean": "git clean -fXd"
},
"devDependencies": {
"@midnight-ntwrk/compact-js": "2.5.5-rc.8",
"@midnight-ntwrk/midnight-js-contracts": "5.0.0-beta.7",
"@midnight-ntwrk/midnight-js-types": "5.0.0-beta.7",
"@noble/curves": "^2.2.0",
Expand All @@ -57,10 +58,14 @@
"@midnight-ntwrk/compact-runtime": "0.19.0"
},
"peerDependencies": {
"@midnight-ntwrk/compact-js": "^2.5.5-rc.8",
"@midnight-ntwrk/midnight-js-contracts": "^5.0.0-beta.7",
"@midnight-ntwrk/midnight-js-types": "^5.0.0-beta.7"
},
"peerDependenciesMeta": {
"@midnight-ntwrk/compact-js": {
"optional": true
},
"@midnight-ntwrk/midnight-js-contracts": {
"optional": true
},
Expand Down
106 changes: 44 additions & 62 deletions packages/simulator/src/live/createLiveContext.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Contract } from '@midnight-ntwrk/compact-js';
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 `loadContracts`.
import type { ScopedTransactionOptions } from '@midnight-ntwrk/midnight-js-contracts';
import type {
PrivateStateProvider,
PublicDataProvider,
} from '@midnight-ntwrk/midnight-js-types';
ContractProviders,
FindDeployedContractOptionsExistingPrivateState,
ScopedTransactionOptions,
} from '@midnight-ntwrk/midnight-js-contracts';
import type { DeployedTxHandle, LiveContext } from './LiveContext.js';

/**
Expand Down Expand Up @@ -34,38 +35,21 @@ export const DEFAULT_INDEXER_LAG: IndexerLagPolicy = {
* 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.
* construction, and wallet funding are the caller's harness.
*
* @template P - Private state type.
* @template C - Contract type.
*/
export interface CreateLiveContextOptions<P> {
/** The address of the already-deployed contract. */
contractAddress: string;
export interface CreateLiveContextOptions<
C extends Contract.Any = Contract.Any,
> {
/**
* Per-alias `ContractProviders`, supplied by the harness. The wallet/signing
* differs per alias; `null` is the default signer. Threaded into
* `findDeployedContract`.
* Per-alias providers; `null` is the default signer. Public and private
* state are read through `providersFor(null)`, so aliases should share its
* private-state provider, and the harness calls its `setContractAddress`.
*/
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 and (optionally) writing private state.
*
* Invariant: this MUST be the same provider instance wired into the
* `providersFor(alias)` bundle handed to `findDeployedContract`. The
* `setPrivateState` write below targets this provider, and the next
* `callTx` reads private state from the bundle's provider; if they are
* different instances the write is invisible to proving. `setContractAddress`
* is the harness's responsibility (already required for `get`).
*/
privateStateProvider: PrivateStateProvider<string, P>;
providersFor: (alias: string | null) => ContractProviders<C>;
/** Passed to `findDeployedContract` for every alias. */
findOptions: FindDeployedContractOptionsExistingPrivateState<C>;
/** Runs every live call in a midnight-js scoped transaction with these options. */
scopedTransactionOptions?: ScopedTransactionOptions;
/** Optional override of the indexer-lag policy. */
Expand Down Expand Up @@ -110,32 +94,33 @@ const sleep = (ms: number): Promise<void> =>
* 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.
* @param options - Harness-provided providers and `findDeployedContract` options.
* @returns A {@link LiveContext} ready to pass to `create(args, { live })`.
*/
export function createLiveContext<P>(
options: CreateLiveContextOptions<P>,
): LiveContext<P> {
export function createLiveContext<C extends Contract.Any>(
options: CreateLiveContextOptions<C>,
): LiveContext<Contract.PrivateState<C>> {
const lag: IndexerLagPolicy = {
...DEFAULT_INDEXER_LAG,
...options.indexerLag,
};
const { findOptions, scopedTransactionOptions } = options;
const { contractAddress, privateStateId } = findOptions;
const handleCache = new Map<string, Promise<DeployedTxHandle>>();

const resolveHandle = (alias: string | null): Promise<DeployedTxHandle> => {
const key = alias ?? '\u0000default';
const cached = handleCache.get(key);
if (cached) return cached;
const built = loadContracts().then(async (contracts) => {
const providers = options.providersFor(alias) as never;
const compiledContract = options.compiledContract as never;
const { contractAddress, privateStateId, scopedTransactionOptions } =
options;
const handle = await contracts.findDeployedContract(providers, {
compiledContract,
contractAddress,
privateStateId,
});
const providers = options.providersFor(alias);
const found = await contracts.findDeployedContract(
providers,
findOptions,
);
// Erase `FoundContract<C>` to the structural handle `LiveBackend` reads;
// its typed `callTx` overloads are not assignable to `DeployedTxHandle`.
const handle = found as unknown as DeployedTxHandle;
if (scopedTransactionOptions === undefined) return handle;
// A scoped transaction is the only midnight-js entry point that takes
// these options, so each call becomes a scope holding just that call.
Expand All @@ -159,7 +144,7 @@ export function createLiveContext<P>(
};

return {
contractAddress: options.contractAddress,
contractAddress,

handleFor: resolveHandle,

Expand All @@ -170,13 +155,13 @@ export function createLiveContext<P>(
* `ledgerExtractor` consumes.
*/
async queryLedger(): Promise<StateValue> {
const { publicDataProvider } = options.providersFor(null);
let delay = lag.baseDelayMs;
let lastErr: unknown;
for (let attempt = 0; attempt <= lag.retries; attempt++) {
try {
const state = await options.publicDataProvider.queryContractState(
options.contractAddress,
);
const state =
await publicDataProvider.queryContractState(contractAddress);
if (state != null) {
// `ContractState.data` is the StateValue the dry path also extracts.
return (state as unknown as { data: StateValue }).data;
Expand All @@ -190,31 +175,28 @@ export function createLiveContext<P>(
}
}
throw new Error(
`no contract state at ${options.contractAddress} after ${lag.retries + 1} ` +
`no contract state at ${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<P> {
const state = await options.privateStateProvider.get(
options.privateStateId,
);
async queryPrivateState(): Promise<Contract.PrivateState<C>> {
const { privateStateProvider } = options.providersFor(null);
const state = await privateStateProvider.get(privateStateId);
if (state == null) {
throw new Error(
`no private state stored at "${options.privateStateId}"`,
);
throw new Error(`no private state stored at "${privateStateId}"`);
}
return state;
},

/**
* Writes the whole private state to the provider under `privateStateId`.
* The next impure `callTx` reads it fresh (no handle-cache invalidation
* needed). See the invariant on {@link CreateLiveContextOptions.privateStateProvider}.
* Writes the whole private state under `privateStateId`. The next impure
* `callTx` reads it fresh (no handle-cache invalidation needed).
*/
async setPrivateState(state: P): Promise<void> {
await options.privateStateProvider.set(options.privateStateId, state);
async setPrivateState(state: Contract.PrivateState<C>): Promise<void> {
const { privateStateProvider } = options.providersFor(null);
await privateStateProvider.set(privateStateId, state);
},
};
}
Loading
Loading