# 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", ""]` 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.