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.
This commit is contained in:
parent
33c45596ef
commit
add06b6476
3 changed files with 173 additions and 1 deletions
34
docs/dapp.md
34
docs/dapp.md
|
|
@ -37,6 +37,12 @@ class DappConnectionManager extends EventEmitter {
|
|||
* 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)
|
||||
|
|
@ -72,6 +78,12 @@ 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
|
||||
|
|
@ -172,6 +184,28 @@ The `disconnect` event fires in two cases:
|
|||
`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"`,
|
||||
|
|
|
|||
108
packages/dapp/src/dapp-connection-manager.test.ts
Normal file
108
packages/dapp/src/dapp-connection-manager.test.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright (C) 2026 Whiterun LLC,
|
||||
// This software is licensed under the GNU Lesser General Public License (LGPL), version 3.0 or later.
|
||||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/lgpl-3.0.html
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { encodeHdPublicKey } from "@bitauth/libauth";
|
||||
import type { PathXpub } from "@wizardconnect/core";
|
||||
import { DappConnectionManager } from "./dapp-connection-manager.js";
|
||||
|
||||
// Build a valid xpub string from a deterministic test node.
|
||||
// Uses the secp256k1 generator point (a known valid public key).
|
||||
function makeTestXpub(): string {
|
||||
const node = {
|
||||
chainCode: new Uint8Array(32).fill(0xbb),
|
||||
// secp256k1 generator point (compressed) — always valid
|
||||
publicKey: Uint8Array.from(
|
||||
"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
||||
.match(/.{2}/g)!
|
||||
.map((h) => parseInt(h, 16)),
|
||||
),
|
||||
childIndex: 0,
|
||||
depth: 3,
|
||||
parentFingerprint: new Uint8Array(4),
|
||||
};
|
||||
const result = encodeHdPublicKey({ node, network: "mainnet" });
|
||||
if (typeof result === "string")
|
||||
throw new Error("Failed to encode test xpub: " + result);
|
||||
return result.hdPublicKey;
|
||||
}
|
||||
|
||||
describe("DappConnectionManager", () => {
|
||||
describe("getSessionPaths", () => {
|
||||
it("returns empty array before wallet_ready", () => {
|
||||
const mgr = new DappConnectionManager();
|
||||
expect(mgr.getSessionPaths()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("restoreSessionPaths", () => {
|
||||
it("restores xpubs so getPubkey works", () => {
|
||||
const mgr = new DappConnectionManager();
|
||||
const xpub = makeTestXpub();
|
||||
const paths: PathXpub[] = [{ name: "receive", xpub }];
|
||||
|
||||
mgr.restoreSessionPaths(paths);
|
||||
|
||||
expect(mgr.hasPath(0)).toBe(true);
|
||||
const pk = mgr.getPubkey(0, 0n);
|
||||
expect(pk).toBeInstanceOf(Uint8Array);
|
||||
expect(pk!.length).toBe(33);
|
||||
});
|
||||
|
||||
it("makes paths available via getSessionPaths", () => {
|
||||
const mgr = new DappConnectionManager();
|
||||
const xpub = makeTestXpub();
|
||||
const paths: PathXpub[] = [
|
||||
{ name: "receive", xpub },
|
||||
{ name: "change", xpub },
|
||||
];
|
||||
|
||||
mgr.restoreSessionPaths(paths);
|
||||
|
||||
const stored = mgr.getSessionPaths();
|
||||
expect(stored).toHaveLength(2);
|
||||
expect(stored[0].name).toBe("receive");
|
||||
expect(stored[1].name).toBe("change");
|
||||
});
|
||||
|
||||
it("returns a copy, not a reference", () => {
|
||||
const mgr = new DappConnectionManager();
|
||||
const xpub = makeTestXpub();
|
||||
mgr.restoreSessionPaths([{ name: "receive", xpub }]);
|
||||
|
||||
const paths = mgr.getSessionPaths();
|
||||
paths.push({ name: "change", xpub });
|
||||
|
||||
expect(mgr.getSessionPaths()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("throws on invalid xpub strings", () => {
|
||||
const mgr = new DappConnectionManager();
|
||||
const paths: PathXpub[] = [{ name: "receive", xpub: "not-a-valid-xpub" }];
|
||||
|
||||
expect(() => mgr.restoreSessionPaths(paths)).toThrow("Invalid xpub");
|
||||
});
|
||||
|
||||
it("can derive multiple child indices after restore", () => {
|
||||
const mgr = new DappConnectionManager();
|
||||
const xpub = makeTestXpub();
|
||||
mgr.restoreSessionPaths([{ name: "receive", xpub }]);
|
||||
|
||||
for (const idx of [0n, 1n, 5n, 10n]) {
|
||||
const pk = mgr.getPubkey(0, idx);
|
||||
expect(pk, `index ${idx}`).toBeInstanceOf(Uint8Array);
|
||||
expect(pk!.length, `index ${idx}`).toBe(33);
|
||||
}
|
||||
});
|
||||
|
||||
it("does not affect paths not in the restore set", () => {
|
||||
const mgr = new DappConnectionManager();
|
||||
const xpub = makeTestXpub();
|
||||
mgr.restoreSessionPaths([{ name: "receive", xpub }]);
|
||||
|
||||
expect(mgr.hasPath(0)).toBe(true); // receive = 0
|
||||
expect(mgr.hasPath(1)).toBe(false); // change = 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -22,6 +22,7 @@ import {
|
|||
childIndexOfPathName,
|
||||
isHdwalletv1Session,
|
||||
} from "@wizardconnect/core";
|
||||
import type { PathXpub } from "@wizardconnect/core";
|
||||
import { DappPubkeyStateManager } from "./pubkey-state-manager.js";
|
||||
|
||||
export interface DappConnectionManagerEvents {
|
||||
|
|
@ -61,6 +62,7 @@ export class DappConnectionManager extends EventEmitter<DappConnectionManagerEve
|
|||
private readonly supportedProtocols: string[] = [PROTOCOL_NAME];
|
||||
|
||||
private walletDiscovered = false;
|
||||
private sessionPaths: PathXpub[] = [];
|
||||
private pendingSignatureRequests = new Map<
|
||||
number,
|
||||
{
|
||||
|
|
@ -199,6 +201,33 @@ export class DappConnectionManager extends EventEmitter<DappConnectionManagerEve
|
|||
return this.pubkeyState.getXpubNode(childIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw PathXpub entries received from the wallet.
|
||||
* Available after wallet_ready is received.
|
||||
*/
|
||||
getSessionPaths(): PathXpub[] {
|
||||
return [...this.sessionPaths];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore session paths from a previous session (e.g. from localStorage).
|
||||
* Decodes xpub strings and populates pubkeyState so getPubkey() works
|
||||
* without waiting for wallet_ready.
|
||||
*/
|
||||
restoreSessionPaths(paths: PathXpub[]): void {
|
||||
this.sessionPaths = [...paths];
|
||||
for (const pathInfo of paths) {
|
||||
const decoded = decodeHdPublicKey(pathInfo.xpub);
|
||||
if (typeof decoded === "string") {
|
||||
throw new Error(
|
||||
`[wizardconnect/dapp] Invalid xpub for path "${pathInfo.name}": ${decoded}`,
|
||||
);
|
||||
}
|
||||
const ci = childIndexOfPathName(pathInfo.name as PathName);
|
||||
this.pubkeyState.setXpubNode(ci, decoded.node);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Private protocol handling -------------------------------------------
|
||||
|
||||
private onConnected(): void {
|
||||
|
|
@ -294,7 +323,8 @@ export class DappConnectionManager extends EventEmitter<DappConnectionManagerEve
|
|||
return;
|
||||
}
|
||||
|
||||
// Store xpub nodes — no eager derivation; consumer drives it
|
||||
// Store raw paths for getSessionPaths() and xpub nodes for derivation
|
||||
this.sessionPaths = [...sessionData.paths];
|
||||
for (const pathInfo of sessionData.paths) {
|
||||
const decoded = decodeHdPublicKey(pathInfo.xpub);
|
||||
if (typeof decoded === "string") {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue