For the complete documentation index, see llms.txt
Using Compact contracts from JavaScript
Compiling a Compact contract produces zero-knowledge circuits and, alongside them, a JavaScript module that runs the same contract logic. Everything you do with a contract from JavaScript goes through that module: you wire in the witnesses that supply its private data, call its circuits with native values, and decode its ledger state through typed getters. Because a circuit call runs the logic the circuit enforces on-chain, the module also makes contract behavior testable, including the paths your assert statements are supposed to reject, without a node, an indexer, or a proof server.
Running the module off-chain covers contract logic, not the rest of the submission path. It does not generate or verify a proof, assemble or price a transaction, or account for ledger state that changed since your local run, so a call that passes here can still fail when the transaction reaches the network.
This guide walks that path: implement the witnesses a contract needs, call its circuits from JavaScript, and build a unit test suite around it. The reference sections at the end list what the module exports and what its error messages mean, for when a stack trace or a type error sends you into it. The examples use the bulletin board contract throughout.
Prerequisites
These apply to every procedure in this guide:
- The Compact CLI installed, with
compact compileworking. - A compiled contract. This guide compiles
bboard.compactfrom example-bboard; any contract works, with your own names in place of the bulletin board's. - Node.js with Vitest and
@midnight-ntwrk/compact-runtimeinstalled. - A runtime version that matches your compiler. The generated code enforces this pairing at import time; check the support matrix when either changes.
What the compiler generates
Compiling a contract produces two artifacts that mirror each other: the ZK circuits the network verifies, and a JavaScript module that executes the identical contract logic off-chain. Understanding that the two are generated together, from the same source, is what makes the module trustworthy as a testing surface.
When you run compact compile, the compiler:
- Parses your
.compactfile and emits a ZK circuit for each exported circuit that needs a proof, that is, the impure circuits. An exported pure circuit such as the bulletin board'spublicKeycompiles to JavaScript only. - Generates a JavaScript implementation that mirrors the contract's structure: it identifies each circuit's signature, embeds type descriptors for every Compact type the contract uses, and wraps each circuit so you can invoke it with native JavaScript values.
- Links the generated code against
@midnight-ntwrk/compact-runtime, the shared library that implements field arithmetic, serialization, error types, and the ledger query machinery. The generated file and the runtime together form a complete execution environment. - Emits a TypeScript declaration file so the module is fully typed in a TypeScript project.
The JavaScript output lands in the contract/ subdirectory of your compilation target (for example src/managed/bboard/contract/), alongside the keys/, zkir/, and compiler/ directories the compiler also emits:
index.js: the JavaScript implementationindex.d.ts: TypeScript type definitionsindex.js.map: source map for debugging
index.js is regenerated on every compilation. If you add or remove circuits or change types, recompile; never edit the generated files by hand.
Implementing witnesses for a contract
Load the generated module and give the contract its witnesses: the functions that supply private data, such as a secret key, when a circuit asks for it. The contract cannot be instantiated without them, and the generated constructor rejects an incomplete witnesses object with a precise error, which is the behavior the verification below relies on.
Procedure
-
Compile the contract, giving
compact compilethe source and the target directory. The paths below assume the contract lives atsrc/bboard.compact; in example-bboard it sits atcontract/src/bboard.compact, so adjust to match your layout:compact compile src/bboard.compact src/managed/bboard -
Import the module like any other ES module, from a file alongside the
manageddirectory the compiler just wrote. In TypeScript, the declaration file types everything automatically:import { Contract, State, ledger, pureCircuits } from './managed/bboard/contract/index.js'; -
Define the private state your witnesses read, and implement one function per witness the Compact source declares. For the bulletin board, that is
localSecretKey:import { Ledger } from './managed/bboard/contract/index.js';import { WitnessContext } from '@midnight-ntwrk/compact-runtime';export type BBoardPrivateState = {readonly secretKey: Uint8Array;};export const createBBoardPrivateState = (secretKey: Uint8Array) => ({secretKey,});export const witnesses = {localSecretKey: ({privateState,}: WitnessContext<Ledger, BBoardPrivateState>): [BBoardPrivateState, Uint8Array] => [privateState,privateState.secretKey,],};Each witness receives a
WitnessContextcarrying the ledger view, the private state, and the contract address, and returns a tuple of the updated private state and the witness value. -
Instantiate the contract with the witnesses object:
const contract = new Contract(witnesses);
Verification
A complete witnesses object produces a working instance, and the generated validation rejects an incomplete one.
import { describe, it, expect } from 'vitest';
import * as RT from '@midnight-ntwrk/compact-runtime';
import { Contract } from './managed/bboard/contract/index.js';
const COIN = '0'.repeat(64);
const witnesses = {
localSecretKey: ({ privateState }) => [privateState, privateState.secretKey],
};
describe('importing the implementation', () => {
it('wires the witnesses into a working contract instance', () => {
const contract = new Contract(witnesses);
const secretKey = new Uint8Array(32);
const ctor = contract.initialState(RT.createConstructorContext({ secretKey }, COIN));
expect(ctor.currentContractState).toBeDefined();
});
it('rejects a witnesses object missing a declared witness', () => {
expect(() => new Contract({})).toThrow(
'does not contain a function-valued field named localSecretKey',
);
});
});
✓ import-witnesses.test.ts > importing the implementation > wires the witnesses into a working contract instance
✓ import-witnesses.test.ts > importing the implementation > rejects a witnesses object missing a declared witness
Test Files 1 passed (1)
Tests 2 passed (2)
Calling circuits from JavaScript
Run contract logic off-chain by building a circuit context and invoking circuits through the instance. Build the context with the runtime helpers rather than by hand. A real CircuitContext carries query-context state that the wrappers check for, so hand-built objects fail validation.
Prerequisites
- A contract instance with witnesses, from Implementing witnesses for a contract.
Procedure
-
Create the genesis state with
initialState, then build a circuit context from it. The constructor context takes the initial private state and a coin public key; the circuit context adds the contract address:import * as RT from '@midnight-ntwrk/compact-runtime';const COIN = '0'.repeat(64);const ADDR = RT.sampleContractAddress();const secretKey = new Uint8Array(32);const ctor = contract.initialState(RT.createConstructorContext({ secretKey }, COIN));const ctx = RT.createCircuitContext(ADDR, COIN, ctor.currentContractState, { secretKey }); -
Call an impure circuit with the context. The wrapper validates the inputs, runs the contract logic, and returns the result together with the updated context, the proof data, and the gas cost:
const call = contract.impureCircuits.post(ctx, 'Hello from Compact!');// call.result -> the circuit's return value ([] for post)// call.context -> the updated circuit context// call.proofData -> input, output, and transcripts for proof generation// call.gasCost -> cost tracking for the call -
Read the resulting ledger state with the
ledger()helper:const board = ledger(call.context.currentQueryContext.state);// board.state, board.message, board.sequence, board.owner -
Call pure circuits directly, with no context at all:
const commitment = pureCircuits.publicKey(secretKey, new Uint8Array(32));
Verification
The impure circuit transitions the board to occupied and returns proof data; the pure circuit computes deterministically without a context.
import { describe, it, expect } from 'vitest';
import * as RT from '@midnight-ntwrk/compact-runtime';
import { Contract, State, ledger, pureCircuits } from './managed/bboard/contract/index.js';
const COIN = '0'.repeat(64);
const ADDR = RT.sampleContractAddress();
const key = (n) => { const a = new Uint8Array(32); a[31] = n; return a; };
const witnesses = {
localSecretKey: ({ privateState }) => [privateState, privateState.secretKey],
};
describe('calling contract circuits', () => {
it('runs an impure circuit and returns the result, context, and proof data', () => {
const contract = new Contract(witnesses);
const ctor = contract.initialState(RT.createConstructorContext({ secretKey: key(7) }, COIN));
const ctx = RT.createCircuitContext(ADDR, COIN, ctor.currentContractState, { secretKey: key(7) });
const call = contract.impureCircuits.post(ctx, 'Hello from Compact!');
expect(call.result).toEqual([]);
expect(call.proofData.publicTranscript.length).toBeGreaterThan(0);
expect(call.gasCost).toBeDefined();
const board = ledger(call.context.currentQueryContext.state);
expect(board.state).toBe(State.OCCUPIED);
expect(board.message.value).toBe('Hello from Compact!');
});
it('calls a pure circuit directly, with no circuit context', () => {
const commitment = pureCircuits.publicKey(key(7), key(1));
expect(commitment).toBeInstanceOf(Uint8Array);
expect(commitment.length).toBe(32);
expect(commitment).toEqual(pureCircuits.publicKey(key(7), key(1)));
});
});
✓ circuits.test.ts > calling contract circuits > runs an impure circuit and returns the result, context, and proof data
✓ circuits.test.ts > calling contract circuits > calls a pure circuit directly, with no circuit context
Test Files 1 passed (1)
Tests 2 passed (2)
Writing a unit test suite
Test contract logic with an ordinary test framework, no node, indexer, or proof server required. A good suite exercises both directions: the paths that must succeed, and the paths your assert statements must reject, including a caller with the wrong private state.
Prerequisites
- The context pattern from Calling circuits from JavaScript.
Procedure
-
Write a setup helper that builds a fresh contract and context per test:
const setup = (secretKey = key(7)) => {const contract = new Contract(witnesses);const ctor = contract.initialState(RT.createConstructorContext({ secretKey }, COIN));const ctx = RT.createCircuitContext(ADDR, COIN, ctor.currentContractState, { secretKey });return { contract, ctx };}; -
Assert the success paths through the typed ledger view, and the failure paths against the exact
assertmessages from the Compact source. To simulate an attacker, run a circuit with a context whosecurrentPrivateStateholds a different secret:const stranger = { ...occupied, currentPrivateState: { secretKey: key(9) } };expect(() => contract.impureCircuits.takeDown(stranger)).toThrow('Attempted to take down post, but not the current owner',);
Verification
The full suite covers the genesis state, the post and take-down lifecycle, both rejection paths, and pure-circuit determinism.
import { describe, it, expect } from 'vitest';
import * as RT from '@midnight-ntwrk/compact-runtime';
import { Contract, State, ledger, pureCircuits } from './managed/bboard/contract/index.js';
const COIN = '0'.repeat(64);
const ADDR = RT.sampleContractAddress();
const key = (n) => { const a = new Uint8Array(32); a[31] = n; return a; };
const witnesses = {
localSecretKey: ({ privateState }) => [privateState, privateState.secretKey],
};
const setup = (secretKey = key(7)) => {
const contract = new Contract(witnesses);
const ctor = contract.initialState(RT.createConstructorContext({ secretKey }, COIN));
const ctx = RT.createCircuitContext(ADDR, COIN, ctor.currentContractState, { secretKey });
return { contract, ctx };
};
describe('bulletin board contract', () => {
it('starts vacant', () => {
const { ctx } = setup();
const board = ledger(ctx.currentQueryContext.state);
expect(board.state).toBe(State.VACANT);
expect(board.message.is_some).toBe(false);
expect(board.sequence).toBe(1n);
});
it('accepts a post on a vacant board', () => {
const { contract, ctx } = setup();
const result = contract.impureCircuits.post(ctx, 'Test message');
const board = ledger(result.context.currentQueryContext.state);
expect(board.state).toBe(State.OCCUPIED);
expect(board.message.is_some).toBe(true);
expect(board.message.value).toBe('Test message');
});
it('rejects a post on an occupied board', () => {
const { contract, ctx } = setup();
const occupied = contract.impureCircuits.post(ctx, 'First message').context;
expect(() => contract.impureCircuits.post(occupied, 'Second message')).toThrow(
'Attempted to post to an occupied board',
);
});
it('lets the owner take the post down and returns the message', () => {
const { contract, ctx } = setup();
const occupied = contract.impureCircuits.post(ctx, 'Mine to remove').context;
const takeDown = contract.impureCircuits.takeDown(occupied);
expect(takeDown.result).toBe('Mine to remove');
expect(ledger(takeDown.context.currentQueryContext.state).state).toBe(State.VACANT);
});
it('rejects a take-down from a non-owner', () => {
const { contract, ctx } = setup();
const occupied = contract.impureCircuits.post(ctx, 'Not yours').context;
const stranger = { ...occupied, currentPrivateState: { secretKey: key(9) } };
expect(() => contract.impureCircuits.takeDown(stranger)).toThrow(
'Attempted to take down post, but not the current owner',
);
});
it('computes a deterministic result from the owner-commitment circuit', () => {
const first = pureCircuits.publicKey(key(7), key(1));
const second = pureCircuits.publicKey(key(7), key(1));
const other = pureCircuits.publicKey(key(8), key(1));
expect(first).toBeInstanceOf(Uint8Array);
expect(first.length).toBe(32);
expect(first).toEqual(second);
expect(first).not.toEqual(other);
});
});
✓ bboard.test.ts > bulletin board contract > starts vacant
✓ bboard.test.ts > bulletin board contract > accepts a post on a vacant board
✓ bboard.test.ts > bulletin board contract > rejects a post on an occupied board
✓ bboard.test.ts > bulletin board contract > lets the owner take the post down and returns the message
✓ bboard.test.ts > bulletin board contract > rejects a take-down from a non-owner
✓ bboard.test.ts > bulletin board contract > computes a deterministic result from the owner-commitment circuit
Test Files 1 passed (1)
Tests 6 passed (6)
The generated export surface
What the module and its declaration file export, and what each export is for. Consult this when wiring the implementation into an application or test suite.
| Export | Kind | Purpose |
|---|---|---|
Contract | class | Instantiated with your witnesses; exposes circuits, impureCircuits, provableCircuits, and initialState() |
pureCircuits | object | Pure circuits callable without a circuit context |
ledger(state) | function | Decodes a StateValue or ChargedState into typed per-field getters |
State | enum | The contract's exported Compact enum, mirrored in JavaScript |
contractReferenceLocations | constant | Internal metadata about contract references in ledger state |
The declaration file types the same surface for TypeScript projects:
export type Witnesses<PS> = {
localSecretKey(context: __compactRuntime.WitnessContext<Ledger, PS>): [PS, Uint8Array];
}
// ... State enum and Circuits / ProvableCircuits types omitted ...
export type ImpureCircuits<PS> = {
post(context: __compactRuntime.CircuitContext<PS>, newMessage_0: string): __compactRuntime.CircuitResults<PS, []>;
takeDown(context: __compactRuntime.CircuitContext<PS>): __compactRuntime.CircuitResults<PS, string>;
}
export type PureCircuits = {
publicKey(sk_0: Uint8Array, sequence_0: Uint8Array): Uint8Array;
}
export type Ledger = {
readonly state: State;
readonly message: { is_some: boolean, value: string };
readonly sequence: bigint;
readonly owner: Uint8Array;
}
export declare class Contract<PS = any, W extends Witnesses<PS> = Witnesses<PS>> {
witnesses: W;
circuits: Circuits<PS>;
impureCircuits: ImpureCircuits<PS>;
provableCircuits: ProvableCircuits<PS>;
constructor(witnesses: W);
initialState(context: __compactRuntime.ConstructorContext<PS>): __compactRuntime.ConstructorResult<PS>;
}
// ... ContractReferenceLocations omitted ...
export declare function ledger(state: __compactRuntime.StateValue | __compactRuntime.ChargedState): Ledger;
export declare const pureCircuits: PureCircuits;
The generic parameter PS is your private state type, which the witnesses read and update. With these declarations, a TypeScript project gets autocomplete and compile-time checking on every circuit call.
Everything else in the module, the _descriptor_* objects and the classes generated for composite types such as Maybe, is internal encoding machinery. It is regenerated on every compile and its numbering shifts as the contract changes, so the exports above are the only supported surface.
Errors from the generated module
The generated code validates at three points: at import time, in the Contract constructor, and on every circuit call. The import-time check is the version guard at the top of index.js:
import * as __compactRuntime from '@midnight-ntwrk/compact-runtime';
__compactRuntime.checkRuntimeVersion('0.16.0');
Match an error against this table before reading the generated source:
| Error | Cause | Fix |
|---|---|---|
| Throws at import, naming a runtime version | The installed @midnight-ntwrk/compact-runtime is not compatible with the version the compiler expects: for a 0.x runtime, compatible means the same minor version and at least the expected patch | Pair the compiler and runtime using the support matrix |
Contract constructor: expected 1 argument, received 0 | The constructor takes exactly one argument, the witnesses object | Pass the witnesses object, and nothing else |
does not contain a function-valued field named localSecretKey | The witnesses object is missing a witness the Compact source declares | Implement one function per declared witness, as in Implementing witnesses for a contract |
post: expected 2 arguments (as invoked from Typescript), received 0 | An impure circuit takes the circuit context plus each parameter in the Compact signature | Pass the context first, then the circuit's own arguments |
type error: ... expected value of type CircuitContext, citing a line in your .compact source | The context was built by hand, so the wrapper's check for currentQueryContext fails | Build contexts with createConstructorContext and createCircuitContext, as in Calling circuits from JavaScript |
expected instance of ChargedState | ledger() received a ContractState, which it does not accept | Pass the state from a circuit call, or a deployed contract state's data field |
An assert message from your own Compact source | Contract logic rejected the call | Nothing to fix in the harness: this is the rejection path a test asserts with expect(...).toThrow(...) |
Additional resources
- Bulletin board DApp: the complete application built around this contract.
- Test and debug: debugging strategies and common failure scenarios. Its code examples predate the current
CircuitContextAPI, so use the context pattern from this page. - Security and best practices: adversarial testing patterns that build on this harness.
- Compact runtime API reference: the
CircuitContext,WitnessContext, and helper functions used here. - Support matrix: which compiler version pairs with which runtime version.