deviant/Parameters/WalletConnect/WalletConnect_integration_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

26 KiB

WalletConnect v2 + BCH — dApp integration reference

For dApp developers integrating WalletConnect against the Deviant wallet (and any other wc2-bch-bcr-compliant BCH wallet — Cashonize, Paytaca, etc.). This is a how-to with copy-paste recipes, gotchas we hit in real integrations, and all referenced source files co-located in this folder. If you build a BCH dApp that needs to ask a user's wallet to sign something, this is the spec to follow.


Folder contents — everything you need is here

Parameters/WalletConnect/
├── WalletConnect_integration_reference.md     ← this file
│
├── dapp-side/                                 ← the proposer (dApp) primitives
│   ├── proposer.ts                            (validated cashscript co-sign helpers)
│   └── index.ts                               (module barrel)
│
├── wallet-side/                               ← the responder (wallet) reference
│   ├── responder.ts                           (live WalletKit listener + dispatcher)
│   ├── wc-handler.ts                          (bch_getAddresses / signMessage / signTransaction)
│   ├── signer.ts                              (libauth co-signer with isOwnedInput guard)
│   ├── bch-namespace.ts                       (chains, methods, account formats)
│   ├── classifier.ts                          (WC vs CashConnect proposal routing)
│   └── index.ts                               (type surface)
│
└── examples/                                  ← runnable dApp test scripts
    ├── dapp-connect-example.mjs               (pair + getAddresses + signMessage)
    ├── dapp-signtx-example.mjs                (bch_signTransaction co-sign)
    ├── spike-readme.md                        (the spike directory's README)
    └── spike-testing.md                       (testing notes from real device runs)

The dapp-side/ files are what your dApp imports. The wallet-side/ files are reference copies of how the Deviant wallet implements the responder — useful to understand what your bch_signTransaction call does on the receiving end, particularly the signing discipline guarantees.


0. TL;DR — minimum viable dApp integration

import { SignClient } from '@walletconnect/sign-client';
import { stringify } from '@bitauth/libauth';
import { TransactionBuilder, placeholderP2PKHUnlocker } from 'cashscript';

// 1. Get a free Reown project ID at https://cloud.reown.com (one per dApp).
const client = await SignClient.init({
  projectId: 'YOUR_REOWN_PROJECT_ID',
  metadata: {
    name: 'Your dApp',
    description: 'What it does',
    url: 'https://yourapp.example',
    icons: ['https://yourapp.example/icon.png'],
  },
});

// 2. Pair — request BOTH chains so chipnet AND mainnet wallets can join.
const { uri, approval } = await client.connect({
  optionalNamespaces: {
    bch: {
      chains: ['bch:bitcoincash', 'bch:bchtest'],
      methods: ['bch_getAddresses', 'bch_signMessage', 'bch_signTransaction'],
      events: [],
    },
  },
});

// 3. Show `uri` as a QR or deep-link. User scans in their wallet.

// 4. Await approval → read which chain the wallet picked from accounts[0].
const session = await approval();
const acc = session.namespaces.bch.accounts[0]; // 'bch:<chain>:<bare-cashaddr>'
const parts = acc.split(':');
const chainId = `${parts[0]}:${parts[1]}`;       // 'bch:bchtest' or 'bch:bitcoincash'

// 5. Request a signed tx (the load-bearing part).
const tb = new TransactionBuilder({ provider });
tb.addInput(userUtxo, placeholderP2PKHUnlocker(userCashaddr));
tb.addOutput({ to: recipient, amount: 1000n });
const wcObj = tb.generateWcTransactionObject({
  broadcast: true,                               // wallet broadcasts after signing
  userPrompt: 'Send 0.00001 BCH to alice',       // human-readable; wallet shows verbatim
});

// The wire format: libauth `stringify`, NOT plain JSON. Skip this step and
// most BCH wallets reject the request with a zod schema error.
const params = JSON.parse(stringify(wcObj));

const result = await client.request({
  topic: session.topic,
  chainId,
  request: { method: 'bch_signTransaction', params },
});
// → { signedTransaction: '0200...hex', signedTransactionHash: '...' }

That's the whole flow. The rest of this doc explains each step, the wire format, the co-sign mechanics for multi-party txs, and the pitfalls.


1. Architecture

WalletConnect v2 is a relay protocol. Three components:

  • Proposer (dApp) — initiates the session, sends method requests. Uses @walletconnect/sign-client in a browser or Node.
  • Responder (wallet) — receives proposals and method requests, signs locally, returns results. Uses @reown/walletkit (formerly @walletconnect/web3wallet).
  • Relay — Reown's WebSocket relay network. Both ends authenticate with a Reown Cloud project ID. The dApp and the wallet each need their own project ID — don't reuse one for both.
┌─────────────┐    bch_signTransaction     ┌─────────────┐
│  Your dApp  │────────────────────────────│   Wallet    │
│             │       (via Reown relay)    │             │
│ SignClient  │◄───── signed tx hex ───────│  WalletKit  │
└─────────────┘                            └─────────────┘
      ▲                                          ▲
      │  projectId: cloud.reown.com/{app}        │  projectId: cloud.reown.com/{wallet}
      │                                          │

The session, once approved, persists across page reloads (until the wallet disconnects or the session TTL expires). Each session has a topic (a 64-char hex string) used to address requests.


2. Project setup (dApp side)

2.1 Reown project ID

Sign in at https://cloud.reown.com, create a project, copy its ID. **The name

  • icon you set on the dashboard are what the wallet will display in its pairing prompt** — wallets prefer the Cloud-registered metadata over the runtime metadata you pass to SignClient.init. Set both consistently.

2.2 Dependencies

npm install @walletconnect/sign-client@^2.17.0 cashscript@^0.12 @bitauth/libauth@^3.1

@walletconnect/sign-client@2.17.0 is the version pinned in our reference integration (see dapp-side/proposer.ts header comment). Newer versions should work but only 2.17.0 is verified.

For a Node-CLI test, also install qrcode-terminal to print the pair URI as a scannable terminal QR (see examples/dapp-connect-example.mjs).

2.3 Metadata

const metadata = {
  name: 'Your dApp',
  description: 'One-line product blurb',
  url: 'https://yourapp.example',
  icons: ['https://yourapp.example/icon.png'],
};

The icons must be publicly accessible HTTPS URLs. Mobile wallets fetch them at pair time; broken or non-https URLs degrade to a generic placeholder.


3. The pairing flow

3.1 Initialize the client

const client = await SignClient.init({ projectId, metadata });

3.2 Connect — request both chains

const { uri, approval } = await client.connect({
  optionalNamespaces: {
    bch: {
      chains: ['bch:bitcoincash', 'bch:bchtest'],
      methods: ['bch_getAddresses', 'bch_signMessage', 'bch_signTransaction'],
      events: [],
    },
  },
});

Use optionalNamespaces, not requiredNamespaces. Wallets are stricter about required ones; if a chipnet-only wallet sees bch:bitcoincash in requiredNamespaces it'll reject the proposal outright. With optional, the wallet picks whichever chain it supports.

Request BOTH chains — one dApp build then pairs with mainnet wallets (Cashonize, Paytaca) AND chipnet wallets (Deviant chipnet beta) without separate builds. The pitfalls section (§9) explains the chain-mismatch symptom we hit in the field.

3.3 Show the URI

console.log(uri);              // 'wc:abc123...@2?relay-protocol=irn&symKey=...'
qrcode.generate(uri, { small: true });  // optional: terminal QR

In a browser dApp, render it as a <canvas> QR (e.g., qrcode npm package) and also expose a "Copy URI" button + a "Open in wallet" button that follows window.location.href = uri (mobile wallets register a wc: URI handler).

3.4 Await approval

const session = await approval();
// session.topic — use this for all subsequent requests
// session.namespaces.bch.accounts — the wallet's account string(s)
// session.peer.metadata — the wallet's identity

Read which chain the wallet actually picked from the account string — don't assume the dApp's preferred chain. The CAIP-10 account format is strictly three colon-separated segments:

bch:<chainRef>:<bare-cashaddr>

bch:bitcoincash:qpx7lm...        ← mainnet
bch:bchtest:qzj9dn...            ← chipnet

<chainRef> is bitcoincash or bchtest. <bare-cashaddr> is the address without its bitcoincash: / bchtest: prefix. Code to recover the full prefixed cashaddr if your BCH SDK needs it:

const parts = acc.split(':');             // ['bch', 'bchtest', 'qzj9dn...']
const chainId = `${parts[0]}:${parts[1]}`; // 'bch:bchtest'
const cashaddr = `${parts[1]}:${parts.slice(2).join(':')}`; // 'bchtest:qzj9dn...'

3.5 Session persistence

WalletConnect persists sessions in localStorage (browser) or memory (Node). On page reload, SignClient.init will re-hydrate any existing sessions — call client.session.getAll() to enumerate them. To disconnect cleanly:

await client.disconnect({
  topic: session.topic,
  reason: { code: 6000, message: 'User disconnected' },
});

4. The wc2-bch-bcr namespace

This is the BCH-specific WalletConnect v2 method set, designed alongside CashScript and validated by Cashonize / Paytaca / Deviant. The community spec lives at bitcoincashresearch.org.

4.1 Identifiers

Field Value
Namespace bch
Chains bch:bitcoincash (mainnet), bch:bchtest (chipnet)
Account format (CAIP-10) bch:<chainRef>:<bare-cashaddr>
Methods bch_getAddresses, bch_signMessage, bch_signTransaction
Events (none currently)

4.2 Methods

bch_getAddresses

params:  {} (or unspecified)
returns: string[]  — array of the wallet's prefixed cashaddrs
                     e.g. ['bchtest:qzj9dn...']

Always returns the prefixed form (with bchtest: / bitcoincash:). Most BCH SDKs (mainnet-js, cashscript) expect this format. Use it for balance queries.

bch_signMessage

params:  { message: <utf-8 string>, userPrompt?: string }
returns: { signedMessage: <utf-8 echo>, signature: <hex> }

Useful for proving wallet ownership (login challenges). The exact signature algorithm (classical BIP-322-like vs simple ecdsa(sha256(msg))) varies by wallet — verify with your target wallet's docs. Deviant currently signs ECDSA-DER(sha256(message)) (see wallet-side/wc-handler.ts:handleSignMessage).

bch_signTransaction

The load-bearing method. Covered in §6.


5. The wire format (libauth stringify / parseExtendedJson)

This is the one detail you can't ignore. WalletConnect's JSON-RPC params for bch_signTransaction must be encoded with libauth's stringify, NOT plain JSON.stringify. Two transformations:

Uint8Array → "<Uint8Array: 0x..>"
bigint     → "<bigint: ..n>"

Wallets validate these patterns strictly (Cashonize's zod schema fails fast with invalid_type then invalid_format if you pass plain hex or arrays). We learned this the hard way — see snapshot 0057.

import { stringify } from '@bitauth/libauth';

const wcObj = tb.generateWcTransactionObject({ broadcast: false, userPrompt: '...' });

// CORRECT: round-trip via libauth stringify
const params = JSON.parse(stringify(wcObj));

// WRONG: plain JSON loses Uint8Array / bigint
const wrongParams = JSON.parse(JSON.stringify(wcObj));

await client.request({ topic, chainId, request: { method: 'bch_signTransaction', params } });

The response is also libauth-stringify'd on the wallet side, but JSON-RPC delivers it as a parsed object — signedTransaction and signedTransactionHash arrive as plain hex strings, no decoding needed.


6. bch_signTransaction recipes

6.1 Single-input single-sig — simple send

import { TransactionBuilder, placeholderP2PKHUnlocker } from 'cashscript';
import { stringify } from '@bitauth/libauth';

const userUtxo = await provider.getUtxos(userCashaddr).then(u => u[0]);

const tb = new TransactionBuilder({ provider });
tb.addInput(userUtxo, placeholderP2PKHUnlocker(userCashaddr));
tb.addOutput({ to: recipientCashaddr, amount: 100_000n });    // 0.001 BCH
tb.addOutput({ to: userCashaddr, amount: BigInt(userUtxo.satoshis) - 100_000n - 1_000n });  // change

const wcObj = tb.generateWcTransactionObject({
  broadcast: true,                                        // wallet broadcasts after signing
  userPrompt: 'Send 0.001 BCH to alice@example',          // human prompt
});

const result = await client.request({
  topic: session.topic,
  chainId,
  request: { method: 'bch_signTransaction', params: JSON.parse(stringify(wcObj)) },
});
// → { signedTransaction: '0200...', signedTransactionHash: 'abc...' }

6.2 Multi-input co-sign — both users sign one tx

For 2-party flows (escrows, atomic swaps, multi-payer collections): build ONE tx with placeholder unlockers for each party's input. Send the SAME WcTransactionObject to both wallets via their respective sessions. Each wallet fills in only its own placeholder.

const tb = new TransactionBuilder({ provider });
tb.addInput(aliceUtxo, placeholderP2PKHUnlocker(aliceCashaddr));
tb.addInput(bobUtxo,   placeholderP2PKHUnlocker(bobCashaddr));
tb.addInput(operatorBchUtxo, operatorSigTemplate.unlockP2PKH());  // operator signs immediately
tb.addOutput({ to: escrowAddress, amount: poolSats, token: { amount: poolFt, category } });
// + change

const wcObj = tb.generateWcTransactionObject({
  broadcast: false,                                      // server merges + broadcasts after
  userPrompt: 'Stake 1 CHE in match #1234',
});
const params = JSON.parse(stringify(wcObj));

const [aResult, bResult] = await Promise.all([
  client.request({ topic: aliceSession.topic, chainId, request: { method: 'bch_signTransaction', params } }),
  client.request({ topic: bobSession.topic,   chainId, request: { method: 'bch_signTransaction', params } }),
]);
// Each result is the full tx hex with ONLY that party's input filled
// (the other party's input is still a placeholder unlocker).

6.3 Merging two signed copies

import { decodeTransaction, encodeTransaction, hexToBin, binToHex } from '@bitauth/libauth';

const aTx = decodeTransaction(hexToBin(aResult.signedTransaction));
const bTx = decodeTransaction(hexToBin(bResult.signedTransaction));

// Build a final tx that takes each input's unlockingBytecode from the wallet
// that owns it. Other inputs (operator's BCH input) keep the operator's
// signature from the original build.
const merged = {
  ...aTx,
  inputs: aTx.inputs.map((aInput, i) => {
    if (isOwnedBy(i, aliceCashaddr)) return aInput;       // alice's signed input
    if (isOwnedBy(i, bobCashaddr))   return bTx.inputs[i];// bob's signed input
    return aInput;                                          // operator's input (already signed)
  }),
};

const finalHex = binToHex(encodeTransaction(merged));
await provider.sendRawTransaction(finalHex);

6.4 Signing discipline — the structural guarantee

The wallet signs ONLY inputs whose sourceOutputs[i].lockingBytecode == p2pkh(walletPubkeyHash). This is enforced at the wallet's signer (see wallet-side/signer.ts — isOwnedInput). The wallet:

  • Will sign an input whose source-output is the wallet's own P2PKH.
  • Will NOT sign any other input — even if your userPrompt asks for it.

This means a malicious dApp cannot trick wallet A into signing wallet B's input, even if the dApp ships a Wc tx with B's source output. The wallet ignores it. Rely on this property: it's structural, not advisory.


7. Error handling

7.1 Common error codes

Code Meaning What to do
5000 Generic "user rejected" Show "Cancelled" UI; let user retry
5100 "No supported chain in proposal" The wallet doesn't support any of the chains you asked for. Add the wallet's chain to your optionalNamespaces.bch.chains
6000 "User disconnected" Session torn down; reconnect required
USER_REJECTED (no numeric code) Standard rejection Same as 5000

7.2 Expired proposals / sessions

WalletConnect session proposals have a 5-minute TTL by default. If the user takes too long between scanning the QR and approving in the wallet, the proposal expires. Your await approval() Promise rejects with a relay timeout error. Mitigation: surface the QR with a visible countdown; if the user hesitates, allow re-generating.

7.3 Malformed requests

If you send raw JSON (without libauth stringify), most wallets respond with a JSON-RPC error. The error body usually contains the validation failure (zod-style "invalid_type" or "invalid_format" complaints). Always go through JSON.parse(stringify(wcObj)).


8. Folder breakdown — what each reference file is

dapp-side/

  • proposer.ts — buildCoSignRequest, proposeCoSign, toWcParams, placeholderP2PKHUnlocker. Wraps the cashscript primitives for clean re-use in any dApp. The WalletSession interface seam lets you plug in a WalletConnect adapter, a CashConnect adapter, or a test mock.
  • index.ts — module barrel; what you import from your dApp.

wallet-side/ (reference for understanding what the wallet does)

  • responder.ts — createResponder({walletKit, key, network, events}). Listens to session_proposal, session_request, session_delete; classifies each by shape; dispatches WC requests to wc-handler.ts.
  • wc-handler.ts — implementations of bch_getAddresses, bch_signMessage, bch_signTransaction. The signTransaction handler delegates to signer.ts.
  • signer.ts — libauth-based per-input signer. coSignTransaction walks the tx's inputs, signs only those whose source-output P2PKH matches the wallet's own pubkey hash. The isOwnedInput predicate is the structural safety guarantee.
  • bch-namespace.ts — wire constants: SUPPORTED_CHAINS, WC_METHODS, CC_METHODS, CC_PROTOCOL, account format helpers.
  • classifier.ts — pure routing functions: classifyProposal, classifyRequest, classifyUri. Decide WC vs CashConnect by shape.
  • index.ts — type surface: BchChain, BchSignTxRequest, SignedTxResponse, Responder interface, etc.

examples/

  • dapp-connect-example.mjs — runnable Node script: pair + getAddresses + signMessage against any wc2-bch-bcr wallet. Includes the chipnet+mainnet co-pair pattern. Run with WC_PROJECT_ID=<id> node dapp-connect-example.mjs.
  • dapp-signtx-example.mjs — runnable bch_signTransaction demo. Mainnet by default; override with WC_CHAIN=bch:bchtest. Sets up a self-send with a placeholder unlocker; the wallet returns the signed tx with broadcast:false.
  • spike-readme.md + spike-testing.md — the spike directory's own README + testing notes covering on-device validation against Cashonize.

9. Pitfalls we hit in real integrations (READ THIS)

9.1 Don't send 4-segment account strings

CAIP-10 requires namespace:chainId:address — three segments. If the wallet returns the canonical cashaddr (bchtest:qpx...) and your code blindly constructs bch:<chain>:<cashaddr> = bch:bchtest:bchtest:qpx..., that's 4 segments and WalletKit's validator rejects with:

"Unsupported accounts. approve() namespace account ... should be a string and conform to namespace:chainId:... format"

Fix: strip the network prefix from the cashaddr before concatenating. bare = cashaddr.includes(':') ? cashaddr.slice(cashaddr.indexOf(':') + 1) : cashaddr.

9.2 Request BOTH chains in optionalNamespaces

If your dApp build hardcodes chains: ['bch:bitcoincash'] (mainnet only), chipnet wallets reject the proposal silently with code 5100 and your approval() never resolves. Symptom: the wallet shows "Pairing…" forever (or a generic error); the dApp shows "WalletConnect cancelled or failed."

Fix: chains: ['bch:bitcoincash', 'bch:bchtest'] and read back the wallet's chosen chain from session.namespaces.bch.accounts[0]. Test scripts in examples/ already do this.

9.3 Use libauth stringify, not JSON.stringify

Repeat from §5 because it bites everyone: the wire format encodes Uint8Array and bigint with libauth's special markers. Plain JSON drops them. Always: JSON.parse(stringify(wcObj)).

9.4 Use the wallet's chosen chain in request.chainId, not your preferred

// WRONG: assumes the chain you requested
chainId: 'bch:bitcoincash'

// RIGHT: read from the session
const acc = session.namespaces.bch.accounts[0];
const chainId = `${acc.split(':')[0]}:${acc.split(':')[1]}`;
client.request({ topic, chainId, request: {...} });

If you ask the wallet to sign on bch:bitcoincash but it's a chipnet wallet, you'll get a chain-mismatch error.

9.5 Separate project IDs for the dApp and the wallet

When designing your dApp's product setup: never reuse the wallet's project ID for the dApp's, or vice versa. Wallets and dApps each present themselves to peers via their own Reown Cloud project metadata. Reusing IDs means peers display the wrong name (e.g. wallet shows "Cauldron connected" instead of "Your dApp connected" because it pulled the metadata from the wrong project).

9.6 Don't generate a new pair URI per attempt without disconnecting old ones

Each client.connect() creates a new pairing topic. If the user keeps tapping "connect" without the wallet completing pairing, you accumulate dead topics on the relay. Clean up: await client.pairing.disconnect({ topic: oldTopic }) when generating a new URI.

9.7 broadcast: true means the wallet broadcasts — not the dApp

broadcast: true in WcTransactionObject tells the responder to broadcast the signed tx itself after signing. The wallet uses its own network provider to do so. If you want to broadcast yourself (e.g., to merge with other signatures first), use broadcast: false and broadcast from your own provider after receiving signedTransaction.


10. CashConnect — a different protocol on the same relay

The BCH ecosystem also has CashConnect (CashRPC), a richer protocol that runs over the same WC2 relay but has different methods, wire shape, and URI scheme. It is NOT a drop-in for wc2-bch-bcr. Key differences (see wallet-side/classifier.ts for the routing):

Aspect wc2-bch-bcr (this doc) CashConnect (CashRPC v0.0.25)
URI scheme wc: cc: (rewritten by the cashconnect bundle)
Methods bch_getAddresses, bch_signMessage, bch_signTransaction _V0-suffixed: bch_getBalance_V0, bch_signMessage_V0, bch_signTransaction_V0, ...; plus wc_authRequest
Proposal marker (none) bch.protocol = 'CashRPC:0' in requiredNamespaces
Account format bch:<chain>:<cashaddr> bch:<chain>:<pubkeyHex>
Co-sign model dApp-built tx pass-through (you build the tx, wallet just signs your inputs) Wallet-orchestrated (you give wallet a template + amount; wallet builds, VM-verifies, signs)
Carries dApp covenant unlocker? Yes — full WcTransactionObject with unlockingBytecode No — wallet-template-scoped only

For dApps that build their own txs with CashScript covenants (escrows, swaps, prediction markets, etc.), use wc2-bch-bcr — that's this doc. CashConnect is better suited for standard-send dApps (payment apps, faucets) where the wallet builds the tx. If you want both, you need two adapters; the Deviant wallet ships both behind one relay socket (see wallet-side/responder.ts for the dispatch pattern).


11. End-to-end test it on chipnet (before mainnet)

The Deviant chipnet beta is a wc2-bch-bcr responder live at https://testchipnet.duckdns.org/. To test:

  1. Get your Reown project ID + set WC_PROJECT_ID env var.
  2. From examples/:
    WC_PROJECT_ID=<your_id> WC_CHAIN=bch:bchtest node dapp-connect-example.mjs
    
  3. Scan the printed QR in the Deviant chipnet beta wallet (Apps → Connect → paste/scan URI).
  4. Approve. The script prints bch_getAddresses and bch_signMessage results.
  5. Then node dapp-signtx-example.mjs to test signTransaction with broadcast: false.

Once those work, you've validated your dApp end-to-end on chipnet. Promote to mainnet by switching WC_CHAIN=bch:bitcoincash (or by requesting both chains and letting the wallet pick).


12. References

Canonical sources (in this repo)

  • dApp proposer: src/connect/proposer.ts, src/connect/index.ts
  • Wallet responder (live): shells/app/src/connect/*.ts
  • Working dApp test scripts: spike/wc-connect/connect.mjs, spike/wc-connect/signtx.mjs
  • The connector design doc: wiki/connector.md
  • ADRs: 0016 (WC routing), 0041 (responder), 0043 (multi-protocol responder)
  • Validation snapshots: 0055 (handshake), 0057 (co-sign), 0061 (CashConnect)

External

  • Reown docs: https://docs.reown.com/
  • WalletConnect v2 spec: https://specs.walletconnect.com/
  • wc2-bch-bcr discussion: https://bitcoincashresearch.org/t/wallet-connect-v2-support-for-bitcoincash/1100
  • CashScript: https://cashscript.org/docs/guides/walletconnect
  • Reown Cloud (project IDs): https://cloud.reown.com/

If you find a divergence between this doc and the runtime behaviour (the WC SDK moves, wallets ship new versions, the BCH spec evolves), trust the runtime and annotate the diff back here. This is a living reference.