2026-02-26 11:19:47 +01:00
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
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>
|
|
|
|
|
|
|
2026-03-22 20:59:37 +01:00
|
|
|
|
/** Get raw PathXpub[] received in wallet_ready (for caching). */
|
|
|
|
|
|
getSessionPaths(): PathXpub[]
|
|
|
|
|
|
|
|
|
|
|
|
/** Restore cached xpub paths — enables getPubkey() without wallet_ready. */
|
|
|
|
|
|
restoreSessionPaths(paths: PathXpub[]): void
|
|
|
|
|
|
|
2026-02-26 11:19:47 +01:00
|
|
|
|
// 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:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 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
|
2026-03-22 20:59:37 +01:00
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-02-26 11:19:47 +01:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
dappMgr.on("walletready", (msg) => {
|
|
|
|
|
|
console.log("Wallet:", msg.wallet_name);
|
|
|
|
|
|
// pubkeyState is now populated with xpub nodes.
|
|
|
|
|
|
// You can start deriving addresses.
|
|
|
|
|
|
});
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Deriving addresses
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Get the first receive address pubkey:
|
|
|
|
|
|
const RECEIVE = 0;
|
|
|
|
|
|
const pubkey = dappMgr.getPubkey(RECEIVE, 0n); // derives from xpub if needed
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
See [pubkey-derivation.md](pubkey-derivation.md) for full details.
|
|
|
|
|
|
|
|
|
|
|
|
### Sending a sign request
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
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,
|
|
|
|
|
|
},
|
2026-03-16 16:53:56 +01:00
|
|
|
|
inputPaths: [[0, "receive", 0], [1, "defi", 5]], // [inputIndex, pathName, addressIndex]
|
2026-02-26 11:19:47 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 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.
|
|
|
|
|
|
|
2026-03-22 20:59:37 +01:00
|
|
|
|
### 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:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 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).
|
|
|
|
|
|
|
2026-02-26 11:19:47 +01:00
|
|
|
|
### 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:
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
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.
|