example-counter
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.
How do I install this agent skill?
npx skills add https://github.com/kali-decoder/midnight-skills --skill example-counterIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides a legitimate boilerplate for the Midnight Network DApp ecosystem, following official guides and utilizing standard blockchain development tools. Potential security patterns identified, such as remote installation scripts and network healthchecks, were found to be benign and consistent with industry-standard development practices.
- Socketwarn
1 alert: gptAnomaly
- Snykfail
Risk: CRITICAL · 5 issues
What does this agent skill do?
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 repodocs.midnight.network/guides/deploy-mn-app— official deploy guidedocs.midnight.network/guides/interact-with-mn-app— official interact guidedocs.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.js 22.15+ required
node --version
# Yarn 1.x (classic) required
yarn --version
# Docker required (proof server + optional local stack)
docker --version
# Install Compact compiler
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:
# From the contract/ directory
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 };
};
/**
* Sign all unshielded offers in a transaction's intents, using the correct
* proof marker for Intent.deserialize. This works around a bug in the wallet
* SDK where signRecipe hardcodes 'pre-proof', which fails for proven
* (UnboundTransaction) intents that contain 'proof' data.
*/
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 = WebSocketis required for GraphQL subscriptions (wallet sync) in Node.jsWalletFacade.init()static factory replaces directnew WalletFacade()usagesignTransactionIntentsworks around a wallet SDK bug wheresignRecipehardcodes'pre-proof'marker- The
configureProvidersfunction usesaccountIdandprivateStoragePasswordProvideras required bylevelPrivateStateProvider
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
# 1. Install deps
yarn
# 2. Compile contract (from contract/ directory)
cd contract && npm run compact && cd ..
# → circuit "increment" (k=10, rows=29)
# 3. Start proof server (keep this terminal open)
cd counter-cli && docker compose -f proof-server.yml pull && docker compose -f proof-server.yml up
# 4. In another terminal, run the CLI
cd counter-cli && tsx src/preprod.ts
# or with auto-started proof server:
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)
# 1. Start full local stack with testcontainers (auto-managed)
cd counter-cli && yarn standalone
# 2. This will:
# - Pull Docker images
# - Start node + indexer + proof server
# - Use the genesis wallet (pre-funded)
# - Deploy contract and enter the interaction loop
Note: Standalone mode uses the genesis mint wallet seed (0000...0001) automatically. No faucet needed.
20) Running Tests
# Contract unit tests (from contract/ directory)
cd contract && npm run test:compile
# API integration tests (from counter-cli/ directory)
cd counter-cli && yarn test-api
# Full validation (from root)
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.compactwith your contract - Run
cd contract && npm run compact - Update
contract/src/witnesses.tswith your private state type - Update
contract/src/index.tsto 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
counterCompiledContractwith your contract reference - Update
counterContractInstance,deploy,joinContractas needed - Replace
incrementwith your circuit calls - Replace
Counter.ledger(contractState.data)with your ledger field
- Rename
privateStateStoreNameinconfig.tsto avoid LevelDB collisions
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/kali-decoder/midnight-skills/example-counter">View example-counter on skillZs</a>