WizardConnect/docs/dapp.md
Dagur Valberg Johannsson add06b6476
Add getSessionPaths() and restoreSessionPaths() to DappConnectionManager
Allows dapps to cache the raw PathXpub[] from wallet_ready and restore
them on subsequent page loads. restoreSessionPaths decodes the xpub strings
and populates pubkeyState so getPubkey() works without waiting for the
wallet to reconnect. Throws on invalid xpub data.
2026-03-23 08:26:12 +01:00

8.7 KiB
Raw Blame History

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 dappwallet 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)

  /** Call from the RelayStatusCallback each time the relay status changes.
   *  Attaches the message listener exactly once (on first client seen).
   *  Triggers onConnected() on each "connected" event. */
  updateConnection(client: RelayClient | null, status: RelayStatus): void

  isWalletDiscovered(): boolean

  /** Get the next sequence number for a SignTransactionRequest. */
  nextSequence(): number

  /** Send a sign request and wait for the wallet's response.
   *  Rejects if the wallet returns an error or if the connection drops. */
  sendSignRequest(request: SignTransactionRequest): Promise<SignTransactionResponse>

  /** Send a UserDisconnect courtesy message to the wallet.
   *  Caller is responsible for calling dappRelay.cleanup() afterwards. */
  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

  // Events
  on("walletready",     (msg: WalletReadyMessage)                                    => void)
  on("messagesent",     (msg: ProtocolMessage)                                       => void)
  on("messagereceived", (msg: ProtocolMessage)                                       => void)
  on("disconnect",      (reason: DisconnectReason, message: string | undefined)      => void)
}

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.

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);
    // also update your own UI state here (connected/disconnected indicator)
  },
  { explicitRelayUrls: ["wss://relay.cauldron.quest:443"] },
);

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

const seq = dappMgr.nextSequence();
const request: SignTransactionRequest = {
  action: RelayMsgAction.SignTransactionRequest,
  sequence: seq,
  time: Math.floor(Date.now() / 1000),
  transaction: {
    transaction: { inputs, outputs, version: 2, locktime: 0 },
    sourceOutputs,
    userPrompt: "Confirm swap",
    broadcast: true,
  },
  inputPaths: [[0, "receive", 0], [1, "defi", 5]],  // [inputIndex, pathName, addressIndex]
};

try {
  const response = await dappMgr.sendSignRequest(request);
  console.log("Signed tx:", response.signedTransaction);
} catch (err) {
  console.error("Signing failed:", err.message);
}

sendSignRequest returns a Promise that resolves when the wallet sends back a sign_transaction_response with the matching sequence. It rejects if the wallet sends an error response.

Disconnecting

// Dapp-initiated: send courtesy message, then tear down the relay
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 path persistence

After wallet_ready, the manager stores the raw PathXpub[] from the wallet. Use getSessionPaths() to retrieve them (e.g. for caching in localStorage). On a subsequent page load, restore them with restoreSessionPaths() so getPubkey() works immediately without waiting for the wallet to reconnect:

// After wallet_ready — save for later
const paths = dappMgr.getSessionPaths();
localStorage.setItem("myapp-paths", JSON.stringify(paths));

// On page load — restore before wallet reconnects
const cached = JSON.parse(localStorage.getItem("myapp-paths") ?? "null");
if (cached) {
  dappMgr.restoreSessionPaths(cached);
  // getPubkey() now works without wallet_ready
}

Throws if any xpub string is invalid (corrupt cached data should be cleared).

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.

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, ... });
});

Cauldron (cauldron-beta) implementation notes

Cauldron uses DappConnectionManager via a vendored adapter in src/relay/RelayWalletDapp.ts. This adapter wraps DappConnectionManager to implement Cauldron's internal Wallet interface.

Key design points:

  • DappConnectionManager is created per connection session (not a singleton).
  • updateConnection() is called from the relay status callback.
  • getXpubNode(childIndex) and getPubkey(childIndex, index) are the primary access patterns.
  • Child indices are used internally (0/1/7); childIndexOfPathName() converts from PathName when processing wallet_ready data.