Skip to main content
Version: Canary 🚧

The Compact JavaScript Implementation: run your contracts off-chain

If you’ve written smart contracts before, you’re probably used to languages like Solidity or Rust that compile to on-chain bytecode.

The Midnight blockchain takes a different approach. It uses a domain-specific language called Compact, designed from the ground up for zero-knowledge smart contracts: programs that can prove that private data exists (and is known) and that satisfies the constraints required by the contract.

When you compile a Compact contract, the compiler doesn’t just generate the zero-knowledge circuits. It also produces a JavaScript implementation, typically found in a file called index.cjs in a subfolder next to your .compact file.

This implementation lets you execute and test your Compact contract logic directly in JavaScript, using Node.js or any standard testing framework. As we have not (yet) provided a Compact contract unit testing framework, but since contracts are compiled to JavaScript, you can write JavaScript unit tests against their JavaScript implementations. It’s your bridge between the high-level Compact code and the low-level ZK circuit that runs on-chain.

In this tutorial, we’ll explore the implementation generated for the bboard contract in depth. You’ll learn:

  • How the index.cjs file is generated during compilation

  • How it’s structured and what each section does

  • How the implementation connects JavaScript to Compact’s zero-knowledge circuits

  • And how you can use it to write unit tests or run the contract locally

By the end, you’ll understand how Compact’s JavaScript implementation fits into the development workflow — and how it makes testing private smart contracts both accessible and familiar.

How the JavaScript implementation Gets Generated

When you compile a Compact contract, the compiler produces more than just zero-knowledge circuits — it also emits a matching JavaScript implementation (typically named index.cjs). This implementation is essential for simulating, testing, and interacting with your contract logic in a plain JavaScript environment (e.g., Node.js, browser tests). In this section, we’ll walk through how and why that implementation is generated.

The Compilation Pipeline: Compact → Circuits + JS implementation

Here’s a high-level view of what happens under the hood:

  1. Circuit Generation
    The compiler checks your .compact files and emits zero-knowledge circuits for each contract function or “entry point”.

  2. Implementation File Generation
    Concurrently, the compiler generates a JavaScript implementation file that mirrors the contract’s structure:

    • It knows which functions exist (their signatures, inputs, outputs).

    • It embeds type descriptors for all Compact types used (integers, booleans, enums, bytes, composite types).

    • It wraps each contract entry point so that you can invoke it in JS, passing native JS values, and get back state transitions.

  3. Linking to the Compact Implementation Library
    The generated index.cjs does not reimplement arithmetic, field operations, or other foundational ZK logic; instead, it imports a shared implementation library from @midnight-ntwrk/compact-runtime. That library implements:

    • Finite field arithmetic

    • Serialization and deserialization

    • Error types, type checks, alignment rules

    • Circuit-related helper functions
      The generated file and the implementation library together form a complete environment.

  4. Source Maps & Type Declarations

    • A source map (index.cjs.map) is emitted so that debugging or tracing can map back to your original Compact source (line numbers, symbol names).

    • A TypeScript declaration file (index.d.cts) 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.cjs is not a hand-written artifact but a systematically generated adapter between Compact’s ZK circuits and the JavaScript world.

Tip: If you ever change your Compact contract (add or remove functions, change types), a fresh compilation will regenerate index.cjs accordingly. Always treat it as generated rather than handwritten, and refrain from changing the code manually.

In the next section, we’ll break down how index.cjs is structured internally, from boilerplate and imports, through type descriptors and composite types, all the way to the wrapper functions you actually call in your tests.

Inside the JavaScript Implementation: Understanding the Structure

Once you’ve compiled your Compact contract, you’ll find the generated implementation in a file named index.cjs.

This file is a self-contained CommonJS module that mirrors your contract’s structure, from type definitions to callable functions.

Let’s look at how it’s built, piece by piece.

1. Implementation Initialization and Safety Checks

At the very top, the implementation ensures that the version of @midnight-ntwrk/compact-runtime installed in your project matches the version expected by the compiler. It also verifies that the arithmetic field used by the circuits is consistent.

'use strict';

const __compactRuntime = require('@midnight-ntwrk/compact-runtime');
const expectedRuntimeVersionString = '0.8.1';
const expectedRuntimeVersion = expectedRuntimeVersionString.split('-')[0].split('.').map(Number);
const actualRuntimeVersion = __compactRuntime.versionString.split('-')[0].split('.').map(Number);
if (expectedRuntimeVersion[0] != actualRuntimeVersion[0] || [...])
throw new __compactRuntime.CompactError(`Version mismatch...`);

{
const MAX_FIELD = 52435875175126190479447740508185965837690552500527637822603658699938581184512n;
if (__compactRuntime.MAX_FIELD !== MAX_FIELD)
throw new __compactRuntime.CompactError(`compiler thinks maximum field value is ${MAX_FIELD}...`);
}

This boilerplate ensures that your locally installed implementation matches what the Compact compiler expected when it generated the circuits.

2. Type Definitions and Descriptors

Next, the file defines enumerations and type descriptors.

These tell the implementation how to encode and decode the data types used in your contract — like integers, strings, or custom structs.

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

const _descriptor_1 = new __compactRuntime.CompactTypeUnsignedInteger(18446744073709551615n, 8);
const _descriptor_2 = new __compactRuntime.CompactTypeBytes(32);
const _descriptor_3 = new __compactRuntime.CompactTypeBoolean();
const _descriptor_4 = new __compactRuntime.CompactTypeOpaqueString();

Each _descriptor_* object defines how JavaScript and the on-chain values are represented.

3. Composite Types and Data Structures

Complex Compact types, like Option, Maybe, or structured records, are represented as small JavaScript 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));
}
}

Each of these classes provides methods like fromValue() and toValue() to convert between JavaScript objects and ledger-compatible encodings.

4. The Contract Class and Circuit Wrappers

This is where things get interesting; the generated implementation defines a Contract class that mirrors your Compact contract’s entry points (such as post, takeDown, etc.).

These methods don’t execute business logic directly; instead, they prepare inputs, execute the entrypoints of the contract implementation, and handle proof data.

class Contract {
constructor(witnesses) {
this.witnesses = witnesses;
this.circuits = {
post: (...args) => {
const context = args[0];
const newMessage = args[1];
const partialProofData = {
input: { value: _descriptor_4.toValue(newMessage), alignment: _descriptor_4.alignment() },
output: undefined,
publicTranscript: [],
privateTranscriptOutputs: []
};
const result = this._post_0(context, partialProofData, newMessage);
return { result, context, proofData: partialProofData };
},
takeDown: (...args) => {
[...]
}
};
}
}

When you call contract.circuits.post(context, newMessage) in JavaScript, the implementation automatically:

  • Validates input types

  • Encodes data for the ZK circuit

  • Executes the Compact logic

  • Returns structured proof data for verification

5. Exports and Type Bindings

Finally, the implementation exports everything you’ll need to interact with the contract in your JavaScript code or tests.

exports.Contract = Contract;

exports.State = State;

The corresponding index.d.cts file provides type hints so that when you import this module in a TypeScript project, your IDE understands what functions and structures are available.

Each of these sections works together to make the Compact contract executable outside the blockchain, in a safe and fully verifiable way.

In the next section, we’ll see how you can use this implementation in practice: setting it up in a JavaScript test environment and running contract calls just like you would on-chain.

Using the JavaScript Implementation

Now that you understand how the index.cjs file is structured, let’s see how to use it in your own development workflow.

In this section, we’ll walk through how to import the implementation, instantiate the contract, and call its functions from a regular JavaScript or TypeScript environment.

1. Importing the Implementation

Once you’ve compiled your Compact contract, the compiler will output three key files in the build directory:

  • index.cjs
  • index.cjs.map
  • index.d.cts

You can load the implementation just like any other Node.js module:

const { Contract, State } = require('./index.cjs');

If you’re using TypeScript, the accompanying declaration file (index.d.cts) will automatically provide type hints for your contract and its methods.

2. Instantiating the Contract

Every Compact contract implementation expects a witness object when it’s created.
This object typically contains references to cryptographic keys or helper functions that represent a party’s identity in the ZK system.

Here’s a minimal example:

const contract = new Contract({  
localSecretKey: () =>
Buffer.from('aabbccddeeff00112233445566778899', 'hex')
});

The implementation checks that this object is correctly formed before allowing any circuit execution — if you forget a required field, it throws a descriptive CompactError.

3. Calling Contract Functions

Each contract entry point (like post or takeDown in this example) is exposed as a JavaScript function under contract.circuits.

These wrappers prepare the inputs, run the JavaScript code, and return structured results containing the output, context, and proof data.

const context = {  
originalState: { status: State.VACANT },
transactionContext: { timestamp: Date.now() }
};

const message = "Hello from Compact!";

const { result, proofData, context } =
contract.circuits.post(context, message);

// result: Circuit result (e.g., new state)
// proofData: Data you can use for proof verification
// context: The new context

The proofData object contains the same type of witness and transcript information that the blockchain implementation would generate during an actual transaction — meaning you can verify or inspect the same structure locally.

4. Writing Unit Tests

Because the Compact implementation is just a CommonJS module, you can easily integrate it with Vitest, Jest, Mocha, or any testing framework you prefer.

describe('Bulletin Board Contract', () => {  
it('should accept a new post', () => {
const ctx = { originalState: { status: State.VACANT }, transactionContext: {} };
const result = contract.circuits.post(ctx, "Test message");
expect(result.result.status).toBe(State.OCCUPIED);
});
});

This makes it easy to test your contract logic off-chain, with full control over inputs and without having to run a Midnight node or a proof server.

The implementation thus serves as your development playground: a place to test, simulate, and reason about Compact contract logic before deploying it.

In the next section, we’ll look at why this design matters, how the JavaScript implementation bridges human-readable code and zero-knowledge execution, and why it’s key to making privacy-preserving smart contracts developer-friendly.

Why This Design Matters

In the previous section, we saw how you can import the generated implementation, instantiate your contract, and call its functions just like any normal JavaScript class.

But you might be wondering — why does Compact bother generating a JavaScript implementation at all?

Why not just compile directly to zero-knowledge circuits and leave it at that?

The answer comes down to developer experience, reproducibility, and trust.

1. A Bridge Between ZK Circuits and Everyday Code

Zero-knowledge circuits are powerful, but they’re also complex and opaque, not something you can easily debug or test directly.

The JavaScript implementation acts as a human-friendly bridge between the low-level proof system and the high-level contract logic.

When you call:

const { result, proofData } = contract.circuits.post(context, "Hello world!");

You’re running exactly the same logic that the ZK circuit 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 ever need to touch the proof server or submit a transaction to the Midnight network.

2. Type Safety and Consistency Across Environments

Because the implementation uses Compact’s own type descriptors (CompactTypeBoolean, CompactTypeBytes, etc.), the data you pass into your JavaScript tests is encoded in the exact same way it will be on-chain. That consistency eliminates a whole class of subtle bugs, for example, differences in byte order, field alignment, or encoding length:

// Your JS test environment
const message = "Hello Midnight!";
const proof = contract.circuits.post(context, message);

// Your blockchain execution
// (same inputs, same deterministic behavior)

You can test and reason about your contract logic with confidence that the ZK circuit behaves identically.

3. Reproducibility and Proof Transparency

Each call to a contract method in the implementation returns a structured proofData object — the input (along with a representation of the circuit) to the prover.

That data is crucial for reproducible testing and transparent verification:

{
input: { value: [...], alignment: [...] },
output: { value: [...], alignment: [...] },
publicTranscript: [...],
privateTranscriptOutputs: [...]
}

Having this available directly in JavaScript allows you to record, replay, and verify circuit executions as part of your normal testing flow, without relying on external tools.

4. Developer Productivity Without Compromising Privacy

The implementation design lets Compact developers use familiar tools,TypeScript, Jest, VSCode, Node.js, while still working with privacy-preserving logic.

Instead of being locked into a specialized proving environment, you can:

  • Write integration tests in the same language as your app.

  • Simulate user flows off-chain.

  • Validate logic changes before recompiling circuits.

It’s the best of both worlds: developer-friendly ergonomics with cryptographic guarantees under the hood.

Compact’s JavaScript implementation isn’t just a convenience feature; it’s what makes zero-knowledge smart contract development practical.

By exposing ZK logic through familiar code, it shortens the gap between concept, implementation, and verification.

In the next and final section, we’ll wrap up with a brief summary of what we’ve learned and how you can start experimenting with Compact contracts yourself.

Conclusion

By now, you’ve seen how Compact smart contracts on the Midnight blockchain are more than just cryptographic programs; they’re part of a full developer workflow.

When you compile a Compact contract, you don’t just get zero-knowledge circuits that the blockchain can prove; you also get a JavaScript implementation that mirrors those circuits and lets you test, simulate, and understand your contract logic in a familiar environment.

We started by exploring how this implementation is generated, then walked through its structure, looking at type descriptors, composite data types, and the Contract class that wraps each circuit.

From there, we learned how to use it in practice: importing the implementation, creating a witness object, calling contract methods like post, and writing repeatable tests in JavaScript.

Finally, we looked at why this design matters: it bridges the gap between developer experience and cryptographic correctness, letting you reason about private, verifiable logic with ordinary code.

The Compact implementation is more than a side product; it’s a key part of what makes building zero-knowledge smart contracts approachable. It gives you confidence that what you test locally will behave the same way once deployed on the Midnight network.

So the next time you compile a Compact contract and see an index.cjs file appear, take a moment to open it up, you’ll find a window into how your private, provable logic comes to life.