Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TypeScript Integration

This note covers how to use your compiled Compact contract from TypeScript, implement witnesses, and interact with the Midnight SDK.

Docs: Writing a Contract · SDK


Intuition First

When you compile a Compact contract, you get TypeScript output. This output is your DApp’s interface to the contract.

Three things happen on the TypeScript side:

  1. Witness implementations, You provide the callback bodies for witnesses
  2. Circuit calls, You call circuits through the generated API
  3. State reads, You read ledger state to display to users

The Compiled Output

After compiling, you get:

contracts/managed/myContract/
├── contract/
│   ├── index.js          # Main API
│   ├── index.d.ts      # Type definitions
│   └── index.js.map   # Source map
├── compiler/
│   └── contract-info.json
├── zkir/
│   └── *.zkir
└── keys/
    ├── *.prover
    └── *.verifier

The Contract API

import { myContract } from './contract';

const contract = new myContract(witnesses);

// Call a circuit
const tx = await contract.callTx.mint(metadataHash);

// Read state
const totalSupply = contract.state.totalSupply;

Implementing Witnesses

Witnesses are the bridge between your Compact contract and private user data.

In Compact (declaration)

witness callerAddress(): Bytes<32>;
witness secretKey(): Bytes<32>;

In TypeScript (implementation)

import { findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
import { createWallet, createProviders } from './utils';

const SEED = process.env.WALLET_SEED;

async function main() {
  const wallet = await createWallet(SEED);
  const providers = await createProviders(wallet);

  const contract = await findDeployedContract(providers, {
    contractAddress: 'addr...',
    compiledContract: await getCompiled('contract'),
  });

  // Implement witnesses
  const witnesses = {
    callerAddress: () => {
      // Get the caller's derived address
      return derivedAddress(wallet);
    },
    secretKey: () => {
      // Get secret key for signing (never goes on-chain)
      return wallet.secretKey;
    },
  };

  // Call circuit with witnesses
  const tx = await contract.circuits.transfer(
    { context: providers },
    tokenId,
    newOwner,
    tokenMetaHash
  );
}

Witness Context

interface WitnessContext {
  transactionId: string;
  proposer: Uint8Array;
  nonce: bigint;
}

Circuit Invocation

Impure Circuits

These access ledger state:

// Get context from providers
const context = await providers.midnight();

const result = await contract.circuits.mint(context, metadataHash);

console.log('Transaction:', result.transactionId);

Pure Circuits

These don’t access ledger state:

// Pure circuits don't need context
const hash = contract.circuits.hashData(data);

console.log('Hash:', hash);

Return Values

const result = await contract.circuits.getValue(context);

console.log(result.value);  // The returned value

Reading Ledger State

Simple Fields

const state = contract.state;

console.log('Total supply:', state.totalSupply);
console.log('Owner:', state.owner);

Maps

// Check membership
const hasKey = state.tokenCommitments.member(tokenId);

// Get value
const commitment = state.tokenCommitments.lookup(tokenId);

// Get root (MerkleTree)
const root = state.merkleTree.root();

Counters

const count = state.counter.value();

SDK Usage

Installation

npm install @midnight-ntwrk/midnight-js-contracts @midnight-ntwrk/midnight-js-sdk

Basic Setup

import {
  createWallet,
  createProviders,
  findDeployedContract,
} from '@midnight-ntwrk/midnight-js-contracts';

async function setup() {
  // Create wallet from seed
  const wallet = createWallet('your-seed-phrase');

  // Create providers (connects to chain)
  const providers = await createProviders(wallet);

  return { wallet, providers };
}

Deployment

import { deployContract } from '@midnight-ntwrk/midnight-js-contracts';

async function deploy(providers, initialState) {
  const contract = await deployContract(providers, {
    initialState,
    compiledContract: await getCompiled('contract'),
  });

  console.log('Deployed at:', contract.address);
  return contract;
}

Error Handling

try {
  const tx = await contract.circuits.mint(context, metadataHash);
} catch (error) {
  if (error.message.includes('Assertion failed')) {
    console.log('Circuit assertion failed');
  } else if (error.message.includes('Insufficient balance')) {
    console.log('Not enough tokens');
  } else {
    throw error;
  }
}

Type Definitions

The .d.ts file tells you what’s available:

export interface CircuitResults<PS, Returns> {
  state: PS;           // Post-state
  returns: Returns;   // Return values
}

export interface Witnesses {
  callerAddress(context: WitnessContext): [PS, Uint8Array];
  secretKey(context: WitnessContext): [PS, Uint8Array];
}

export interface Ledger {
  readonly totalSupply: bigint;
  readonly owner: Uint8Array;
  tokenCommitments: {
    member(key: bigint): boolean;
    lookup(key: bigint): Uint8Array;
  };
}

Quick Recap

  • Compile produces TypeScript bindings in contract/
  • Witnesses are implemented in TypeScript, declared in Compact
  • Call circuits via contract.circuits.method(context, args)
  • Read state via contract.state.field
  • Use SDK for wallet and deployment