SkateSpots/src/lib/featuredVerification.ts

64 lines
2.5 KiB
TypeScript

/**
* Featured ("Pro") ranking verification.
*
* SECURITY: a `["featured", <expiry>]` tag is SELF-ASSERTED — anyone can add it
* and get top placement, and `useFeaturedVerification` only downgrades the badge,
* never the ordering. This helper resolves which featured coordinates are backed
* by a **provider-authored NIP-57 zap receipt** so list hooks can rank only those.
*
* Availability over strictness: if the provider pubkey can't be resolved or the
* receipt query fails, it returns `null` and the caller keeps the self-asserted
* ordering (so a provider outage never blanks paid placement).
*/
import { PRO_LIGHTNING_ADDRESS } from '@/lib/sasquatchPro';
import { getZapProviderPubkey } from '@/lib/lightningAddress';
import { getSatoshisFromBolt11 } from '@/lib/bolt11';
import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
import type { NostrEvent, NostrFilter } from '@nostrify/nostrify';
/** Minimum paid amount for a "featured" receipt to count (closes the 1-sat self-zap hole). */
const MIN_FEATURED_SATS = 1000;
let providerPromise: Promise<string | null> | null = null;
function cachedProvider(): Promise<string | null> {
if (!providerPromise) {
providerPromise = getZapProviderPubkey(PRO_LIGHTNING_ADDRESS).catch(() => null);
}
return providerPromise;
}
/**
* @returns the set of featured coords with a verified provider receipt, or
* `null` when verification is unavailable (caller should not demote).
*/
export async function verifiedFeaturedCoords(
coords: string[],
query: (filter: NostrFilter) => Promise<NostrEvent[]>,
): Promise<Set<string> | null> {
if (coords.length === 0) return null;
const provider = await cachedProvider();
if (!provider) return null;
try {
const receipts = await query({
kinds: [9735],
authors: [provider],
'#a': coords,
'#p': PLATFORM_OWNER_PUBKEYS,
limit: coords.length * 4,
});
const set = new Set<string>();
for (const r of receipts) {
// Require a real payment — a receipt's mere presence let anyone self-zap
// 1 sat and buy a "Verified" badge. (The exact tier also depends on the
// expiry; a floor is enough to close the hole.)
const invoice = r.tags.find(([n]) => n === 'bolt11')?.[1];
const sats = invoice ? getSatoshisFromBolt11(invoice) : 0;
if (!Number.isFinite(sats) || sats < MIN_FEATURED_SATS) continue;
for (const [n, v] of r.tags) if (n === 'a' && v) set.add(v);
}
return set;
} catch {
return null;
}
}