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>
22 KiB
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):
- Parses dApp inputs/outputs into libauth template format
- For template-scoped inputs: compiles via the scope template (the
BchNamespace
template, typically the wallet's P2PKHWalletTemplate) - Runs
usingUnspents → addTokensFromUnspents → addTokenChangeOutputs(changeTemplate) → addSatoshisFromUnspents → addSatoshiChangeOutput(changeTemplate)— auto-adds wallet UTXOs + change - Generates the tx, hashes it
- Runs libauth VM
verifyon the assembled tx (rejects on failure) - 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 applytoWcParams(that's the WC2 path). - The wallet receives
request.paramsas a single EJSON-string blob (per the package'sRequestEvent.params: stringtype). 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/walletkitinstance (NOT used by Deviant — we use the standaloneCashRPC_V0so we can share a single WalletKit across protocols)CashRPC_V0— standalone RPC handler —signMessage(),signTransaction(),getBalance(),getChangeLockingBytecode(),getTokens(). Takes{ network, wallet, scope, signerKey }callbacksBaseNewSessionDialog/BaseReconnectSessionDialog/BaseRequestDialog— UI hooks to subclassSupportedChains/SupportedMethods/SupportedEvents— exported lists
Types worth knowing:
SignTransactionV0Params— the dApp's request payload for tx-signSignTransactionV0Response— what the wallet returnsBchSession/BchNamespace/BchSessionProposal— typed session shapesWalletCallbacks—{ 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/classifyUriroute between WC and CashConnectwallet-side/responder.ts— dispatcherWalletConnect_integration_reference.md— the sibling reference for WC2WizardConnect_reference.md— the third BCH connector (future)wiki/connector.md— full multi-protocol designwiki/decisions.mdADR-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.jsin 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)
-
libauth WalletTemplate as the permission boundary. A session's
templateis a full libauthWalletTemplatedescribing the contract's scripts and its namedactions. 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. -
bch_executeActionreplacesbch_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.
-
Token-category permissions. Sessions grant access scoped to specific token category IDs — the dApp requests only UTXOs it's whitelisted for. Same
allowedTokensmechanism as v0.0.25 but centered in the security model. -
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; addingbch_executeActionis a single case incc-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.