A ['featured', expiry] tag is self-asserted, so anyone could mint one and buy top placement (the badge verified it, ordering didn't). - New src/lib/featuredVerification.ts resolves the provider pubkey (cached) and batch-checks provider-authored kind-9735 zap receipts for the featured coords. - useTours / useSkateEvents / useUpcomingSkateEvents / useLabProfiles now rank only receipts-backed featured items first. - Availability fallback: if the provider can't be resolved or the query fails, the self-asserted ordering is kept, so a provider outage never blanks paid placement.
54 lines
1.9 KiB
TypeScript
54 lines
1.9 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 { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions';
|
|
import type { NostrEvent, NostrFilter } from '@nostrify/nostrify';
|
|
|
|
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) {
|
|
for (const [n, v] of r.tags) if (n === 'a' && v) set.add(v);
|
|
}
|
|
return set;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|