example-counter
Skill18 starsChanged 16 days agoMITView on GitHub
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.
- Reads credentials
- Deletes or force-pushes
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- tusharpamnani/Midnight-Skillsedited, first seen May 2, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- tusharpamnani/Midnight-Skills★ 198% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository36 other files it gives its agents.
- AGENTS.mdAGENTS.md
- 1am-walletSkill
- android-example-votingSkill
- ankr-midnightSkill
- compactSkill
- dynamic-midnight-walletSkill
- example-hello-worldSkill
- example-leaderboard-dappSkill
- example-locker-dappSkill
- example-passport-dappSkill
- example-payment-dappSkill
- example-private-party-dappSkill
- example-private-reserve-auction-dappSkill
- example-zk-loan-applicationSkill
- gsap-coreSkill
- gsap-utilsSkill
- indexerSkill
- midnight-consensusSkill
- midnight-cryptographySkill
- midnight-environment-setupSkill
- midnight-jsSkill
- midnight-onchain-logicSkill
- midnight-p2p-networkingSkill
- midnight-passportSkill
- midnight-rpcSkill
- midnightskillSkill
- midnight-storageSkill
- midnight-transactionsSkill
- multinetworkSkill
- nftSkill
- react-wallet-connectorSkill
- midnight-securitySkill
- testingSkill
- token-transfersSkill
- why-midnightSkill
- zk-vs-fhe-vs-mpcSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 4 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 329
Note: All networks use indexer API v3 (/api/v3/graphql).
- Distinctive: line 1590
Wait for sync, then display the unshielded address
- Distinctive: line 1592
Wait for funds and register NIGHT UTXOs for DUST generation
- Distinctive: line 1594
After deployment, enter the counter menu — choose [1] to increment, [2] to read
- Very specific: line 1654
Update contract/src/witnesses.ts with your private state type
- Very specific: line 1655
Update contract/src/index.ts to export your contract symbols
- Distinctive: line 1658
Update imports to use your contract package
- Very specific: line 1659
Update counterCompiledContract with your contract reference
- Very specific: line 1660
Update counterContractInstance, deploy, joinContract as needed
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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 below are a dated snapshot (May 2026). Resolve the current tested set from the support matrix before pinning — see references/volatile.md. Never install @latest.
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 tests2) 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 --version3) 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 compactExpected 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=30313233343536373839303132333435363738393031323334353637383930313217) 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-psFaucet: 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 loopNote: 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 validate21) Troubleshooting
| Error | Cause | Fix |
compact: command not found | PATH not set | source $HOME/.local/bin/env |
ETARGET No matching version found | Version not in the tested set | Resolve current versions from the support matrix — see references/volatile.md. Do not install @latest. |
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
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

