(Versions referenced below: @midnight-ntwrk/* packages at 4.1.1, wallet-sdk-facade at 4.0.1. Pin your own versions before you rely on this, the provider surface, and some package names, have moved between releases.)
Midnight providers are modular, pluggable components that each handle a specific capability required for transaction construction and submission to the Midnight blockchain: proof generation, private state management, public data queries, and transaction balancing and submission. They're the toolset that powers Midnight.js's architecture.
There are six provider slots in total, defined by the MidnightProviders interface in @midnight-ntwrk/midnight-js-types. Four of them, proof generation, ZK config, private state, and public data, ship as ready made packages. The remaining two, transaction balancing and submission, are interfaces you implement yourself, by wrapping whatever wallet you're integrating with.
The five providers in Midnight.js
-
midnight-js-indexer-public-data-provider: a GraphQL based blockchain data provider offering query and subscription operations against public blockchain data. -
midnight-js-level-private-state-provider: AES 256 GCM encrypted persistent state storage via LevelDB. -
midnight-js-http-client-proof-provider: an HTTP client for the Midnight proof server. -
midnight-js-fetch-zk-config-provider: a browser compatible zero knowledge artifact provider, using the Fetch API to retrieve the ZK configuration (proving key, verifier key, ZKIR). -
midnight-js-node-zk-config-provider: a Node.js filesystem based equivalent, used in place of the fetch provider when running outside a browser.
The two ZK config providers aren't separate slots. They're two implementations of the same zkConfigProvider role, one for browser environments, one for Node.js. Only one occupies the slot at a time. That's really four provider roles filled by five packages.
There's also midnight-js-dapp-connector-proof-provider, which delegates proof generation to the DApp Connector wallet instead of an HTTP proof server, an alternative to httpClientProofProvider.
For the optional loggerProvider slot mentioned below, @midnight-ntwrk/midnight-js-logger-provider is a real Pino based package, not something you have to build yourself.
The two providers you write yourself: walletProvider and midnightProvider
walletProvider and midnightProvider are not exports of any package. Not wallet-sdk-facade, not midnight-js-types. What those packages give you are interfaces: WalletProvider and MidnightProvider, both in @midnight-ntwrk/midnight-js-types. You build an object that satisfies each interface by wrapping whatever wallet API your dApp actually talks to, a headless WalletFacade in a script, or a ConnectedAPI session from the DApp Connector API in a browser dApp.
Every official Midnight tutorial (the bulletin board CLI, hello world, battleship, the token guide) writes one class implementing both interfaces, then assigns that single instance to both slots:
import { type CoinPublicKey, type EncPublicKey, type FinalizedTransaction, ZswapSecretKeys, DustSecretKey } from '@midnight-ntwrk/midnight-js-protocol/ledger';
import { type WalletProvider, type MidnightProvider, type UnboundTransaction } from '@midnight-ntwrk/midnight-js-types';
import { ttlOneHour } from '@midnight-ntwrk/midnight-js-utils';
import { type WalletFacade } from '@midnight-ntwrk/wallet-sdk';
class BBoardWalletProvider implements WalletProvider, MidnightProvider {
constructor(
private readonly wallet: WalletFacade,
private readonly zswapSecretKeys: ZswapSecretKeys,
private readonly dustSecretKey: DustSecretKey,
) {}
getCoinPublicKey(): CoinPublicKey {
return this.zswapSecretKeys.coinPublicKey;
}
getEncryptionPublicKey(): EncPublicKey {
return this.zswapSecretKeys.encryptionPublicKey;
}
async balanceTx(tx: UnboundTransaction, ttl: Date = ttlOneHour()): Promise<FinalizedTransaction> {
const recipe = await this.wallet.balanceUnboundTransaction(
tx,
{ shieldedSecretKeys: this.zswapSecretKeys, dustSecretKey: this.dustSecretKey },
{ ttl },
);
return await this.wallet.finalizeRecipe(recipe);
}
submitTx(tx: FinalizedTransaction): Promise<string> {
return this.wallet.submitTransaction(tx);
}
}
const walletProvider = new BBoardWalletProvider(wallet, zswapSecretKeys, dustSecretKey);
const providers: BBoardProviders = {
privateStateProvider,
publicDataProvider,
zkConfigProvider,
proofProvider,
walletProvider,
midnightProvider: walletProvider, // same instance satisfies both interfaces
};
Compiles as shown, given the imports above and wallet, zswapSecretKeys, dustSecretKey, and the other four providers already constructed, copied from Midnight's official "Deploying and operating a contract" guide.
Note the ttl default: ttlOneHour(), a small helper from midnight-js-utils, rather than something computed by hand. Some real world adapters instead hand roll their own transaction signing, a custom loop over tx.intents, where a project's pinned SDK version doesn't yet expose the equivalent of wallet.signRecipe(). Functionally the same outcome; reaching for signRecipe first, if your version has it, saves the manual loop.
Not every project follows the one class shape above. The interfaces are what matter, not the object shape. Here's the same two interfaces satisfied as two plain objects instead, against a browser wallet session via the DApp Connector API. A full, working modular implementation of both providers lives in my BrowseMe repo: https://github.com/Gutopro/BrowseMe/blob/main/frontend/my-wallet-app/src/providers.ts
walletProvider: {
getCoinPublicKey: () => shielded.shieldedCoinPublicKey,
getEncryptionPublicKey: () => shielded.shieldedEncryptionPublicKey,
balanceTx: async (tx: { serialize: () => Uint8Array }, _newCoins?: unknown) => {
const { tx: balancedHex } = await connected.balanceUnsealedTransaction(toHex(tx.serialize()));
return Transaction.deserialize('signature', 'proof', 'binding', fromHex(balancedHex));
},
},
midnightProvider: {
submitTx: async (tx: { serialize: () => Uint8Array; identifiers: () => string[] }) => {
await connected.submitTransaction(toHex(tx.serialize()));
return tx.identifiers()[0];
},
},
Copied from a working providers.ts, with its import lines and surrounding initializeProviders function trimmed for space. The bodies compile as is in that file.
Watch this one if you're copying it into a new project: WalletProvider declares getCoinPublicKey() and getEncryptionPublicKey() as methods, not properties named coinPublicKey/encryptionPublicKey. Supplying properties instead only type checks if the whole object gets cast away, e.g. as unknown as MidnightProviders, which compiles cleanly but crashes at runtime the first time midnight-js-contracts calls providers.walletProvider.getCoinPublicKey() internally. The version above already uses methods. If a double cast is still needed after fixing that, the remaining gap is usually the transaction types, tx typed loosely here as { serialize: () => Uint8Array } rather than the SDK's actual UnboundTransaction/FinalizedTransaction.
You may also see walletProvider, // from @midnight-ntwrk/wallet-sdk-facade in one of Midnight's own quick start snippets. That's a simplification, not a real import path. Every worked tutorial builds the adapter by hand, as above.
Given what they actually do:
-
walletProvider: handles transaction balancing and finalization, turning an unbalanced transaction into a finalized, submittable one. Key access lives here too. -
midnightProvider: submits the finalized transaction to the network.
Putting it together
const providers: MidnightProviders = {
privateStateProvider: levelPrivateStateProvider({
privateStoragePasswordProvider: () => password,
accountId: walletAddress,
}),
publicDataProvider: indexerPublicDataProvider(queryUrl, subscriptionUrl),
zkConfigProvider,
proofProvider: httpClientProofProvider(proofServerUrl, zkConfigProvider),
walletProvider,
midnightProvider,
};
Illustrative, password, walletAddress, queryUrl, etc. are placeholders for values supplied elsewhere, matching how Midnight's own docs present this same assembly.
loggerProvider is the optional seventh field, for diagnostics logging.
A note for anyone just starting out
Package names have shifted between SDK releases, some current docs call the wallet package wallet-sdk, where earlier tutorials use wallet-sdk-facade. And some provider constructors are moving from positional arguments toward an options object form (fetchZkConfigProvider({ baseURL, ... }) alongside the older new FetchZkConfigProvider(baseURL, ...)), with both documented as valid for now. If a snippet you find doesn't match your installed package, check which SDK version it targets before assuming it's wrong, check against the docs for the version pinned in your own package.json, not the latest docs.
Why this separation matters
Proof generation, private state, and public data queries are concerns internal to how a dApp talks to the chain. They don't need to know anything about a specific wallet. Balancing and submitting transactions are inherently wallet concerns: they touch keys and account state, cover balancing and finalization, and their implementation depends on whether you're driving a headless wallet or a browser extension.
Keeping that boundary clean means you can swap wallet integrations without touching your proof or state logic, as long as the replacement satisfies the same WalletProvider/MidnightProvider interfaces, whether as one class filling both slots or two objects filling them separately. Code built against MidnightProviders doesn't care which.
If you're setting this up yourself, start from the MidnightProviders, WalletProvider, and MidnightProvider interfaces in midnight-js-types, and write the wallet adapter against your actual wallet API, rather than assuming one ships pre built. That's the step that gets you a working deploy.
Top comments (0)