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