Skip to main content

The Compact JavaScript implementation

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.

This guide explores the JavaScript implementation generated for the bulletin board smart contract in depth.

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:

1

Circuit generation: The compiler parses your .compact files and emits ZK circuits for each exported circuit function.

2

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.
3

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.

4

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.

tip

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.14.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.

info

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 the owner field)
  • CompactTypeBoolean: Represents boolean values
  • CompactTypeOpaqueString: Represents string data (the type of the message field)
  • CompactTypeEnum: Represents the State enum
  • CompactTypeUnsignedInteger: Represents Counter and other numeric types (the type of the sequence field)

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 StateValue or ChargedState from 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 queryLedgerState with 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.

Next steps

Now you understand how the Compact JavaScript implementation is generated. You can learn how to use it in the Use the Compact JavaScript implementation guide.