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>
122 lines
5.5 KiB
TypeScript
122 lines
5.5 KiB
TypeScript
/**
|
|
* wc-handler.ts — wc2-bch-bcr method dispatch (the WalletConnect path).
|
|
*
|
|
* Methods (per ADR-0041 §3 and snapshot 0057 — device-validated against Cashonize):
|
|
* - `bch_getAddresses` → returns the wallet's cashaddr(s)
|
|
* - `bch_signMessage` → ECDSA-DER over sha256(message) ⚠ format TBD on-device
|
|
* - `bch_signTransaction` → libauth pass-through co-sign (delegated to signer.ts)
|
|
*
|
|
* The signTransaction handler is the load-bearing one for Deviant covenant flows
|
|
* (tote `bet()`, `GamePoolEscrowFlat.fund()`, checkers stake) per ADR-0043 §2.3:
|
|
* CashConnect V0 cannot carry dApp-compiled covenant unlockers, so all covenant
|
|
* co-signs route through this handler.
|
|
*
|
|
* Signing discipline (ADR-0041 §5): the signer NEVER signs an input it doesn't
|
|
* own — even if asked. See signer.ts `isOwnedInput`.
|
|
*
|
|
* The wire formatting (libauth `stringify` / `parseExtendedJson`) is the
|
|
* responder's responsibility. By the time we get here, requests are decoded
|
|
* objects with `Uint8Array` bytes and `bigint` values.
|
|
*/
|
|
import { sha256, ripemd160, secp256k1, binToHex } from '@bitauth/libauth';
|
|
import { coSignTransaction } from './signer';
|
|
import type {
|
|
BchSignMessageRequest,
|
|
BchSignTxRequest,
|
|
SignedMessageResponse,
|
|
SignedTxResponse,
|
|
} from './index';
|
|
import type { KeyProvider } from './responder';
|
|
import { p2pkhLockingBytecode, isOwnedInput } from './signer';
|
|
|
|
/** Result of `bch_getAddresses`. The wc2-bch-bcr spec returns a string list. */
|
|
export type GetAddressesResponse = readonly string[];
|
|
|
|
/** `bch_getAddresses` — the wallet exposes the cashaddr(s) it controls. We surface
|
|
* only the active account's `cashaddr` (singular) at v1; multi-account is a future
|
|
* enhancement (the KeyStore is single-account today — `wiki/wallet-ui-design.md`).
|
|
*/
|
|
export function handleGetAddresses(key: KeyProvider): GetAddressesResponse {
|
|
const id = key.identity();
|
|
return [id.cashaddr];
|
|
}
|
|
|
|
/** `bch_signMessage` — ECDSA-DER signature over `sha256(message)`.
|
|
*
|
|
* ⚠ Wire format pending on-device confirmation against Cashonize (ADR-0041 "What
|
|
* this ADR does NOT resolve" §2). Two candidates exist on BCH:
|
|
* (a) **Bitcoin Cash Signed-Message format** (classical): recoverable ECDSA over
|
|
* `hash256("\x18Bitcoin Signed Message:\n" || varint(len(msg)) || msg)`,
|
|
* base64-encoded with a 1-byte recovery header. Used by historical wallets.
|
|
* (b) **Simple `ecdsa(sha256(msg))` DER** — what most modern WC2 BCH wallets
|
|
* appear to do; matches CashConnect's `signMessage` (Schnorr) in shape.
|
|
*
|
|
* We ship (b) since the proposer in `spike/wc-connect/connect.mjs` issues a plain
|
|
* message and Cashonize returned a sig (snapshot 0055) — and the simpler path
|
|
* keeps the responder unblocked. On-device validation will confirm; if Cashonize
|
|
* actually expects (a), switch this function (NOT the wider responder) and bump
|
|
* the wire-format note in ADR-0041.
|
|
*
|
|
* SECURITY: we always sign with the active account's key — there is no
|
|
* per-address routing yet, since the wallet is single-account at v1. */
|
|
export function handleSignMessage(
|
|
request: BchSignMessageRequest,
|
|
key: KeyProvider,
|
|
): SignedMessageResponse {
|
|
// request.address: the dApp may name an address; we don't currently disambiguate
|
|
// between accounts. If/when multi-account lands, look it up against the
|
|
// KeyStore's account list and fail closed if the address isn't ours.
|
|
const digest = sha256.hash(request.message);
|
|
const sig = secp256k1.signMessageHashDER(key.privateKey(), digest);
|
|
if (typeof sig === 'string') {
|
|
throw new Error(`bch_signMessage failed: ${sig}`);
|
|
}
|
|
// The "signedMessage" field carries a UTF-8 echo of what was actually signed,
|
|
// so the dApp can verify it presented the right text to the user.
|
|
const printable = isProbablyUtf8(request.message);
|
|
return {
|
|
signedMessage: printable ? new TextDecoder('utf-8', { fatal: false }).decode(request.message) : binToHex(request.message),
|
|
signature: binToHex(sig),
|
|
};
|
|
}
|
|
|
|
/** `bch_signTransaction` — libauth pass-through co-sign. Fills in the unlocking
|
|
* bytecode for every input owned by this wallet (sourceOutput.lockingBytecode
|
|
* matches our P2PKH); leaves all other inputs untouched.
|
|
*
|
|
* This is the path snapshot 0057 device-validated; it's also the ONLY path for
|
|
* Deviant covenant co-sign (ADR-0043 §2.3). */
|
|
export async function handleSignTransaction(
|
|
request: BchSignTxRequest,
|
|
key: KeyProvider,
|
|
): Promise<SignedTxResponse> {
|
|
const id = key.identity();
|
|
return coSignTransaction({
|
|
request,
|
|
ownPubkeyHash: hash160OfPubkey(id.publicKey),
|
|
ownPublicKey: id.publicKey,
|
|
privateKey: key.privateKey(),
|
|
network: id.network,
|
|
});
|
|
}
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function isProbablyUtf8(bin: Uint8Array): boolean {
|
|
try {
|
|
new TextDecoder('utf-8', { fatal: true }).decode(bin);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** RIPEMD160(SHA256(pubkey)) — the 20-byte pubkey hash used as the P2PKH key.
|
|
* Two-primitive form (not the helper alias) to stay portable across libauth
|
|
* versions. The responder caches the result per-session via `KeyProvider.pubkeyHash()`. */
|
|
function hash160OfPubkey(publicKey: Uint8Array): Uint8Array {
|
|
return ripemd160.hash(sha256.hash(publicKey));
|
|
}
|
|
|
|
// Re-export so the signing-discipline check is callable from tests + responder.
|
|
export { isOwnedInput, p2pkhLockingBytecode };
|