feat(escrow): seller-advertised mint, preferred by the buyer
Finishes the escrow-mint work: advertiseEscrowKey now also publishes cashu_mint (the seller's preferred mint from Wallet > Escrow) on kind 0, documented in NIP.md. The buyer's EscrowRequestDialog reads the seller's cashu_mint, includes it in the mint picker even if it is not in the buyer's trust list, and prefers it (falling back to the buyer's preference, then first trusted mint). Funds locked at a mint the seller actually uses are the ones they can redeem. Still open: seller-side receive from a mint they do not already have. New marketUi.escrowRequest.sellerMintHint (en/es).
This commit is contained in:
parent
c71d176226
commit
b48f0e53ee
7 changed files with 43 additions and 12 deletions
|
|
@ -73,8 +73,11 @@ visibility, terminology, translation).
|
|||
trust/reach that mint may fail to redeem. There was also no P2PK (NUT-11)
|
||||
capability check before minting (`cashuEscrow.ts:261-282`). **Partly fixed this
|
||||
pass**: the buyer now sees and picks the mint in the dialog, and a mint that
|
||||
advertises no NUT-11 is blocked with a clear message. Still open: consult a
|
||||
seller-advertised mint, and seller-side receive from an unfamiliar mint.
|
||||
advertises no NUT-11 is blocked with a clear message. **Extended:** the seller
|
||||
can advertise a preferred mint (`cashu_mint` on kind 0, documented in NIP.md)
|
||||
and the buyer's escrow dialog prefers it, so the buyer locks at a mint the
|
||||
seller can actually redeem from. Still open: seller-side receive from an
|
||||
unfamiliar mint (the edge case where the buyer ignores the advertised mint).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -210,6 +213,11 @@ Second batch (payment, attestation, escrow):
|
|||
an in-app sats Pay button — a fiat amount can never be paid as if it were
|
||||
sats. (Still open: WebLN/Cashu fallback after a normal NWC failure, and the
|
||||
empty-preimage case.)
|
||||
12. **Seller-advertised escrow mint:** `advertiseEscrowKey` also publishes
|
||||
`cashu_mint` (the seller's preferred mint, from Wallet → Escrow) on kind 0,
|
||||
documented in NIP.md; the buyer's escrow dialog includes that mint in the
|
||||
picker and prefers it, so the buyer locks at a mint the seller can redeem
|
||||
from. (Still open: seller-side receive from a mint they do not already have.)
|
||||
|
||||
## 7. Recommended order
|
||||
|
||||
|
|
|
|||
1
NIP.md
1
NIP.md
|
|
@ -16,6 +16,7 @@ other clients, and preserved on edit:
|
|||
| `sponsors` | string | Freeform highlight of the skater's sponsors / supporters (brands, shops, people). |
|
||||
| `cordn` | string | The **Cordn coordinator** others use to reach you — private, end-to-end encrypted messaging. Holds a coordinator **pointer**: either an `nprofile` (coordinator pubkey + relay hints), Cordn's canonical `https://cordn.net/chat/coordinators?c=<nprofile>` add-link, or an `http(s)` endpoint URL. This is the discovery field: clients SHOULD offer a one-tap **add** when it parses as a pointer, and otherwise open it as a plain link. See *Private Crew Chat (Cordn / MLS)*. |
|
||||
| `cashu_escrow` | string | Cashu escrow pubkey (33-byte compressed secp256k1 hex, `02…`) used as the P2PK lock target for marketplace escrow. Derived from the Cashu wallet keyring. See `ESCROW.md`. |
|
||||
| `cashu_mint` | string | Preferred Cashu mint URL for marketplace escrow (`https://…`). Buyers are nudged to lock funds at this mint, since a mint the seller already uses is one they can redeem from. Optional; see `ESCROW.md`. |
|
||||
|
||||
`lud16` (Lightning address) is standard NIP-01 but is also editable directly in
|
||||
the SkateSpots profile form for convenience.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { Input } from '@/components/ui/input';
|
|||
import { Label } from '@/components/ui/label';
|
||||
import { useCashuEscrow } from '@/hooks/wallet/useCashuEscrow';
|
||||
import { useEscrowPrefs } from '@/hooks/wallet/useEscrowPrefs';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useTradeAttestationRequest } from '@/hooks/useTradeAttestations';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { ESCROW_MIN_LOCK_DAYS, mintSupportsP2PK } from '@/lib/cashuEscrow';
|
||||
|
|
@ -51,10 +52,20 @@ export function EscrowRequestDialog({
|
|||
// Buyer-facing mint choice: default to their preference, else the first trusted
|
||||
// mint. Shown explicitly so the buyer knows where the funds will be locked.
|
||||
const [mintChoice, setMintChoice] = useState('');
|
||||
const sellerAuthor = useAuthor(sellerPubkey);
|
||||
const sellerMintRaw = (sellerAuthor.data?.metadata as Record<string, unknown> | undefined)?.cashu_mint;
|
||||
const sellerMint =
|
||||
typeof sellerMintRaw === 'string' && sellerMintRaw.trim() ? sellerMintRaw.trim() : undefined;
|
||||
const mintOptions = escrow.mints;
|
||||
const effectiveMint = mintChoice || prefs.preferredMintUrl || mintOptions[0]?.url || '';
|
||||
// Include the seller's advertised mint even if it is not in our trust list, and
|
||||
// prefer it — a mint the seller actually uses is one they can redeem from.
|
||||
const mintChoices =
|
||||
sellerMint && !mintOptions.some((m) => m.url === sellerMint)
|
||||
? [...mintOptions, { url: sellerMint }]
|
||||
: mintOptions;
|
||||
const effectiveMint = mintChoice || sellerMint || prefs.preferredMintUrl || mintChoices[0]?.url || '';
|
||||
const p2pkBlocked =
|
||||
mintSupportsP2PK(mintOptions.find((m) => m.url === effectiveMint)?.info) === false;
|
||||
mintSupportsP2PK(mintChoices.find((m) => m.url === effectiveMint)?.info) === false;
|
||||
|
||||
const submit = async () => {
|
||||
const amountSats = parseInt(amount, 10);
|
||||
|
|
@ -141,16 +152,21 @@ export function EscrowRequestDialog({
|
|||
onChange={(e) => setMintChoice(e.target.value)}
|
||||
className="h-11 w-full rounded-md border border-gray-700 bg-gray-900 px-3 text-sm text-white"
|
||||
>
|
||||
{mintOptions.length === 0 && (
|
||||
{mintChoices.length === 0 && (
|
||||
<option value="">{t('marketUi.escrowRequest.noMints')}</option>
|
||||
)}
|
||||
{mintOptions.map((m) => (
|
||||
{mintChoices.map((m) => (
|
||||
<option key={m.url} value={m.url}>
|
||||
{m.url.replace(/^https?:\/\//, '')}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-[11px] text-gray-500">{t('marketUi.escrowRequest.mintHint')}</p>
|
||||
{sellerMint && (
|
||||
<p className="text-[11px] text-[#ccff00]">
|
||||
{t('marketUi.escrowRequest.sellerMintHint')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useCashuEscrow } from '@/hooks/wallet/useCashuEscrow';
|
||||
import { useEscrowPrefs } from '@/hooks/wallet/useEscrowPrefs';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
|
|
@ -16,6 +17,7 @@ import { Handshake, Loader2, Check } from 'lucide-react';
|
|||
export function EscrowKeyCard() {
|
||||
const { t } = useTranslation();
|
||||
const escrow = useCashuEscrow();
|
||||
const { prefs } = useEscrowPrefs();
|
||||
const { user } = useCurrentUser();
|
||||
const author = useAuthor(user?.pubkey);
|
||||
const { toast } = useToast();
|
||||
|
|
@ -29,7 +31,7 @@ export function EscrowKeyCard() {
|
|||
const publish = async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await escrow.advertiseEscrowKey();
|
||||
await escrow.advertiseEscrowKey(prefs.preferredMintUrl || escrow.defaultMintUrl || undefined);
|
||||
toast({
|
||||
title: t('walletUi.escrow.published'),
|
||||
description: t('walletUi.escrow.publishedDesc'),
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export function useCashuEscrow() {
|
|||
* Publish this user's escrow pubkey to their kind 0 profile (`cashu_escrow`)
|
||||
* so buyers can lock funds to them.
|
||||
*/
|
||||
const advertiseEscrowKey = useCallback(async (): Promise<string> => {
|
||||
const advertiseEscrowKey = useCallback(async (mintUrl?: string): Promise<string> => {
|
||||
if (!user) throw new Error('Log in first.');
|
||||
const pubkey = await getEscrowPubkey();
|
||||
if (!pubkey) throw new Error('Could not derive your escrow key.');
|
||||
|
|
@ -138,10 +138,12 @@ export function useCashuEscrow() {
|
|||
throw new Error('Your profile has not loaded yet. Try again in a moment.');
|
||||
}
|
||||
const current = (author.data.metadata ?? {}) as Record<string, unknown>;
|
||||
await publish({
|
||||
kind: 0,
|
||||
content: JSON.stringify({ ...current, cashu_escrow: pubkey }),
|
||||
});
|
||||
const next: Record<string, unknown> = { ...current, cashu_escrow: pubkey };
|
||||
// Advertise the mint the seller wants escrow funded at, so a buyer can prefer
|
||||
// one the seller can actually redeem from.
|
||||
const mint = mintUrl?.trim();
|
||||
if (mint) next.cashu_mint = mint;
|
||||
await publish({ kind: 0, content: JSON.stringify(next) });
|
||||
return pubkey;
|
||||
}, [user, getEscrowPubkey, author.data, publish]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2081,6 +2081,7 @@ marketUi: {
|
|||
daysHint: "After this many days you can reclaim the funds if the seller hasn't delivered.",
|
||||
lockedNotSent: 'Funds are locked in escrow and saved on this device, but the request did not send. Reopen the listing to retry. Do not pay again.',
|
||||
mintLabel: 'Mint (where the funds are locked)',
|
||||
sellerMintHint: 'The seller prefers this mint; funds locked here are the easiest for them to redeem.',
|
||||
mintHint: 'Escrow lives at this mint. The seller redeems here, so pick one you both trust.',
|
||||
noMints: 'No mints available',
|
||||
p2pkUnsupported: 'This mint does not advertise P2PK (NUT-11) support, so escrow cannot be locked here. Choose another mint.',
|
||||
|
|
|
|||
|
|
@ -2081,6 +2081,7 @@ marketUi: {
|
|||
daysHint: 'Tras estos días puedes recuperar los fondos si el vendedor no ha entregado.',
|
||||
lockedNotSent: 'Los fondos están bloqueados en escrow y guardados en este dispositivo, pero la solicitud no se envió. Reabre el anuncio para reintentar. No pagues otra vez.',
|
||||
mintLabel: 'Mint (donde se bloquean los fondos)',
|
||||
sellerMintHint: 'El vendedor prefiere este mint; los fondos bloqueados aquí son los más fáciles de canjear para él.',
|
||||
mintHint: 'El escrow vive en este mint. El vendedor lo canjea aquí, así que elige uno en el que confiéis ambos.',
|
||||
noMints: 'No hay mints disponibles',
|
||||
p2pkUnsupported: 'Este mint no anuncia compatibilidad con P2PK (NUT-11), así que el escrow no se puede bloquear aquí. Elige otro mint.',
|
||||
|
|
|
|||
Loading…
Reference in a new issue