| name | midnight-js |
| description | The Midnight.js TypeScript SDK for building DApps on Midnight Network. Covers all provider setup, wallet SDK integration (HDWallet, WalletFacade, ShieldedWallet, UnshieldedWallet, DustWallet), contract deployment, circuit calls, private state management, DUST generation, and testkit usage. Use this skill whenever a user is wiring up providers, managing wallets in a Node.js or browser context, deploying or calling Compact contracts from TypeScript, handling DUST/fee flow, reading ledger state, or writing tests for Midnight DApps. |
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';
setNetworkId('preprod');
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';
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),
);
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';
export type CounterCircuits = ImpureCircuitId<Counter.Contract<CounterPrivateState>>;
export const CounterPrivateStateId = 'counterPrivateState' as const;
export type CounterPrivateStateId = typeof CounterPrivateStateId;
export type CounterProviders = MidnightProviders<
CounterCircuits,
CounterPrivateStateId,
CounterPrivateState
>;
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';
const seed = generateRandomSeed();
const seedHex = Buffer.from(seed).toString('hex');
const seedHex = '...';
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();
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')),
}).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';
const syncedState = await Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((state) => state.isSynced),
),
);
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';
globalThis.WebSocket = WebSocket as unknown as typeof globalThis.WebSocket;
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.
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
if (state.dust.availableCoins.length > 0) {
console.log('DUST already available:', state.dust.walletBalance(new Date()));
return;
}
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);
}
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',
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),
walletProvider: { },
midnightProvider: { },
privateStateProvider: createPrivateStateProvider(),
};
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) },
);
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
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
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...',
compiledContract,
privateStateId: CounterPrivateStateId,
initialPrivateState: { privateCounter: 0 },
});
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 {
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();
});
afterAll(async () => {
await testEnvironment.shutdown();
});
const walletProvider = await testEnvironment.getMidnightWalletProvider();
const [wallet1, wallet2] = await testEnvironment.startMidnightWalletProviders(2);
Environment selection via env var:
MN_TEST_ENVIRONMENT=undeployed yarn test
MN_TEST_ENVIRONMENT=devnet yarn test
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:
cd counter-cli && npm run preprod-ps
docker compose -f proof-server.yml up
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 (Pinned — Official Counter Example)
{
"@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)