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;
|
2026-03-16 16:53:56 +01:00
|
|
|
inputPaths: [number, PathName, number][]; // [inputIndex, pathName, addressIndex]
|
2026-02-26 11:19:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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" &&
|
2026-03-16 15:17:37 +01:00
|
|
|
typeof msg.sequence === "number" &&
|
|
|
|
|
Array.isArray(msg.inputPaths) &&
|
|
|
|
|
msg.inputPaths.every(
|
|
|
|
|
(p: any) =>
|
|
|
|
|
Array.isArray(p) &&
|
2026-03-16 16:53:56 +01:00
|
|
|
p.length === 3 &&
|
|
|
|
|
typeof p[0] === "number" &&
|
|
|
|
|
typeof p[1] === "string" &&
|
|
|
|
|
typeof p[2] === "number",
|
2026-03-16 15:17:37 +01:00
|
|
|
)
|
2026-02-26 11:19:47 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isSignCancelMessage(msg: any): msg is SignCancelMessage {
|
|
|
|
|
return (
|
|
|
|
|
msg &&
|
|
|
|
|
typeof msg === "object" &&
|
|
|
|
|
msg.action === RelayMsgAction.SignCancel &&
|
|
|
|
|
typeof msg.sequence === "number"
|
|
|
|
|
);
|
|
|
|
|
}
|