SkateSpots/src/lib/pendingEscrow.ts
SkateSpots 077fb2d5d1 fix(marketplace): payment, attestation, and escrow safety
Next batch from MARKETPLACE_FLOW_AUDIT.md:
- bolt11.ts: getBolt11Expiry / isBolt11Expired; usePayPaymentPointer refuses
  an expired invoice before paying (a lapsed offer invoice used to fail deep
  in the wallet with a confusing error).
- PayWithSats shows unsupported pointer kinds (creq/clink) as unavailable up
  front instead of enabling a button that fails after the click.
- useTradeAttestations: verify events (onlyVerified) and keep the newest per
  (author, kind, d), so a newer s=revoked supersedes a stale valid copy;
  useTradeReputation no longer trusts unverified relay events.
- EscrowRequestDialog persists the minted token locally before publishing
  (lib/pendingEscrow.ts), so a failed send cannot strand the locked funds.
2026-10-09 23:00:35 +07:00

71 lines
2 KiB
TypeScript

/**
* Device-local record of an escrow token that was minted (funds locked) but not
* yet confirmed on a relay.
*
* WHY: escrow creation mints P2PK-locked proofs and then publishes a kind-31872
* request carrying the token. If the publish fails, the token lived only in an
* in-memory variable and the locked funds were unrecoverable. Persisting it here
* before the publish keeps the buyer able to recover/refund.
*
* The token is a bearer instrument, but it is P2PK-locked to the seller with the
* buyer's refund key — so storing it is safe and is exactly what protects the
* buyer if the send does not go through.
*/
const STORAGE_KEY = 'skatespots:pending-escrow-v1';
export interface PendingEscrow {
token: string;
amountSats?: number;
lockDays?: number;
/** Unix seconds. */
at: number;
}
type Store = Record<string, PendingEscrow>;
function read(): Store {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return {};
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? (parsed as Store) : {};
} catch {
return {};
}
}
function write(store: Store): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(store));
} catch {
/* storage full / unavailable — best-effort */
}
}
function keyFor(listing: { pubkey: string; identifier: string }): string {
return `30402:${listing.pubkey}:${listing.identifier}`;
}
export function savePendingEscrow(
listing: { pubkey: string; identifier: string },
token: string,
amountSats?: number,
lockDays?: number,
): void {
const store = read();
store[keyFor(listing)] = { token, amountSats, lockDays, at: Math.floor(Date.now() / 1000) };
write(store);
}
export function getPendingEscrow(
listing: { pubkey: string; identifier: string },
): PendingEscrow | undefined {
return read()[keyFor(listing)];
}
export function clearPendingEscrow(listing: { pubkey: string; identifier: string }): void {
const store = read();
delete store[keyFor(listing)];
write(store);
}