Skip to main content
A Midnight contract call is a zero-knowledge proof, not a signed transaction. This means an embedded wallet cannot simply “sign the contract call” — the work splits into two halves: primaryWallet.getWalletProvider() returns the settlement half as a midnight-js–compatible provider. Balancing and MPC signing happen inside the wallet iframe, so no shielded, DUST, or MPC key material ever leaves it — only public keys and serialized transactions cross the boundary.
This is for embedded Midnight wallets. External wallets (e.g. 1am) expose their own provider surface — see Using Midnight external wallets.
Before you start: you need a typed Midnight embedded wallet (primaryWallet) from Using Midnight embedded wallets, plus DUST, compiled Compact artifacts, and a proof server (see Prerequisites below).

Prerequisites

1

Fund the wallet and register DUST

Contract calls cost DUST. Fund the wallet with unshielded NIGHT, call registerDust(), then wait for a non-zero DUST balance — see Registering for DUST.
2

Compile your contract

compact compile produces the ZK artifacts your zkConfigProvider serves: zkir/<circuit>.bzkir, keys/<circuit>.prover, keys/<circuit>.verifier. These are deterministic, public build outputs — not secrets.
Prover keys are large: expect single-digit to tens of megabytes per circuit. They are uploaded to the proof server on every proof, so keep them served from somewhere fast.
3

Point at a proof server

Your proofProvider needs a Midnight proof server for your circuits. The wallet handles its own shielded and DUST proofs separately, inside the iframe.

What the wallet gives you

React
This satisfies both walletProvider and midnightProvider in MidnightProviders. The remaining four are yours.

Deploying and calling

React
Both calls go through balanceTx and submitTx on the wallet provider, so the user’s shielded coins and DUST pay for the transaction, and the unshielded segment is MPC-signed — without your app ever holding key material.

Common pitfalls

These are not obvious from the midnight-js types, and each one fails with a misleading error.
A circuit that creates a shielded coin needs prover keys for Midnight’s own midnight/zswap/output, input and spend circuits. Those are not in your compiled artifacts — the proof server has them.The preimage marks proving data as optional, so your provider should throw for circuit IDs it does not own. midnight-js then omits proving data and the server uses its own keys. If your provider returns something for those IDs instead, it ends up in the preimage and the proof server rejects the request with a bare 400.
createProverKey does not inspect what you hand it. A dev server’s single-page-app fallback returns 200 with index.html for unknown paths, so a wrong artifact path becomes HTML embedded in your proof preimage — and the only symptom is a bare 400 from the proof server.Reject HTML in the custom fetch, and a bad path becomes an obvious error:
Throwing here is also what makes the built-in Zswap circuits work, since those requests hit the same 404 fallback.
Deploying publishes state and verifier keys — there is no circuit execution to prove, so a deploy can succeed even when no proof server is reachable. The first callTx is where proofProvider is actually used, which is why a misconfigured proof server often looks like “deploy worked, calls broke”.
getWalletProvider() initializes the wallet inside the iframe, which includes a full sync on a cold cache. Expect well over a minute on first use, with no intermediate progress. Later calls reuse the synced wallet.
Witnesses live in your app’s private state, never in the wallet. If they are lost, gated circuits on that contract can no longer be called by anyone — the wallet cannot help recover them, because it never held them. Persist anything you cannot regenerate.
A shielded coin exists on chain as soon as the transaction is included, but the wallet has to sync the Zswap tree before it appears in your balance. A zero balance right after a successful mint usually means sync, not failure.

Confirming a transaction landed

submitTx returns the submission identifier, which is what you poll for inclusion. It is a different value from the canonical transaction hash that block explorers index — querying an indexer by the wrong one returns an empty result rather than an error, which reads like a failed transaction.
contractActions returns ContractDeploy or ContractCall with the contract address — the simplest way to confirm a deploy and recover its address.

Resources

Last modified on August 5, 2026