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.
71 lines
2 KiB
TypeScript
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);
|
|
}
|