Skip to content

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: saves walletName, walletIcon, and xpub paths to 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.