Skip to main content

The Silent Killer: SDK Version Mismatch Between Preprod and Preview

· 6 min read
Frederico Santana
Founder & Technical Writer, DPO2U

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:

  1. Docker nodemidnight-node:0.21.0 was running, producing blocks in standalone mode, WebSocket on port 9944 responding to RPC calls. Healthy.

  2. Indexerindexer-standalone:4.0.0-rc.4 was syncing blocks from the node. GraphQL endpoint on port 8088 returned valid responses. Healthy.

  3. Proof serverproof-server:7.0.0 was up on port 6300. The /health endpoint returned OK. Healthy.

  4. WebSocket connections — The wallet SDK was connecting to all three services. I could see the WebSocket handshakes in the Docker logs. No connection errors.

  5. Network ID — Already set to 'preprod' before wallet initialization (learned that lesson from a previous debugging session).

  6. 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:

PackageHad (preview)Needed (preprod)Action
wallet-sdk-facade2.0.01.0.0DOWNGRADE
wallet-sdk-shielded2.0.01.0.0DOWNGRADE
wallet-sdk-unshielded-wallet2.0.01.0.0DOWNGRADE
wallet-sdk-dust-wallet2.0.01.0.0DOWNGRADE
wallet-sdk-abstractions2.0.01.0.0DOWNGRADE
wallet-sdk-hd3.0.13.0.0DOWNGRADE
wallet-sdk-address-format3.0.13.0.0DOWNGRADE
midnight-js-contracts3.2.03.1.0DOWNGRADE
midnight-js-types3.2.03.1.0DOWNGRADE
midnight-js-utils3.2.03.1.0DOWNGRADE
midnight-js-network-id3.2.03.1.0DOWNGRADE
midnight-js-* (4 more)3.2.03.1.0DOWNGRADE
compact-js2.4.32.4.0DOWNGRADE
ledger-v77.0.37.0.0DOWNGRADE
compact-runtime0.14.00.14.0OK

The Docker images also needed alignment:

ComponentHadNeeded
midnight-node0.21.00.21.0 (OK)
indexer-standalone4.0.0-rc.43.1.0
proof-server7.0.07.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 ps and 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-facade and check the version. SDK 1.0.0 = preprod, 2.0.0 = preview.
  • Check for duplicate ledger-v7. Run find 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 install defaults. When you npm 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.0 for preprod, 4.0.0 for 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.