Q&A: Make a detailed comparison of the three main ways of connecting a wallet to a da...
- Connecting a wallet to a BCH dapp: WalletConnectV2 vs. WizardConnect vs. CashConnect
- 1. The 30-second summary
- 2. Architecture & transport layer
- 3. Pairing & session flow
- 4. Dapp-side API surface
- 5. Wallet-side API surface
- 6. Method / message names on the wire
- 7. Transaction data model
- 8. HD-wallet support (the biggest architectural fork)
- 9. Message signing
- 10. Security posture
- 11. Ecosystem & maturity
- 12. The emerging unifier: BCH-Connect/bchconnect
- 13. Which to reach for, and why (API-design takeaways)
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 bytess= 8-byte shared secret for MITM prevention- relay host/port/
proptional (defaults omitted to keep QR compact; a QR-alphanumeric-safeWIZ://%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_getAddressesbch_signTransactionbch_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_canceldisconnectchunk(transport-level, large-message splitting)- Extensions may add actions like
decrypt_request/decrypt_response
CashConnect / CashRPC v0:
bch_getBalance_V0bch_getChangeLockingBytecode_V0bch_getTokens_V0bch_signMessage_V0bch_signTransaction_V0wc_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(+ optionaldata) 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_V0also 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 receivem/44'/145'/0'/0changem/44'/145'/0'/1defim/44'/145'/0'/7The 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:
Sandboxed accounts are identified by public key (sandboxedPrivateKey = sha256(`${masterPrivateKey}cashrpc-over-wc:${domainName}`)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:\nprefix (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), pluswc_authRequestfor 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 optionalchunkextension 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*), andactionsin the template constrain which scripts can be called together (the thread’s example: apayoutLockingBytecodescript 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/ProtocolDefinitionabstractions 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
IConnectorAPI (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
bchconnectif 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
- bch-wc2 spec/README — https://github.com/mainnet-pat/wc2-bch-bcr • monorepo https://github.com/mainnet-pat/bch-wc2
- CashScript connection guide (WC2 + WizardConnect) — https://cashscript.org/docs/guides/walletconnect
- WizardConnect docs — https://docs.riftenlabs.com/wizardconnect/ (protocol, transport, connection-uri, dapp)
- Riften Cauldron wallet list — https://docs.riftenlabs.com/cauldron/wallets/
- CashConnect SDK
index.d.ts— https://unpkg.com/cashconnect/dist/index.d.ts • npmcashconnect• GitLab https://gitlab.com/developers.cash/cashconnect-js - CashRPC design thread — https://bitcoincashresearch.org/t/cashrpc-protocol-to-attempt-to-unify-wallet-app-comms/1201
- Cashonize wallet (supports all three) — https://github.com/cashonize/cashonize-wallet • legacy CashConnect integration https://github.com/cashonize/legacy-wallet/blob/main/cash-connect.js
- BCH-Connect unified library + API report — https://github.com/BCH-Connect/bchconnect (packages/core/etc/core.api.md)
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