Skip to main content
For the complete documentation index, see llms.txt

Integrate a wallet

Connect a community wallet in a browser DApp through the DApp Connector API (CAIP-372). This page covers discovery, connect, ZK proving, fees, and portable React code. To choose a wallet first, see the overview. For headless or agent flows with no extension, see the CLI and MCP workflow.

ItemDetail
DApp Connector API@midnight-ntwrk/dapp-connector-api v4.0.1
Local proof serverhttp://localhost:6300 (Lace and local networks); 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. The shape is compatible with the draft CAIP-372 (Chain Agnostic Improvement Proposal), so the same DApp code works across wallets. (npm, connector repo and 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>;
};

Do not rely on a fixed key. The v4 specification has each wallet install its Initial API under a freshly generated UUID key and expose a stable rdns field. Current Lace builds do this: the key changes on every page load, they do not set mnLace, and their rdns is io.lace.wallet. 1AM keeps the fixed key window.midnight['1am'], with the rdns com.midnight.1am. Discover wallets by scanning Object.values(window.midnight) and matching on rdns, and let your users choose when more than one wallet is present. Before using a wallet, check its apiVersion against the range your DApp supports. The spec notes event-based discovery, like Ethereum's EIP-6963, as a possible future addition, but the current mechanism is the shared window.midnight object. (React wallet-connect guide)

Connect and the ConnectedAPI surface​

connect(networkId) prompts you to authorize, then resolves to a ConnectedAPI that spans Midnight's three asset kinds (shielded, unshielded, and 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', and '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 or an unbalanced intent
balanceUnsealedTransaction(tx) / balanceSealedTransaction(tx)Balance and pay fees for a contract-built transaction
signData(data, {encoding, keyType})Sign arbitrary data with the unshielded key
submitTransaction(tx)Broadcast a sealed transaction (the wallet acts as relayer)
getProvingProvider(keyMaterialProvider)Delegate ZK proving to the wallet
getConfiguration() / getConnectionStatus() / hintUsage(methodNames)Config; status (with networkId); pre-request hints
Feature-detect, because coverage varies

The connector is a contract, but not every wallet implements every method. Lace 2.4.2, for instance, leaves hintUsage undefined and rejects getTxHistory() calls with a Method not implemented. error (see Lace). Lace builds before 2.1.0 also lack getProvingProvider(). Always check before calling, for example typeof api.getProvingProvider === "function". Catch rejections too, because a wallet can expose a method without implementing it.

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

Lace opens a real authorization pop-up. The browser silently blocks it if the browser lost transient user activation, for example if you awaited something, or used setTimeout or an RxJS interval first. Page-load auto-reconnect has no user gesture, so poll window.midnight until the wallet injects it, then connect. Extensions inject slightly after DOMContentLoaded, and users may need to refresh after installing.

Where ZK proofs come from​

Every Midnight transaction needs a ZK proof. Who generates that proof is the main difference between wallets, and it maps onto the custody models:

  • Local proof server (Lace). Lace requires a local proof server (Settings, then Midnight, then Local, at http://localhost:6300). Witness data stays on your machine, but you must run the server. Lace 2.1.0 and later expose getProvingProvider(), and the provider it returns sends each proof request to that same server.
  • In-browser WASM (1AM). 1AM compiles Midnight's prover, a Halo2-based zk-SNARK over the BLS12-381 curve, into a few MB of WASM and proves in the tab. There is no separate process, though proving time still depends on circuit size and the one-time cold-start key load. 1AM implements getProvingProvider() and offers a hosted Proof Station.
  • Delegated (getProvingProvider). The v4 connector abstracts proving behind this method; the connector deprecates the old Configuration.proverServerUri. Obtain a proving provider from the wallet when it is available, and fall back to a configured proof server when it is not.

Fees and DUST​

You pay fees in DUST, which a wallet generates by holding NIGHT and regenerates over time. getDustBalance() returns { balance, cap }, and submitTransaction uses the wallet as a relayer with a payFees option. A wallet can therefore sponsor fees, but sponsorship is a capability the connector allows, not a guarantee that any given wallet ships. On a fresh wallet, DUST takes time to generate: about 12 hours on Lace mainnet or testnet, and about 5 minutes on a local network. Overspending surfaces as BalanceCheckOverspend (138) (see Troubleshooting).

The portable integration code​

This works for any standard-connector wallet (Lace, 1AM). The CLI connector is a no-extension development fallback.

import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id";
import type { InitialAPI, ConnectedAPI } from "@midnight-ntwrk/dapp-connector-api";

// 1. Discover: scan window.midnight (keys are not stable, so do not hardcode one)
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 or 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, and so does Lace from 2.1.0, where the returned provider sends proof requests to the proof server set in the wallet. If the method is missing, fall back to the local proof server, and do not hard-code the deprecated proverServerUri.
  • signData: Lace implements it and asks your users to confirm each request in a pop-up. As the specification requires, Lace 2.4.0 and later sign midnight_signed_message:<size>: followed by your data, where <size> is the byte length of the decoded data. Verify the signature against those prefixed bytes. Feature-detect before any signature-based authentication on other wallets.
  • Pop-up: Lace opens an auth pop-up, so 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 both { balance: 0n } and the cap, and do not assume sponsored fees.
  • Non-standard tiles: Ctrl and Gero may not inject a conformant connector, so feature-detect and degrade to Lace or 1AM.

React: the production pattern​

The Edda Labs midnight-starter-template (demo at counter.nebula.builders) wires Lace and 1AM through a copy-pasteable wallet widget. Use it rather than re-deriving the logic.

Pass the wallet's rdns to the widget's connectWallet, not its window.midnight key. Current Lace builds install their connector under a new UUID key on every page load and do not set mnLace, while 1AM keeps the fixed key 1am. The widget saves the value you pass and reuses it to reconnect after a reload, so a saved UUID key stops matching. For the discovery rules, see the React wallet-connect guide. This button lists each compatible wallet so your users can choose one:

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>;

// List compatible wallets instead of hardcoding a key, and connect by rdns.
// The widget saves that value, so auto-reconnect still works after a reload.
const wallets = Object.values(window.midnight ?? {}).filter(
(w) => w?.rdns && w?.apiVersion?.startsWith("4."),
);
if (wallets.length === 0) return <button disabled>No Midnight wallet</button>;
return (
<>
{wallets.map((w) => (
<button key={w.rdns} onClick={() => connectWallet(w.rdns, "preprod")}>
Connect {w.name}
</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 and 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)