Dapp integration
Dapp integration uses @wizardconnect/dapp (for session management and pubkey state) together
with @wizardconnect/core (for the relay connection and URI generation).
DappConnectionManager
Manages a single dapp–wallet session. Handles the handshake, xpub storage, and sign request round-trips. Most dapps only ever have one active session at a time.
class DappConnectionManager extends EventEmitter {
readonly pubkeyState: DappPubkeyStateManager;
walletName: string | null;
walletIcon: string | null;
/** The agreed protocol name after handshake, e.g. "hdwalletv1". Null until wallet_ready. */
protocol: string | null;
constructor(dappName?: string, dappIcon?: string, options?: {
/** Session persistence config. Enabled by default (key: "wizardconnect-session",
* storage: localStorage). Pass `false` to disable. */
session?: DappSessionOptions | false;
})
/** Call from the RelayStatusCallback each time the relay status changes. */
updateConnection(client: RelayClient | null, status: RelayStatus): void
isWalletDiscovered(): boolean
/** Convenience: build a full SignTransactionRequest, send it, and optionally
* cancel via AbortSignal. See "Sending a sign request" below. */
signTransaction(
request: Pick<SignTransactionRequest, "transaction" | "inputPaths">,
options?: { signal?: AbortSignal },
): Promise<SignTransactionResponse>
/** Low-level: send a fully constructed sign request. */
sendSignRequest(request: SignTransactionRequest): Promise<SignTransactionResponse>
/** Cancel an in-flight sign request by sequence number. */
sendSignCancel(sequence: number, reason?: string): Promise<void>
/** Get the next sequence number (for manual request construction). */
nextSequence(): number
/** Send a UserDisconnect courtesy message to the wallet. */
sendDisconnect(message?: string): Promise<void>
/** Get raw PathXpub[] received in wallet_ready (for caching). */
getSessionPaths(): PathXpub[]
/** Restore cached xpub paths — enables getPubkey() without wallet_ready. */
restoreSessionPaths(paths: PathXpub[]): void
// Session persistence (see "Session persistence" section below)
attachRelay(relay: DappRelayResult): void
loadStoredSession(): StoredSession | null
clearStoredSession(): void
// Events
on("walletready", (msg: WalletReadyMessage) => void)
on("messagesent", (msg: ProtocolMessage) => void)
on("messagereceived", (msg: ProtocolMessage) => void)
on("disconnect", (reason: DisconnectReason, message: string | undefined) => void)
}
The "messagereceived" event fires for all protocol messages, including extension-defined
actions. Use it to handle custom messages from wallet extensions. See
extensions.md for the extension system and graceful degradation patterns.
Pubkey state — convenience delegation
DappConnectionManager delegates to pubkeyState for all pubkey operations. These methods
are also available directly on the manager:
// Get a pubkey (derives on demand from xpub if not cached)
getPubkey(childIndex: number, index: bigint): Uint8Array | undefined
// Get all cached pubkeys for a path
getPubkeys(childIndex: number): Map<bigint, Uint8Array>
// Get/set the current address index for a path
getAddressIndex(childIndex: number): bigint
setAddressIndex(childIndex: number, index: bigint): void
// Get a smart "next index to use" (see pubkey-derivation.md)
getIndexToUse(childIndex: number, options?: { index?: bigint; reuseLast?: boolean }): bigint
// Get the min/max indices seen for a path
getIndexRange(childIndex: number): { min?: bigint; max?: bigint }
// Remove a used change address from the gap-fill queue
removeFromChangeQueue(index: bigint): void
// Get the stored xpub node (after wallet_ready)
getXpubNode(childIndex: number): HdPublicNodeValid | undefined
// Get raw PathXpub[] from wallet_ready (for caching)
getSessionPaths(): PathXpub[]
// Restore cached PathXpub[] — populates pubkeyState so getPubkey works without wallet_ready
restoreSessionPaths(paths: PathXpub[]): void
Child index values: 0 = receive, 1 = change, 7 = defi (Cauldron). These are
internal to the dapp layer; use childIndexOfPathName() to convert from PathName if needed.
It returns undefined for extension path names — callers should skip those.
Session lifecycle
Initial connect
import { initiateDappRelay } from "@wizardconnect/core";
import { DappConnectionManager } from "@wizardconnect/dapp";
const dappMgr = new DappConnectionManager("My Dapp", "https://example.com/icon.png");
const relay = initiateDappRelay(
(payload) => {
dappMgr.updateConnection(payload.client, payload.status);
},
);
// Persist relay credentials and auto-save walletPublicKey on key exchange
dappMgr.attachRelay(relay);
// Show relay.uri as a QR code for the wallet to scan.
console.log("Scan this URI:", relay.uri);
After wallet connects
dappMgr.on("walletready", (msg) => {
console.log("Wallet:", msg.wallet_name);
// pubkeyState is now populated with xpub nodes.
// You can start deriving addresses.
});
Deriving addresses
// Get the first receive address pubkey:
const RECEIVE = 0;
const pubkey = dappMgr.getPubkey(RECEIVE, 0n); // derives from xpub if needed
See pubkey-derivation.md for full details.
Sending a sign request
The signTransaction convenience method auto-fills action, sequence, and time:
const response = await dappMgr.signTransaction({
transaction: {
transaction: txHex,
sourceOutputs,
userPrompt: "Confirm swap",
broadcast: true,
},
inputPaths: [[0, "receive", 0], [1, "defi", 5]], // [inputIndex, pathName, addressIndex]
});
console.log("Signed tx:", response.signedTransaction);
Cancellation via AbortSignal
Pass an AbortSignal to automatically cancel the request when aborted. This sends
sign_cancel to the wallet and rejects the promise with an AbortError:
const controller = new AbortController();
cancelButton.onclick = () => controller.abort("User cancelled");
try {
const response = await dappMgr.signTransaction(
{ transaction: { ... }, inputPaths: [...] },
{ signal: controller.signal },
);
} catch (err) {
if (err.name === "AbortError") {
console.log("User cancelled the signature request");
}
}
Low-level: sendSignRequest
For full control over the request, use sendSignRequest directly:
const seq = dappMgr.nextSequence();
const request: SignTransactionRequest = {
action: RelayMsgAction.SignTransactionRequest,
sequence: seq,
time: Math.floor(Date.now() / 1000),
transaction: { ... },
inputPaths: [[0, "receive", 0]],
};
const response = await dappMgr.sendSignRequest(request);
// Cancel with: await dappMgr.sendSignCancel(seq, "reason");
Disconnecting
// Dapp-initiated: send courtesy message, clear session, then tear down the relay
dappMgr.clearStoredSession();
await dappMgr.sendDisconnect("user closed the tab");
relay.cleanup();
// Listen for wallet-initiated disconnect or protocol mismatch:
dappMgr.on("disconnect", (reason, message) => {
if (reason === DisconnectReason.ProtocolMismatch) {
console.error("Protocol mismatch:", message);
} else {
console.log("Wallet disconnected:", reason, message);
}
relay.cleanup();
});
The disconnect event fires in two cases:
1. Remote disconnect: the wallet sent a disconnect message (any reason).
2. Protocol mismatch: handleWalletReady found no overlap between the dapp's and wallet's
supported_protocols. The dapp automatically sends a ProtocolMismatch disconnect to the
wallet before emitting the event.
Session persistence
Session persistence is enabled by default. The manager automatically:
- On construction: restores xpub paths from storage so
getPubkey()works immediately. - On
walletready: saveswalletName,walletIcon, and xpubpathsto storage.
The default storage key is "wizardconnect-session" and the default backend is localStorage.
Saving session data
Call attachRelay() after initiateDappRelay() to persist relay credentials and
automatically save the wallet public key when key exchange completes:
const relay = initiateDappRelay(callback);
dappMgr.attachRelay(relay); // saves credentials + auto-saves walletPublicKey
// On disconnect:
dappMgr.clearStoredSession();
Loading for reconnection
Use loadStoredSession() on an existing manager, or the standalone loadSession() when
you need to read the session before creating the manager (e.g. to get relay credentials
for initiateDappRelay):
import { loadSession } from "@wizardconnect/dapp";
const session = loadSession(); // uses default key and localStorage
if (session?.walletPublicKey) {
const relay = initiateDappRelay(callback, { existingCredentials: session });
}
Configuration
// Default: session enabled, key "wizardconnect-session", localStorage
const mgr = new DappConnectionManager("My Dapp");
// Custom key:
const mgr = new DappConnectionManager("My Dapp", undefined, {
session: { key: "my-app-session" },
});
// Custom storage backend (e.g. for React Native or SSR):
const mgr = new DappConnectionManager("My Dapp", undefined, {
session: { storage: myCustomStorage },
});
// Disable session persistence:
const mgr = new DappConnectionManager("My Dapp", undefined, {
session: false,
});
The SessionStorage interface matches the Web Storage API:
interface SessionStorage {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}
Manual path management
For advanced use cases, getSessionPaths() and restoreSessionPaths() are still available:
const paths = dappMgr.getSessionPaths(); // raw PathXpub[] from wallet_ready
dappMgr.restoreSessionPaths(paths); // re-populate pubkeyState from cached paths
restoreSessionPaths throws if any xpub string is invalid.
Reconnection
updateConnection() is called on every relay status change. When status.status === "connected",
it calls onConnected() which waits for key exchange and then sends a fresh dapp_ready. The
walletDiscovered flag carries over reconnects (it is only reset by creating a new manager),
so the correct wallet_discovered value is sent on each reconnect.
When wallet_ready is received and there are pending (unresponded) sign requests, the manager
automatically re-sends them. This handles the case where the user triggers a signature in the
dapp before the wallet app is open — the relay's time filter would otherwise discard the
original request. Dapps do not need to handle this manually; sendSignRequest promises remain
valid across reconnects.
Using initiateDappRelay without DappConnectionManager
If you need lower-level control (e.g., in the test-cli), you can work directly with the
RelayClient and handle wallet_ready manually:
const relay = initiateDappRelay(statusCallback, options);
relay.events.on("keyexchangecomplete", async (walletPubkey) => {
// Key exchange done — wait for relay client to be fully ready
while (!relay.client.isKeyExchangeComplete()) {
await sleep(50);
}
relay.client.on("message", (msg) => {
if (isDappReadyMessage(msg)) { /* ... */ }
if (isWalletReadyMessage(msg)) { /* ... */ }
});
await relay.client.relay({ action: RelayMsgAction.DappReady, ... });
});
Using with React
For React dapps, prefer the useWizardConnect hook from @wizardconnect/react over
managing the relay lifecycle manually. The hook handles session persistence, auto-reconnect,
and relay cleanup automatically. See react.md.
Dapps that need a custom wallet adapter (e.g. Cauldron, Moria) can use the hook and wrap
the returned manager in their adapter:
const wc = useWizardConnect({ dappName: "My Dapp" });
useEffect(() => {
if (!wc.manager) return;
const wallet = new MyWalletAdapter(wc.manager);
// dispatch wallet to your store
}, [wc.manager]);
The DappConnectionManager is created by the hook; the adapter receives it rather than
creating its own.