deviant/Parameters/WalletConnect/CashConnect_reference.md
walerikus 35076606c7 chore: checkpoint in-flight work across shell, contracts, and docs
Snapshot of the shared working tree's outstanding changes (multiple
concurrent workstreams), excluding generated runtime state files
(.football-tote-state.json, .tote-platform-state.json, .usdm-historical-chart.html).

Spans: wallet shell (connect/, keystore MultiKeyStore+vault, screens,
currency picker, sign-message), verify artifacts, new Potidaea/DRA/tote
contracts + regtest suites, cross-chain/T6 + WalletConnect Parameters,
a telegram-orderbook subproject, design mockups, and docs. See
shells/app/WALLET-COORDINATION.md for the per-file ownership split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 02:02:05 +02:00

22 KiB
Raw Blame History

CashConnect / CashRPC — integration + design reference

BCH community connector standard. CashRPC is the underlying protocol (transport-agnostic — designed to run over WalletConnect v2, HTTP, LibP2P, or intra-wallet). CashConnect is specifically CashRPC over WalletConnect v2 — one of several planned transport pairings. Naming convention proposed by the authors: CashRPC + [Transport] = [Product Name] — so CashRPC + WalletConnect = CashConnect, CashRPC + HTTP = CashPaymentProto, CashRPC + Intra-Wallet = just CashRPC.

Proposed by @jimtendo on bitcoincashresearch.org (Dec 2023, forum thread CashRPC — protocol to attempt to unify wallet ↔ app comms); merged into mainline Cashonize by end of Dec 2023. BCH community effort, not one company. This doc sits next to WalletConnect v2 (WalletConnect_integration_reference.md) because CashConnect rides the same WC2 relay and shares the same bch namespace name — but the protocol body (proposal shape, methods, wire encoding, signing model) differs in ways that matter. Source-grade reference pinned against cashconnect@0.0.25 (the installed version validated in this project) + the Deviant multi-protocol responder we built in shells/app/src/connect/ (snapshots 0061, 0062, 0152, ADR-0043).


0. TL;DR

Question Answer
Protocol layer CashRPC — transport-agnostic (WalletConnect v2, HTTP, LibP2P, intra-wallet)
Product name CashConnect = CashRPC + WalletConnect v2 (the pairing this doc covers)
Same project ID as WC2? Yes — one Reown project ID covers both
URI scheme cc: (the bundle rewrites wc: → cc: at proposal time)
Namespace bch (same as WC2) but with extra protocol: 'CashRPC:0' + template + allowedTokens fields
Method names — shipping (v0.0.25) _V0-suffixed: bch_signMessage_V0, bch_signTransaction_V0, bch_getBalance_V0, bch_getTokens_V0, bch_getChangeLockingBytecode_V0, wc_authRequest
Method names — proposed v-next bch_executeAction — template-bound named actions (see §10)
Wire format libauth stringify (Extended JSON) — Uint8Array → "<Uint8Array: 0x..>", bigint → "<bigint: ..n>"
params encoding One EJSON-string blob per request (NOT a JSON tree — different from WC2)
Wallet support Cashonize (native)
Best fit for Plain BCH sends, CashTokens FT/NFT sends, message-sign, balance/token queries — anything wallet-orchestrated
NOT good for (at V0) dApp-built covenant co-sign (see §6) — Deviant covenant flows route through WalletConnect instead. The v-next bch_executeAction model (§10) is designed to fix exactly this
License MIT

1. Architecture — what makes it different from WC2

CashConnect is wallet-orchestrated, not pass-through. The dApp does not build the transaction; the wallet does, using instructions the dApp sends. This is the load-bearing distinction vs WC2's bch_signTransaction (where the dApp builds the entire tx and the wallet only fills in placeholder unlockers).

WalletConnect v2 / wc2-bch-bcr        CashConnect / CashRPC v0
─────────────────────────────         ────────────────────────
dApp builds full tx with covenant     dApp sends { inputs[], outputs[], ... }
unlockers + placeholderP2PKHUnlocker  Wallet builds tx using its scope template
for user inputs                       and auto-adds its own unspents + change
                                      Wallet runs libauth VM verify before signing
Wallet signs only the placeholder     Wallet signs the assembled tx
inputs (`isOwnedInput` check)

The wallet handler (CashRPC_V0.signTransaction in the installed package, lines ~995–1112 of dist/index.mjs.js):

  1. Parses dApp inputs/outputs into libauth template format
  2. For template-scoped inputs: compiles via the scope template (the BchNamespace template, typically the wallet's P2PKH WalletTemplate)
  3. Runs usingUnspents → addTokensFromUnspents → addTokenChangeOutputs(changeTemplate) → addSatoshisFromUnspents → addSatoshiChangeOutput(changeTemplate) — auto-adds wallet UTXOs + change
  4. Generates the tx, hashes it
  5. Runs libauth VM verify on the assembled tx (rejects on failure)
  6. Returns { transactionHash, transaction, sourceOutputs }

2. Project setup (dApp side)

2.1 Reown project ID

Same as WalletConnect v2 — one project at https://cloud.reown.com. The relay is shared.

2.2 Dependencies

{
  "dependencies": {
    "cashconnect": "0.0.25"
  }
}

2.3 Known ESM bundle issue (and the workaround)

cashconnect@0.0.25's ESM bundle does import Client from '@walletconnect/sign-client'. But sign-client's ESM build sets default to the package namespace (object), not the SignClient class. So Client.init is undefined at construction.

The class is the default only under CJS interop. ESM consumers must patch:

// patches/apply-cashconnect-fix.mjs (idempotent; wire as `postinstall`)
import { readFileSync, writeFileSync } from 'node:fs';
const target = 'node_modules/cashconnect/dist/index.mjs.js';
const src = readFileSync(target, 'utf8');
const NEEDLE = `import Client from '@walletconnect/sign-client';`;
const FIXED = `import { SignClient as Client } from '@walletconnect/sign-client';`;
if (src.includes(NEEDLE) && !src.includes(FIXED)) {
  writeFileSync(target, src.replace(NEEDLE, FIXED));
}

Also pin @walletconnect/sign-client to 2.17.0 — newer versions maintain the ESM-default shape and won't help. Filing an upstream fix is the obvious follow-up (one line).


3. The pairing flow (dApp side)

import {
  CashConnectDapp,
  BaseNewSessionDialog,
  BaseReconnectSessionDialog,
  BaseRequestDialog,
} from 'cashconnect';
import { utf8ToBin } from '@bitauth/libauth';

// Headless terminal dialogs — print the URI; override close() so the SDK's
// default "Close not implemented" log is silent.
class TerminalNewSessionDialog extends BaseNewSessionDialog {
  constructor(url, cancelCallback) {
    super(url, cancelCallback);
    console.log(url); // cc:abc@2?...
  }
  close() {}
}
class TerminalReconnectSessionDialog extends BaseReconnectSessionDialog {
  constructor(cancelCallback, newSessionCallback) {
    super(cancelCallback, newSessionCallback);
    this.newConnection(); // skip the reconnect dialog
  }
  close() {}
}
class TerminalRequestDialog extends BaseRequestDialog { close() {} }

const dapp = new CashConnectDapp(
  { projectId: 'YOUR_REOWN_PROJECT_ID', metadata: {...} },
  'bch:bchtest', // chain
  {
    newSession: TerminalNewSessionDialog,
    reconnectSession: TerminalReconnectSessionDialog,
    request: TerminalRequestDialog,
  },
);

// Optional: surface the account at connection
dapp.onConnected = async (session) => {
  const account = session?.namespaces?.bch?.accounts?.[0];
  // 'bch:bchtest:<pubkeyHex>'  (NOT a cashaddr — see §4.3)
};

await dapp.start();

// connect() returns a boolean (NOT { uri, approval }) — the URI is surfaced
// via dialogs.newSession constructor.
const connected = await dapp.connect({
  bch: {
    chains: ['bch:bchtest'],
    methods: [
      'wc_authRequest',
      'bch_getBalance_V0',
      'bch_getChangeLockingBytecode_V0',
      'bch_getTokens_V0',
      'bch_signMessage_V0',
      'bch_signTransaction_V0',
    ],
    events: ['balancesChanged'],
  },
});

// Request — dapp.request() applies libauth `stringify(params)` internally,
// so pass RAW objects (Uint8Array / bigint), do NOT pre-apply toWcParams.
const balance = await dapp.request('bch_getBalance_V0');
const signed = await dapp.request(
  'bch_signMessage_V0',
  { message: utf8ToBin('hello') },
  { message: 'Sign to prove your link' }, // UI prompt
);

4. The bch namespace (CashConnect dialect)

4.1 Identifiers

Same chains as wc2-bch-bcr: bch:bitcoincash (mainnet), bch:bchtest (chipnet), bch:bchreg (regtest).

4.2 Methods (_V0-suffixed)

Method Purpose
wc_authRequest Cacao auth payload signed with the active key (NIP-style)
bch_getBalance_V0 () → { [categoryId: string]: bigint } — wallet's BCH + per-token balances
bch_getChangeLockingBytecode_V0 () → Uint8Array — the wallet's change script
bch_getTokens_V0 () → [{ outpointTransactionHash, outpointIndex, token }] — wallet's token UTXOs in the namespace's allowedTokens
bch_signMessage_V0 ({ message: Uint8Array }) → Uint8Array — Schnorr over sha256(message)
bch_signTransaction_V0 (SignTransactionV0Params[]) → SignTransactionV0Response[] — wallet builds + signs + (optionally) broadcasts

There is no bch_getAddresses (the account comes through the session namespace as a pubkey, see §4.3).

4.3 Account format

session.namespaces.bch.accounts[0] is bch:<network>:<pubkeyHex> — a 33-byte compressed pubkey, hex-encoded. NOT a cashaddr. (The wallet derives a sandboxed key per dApp domain — see §7.)

4.4 BchNamespace extra fields

interface BchNamespace {
  protocol: 'CashRPC:0';      // the version marker the classifier keys off
  template: WalletTemplate;    // libauth WalletTemplate (typically P2PKH)
  allowedTokens: string[];     // category IDs whose tokens may be touched, or ['*']
  chains: string[];
  methods: string[];
  events: string[];
}

template is supplied by the dApp. The wallet uses it as the scope compiler for template-scoped inputs/outputs. allowedTokens is the permission boundary on which token categories the wallet will spend.

4.5 Wire encoding

dapp.request(method, params) calls libauth stringify(params) internally and parseExtendedJson-es the response. So:

  • Pass raw objects (Uint8Array, bigint, etc.) — don't pre-serialize, don't apply toWcParams (that's the WC2 path).
  • The wallet receives request.params as a single EJSON-string blob (per the package's RequestEvent.params: string type). Different from WC2 where params is a JSON tree of EJSON-marker strings.

5. SDK API summary

The package's top-level exports:

  • CashConnectDapp — dApp-side orchestrator (sessions, requests)
  • CashConnectWallet — wallet-side; runs its own @reown/walletkit instance (NOT used by Deviant — we use the standalone CashRPC_V0 so we can share a single WalletKit across protocols)
  • CashRPC_V0 — standalone RPC handler — signMessage(), signTransaction(), getBalance(), getChangeLockingBytecode(), getTokens(). Takes { network, wallet, scope, signerKey } callbacks
  • BaseNewSessionDialog / BaseReconnectSessionDialog / BaseRequestDialog — UI hooks to subclass
  • SupportedChains / SupportedMethods / SupportedEvents — exported lists

Types worth knowing:

  • SignTransactionV0Params — the dApp's request payload for tx-sign
  • SignTransactionV0Response — what the wallet returns
  • BchSession / BchNamespace / BchSessionProposal — typed session shapes
  • WalletCallbacks — { getUnspents, getChangeTemplate }
  • NetworkCallbacks — { getSourceOutput }

6. The covenant co-sign constraint (correction 2026-07-04)

CashConnect V0 CAN carry covenants — a previous version of this doc was wrong about that. The proof-by-existence is BCH Guru: "BCH Guru uses the CashConnect protocol to connect the application to compatible CashToken wallets. Currently, Cashonize is the only compatible wallet with CashTokens." Their prediction-game contract is a full CashScript covenant, and it ships in production over CashConnect V0.

The mechanism is the BchNamespace.template field at session-proposal time: the dApp sends its libauth WalletTemplate (which can encode ANY CashScript covenant's scripts + variables). The wallet compiles against this template — via walletTemplateToCompilerBCH(this.template) — so template-scoped inputs in bch_signTransaction_V0 DO resolve to the covenant's unlocker, produced by the wallet at signing time.

What's really constrained — not the covenant itself, but the covenant DESIGN. Because the wallet auto-orchestrates (usingUnspents → addTokensFromUnspents → addTokenChangeOutputs → addSatoshisFromUnspents → addSatoshiChangeOutput) AFTER parsing dApp inputs/outputs, only covenants that are position-tolerant to appended wallet inputs and change outputs work cleanly:

  • ✅ "Input 0 must be the pool UTXO. User contribution input at position 1+. Output 0 must be the updated pool. User change output at position 1+." — BCH Guru + Cauldron's Cauldron-Wallet browser build both use this pattern. Wallet auto-added inputs go to positions 1+ and change outputs go to positions 1+; the covenant checks positions 0 for its own input/output only.
  • ❌ "Exactly 2 inputs, exactly 3 outputs, no extras." — wallet auto-orchestration would append UTXOs/change and break the count check. Redesign to be position-tolerant, or route through WalletConnect where the dApp builds the whole tx bit-exact.

Practical implication for the Deviant covenants:

Covenant CC V0 viable?
Tote bet() — if written with position-tolerance for user stakes Yes (with template shipped at proposal)
GamePoolEscrowFlat.fund() — 2-party co-funded, exact count Currently no — redesign to position-tolerant, or use WC
Anything with strict input/output count checks No — use WC's pass-through

Revised routing rule applied in Deviant:

Flow Protocol
User outbound BCH send / token send (wallet→external) WC or CC (both fine)
Position-tolerant dApp covenant co-sign (BCH Guru-style, single-covenant-input) WC or CC (both fine, CC needs the WalletTemplate shipped at proposal)
Count-strict dApp covenant co-sign (exact input/output invariants, no wallet-added change) WC only (true pass-through, validated snapshot 0057)
Authentication / bch_signMessage Either

Multi-protocol wallets (Cashonize) speak both — users never see the routing decision.


7. Sandboxed-key derivation

CashConnect wallets derive a per-dApp-domain sandboxed key from the user's master key:

sandboxedPrivateKey = sha256(sha256(masterPrivateKey)
                            + sha256(':cashconnect:' + dAppDomain))

So the pubkey returned in session.namespaces.bch.accounts[0] is the dApp-scoped one, not the user's main wallet pubkey. This is a privacy feature — different dApps see different pubkeys for the same user. It also means: the account is NOT a cashaddr usable in your main wallet UI (you cannot share it as a deposit address).

For Deviant: at signing time we use the active wallet's private key directly (via KeyProvider), not a sandboxed derivation. The Deviant responder's cc-handler.ts passes key.privateKey() as signerKey to CashRPC_V0 — so we override the sandboxing for our own integration. This is fine for a multi-protocol wallet (sandboxing is a wallet-side opt-in feature; we choose not to opt in because our identity model is already non-custodial per-account).


8. Why we don't use CashConnectWallet

The package's CashConnectWallet instantiates its own @reown/walletkit instance — meaning if you also run a WalletConnect v2 responder you have two clients on the same project ID, racing for session_proposal / session_request events. The Deviant multi-protocol responder (shells/app/src/connect/responder.ts) instead uses one shared WalletKit and dispatches by proposal/request shape (the classifier.ts module), routing CashRPC methods through the standalone exported CashRPC_V0 class. Pattern documented in wiki/connector.md §3 + ADR-0043.


9. Cross-references

  • wallet-side/classifier.ts — classifyProposal / classifyRequest / classifyUri route between WC and CashConnect
  • wallet-side/responder.ts — dispatcher
  • WalletConnect_integration_reference.md — the sibling reference for WC2
  • WizardConnect_reference.md — the third BCH connector (future)
  • wiki/connector.md — full multi-protocol design
  • wiki/decisions.md ADR-0043 — multi-protocol architecture
  • Snapshots: 0061 (pinned to installed API), 0062 (bundle patch + structural finding), 0152 (multi-protocol runtime)
  • Package: https://gitlab.com/developers.cash/cashconnect-js (MIT)
  • Reference wallet: https://github.com/cashonize/cashonize-wallet (cash-connect.js in the legacy branch)

10. The v-next design direction — bch_executeAction (template-bound actions)

The BCR forum thread (Dec 2023) describes a broader, more security-focused CashRPC model than the _V0 methods that ship in cashconnect@0.0.25. This is where CashConnect / CashRPC is heading, and it is designed to fix exactly the covenant-co-sign gap flagged in §6. Community member summary (bitjson-adjacent quote): "CashConnect is much more secure for the end user — by using libauth templates, wallets can recognize the smart contract the user is about to interact with & under which specific parameters."

10.1 The four design pillars (from the proposal)

  1. libauth WalletTemplate as the permission boundary. A session's template is a full libauth WalletTemplate describing the contract's scripts and its named actions. The wallet recognizes the template by hash, matches it against a curated list (or user-approved registrations), and knows exactly what shapes of transactions the dApp is allowed to request. Effectively D-1 canonical-template recognition on the connector wire — ADR-0008 / 0015 / 0018's verifier-not-authority posture applied at signing time.

  2. bch_executeAction replaces bch_signTransaction_V0. The dApp doesn't ship a raw tx anymore; it invokes a named action from the template. Example (illustrative):

    "actions": {
      "bch.create_offer": {
        "signer": "player_1_key",
        "transactions": [...]
      }
    }
    

    The wallet resolves the action name against the bound template, builds the tx per the template's specification, and shows the user a human-readable label — "Creating a Flipstarter Campaign", not raw bytecode.

  3. Token-category permissions. Sessions grant access scoped to specific token category IDs — the dApp requests only UTXOs it's whitelisted for. Same allowedTokens mechanism as v0.0.25 but centered in the security model.

  4. Sandboxed keys per session. Session keys are derived: sha256(masterPrivateKey + 'cashrpc-over-wc:' + domainName). Apps never touch the wallet master key; different apps see different sandboxed pubkeys for the same user. (This is the mechanism already active in v0.0.25 — see §7 — but is a first-class design pillar in the proposal.)

10.2 Callbacks the wallet must implement

getUnspents, getSourceOutput, getChangeTemplate — the same three lifecycle hooks the standalone CashRPC_V0 already asks for (cc-handler.ts wires them from the shell's NetworkProvider). So the Deviant responder's shape is compatible with the v-next model at the callback level.

10.3 Status

Draft / pre-alpha at the proposal level. From the thread: "This is heavy WIP" / "pre-alpha release." However CashConnect (i.e. this protocol's WalletConnect transport) was merged into mainline Cashonize by end of Dec 2023 — the shipping implementation (v0.0.25) covers the _V0 methods; the bch_executeAction model is the next revision.

Terminology critique in the thread: Tom questioned "RPC" — noting the term conceptually conflicts with the sandboxed-key model (a real RPC would need direct wallet access). @jimtendo acknowledged this but defended the naming, citing potential intra-process use cases. Full thread.

10.4 What this changes for Deviant

The covenant co-sign structural blocker documented in §6 — CashConnect V0's inability to carry dApp-compiled unlockers — is precisely what the bch_executeAction + template-binding model is designed to solve. If / when v-next ships:

  • The Deviant multi-protocol classifier (shells/app/src/connect/ classifier.ts) already routes CC methods to the CC handler; adding bch_executeAction is a single case in cc-handler.ts.
  • Template registration becomes the load-bearing step: Deviant's existing src/verify/ multi-template verifier (D-1, ADR-0008/0015/0018) is exactly the "which templates does this wallet recognize?" store, and ports directly to the session-approval flow.
  • The covenant routing rule (§6 / wiki/connector.md §5) can relax: Deviant covenant txs can then route through CashConnect too, subject to D-1 template recognition.

Worth revisiting the routing rule when v-next ships. For now, WC remains the covenant path.


11. Quick decision matrix for dApp authors

Use CashConnect when:

  • Your dApp does standard sends (plain BCH or CashTokens FT/NFT) and wants the wallet-orchestrated UX (wallet picks UTXOs, builds change, validates)
  • You want CC-only wallets in your audience (BCH-native preference)
  • You want per-dApp sandboxed identities (no cross-dApp correlation by default)

Use WalletConnect v2 (wc2-bch-bcr) when:

  • Your dApp builds covenant transactions the user co-signs (tote, escrow, AMM input swap, AnyHedge claim, etc.)
  • You need the broadest BCH wallet coverage today (Cashonize + Paytaca
    • others speak WC2; CC is Cashonize-only as of 2026-06)
  • You want a single transport with predictable pass-through semantics

Use both (Deviant's choice) when you want maximum reach + the right protocol per flow. Wallets that speak both (Cashonize) handle the routing transparently.