For the complete documentation index, see llms.txt
Work with your compiled contract
If you have written smart contracts before, then you are likely familiar with languages like Solidity or Rust that compile to on-chain bytecode. The Midnight blockchain takes a different approach by using a domain-specific language called Compact, designed from the ground up for zero-knowledge (ZK) smart contracts.
When you compile a Compact contract, the compiler emits a matching JavaScript implementation alongside the ZK circuits. That implementation is how your application code talks to the contract: you import it, implement its witnesses, call its circuits, and test it like any other module.
This guide first explains what that generated implementation is and how the compiler structures it, using the bulletin board smart contract as the running example. It then shows how to use the implementation in your development workflow.
How the JavaScript implementation gets generated
When you compile a Compact contract, the compiler produces more than just ZK circuits. It also emits a matching JavaScript implementation named index.js.
This implementation is essential for simulating, testing, and interacting with your contract logic in a plain JavaScript environment, such as Node.js or browser tests. This section explains how and why that implementation is generated.
The compilation pipeline: Compact to circuits and JavaScript implementation
The compilation process follows these steps:
Circuit generation: The compiler parses your .compact files and emits ZK circuits for each exported circuit function.
Implementation file generation: Concurrently, the compiler generates a JavaScript implementation file that mirrors the contract's structure. The compiler:
- Identifies which circuits exist with their signatures, inputs, and outputs.
- Embeds type descriptors for all Compact types used, such as integers, booleans, enums, bytes, and composite types.
- Wraps each circuit so you can invoke it in JavaScript, passing native JavaScript values and receiving state transitions in return.
Linking to the Compact runtime library: The generated index.js does not reimplement arithmetic, field operations, or other foundational ZK logic. Instead, it imports a shared runtime library from @midnight-ntwrk/compact-runtime. That library implements:
- Finite field arithmetic
- Serialization and deserialization
- Error types and type checks
- Circuit-related helper functions
Together, the generated file and the runtime library form a complete execution environment.
Type declarations: A TypeScript declaration file index.d.ts is generated so that when you import this implementation in a TypeScript project, you get proper types, autocomplete, and compile-time safety.
Because of these steps, index.js is not a hand-written artifact, but a systematically generated adapter between Compact's ZK circuits and the JavaScript world.
If you change your Compact contract by adding or removing functions or changing types, then recompile to regenerate index.js accordingly. Always treat it as generated code rather than hand-written, and avoid modifying it manually.
Understand the JavaScript implementation structure
The generated implementation for your Compact contract appears in a file named index.js within the managed directory.
This file is a self-contained ES module that mirrors your contract's structure, from type definitions to callable functions.
Runtime initialization and version checks
At the very top, the implementation imports the Compact runtime and verifies version compatibility:
import * as __compactRuntime from '@midnight-ntwrk/compact-runtime';
__compactRuntime.checkRuntimeVersion('0.16.0');
This ensures that the version of @midnight-ntwrk/compact-runtime installed in your project matches the version expected by the compiler. Version mismatches can cause runtime errors or incorrect circuit behavior.
Always refer to the compatibility matrix to ensure that the version of the Compact runtime matches the version of the compiler.
Type definitions and descriptors
The file defines enumerations and type descriptors. These tell the implementation how to encode and decode the data types used in your contract, such as integers, strings, or custom structures.
export var State;
(function (State) {
State[State['VACANT'] = 0] = 'VACANT';
State[State['OCCUPIED'] = 1] = 'OCCUPIED';
})(State || (State = {}));
const _descriptor_0 = new __compactRuntime.CompactTypeBytes(32);
const _descriptor_1 = __compactRuntime.CompactTypeBoolean;
const _descriptor_2 = __compactRuntime.CompactTypeOpaqueString;
const _descriptor_4 = new __compactRuntime.CompactTypeEnum(1, 1);
const _descriptor_5 = new __compactRuntime.CompactTypeUnsignedInteger(18446744073709551615n, 8);
Each descriptor object defines how JavaScript values are converted to and from their on-chain representations:
CompactTypeBytes(32): Represents 32-byte arrays (the type of theownerfield)CompactTypeBoolean: Represents boolean valuesCompactTypeOpaqueString: Represents string data (the type of themessagefield)CompactTypeEnum: Represents the State enumCompactTypeUnsignedInteger: Represents Counter and other numeric types (the type of thesequencefield)
Composite types and data structures
Complex Compact types such as Maybe or Either are represented as JavaScript classes that combine primitive descriptors.
class _Maybe_0 {
alignment() {
return _descriptor_1.alignment().concat(_descriptor_2.alignment());
}
fromValue(value_0) {
return {
is_some: _descriptor_1.fromValue(value_0),
value: _descriptor_2.fromValue(value_0)
}
}
toValue(value_0) {
return _descriptor_1.toValue(value_0.is_some).concat(_descriptor_2.toValue(value_0.value));
}
}
const _descriptor_3 = new _Maybe_0();
Each composite type class provides methods for converting between JavaScript objects and ledger-compatible encodings:
alignment(): Returns the field alignment requirements for ZK circuit encoding.fromValue(): Decodes ledger values into JavaScript objects.toValue(): Encodes JavaScript objects into ledger values.
The Maybe type corresponds to the message ledger field in the bulletin board contract, representing optional string values.
The Contract class and circuit wrappers
The generated implementation defines a Contract class that mirrors your Compact contract's circuits. The constructor validates the witnesses object and sets up circuit methods.
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;
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];
const context = { ...contextOrig_0, gasCost: __compactRuntime.emptyRunningCost() };
const partialProofData = {
input: {
value: _descriptor_2.toValue(newMessage_0),
alignment: _descriptor_2.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: (...args_1) => {
if (args_1.length !== 1) {
throw new __compactRuntime.CompactError(`takeDown: expected 1 argument, received ${args_1.length}`);
}
const contextOrig_0 = args_1[0];
const context = { ...contextOrig_0, gasCost: __compactRuntime.emptyRunningCost() };
const partialProofData = {
input: { value: [], alignment: [] },
output: undefined,
publicTranscript: [],
privateTranscriptOutputs: []
};
const result_0 = this._takeDown_0(context, partialProofData);
partialProofData.output = { value: _descriptor_2.toValue(result_0), alignment: _descriptor_2.alignment() };
return { result: result_0, context: context, proofData: partialProofData, gasCost: context.gasCost };
},
publicKey(context, ...args_1) {
return { result: pureCircuits.publicKey(...args_1), context };
}
};
this.impureCircuits = {
post: this.circuits.post,
takeDown: this.circuits.takeDown
};
}
}
When you call contract.circuits.post(context, newMessage) in JavaScript, the implementation automatically validates input types and encodes data for the ZK circuit.
It then executes the Compact logic and returns structured proofData for verification.
The circuits object contains all callable functions, including both impure circuits (post and takeDown) and pure circuits (publicKey). The impureCircuits object contains only the circuits that interact with witnesses and modify state.
Pure circuits implementation
The implementation also exports pure circuits that can be called directly without a circuit context:
export const pureCircuits = {
publicKey(sk_0, sequence_0) {
const mem_0 = __compactRuntime.emptyMemory();
if (_descriptor_0.sizeOf(sk_0) != 32) {
__compactRuntime.valueSizeError('publicKey',
'argument 1',
'bboard.compact line 60 char 1',
'Bytes<32>',
_descriptor_0.sizeOf(sk_0),
32)
}
if (_descriptor_0.sizeOf(sequence_0) != 32) {
__compactRuntime.valueSizeError('publicKey',
'argument 2',
'bboard.compact line 60 char 1',
'Bytes<32>',
_descriptor_0.sizeOf(sequence_0),
32)
}
return __compactRuntime.persistentHash(
mem_0,
_descriptor_7,
[__compactRuntime.padStringToBytes(32, "bboard:pk:"), sequence_0, sk_0]
);
}
};
Pure circuits like publicKey perform deterministic computations without accessing ledger state or witnesses. They can be called independently for operations, such as generating owner commitments or computing hashes.
Ledger state deserialization
The implementation provides a function to deserialize raw ledger state into typed JavaScript objects:
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()
};
const partialProofData = {
input: { value: [], alignment: [] },
output: undefined,
publicTranscript: [],
privateTranscriptOutputs: []
};
return {
get state() {
return _descriptor_4.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
);
},
// Similar getter implementations for message, sequence, and owner fields
// Each uses queryLedgerState with the appropriate field index
};
}
This function converts raw contract state from the blockchain into a structured Ledger object with properly typed fields:
- Accepts either a
StateValueorChargedStatefrom the indexer. - Creates a query context for accessing ledger fields with cost tracking.
- Returns an object with getter properties for each ledger field (state, message, sequence, owner).
- Each getter uses
queryLedgerStatewith field index paths to retrieve the specific value lazily. - DApps use this function to interpret contract state returned from the indexer.
Exports and type bindings
The implementation exports everything you need to interact with the contract:
export class Contract { ... }
export var State;
export const pureCircuits = { ... };
export function ledger(state) { ... }
The corresponding index.d.ts file provides TypeScript type definitions:
export enum State { VACANT = 0, OCCUPIED = 1 }
export type Maybe<T> = { is_some: boolean; value: T };
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: Maybe<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>;
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;
These type definitions enable type-safe contract interaction in TypeScript projects. Your IDE understands what functions and structures are available, providing autocomplete and compile-time error checking.
Import the implementation
With the structure clear, the rest of this guide puts the implementation to work in your development workflow.
Once you have compiled your Compact contract, the compiler outputs these key files in the managed directory:
index.js: The JavaScript implementationindex.d.ts: TypeScript type definitionsindex.js.map: Source map for debugging
You can load the implementation like any other ECMAScript (ES) module:
import { Contract, State, pureCircuits, ledger } from './managed/bboard/contract/index.js';
If you are using TypeScript, then the accompanying declaration file index.d.ts automatically provides type hints for your contract and its methods.
Implement witnesses
Every Compact contract with witness functions requires a witnesses object when instantiated. This object contains implementations for all witness functions declared in your Compact code.
For the bulletin board contract, create a witnesses.ts file in the contract/src directory:
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],
};
The witnesses object maps witness function names to their implementations. Each witness function receives a WitnessContext containing the ledger state, private state, and contract address. The function returns a tuple of the updated private state and the witness value.
Call contract circuits
The implementation exposes each circuit as a JavaScript function under contract.circuits or contract.impureCircuits. These wrappers prepare the inputs, run the JavaScript implementation, and return structured results containing the output, updated context, and proof data.
Here's an example of calling the post impure circuit:
const initialContext = {
originalState: {
state: State.VACANT,
message: { is_some: false, value: '' },
sequence: 1n,
owner: new Uint8Array(32)
},
privateState: {
secretKey: new Uint8Array(32)
},
contractAddress: '0x...',
transactionContext: {}
};
const message = "Hello from Compact!";
const { result, context, proofData, gasCost } =
contract.circuits.post(initialContext, message);
The returned object contains:
result: The circuit's return value (empty array for post)context: The updated circuit context with new ledger stateproofData: Data structure containing input, output, and transcripts for proof generationgasCost: Gas cost tracking information
Here's an example of calling the publicKey pure circuit:
const secretKey = new Uint8Array(32);
const sequenceBytes = new Uint8Array(32);
const ownerCommitment = pureCircuits.publicKey(secretKey, sequenceBytes);
You can call pure circuits directly without a circuit context. They perform deterministic computations and return values immediately.
Write unit tests
Because the Compact implementation is a standard ES module, you can integrate it with testing frameworks such as Vitest, Jest, or Mocha.
import { describe, it, expect } from 'vitest';
import { Contract, State } from './managed/bboard/contract/index.js';
import { witnesses, createBBoardPrivateState } from './witnesses.js';
describe('Bulletin board contract', () => {
it('accepts a new post on vacant board', () => {
const contract = new Contract(witnesses);
const context = {
originalState: {
state: State.VACANT,
message: { is_some: false, value: '' },
sequence: 1n,
owner: new Uint8Array(32)
},
privateState: createBBoardPrivateState(new Uint8Array(32)),
contractAddress: '0x0000000000000000000000000000000000000000000000000000000000000000',
transactionContext: {}
};
const { result, context: newContext } = contract.circuits.post(context, "Test message");
expect(newContext.originalState.state).toBe(State.OCCUPIED);
expect(newContext.originalState.message.is_some).toBe(true);
expect(newContext.originalState.message.value).toBe("Test message");
});
it('rejects post on occupied board', () => {
const contract = new Contract(witnesses);
const context = {
originalState: {
state: State.OCCUPIED,
message: { is_some: true, value: 'Existing message' },
sequence: 1n,
owner: new Uint8Array(32)
},
privateState: createBBoardPrivateState(new Uint8Array(32)),
contractAddress: '0x0000000000000000000000000000000000000000000000000000000000000000',
transactionContext: {}
};
expect(() => contract.circuits.post(context, "New message"))
.toThrow("Attempted to post to an occupied board");
});
});
This allows you to test your contract logic off-chain with full control over inputs and without requiring a Midnight Node or proof server.
Why the Compact JavaScript implementation matters
This section explains why Compact generates a JavaScript implementation and why this design is critical for building privacy-preserving smart contracts.
A bridge between ZK circuits and everyday code
Zero-knowledge (ZK) circuits are complex and opaque, and you cannot debug or test them directly.
The JavaScript implementation acts as a bridge between the low-level proof system and the high-level contract logic. When you call contract.circuits.post(context, "Hello world!"), you are running exactly the same logic that the ZK circuit executes on-chain, but in a form that you can step through, log, and inspect in Node.js.
This means you can validate the behavior of your contract locally before you need to generate proofs or submit transactions to the Midnight network.
Type safety and consistency across environments
The implementation uses Compact's own type descriptors, such as CompactTypeBoolean and CompactTypeBytes, so the data you pass in your JavaScript tests uses exactly the same encoding as the chain. This consistency eliminates a whole class of subtle bugs related to differences in byte order, field alignment, or encoding length.
const message = "Hello Midnight!";
const proof = contract.circuits.post(context, message);
You can test and reason about your contract logic with confidence that the ZK circuit behaves identically.
Reproducibility and proof transparency
Each call to a contract circuit returns a structured proofData object. This data is the input to the prover along with a representation of the circuit.
That data is crucial for reproducible testing and transparent verification:
{
input: { value: [...], alignment: [...] },
output: { value: [...], alignment: [...] },
publicTranscript: [...],
privateTranscriptOutputs: [...]
}
Having this available directly in JavaScript lets you record, replay, and verify circuit executions as part of your normal testing flow. You don't need to rely on external tools.
Developer productivity without compromising privacy
The implementation design allows Compact developers to use familiar tools, such as TypeScript, Jest, VSCode, and Node.js, while working with privacy-preserving logic.
Instead of depending on a specialized proving environment, you can:
- Write integration tests in the same language as your application.
- Simulate user flows off-chain.
- Validate logic changes before recompiling circuits.
This combination provides developer-friendly ergonomics with cryptographic guarantees under the hood.
Next steps
Now you understand how the compiler generates the Compact JavaScript implementation and how to use it. Explore the Bulletin board DApp for a complete example of using the JavaScript implementation.