Loading...
Loading...
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.
npx skill4agent add kali-decoder/midnight-skills midnight-jsdocs.midnight.network/tutorials/counter/counter-cligithub.com/midnightntwrk/example-countergithub.com/webisoftSoftware/1AM-starter-template| Package | Purpose |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
setNetworkIdimport { 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 | Indexer HTTP | Indexer WS | RPC |
|---|---|---|---|
| | | |
| | | |
| | | |
| | | |
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'),
);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>;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]);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);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),
),
);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// 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),
),
);pendingCoins > 0 && availableCoins === 0import { 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,
};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
};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;
},
};/**
* 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);
}
};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);// 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);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();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);
}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'); },
};
}levelPrivateStateProviderimport { 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);# 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# 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 -vhttp://127.0.0.1:6300setNetworkIdglobalThis.WebSocket = WebSocketzkConfigPathkeys/zkir/sync:assetssignTransactionIntentsUnboundTransaction'pre-proof''proof'mn_addr_preprod1...relayURLhttps://wss://http://ws://CompiledContract.makelevelPrivateStateProvidercreatePrivateStateProvider{
"@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"
}