WizardConnect/packages/core/src/protocols/hdwalletv1.ts

170 lines
5.4 KiB
TypeScript
Raw Normal View History

2026-02-26 11:19:47 +01:00
// 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
/// This is a protocol within the relayed messages (which is protocol agnostic).
import { WcSignTransactionRequest } from "@bch-wc2/interfaces";
/// Dapp <-> Wallet messages
export const PROTOCOL_NAME = "hdwalletv1" as const;
/// What this message is about
export enum RelayMsgAction {
/// Notification: Dapp has joined the session and is ready to receive relayed messages.
DappReady = "dapp_ready",
/// Notification: Wallet has joined the session and is ready to send relayed messages.
/// Also carries key exchange data (public_key, secret) embedded in the message.
WalletReady = "wallet_ready",
/// Request: The dapp wants wallet to sign a transaction.
SignTransactionRequest = "sign_transaction_request",
SignTransactionResponse = "sign_transaction_response",
/// Dapp-only: cancels an in-flight sign_transaction_request.
SignCancel = "sign_cancel",
/// Courtesy notification: one side is closing the connection.
Disconnect = "disconnect",
}
export interface ProtocolMessage {
action: RelayMsgAction;
time: number;
}
export type PathName = "receive" | "change" | "defi";
export interface PathXpub {
name: PathName; // "receive" | "change" | "defi"
xpub: string; // BIP32 base58 xpub (wallet chooses the derivation path internally)
}
export function isPathXpub(obj: any): obj is PathXpub {
return (
obj &&
typeof obj === "object" &&
typeof obj.name === "string" &&
typeof obj.xpub === "string"
);
}
/** Returns the numeric BIP44 child index for the given named path.
* Use only for internal HD derivation not a protocol concern. */
export function childIndexOfPathName(name: PathName): number {
switch (name) {
case "receive":
return 0;
case "change":
return 1;
case "defi":
return 7;
}
}
/// Handshake Protocol
///
/// Either side may connect or reconnect at any time (mobile app switch, browser
/// refresh, relay drop). The handshake uses a **mutual-discovery** pattern so
/// both sides converge regardless of who connects first:
///
/// 1. On every connect/reconnect, both sides independently send their "ready"
/// message once key exchange completes.
/// 2. Each "ready" carries a boolean indicating whether the sender has already
/// seen the other party before (in this runtime session).
/// 3. On receiving a "ready" with `_discovered=false`, the receiver sends back
/// their own "ready" — **only if they have not already sent one this cycle**.
/// 4. Once both sides have exchanged wallet_ready/dapp_ready, the wallet sends
/// its xpubs and the dapp derives all needed pubkeys locally.
///
/// INITIAL CONNECT (neither has seen the other):
///
/// Dapp --dapp_ready(supported=["v1"])--> Wallet (proactive)
/// Dapp <--wallet_ready(supported=["v1"], session={v1:{paths,...}})-- Wallet
/// Dapp --dapp_ready(supported=["v1"], selected="hdwalletv1")--> Wallet (reactive)
///
/// WALLET RECONNECTS (dapp still running, has walletDiscovered=true):
///
/// Dapp --dapp_ready(supported=["v1"], wallet_discovered=true)--> Wallet
/// Dapp <--wallet_ready(supported=["v1"], session={...})-- Wallet
/// Dapp --dapp_ready(selected="hdwalletv1", wallet_discovered=true)--> Wallet
///
/// See base.ts for DappReadyMessage / WalletReadyMessage definitions.
/// Session data for the hdwalletv1 protocol.
/// Carried inside WalletReadyMessage.session["hdwalletv1"].
export interface Hdwalletv1Session {
/// BIP32 xpubs for each named derivation path.
paths: PathXpub[];
}
export function isHdwalletv1Session(obj: unknown): obj is Hdwalletv1Session {
return (
obj !== null &&
typeof obj === "object" &&
Array.isArray((obj as Hdwalletv1Session).paths) &&
(obj as Hdwalletv1Session).paths.every((p) => isPathXpub(p))
);
}
export interface ErrorMessage extends ProtocolMessage {
error: string;
}
export interface SignTransactionRequest extends ProtocolMessage {
action: RelayMsgAction.SignTransactionRequest;
transaction: WcSignTransactionRequest;
sequence: number;
}
export interface SignTransactionResponse extends ProtocolMessage {
action: RelayMsgAction.SignTransactionResponse;
sequence: number;
signedTransaction: string;
error?: string;
}
export interface SignCancelMessage extends ProtocolMessage {
action: RelayMsgAction.SignCancel;
sequence: number;
reason?: string;
}
// Type guard functions
export function isProtocolMessage(payload: any): payload is ProtocolMessage {
return (
payload &&
typeof payload === "object" &&
typeof payload.action === "string" &&
typeof payload.time === "number"
);
}
export function isErrorMessage(payload: any): payload is ErrorMessage {
return (
payload && typeof payload === "object" && typeof payload.error === "string"
);
}
export function isSignTransactionRequest(
msg: any,
): msg is SignTransactionRequest {
return (
msg &&
typeof msg === "object" &&
msg.action === RelayMsgAction.SignTransactionRequest &&
msg.transaction &&
typeof msg.transaction === "object" &&
typeof msg.sequence === "number"
);
}
export function isSignCancelMessage(msg: any): msg is SignCancelMessage {
return (
msg &&
typeof msg === "object" &&
msg.action === RelayMsgAction.SignCancel &&
typeof msg.sequence === "number"
);
}