SuperAPI AI Directory

midnight-js

Coding & development

View repository

Updated 10-15 23:44

About this skill

Midnight.js Skill

Midnight.js is the TypeScript SDK for building DApps on Midnight Network — analogous to Web3.js for Ethereum. The official SDK page is "coming soon"; this skill is grounded in the official Counter CLI tutorial and the 1AM starter template.

Primary references:

  • docs.midnight.network/tutorials/counter/counter-cli — official full implementation
  • github.com/midnightntwrk/example-counter — reference repo (Node.js / headless wallet)
  • github.com/webisoftSoftware/1AM-starter-template — browser wallet variant

1) Package Map

Package Purpose
@midnight-ntwrk/midnight-js-contracts deployContract, findDeployedContract, submitCallTx
@midnight-ntwrk/midnight-js-types MidnightProviders, WalletProvider, MidnightProvider, createProofProvider
@midnight-ntwrk/midnight-js-indexer-public-data-provider indexerPublicDataProvider (GraphQL indexer client)
@midnight-ntwrk/midnight-js-fetch-zk-config-provider FetchZkConfigProvider (browser / CDN)
@midnight-ntwrk/midnight-js-node-zk-config-provider NodeZkConfigProvider (Node.js / filesystem)
@midnight-ntwrk/midnight-js-http-client-proof-provider httpClientProofProvider (self-hosted proof server)
@midnight-ntwrk/midnight-js-level-private-state-provider levelPrivateStateProvider (persistent LevelDB private state)
@midnight-ntwrk/midnight-js-network-id setNetworkId, getNetworkId
@midnight-ntwrk/compact-runtime ContractState (indexer deserialization)
@midnight-ntwrk/ledger-v8 Transaction, LedgerParameters, ZswapSecretKeys, DustSecretKey
@midnight-ntwrk/compact-js CompiledContract, ImpureCircuitId
@midnight-ntwrk/wallet-sdk-facade WalletFacade (unified wallet)
@midnight-ntwrk/wallet-sdk-hd HDWallet, generateRandomSeed, Roles
@midnight-ntwrk/wallet-sdk-shielded ShieldedWallet
@midnight-ntwrk/wallet-sdk-unshielded-wallet UnshieldedWallet, createKeystore, PublicKey
@midnight-ntwrk/wallet-sdk-dust-wallet DustWallet
@midnight-ntwrk/wallet-sdk-address-format MidnightBech32m, ShieldedAddress, etc.

2) Network Configuration

Always call setNetworkId before any SDK operation. It sets a global that all libraries use automatically.

import { setNetworkId, getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';

// Call once at startup, in the config constructor or before building providers
setNetworkId('preprod'); // 'preprod' | 'preview' | 'mainnet' | 'undeployed'

Network endpoints:

Network Indexer HTTP Indexer WS RPC
preprod https://indexer.preprod.midnight.network/api/v4/graphql wss://indexer.preprod.midnight.network/api/v4/graphql/ws https://rpc.preprod.midnight.network
preview https://indexer.preview.midnight.network/api/v4/graphql wss://indexer.preview.midnight.network/api/v4/graphql/ws wss://rpc.preview.midnight.network
mainnet https://indexer.mainnet.midnight.network/api/v4/graphql wss://indexer.mainnet.midnight.network/api/v4/graphql/ws https://rpc.mainnet.midnight.network
undeployed (local) http://localhost:8088/api/v4/graphql ws://localhost:8088/api/v4/graphql/ws ws://localhost:9944

3) Compiled Contract Setup

Pre-compile the contract once at module load — not on every deploy/call.

import { CompiledContract } from '@midnight-ntwrk/compact-js';
import { Contract, witnesses } from './managed/counter'; // generated by compact compiler

// Node.js: load ZK assets from filesystem
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
const zkConfigPath = path.resolve('contract/src/managed/counter');

const compiledContract = CompiledContract.make('counter', Contract).pipe(
  CompiledContract.withVacantWitnesses,
  CompiledContract.withCompiledFileAssets(zkConfigPath),
);

// Browser / CDN: load ZK assets via fetch
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
const compiledContract = CompiledContract.make('YourContract', Contract).pipe(
  CompiledContract.withVacantWitnesses,
  CompiledContract.withCompiledFileAssets('/zk/your-contract'),
);

4) Type Definitions (Important Pattern)

Define typed aliases for your contract's providers — this is the Midnight.js pattern:

import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types';
import type { DeployedContract, FoundContract } from '@midnight-ntwrk/midnight-js-contracts';
import type { ImpureCircuitId } from '@midnight-ntwrk/compact-js';
import { Counter, type CounterPrivateState } from './managed/counter';

// Extract circuit IDs from the contract type
export type CounterCircuits = ImpureCircuitId<Counter.Contract<CounterPrivateState>>;

// Private state key (string literal type)
export const CounterPrivateStateId = 'counterPrivateState' as const;
export type CounterPrivateStateId = typeof CounterPrivateStateId;

// Full providers type for this contract
export type CounterProviders = MidnightProviders<
  CounterCircuits,
  CounterPrivateStateId,
  CounterPrivateState
>;

// Contract instance types
export type CounterContract = Counter.Contract<CounterPrivateState>;
export type DeployedCounterContract = DeployedContract<CounterContract> | FoundContract<CounterContract>;

5) Wallet Setup (Node.js / Headless)

HD Key Derivation

import { HDWallet, generateRandomSeed, Roles } from '@midnight-ntwrk/wallet-sdk-hd';
import * as ledger from '@midnight-ntwrk/ledger-v8';
import { Buffer } from 'buffer';

// Generate a fresh random seed (returns Uint8Array)
const seed = generateRandomSeed();
const seedHex = Buffer.from(seed).toString('hex'); // save this

// Or restore from existing hex seed
const seedHex = '...'; // from user input

const hdWallet = HDWallet.fromSeed(Buffer.from(seedHex, 'hex'));
if (hdWallet.type !== 'seedOk') throw new Error('Invalid seed');

const derivationResult = hdWallet.hdWallet
  .selectAccount(0)
  .selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
  .deriveKeysAt(0);

if (derivationResult.type !== 'keysDerived') throw new Error('Key derivation failed');

hdWallet.hdWallet.clear(); // wipe secret material from memory

const keys = derivationResult.keys;
const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);

Wallet Construction

import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
import { UnshieldedWallet, createKeystore, PublicKey, InMemoryTransactionHistoryStorage } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';

const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], getNetworkId());

const shieldedWallet = ShieldedWallet({
  networkId: getNetworkId(),
  indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
  provingServerUrl: new URL(proofServer),
  relayURL: new URL(node.replace(/^http/, 'ws')), // convert http → ws
}).startWithSecretKeys(shieldedSecretKeys);

const unshieldedWallet = UnshieldedWallet({
  networkId: getNetworkId(),
  indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
  txHistoryStorage: new InMemoryTransactionHistoryStorage(),
}).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore));

const dustWallet = DustWallet({
  networkId: getNetworkId(),
  costParameters: {
    additionalFeeOverhead: 300_000_000_000_000n,
    feeBlocksMargin: 5,
  },
  indexerClientConnection: { indexerHttpUrl: indexer, indexerWsUrl: indexerWS },
  provingServerUrl: new URL(proofServer),
  relayURL: new URL(node.replace(/^http/, 'ws')),
}).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust);

const wallet = new WalletFacade(shieldedWallet, unshieldedWallet, dustWallet);
await wallet.start(shieldedSecretKeys, dustSecretKey);

Wallet Sync (RxJS pattern — always used with WalletFacade)

import * as Rx from 'rxjs';

// Wait until wallet is fully synced
const syncedState = await Rx.firstValueFrom(
  wallet.state().pipe(
    Rx.throttleTime(5_000),
    Rx.filter((state) => state.isSynced),
  ),
);

// Wait until wallet has non-zero unshielded balance
import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';

const balance = await Rx.firstValueFrom(
  wallet.state().pipe(
    Rx.throttleTime(10_000),
    Rx.filter((s) => s.isSynced),
    Rx.map((s) => s.unshielded.balances[unshieldedToken().raw] ?? 0n),
    Rx.filter((balance) => balance > 0n),
  ),
);

Node.js WebSocket requirement:

import { WebSocket } from 'ws';
// Required for GraphQL subscriptions (wallet sync) to work in Node.js
globalThis.WebSocket = WebSocket as unknown as typeof globalThis.WebSocket;
// Put this at the very top of your entry file, before any wallet imports

6) DUST Generation (Preprod / Undeployed Only)

NIGHT tokens generate DUST over time, but only after UTXOs are explicitly registered on-chain. DUST is the non-transferable fee resource for all transactions.

// 1. Wait for sync
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));

// 2. Check if DUST already available
if (state.dust.availableCoins.length > 0) {
  console.log('DUST already available:', state.dust.walletBalance(new Date()));
  return;
}

// 3. Register unregistered NIGHT UTXOs
const nightUtxos = state.unshielded.availableCoins.filter(
  (coin: any) => coin.meta?.registeredForDustGeneration !== true,
);

if (nightUtxos.length > 0) {
  const recipe = await wallet.registerNightUtxosForDustGeneration(
    nightUtxos,
    unshieldedKeystore.getPublicKey(),
    (payload) => unshieldedKeystore.signData(payload),
  );
  const finalized = await wallet.finalizeRecipe(recipe);
  await wallet.submitTransaction(finalized);
}

// 4. Wait for DUST balance > 0 (may take a few minutes)
await Rx.firstValueFrom(
  wallet.state().pipe(
    Rx.throttleTime(5_000),
    Rx.filter((s) => s.isSynced),
    Rx.filter((s) => s.dust.walletBalance(new Date()) > 0n),
  ),
);

DUST troubleshooting:

  • DUST balance drops to 0 after failed deploy → restart DApp to release locked DUST coins
  • pendingCoins > 0 && availableCoins === 0 → DUST locked by a pending/failed transaction
  • NIGHT registered but DUST = 0 → still accruing, wait a few minutes

7) Provider Configuration

Node.js (self-hosted proof server)

import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';

const zkConfigProvider = new NodeZkConfigProvider<CounterCircuits>(zkConfigPath);

const providers: CounterProviders = {
  privateStateProvider: levelPrivateStateProvider<CounterPrivateStateId>({
    privateStateStoreName: 'counter-private-state',  // LevelDB store name
    walletProvider: walletAndMidnightProvider,
  }),
  publicDataProvider: indexerPublicDataProvider(indexerHttp, indexerWs),
  zkConfigProvider,
  proofProvider: httpClientProofProvider(proofServerUrl, zkConfigProvider),
  walletProvider: walletAndMidnightProvider,
  midnightProvider: walletAndMidnightProvider,
};

Browser (1AM wallet / CDN ZK assets)

import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { createProofProvider } from '@midnight-ntwrk/midnight-js-types';

const config = await connectedAPI.getConfiguration();
setNetworkId(config.networkId);

const zkConfigProvider = new FetchZkConfigProvider(
  new URL(zkAssetBasePath, window.location.origin).toString(),
  window.fetch.bind(window),
);

const provingProvider = await connectedAPI.getProvingProvider(zkConfigProvider);

const providers = {
  publicDataProvider: indexerPublicDataProvider(config.indexerUri, config.indexerWsUri),
  zkConfigProvider,
  proofProvider: createProofProvider(provingProvider),  // wraps 1AM proving provider
  walletProvider: { /* see 1AM skill */ },
  midnightProvider: { /* see 1AM skill */ },
  privateStateProvider: createPrivateStateProvider(), // in-memory for browser
};

8) WalletProvider & MidnightProvider (Node.js headless)

This is the bridge between WalletFacade and the midnight-js contracts API:

import type { WalletProvider, MidnightProvider } from '@midnight-ntwrk/midnight-js-types';
import * as ledger from '@midnight-ntwrk/ledger-v8';

const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));

const walletAndMidnightProvider: WalletProvider & MidnightProvider = {
  getCoinPublicKey() {
    return state.shielded.coinPublicKey.toHexString();
  },
  getEncryptionPublicKey() {
    return state.shielded.encryptionPublicKey.toHexString();
  },
  async balanceTx(tx, ttl?) {
    const recipe = await wallet.balanceUnboundTransaction(
      tx,
      { shieldedSecretKeys, dustSecretKey },
      { ttl: ttl ?? new Date(Date.now() + 30 * 60 * 1000) },
    );

    // ⚠️ KNOWN BUG WORKAROUND: wallet SDK signRecipe hardcodes 'pre-proof' but
    // proven (UnboundTransaction) intents contain 'proof' data → "Failed to clone intent"
    // Sign intents manually with the correct proof marker (see signTransactionIntents below)
    signTransactionIntents(recipe.baseTransaction, signFn, 'proof');
    if (recipe.balancingTransaction) {
      signTransactionIntents(recipe.balancingTransaction, signFn, 'pre-proof');
    }

    return wallet.finalizeRecipe(recipe);
  },
  submitTx(tx) {
    return wallet.submitTransaction(tx) as any;
  },
};

The "Failed to clone intent" Bug Fix

/**
 * Workaround for wallet SDK bug where signRecipe hardcodes 'pre-proof',
 * causing failures when signing proven (UnboundTransaction) intents.
 * Call with proofMarker='proof' for baseTransaction, 'pre-proof' for balancingTransaction.
 */
const signTransactionIntents = (
  tx: { intents?: Map<number, any> },
  signFn: (payload: Uint8Array) => ledger.Signature,
  proofMarker: 'proof' | 'pre-proof',
): void => {
  if (!tx.intents || tx.intents.size === 0) return;

  for (const segment of tx.intents.keys()) {
    const intent = tx.intents.get(segment);
    if (!intent) continue;

    const cloned = ledger.Intent.deserialize<
      ledger.SignatureEnabled, ledger.Proofish, ledger.PreBinding
    >('signature', proofMarker, 'pre-binding', intent.serialize());

    const signature = signFn(cloned.signatureData(segment));

    if (cloned.fallibleUnshieldedOffer) {
      const sigs = cloned.fallibleUnshieldedOffer.inputs.map(
        (_: ledger.UtxoSpend, i: number) =>
          cloned.fallibleUnshieldedOffer!.signatures.at(i) ?? signature,
      );
      cloned.fallibleUnshieldedOffer = cloned.fallibleUnshieldedOffer.addSignatures(sigs);
    }

    if (cloned.guaranteedUnshieldedOffer) {
      const sigs = cloned.guaranteedUnshieldedOffer.inputs.map(
        (_: ledger.UtxoSpend, i: number) =>
          cloned.guaranteedUnshieldedOffer!.signatures.at(i) ?? signature,
      );
      cloned.guaranteedUnshieldedOffer = cloned.guaranteedUnshieldedOffer.addSignatures(sigs);
    }

    tx.intents.set(segment, cloned);
  }
};

9) Deploy & Call Contracts

Deploy

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

const deployed = await deployContract(providers, {
  compiledContract,
  privateStateId: CounterPrivateStateId,
  initialPrivateState: { privateCounter: 0 },
});

console.log('Contract address:', deployed.deployTxData.public.contractAddress);

Call a Circuit

// Using callTx (recommended — automatically builds, proves, balances, submits)
const result = await deployed.callTx.increment();
console.log('txId:', result.public.txId);
console.log('blockHeight:', result.public.blockHeight);

Join an Existing Contract

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

const contract = await findDeployedContract(providers, {
  contractAddress: '09dbe05f...', // hex string
  compiledContract,
  privateStateId: CounterPrivateStateId,
  initialPrivateState: { privateCounter: 0 },
});

// Now call circuits on it
await contract.callTx.increment();

Read Ledger State

import { ContractState } from '@midnight-ntwrk/compact-runtime';
import { Counter } from './managed/counter';

const contractState = await providers.publicDataProvider.queryContractState(contractAddress);

if (contractState === null) {
  console.log('Contract not found');
} else {
  // Apply generated ledger() function to deserialize typed state
  const ledgerState = Counter.ledger(contractState.data);
  console.log('Counter value:', ledgerState.round);
}

10) Private State Provider (In-Memory — Browser / Minimal)

import type {
  PrivateStateProvider, PrivateStateId, PrivateStateExport,
  SigningKeyExport,
} from '@midnight-ntwrk/midnight-js-types';

function createPrivateStateProvider() {
  let scope = '';
  const stateStore = new Map<string, unknown>();
  const signingKeyStore = new Map<string, unknown>();
  const key = (id: string) => `${scope}:${id}`;

  return {
    setContractAddress(address: string) { scope = address; },
    async set(id: string, state: unknown) { stateStore.set(key(id), state); },
    async get(id: string) { return stateStore.get(key(id)) ?? null; },
    async remove(id: string) { stateStore.delete(key(id)); },
    async clear() { stateStore.clear(); },
    async setSigningKey(addr: string, k: unknown) { signingKeyStore.set(addr, k); },
    async getSigningKey(addr: string) { return signingKeyStore.get(addr) ?? null; },
    async removeSigningKey(addr: string) { signingKeyStore.delete(addr); },
    async clearSigningKeys() { signingKeyStore.clear(); },
    async exportPrivateStates(): Promise<PrivateStateExport> { throw new Error('Not implemented'); },
    async importPrivateStates() { throw new Error('Not implemented'); },
    async exportSigningKeys(): Promise<SigningKeyExport> { throw new Error('Not implemented'); },
    async importSigningKeys() { throw new Error('Not implemented'); },
  };
}

For persistent Node.js private state, use levelPrivateStateProvider instead.


11) Testkit (Integration Testing)

import { getTestEnvironment } from '@midnight-ntwrk/testkit-js';
import pino from 'pino';

const logger = pino({ level: 'info' });
let testEnvironment: Awaited<ReturnType<typeof getTestEnvironment>>;

beforeAll(async () => {
  testEnvironment = getTestEnvironment(logger);
  const environmentConfig = await testEnvironment.start();
  // environmentConfig contains indexer, node, proofServer URLs
});

afterAll(async () => {
  await testEnvironment.shutdown();
});

// Get wallet providers for testing
const walletProvider = await testEnvironment.getMidnightWalletProvider();
// or multiple wallets (max 4 on local):
const [wallet1, wallet2] = await testEnvironment.startMidnightWalletProviders(2);

Environment selection via env var:

# Local Docker (default)
MN_TEST_ENVIRONMENT=undeployed yarn test

# Against preprod
MN_TEST_ENVIRONMENT=devnet yarn test

# Custom endpoints
MN_TEST_ENVIRONMENT=env-var-remote \
MN_TEST_NETWORK_ID=undeployed \
MN_TEST_INDEXER=http://localhost:8088/api/ \
MN_TEST_INDEXER_WS=ws://localhost:8088/ws/ \
MN_TEST_NODE=http://localhost:9944 \
yarn test

12) Proof Server

For Node.js / headless use, run the proof server locally via Docker:

# Auto-started proof server (preprod, pulls image automatically)
cd counter-cli && npm run preprod-ps

# Or manually
docker compose -f proof-server.yml up
# Wait for: "starting service... listening on: 0.0.0.0:6300"

# Direct Docker run
docker run -p 6300:6300 midnightntwrk/proof-server:latest midnight-proof-server -v

Proof server URL: http://127.0.0.1:6300

Mac ARM (Apple Silicon) fix: If the proof server hangs, go to Docker Desktop → Settings → General → Virtual Machine Options → select Docker VMM → restart Docker.


13) Common Pitfalls

setNetworkId not called → cryptic type mismatch errors deep in SDK. Call it immediately at startup, before any SDK operations.

WebSocket not set globally in Node.js → wallet sync GraphQL subscriptions silently fail. Put globalThis.WebSocket = WebSocket at the top of your entry file.

ZK assets not found → double-check zkConfigPath is absolute, points to the compiled managed folder, and contains keys/ and zkir/ subdirectories. Run the sync:assets npm script before dev.

"Failed to clone intent" → wallet SDK bug, use the signTransactionIntents workaround. Occurs when a proven UnboundTransaction is balanced — the SDK hardcodes 'pre-proof' but the intents contain 'proof' data.

DUST = 0 after failed deploy → known wallet SDK issue. Restart the DApp to release the locked DUST coins. Use the DUST monitor to verify status before deploying.

Wallet 0 balance after faucet → wait for sync to complete first. If still 0, verify you used the unshielded address (mn_addr_preprod1...), not the shielded address.

relayURL must be WebSocket → convert https:// → wss://, http:// → ws://. The wallet SDK expects a WebSocket URL for the relay.

CompiledContract.make called on every deploy → call it once at module load, not inside the deploy function. Loading ZK keys on every call is slow.

levelPrivateStateProvider in browser → LevelDB is Node.js only. Use the in-memory createPrivateStateProvider for browser contexts.

Old contract address after recompile → verifier keys change when the Compact contract changes. Old addresses will fail proof verification. Always redeploy after any contract change.


14) Dependency Versions (snapshot — not a pin)

These version numbers are a dated snapshot and drift. Resolve the current tested set from the support matrix before installing — see references/volatile.md. The package names below are the useful part; never install @latest.

{
  "@midnight-ntwrk/compact-runtime": "0.15.0",
  "@midnight-ntwrk/ledger-v8": "8.0.3",
  "@midnight-ntwrk/midnight-js-contracts": "4.0.2",
  "@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.2",
  "@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.2",
  "@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.2",
  "@midnight-ntwrk/midnight-js-network-id": "4.0.2",
  "@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.2",
  "@midnight-ntwrk/midnight-js-types": "4.0.2",
  "@midnight-ntwrk/midnight-js-utils": "4.0.2",
  "@midnight-ntwrk/wallet-sdk-address-format": "3.1.0",
  "@midnight-ntwrk/wallet-sdk-dust-wallet": "3.0.0",
  "@midnight-ntwrk/wallet-sdk-facade": "3.0.0",
  "@midnight-ntwrk/wallet-sdk-hd": "3.0.1",
  "@midnight-ntwrk/wallet-sdk-shielded": "2.1.0",
  "@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0",
  "@midnight-ntwrk/compact-js": "2.5.0"
}

Node.js version: 22+ (required)