deviant/Parameters/Pari_P2P/connector/dapp-proposer.ts
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

110 lines
5.2 KiB
TypeScript

/**
* src/connect/proposer.ts — the generic dApp-side ("proposer") connector core,
* shared by every Deviant dApp (tote, checkers, event-prediction) and by both
* wallet-connection protocols (WalletConnect wc2-bch-bcr + CashConnect).
*
* Roles (see spike/wc-connect/README.md):
* - dApp = PROPOSER: opens a session, builds an unsigned tx leaving the USER's
* own inputs as placeholders, and requests the wallet co-sign them.
* - wallet = RESPONDER: connects, and signs only its own inputs.
*
* This module is protocol-agnostic: a dApp builds its transaction with CashScript
* (covenant inputs carry their real unlockers; each user input is added with
* `placeholderP2PKHUnlocker(userAddress)`), turns it into a `WcTransactionObject`
* via `buildCoSignRequest`, and hands it to a `WalletSession` (a WalletConnect or
* CashConnect adapter). Validated handshake: snapshot 0055.
*
* What is shared across protocols is the TX-BUILDING (the `TransactionBuilder` with
* placeholder user inputs). The WIRE REQUEST is NOT identical across protocols:
* - WalletConnect (wc2-bch-bcr): `bch_signTransaction` takes the `WcTransactionObject`
* verbatim, serialized via `toWcParams` (libauth `stringify`). This is the
* device-validated path (snapshot 0057).
* - CashConnect (CashRPC, v0.0.25): `bch_signTransaction_V0` is **wallet-
* orchestrated**, not a dApp-built pass-through. Template-scoped inputs use the
* BchNamespace `template` (a libauth `WalletTemplate` the dApp CAN supply at
* session establishment), but the wallet then auto-runs
* `usingUnspents → addTokensFromUnspents → addTokenChangeOutputs →
* addSatoshisFromUnspents → addSatoshiChangeOutput` and VM-`verify`s before
* signing. Net: CashConnect V0 is the right fit for **standard wallet-side flows**
* (plain BCH sends, CashTokens FT/NFT sends) — the wallet builds and signs. For
* Deviant's **dApp-built covenant txs** (tote `bet()`, `GamePoolEscrowFlat.fund()`),
* route through WalletConnect: the dApp-supplied-template path is technically
* open but blocked in practice by the auto-orchestration breaking covenant
* structure, wallets' refusal to run arbitrary dApp templates, and the VM-verify
* gate. `toWcParams` below remains WalletConnect-only.
* Multi-protocol wallets (e.g. Cashonize) speak both, so users never see the routing
* decision. See spike/wc-connect/cashconnect.mjs + TESTING.md §2 for the CashConnect
* handshake test.
*/
import { TransactionBuilder } from 'cashscript';
import {
placeholderP2PKHUnlocker,
type WcTransactionObject,
type WcTransactionOptions,
} from 'cashscript';
import { stringify } from '@bitauth/libauth';
export { placeholderP2PKHUnlocker };
export type { WcTransactionObject, WcTransactionOptions };
/** What the wallet returns from `bch_signTransaction`. */
export interface SignedTxResult {
/** Fully-signed transaction hex. */
readonly signedTransaction: string;
/** Its txid (hash). */
readonly signedTransactionHash: string;
}
/**
* A connected wallet session — the seam every adapter implements, so a Deviant
* dApp is written ONCE against this interface and works over WalletConnect,
* CashConnect, or (later) WizardConnect. Concrete adapters wrap the relay/SDK
* (device-side; see spike/wc-connect/{connect,cashconnect}.mjs).
*/
export interface WalletSession {
/** The bch account(s) the wallet exposed at pairing (cashaddr). */
getAddresses(): Promise<string[]>;
/** Ask the wallet to co-sign the placeholder inputs of a proposed tx. */
signTransaction(request: WcTransactionObject): Promise<SignedTxResult>;
}
/**
* Turn a fully-built (but user-inputs-unsigned) transaction into a co-sign
* request. Thin wrapper over CashScript's `generateWcTransactionObject` so every
* dApp has one named entry point (and a place to normalise across protocols if
* they ever diverge). The builder must already have:
* - covenant inputs added with their real `contract.unlock.*(...)` unlockers,
* - each user input added with `placeholderP2PKHUnlocker(userAddress)`,
* - all outputs added.
*/
export function buildCoSignRequest(
tb: TransactionBuilder,
opts: WcTransactionOptions = {},
): WcTransactionObject {
return tb.generateWcTransactionObject(opts);
}
/**
* Serialize a `WcTransactionObject` into the JSON-RPC `params` a BCH wallet
* expects for `bch_signTransaction` (wc2-bch-bcr). The wire format is **libauth's
* `stringify`**: `Uint8Array → "<Uint8Array: 0x..>"`, `bigint → "<bigint: ..n>"`.
* Wallets (e.g. Cashonize) validate these exact patterns and reject plain
* hex/decimal (or raw arrays/numbers) — so an adapter MUST pass `toWcParams(req)`,
* never the raw object, to `client.request`.
*/
export function toWcParams(req: WcTransactionObject): unknown {
return JSON.parse(stringify(req));
}
/**
* Build the co-sign request and send it to the wallet in one step. The dApp signs
* its covenant inputs; the wallet fills the placeholder (user) inputs and — when
* `broadcast` is set — broadcasts. Returns the signed tx.
*/
export async function proposeCoSign(
session: WalletSession,
tb: TransactionBuilder,
opts: WcTransactionOptions = {},
): Promise<SignedTxResult> {
return session.signTransaction(buildCoSignRequest(tb, opts));
}