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.
249 lines
8.7 KiB
Markdown
249 lines
8.7 KiB
Markdown
# 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>
|
||
|
||
/** 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:
|
||
|
||
```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
|
||
|
||
// 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
|
||
|
||
```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,
|
||
},
|
||
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
|
||
|
||
```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.
|
||
|
||
### 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).
|
||
|
||
### 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.
|