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.
93 lines
4.1 KiB
Markdown
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.
|