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.
| Item | Detail |
|---|---|
| DApp Connector API | @midnight-ntwrk/dapp-connector-api v4.0.1 |
| Local proof server | http://localhost:6300 (Lace and local networks); 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. 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.
| 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 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 |
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.
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 exposegetProvingProvider(), 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 oldConfiguration.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 deprecatedproverServerUri. 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 signmidnight_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 pollwindow.midnight, then connect. - Network: always reconcile the DApp to
getConnectionStatus().networkId. - DUST: read
getDustBalance(), handle both{ balance: 0n }and thecap, 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 checkwallet-widget/hooks/useWallet.tsandcontexts/wallet.tsx: statewallet-widget/ui/midnightWallet.tsx: connect modalcounter-sdk/: wiring a connected wallet into a Compact call