Help us improve
Share bugs, ideas, or general feedback.
From midnight-dapp-dev
This skill should be used when the user asks about connecting a browser-based DApp to the Midnight Lace wallet extension using the DApp Connector API. Covers the full connection lifecycle (InitialAPI, ConnectedAPI, WalletConnectedAPI), wallet detection via window.midnight, error handling with DAppConnectorAPIError, React 19.x and Next.js 16.x wallet integration patterns, building MidnightProviders from the DApp Connector, FetchZkConfigProvider, balanceUnsealedTransaction, getConfiguration, shielded and unshielded addresses, Lace setup and funding, and wallet-delegated proving.
npx claudepluginhub devrelaicom/midnight-expert --plugin midnight-dapp-devHow this skill is triggered — by the user, by Claude, or both
Slash command
/midnight-dapp-dev:dapp-connectorThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill covers the Midnight DApp Connector API for browser-based wallet integration: connecting to the Lace wallet extension, using the ConnectedAPI for transactions and balances, handling errors, and building React 19.x / Next.js 16.x DApps. For Node.js CLI deployment with WalletFacade and HD keys, see `midnight-dapp-dev:midnight-sdk`. For contract runtime and witness types, see `compact-co...
This skill should be used when building a Midnight DApp frontend, "create a React component for contract interaction", "set up wallet connection", "add a contract state subscription", "configure Vite for Midnight", "write tests for a DApp component", "debug wallet connection", "provider assembly", "transaction flow in the browser", "DApp Connector API", "RxJS observable for contract state", "scaffold a Midnight DApp", "useContractState hook", or working with Midnight SDK packages (@midnight-ntwrk/*) in a Vite + React project.
Verification by source code inspection of the Midnight Wallet SDK repositories. Searches and reads the actual wallet SDK source code to verify claims about wallet packages, the DApp Connector API, HD derivation, address encoding, and the three-wallet architecture. Uses octocode-mcp for quick lookups, falls back to local cloning for deep investigation. Loaded by the source-investigator agent when the claim domain is wallet SDK.
This skill should be used when the user asks about Midnight network architecture, transaction structure, guaranteed vs fallible sections, Zswap/Kachina integration, ledger and state management, cryptographic binding, balance verification, nullifiers, address derivation, transaction merging, atomic swaps, fee handling, or the privacy model separating private and public domains.
Share bugs, ideas, or general feedback.
This skill covers the Midnight DApp Connector API for browser-based wallet integration: connecting to the Lace wallet extension, using the ConnectedAPI for transactions and balances, handling errors, and building React 19.x / Next.js 16.x DApps. For Node.js CLI deployment with WalletFacade and HD keys, see midnight-dapp-dev:midnight-sdk. For contract runtime and witness types, see compact-core:compact-witness-ts. For local development network setup, see midnight-tooling:devnet.
window.midnight.mnLace -> InitialAPI.connect(networkId) -> ConnectedAPI
(injected) (WalletConnectedAPI & HintUsage)
window.midnight?.mnLace for the injected wallet object. For TypeScript window augmentation, see references/connector-api-types.md.connect(networkId) with the target network ("undeployed", "preview", or "preprod")ConnectedAPI for addresses, balances, transfers, and provingThe wallet extension injects an InitialAPI object at window.midnight.{walletId}. For the Lace wallet, the identifier is mnLace:
import type { InitialAPI } from "@midnight-ntwrk/dapp-connector-api";
const wallet: InitialAPI | undefined = window.midnight?.mnLace;
| Property | Type | Description |
|---|---|---|
name | string | Wallet display name |
icon | string | Wallet icon URL |
apiVersion | string | Semver version of the connector API |
rdns | string | Reverse DNS identifier for the wallet |
connect(networkId) | (networkId: string) => Promise<ConnectedAPI> | Initiates the connection handshake |
Calling connect() triggers the Lace authorization prompt. The user chooses "Always" (persistent across sessions) or "Only once" (single-session permission).
ConnectedAPI is the intersection of WalletConnectedAPI and HintUsage:
type ConnectedAPI = WalletConnectedAPI & HintUsage;
interface HintUsage {
hintUsage(methodNames: string[]): void;
}
Call hintUsage to pre-declare which methods the DApp intends to use. This allows the wallet to optimize permission prompts.
| Method | Returns | Purpose |
|---|---|---|
getConfiguration() | Configuration | indexerUri, indexerWsUri, substrateNodeUri, networkId |
getConnectionStatus() | ConnectionStatus | connected or disconnected |
getShieldedAddresses() | {shieldedAddress, shieldedCoinPublicKey, shieldedEncryptionPublicKey} | All Bech32m-encoded |
getUnshieldedAddress() | {unshieldedAddress} | Bech32m-encoded |
getDustAddress() | {dustAddress} | Bech32m-encoded |
getShieldedBalances() | Record<string, bigint> | Token type to balance mapping |
getUnshieldedBalances() | Record<string, bigint> | Token type to balance mapping |
getDustBalance() | {balance, cap} | Current balance and max cap from NIGHT staking |
getTxHistory(page, size) | HistoryEntry[] | txHash, txStatus per entry |
makeTransfer(outputs, options?) | {tx} | Create a balanced transfer transaction |
makeIntent(inputs, outputs, options) | {tx} | Create an unbalanced intent for swaps |
balanceUnsealedTransaction(tx, options?) | {tx} | Balance a transaction from a contract call |
balanceSealedTransaction(tx, options?) | {tx} | Balance a sealed transaction for swap completion |
submitTransaction(tx) | void | Submit a balanced and proven transaction |
signData(data, options) | Signature | Sign arbitrary data with the unshielded key |
getProvingProvider(keyMaterialProvider) | ProvingProvider | Delegate ZK proving to the wallet |
All WalletConnectedAPI methods are async and return
Promise<...>wrappers around the types shown above.
getConfiguration() returns the network endpoints the wallet is connected to:
interface Configuration {
indexerUri: string; // GraphQL HTTP endpoint
indexerWsUri: string; // GraphQL WebSocket endpoint
substrateNodeUri: string; // Substrate node RPC
networkId: string; // "undeployed", "preview", or "preprod"
}
Use these values to configure SDK providers instead of hardcoding endpoints. This respects the user's network selection and is the recommended privacy-preserving approach.
All DApp Connector errors use a discriminated type (not class instances):
interface APIError {
type: "DAppConnectorAPIError";
code: ErrorCode;
reason: string;
}
type ErrorCode =
| "Disconnected"
| "InternalError"
| "InvalidRequest"
| "PermissionRejected"
| "Rejected";
Check errors by type field, not instanceof:
try {
const api = await wallet.connect("preview");
} catch (error: unknown) {
if (
typeof error === "object" &&
error !== null &&
"type" in error &&
(error as APIError).type === "DAppConnectorAPIError"
) {
const apiError = error as APIError;
switch (apiError.code) {
case "PermissionRejected":
console.error("User rejected the connection request");
break;
case "Disconnected":
console.error("Wallet disconnected");
break;
case "InternalError":
console.error("Wallet internal error:", apiError.reason);
break;
case "InvalidRequest":
console.error("Invalid request:", apiError.reason);
break;
case "Rejected":
console.error("Request rejected:", apiError.reason);
break;
}
}
}
Never use instanceof for DApp Connector errors. The error objects are plain objects serialized across the extension boundary, so instanceof checks always fail.
Lace supports three networks:
| Network | Network ID | Use Case |
|---|---|---|
| Undeployed | undeployed | Local development with Docker containers |
| Preview | preview | Public testnet for integration testing |
| Preprod | preprod | Pre-production testing |
Select the network in Lace settings. The "Undeployed" network connects to localhost endpoints matching the local Docker stack.
https://faucet.preview.midnight.network)When a DApp calls connect(), Lace prompts the user:
"use client";
import { useState, useCallback, useEffect } from "react";
import type { InitialAPI, ConnectedAPI } from "@midnight-ntwrk/dapp-connector-api";
interface WalletState {
connectedApi: ConnectedAPI | null;
isConnecting: boolean;
error: string | null;
}
export function useWalletConnection() {
const [state, setState] = useState<WalletState>({
connectedApi: null,
isConnecting: false,
error: null,
});
const connect = useCallback(async (networkId: string) => {
setState((prev) => ({ ...prev, isConnecting: true, error: null }));
const wallet: InitialAPI | undefined = window.midnight?.mnLace;
if (!wallet) {
setState((prev) => ({
...prev,
isConnecting: false,
error: "Lace wallet extension not found. Install it from the Chrome Web Store.",
}));
return;
}
try {
const api = await wallet.connect(networkId);
setState({ connectedApi: api, isConnecting: false, error: null });
} catch (err: unknown) {
const message =
typeof err === "object" && err !== null && "reason" in err
? (err as { reason: string }).reason
: "Failed to connect to wallet";
setState((prev) => ({ ...prev, isConnecting: false, error: message }));
}
}, []);
const disconnect = useCallback(() => {
setState({ connectedApi: null, isConnecting: false, error: null });
}, []);
return { ...state, connect, disconnect };
}
"use client";
import { useState, useEffect } from "react";
import type { ConnectedAPI } from "@midnight-ntwrk/dapp-connector-api";
export function WalletBalances({ api }: { api: ConnectedAPI }) {
const [shielded, setShielded] = useState<Record<string, bigint>>({});
const [unshielded, setUnshielded] = useState<Record<string, bigint>>({});
useEffect(() => {
const fetchBalances = async () => {
const [s, u] = await Promise.all([
api.getShieldedBalances(),
api.getUnshieldedBalances(),
]);
setShielded(s);
setUnshielded(u);
};
fetchBalances();
}, [api]);
return (
<div>
<h3>Shielded</h3>
{Object.entries(shielded).map(([token, balance]) => (
<p key={token}>{token}: {balance.toString()}</p>
))}
<h3>Unshielded</h3>
{Object.entries(unshielded).map(([token, balance]) => (
<p key={token}>{token}: {balance.toString()}</p>
))}
</div>
);
}
The window object is undefined during server-side rendering. All wallet access must be in Client Components:
"use client";
import { useEffect, useState } from "react";
function WalletDetector() {
const [walletAvailable, setWalletAvailable] = useState(false);
useEffect(() => {
// window.midnight is only available in the browser
setWalletAvailable(window.midnight?.mnLace !== undefined);
}, []);
if (!walletAvailable) {
return <p>Install the Lace wallet extension to continue.</p>;
}
return <p>Lace wallet detected.</p>;
}
For modules that reference window at import time, use next/dynamic with SSR disabled:
import dynamic from "next/dynamic";
const WalletPanel = dynamic(() => import("./WalletPanel"), { ssr: false });
Instead of hardcoding network endpoints, read them from the wallet's configuration. This ensures the DApp uses the same network the user selected in Lace:
"use client";
import type { ConnectedAPI } from "@midnight-ntwrk/dapp-connector-api";
import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider";
async function createProvidersFromWallet(api: ConnectedAPI) {
const config = await api.getConfiguration();
const publicDataProvider = indexerPublicDataProvider(
config.indexerUri,
config.indexerWsUri,
);
// Use config.substrateNodeUri for node connections
// Use config.networkId for setNetworkId()
return { publicDataProvider, networkId: config.networkId };
}
In the browser, the DApp Connector replaces the Node.js WalletFacade. The ConnectedAPI provides the wallet and midnight provider capabilities:
import type { WalletProvider, MidnightProvider } from "@midnight-ntwrk/midnight-js-types";
import type { ConnectedAPI } from "@midnight-ntwrk/dapp-connector-api";
async function createWalletProvider(api: ConnectedAPI): Promise<WalletProvider> {
const { shieldedCoinPublicKey, shieldedEncryptionPublicKey } =
await api.getShieldedAddresses();
return {
getCoinPublicKey: () => shieldedCoinPublicKey,
getEncryptionPublicKey: () => shieldedEncryptionPublicKey,
balanceTx: async (tx, newCoins, ttl) => {
const result = await api.balanceUnsealedTransaction(tx, { newCoins, ttl });
return result.tx;
},
};
}
function createMidnightProvider(api: ConnectedAPI): MidnightProvider {
return {
submitTx: async (tx) => {
await api.submitTransaction(tx);
return tx.txId;
},
};
}
For the complete browser provider assembly including FetchZkConfigProvider and in-memory private state, see references/browser-providers.md. For the full type reference of all DApp Connector interfaces, see references/connector-api-types.md.
| Mistake | Fix |
|---|---|
Accessing window.midnight during SSR | Guard with "use client" directive and useEffect or typeof window !== "undefined" checks |
Using instanceof for error checking | Check error.type === "DAppConnectorAPIError" instead |
| Hardcoding network endpoints | Use getConfiguration() to read the wallet's active endpoints |
Not handling PermissionRejected | Users can deny the connection prompt; always handle this error code |
| Forgetting to fund the wallet | Transfer tDUST from faucet and delegate NIGHT for DUST generation before transacting |
| Using Brave without disabling shields | Brave Shields blocks extension injection; disable for the DApp origin |
| Topic | Reference File |
|---|---|
| Complete type definitions for InitialAPI, ConnectedAPI, WalletConnectedAPI, all method signatures, Configuration, ErrorCode | references/connector-api-types.md |
| Full browser provider assembly pattern, FetchZkConfigProvider, in-memory private state, wallet-to-SDK bridge | references/browser-providers.md |