64 lines
2.5 KiB
TypeScript
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;
|
|
}
|
|
}
|