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.
8.7 KiB
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>
/** 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:
- Remote disconnect: the wallet sent a
disconnectmessage (any reason). - Protocol mismatch:
handleWalletReadyfound no overlap between the dapp's and wallet'ssupported_protocols. The dapp automatically sends aProtocolMismatchdisconnect 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:
DappConnectionManageris created per connection session (not a singleton).updateConnection()is called from the relay status callback.getXpubNode(childIndex)andgetPubkey(childIndex, index)are the primary access patterns.- Child indices are used internally (0/1/7);
childIndexOfPathName()converts from PathName when processingwallet_readydata.