For the complete documentation index, see llms.txt
Integrate a wallet
How to connect a community wallet in a browser DApp using the DApp Connector API (CAIP-372): discovery, connect, ZK proving, fees, and portable React code. For choosing a wallet first, see the overview; for no-extension, headless, or agent flows, see the CLI and MCP workflow.
| Item | Detail |
|---|---|
| DApp Connector API | @midnight-ntwrk/dapp-connector-api v4.0.1 |
| Local proof server | http://localhost:6300 (Lace); CLI connector on ws://localhost:9932 |
| Networks | mainnet, preview, preprod, undeployed (local); only 'mainnet' is connector-standard, other ids are wallet-defined (see Connect and the ConnectedAPI surface) |
The DApp Connector API (CAIP-372)
A Midnight browser wallet injects an Initial API under the global window.midnight, keyed by an identifier, compatible with the draft CAIP-372 (Chain Agnostic Improvement Proposal) standard, so the same DApp code works across wallets. (npm, connector repo + SPEC, API reference)
type InitialAPI = {
rdns: string; // reverse-DNS id, stable per product
name: string; // display name (sanitize before rendering, XSS)
icon: string; // URL or data: URL (render via <img>, not innerHTML)
apiVersion: string; // version of @midnight-ntwrk/dapp-connector-api implemented
connect: (networkId: string) => Promise<ConnectedAPI>;
};
Discovery has two generations, support both. Legacy direct key (Lace at window.midnight.mnLace, 1AM at window.midnight['1am']) and the v4 rdns scan (iterate keys, match on name/rdns, EIP-6963-style, the Ethereum Improvement Proposal for multi-wallet discovery). (React wallet-connect guide)
Connect and the ConnectedAPI surface
connect(networkId) prompts the user and resolves to a ConnectedAPI spanning Midnight's three asset kinds (shielded, unshielded, DUST). Only 'mainnet' is standardized by the connector spec; every other network id is wallet-defined, and they differ (the CLI connector, for example, expects capitalized 'Preview'/'PreProd'/'Undeployed', which is why the CLI snippet uses networkId: "Preview"). Check your target wallet's docs for the exact strings.
| Method | Purpose |
|---|---|
getShieldedAddresses() / getUnshieldedAddress() / getDustAddress() | Bech32m addresses per asset kind |
getShieldedBalances() / getUnshieldedBalances() | per-token balances |
getDustBalance() | { balance, cap }: DUST regenerates toward cap |
makeTransfer(outputs, {payFees}) / makeIntent(inputs, outputs, {...}) | build a transfer / unbalanced intent |
balanceUnsealedTransaction(tx) / balanceSealedTransaction(tx) | balance and pay fees for a contract-built tx |
signData(data, {encoding, keyType}) | sign arbitrary data with the unshielded key |
submitTransaction(tx) | broadcast a sealed tx (the wallet acts as relayer) |
getProvingProvider(keyMaterialProvider) | delegate ZK proving to the wallet |
getConfiguration() / getConnectionStatus() / hintUsage(methodNames) | config; status (plus networkId); pre-request hints |
The connector is a contract, but not every wallet implements every method. Lace does not implement getProvingProvider() or signData() (see Lace). Always check, for example typeof api.getProvingProvider === "function", before calling.
Lace opens a real authorization pop-up, and the browser silently blocks it if transient user activation was lost (you awaited something, or used setTimeout or an rxjs interval first). For page-load auto-reconnect there is no gesture: poll window.midnight until injected (extensions inject slightly after DOMContentLoaded; users may need to refresh after installing), then connect.
Where ZK proofs come from
A Midnight tx needs a ZK proof; who generates it is the main axis between wallets (and maps onto the custody models):
- Local proof server (Lace). Lace requires a local proof server (Settings » Midnight » Local,
http://localhost:6300). Witness data stays on the user's machine, but they must run the server, and Lace does not exposegetProvingProvider(), so a DApp cannot delegate proving to it. - In-browser WASM (1AM). 1AM compiles Midnight's prover (a Halo2-based zk-SNARK over the BLS12-381 curve, a few MB of WASM) and proves in the tab: no separate process, sub-second after a cold-start load. 1AM does implement
getProvingProvider()and offers a hosted Proof Station. - Delegated (
getProvingProvider). The v4 connector abstracts proving behind it; the oldConfiguration.proverServerUriis deprecated. Obtain a proving provider from the wallet when available, and fall back to a configured proof server for Lace.
Fees and DUST
Fees are paid in DUST, generated by holding NIGHT and regenerating over time. getDustBalance() returns { balance, cap }, and submitTransaction uses the wallet as a relayer with a payFees option, so a wallet can sponsor fees, but sponsorship is a capability the connector allows, not a guarantee any wallet ships. On a fresh wallet, DUST takes time to generate (about 12 h on Lace mainnet/testnet, about 5 min on a local network), and overspending surfaces as BalanceCheckOverspend (138) (see Troubleshooting).
The portable integration code
Works for any standard-connector wallet (Lace, 1AM); the CLI connector is a no-extension dev fallback.
import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id";
import type { InitialAPI, ConnectedAPI } from "@midnight-ntwrk/dapp-connector-api";
// 1. Discover (legacy key first, then v4 rdns scan)
const wallets = Object.values(window.midnight ?? {})
.filter((w): w is InitialAPI => !!w?.name && !!w?.apiVersion);
// 2. Connect: MUST be synchronous in the click handler (pop-up blocking)
// networkId: only 'mainnet' is standard; others are wallet-defined (CLI uses 'Preview' etc.)
async function connect(w: InitialAPI, networkId = "preprod"): Promise<ConnectedAPI> {
const api = await w.connect(networkId); // user authorizes here
const status = await api.getConnectionStatus();
if (status.status !== "connected") throw new Error("wallet disconnected");
setNetworkId(status.networkId); // align the DApp to the wallet's network
return api;
}
// To submit: build the tx (api.makeTransfer / your contract), prove (feature-detect
// api.getProvingProvider, else local proof server), then api.submitTransaction(tx).
Where wallets diverge
Handle these differences between connectors:
- Proving: feature-detect
getProvingProvider; 1AM has it, Lace doesn't (fall back to the local proof server; don't hard-code the deprecatedproverServerUri). signData: not on Lace; feature-detect before signature-based auth.- Pop-up: Lace opens an auth pop-up (keep
connect()synchronous); the reconnect path has no gesture, so pollwindow.midnightthen connect. - Network: always reconcile the DApp to
getConnectionStatus().networkId. - DUST: read
getDustBalance(), handle{ balance: 0n }and thecap; don't assume sponsored fees. - Non-standard tiles: Ctrl/Gero may not inject a conformant connector; feature-detect and degrade to Lace/1AM.
React: the production pattern
The Edda Labs midnight-starter-template (demo at counter.nebula.builders) wires Lace plus 1AM through a copy-pasteable wallet-widget; use it rather than re-deriving:
import { useWallet } from "@/modules/midnight/wallet-widget/hooks/useWallet";
function ConnectButton() {
const { connectWallet, disconnect, status, dustBalance } = useWallet();
if (status?.status === "connected")
return <button onClick={() => disconnect()}>Connected · DUST {String(dustBalance?.balance)}</button>;
return <button onClick={() => connectWallet("mnLace", "preprod")}>Connect Lace</button>;
}
Key files in the repo, under frontend-vite-react/src/modules/midnight/: wallet-widget/api/walletController.ts (discovery, pop-up-safe connect, proof-server check), wallet-widget/hooks/useWallet.ts plus contexts/wallet.tsx (state), wallet-widget/ui/midnightWallet.tsx (connect modal), counter-sdk/ (wiring a connected wallet into a Compact call). (starter template, React guide)