Skip to main content
For the complete documentation index, see llms.txt

The Compact JavaScript implementation

When you compile a Compact contract, the compiler emits more than zero-knowledge circuits: it also generates a JavaScript implementation of your contract. Calling a circuit through that implementation runs the same logic the ZK circuit enforces on-chain, in a form you can step through, log, and test in Node.js, so you can validate a contract's behavior long before you generate proofs or submit transactions. This guide explains what the generated module contains and walks through importing it, calling circuits, and unit testing a contract, using the bulletin board contract as the example throughout.

Prerequisites

These apply to every procedure in this guide:

  • The Compact CLI installed, with compact compile working.
  • A compiled contract. This guide compiles bboard.compact from example-bboard; any contract works, with your own names in place of the bulletin board's.
  • Node.js with Vitest and @midnight-ntwrk/compact-runtime installed, for calling circuits and running the verification tests.
  • A runtime version that matches your compiler. The generated code enforces this pairing at import time; check the support matrix when either changes.

From Compact source to the JavaScript implementation

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:

  1. Parses your .compact file 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's publicKey compiles to JavaScript only.
  2. 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.
  3. 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.
  4. 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 implementation
  • index.d.ts: TypeScript type definitions
  • index.js.map: source map for debugging
Generated code only

index.js is regenerated on every compilation. If you add or remove circuits or change types, recompile; never edit the generated files by hand.

Generated module structure

The generated index.js is a self-contained ES module. Three things sit at the top of it: a version guard, type descriptors, and classes for composite types. Knowing what each does makes the rest of the file readable.

The version guard runs at import time, before anything else:

import * as __compactRuntime from '@midnight-ntwrk/compact-runtime';
__compactRuntime.checkRuntimeVersion('0.16.0');

If the installed @midnight-ntwrk/compact-runtime does not match what the compiler expects, the import throws instead of failing later in subtler ways. The exact version string depends on the compiler release that generated the file; the support matrix lists which compiler pairs with which runtime.

Type descriptors tell the module how to encode and decode every Compact type the contract uses. For the bulletin board contract, the compiler emits:

export var State;
(function (State) {
State[State['VACANT'] = 0] = 'VACANT';
State[State['OCCUPIED'] = 1] = 'OCCUPIED';
})(State || (State = {}));

const _descriptor_0 = new __compactRuntime.CompactTypeEnum(1, 1);

const _descriptor_1 = new __compactRuntime.CompactTypeUnsignedInteger(18446744073709551615n, 8);

const _descriptor_2 = new __compactRuntime.CompactTypeBytes(32);

const _descriptor_3 = __compactRuntime.CompactTypeBoolean;

const _descriptor_4 = __compactRuntime.CompactTypeOpaqueString;

// ... further descriptors follow ...

Each descriptor converts between JavaScript values and the on-chain representation: CompactTypeEnum for the State enum, CompactTypeUnsignedInteger for the Counter-backed sequence field, CompactTypeBytes(32) for the owner field, and CompactTypeOpaqueString for the message text. The numbering is assigned by the compiler and changes as the contract changes; treat it as internal.

Composite types such as Maybe become small classes that combine primitive descriptors:

class _Maybe_0 {
alignment() {
return _descriptor_3.alignment().concat(_descriptor_4.alignment());
}
fromValue(value_0) {
return {
is_some: _descriptor_3.fromValue(value_0),
value: _descriptor_4.fromValue(value_0)
}
}
toValue(value_0) {
return _descriptor_3.toValue(value_0.is_some).concat(_descriptor_4.toValue(value_0.value));
}
}

const _descriptor_5 = new _Maybe_0();

alignment() returns the field alignment for circuit encoding, fromValue() decodes ledger values into JavaScript objects, and toValue() encodes them back. This Maybe instance backs the bulletin board's message field, which is a Maybe<Opaque<"string">> in the Compact source and a plain { is_some, value } object in JavaScript.

The Contract class and circuits

The heart of the module is the Contract class, which mirrors your Compact contract circuit for circuit, plus two standalone exports: pureCircuits for context-free computation and ledger() for reading contract state.

The constructor validates your witnesses. A contract instance is created with one argument, the witnesses object, and the generated code checks that every witness the Compact source declares is present as a function:

export class Contract {
witnesses;
constructor(...args_0) {
if (args_0.length !== 1) {
throw new __compactRuntime.CompactError(`Contract constructor: expected 1 argument, received ${args_0.length}`);
}
const witnesses_0 = args_0[0];
if (typeof(witnesses_0) !== 'object') {
throw new __compactRuntime.CompactError('first (witnesses) argument to Contract constructor is not an object');
}
if (typeof(witnesses_0.localSecretKey) !== 'function') {
throw new __compactRuntime.CompactError('first (witnesses) argument to Contract constructor does not contain a function-valued field named localSecretKey');
}
this.witnesses = witnesses_0;
// ... circuit wrappers, shown below ...
}
}

Circuit wrappers validate inputs and package results. Each circuit becomes a method that checks its arguments, encodes the inputs with the type descriptors, runs the contract logic, and returns a structured result:

this.circuits = {
post: (...args_1) => {
if (args_1.length !== 2) {
throw new __compactRuntime.CompactError(`post: expected 2 arguments (as invoked from Typescript), received ${args_1.length}`);
}
const contextOrig_0 = args_1[0];
const newMessage_0 = args_1[1];
if (!(typeof(contextOrig_0) === 'object' && contextOrig_0.currentQueryContext != undefined)) {
__compactRuntime.typeError('post',
'argument 1 (as invoked from Typescript)',
'bboard.compact line 41 char 1',
'CircuitContext',
contextOrig_0)
}
const context = { ...contextOrig_0, gasCost: __compactRuntime.emptyRunningCost() };
const partialProofData = {
input: {
value: _descriptor_4.toValue(newMessage_0),
alignment: _descriptor_4.alignment()
},
output: undefined,
publicTranscript: [],
privateTranscriptOutputs: []
};
const result_0 = this._post_0(context, partialProofData, newMessage_0);
partialProofData.output = { value: [], alignment: [] };
return { result: result_0, context: context, proofData: partialProofData, gasCost: context.gasCost };
},
// ... takeDown follows the same shape ...
publicKey(context, ...args_1) {
return { result: pureCircuits.publicKey(...args_1), context };
}
};
this.impureCircuits = {
post: this.circuits.post,
takeDown: this.circuits.takeDown
};
// ... provableCircuits follows the same shape ...

circuits contains every callable circuit; impureCircuits narrows to the ones that touch witnesses or ledger state (provableCircuits lists the same set, naming the circuits a proof can be generated for). The class also exposes initialState(constructorContext), which runs the Compact constructor block to produce the contract's genesis state.

Pure circuits need no context. Circuits that read neither ledger state nor witnesses are exported once more on a standalone object, callable as plain functions:

export const pureCircuits = {
publicKey: (...args_0) => {
if (args_0.length !== 2) {
throw new __compactRuntime.CompactError(`publicKey: expected 2 arguments (as invoked from Typescript), received ${args_0.length}`);
}
const sk_0 = args_0[0];
const sequence_0 = args_0[1];
if (!(sk_0.buffer instanceof ArrayBuffer && sk_0.BYTES_PER_ELEMENT === 1 && sk_0.length === 32)) {
__compactRuntime.typeError('publicKey',
'argument 1',
'bboard.compact line 58 char 1',
'Bytes<32>',
sk_0)
}
// ... same check for the second argument ...
return _dummyContract._publicKey_0(sk_0, sequence_0);
}
};

ledger() turns raw state into typed getters. The exported ledger function accepts the state you get back from a circuit call, or the state of a deployed contract obtained for example through the indexer, and returns an object with one lazy getter per ledger field:

export function ledger(stateOrChargedState) {
const state = stateOrChargedState instanceof __compactRuntime.StateValue ? stateOrChargedState : stateOrChargedState.state;
const chargedState = stateOrChargedState instanceof __compactRuntime.StateValue ? new __compactRuntime.ChargedState(stateOrChargedState) : stateOrChargedState;
const context = {
currentQueryContext: new __compactRuntime.QueryContext(chargedState, __compactRuntime.dummyContractAddress()),
costModel: __compactRuntime.CostModel.initialCostModel()
};
// ...
return {
get state() {
return _descriptor_0.fromValue(__compactRuntime.queryLedgerState(context,
partialProofData,
[
{ dup: { n: 0 } },
{ idx: { cached: false,
pushPath: false,
path: [
{ tag: 'value',
value: { value: _descriptor_11.toValue(0n),
alignment: _descriptor_11.alignment() } }] } },
{ popeq: { cached: false,
result: undefined } }]).value);
},
// ... message, sequence, and owner getters follow the same shape ...
};
}

Each getter runs a small ledger query program against the state and decodes the answer with the right descriptor, so a DApp reads board.message.value instead of decoding raw state cells.

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.

ExportKindPurpose
ContractclassInstantiated with your witnesses; exposes circuits, impureCircuits, provableCircuits, and initialState()
pureCircuitsobjectPure circuits callable without a circuit context
ledger(state)functionDecodes a StateValue or ChargedState into typed per-field getters
StateenumThe contract's exported Compact enum, mirrored in JavaScript
contractReferenceLocationsconstantInternal 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];
}

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>;
}

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.

Importing the implementation and implementing witnesses

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

  1. Compile the contract and locate the generated module in the managed output:

    compact compile src/bboard.compact src/managed/bboard
  2. Import the module like any other ES module. In TypeScript, the declaration file types everything automatically:

    import { Contract, State, ledger, pureCircuits } from './managed/bboard/contract/index.js';
  3. 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 WitnessContext carrying the ledger view, the private state, and the contract address, and returns a tuple of the updated private state and the witness value.

  4. 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-witnesses.test.ts
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 contract circuits

Run contract logic off-chain by building a circuit context and invoking circuits through the instance. The context is created with runtime helpers, not assembled by hand; hand-built context objects fail the generated validation because a real CircuitContext carries query-context state the wrappers check for.

Prerequisites

Procedure

  1. 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 });
  2. 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
  3. Read the resulting ledger state with the ledger() helper:

    const board = ledger(call.context.currentQueryContext.state);
    // board.state, board.message, board.sequence, board.owner
  4. 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.

circuits.test.ts
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)

Unit testing a contract

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

Procedure

  1. 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 };
    };
  2. Assert the success paths through the typed ledger view, and the failure paths against the exact assert messages from the Compact source. To simulate an attacker, run a circuit with a context whose currentPrivateState holds 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.

bboard.test.ts
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 owner commitment with the pure 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 owner commitment with the pure circuit

Test Files 1 passed (1)
Tests 6 passed (6)

Additional resources