SkateSpots/ESCROW.md
SkateSpots 26e951710e feat(marketplace): Cashu escrow core (time-locked P2PK) + design
Real, oracle-free escrow built on Cashu P2PK with a locktime + refund key:
buyer locks funds to the seller; seller redeems on delivery; the buyer can
reclaim after the deadline. Locked (not plain) tokens are what make the
cashu_token on a public 31872 safe.

- src/lib/cashuEscrow.ts: pubkey normalisation (x-only -> compressed), P2PK
  policy builder, offline token inspection (amount/mint/locktime/refund), and
  an injectable createEscrowToken mint flow (quote -> pay -> mintProofs{p2pk})
- 15 unit tests incl. the mint orchestration against a fake wallet
- Protocol: advertise a 'cashu_escrow' pubkey in kind 0 (NIP.md); token rides
  the existing 31872 cashu_token tag
- ESCROW.md: design, threat model, status, limitations
- AGENTS.md: project map + docs index updated

Wallet UI (cashu-ts + payment rail + profile publish) is native-only and still
needs on-device verification.
2026-09-26 21:01:31 +07:00

93 lines
4.1 KiB
Markdown

# Cashu Escrow (marketplace)
Status: **core implemented & unit-tested; wallet UI is native-only and awaits
on-device verification.** See "Status" at the bottom before trusting it with
real money.
## Goal
Let a buyer and seller close a marketplace deal where the buyer's funds are
**committed but recoverable**, so neither side has to trust the other blindly —
without a custodial third party and without an always-online oracle.
## Model — time-locked P2PK
Cashu **P2PK** (NUT-11) supports a **`locktime`** and **`refund` keys**. We use
exactly that:
```
buyer ──locks funds to──▶ seller pubkey
locktime = now + N days
refund = buyer pubkey
```
- **Seller delivered** → redeems with their key (any time).
- **Seller vanished / deal fell through** → after `locktime`, the **buyer**
reclaims with their refund key.
This is real escrow (both sides protected) with **no witness/oracle** — the only
thing "release on attestation" needs is the locktime, which the mint enforces.
Contrast with the naive option (lock to the seller, no expiry): that's
**prepayment**, not escrow — the seller can take the money and never deliver —
so we deliberately don't do it.
## Keys
Cashu P2PK needs a secp256k1 key the wallet can **sign a witness** with. The
Nostr signer (Amber / NIP-07) can't produce a bare schnorr signature, so we use
the **Cashu wallet's own keyring** (`coco.keyring`), not the Nostr key.
Each user advertises one escrow pubkey for others to lock to:
- **kind 0** content field **`cashu_escrow`** — 33-byte compressed hex
(`02…`), derived from the Cashu keyring. See `NIP.md` → Profile Metadata
Extensions. Nostr x-only keys are accepted too (normalised with an `02`
prefix, matching coco's own `getPublicKeyHex`).
## Flow
1. Seller advertises `cashu_escrow` (set when they touch their Cashu wallet).
2. Buyer reads the seller's pubkey, then **funds escrow by paying the mint's
Lightning invoice** and mints fresh proofs locked to the seller:
- `createMintQuote(amount)` → pay `request` via NWC/WebLN/Cashu melt
- `mintProofs(amount, quote, { p2pk: { pubkey, locktime, refundKeys:[buyer] } })`
- encode → the `cashu_token` on the `31872` confirmation request.
- Funding by mint (not by spending the coco balance) keeps this independent
of the coco wallet's proof state — no state bridging, no counter desync.
3. Seller sees the escrowed token on the request and redeems (`wallet.receive`).
4. If not delivered by `locktime`, the buyer refunds.
## Protocol
The escrow token rides the Attestations NIP's existing optional
`["cashu_token", "<token>"]` tag on the **kind 31872** request (`NIP.md` → Trade
Attestations). The lock is what makes publishing it safe: a plain (unlocked)
token in a public event is spendable by anyone who sees it.
## Threat model / limitations
- **Mint trust**: P2PK keeps the mint from spending your funds, but you still
trust the mint to be solvent and redeemable. Use a reputable mint.
- **No delivery proof on-chain**: "delivered" is the seller's word; the buyer's
protection is the locktime refund, and the marketplace reputation system
(`31871`) records the outcome.
- **Locktime is unilateral**: the buyer can always refund after the deadline even
if the seller did deliver but was slow to redeem — so redemption should happen
promptly, and lock windows should be generous.
- **Native only**: the app's Cashu wallet is Capacitor-only, so escrow is too.
## Status
- ✅ **Core** (`src/lib/cashuEscrow.ts`, unit-tested): pubkey normalisation,
`escrowP2PKOptions`, offline token inspection/`parseEscrowToken`,
locktime/refund policy, and the injectable `createEscrowToken` mint flow.
- 🟡 **Wallet UI** (native-only): needs a real mint + Android device to verify;
do not treat as production until exercised end-to-end on-device.
## Files
- `src/lib/cashuEscrow.ts` (+ `.test.ts`) — pure escrow policy & inspection.
- `src/hooks/wallet/useCashu.ts` — Cashu wallet (native-only), keyring source.
- `src/hooks/useTradeAttestations.ts` — `31872` request carries `cashu_token`.
- `NIP.md` — profile field + protocol notes.