Q&A: Make a detailed comparison of the three main ways of connecting a wallet to a da...

Block answers a tech-support question about Make a detailed comparison of the three main ways of connecting a wallet to a dapp: WalletConnectV2 ...

Question:

Make a detailed comparison of the three main ways of connecting a wallet to a dapp: WalletConnectV2 (bch-wc2 by mainnet-pat), Wizard Connect (by Riften labs) and CashConnect.

Go as further as possible, comparing the APIs surfaces of each one.


Answer:

I’ve gathered the primary sources (specs, repos, TypeScript declaration files, and docs) for all three. Here’s the detailed comparison.


Connecting a wallet to a BCH dapp: WalletConnectV2 vs. WizardConnect vs. CashConnect

1. The 30-second summary

WalletConnect V2 (bch-wc2) WizardConnect CashConnect
Author mainnet-pat Riften Labs / Whiterun LLC Jim Hamill (jimtendo)
Transport WalletConnect v2 relay (relay.walletconnect.com), needs a projectId Nostr relays (NIP-17 gift wrap), any relay, no projectId WalletConnect v2 relay (inherits WC transport)
What’s on the wire bch_* RPC methods (BCH-specific payloads over generic WC) hdwalletv1 protocol messages over encrypted relay CashRPC:0 methods (LibAuth-template payloads)
Address model Single address (deliberately) HD wallet (xpubs; derives unlimited addresses locally) Sandboxed keypair per domain
Message signing ✅ bch_signMessage (Electron Cash compatible) ⚠️ Not in core (tx signing only; use WC2 or a custom extension) ✅ bch_signMessage_V0
Tx signing ✅ bch_signTransaction ✅ sign_transaction_request (reuses WC2 tx object + inputPaths) ✅ bch_signTransaction_V0 (template-based)
Maturity Production Production Pre-alpha / evolving
Representative wallets Cashonize, Paytaca, Zapit Selene, Paytaca (Cashonize added recently) Cashonize

Primary sources: bch-wc2 README, WizardConnect docs, cashconnect SDK on npm/GitLab, CashRPC thread, CashScript wallet-connection guide.


2. Architecture & transport layer

WalletConnect V2 (bch-wc2)

The spec (mainnet-pat/wc2-bch-bcr, implementation monorepo mainnet-pat/bch-wc2) is deliberately thin: it reuses the generic WalletConnect v2 transport and only defines BCH-specific message contents. The dapp uses @walletconnect/sign-client:

import Client from "@walletconnect/sign-client";
const client = await Client.init({
  logger: "debug",
  relayUrl: "wss://relay.walletconnect.com",
  projectId: "…", // required — WalletConnect/Reown Cloud
  metadata: { name: "TapSwap.cash", description: "…", url: "…", icons: ["…"] },
});

Because it rides on WalletConnect Cloud, it needs a projectId and depends on their relay infrastructure.

WizardConnect

From the WizardConnect docs: it is a “protocol and library for connecting dapps to HD cryptocurrency wallets over an encrypted relay.” Transport is Nostr:

App message (hdwalletv1 JSON)
  → NIP-17 gift wrap (kind 1059)
  → Nostr relay over WebSocket (default wss://relay.riften.net:443, secondary wss://relay.cauldron.quest:443)

Every relay message is end-to-end encrypted with NIP-44 inside a NIP-17 gift wrap; the relay only sees a kind-1059 event addressed to the recipient’s Nostr pubkey. There is no WizardConnect-operated server in the critical path — any standard Nostr relay works. NDK/nostr-tools handle encryption, reconnection (heartbeat every 29 s, 5 s reconnect loop), replay protection (time filtering) and a chunk extension for messages over NIP-44’s 65,535-byte ceiling.

CashConnect

CashConnect = CashRPC + WalletConnect. It is built on @walletconnect/sign-client + @reown/walletkit, so it shares the WalletConnect transport (and its projectId/relay dependency). The key idea (from the CashRPC thread) is that CashRPC is transport-agnostic:

CashRPC + WalletConnect = CashConnect
CashRPC + HTTP = CashPaymentProto
CashRPC + LibP2P = ???
CashRPC + Intra-Wallet-Use = CashRPC

It’s essentially what the author called Pat’s WC2 “+ more” — an RPC layer that adds capabilities WC2 lacked (UTXO selection, template-scoped signing, token whitelisting).


3. Pairing & session flow

WalletConnect V2

Standard WC2 pairing. The dapp declares a bch namespace in CAIP-2 form and gets back a uri + async approval():

const requiredNamespaces = {
  bch: {
    chains:  ["bch:bitcoincash"],           // bch:bchtest, bch:bchreg also defined
    methods: ["bch_getAddresses", "bch_signTransaction", "bch_signMessage"],
    events:  ["addressesChanged"],
  },
};
const { uri, approval } = await client.connect({ requiredNamespaces });
// show `uri` as QR; wallet scans it
const session = await approval();            // then message via session.topic

WizardConnect

The dapp produces a wiz:// URI that the wallet scans (reverse of WC’s “wallet produces, dapp scans” convention):

wiz://[hostname[:port]]?p=<bech32 dapp Nostr pubkey>&s=<8-byte secret>&pr=wss
  • p = dapp’s x-only secp256k1 (Nostr) pubkey, 32 bytes
  • s = 8-byte shared secret for MITM prevention
  • relay host/port/pr optional (defaults omitted to keep QR compact; a QR-alphanumeric-safe WIZ://%3F... form is also emitted)

The handshake is mutual discovery (reconnection-proof): both sides send their “ready” message proactively and converge on a live session regardless of who reconnects first. Protocol versioning is negotiated via supported_protocols lists.

CashConnect

Also WC2 pairing, via CashConnectDapp:

const cashConnect = new CashConnectDapp(options, chain, dialogs);
await cashConnect.start();
await cashConnect.connect(requiredNamespaces);  // or newSession() / reconnectSession()

Its namespace declares protocol: 'CashRPC:0' plus a template (LibAuth WalletTemplate) and an allowedTokens array. The wallet-side URI scheme is cc: (and web+cc: for PWAs) — deliberately different from Pat’s WC handler.


4. Dapp-side API surface

WalletConnect V2 — the IConnector interface

The canonical interface used by WC2 and Paytaca Connect (this is the “WalletConnect + Paytaca Connector interface” people refer to):

export interface IConnector {
  address(): Promise<string | undefined>;
  signTransaction(options: WcSignTransactionRequest): Promise<WcSignTransactionResponse | undefined>;
  signMessage(options: WcSignMessageRequest): Promise<WcSignMessageResponse | undefined>;
  connect(): Promise<void>;
  connected(): Promise<boolean>;
  disconnect(): Promise<void>;
  on(event: string, callback: Function): void;
  on(event: "addressChanged", callback: Function): void;
  on(event: "disconnect", callback: Function): void;
}

Raw requests are the same calls prefixed with bch_ (e.g. bch_getAddresses). Proposed-but-not-required extensions: bch_sendTransaction (simplified send) and bch_batchSignTransaction (sign several tx in one prompt).

WizardConnect — DappConnectionManager

class DappConnectionManager extends EventEmitter {
  readonly pubkeyState: DappPubkeyStateManager;
  walletName: string | null; walletIcon: string | null; protocol: string | null;

  updateConnection(client, status): void;
  isWalletDiscovered(): boolean;

  signTransaction(request, options?: { signal?: AbortSignal }): Promise<SignTransactionResponse>;
  sendSignRequest(request): Promise<SignTransactionResponse>;
  sendSignCancel(sequence, reason?): Promise<void>;
  nextSequence(): number;
  sendDisconnect(message?): Promise<void>;

  getSessionPaths(): PathXpub[];
  restoreSessionPaths(paths): void;
  attachRelay(relay): void;
  loadStoredSession(): StoredSession | null;
  clearStoredSession(): void;

  // events: "walletready" | "messagesent" | "messagereceived" | "disconnect"

  // pubkey delegation
  getPubkey(childIndex, index): Uint8Array | undefined;
  getPubkeys(childIndex): Map<bigint, Uint8Array>;
  getAddressIndex(childIndex) / setAddressIndex(childIndex, index);
  getIndexToUse(childIndex, options?);
  getIndexRange(childIndex);
  removeFromChangeQueue(index);
  getXpubNode(childIndex);
}

Setup uses initiateDappRelay() (returns { client, uri, credentials, events, cleanup }); a React layer (@wizardconnect/react) provides a useWizardConnect hook and QR dialog. Session persistence defaults to localStorage key wizardconnect-session.

CashConnect — CashConnectDapp

class CashConnectDapp extends EventEmitter {
  readonly options: SignClientTypes.Options;
  readonly chain: string;
  readonly dialogs: DappDialogs;   // newSession / reconnectSession / request dialogs
  client: Client; session?: SessionTypes.Struct;

  start(): Promise<void>;
  connect(requiredNamespaces): Promise<boolean>;
  newSession(requiredNamespaces): Promise<"CONNECTED" | "CANCELLED">;
  reconnectSession(requiredNamespaces): Promise<"CONNECTED" | "CANCELLED" | "NEW_SESSION">;
  disconnect(): Promise<void>;

  request<T extends RpcRequestResponse>(method, params?, opts?: RequestOpts): Promise<T['response']>;
  authRequest(opts): Promise<CacaoAuth>;   // CAIP-122-style CACAO login
  disconnectInactiveSessions(): Promise<void>;
  onSessionDisconnect() / onSessionUpdate() / onSessionEvent(event): Promise<void>;
}

RequestOpts = { hide?: true; message?: string }.


5. Wallet-side API surface

Wallet integration entry point
WC2 No packaged wallet class; wallets implement the IConnector-equivalent natively (Cashonize, Paytaca, Zapit each wire up the bch_* handlers themselves).
WizardConnect WalletConnectionManager + a WalletAdapter interface; set up with initiateWalletRelay({ uri, walletPrivateKey }). Handles multi-connection management and sign-request queuing; dedupes by sequence; emits signCancelled, remoteDisconnect.
CashConnect CashConnectWallet class (see below).

CashConnectWallet (from the published cashconnect .d.ts):

class CashConnectWallet {
  constructor(
    privateKey: Uint8Array,
    projectId: string,
    metadata: WalletKitTypes.Metadata,
    eventCallbacks: EventCallbacks,
    cashRpcOpts: Required<Pick<CashRPCV0Opts, 'wallet' | 'network'>>,
  );
  start(): Promise<void>;
  stop(): Promise<void>;
  pair(url: string): Promise<PairingTypes.Struct>;
  getActiveSessions(): Record<string, BchSession>;
  disconnectSession(topic): Promise<void>;
  disconnectAllSessions(): Promise<void>;
  walletStateHasChanged(chainId?): Promise<void>;
  onSessionProposal(proposalEvent) / onSessionDelete(...) / onSessionRequest(...): Promise<void>;
  static checkMethodIsAllowed(session, method): void;
  static deleteAccount(privateKey): Promise<void>;
}

Wallet callbacks the library expects:

interface EventCallbacks {
  onSessionsUpdated(sessions): Promisable<void>;
  onSessionProposal(proposalEvent): Promisable<WalletProperties>;  // return { autoApprove: string[] }
  onSessionDelete(): Promisable<void>;
  onRPCRequest(session, request, response): Promisable<void>;
  onError(error): Promisable<void>;
}
interface NetworkCallbacks { getSourceOutput(txHash, index): Promise<Output>; }
interface WalletCallbacks {
  getChangeTemplate(): Promisable<ChangeTemplate>;
  getUnspents(): Promisable<Array<Unspent>>;
}

That’s the whole integration — the CashRPC library does the rest. (Cashonize’s original integration was ~150 LoC excluding UI.)


6. Method / message names on the wire

WalletConnect V2 (CAIP-2 namespace bch:*):

  • bch_getAddresses
  • bch_signTransaction
  • bch_signMessage
  • events: addressesChanged
  • proposed: bch_sendTransaction, bch_batchSignTransaction

WizardConnect protocol actions:

  • dapp_ready, wallet_ready (handshake + key exchange + session data)
  • sign_transaction_request, sign_transaction_response, sign_cancel
  • disconnect
  • chunk (transport-level, large-message splitting)
  • Extensions may add actions like decrypt_request / decrypt_response

CashConnect / CashRPC v0:

  • bch_getBalance_V0
  • bch_getChangeLockingBytecode_V0
  • bch_getTokens_V0
  • bch_signMessage_V0
  • bch_signTransaction_V0
  • wc_authRequest (CACAO / CAIP-122-style authentication)
  • namespace protocol string: CashRPC:0

(An early draft used a single bch_executeAction method; the shipped library exposes the _V0 set above, and “actions” moved into the LibAuth template.)


7. Transaction data model

WC2 & WizardConnect share the same tx object

WizardConnect reuses the WC2 transaction format (WcSignTransactionRequest from @bch-wc2/interfaces):

interface WcSignTransactionRequest {
  transaction: Transaction | string;   // libauth tx (stringified via libauth `stringify`)
  sourceOutputs: WcSourceOutput[];      // each = Input & Output & { contract?: {...} }
  broadcast?: boolean;
  userPrompt?: string;
}
interface WcSignTransactionResponse { signedTransaction: string; signedTransactionHash: string; }
type WcSignMessageResponse = string;    // base64, "\x18Bitcoin Signed Message:\n" prefix

sourceOutputs[].contract can carry { abiFunction, redeemScript, artifact } so the wallet can render CashScript contract details. Wallet fills placeholders: a 33-byte zero array signals “insert my pubkey”, a 65-byte zero array signals “insert my schnorr signature” (CashScript’s placeholderPublicKey() / placeholderSignature()).

WizardConnect’s only addition: inputPaths

interface SignTransactionRequest {
  transaction: WcSignTransactionRequest;
  sequence: number;
  inputPaths: [inputIndex, pathName, addressIndex][]; // e.g. [0, "receive", 5]
}

Each entry tells the wallet which HD key signs input n (pathName ∈ receive/change/defi, or extension names). WizardConnect mandates SIGHASH_ALL | SIGHASH_FORKID | SIGHASH_UTXOS and wallets must reject other flags — because with inputPaths the wallet no longer independently checks the key against the UTXO’s locking bytecode, SIGHASH_ALL is what prevents output/input substitution.

CashConnect: template-scoped, not raw bytecode

CashConnect never lets a dapp submit raw locking/unlocking bytecode. Instead:

interface SignTransactionV0Params {
  transaction: {
    version?: number; locktime?: number;
    inputs?:  Array<InputTemplateScoped | InputWalletScoped>;
    outputs?: Array<OutputTemplateScoped | OutputWalletScoped>;
  };
  signerKey?: string;
  userPrompt?: string;
  validate?: boolean;
}
  • Template-scoped inputs/outputs reference a script (+ optional data) in the session’s LibAuth template.
  • Wallet-scoped inputs must contain a whitelisted token category; wallet-scoped outputs are change (wallet supplies the lockscript).
  • The wallet performs its own UTXO selection and appends change to satisfy the template’s required shapes.
  • CashRPC_V0 also offers helper/validation methods: provideData, parseProvidedInputs, parseProvidedOutputs, getSourceOutput, doesUnspentContainAllowedToken, doesPayloadLookMalicious.

8. HD-wallet support (the biggest architectural fork)

  • WC2 = single address by design. The README’s Preamble explains the deliberate decision: connecting an entire HD wallet would expose the seed’s xpub-equivalent and multiply the number of signatures produced by each key, so the implementation “is intentionally narrowed down to reusing the single address,” with key rotation advised. address() returns one address; you sign repeatedly with it.
  • WizardConnect = HD-first. In the handshake the wallet returns BIP32 xpubs for named paths:
    name recommended BIP44 path
    receive m/44'/145'/0'/0
    change m/44'/145'/0'/1
    defi m/44'/145'/0'/7
    The dapp then derives all addresses locally (no per-address round-trips) and references them by name/index in inputPaths. Paths are identified by name, and a privacy-minded wallet may rotate even non-standard paths per session (the dapp never sees the path, only the xpub). The docs argue xpub sharing is safe because an xpub alone cannot spend.
  • CashConnect = sandboxed keypair. A per-session, domain-scoped key is derived:
    sandboxedPrivateKey = sha256(`${masterPrivateKey}cashrpc-over-wc:${domainName}`)
    
    Sandboxed accounts are identified by public key (bch:${chain}:${publicKey}), which also discourages dapps from sending funds to them and enables ECIES-style uses.

9. Message signing

  • WC2: bch_signMessage → base64 signature using the Electron Cash \x18Bitcoin Signed Message:\n prefix (interoperable with Electron Cash).
  • WizardConnect: not in the core spec — it covers transaction signing only. The CashScript guide states: “For arbitrary message signing, use BCH WalletConnect or a WizardConnect custom extension.” Extensions can add decrypt_request/decrypt_response, etc.
  • CashConnect: bch_signMessage_V0 ({ message: Uint8Array } → Uint8Array), plus wc_authRequest for full CACAO-based login.

10. Security posture

  • WC2: relies on WalletConnect’s relay + its own encryption; the security-relevant work is done in the tx object (placeholders, source outputs). Single-address reuse is a deliberate risk-minimization trade-off. No template sandbox.
  • WizardConnect: E2E-encrypted over Nostr (relay sees only ciphertext, wrapped with a throwaway outer key so it can’t even link senders), 8-byte pairing secret to stop MITM, mandatory SIGHASH_ALL|FORKID|UTXOS, peer-pubkey filtering, replay protection via timestamps, and an optional chunk extension for >64 KB messages (policy/consensus-max transactions can exceed NIP-44’s plaintext cap). Licensing/copyright: LGPL-3.0-or-later, © Whiterun LLC, with a CLA.
  • CashConnect: strongest sandboxing model for dapp-supplied intent — the service can only reference template scripts, token categories are session-whitelisted (allowedTokens, or *), and actions in the template constrain which scripts can be called together (the thread’s example: a payoutLockingBytecode script can’t be invoked in isolation by a malicious service, because actions define the only legal transaction shapes). Templates are intended to be hashable/whitelistable by wallets. Its own risk surface inherits from WalletConnect transport.

11. Ecosystem & maturity

Wallets Dapps Status
WC2 Cashonize, Paytaca, Zapit Tapswap, CashTokens Studio, Cash-Ninjas mint, more Production (Flipstarter-funded)
WizardConnect Selene, Paytaca (+ Cashonize since v0.9.0) Cauldron (recommended over WalletConnect there), Moria Production; Riften docs call it “the recommended option when available… faster and more reliable than WalletConnect” for HD wallets
CashConnect Cashonize (merged Dec 2023) CashRPC IDE, BCHouse Flipstarter demo, BCH Guru contracts Pre-alpha; spec “still marked as pre-alpha”; got a Nostr-based overhaul in Cashonize v0.9.0

12. The emerging unifier: BCH-Connect/bchconnect

Worth flagging because it directly answers “which of the three should I use”: the BCH-Connect/bchconnect project (status: pre-release, not yet usable) gives dapps one TypeScript API across all three protocols plus a built-in modal:

const client = createClient({ appMetadata, network, connectors: [ /* WC2, WizardConnect, CashConnect adapters */ ] });
await client.connect("wizardconnect", { onPairing: (p) => showQr(p.uri) });
const session = client.current;
await client.request(session, "…", params);

It normalizes everything into a single model with:

  • Connector / ProtocolDefinition abstractions and per-protocol method/event maps.
  • Capabilities rather than raw methods: "libauth-signing" and "message-signing" (protocols that don’t support message signing simply don’t advertise that capability — exactly WizardConnect’s gap).
  • Uniform Session, SessionStatus { peer, transport }, Pairing { kind: "uri" | "none" }, typed errors (RequestRejectedError, TimeoutError, CapabilityMissingError, NetworkMismatchError, …), and lifecycle events (session:connected/changed/disconnected, request:pending/settled).

The core also standardizes the transaction types (SignTransactionRequest, SourceOutput, ContractDisplay, Transaction) — which are essentially the WC2 types WizardConnect already reuses.


13. Which to reach for, and why (API-design takeaways)

  • Use WC2 (bch-wc2) when you want maximum wallet reach and simplest single-address UX, and you need both tx and message signing today. Its IConnector API (connect/connected/address/signTransaction/signMessage/disconnect/on) is the de-facto baseline the whole BCH ecosystem mirrors. Cost: a WalletConnect projectId and Cloud relay dependency.
  • Use WizardConnect when you’re building a DeFi/contract dapp that benefits from HD addresses (privacy, per-user derived addresses), want no projectId / no WalletConnect relay, and like reconnection-proof sessions (mobile app-switch and refresh tolerant). Cost: transaction-signing only (no generic message signing), Nostr relay dependency (default Riften relays, but any relay works), and you must supply inputPaths.
  • Use CashConnect when you want the strongest dapp-sandboxing/security model — templates, token whitelisting, wallet-side UTXO selection, constrained “actions,” and CACAO auth — and you’re comfortable with a pre-alpha, template-centric integration. Cost: it inherits WalletConnect transport, has the smallest wallet ecosystem, is version-prefixed (_V0, CashRPC:0), and its spec explicitly warns of breaking changes.
  • Watch bchconnect if you want to support all three without writing three integrations.

A final API-shaped observation: all three converge on the same core signing request (WC2’s WcSignTransactionRequest), and both WizardConnect and CashConnect were explicitly designed to extend — not replace — that shared shape. WC2 contributes the transport + base tx format; WizardConnect adds HD key routing (inputPaths); CashConnect adds template-scoped intent + sandboxing (CashRPC:0, actions, allowedTokens).


Sources

Note: I didn’t find dedicated WalletConnect/CashConnect/WizardConnect pages in the local PSF wiki, so the above is sourced from the upstream repos and docs. If you want, I can drill into any single API surface (e.g., a side-by-side of WC2 signTransaction vs WizardConnect signTransaction return handling, or the exact CashConnect template/actions JSON schema) in more depth.


Public Q&A mirror from the Block tech-support bot.


Write a comment