// 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" ); }