diff --git a/packages/simulator/CHANGELOG.md b/packages/simulator/CHANGELOG.md index 92d41f3..938998a 100644 --- a/packages/simulator/CHANGELOG.md +++ b/packages/simulator/CHANGELOG.md @@ -12,6 +12,7 @@ Releases before `0.4.0` predate this file; see the `compact-simulator/v*` tags. ### Added - `BaseSimulatorOptions.time` sets the block time the kernel's time operations observe, in seconds since the epoch. Defaults to `0` for reproducible runs (#147) +- `CreateLiveContextOptions.scopedTransactionOptions` runs every live call in a midnight-js scoped transaction with those options. With `additionalCoinEncPublicKeyMappings`, a circuit can send a shielded coin to a wallet other than the caller (#195) ### Changed diff --git a/packages/simulator/README.md b/packages/simulator/README.md index 77faad1..5ac565d 100644 --- a/packages/simulator/README.md +++ b/packages/simulator/README.md @@ -198,6 +198,29 @@ const owner = await simulator.signers.eitherFor('OWNER'); > mechanism. These helpers exist for circuits that take the caller as an input > to other computations (e.g. commitment derivation). +In live mode, midnight-js encrypts each shielded output sent to a coin public +key with the recipient's encryption public key, and on its own knows only the +calling wallet's. When a circuit sends a coin to another wallet, pass that +wallet's coin and encryption public keys (its `WalletProvider`'s +`getCoinPublicKey()` and `getEncryptionPublicKey()`) through +`scopedTransactionOptions`. Otherwise the call fails to build with +`Unable to resolve encryption public key`: + +```typescript +createLiveContext({ + // ... + scopedTransactionOptions: { + additionalCoinEncPublicKeyMappings: new Map([ + [bob.getCoinPublicKey(), bob.getEncryptionPublicKey()], + ]), + }, +}); +``` + +With the option set, each call runs as its own midnight-js scoped transaction. +midnight-js then wraps circuit and submission errors (`Unexpected error +executing scoped transaction ...`), keeping the original message inside. + ### 🔧 Witness Overrides Perfect for testing edge cases and tracking witness usage. Dry only: the live diff --git a/packages/simulator/src/live/createLiveContext.ts b/packages/simulator/src/live/createLiveContext.ts index 8f3da06..6cf943f 100644 --- a/packages/simulator/src/live/createLiveContext.ts +++ b/packages/simulator/src/live/createLiveContext.ts @@ -1,7 +1,8 @@ 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`. +// lazy dynamic import inside `loadContracts`. +import type { ScopedTransactionOptions } from '@midnight-ntwrk/midnight-js-contracts'; import type { PrivateStateProvider, PublicDataProvider, @@ -65,29 +66,26 @@ export interface CreateLiveContextOptions
{
* is the harness's responsibility (already required for `get`).
*/
privateStateProvider: PrivateStateProvider (
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,
- }),
- );
+ 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,
+ });
+ 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.
+ const callTx = Object.fromEntries(
+ Object.entries(handle.callTx).map(([circuitId, call]) => [
+ circuitId,
+ (...args: unknown[]) =>
+ contracts.withContractScopedTransaction(
+ providers,
+ async (txCtx) => {
+ await call(txCtx, ...args);
+ },
+ scopedTransactionOptions,
+ ),
+ ]),
+ );
+ return { ...handle, callTx };
+ });
handleCache.set(key, built);
return built;
};
diff --git a/packages/simulator/test/unit/createLiveContext.test.ts b/packages/simulator/test/unit/createLiveContext.test.ts
index 6153bfd..8b1db2a 100644
--- a/packages/simulator/test/unit/createLiveContext.test.ts
+++ b/packages/simulator/test/unit/createLiveContext.test.ts
@@ -1,14 +1,21 @@
+import type { ScopedTransactionOptions } from '@midnight-ntwrk/midnight-js-contracts';
import type { PrivateStateProvider } from '@midnight-ntwrk/midnight-js-types';
-import { describe, expect, it } from 'vitest';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createLiveContext } from '../../src/live/createLiveContext.js';
+const contracts = vi.hoisted(() => ({
+ findDeployedContract: vi.fn(),
+ withContractScopedTransaction: vi.fn(),
+}));
+
+vi.mock('@midnight-ntwrk/midnight-js-contracts', () => contracts);
+
type PS = { secretKey: Uint8Array };
/**
* An in-memory stand-in for the harness's `PrivateStateProvider`, exercising
- * only the `get`/`set` slice `createLiveContext` uses. `findDeployedContract`
- * (and thus midnight-js) is never reached, so these tests run without the
- * optional live peers installed.
+ * only the `get`/`set` slice `createLiveContext` uses. midnight-js-contracts is
+ * mocked; the private-state tests never reach it.
*/
const fakeProvider = (initial: Record