SkateSpots/ESCROW.md
SkateSpots 0956b1368a docs(escrow): exact Android test-build steps for on-device verification
Add 'Producing a test build' (android:init -> android:sync -> assembleDebug ->
adb install) to the ESCROW.md checklist, plus the two-account requirement. The
mint-touching paths still need a real device run.
2026-09-26 23:19:47 +07:00

160 lines
7.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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): implement → verify with the **On-device test
checklist** below before treating as production. Do not use real money until
every box passes.
## On-device test checklist (Android)
Escrow is native-only and its mint paths are unverified — run this on a real
device with **tiny amounts** before trusting it. Check every box; anything that
fails is a bug to fix (or a mint that doesn't support P2PK — note which).
### Producing a test build
```bash
# First time only — generates android/ (gitignored)
npm run android:init
# Build the web assets + sync into the native project
npm run android:sync
# Debug APK (debug keystore — no release secrets needed)
cd android && ./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
```
To test both sides you need **two accounts** — easiest is one device (this build)
plus one other client/device with a different key.
### Preconditions
- [ ] Latest Android build installed (`npm run android:build`), logged in.
- [ ] Cashu wallet initialised and a reachable, trusted **mint** added (Wallet).
- [ ] A funded **NWC** connection (Wallet → NWC) to pay the mint invoice.
- [ ] **Two accounts** (seller + buyer) on two devices/profiles.
- [ ] First pass: **10 sats**, **1‑day** lock.
### 1. Seller advertises the escrow key
- [ ] Wallet → Escrow → **Publish escrow key** → toast "Escrow key published".
- [ ] Badge flips to "Escrow key active"; persists after reload.
- [ ] The kind 0 event on a relay contains `cashu_escrow` (66 hex chars, `02…`).
### 2. Buyer creates escrow
- [ ] Buyer sees **Escrow & request** on the seller's listing (native only).
- [ ] Enter 10 sats / 1 day → **Lock & request**.
- [ ] NWC pays the mint invoice; toast "Escrow created".
- [ ] Buyer balance drops by ~10 sats **+ mint/Lightning fees**.
### 3. Token looks right
- [ ] DM thread shows the escrow panel: "Escrowed 10 sats", mint host, refund date ≈ +1 day.
- [ ] Buyer can still retrieve the token (needed for the refund test).
### 4. Seller redeems on delivery
- [ ] Seller's DM shows **Confirm this sale?** + panel + **Redeem escrow**.
- [ ] Redeem → toast "Escrow redeemed"; seller balance rises ~10 sats.
- [ ] **Confirm sale** publishes the `31871`; buyer then sees **Confirm this trade**.
- [ ] Buyer confirms → both profiles show **1 verified trade** (trust-weighted).
### 5. Refund path (seller never delivers)
- [ ] New escrow, 1‑day lock; seller does **not** redeem.
- [ ] Before the deadline: no "Refund escrow" on the buyer's card (expected).
- [ ] After the deadline: **Refund escrow** appears → tap → balance returns.
- [ ] A second redeem of the same token fails cleanly (already spent).
### 6. Guard rails
- [ ] Web build: escrow action hidden; panel is read-only if a token exists.
- [ ] No NWC/WebLN → "No Lightning wallet available…" on lock.
- [ ] No mint → "No Cashu mint available…".
- [ ] Seller hasn't published `cashu_escrow` → buyer only sees the plain request.
### Watch for
- **Mint rejects P2PK** (some disable it) → creation errors; record the mint.
- **coco can't redeem P2PK** (witness signing) → redeem/refund errors.
- **Clock skew** — keep lock windows generous.
## 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.