Warning
This repository is archived and no longer maintained.
midnight-local-dev ran a local Midnight network and funded test accounts. midnight-examples now does both: yarn env:up starts a local network for any example, and yarn fund:wallet funds a browser wallet such as Lace with DUST.
See Getting started. This repo is read-only and will not be updated for future SDK releases.
A standalone tool for running a local Midnight development network and funding test accounts. It handles Docker container orchestration, genesis wallet initialization, NIGHT token transfers, and DUST registration — so your DApp projects can connect to a ready-to-use local blockchain without managing infrastructure themselves.
- Midnight Local Network
- Node.js >= 22.0.0
- Docker and Docker Compose (v2)
cd midnight-local-dev
npm installNo further setup is needed. The first npm start creates .env from
.env.example automatically already filled in with working defaults and
checks your Node version and dependencies before it does anything else.
npm startThis single command will:
- Detect if a local Midnight network is already running
- If running: prompt to reuse it or restart with fresh images
- If not running: pull Docker images and start all containers
- Initialize the genesis master wallet (seed
0x00...001) which holds all minted NIGHT tokens - Register DUST for the master wallet (required to pay transaction fees)
- Display the master wallet balance
- Present an interactive menu for funding test accounts
If a network is already running, you'll first see:
A local dev network is already running:
[1] Use the existing network
[2] Stop containers, pull latest images, and restart
>
Then the main menu:
Choose an option:
[1] Fund accounts from config file (NIGHT + DUST registration)
[2] Fund accounts by public key (NIGHT transfer only)
[3] Display master wallet balances
[4] Exit
>
When you select [4] Exit or press Ctrl+C, the tool gracefully shuts down all wallets. Docker containers are only stopped if they were started by this session (reusing an existing network leaves containers running).
npm start on its own is interactive: it presents a readline menu and waits
for input, so it will hang in any environment without a TTY.
Pass --fund-config <path> to run non-interactively instead. It is the only
headless entry point, and it does everything the menu's option 1 does:
cp accounts.example.json accounts.json
npm start -- --fund-config ./accounts.jsonIn this mode the tool will:
- reuse an already-running network instead of prompting to restart it,
- fund every account in the file with NIGHT and register DUST for each,
- print the funded account details, then tear the network down and exit.
Two constraints worth knowing:
- The config path must resolve inside the project directory. A file under a
system temp directory is rejected, so write
accounts.jsoninto the repo root. - Each account costs a full funding round-trip, so keep the file short in CI.
This is what .github/workflows/ci.yml runs as its
devnet smoke test.
| Command | What it does |
|---|---|
npm run typecheck |
Type-check the project (tsc --noEmit). |
npm test |
Run the unit tests on Node's built-in test runner. |
npm run devnet:health |
Check all three containers are healthy and reachable from the host. Requires bash. |
npm run clean |
Stop and remove the devnet containers. |
The local network runs three Docker containers on fixed ports:
| Service | Container Name | Host Port | URL |
|---|---|---|---|
| Midnight Node | midnight-node |
9944 |
http://localhost:9944 |
| Indexer (GraphQL) | midnight-indexer |
8088 |
http://localhost:8088/api/v4/graphql |
| Indexer (WebSocket) | midnight-indexer |
8088 |
ws://localhost:8088/api/v4/graphql/ws |
| Proof Server | midnight-proof-server |
6300 |
http://localhost:6300 |
These ports match the defaults hardcoded by the Lace wallet extension when configured for the undeployed network type. This means Lace connects to the local network out of the box — no custom endpoint configuration required. Just select "Undeployed" in Lace's network settings and it will point to localhost:9944, localhost:8088, and localhost:6300 automatically.
All services use the undeployed network ID with the dev node preset.
Each funding operation transfers 50,000 NIGHT (in smallest denomination: 50,000 * 10^6) from the genesis master wallet. You can fund up to 10 accounts per operation.
Best for development setups where you control the test wallets. Provide a JSON file with BIP39 mnemonics. For each account, the tool will:
- Derive a wallet from the mnemonic
- Transfer 50,000 NIGHT from the master wallet
- Wait for the recipient to see the funds
- Register the recipient's NIGHT UTXOs for DUST generation
- Wait for DUST to be available
After this, each account is fully ready to submit transactions (deploy contracts, call circuits, etc.) because they have both NIGHT (for value) and DUST (for fees).
> 1
Path to accounts JSON file: ./accounts.json
See Accounts Config File Format for the JSON schema.
Best when you already have wallet addresses (e.g., from the Lace wallet or another DApp) and just need tokens. Provide one or more Bech32 addresses separated by commas.
> 2
Enter Bech32 addresses (comma-separated): mn1q..., mn1q...
Each address receives 50,000 NIGHT. DUST is not registered — recipients must register for DUST themselves before they can pay transaction fees.
Once the network is running, any Midnight DApp can connect using the standard localhost endpoints. No special configuration needed.
In a separate terminal, while the network is running:
cd ../zkloan-credit-scorer/zkloan-credit-scorer-cli
npm run standaloneThe CLI's standalone mode connects to the same fixed ports (9944, 8088, 6300) without starting its own Docker containers.
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
setNetworkId('undeployed');
const config = {
indexer: 'http://127.0.0.1:8088/api/v4/graphql',
indexerWS: 'ws://127.0.0.1:8088/api/v4/graphql/ws',
node: 'http://127.0.0.1:9944',
proofServer: 'http://127.0.0.1:6300',
networkId: 'undeployed',
};
// Use these endpoints with the Midnight wallet SDK, contract deployment, etc.Point your UI's environment variables to the local endpoints:
VITE_INDEXER_URL=http://localhost:8088/api/v4/graphql
VITE_INDEXER_WS_URL=ws://localhost:8088/api/v4/graphql/ws
VITE_NODE_URL=http://localhost:9944
VITE_PROOF_SERVER_URL=http://localhost:6300
VITE_NETWORK_ID=undeployedIf you only need the Docker containers running (no wallet initialization or funding), you can use Docker Compose directly:
# Pull images and start all services
docker compose -f standalone.yml up -d
# Check service health
docker compose -f standalone.yml ps
# View logs
docker compose -f standalone.yml logs -f
# Stop everything
docker compose -f standalone.yml downThis is useful when:
- Your DApp handles its own wallet initialization
- You want to keep the network running across multiple test sessions
- You're debugging container-level issues
Note: In this mode you must handle genesis wallet funding and DUST registration yourself.
Create a JSON file with the following structure:
{
"accounts": [
{
"name": "Alice",
"mnemonic": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"
},
{
"name": "Bob",
"mnemonic": "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote"
}
]
}| Field | Type | Description |
|---|---|---|
accounts |
array |
List of accounts to fund (max 10) |
accounts[].name |
string |
Display name for logging |
accounts[].mnemonic |
string |
BIP39 mnemonic phrase (24 words) |
An example file is provided at accounts.example.json.
Security note: These mnemonics are for local development only. Never use real wallet mnemonics in config files.
.env is created from .env.example on your first npm start, so there is
nothing to copy by hand — it arrives populated with the values below, ready to
edit. An existing .env is never overwritten, and anything already set in your
shell takes precedence over it.
| Variable | Default | Description |
|---|---|---|
DEBUG_LEVEL |
info |
Log level: trace, debug, info, warn, error, fatal |
MN_NODE_PORT |
9944 |
Host port the node is published on |
MN_INDEXER_PORT |
8088 |
Host port the indexer is published on |
MN_PROOF_SERVER_PORT |
6300 |
Host port the proof server is published on |
MN_NODE_URL |
http://127.0.0.1:9944 |
Node RPC endpoint the clients connect to |
MN_NODE_WS |
ws://127.0.0.1:9944 |
Node websocket endpoint |
MN_INDEXER_URL |
http://127.0.0.1:8088/api/v4/graphql |
Indexer GraphQL endpoint |
MN_INDEXER_WS |
ws://127.0.0.1:8088/api/v4/graphql/ws |
Indexer GraphQL subscription endpoint |
MN_PROOF_SERVER_URL |
http://127.0.0.1:6300 |
Proof server endpoint |
The MN_*_PORT values control what docker compose publishes; the URLs control
where the clients connect. Change both together when moving off the defaults.
┌─────────────────────────────────────────────────┐
│ midnight-local-network │
│ │
│ src/index.ts Entry point & interactive menu │
│ src/network.ts Docker compose orchestration │
│ src/wallet.ts Wallet SDK operations │
│ src/funding.ts NIGHT transfer & DUST reg. │
│ src/config.ts Network endpoint config │
│ src/logger.ts Pino logger setup │
└──────────┬──────────────────────────┬────────────┘
│ │
testcontainers Wallet SDK
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────┐
│ Docker Compose │ │ Midnight Blockchain │
│ │ │ │
│ ┌────────────┐ │ │ Genesis Wallet (master) │
│ │ Node │◄─┼───┤ ├─ Transfer NIGHT │
│ │ :9944 │ │ │ ├─ Register DUST │
│ └────────────┘ │ │ └─ Fund recipients │
│ ┌────────────┐ │ │ │
│ │ Indexer │ │ │ Recipient Wallets │
│ │ :8088 │ │ │ ├─ From mnemonic │
│ └────────────┘ │ │ └─ From Bech32 address │
│ ┌────────────┐ │ └──────────────────────────┘
│ │ Proof │ │
│ │ Server │ │
│ │ :6300 │ │
│ └────────────┘ │
└──────────────────┘
1. Network detection
├── Check if midnight-node, midnight-indexer, midnight-proof-server are running
├── If running → prompt: reuse existing or restart fresh
└── If not running → docker compose up
├── midnight-node starts (waits for health: /health endpoint)
├── midnight-indexer starts (waits for: "starting indexing" log)
└── midnight-proof-server starts (waits for: "Actix runtime found" log)
2. Master wallet initialization (WalletFacade.init)
├── Derive HD wallet from genesis seed (0x00...001)
├── Create unified DefaultConfiguration
├── Initialize WalletFacade with shielded, unshielded, and dust builders
├── Start wallet facade and wait for sync
└── Display genesis NIGHT balance
3. DUST registration for master wallet
├── Find unregistered Night UTXOs
├── Submit registration transaction
└── Wait for dust balance > 0
4. Interactive menu (fund accounts, check balances, exit)
5. Cleanup on exit
├── Close all wallet connections
└── docker compose down (only if network was started by this session)
- NIGHT: The native token on Midnight. The genesis block mints a large supply accessible via the master wallet seed.
- DUST: Transaction fees on Midnight are paid in DUST, which is generated by registering NIGHT UTXOs. Without DUST registration, a wallet cannot submit transactions even if it holds NIGHT.
- Master Wallet: The genesis wallet (seed
0x00...001) that holds all initially minted tokens. All funding transfers originate from this wallet. - Unshielded vs Shielded: NIGHT can be held in unshielded (public) or shielded (private) form. This tool transfers unshielded NIGHT.
Error: Bind for 0.0.0.0:9944 failed: port is already allocated
Another process or a previous run is using the port. Stop it:
npm run clean
# or find and kill the process
lsof -i :9944If the conflict is a second Midnight devnet you want to keep running, publish
this one elsewhere instead. Edit .env and change both the port and the URL
that goes with it:
MN_NODE_PORT=19944
MN_NODE_URL=http://127.0.0.1:19944
MN_NODE_WS=ws://127.0.0.1:19944Check Docker is running and you have access to the Midnight Docker registry:
docker compose -f standalone.yml pull
docker compose -f standalone.yml upWatch the logs for specific errors:
docker compose -f standalone.yml logs -f node
docker compose -f standalone.yml logs -f indexer
docker compose -f standalone.yml logs -f proof-serverThe indexer needs time to catch up with the node after startup. If sync seems stuck:
- Check the indexer logs:
docker compose -f standalone.yml logs -f indexer - Verify the node is producing blocks:
curl http://localhost:9944/health - Set
DEBUG_LEVEL=debugin.envfor more detailed wallet logs
On a freshly started network the genesis wallet reports a full DUST balance and several spendable coins within a second of the first block, but the transaction balancer cannot cover a fee until the chain has advanced a little further.
Nothing in the wallet state distinguishes the two situations, so the funding code
simply retries the transfer until it succeeds (up to three minutes), logging
DUST not spendable yet (attempt N) while it waits. No action is needed — if you
see this scroll past once or twice, it is working as intended.
DUST registration requires the wallet to have synced and found its NIGHT UTXOs. If it fails:
- Ensure the wallet has a non-zero NIGHT balance
- Check the proof server is healthy:
curl http://localhost:6300/version - Try again — transient network issues during startup can cause one-time failures
Logs are written to both the console and a timestamped file in the logs/ directory:
ls logs/
# 2026-02-24T12:00:00.000Z.log