| name | example-counter |
| description | Generate a complete Midnight Network counter DApp from scratch — Compact smart contract, interactive CLI, wallet setup, DUST generation, deploy + interact loop, Docker configs for local/preprod/preview, and tests. Use when a user wants to build a full Midnight DApp, bootstrap a new project, understand the end-to-end lifecycle, or needs working boilerplate for wallet + contract + CLI. This skill produces the official midnightntwrk/example-counter reference implementation using yarn workspaces. |
Example Counter DApp Skill
This skill generates the complete, runnable midnightntwrk/example-counter reference DApp. It covers every file: Compact contract, TypeScript sources, entry points for each network, Docker compose files, test infrastructure, and all supporting config. The code matches the official implementation at github.com/midnightntwrk/example-counter (v2.1.1).
Primary references:
github.com/midnightntwrk/example-counter — official reference repo
docs.midnight.network/guides/deploy-mn-app — official deploy guide
docs.midnight.network/guides/interact-with-mn-app — official interact guide
docs.midnight.network/relnotes/support-matrix — authoritative version compatibility matrix
Package versions match the official compatibility matrix (May 2026). Always cross-check against the support matrix before pinning versions — Midnight packages update frequently and version mismatches cause ETARGET errors at install time.
1) Project Structure
Generate this exact layout:
my-dapp/
├── package.json # Root workspace config
├── compose.yml # Docker: full local stack (root, localhost-only)
│
├── contract/
│ ├── package.json # @midnight-ntwrk/counter-contract
│ ├── tsconfig.json
│ ├── tsconfig.build.json
│ ├── vitest.config.ts
│ ├── eslint.config.mjs
│ ├── .prettierrc
│ ├── .vscode/
│ │ ├── launch.json
│ │ └── tasks.json
│ └── src/
│ ├── counter.compact # Compact smart contract
│ ├── index.ts # Re-exports managed contract + witnesses
│ ├── witnesses.ts # CounterPrivateState type + witnesses object
│ ├── managed/ # Compiled contract output (gitignored)
│ └── test/
│ ├── counter-simulator.ts
│ └── counter.test.ts
│
└── counter-cli/
├── package.json # @midnight-ntwrk/counter-cli
├── tsconfig.json
├── tsconfig.build.json
├── vitest.config.ts
├── vitest.setup.ts
├── eslint.config.mjs
├── .prettierrc.json
├── proof-server.yml # Docker: proof server only (preprod/preview)
├── standalone.yml # Docker: full local stack (node + indexer + proof server)
├── standalone.env.example
└── src/
├── index.ts # Re-exports api + cli
├── cli.ts # Interactive CLI (wallet, deploy, join, increment)
├── api.ts # Core API (wallet creation, deploy, increment, providers)
├── config.ts # Network configuration classes
├── common-types.ts # Type definitions
├── logger-utils.ts # Pino logger setup
├── preprod.ts # Preprod entry point
├── preview.ts # Preview entry point
├── standalone.ts # Local entry point (with Docker lifecycle)
├── preprod-start-proof-server.ts
├── preview-start-proof-server.ts
└── test/
├── commons.ts # Test environment helpers
└── counter.api.test.ts # API integration tests
2) Prerequisites
node --version
yarn --version
docker --version
curl --proto '=https' --tlsv1.2 -sSf \
https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh
source $HOME/.local/bin/env
compact update 0.30.0
compact compile --version
3) Root package.json
{
"name": "example-counter",
"version": "2.1.1",
"private": true,
"type": "module",
"engines": {
"node": ">=22.0.0"
},
"workspaces": [
"counter-cli",
"contract"
],
"scripts": {
"test": "NODE_OPTIONS='--experimental-vm-modules' vitest run",
"test:local": "MIDNIGHT_NETWORK=local yarn test",
"env:up": "docker compose up -d --wait",
"env:down": "docker compose down",
"validate": "yarn env:up && yarn test:local; yarn env:down"
},
"dependencies": {
"@midnight-ntwrk/compact-runtime": "0.16.0",
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-contracts": "4.0.4",
"@midnight-ntwrk/midnight-js-http-client-proof-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-indexer-public-data-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-level-private-state-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-network-id": "4.0.4",
"@midnight-ntwrk/midnight-js-node-zk-config-provider": "4.0.4",
"@midnight-ntwrk/midnight-js-types": "4.0.4",
"@midnight-ntwrk/midnight-js-utils": "4.0.4",
"@midnight-ntwrk/testkit-js": "4.0.4",
"@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",
"axios": "^1.15.0",
"pino": "^9.0.0",
"pino-pretty": "^13.0.0",
"rxjs": "^7.8.2",
"testcontainers": "^11.13.0",
"ws": "^8.14.2"
},
"devDependencies": {
"@midnight-ntwrk/midnight-js-compact": "4.0.4",
"@types/node": "^22.0.0",
"@types/ws": "^8.5.9",
"ts-node": "^10.9.2",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
},
"resolutions": {
"@midnight-ntwrk/ledger-v8": "8.0.3",
"@midnight-ntwrk/midnight-js-network-id": "4.0.4",
"@midnight-ntwrk/compact-runtime": "0.16.0"
},
"packageManager": "yarn@1.22.22"
}
4) Workspace Package: contract/package.json
{
"name": "@midnight-ntwrk/counter-contract",
"version": "0.1.0",
"license": "Apache-2.0",
"private": true,
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"scripts": {
"compact": "compact compile src/counter.compact src/managed/counter",
"test": "vitest run",
"test:compile": "npm run compact && vitest run",
"build": "rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/managed ./dist/managed && cp ./src/counter.compact ./dist",
"lint": "eslint src",
"typecheck": "tsc -p tsconfig.json --noEmit"
}
}
5) Workspace Package: counter-cli/package.json
{
"name": "@midnight-ntwrk/counter-cli",
"version": "0.1.0",
"license": "Apache-2.0",
"private": true,
"type": "module",
"scripts": {
"standalone": "docker compose -f standalone.yml pull && tsx src/standalone.ts",
"test-api": "docker compose -f standalone.yml pull && DEBUG='testcontainers' vitest run",
"build": "rm -rf dist && tsc --project tsconfig.build.json",
"lint": "eslint src",
"typecheck": "tsc -p tsconfig.json --noEmit",
"preview": "tsx src/preview.ts",
"preview-ps": "docker compose -f proof-server.yml pull && tsx src/preview-start-proof-server.ts",
"preprod": "tsx src/preprod.ts",
"preprod-ps": "docker compose -f proof-server.yml pull && tsx src/preprod-start-proof-server.ts"
},
"dependencies": {
"@midnight-ntwrk/counter-contract": "*"
}
}
6) contract/src/counter.compact
pragma language_version >= 0.20;
import CompactStandardLibrary;
export ledger round: Counter;
export circuit increment(): [] {
round.increment(1);
}
Compile it:
cd contract && npm run compact
Expected output:
Compiling 1 circuits:
circuit "increment" (k=10, rows=29)
7) contract/src/witnesses.ts
export type CounterPrivateState = {
privateCounter: number;
};
export const witnesses = {};
8) contract/src/index.ts
export * as Counter from "./managed/counter/contract/index.js";
export * from "./witnesses";
9) counter-cli/src/config.ts
Network configuration classes and shared contract config:
import path from 'node:path';
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
export const currentDir = path.resolve(new URL(import.meta.url).pathname, '..');
export const contractConfig = {
privateStateStoreName: 'counter-private-state',
zkConfigPath: path.resolve(currentDir, '..', '..', 'contract', 'src', 'managed', 'counter'),
};
export interface Config {
readonly logDir: string;
readonly indexer: string;
readonly indexerWS: string;
readonly node: string;
readonly proofServer: string;
}
export class StandaloneConfig implements Config {
logDir = path.resolve(currentDir, '..', 'logs', 'standalone', `${new Date().toISOString()}.log`);
indexer = 'http://127.0.0.1:8088/api/v3/graphql';
indexerWS = 'ws://127.0.0.1:8088/api/v3/graphql/ws';
node = 'http://127.0.0.1:9944';
proofServer = 'http://127.0.0.1:6300';
constructor() { setNetworkId('undeployed'); }
}
export class PreviewConfig implements Config {
logDir = path.resolve(currentDir, '..', 'logs', 'preview', `${new Date().toISOString()}.log`);
indexer = 'https://indexer.preview.midnight.network/api/v3/graphql';
indexerWS = 'wss://indexer.preview.midnight.network/api/v3/graphql/ws';
node = 'https://rpc.preview.midnight.network';
proofServer = 'http://127.0.0.1:6300';
constructor() { setNetworkId('preview'); }
}
export class PreprodConfig implements Config {
logDir = path.resolve(currentDir, '..', 'logs', 'preprod', `${new Date().toISOString()}.log`);
indexer = 'https://indexer.preprod.midnight.network/api/v3/graphql';
indexerWS = 'wss://indexer.preprod.midnight.network/api/v3/graphql/ws';
node = 'https://rpc.preprod.midnight.network';
proofServer = 'http://127.0.0.1:6300';
constructor() { setNetworkId('preprod'); }
}
Note: All networks use indexer API v3 (/api/v3/graphql). Do not change to v4.
10) counter-cli/src/common-types.ts
import { Counter, type CounterPrivateState } from '@midnight-ntwrk/counter-contract';
import type { MidnightProviders } from '@midnight-ntwrk/midnight-js-types';
import type { DeployedContract, FoundContract } from '@midnight-ntwrk/midnight-js-contracts';
import type { ProvableCircuitId } from '@midnight-ntwrk/compact-js';
export type CounterCircuits = ProvableCircuitId<Counter.Contract<CounterPrivateState>>;
export const CounterPrivateStateId = 'counterPrivateState';
export type CounterProviders = MidnightProviders<CounterCircuits, typeof CounterPrivateStateId, CounterPrivateState>;
export type CounterContract = Counter.Contract<CounterPrivateState>;
export type DeployedCounterContract = DeployedContract<CounterContract> | FoundContract<CounterContract>;
11) counter-cli/src/logger-utils.ts
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import pinoPretty from 'pino-pretty';
import pino from 'pino';
import { createWriteStream } from 'node:fs';
export const createLogger = async (logPath: string): Promise<pino.Logger> => {
await fs.mkdir(path.dirname(logPath), { recursive: true });
const pretty: pinoPretty.PrettyStream = pinoPretty({ colorize: true, sync: true });
const level =
process.env.DEBUG_LEVEL !== undefined && process.env.DEBUG_LEVEL !== null && process.env.DEBUG_LEVEL !== ''
? process.env.DEBUG_LEVEL
: 'info';
return pino(
{ level, depthLimit: 20 },
pino.multistream([
{ stream: pretty, level },
{ stream: createWriteStream(logPath), level },
]),
);
};
12) counter-cli/src/api.ts
Core API — wallet creation, key derivation, contract deploy/join, increment, transaction signing workaround, DUST management.
import { type ContractAddress } from '@midnight-ntwrk/compact-runtime';
import { Counter, type CounterPrivateState, witnesses } from '@midnight-ntwrk/counter-contract';
import * as ledger from '@midnight-ntwrk/ledger-v8';
import { unshieldedToken } from '@midnight-ntwrk/ledger-v8';
import { deployContract, findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { type FinalizedTxData, type MidnightProvider, type WalletProvider } from '@midnight-ntwrk/midnight-js-types';
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
import { HDWallet, Roles, generateRandomSeed } from '@midnight-ntwrk/wallet-sdk-hd';
import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
import {
createKeystore,
InMemoryTransactionHistoryStorage,
PublicKey,
UnshieldedWallet,
type UnshieldedKeystore,
} from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
import { type Logger } from 'pino';
import * as Rx from 'rxjs';
import { WebSocket } from 'ws';
import {
type CounterCircuits,
type CounterContract,
CounterPrivateStateId,
type CounterProviders,
type DeployedCounterContract,
} from './common-types';
import { type Config, contractConfig } from './config';
import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
import { assertIsContractAddress, toHex } from '@midnight-ntwrk/midnight-js-utils';
import { getNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
import { CompiledContract } from '@midnight-ntwrk/compact-js';
import { Buffer } from 'buffer';
import {
MidnightBech32m,
ShieldedAddress,
ShieldedCoinPublicKey,
ShieldedEncryptionPublicKey,
} from '@midnight-ntwrk/wallet-sdk-address-format';
let logger: Logger;
globalThis.WebSocket = WebSocket;
const counterCompiledContract = CompiledContract.make('counter', Counter.Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets(contractConfig.zkConfigPath),
);
export interface WalletContext {
wallet: WalletFacade;
shieldedSecretKeys: ledger.ZswapSecretKeys;
dustSecretKey: ledger.DustSecretKey;
unshieldedKeystore: UnshieldedKeystore;
}
export const getCounterLedgerState = async (
providers: CounterProviders,
contractAddress: ContractAddress,
): Promise<bigint | null> => {
assertIsContractAddress(contractAddress);
logger.info('Checking contract ledger state...');
const state = await providers.publicDataProvider
.queryContractState(contractAddress)
.then((contractState) => (contractState != null ? Counter.ledger(contractState.data).round : null));
logger.info(`Ledger state: ${state}`);
return state;
};
export const counterContractInstance: CounterContract = new Counter.Contract(witnesses);
export const joinContract = async (
providers: CounterProviders,
contractAddress: string,
): Promise<DeployedCounterContract> => {
const counterContract = await findDeployedContract(providers, {
contractAddress,
compiledContract: counterCompiledContract,
privateStateId: 'counterPrivateState',
initialPrivateState: { privateCounter: 0 },
});
logger.info(`Joined contract at address: ${counterContract.deployTxData.public.contractAddress}`);
return counterContract;
};
export const deploy = async (
providers: CounterProviders,
privateState: CounterPrivateState,
): Promise<DeployedCounterContract> => {
logger.info('Deploying counter contract...');
const counterContract = await deployContract(providers, {
compiledContract: counterCompiledContract,
privateStateId: 'counterPrivateState',
initialPrivateState: privateState,
});
logger.info(`Deployed contract at address: ${counterContract.deployTxData.public.contractAddress}`);
return counterContract;
};
export const increment = async (counterContract: DeployedCounterContract): Promise<FinalizedTxData> => {
logger.info('Incrementing...');
const finalizedTxData = await counterContract.callTx.increment();
logger.info(`Transaction ${finalizedTxData.public.txId} added in block ${finalizedTxData.public.blockHeight}`);
return finalizedTxData.public;
};
export const displayCounterValue = async (
providers: CounterProviders,
counterContract: DeployedCounterContract,
): Promise<{ counterValue: bigint | null; contractAddress: string }> => {
const contractAddress = counterContract.deployTxData.public.contractAddress;
const counterValue = await getCounterLedgerState(providers, contractAddress);
if (counterValue === null) {
logger.info(`There is no counter contract deployed at ${contractAddress}.`);
} else {
logger.info(`Current counter value: ${Number(counterValue)}`);
}
return { contractAddress, counterValue };
};
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 sigData = cloned.signatureData(segment);
const signature = signFn(sigData);
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);
}
};
export const createWalletAndMidnightProvider = async (
ctx: WalletContext,
): Promise<WalletProvider & MidnightProvider> => {
const state = await Rx.firstValueFrom(ctx.wallet.state().pipe(Rx.filter((s) => s.isSynced)));
return {
getCoinPublicKey() {
return state.shielded.coinPublicKey.toHexString();
},
getEncryptionPublicKey() {
return state.shielded.encryptionPublicKey.toHexString();
},
async balanceTx(tx, ttl?) {
const recipe = await ctx.wallet.balanceUnboundTransaction(
tx,
{ shieldedSecretKeys: ctx.shieldedSecretKeys, dustSecretKey: ctx.dustSecretKey },
{ ttl: ttl ?? new Date(Date.now() + 30 * 60 * 1000) },
);
const signFn = (payload: Uint8Array) => ctx.unshieldedKeystore.signData(payload);
signTransactionIntents(recipe.baseTransaction, signFn, 'proof');
if (recipe.balancingTransaction) {
signTransactionIntents(recipe.balancingTransaction, signFn, 'pre-proof');
}
return ctx.wallet.finalizeRecipe(recipe);
},
submitTx(tx) {
return ctx.wallet.submitTransaction(tx) as any;
},
};
};
export const waitForSync = (wallet: WalletFacade) =>
Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((state) => state.isSynced),
),
);
export const waitForFunds = (wallet: WalletFacade): Promise<bigint> =>
Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(10_000),
Rx.filter((state) => state.isSynced),
Rx.map((s) => s.unshielded.balances[unshieldedToken().raw] ?? 0n),
Rx.filter((balance) => balance > 0n),
),
);
const buildShieldedConfig = ({ indexer, indexerWS, node, proofServer }: Config) => ({
networkId: getNetworkId(),
indexerClientConnection: {
indexerHttpUrl: indexer,
indexerWsUrl: indexerWS,
},
provingServerUrl: new URL(proofServer),
relayURL: new URL(node.replace(/^http/, 'ws')),
});
const buildUnshieldedConfig = ({ indexer, indexerWS }: Config) => ({
networkId: getNetworkId(),
indexerClientConnection: {
indexerHttpUrl: indexer,
indexerWsUrl: indexerWS,
},
txHistoryStorage: new InMemoryTransactionHistoryStorage(),
});
const buildDustConfig = ({ indexer, indexerWS, node, proofServer }: Config) => ({
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')),
});
const deriveKeysFromSeed = (seed: string) => {
const hdWallet = HDWallet.fromSeed(Buffer.from(seed, 'hex'));
if (hdWallet.type !== 'seedOk') {
throw new Error('Failed to initialize HDWallet from seed');
}
const derivationResult = hdWallet.hdWallet
.selectAccount(0)
.selectRoles([Roles.Zswap, Roles.NightExternal, Roles.Dust])
.deriveKeysAt(0);
if (derivationResult.type !== 'keysDerived') {
throw new Error('Failed to derive keys');
}
hdWallet.hdWallet.clear();
return derivationResult.keys;
};
const formatBalance = (balance: bigint): string => balance.toLocaleString();
export const withStatus = async <T>(message: string, fn: () => Promise<T>): Promise<T> => {
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let i = 0;
const interval = setInterval(() => {
process.stdout.write(`\r ${frames[i++ % frames.length]} ${message}`);
}, 80);
try {
const result = await fn();
clearInterval(interval);
process.stdout.write(`\r ✓ ${message}\n`);
return result;
} catch (e) {
clearInterval(interval);
process.stdout.write(`\r ✗ ${message}\n`);
throw e;
}
};
const registerForDustGeneration = async (
wallet: WalletFacade,
unshieldedKeystore: UnshieldedKeystore,
): Promise<void> => {
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
if (state.dust.availableCoins.length > 0) {
const dustBal = state.dust.balance(new Date());
console.log(` ✓ Dust tokens already available (${formatBalance(dustBal)} DUST)`);
return;
}
const nightUtxos = state.unshielded.availableCoins.filter(
(coin: any) => coin.meta?.registeredForDustGeneration !== true,
);
if (nightUtxos.length === 0) {
await withStatus('Waiting for dust tokens to generate', () =>
Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((s) => s.isSynced),
Rx.filter((s) => s.dust.balance(new Date()) > 0n),
),
),
);
return;
}
await withStatus(`Registering ${nightUtxos.length} NIGHT UTXO(s) for dust generation`, async () => {
const recipe = await wallet.registerNightUtxosForDustGeneration(
nightUtxos,
unshieldedKeystore.getPublicKey(),
(payload) => unshieldedKeystore.signData(payload),
);
const finalized = await wallet.finalizeRecipe(recipe);
await wallet.submitTransaction(finalized);
});
await withStatus('Waiting for dust tokens to generate', () =>
Rx.firstValueFrom(
wallet.state().pipe(
Rx.throttleTime(5_000),
Rx.filter((s) => s.isSynced),
Rx.filter((s) => s.dust.balance(new Date()) > 0n),
),
),
);
};
const printWalletSummary = (state: any, unshieldedKeystore: UnshieldedKeystore) => {
const networkId = getNetworkId();
const unshieldedBalance = state.unshielded.balances[unshieldedToken().raw] ?? 0n;
const coinPubKey = ShieldedCoinPublicKey.fromHexString(state.shielded.coinPublicKey.toHexString());
const encPubKey = ShieldedEncryptionPublicKey.fromHexString(state.shielded.encryptionPublicKey.toHexString());
const shieldedAddress = MidnightBech32m.encode(networkId, new ShieldedAddress(coinPubKey, encPubKey)).toString();
const DIV = '──────────────────────────────────────────────────────────────';
console.log(`
${DIV}
Wallet Overview Network: ${networkId}
${DIV}
Shielded (ZSwap)
└─ Address: ${shieldedAddress}
Unshielded
├─ Address: ${unshieldedKeystore.getBech32Address()}
└─ Balance: ${formatBalance(unshieldedBalance)} tNight
Dust
└─ Address: ${MidnightBech32m.encode(networkId, state.dust.address).toString()}
${DIV}`);
};
export const buildWalletAndWaitForFunds = async (config: Config, seed: string): Promise<WalletContext> => {
console.log('');
const { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore } = await withStatus(
'Building wallet',
async () => {
const keys = deriveKeysFromSeed(seed);
const shieldedSecretKeys = ledger.ZswapSecretKeys.fromSeed(keys[Roles.Zswap]);
const dustSecretKey = ledger.DustSecretKey.fromSeed(keys[Roles.Dust]);
const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], getNetworkId());
const walletConfig = {
...buildShieldedConfig(config),
...buildUnshieldedConfig(config),
...buildDustConfig(config),
};
const wallet = await WalletFacade.init({
configuration: walletConfig,
shielded: (cfg) => ShieldedWallet(cfg).startWithSecretKeys(shieldedSecretKeys),
unshielded: (cfg) => UnshieldedWallet(cfg).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)),
dust: (cfg) =>
DustWallet(cfg).startWithSecretKey(dustSecretKey, ledger.LedgerParameters.initialParameters().dust),
});
await wallet.start(shieldedSecretKeys, dustSecretKey);
return { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore };
},
);
const networkId = getNetworkId();
const DIV = '──────────────────────────────────────────────────────────────';
console.log(`
${DIV}
Wallet Overview Network: ${networkId}
${DIV}
Unshielded Address (send tNight here):
${unshieldedKeystore.getBech32Address()}
Fund your wallet with tNight from the Preprod faucet:
https://faucet.preprod.midnight.network/
${DIV}
`);
const syncedState = await withStatus('Syncing with network', () => waitForSync(wallet));
printWalletSummary(syncedState, unshieldedKeystore);
const balance = syncedState.unshielded.balances[unshieldedToken().raw] ?? 0n;
if (balance === 0n) {
const fundedBalance = await withStatus('Waiting for incoming tokens', () => waitForFunds(wallet));
console.log(` Balance: ${formatBalance(fundedBalance)} tNight\n`);
}
await registerForDustGeneration(wallet, unshieldedKeystore);
return { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore };
};
export const buildFreshWallet = async (config: Config): Promise<WalletContext> => {
const seed = toHex(Buffer.from(generateRandomSeed()));
const DIV = '──────────────────────────────────────────────────────────────';
console.log(`
${DIV}
New Wallet Seed — save this before continuing
${DIV}
${seed}
${DIV}
`);
return await buildWalletAndWaitForFunds(config, seed);
};
export const configureProviders = async (ctx: WalletContext, config: Config) => {
const walletAndMidnightProvider = await createWalletAndMidnightProvider(ctx);
const zkConfigProvider = new NodeZkConfigProvider<CounterCircuits>(contractConfig.zkConfigPath);
const accountId = walletAndMidnightProvider.getCoinPublicKey();
const storagePassword = `${Buffer.from(accountId, 'hex').toString('base64')}!`;
return {
privateStateProvider: levelPrivateStateProvider<typeof CounterPrivateStateId>({
privateStateStoreName: contractConfig.privateStateStoreName,
accountId,
privateStoragePasswordProvider: () => storagePassword,
}),
publicDataProvider: indexerPublicDataProvider(config.indexer, config.indexerWS),
zkConfigProvider,
proofProvider: httpClientProofProvider(config.proofServer, zkConfigProvider),
walletProvider: walletAndMidnightProvider,
midnightProvider: walletAndMidnightProvider,
};
};
export const getDustBalance = async (
wallet: WalletFacade,
): Promise<{ available: bigint; pending: bigint; availableCoins: number; pendingCoins: number }> => {
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
const available = state.dust.balance(new Date());
const availableCoins = state.dust.availableCoins.length;
const pendingCoins = state.dust.pendingCoins.length;
const pending = state.dust.pendingCoins.reduce((sum, c) => sum + c.initialValue, 0n);
return { available, pending, availableCoins, pendingCoins };
};
export const monitorDustBalance = async (wallet: WalletFacade, stopSignal: Promise<void>): Promise<void> => {
let stopped = false;
void stopSignal.then(() => { stopped = true; });
const sub = wallet
.state()
.pipe(
Rx.throttleTime(5_000),
Rx.filter((s) => s.isSynced),
)
.subscribe((state) => {
if (stopped) return;
const now = new Date();
const available = state.dust.balance(now);
const availableCoins = state.dust.availableCoins.length;
const pendingCoins = state.dust.pendingCoins.length;
const registeredNight = state.unshielded.availableCoins.filter(
(coin: any) => coin.meta?.registeredForDustGeneration === true,
).length;
const totalNight = state.unshielded.availableCoins.length;
let status = '';
if (pendingCoins > 0 && availableCoins === 0) {
status = '⚠ locked by pending tx';
} else if (available > 0n) {
status = '✓ ready to deploy';
} else if (availableCoins > 0) {
status = 'accruing...';
} else if (registeredNight > 0) {
status = 'waiting for generation...';
} else {
status = 'no NIGHT registered';
}
const time = now.toLocaleTimeString();
console.log(
` [${time}] DUST: ${formatBalance(available)} (${availableCoins} coins, ${pendingCoins} pending) | NIGHT: ${totalNight} UTXOs, ${registeredNight} registered | ${status}`,
);
});
await stopSignal;
sub.unsubscribe();
};
export function setLogger(_logger: Logger) {
logger = _logger;
}
Key points:
globalThis.WebSocket = WebSocket is required for GraphQL subscriptions (wallet sync) in Node.js
WalletFacade.init() static factory replaces direct new WalletFacade() usage
signTransactionIntents works around a wallet SDK bug where signRecipe hardcodes 'pre-proof' marker
- The
configureProviders function uses accountId and privateStoragePasswordProvider as required by levelPrivateStateProvider
13) counter-cli/src/cli.ts
Interactive CLI with wallet setup, deploy/join, and counter interaction menus. Supports Docker lifecycle via testcontainers.
import { type WalletContext } from './api';
import { stdin as input, stdout as output } from 'node:process';
import { createInterface, type Interface } from 'node:readline/promises';
import { type Logger } from 'pino';
import { type StartedDockerComposeEnvironment, type DockerComposeEnvironment } from 'testcontainers';
import { type CounterProviders, type DeployedCounterContract } from './common-types';
import { type Config, StandaloneConfig } from './config';
import * as api from './api';
let logger: Logger;
const GENESIS_MINT_WALLET_SEED = '0000000000000000000000000000000000000000000000000000000000000001';
const BANNER = `
╔══════════════════════════════════════════════════════════════╗
║ ║
║ Midnight Counter Example ║
║ ───────────────────── ║
║ A privacy-preserving smart contract demo ║
║ ║
╚══════════════════════════════════════════════════════════════╝
`;
const DIVIDER = '──────────────────────────────────────────────────────────────';
const WALLET_MENU = `
${DIVIDER}
Wallet Setup
${DIVIDER}
[1] Create a new wallet
[2] Restore wallet from seed
[3] Exit
${'─'.repeat(62)}
> `;
const contractMenu = (dustBalance: string) => `
${DIVIDER}
Contract Actions${dustBalance ? ` DUST: ${dustBalance}` : ''}
${DIVIDER}
[1] Deploy a new counter contract
[2] Join an existing counter contract
[3] Monitor DUST balance
[4] Exit
${'─'.repeat(62)}
> `;
const counterMenu = (dustBalance: string) => `
${DIVIDER}
Counter Actions${dustBalance ? ` DUST: ${dustBalance}` : ''}
${DIVIDER}
[1] Increment counter
[2] Display current counter value
[3] Exit
${'─'.repeat(62)}
> `;
const buildWalletFromSeed = async (config: Config, rli: Interface): Promise<WalletContext> => {
const seed = await rli.question('Enter your wallet seed: ');
return await api.buildWalletAndWaitForFunds(config, seed);
};
const buildWallet = async (config: Config, rli: Interface): Promise<WalletContext | null> => {
if (config instanceof StandaloneConfig) {
return await api.buildWalletAndWaitForFunds(config, GENESIS_MINT_WALLET_SEED);
}
while (true) {
const choice = await rli.question(WALLET_MENU);
switch (choice.trim()) {
case '1':
return await api.buildFreshWallet(config);
case '2':
return await buildWalletFromSeed(config, rli);
case '3':
return null;
default:
logger.error(`Invalid choice: ${choice}`);
}
}
};
const getDustLabel = async (wallet: api.WalletContext['wallet']): Promise<string> => {
try {
const dust = await api.getDustBalance(wallet);
return dust.available.toLocaleString();
} catch {
return '';
}
};
const joinContract = async (providers: CounterProviders, rli: Interface): Promise<DeployedCounterContract> => {
const contractAddress = await rli.question('Enter the contract address (hex): ');
return await api.joinContract(providers, contractAddress);
};
const startDustMonitor = async (wallet: api.WalletContext['wallet'], rli: Interface): Promise<void> => {
console.log('');
const stopPromise = rli.question(' Press Enter to return to menu...\n').then(() => {});
await api.monitorDustBalance(wallet, stopPromise);
console.log('');
};
const deployOrJoin = async (
providers: CounterProviders,
walletCtx: api.WalletContext,
rli: Interface,
): Promise<DeployedCounterContract | null> => {
while (true) {
const dustLabel = await getDustLabel(walletCtx.wallet);
const choice = await rli.question(contractMenu(dustLabel));
switch (choice.trim()) {
case '1':
try {
const contract = await api.withStatus('Deploying counter contract', () =>
api.deploy(providers, { privateCounter: 0 }),
);
console.log(` Contract deployed at: ${contract.deployTxData.public.contractAddress}\n`);
return contract;
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
console.log(`\n ✗ Deploy failed: ${msg}`);
if (e instanceof Error && e.cause) {
let cause: unknown = e.cause;
let depth = 0;
while (cause && depth < 5) {
const causeMsg =
cause instanceof Error
? `${cause.message}\n ${cause.stack?.split('\n').slice(1, 3).join('\n ') ?? ''}`
: String(cause);
console.log(` cause: ${causeMsg}`);
cause = cause instanceof Error ? cause.cause : undefined;
depth++;
}
}
if (msg.toLowerCase().includes('dust') || msg.toLowerCase().includes('no dust')) {
console.log(' Insufficient DUST for transaction fees. Use option [3] to monitor your balance.');
}
console.log('');
}
break;
case '2':
try {
return await joinContract(providers, rli);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
console.log(` ✗ Failed to join contract: ${msg}\n`);
}
break;
case '3':
await startDustMonitor(walletCtx.wallet, rli);
break;
case '4':
return null;
default:
console.log(` Invalid choice: ${choice}`);
}
}
};
const mainLoop = async (providers: CounterProviders, walletCtx: api.WalletContext, rli: Interface): Promise<void> => {
const counterContract = await deployOrJoin(providers, walletCtx, rli);
if (counterContract === null) {
return;
}
while (true) {
const dustLabel = await getDustLabel(walletCtx.wallet);
const choice = await rli.question(counterMenu(dustLabel));
switch (choice.trim()) {
case '1':
try {
await api.withStatus('Incrementing counter', () => api.increment(counterContract));
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
console.log(` ✗ Increment failed: ${msg}\n`);
}
break;
case '2':
await api.displayCounterValue(providers, counterContract);
break;
case '3':
return;
default:
console.log(` Invalid choice: ${choice}`);
}
}
};
const mapContainerPort = (env: StartedDockerComposeEnvironment, url: string, containerName: string) => {
const mappedUrl = new URL(url);
const container = env.getContainer(containerName);
mappedUrl.port = String(container.getFirstMappedPort());
return mappedUrl.toString().replace(/\/+$/, '');
};
export const run = async (config: Config, _logger: Logger, dockerEnv?: DockerComposeEnvironment): Promise<void> => {
logger = _logger;
api.setLogger(_logger);
console.log(BANNER);
const rli = createInterface({ input, output, terminal: true });
let env: StartedDockerComposeEnvironment | undefined;
try {
if (dockerEnv !== undefined) {
env = await dockerEnv.up();
if (config instanceof StandaloneConfig) {
config.indexer = mapContainerPort(env, config.indexer, 'counter-indexer');
config.indexerWS = mapContainerPort(env, config.indexerWS, 'counter-indexer');
config.node = mapContainerPort(env, config.node, 'counter-node');
config.proofServer = mapContainerPort(env, config.proofServer, 'counter-proof-server');
}
}
const walletCtx = await buildWallet(config, rli);
if (walletCtx === null) {
return;
}
try {
const providers = await api.withStatus('Configuring providers', () => api.configureProviders(walletCtx, config));
console.log('');
await mainLoop(providers, walletCtx, rli);
} catch (e) {
if (e instanceof Error) {
logger.error(`Error: ${e.message}`);
logger.debug(`${e.stack}`);
} else {
throw e;
}
} finally {
try {
await walletCtx.wallet.stop();
} catch (e) {
logger.error(`Error stopping wallet: ${e}`);
}
}
} finally {
rli.close();
rli.removeAllListeners();
if (env !== undefined) {
try {
await env.down();
} catch (e) {
logger.error(`Error shutting down docker environment: ${e}`);
}
}
logger.info('Goodbye.');
}
};
14) Entry Points
counter-cli/src/preprod.ts
import { createLogger } from './logger-utils.js';
import { run } from './cli.js';
import { PreprodConfig } from './config.js';
const config = new PreprodConfig();
const logger = await createLogger(config.logDir);
await run(config, logger);
counter-cli/src/preview.ts
import { createLogger } from './logger-utils.js';
import { run } from './cli.js';
import { PreviewConfig } from './config.js';
const config = new PreviewConfig();
const logger = await createLogger(config.logDir);
await run(config, logger);
counter-cli/src/standalone.ts
import { createLogger } from './logger-utils.js';
import path from 'node:path';
import { run } from './cli.js';
import { DockerComposeEnvironment, Wait } from 'testcontainers';
import { currentDir, StandaloneConfig } from './config.js';
const config = new StandaloneConfig();
const dockerEnv = new DockerComposeEnvironment(path.resolve(currentDir, '..'), 'standalone.yml')
.withWaitStrategy('counter-proof-server', Wait.forLogMessage('Actix runtime found; starting in Actix runtime', 1))
.withWaitStrategy('counter-indexer', Wait.forLogMessage(/starting indexing/, 1));
const logger = await createLogger(config.logDir);
await run(config, logger, dockerEnv);
counter-cli/src/preprod-start-proof-server.ts
import { createLogger } from './logger-utils.js';
import { run } from './cli.js';
import { currentDir, PreprodConfig } from './config.js';
import { DockerComposeEnvironment, Wait } from 'testcontainers';
import path from 'node:path';
const config = new PreprodConfig();
const dockerEnv = new DockerComposeEnvironment(path.resolve(currentDir, '..'), 'proof-server.yml').withWaitStrategy(
'proof-server',
Wait.forLogMessage('Actix runtime found; starting in Actix runtime', 1),
);
const logger = await createLogger(config.logDir);
await run(config, logger, dockerEnv);
counter-cli/src/preview-start-proof-server.ts
import { createLogger } from './logger-utils.js';
import { run } from './cli.js';
import { currentDir, PreviewConfig } from './config.js';
import { DockerComposeEnvironment, Wait } from 'testcontainers';
import path from 'node:path';
const config = new PreviewConfig();
const dockerEnv = new DockerComposeEnvironment(path.resolve(currentDir, '..'), 'proof-server.yml').withWaitStrategy(
'proof-server',
Wait.forLogMessage('Actix runtime found; starting in Actix runtime', 1),
);
const logger = await createLogger(config.logDir);
await run(config, logger, dockerEnv);
counter-cli/src/index.ts
export * from './api';
export * from './cli';
15) TypeScript Config
contract/tsconfig.json
{
"include": ["src/**/*.ts"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"lib": ["ES2022"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"strict": true,
"isolatedModules": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
}
}
contract/tsconfig.build.json
{
"extends": "./tsconfig.json",
"exclude": ["src/test/**/*.ts"],
"compilerOptions": {
"noEmit": false
}
}
counter-cli/tsconfig.json
{
"include": ["src/**/*.ts"],
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"lib": ["ES2022"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"strict": true,
"isolatedModules": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@contract/*": ["../../contract/src/*"]
}
}
}
counter-cli/tsconfig.build.json
{
"extends": "./tsconfig.json",
"exclude": ["src/**/*.test.ts"],
"compilerOptions": {
"noEmit": false
}
}
16) Docker Config Files
Root compose.yml (full local stack, localhost-only bindings)
services:
proof-server:
image: 'midnightntwrk/proof-server:8.0.3'
command: ['midnight-proof-server', '-v']
ports:
- '127.0.0.1:6300:6300'
environment:
RUST_BACKTRACE: 'full'
healthcheck:
test: ['CMD-SHELL', 'echo > /dev/tcp/127.0.0.1/6300']
interval: 10s
timeout: 5s
retries: 20
start_period: 10s
indexer:
image: 'midnightntwrk/indexer-standalone:4.0.0'
ports:
- '127.0.0.1:8088:8088'
environment:
RUST_LOG: 'indexer=info,chain_indexer=info,indexer_api=info,wallet_indexer=info,indexer_common=info,fastrace_opentelemetry=off,info'
APP__INFRA__NODE__URL: 'ws://node:9944'
APP__APPLICATION__NETWORK_ID: 'undeployed'
APP__INFRA__STORAGE__PASSWORD: 'indexer'
APP__INFRA__PUB_SUB__PASSWORD: 'indexer'
APP__INFRA__LEDGER_STATE_STORAGE__PASSWORD: 'indexer'
APP__INFRA__SECRET: '303132333435363738393031323334353637383930313233343536373839303132'
healthcheck:
test: ['CMD-SHELL', 'cat /var/run/indexer-standalone/running']
interval: 10s
timeout: 5s
retries: 20
start_period: 10s
depends_on:
node:
condition: service_healthy
node:
image: 'midnightntwrk/midnight-node:0.22.3'
ports:
- '127.0.0.1:9944:9944'
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:9944/health']
interval: 2s
timeout: 5s
retries: 20
start_period: 5s
environment:
CFG_PRESET: 'dev'
SIDECHAIN_BLOCK_BENEFICIARY: '04bcf7ad3be7a5c790460be82a713af570f22e0f801f6659ab8e84a52be6969e'
counter-cli/proof-server.yml (preprod/preview — proof server only)
services:
proof-server:
image: 'midnightntwrk/proof-server:8.0.3'
command: ['midnight-proof-server -v']
ports:
- '6300:6300'
environment:
RUST_BACKTRACE: 'full'
counter-cli/standalone.yml (undeployed — full local stack with named containers)
services:
proof-server:
container_name: 'counter-proof-server'
image: 'midnightntwrk/proof-server:8.0.3'
command: ['midnight-proof-server -v']
ports:
- '6300'
environment:
RUST_BACKTRACE: 'full'
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:6300/version']
interval: 10s
timeout: 5s
retries: 20
start_period: 10s
indexer:
container_name: 'counter-indexer'
image: 'midnightntwrk/indexer-standalone:4.0.0'
env_file: standalone.env.example
ports:
- '0:8088'
environment:
RUST_LOG: 'indexer=info,chain_indexer=info,indexer_api=info,wallet_indexer=info,indexer_common=info,fastrace_opentelemetry=off,info'
APP__APPLICATION__NETWORK_ID: 'undeployed'
healthcheck:
test: ['CMD-SHELL', 'cat /var/run/indexer-standalone/running']
interval: 10s
timeout: 5s
retries: 20
start_period: 10s
depends_on:
node:
condition: service_healthy
node:
image: 'midnightntwrk/midnight-node:0.22.3'
container_name: 'counter-node'
ports:
- '9944'
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:9944/health']
interval: 2s
timeout: 5s
retries: 20
start_period: 20s
environment:
CFG_PRESET: 'dev'
SIDECHAIN_BLOCK_BENEFICIARY: '04bcf7ad3be7a5c790460be82a713af570f22e0f801f6659ab8e84a52be6969e'
counter-cli/standalone.env.example
APP__INFRA__NODE__URL=ws://node:9944
APP__INFRA__STORAGE__PASSWORD=indexer
APP__INFRA__PUB_SUB__PASSWORD=indexer
APP__INFRA__LEDGER_STATE_STORAGE__PASSWORD=indexer
APP__INFRA__SECRET=303132333435363738393031323334353637383930313233343536373839303132
17) Test Infrastructure
contract/vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
mode: "node",
test: {
deps: { interopDefault: true },
globals: true,
environment: "node",
include: ["**/*.test.ts"],
exclude: ["node_modules"],
root: ".",
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
thresholds: {
branches: 50,
functions: 73,
lines: 72,
statements: -269,
},
},
reporters: ["default", ["junit", { outputFile: "reports/report.xml" }]],
},
resolve: {
extensions: [".ts", ".js"],
conditions: ["import", "node", "default"],
},
});
counter-cli/vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
mode: 'node',
test: {
setupFiles: ['./vitest.setup.ts'],
testTimeout: 1000 * 60 * 45,
deps: { interopDefault: true },
globals: true,
environment: 'node',
include: ['**/*.test.ts'],
exclude: ['node_modules'],
root: '.',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
thresholds: {
branches: 50,
functions: 73,
lines: 72,
statements: -269,
},
},
reporters: ["default", ["junit", { outputFile: "reports/report.xml" }]],
},
resolve: {
extensions: ['.ts', '.js'],
conditions: ['import', 'node', 'default'],
},
});
counter-cli/vitest.setup.ts
import protobuf from 'protobufjs';
import Long from 'long';
protobuf.util.Long = Long;
protobuf.configure();
18) First Run — Preprod
yarn
cd contract && npm run compact && cd ..
cd counter-cli && docker compose -f proof-server.yml pull && docker compose -f proof-server.yml up
cd counter-cli && tsx src/preprod.ts
cd counter-cli && yarn preprod-ps
Faucet: https://faucet.preprod.midnight.network/
The CLI will:
- Present a wallet menu — choose [1] to create a new wallet
- Save the seed phrase shown
- Wait for sync, then display the unshielded address
- Prompt you to fund via the faucet (paste the address)
- Wait for funds and register NIGHT UTXOs for DUST generation
- Enter the contract menu — choose [1] to deploy
- After deployment, enter the counter menu — choose [1] to increment, [2] to read
19) First Run — Local (standalone)
cd counter-cli && yarn standalone
Note: Standalone mode uses the genesis mint wallet seed (0000...0001) automatically. No faucet needed.
20) Running Tests
cd contract && npm run test:compile
cd counter-cli && yarn test-api
yarn validate
21) Troubleshooting
| Error | Cause | Fix |
|---|
compact: command not found | PATH not set | source $HOME/.local/bin/env |
ETARGET No matching version found | Wrong package version | Use versions from this skill's package.json |
ECONNREFUSED 127.0.0.1:6300 | Proof server not running | Start proof server: docker compose -f proof-server.yml up |
WalletFacade.init is not a function | Old WalletFacade usage | Use WalletFacade.init() static factory, not new WalletFacade() |
MidnightBech32m.encode is not a function | Missing address-format import | Import from @midnight-ntwrk/wallet-sdk-address-format |
Failed to clone intent | Wallet SDK signing bug | Fixed via signTransactionIntents in api.ts |
| DUST = 0 after failed deploy | DUST coins locked | Restart the process — wallet.stop() then rerun |
| 0 balance after faucet | Wallet not synced yet | Wait for sync; check unshielded address was used |
Cannot find module | Contract not compiled | cd contract && npm run compact |
prove: no SRS params for k=6 | Circuit too small for prover | Add dummy ledger fields to increase circuit size |
| Old address fails after recompile | Verifier key changed | Redeploy contract |
22) Adapting This Template
To replace the counter with your own contract:
- Replace
contract/src/counter.compact with your contract
- Run
cd contract && npm run compact
- Update
contract/src/witnesses.ts with your private state type
- Update
contract/src/index.ts to export your contract symbols
- In
counter-cli/src/common-types.ts — update type aliases for your circuits
- In
counter-cli/src/api.ts:
- Update imports to use your contract package
- Update
counterCompiledContract with your contract reference
- Update
counterContractInstance, deploy, joinContract as needed
- Replace
increment with your circuit calls
- Replace
Counter.ledger(contractState.data) with your ledger field
- Rename
privateStateStoreName in config.ts to avoid LevelDB collisions