T6Wallet (scripts/lib/t6-wallet.mjs) — one party's self-custodial wallet holding only its own key. Two wallets pay each other over a transport: payer signs the next state, peer signs it remotely, board is updated (payUni). No shared keys, no tx co-signing; the transport carries only a proposal + a 64-byte state signature. Validated on chipnet (t6-wallet-p2p-demo.mjs): open + 3 payments both directions, monotonic 1->3, final Alice 40000 / Bob 60000. The MVP functional core is now complete (engine + self-custodial board + BTC 2-of-2 PSBT + this protocol). Remaining: Phase 4b front-end UI (relay + client-side keys). Snapshot 0187. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
3.4 KiB
JavaScript
57 lines
3.4 KiB
JavaScript
/**
|
|
* t6-wallet.mjs — a self-custodial T6 wallet (one party). The protocol layer the UI sits on.
|
|
*
|
|
* Each T6Wallet instance represents ONE party and holds ONLY its own key. Two wallets transact by
|
|
* exchanging small messages over a `transport`: a payment PROPOSAL and a 64-byte state SIGNATURE for
|
|
* the board (and, for settlement, a PSBT — Phase 3). Neither wallet ever holds the other's key. The
|
|
* board (BchChannelBoardUni) is the shared canonical state; each party independently signs the state
|
|
* MESSAGE (checkDataSig), so there is no transaction co-signing.
|
|
*
|
|
* The `transport` here is an interface — `request(msg) → Promise<reply>` and an inbound handler — so
|
|
* the same wallet works over an in-process bus (tests) or a WS relay (the UI). The demo passes a
|
|
* peer wallet directly, which is exactly what the relay would carry.
|
|
*/
|
|
import { T6Board, signState } from './t6-engine.mjs';
|
|
|
|
export class T6Wallet {
|
|
/**
|
|
* @param role 'alice' | 'bob' — which board slot this party occupies.
|
|
* @param privateKey this party's private key (its OWN — never shared).
|
|
* @param channelId the 32-byte channel id (== the board category / genesis txid).
|
|
* @param board a T6Board bound to the BchChannelBoardUni artifact.
|
|
* @param feeSource { cashaddr, sign } — this party's fee source when IT broadcasts an update.
|
|
*/
|
|
constructor({ role, privateKey, channelId, board, feeSource }) {
|
|
if (role !== 'alice' && role !== 'bob') throw new Error("role must be 'alice' or 'bob'");
|
|
this.role = role; this.priv = privateKey; this.channelId = channelId; this.board = board; this.feeSource = feeSource;
|
|
this.approve = () => true; // policy hook: accept/reject an incoming proposed state (default: accept)
|
|
}
|
|
|
|
/** Current on-chain channel state (read from the board — no counterparty needed). */
|
|
async state() { return this.board.readState(this.channelId); }
|
|
|
|
/** Sign a proposed new state with MY key (what a peer requests over the transport). Returns a 64-byte datasig. */
|
|
signProposal({ newState, balA, balB }) {
|
|
if (!this.approve({ newState, balA, balB })) throw new Error('proposal rejected by policy');
|
|
return signState(this.priv, this.channelId, newState, balA, balB);
|
|
}
|
|
|
|
/**
|
|
* PAY the peer `amount` (my balance → peer). I compute the next state, sign it, ask the peer to
|
|
* sign it (via `requestPeerSig`, which the transport fulfils in the peer's wallet), then assemble +
|
|
* broadcast the board update. `requestPeerSig({newState,balA,balB}) → Promise<Uint8Array datasig>`.
|
|
*/
|
|
async pay(amount, requestPeerSig) {
|
|
const cur = await this.state();
|
|
if (!cur) throw new Error('channel not open');
|
|
const newState = cur.state + 1;
|
|
let balA = cur.balA, balB = cur.balB;
|
|
if (this.role === 'alice') { if (amount > balA) throw new Error('insufficient balance'); balA -= amount; balB += amount; }
|
|
else { if (amount > balB) throw new Error('insufficient balance'); balB -= amount; balA += amount; }
|
|
const mySig = signState(this.priv, this.channelId, newState, balA, balB); // I sign with my key
|
|
const peerSig = await requestPeerSig({ newState, balA, balB }); // transport → peer signs with its key
|
|
const aliceSig = this.role === 'alice' ? mySig : peerSig;
|
|
const bobSig = this.role === 'alice' ? peerSig : mySig;
|
|
return this.board.payUni({ category: this.channelId, newState, balA, balB, aliceSig, bobSig, feeSource: this.feeSource });
|
|
}
|
|
}
|