Skip to main content
A Midnight contract call is a zero-knowledge proof, not a signed transaction. So an embedded wallet cannot simply “sign the contract call” — the work splits in two: primaryWallet.getWalletProvider() returns the settlement half as a midnight-js–compatible provider. Balancing and MPC signing happen inside the wallet iframe, so shielded, DUST and MPC key material never leaves it — only public keys and serialized transactions cross the boundary.
This is for embedded Midnight wallets. Injected wallets (1am) expose their own provider surface — see Using Midnight wallets.

What the wallet gives you

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

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 artefacts 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 proves its own shielded and DUST work separately, inside the iframe.

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.

Things that will catch you out

These are not obvious from the midnight-js types, and each one fails in a way that points somewhere unhelpful.
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 artefacts — 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 answers unknown paths with 200 and index.html, so a wrong artefact 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 404 into the same fallback.
Deploying publishes state and verifier keys — there is no circuit execution to prove, so a deploy can succeed with no proof server 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() initialises 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 shows in balances. A zero balance straight after a successful mint usually means sync, not failure.

Confirming a transaction landed

submitTx returns the submission identifier, which is what inclusion is polled by. 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 4, 2026