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>
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-clientin 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
userPromptasks 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. TheWalletSessioninterface 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 tosession_proposal,session_request,session_delete; classifies each by shape; dispatches WC requests towc-handler.ts.wc-handler.ts— implementations ofbch_getAddresses,bch_signMessage,bch_signTransaction. The signTransaction handler delegates tosigner.ts.signer.ts— libauth-based per-input signer.coSignTransactionwalks the tx's inputs, signs only those whose source-output P2PKH matches the wallet's own pubkey hash. TheisOwnedInputpredicate 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,Responderinterface, etc.
examples/
dapp-connect-example.mjs— runnable Node script: pair + getAddresses + signMessage against anywc2-bch-bcrwallet. Includes the chipnet+mainnet co-pair pattern. Run withWC_PROJECT_ID=<id> node dapp-connect-example.mjs.dapp-signtx-example.mjs— runnablebch_signTransactiondemo. Mainnet by default; override withWC_CHAIN=bch:bchtest. Sets up a self-send with a placeholder unlocker; the wallet returns the signed tx withbroadcast: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:
- Get your Reown project ID + set
WC_PROJECT_IDenv var. - From
examples/:WC_PROJECT_ID=<your_id> WC_CHAIN=bch:bchtest node dapp-connect-example.mjs - Scan the printed QR in the Deviant chipnet beta wallet (Apps → Connect → paste/scan URI).
- Approve. The script prints
bch_getAddressesandbch_signMessageresults. - Then
node dapp-signtx-example.mjsto test signTransaction withbroadcast: 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.