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
104 changes: 9 additions & 95 deletions packages/simulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ allowing you to simulate contract behavior locally without blockchain deployment
## Features

- 🧪 **Local Testing** - Test contracts without deployment.
- 🎭 **Caller Context Simulation** - Test multi-user interactions.
- 🔧 **Witness Overrides** - Mock and spy on witness functions.
- 📊 **State Inspection** - Access private and contract state.
- 🚀 **Type-Safe** - Full TypeScript support with generics.
Expand Down Expand Up @@ -147,50 +146,6 @@ public getBalance(): bigint {

## Advanced Features

### 🎭 Caller Context Management

Simulate different users interacting with the contract:

```typescript
// One-off caller context
simulator.as(alice).transfer(zBob, 100n);
simulator.as(bob).withdraw(50n);

// Persistent caller context
simulator.setPersistentCaller(alice);
simulator.deposit(100n); // Called by alice
simulator.transfer(zBob, 50n); // Called by alice

// Reset caller context
simulator.resetCaller();
```

### ⚠️ Important: Key Encoding for Contracts

**Same key, different formats:**

- `alice` = CoinPublicKey (hex string) - for simulator context.
- `zAlice` = ZswapCoinPublicKey (encoded) - for contract parameters.

```typescript
// For testing, create mock 64-character hex keys
const alice = '0'.repeat(63) + '1'; // CoinPublicKey format
const bob = '0'.repeat(63) + '2';

// Encode for contract use
const zAlice = { bytes: encodeCoinPublicKey(alice) }; // ZswapCoinPublicKey format
const zBob = { bytes: encodeCoinPublicKey(bob) };

// Incorrect - using wrong format for `bob`
simulator.as(alice).transfer(bob, 100n); // ❌ Contracts need encoded format

// Correct - encoded as ZswapCoinPublicKey
simulator.as(alice).transfer(zBob, 100n); // ✅
simulator.as(alice).transferFrom(zAlice, zBob, 100n); // ✅
```

**Remember**: `as()` takes hex strings, contract circuits take encoded keys.

### 🔧 Witness Overrides

Perfect for testing edge cases and tracking witness usage:
Expand Down Expand Up @@ -248,25 +203,18 @@ import { encodeCoinPublicKey } from '@midnight-ntwrk/compact-runtime';
import { describe, it, expect, beforeEach } from 'vitest';
import { MyContractSimulator } from './MyContractSimulator';

describe('MyContract', () => {
let simulator: MyContractSimulator;
let owner = '0'.repeat(63) + '1';
let zOwner = { bytes: encodeCoinPublicKey(owner) };
let simulator: MyContractSimulator;
let val = 123n;
let newVal = 456n;

describe('MyContract', () => {
beforeEach(() => {
simulator = new MyContractSimulator(
zOwner,
{ privateState: MyPrivateState.generate() }
);
simulator = new MyContractSimulator(val);
});

it('should transfer ownership', () => {
let newOwner = '0'.repeat(63) + '2';
let zNewOwner = { bytes: encodeCoinPublicKey(newOwner) };

simulator.as(owner).transferOwnership(zNewOwner);

expect(simulator.getPublicState()._owner).toEqual(zNewOwner);
it('should set new value', () => {
simulator.setVal(newVal);
expect(simulator.getPublicState()._val).toEqual(newVal);
});
Comment on lines +206 to 218
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep test snippet constructor usage consistent with earlier class example.

Line 212 currently uses new MyContractSimulator(val), but the earlier “Extending the Base Simulator” example defines a two-arg constructor. This can confuse readers and break copy/paste flows.

Suggested doc fix
-    simulator = new MyContractSimulator(val);
+    simulator = new MyContractSimulator(val, 'example-arg2');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let simulator: MyContractSimulator;
let val = 123n;
let newVal = 456n;
describe('MyContract', () => {
beforeEach(() => {
simulator = new MyContractSimulator(
zOwner,
{ privateState: MyPrivateState.generate() }
);
simulator = new MyContractSimulator(val);
});
it('should transfer ownership', () => {
let newOwner = '0'.repeat(63) + '2';
let zNewOwner = { bytes: encodeCoinPublicKey(newOwner) };
simulator.as(owner).transferOwnership(zNewOwner);
expect(simulator.getPublicState()._owner).toEqual(zNewOwner);
it('should set new value', () => {
simulator.setVal(newVal);
expect(simulator.getPublicState()._val).toEqual(newVal);
});
let simulator: MyContractSimulator;
let val = 123n;
let newVal = 456n;
describe('MyContract', () => {
beforeEach(() => {
simulator = new MyContractSimulator(val, 'example-arg2');
});
it('should set new value', () => {
simulator.setVal(newVal);
expect(simulator.getPublicState()._val).toEqual(newVal);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/simulator/README.md` around lines 206 - 218, The test snippet uses a
one-arg constructor new MyContractSimulator(val) but the earlier "Extending the
Base Simulator" example defines a two-argument constructor; update the test to
call MyContractSimulator with the same two-arg signature used earlier (e.g.,
pass the initial value and the second required parameter such as the
owner/signer or a default/mock) so the example is consistent and copy/pasteable,
and ensure the README text around the snippet explains the second parameter if
it isn't obvious.

});
```
Expand All @@ -289,36 +237,6 @@ it('should handle custom witness behavior', () => {
});
```

### Multi-User Interactions

```typescript
it('should handle multi-user token transfers', () => {
// PKs
const alice = '0'.repeat(63) + '1';
const zAlice = { bytes: encodeCoinPublicKey(alice) };

const bob = '0'.repeat(63) + '2';
const zBob = { bytes: encodeCoinPublicKey(bob) };

const charlie = '0'.repeat(63) + '3';
const zCharlie = { bytes: encodeCoinPublicKey(charlie) };

// Alice deposits
simulator.as(alice).deposit(1000n);

// Alice transfers to Bob
simulator.as(alice).transfer(zBob, 300n);

// Bob transfers to Charlie
simulator.as(bob).transfer(zCharlie, 100n);

const state = simulator.getPublicState();
expect(state._balances.lookup(zAlice)).toBe(700n);
expect(state._balances.lookup(zBob)).toBe(200n);
expect(state._balances.lookup(zCharlie)).toBe(100n);
});
```

## Special Cases

### Contracts with No Constructor Arguments
Expand Down Expand Up @@ -363,10 +281,7 @@ interface BaseSimulatorOptions<P, W> {
### Core Methods

| Method | Description |
|--------|-------------|
| `as(caller)` | Execute next operation as specified caller |
| `setPersistentCaller(caller)` | Set persistent caller for all operations |
| `resetCaller()` | Clears the caller context |
| ------ | ----------- |
| `overrideWitness(key, fn)` | Override a specific witness function |
| `getPrivateState()` | Get current private state |
| `getPublicState()` | Get current public ledger state |
Expand All @@ -378,4 +293,3 @@ interface BaseSimulatorOptions<P, W> {
2. **Witness Testing**: Use witness overrides to test edge cases without modifying contract code.
3. **Deterministic Tests**: Override witnesses with fixed values for reproducible tests.
4. **State Validation**: Inspect state after operations to ensure correctness.
5. **Multi-User Testing**: Use caller context to simulate realistic multi-user scenarios.
20 changes: 19 additions & 1 deletion packages/simulator/src/core/AbstractSimulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ export abstract class AbstractSimulator<P, L>
/**
* Sets the caller context for the next circuit call only (auto-resets).
*
* @param caller - The public key to use as the caller for the next circuit execution
* @notice ownPublicKey() is a witness value and MUST NOT be used
* as an authentication mechanism. Any contract using ownPublicKey()
* directly for access control is insecure. This method exists to
* support circuits that use ownPublicKey() as an input to other
* computations (e.g., commitment derivation).
*
* @param caller - The coin public key to use as the caller for the next circuit execution
* @returns This simulator instance for method chaining
*/
public as(caller: CoinPublicKey): this {
Expand All @@ -52,6 +58,12 @@ export abstract class AbstractSimulator<P, L>
/**
* Sets a persistent caller that will be used for all subsequent circuit calls.
*
* @notice ownPublicKey() is a witness value and MUST NOT be used
* as an authentication mechanism. Any contract using ownPublicKey()
* directly for access control is insecure. This method exists to
* support circuits that use ownPublicKey() as an input to other
* computations (e.g., commitment derivation).
*
* @param caller - The public key to use as the caller for all future calls, or null to clear
*/
public setPersistentCaller(caller: CoinPublicKey | null): void {
Expand All @@ -61,6 +73,12 @@ export abstract class AbstractSimulator<P, L>
/**
* Clears persistent caller overrides.
*
* @notice ownPublicKey() is a witness value and MUST NOT be used
* as an authentication mechanism. Any contract using ownPublicKey()
* directly for access control is insecure. This method exists to
* support circuits that use ownPublicKey() as an input to other
* computations (e.g., commitment derivation).
*
* @returns This simulator instance for method chaining
*/
public resetCaller(): this {
Expand Down
6 changes: 6 additions & 0 deletions packages/simulator/src/core/ContractSimulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export abstract class ContractSimulator<P, L> extends AbstractSimulator<P, L> {
/**
* Constructs a circuit context with appropriate caller information.
*
* @notice ownPublicKey() is a witness value and MUST NOT be used
* as an authentication mechanism. Any contract using ownPublicKey()
* directly for access control is insecure. This method exists to
* support circuits that use ownPublicKey() as an input to other
* computations (e.g., commitment derivation).
*
* Checks for caller overrides in priority order:
* 1. Single-use override (set via `as(caller)`)
* 2. Persistent override (set via `setPersistentCaller(caller)`)
Expand Down
Loading