Skip to main content
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.

ItemDetail
DApp Connector API@midnight-ntwrk/dapp-connector-api v4.0.1
Local proof serverhttp://localhost:6300 (Lace); CLI connector on ws://localhost:9932
Networksmainnet, 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.

MethodPurpose
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
Feature-detect, coverage varies

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.

The #1 connect gotcha: call connect() synchronously inside the click handler

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 expose getProvingProvider(), 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 old Configuration.proverServerUri is 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 deprecated proverServerUri).
  • 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 poll window.midnight then connect.
  • Network: always reconcile the DApp to getConnectionStatus().networkId.
  • DUST: read getDustBalance(), handle { balance: 0n } and the cap; 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)