| name | midnight-dapp |
| description | Build and deploy Midnight DApps with TypeScript SDK, wallet integration, and proof servers. Use when deploying contracts, integrating Lace wallet, setting up providers, debugging transaction errors, configuring proof servers, or building Next.js frontends for Midnight. Covers midnight-js, DApp Connector API, headless wallet SDK, indexer GraphQL, and deployment pipeline. |
| version | 1.0.0 |
| scope | public |
Midnight DApp Development
Build TypeScript DApps on Midnight using midnight-js, Lace wallet, and proof servers.
Two Integration Paths
| Path | When | Complexity |
|---|
Wallet-Native (DApp Connector API) | Simple transfers, shield/unshield, V1 MVPs | Low (pure TS, no WASM) |
Full Providers (midnight-js) | Custom contract circuits, AMM, complex DApps | High (WASM, proof server, ZK configs) |
Path 1: Wallet-Native (DApp Connector API v4)
Connect to Lace
import { interval, filter, concatMap, take, timeout, throwError, of, firstValueFrom, map } from 'rxjs';
import * as semver from 'semver';
const connectToWallet = (networkId: string) =>
firstValueFrom(
interval(100).pipe(
map(() => window.midnight?.mnLace),
filter((api) => !!api),
concatMap((api) => semver.satisfies(api.apiVersion, '4.x')
? of(api) : throwError(() => new Error('Incompatible wallet'))),
take(1),
timeout({ first: 1_000, with: () => throwError(() => new Error('Wallet not found')) }),
concatMap(async (api) => api.connect(networkId)),
)
);
Key Methods
| Method | Purpose | Notes |
|---|
getConfiguration() | indexer/prover URIs | |
getShieldedBalances() | Token balances (shielded) | |
getUnshieldedBalances() | Token balances (unshielded) | |
getDustBalance() | DUST cap + balance | |
makeTransfer(outputs) | Simple payments | |
makeIntent(inputs, outputs, options) | Multi-party swap | 3rd arg required: { intentId, payFees } |
balanceSealedTransaction(tx) | Add fees | |
submitTransaction(tx) | Submit to chain | Returns void, NOT tx hash |
Known Limitations (Lace Alpha)
makeTransfer with kind: 'shielded' hangs after approval
makeIntent returns "Method not implemented"
- Use
initSwap via headless SDK for cross-pool operations
Path 2: Full Providers (midnight-js v3.1.0)
Provider Setup (Node.js / CLI)
import { deployContract, findDeployedContract } from '@midnight-ntwrk/midnight-js-contracts';
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { indexerPublicDataProvider } from '@midnight-ntwrk/midnight-js-indexer-public-data-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
import { levelPrivateStateProvider } from '@midnight-ntwrk/midnight-js-level-private-state-provider';
import { CompiledContract } from '@midnight-ntwrk/compact-js';
const compiledContract = CompiledContract.make('my-contract', MyContract.Contract).pipe(
CompiledContract.withVacantWitnesses,
CompiledContract.withCompiledFileAssets(zkConfigPath),
);
const providers = {
privateStateProvider: levelPrivateStateProvider({ ... }),
publicDataProvider: indexerPublicDataProvider(indexerUrl, indexerWsUrl),
zkConfigProvider: new NodeZkConfigProvider(zkConfigPath),
proofProvider: httpClientProofProvider(proofServerUrl, zkConfigProvider),
walletProvider: walletAndMidnightProvider,
midnightProvider: walletAndMidnightProvider,
};
Provider Setup (Browser)
import { FetchZkConfigProvider } from '@midnight-ntwrk/midnight-js-fetch-zk-config-provider';
const zkConfigProvider = new FetchZkConfigProvider(
window.location.origin,
fetch.bind(window)
);
const privateStateProvider = inMemoryPrivateStateProvider();
const walletProvider = {
getCoinPublicKey: () => shieldedAddresses.shieldedCoinPublicKey,
getEncryptionPublicKey: () => shieldedAddresses.shieldedEncryptionPublicKey,
balanceTx: async (tx) => {
const received = await connectedAPI.balanceUnsealedTransaction(toHex(tx.serialize()));
return Transaction.deserialize('signature', 'proof', 'binding', fromHex(received.tx));
},
};
Deploy & Call
const contract = await deployContract(providers, {
compiledContract, args: [100n, 200n],
});
const address = contract.deployTxData.public.contractAddress;
const contract = await findDeployedContract(providers, {
contractAddress: address, compiledContract,
});
const result = await contract.callTx.myCircuit(arg1, arg2);
Witness Implementation
type MyPrivateState = { readonly secretKey: Uint8Array };
const witnesses = {
localSecretKey: ({ privateState }: WitnessContext<Ledger, MyPrivateState>
): [MyPrivateState, Uint8Array] =>
[privateState, privateState.secretKey],
};
Headless Wallet SDK
For CLI tools and backend scripts (no Lace needed).
Correct Package Combo
{
"@midnight-ntwrk/wallet-sdk-facade": "1.0.0",
"@midnight-ntwrk/wallet-sdk-hd": "3.0.0",
"@midnight-ntwrk/wallet-sdk-shielded": "1.0.0",
"@midnight-ntwrk/wallet-sdk-dust-wallet": "1.0.0",
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "1.0.0",
"@midnight-ntwrk/wallet-sdk-address-format": "3.0.0",
"@midnight-ntwrk/ledger-v7": "7.0.2"
}
Signing: Two Paths
| Transaction Type | Signing Method |
|---|
Transfer (transferTransaction) | wallet.signRecipe(recipe, signFn) then finalizeRecipe |
Contract call (balanceUnboundTransaction) | signTransactionIntents workaround |
Mixing these causes error 139.
Cross-Pool Operations
transferTransaction does NOT support unshielded-to-shielded. Use initSwap instead.
Network Configuration
| Network | Indexer HTTP | Indexer WS | Node RPC | Proof Server |
|---|
| Local | http://127.0.0.1:8088/api/v3/graphql | ws://127.0.0.1:8088/.../ws | http://127.0.0.1:9944 | http://127.0.0.1:6300 |
| Preview | https://indexer.preview.midnight.network/api/v3/graphql | wss://indexer.preview.midnight.network/.../ws | https://rpc.preview.midnight.network | https://lace-proof-pub.preview.midnight.network |
| Preprod | https://indexer.preprod.midnight.network/api/v3/graphql | wss://indexer.preprod.midnight.network/.../ws | https://rpc.preprod.midnight.network | Local only |
Network IDs: setNetworkId('undeployed' | 'preview' | 'preprod')
Proof Server
docker run -p 6300:6300 midnightntwrk/proof-server:7.0.0 -- midnight-proof-server -v
docker run -p 6300:6300 bricktowers/proof-server:7.0.0 -- midnight-proof-server -v
docker run -p 6300:6300 meshsdk/midnight-proof-server
Each user runs their own proof server (Lace handles this). For CI/CD deployment, you need one.
Indexer GraphQL
Key Operations
query { contractAction(address: "0x...") { state unshieldedBalances { tokenType amount } } }
subscription { contractActions(address: "0x...") {
... on ContractCall { entryPoint state transaction { hash } }
}}
mutation { connect(viewingKey: "...") }
State Observable (RxJS)
providers.publicDataProvider
.contractStateObservable(address, { type: 'latest' })
.pipe(map(cs => MyContract.ledger(cs.data)));
Next.js Static Export
Config
const nextConfig: NextConfig = {
output: 'export',
webpack: (config) => {
config.experiments = { ...config.experiments, asyncWebAssembly: true };
return config;
},
};
Required Polyfills (BEFORE any SDK import)
import { Buffer } from 'buffer';
globalThis.process = { env: { NODE_ENV: import.meta.env.MODE } };
globalThis.Buffer = Buffer;
Client-Only Loading
const MidnightApp = dynamic(() => import('./MidnightAppInner'), { ssr: false });
Static ZK Assets
Copy keys/ and zkir/ from compiled output to public/.
Token Notes
- NIGHT decimals: 6
- DUST decimals: 15 (on-chain raw values are 10^15 scale)
- Native NIGHT token type: 64 hex zeros
- Custom token type:
tokenType(domain, contractAddress) — 32 bytes
Shielded Addresses
Lace returns hex public keys. makeTransfer expects Bech32m:
import { bech32m } from 'bech32';
const encoded = bech32m.encode(`mn_shield-addr_preview`, bech32m.toWords(hexToBytes(coinPubKey)));
Common Errors
| Error | Cause | Fix |
|---|
| Error 139 | Wrong signing method or re-registering UTXOs | Use correct signing path (see above) |
RuntimeError: unreachable | mintShieldedToken WASM bug | Use mintUnshieldedToken (see midnight-compact skill) |
ERR_UNSUPPORTED_DIR_IMPORT | Stale Node.js environment | Open new terminal, check Node version |
| WS timeout on indexer | Missing graphql-ws subprotocol | Use SDK's built-in client (handles subprotocol) |
additionalFeeOverhead too low | Tx fee underestimated | Set to 300_000_000_000_000n |
| "Failed to clone intent" | Lace v2.38.0 bug | Update Lace |
For Detailed Reference
references/deployment-pipeline.md — CI/CD, Docker, mainnet migration checklist
midnight-compact skill — Contract language and patterns