The Silent Killer: SDK Version Mismatch Between Preprod and Preview
I spent days debugging a wallet that refused to sync. No error messages, no stack traces, no timeouts — just waitForSyncedState() hanging forever in perfect silence. The root cause turned out to be something that no documentation warns you about: the Midnight SDK publishes packages for two different environments under the same @midnight-ntwrk/* npm namespace, and installing the wrong combination will silently break everything.
The symptoms
The DPO2U Wallet project uses a local Docker stack: midnight-node:0.21.0 (preprod-compatible), indexer-standalone, and proof-server:7.0.0. The wallet initialization code followed the SDK examples — create a WalletFacade, configure the providers, call start(), and wait for sync.
What happened: waitForSyncedState() never resolved. Not after 30 seconds, not after 5 minutes, not after 20. No error was thrown. The Observable from wallet.state() emitted exactly one event (idle) and then went silent.
This happened on both standalone mode (local Docker node) and when pointing at the preprod network. The behavior was identical — which made it even more confusing, because it suggested the problem wasn't network-related.
The investigation
I went through every layer of the stack:
-
Docker node —
midnight-node:0.21.0was running, producing blocks in standalone mode, WebSocket on port 9944 responding to RPC calls. Healthy. -
Indexer —
indexer-standalone:4.0.0-rc.4was syncing blocks from the node. GraphQL endpoint on port 8088 returned valid responses. Healthy. -
Proof server —
proof-server:7.0.0was up on port 6300. The/healthendpoint returned OK. Healthy. -
WebSocket connections — The wallet SDK was connecting to all three services. I could see the WebSocket handshakes in the Docker logs. No connection errors.
-
Network ID — Already set to
'preprod'before wallet initialization (learned that lesson from a previous debugging session). -
HD wallet derivation — Keys were derived correctly using
wallet-sdk-hd. The unshielded address matched what the Lace wallet showed for the same mnemonic.
Everything looked correct individually. But the wallet simply would not sync.
The root cause
After hours of comparing my package.json against the Midnight examples repository, I noticed something: the example used wallet-sdk-facade@1.0.0, but npm install had pulled 2.0.0.
That's when the pattern became clear. The Midnight team publishes SDK packages for two different network environments:
- Preview (node 0.22.0): SDK 2.0.0, midnight-js 3.2.0
- Preprod (node 0.21.0): SDK 1.0.0, midnight-js 3.1.0
These are published under the exact same npm package names. There is no @midnight-ntwrk/wallet-sdk-facade-preprod or @midnight-ntwrk/wallet-sdk-facade-preview. There's just @midnight-ntwrk/wallet-sdk-facade with version numbers that silently encode which environment they target.
My project was running midnight-node:0.21.0 (preprod) but had wallet-sdk-facade@2.0.0 (preview). The SDK was speaking a protocol that the node didn't understand, and instead of throwing an error, it just... waited. Forever.
The version matrix
Here's every package that was wrong, and what it needed to be:
| Package | Had (preview) | Needed (preprod) | Action |
|---|---|---|---|
wallet-sdk-facade | 2.0.0 | 1.0.0 | DOWNGRADE |
wallet-sdk-shielded | 2.0.0 | 1.0.0 | DOWNGRADE |
wallet-sdk-unshielded-wallet | 2.0.0 | 1.0.0 | DOWNGRADE |
wallet-sdk-dust-wallet | 2.0.0 | 1.0.0 | DOWNGRADE |
wallet-sdk-abstractions | 2.0.0 | 1.0.0 | DOWNGRADE |
wallet-sdk-hd | 3.0.1 | 3.0.0 | DOWNGRADE |
wallet-sdk-address-format | 3.0.1 | 3.0.0 | DOWNGRADE |
midnight-js-contracts | 3.2.0 | 3.1.0 | DOWNGRADE |
midnight-js-types | 3.2.0 | 3.1.0 | DOWNGRADE |
midnight-js-utils | 3.2.0 | 3.1.0 | DOWNGRADE |
midnight-js-network-id | 3.2.0 | 3.1.0 | DOWNGRADE |
midnight-js-* (4 more) | 3.2.0 | 3.1.0 | DOWNGRADE |
compact-js | 2.4.3 | 2.4.0 | DOWNGRADE |
ledger-v7 | 7.0.3 | 7.0.0 | DOWNGRADE |
compact-runtime | 0.14.0 | 0.14.0 | OK |
The Docker images also needed alignment:
| Component | Had | Needed |
|---|---|---|
midnight-node | 0.21.0 | 0.21.0 (OK) |
indexer-standalone | 4.0.0-rc.4 | 3.1.0 |
proof-server | 7.0.0 | 7.0.0 (OK) |
Code changes
The version downgrade wasn't just a package.json edit — the WalletFacade API changed between 1.0.0 and 2.0.0.
Before — SDK 2.0.0 (wrong for preprod):
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
const facade = await WalletFacade.init({
configuration: walletConfig,
shielded: (cfg) => ShieldedWallet(cfg).startWithSecretKeys(shieldedKeys),
unshielded: (cfg) => UnshieldedWallet(cfg).startWithPublicKey(pubKey),
dust: (cfg) => DustWallet(cfg).startWithSecretKey(dustKey),
});
// WalletFacade.init() returns a ready-to-use facade
await facade.waitForSyncedState();
After — SDK 1.0.0 (correct for preprod):
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
// 1.0.0: manual construction + explicit start
const sw = ShieldedWallet(walletConfig).startWithSecretKeys(shieldedKeys);
const uw = UnshieldedWallet(walletConfig).startWithPublicKey(pubKey);
const dw = DustWallet(walletConfig).startWithSecretKey(dustKey, dustParams);
const facade = new WalletFacade(sw, uw, dw);
await facade.start(shieldedKeys, dustKey);
await facade.waitForSyncedState();
The 2.0.0 API uses a static factory method (WalletFacade.init()) that takes configuration callbacks. The 1.0.0 API uses a plain constructor where you pass pre-initialized wallet instances and then call start() explicitly. If you use the 2.0.0 API against a preprod node, the facade initializes without error but the internal protocol negotiation fails silently.
The debugging flow
What I'd tell another developer
If you're building on Midnight and your wallet won't sync, run through this checklist before anything else:
- Check which node version you're running.
docker psand look at the image tag.0.21.0= preprod,0.22.0= preview. This determines everything else. - Cross-reference your npm packages. Run
npm ls @midnight-ntwrk/wallet-sdk-facadeand check the version. SDK1.0.0= preprod,2.0.0= preview. - Check for duplicate
ledger-v7. Runfind node_modules -path "*/ledger-v7/package.json" | wc -l. If the answer is more than 1, you have version conflicts. The ledger module handles the core serialization protocol — two copies means two incompatible protocol implementations fighting each other. - Pick ONE target and stick with it. Preprod OR preview. Never mix. There is no compatibility layer between them.
- Don't trust
npm installdefaults. When younpm install @midnight-ntwrk/wallet-sdk-facade, npm pulls the latest version — which is the preview SDK. You must pin to the exact preprod version. - Check the indexer version too. The Docker indexer must match the SDK generation. Indexer
3.1.0for preprod,4.0.0for preview.
Conclusion
This bug cost me days of productive development time. The frustrating part isn't that the versions were wrong — it's that there was zero feedback indicating they were wrong. No error message saying "protocol version mismatch." No warning during npm install. No documentation page listing which SDK versions correspond to which network environment.
The Midnight SDK is powerful — the ZK proof system works, the privacy guarantees are real, and Compact is an elegant contract language. But the developer experience around versioning is a trap for anyone who isn't already deeply embedded in the ecosystem.
If you're an independent developer building on Midnight (not using the Lace wallet team's internal tooling), you need to know this. I hope this post saves someone else the days I lost.
Tested on 2026-03-14 with midnight-node 0.21.0 (preprod), SDK 1.0.0, midnight-js 3.1.0, indexer-standalone 3.1.0, proof-server 7.0.0, Node.js v22.22.0.
