import { useQuery } from '@tanstack/react-query'; import { useNostr } from '@nostrify/react'; import { onlyVerified } from '@/lib/verifyEvents'; /** * Relatr NIP-85 Trust Score Hook * * Queries Relatr service for global trust scores using NIP-85 (Trusted Assertions). * Relatr publishes trust scores as kind 30382 addressable events. * * NIP-85 Format: * { * "kind": 30382, * "tags": [ * ["d", ""], * ["rank", "85"], // Trust score 0-100 * ["followers", "1234"], * // ... other metrics * ] * } * * Documentation: * - NIP-85: https://github.com/nostr-protocol/nips/blob/master/85.md * - Relatr: https://www.relatr.xyz/ * - GitHub: https://github.com/ContextVM/relatr */ interface RelatrConfig { enabled: boolean; servicePubkey: string; // Relatr service's pubkey publishing trust scores relay: string; // Relay where Relatr publishes assertions } /** * Relatr service configuration * * ContextVM's official Relatr service publishes trust scores to Nostr relays. * Service pubkey: npub1dvmcpmefwtnn6dctsj3728n64xhrf06p9yude77echmrkgs5zmyqw33jdm * Hex: 6b3780ef2972e73d370b84a3e51e7aa9ae34bf412938dcfbd9c5f63b221416c8 * * Auto-enabled for all logged-in users to provide spam protection and trust scores. */ const RELATR_CONFIG: RelatrConfig = { enabled: true, // Auto-enabled for all users servicePubkey: '6b3780ef2972e73d370b84a3e51e7aa9ae34bf412938dcfbd9c5f63b221416c8', // ContextVM's Relatr service relay: 'wss://relay.contextvm.org', // ContextVM's relay where Relatr publishes NIP-85 events }; export interface RelatrScore { pubkey: string; rank: number; // 0-100 trust score followers?: number; firstCreatedAt?: number; postCount?: number; replyCount?: number; reactionsCount?: number; zapAmountReceived?: number; zapAmountSent?: number; timestamp: number; } /** * Fetch Relatr global trust score for a pubkey using NIP-85 * * @param targetPubkey - The pubkey to get trust score for * @param sourcePubkey - Optional perspective pubkey (most Relatr services use a default source) * @returns RelatrScore or null if unavailable */ export function useRelatrScore(targetPubkey: string | undefined, sourcePubkey?: string) { const { nostr } = useNostr(); return useQuery({ queryKey: ['relatr-score', targetPubkey, sourcePubkey], queryFn: async (c) => { if (!targetPubkey || !RELATR_CONFIG.enabled || !RELATR_CONFIG.servicePubkey) { return null; } const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]); try { // Query NIP-85 Trusted Assertion for this user // Kind 30382 = User trust assertions // d tag = target pubkey // Signature-verify assertions: a forged kind-30382 (spoofed author, // invalid sig) from a rogue relay must not grant fake trust. const events = onlyVerified(await nostr.relay(RELATR_CONFIG.relay).query( [{ kinds: [30382], authors: [RELATR_CONFIG.servicePubkey], '#d': [targetPubkey], limit: 1, }], { signal } )); if (events.length === 0) { console.log(`[Relatr] No trust assertion found for ${targetPubkey.slice(0, 8)}`); return null; } const event = events[0]; // Parse NIP-85 tags const getRankTag = event.tags.find(([name]) => name === 'rank')?.[1]; const rank = getRankTag ? parseInt(getRankTag) : undefined; if (rank === undefined) { console.warn(`[Relatr] Assertion found but no rank tag for ${targetPubkey.slice(0, 8)}`); return null; } // Parse optional metrics const getTag = (name: string) => event.tags.find(([n]) => n === name)?.[1]; const getIntTag = (name: string) => { const value = getTag(name); return value ? parseInt(value) : undefined; }; const score: RelatrScore = { pubkey: targetPubkey, rank, followers: getIntTag('followers'), firstCreatedAt: getIntTag('first_created_at'), postCount: getIntTag('post_cnt'), replyCount: getIntTag('reply_cnt'), reactionsCount: getIntTag('reactions_cnt'), zapAmountReceived: getIntTag('zap_amt_recd'), zapAmountSent: getIntTag('zap_amt_sent'), timestamp: event.created_at, }; console.log(`[Relatr] Trust score for ${targetPubkey.slice(0, 8)}: ${rank}`); return score; } catch (error) { console.warn('[Relatr] Error fetching trust score:', error); return null; } }, enabled: !!targetPubkey && RELATR_CONFIG.enabled && !!RELATR_CONFIG.servicePubkey, staleTime: 3600000, // Cache for 1 hour gcTime: 86400000, // Keep in cache for 24 hours retry: 1, }); } /** * Batch fetch Relatr scores for multiple pubkeys * * More efficient than individual queries when checking many users. */ export function useBatchRelatrScores(targetPubkeys: string[]) { const { nostr } = useNostr(); return useQuery({ queryKey: ['batch-relatr-scores', targetPubkeys.sort().join(',')], queryFn: async (c) => { if (!RELATR_CONFIG.enabled || !RELATR_CONFIG.servicePubkey || targetPubkeys.length === 0) { return new Map(); } const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]); const scores = new Map(); try { // Query all trust assertions for these pubkeys const events = onlyVerified(await nostr.relay(RELATR_CONFIG.relay).query( [{ kinds: [30382], authors: [RELATR_CONFIG.servicePubkey], '#d': targetPubkeys, limit: targetPubkeys.length, }], { signal } )); console.log(`[Relatr] Found ${events.length} trust assertions for ${targetPubkeys.length} pubkeys`); // Parse each assertion for (const event of events) { const dTag = event.tags.find(([name]) => name === 'd')?.[1]; const rankTag = event.tags.find(([name]) => name === 'rank')?.[1]; if (!dTag || !rankTag) continue; const rank = parseInt(rankTag); const getTag = (name: string) => event.tags.find(([n]) => n === name)?.[1]; const getIntTag = (name: string) => { const value = getTag(name); return value ? parseInt(value) : undefined; }; scores.set(dTag, { pubkey: dTag, rank, followers: getIntTag('followers'), firstCreatedAt: getIntTag('first_created_at'), postCount: getIntTag('post_cnt'), replyCount: getIntTag('reply_cnt'), reactionsCount: getIntTag('reactions_cnt'), zapAmountReceived: getIntTag('zap_amt_recd'), zapAmountSent: getIntTag('zap_amt_sent'), timestamp: event.created_at, }); } return scores; } catch (error) { console.warn('[Relatr] Error fetching batch trust scores:', error); return new Map(); } }, enabled: RELATR_CONFIG.enabled && !!RELATR_CONFIG.servicePubkey && targetPubkeys.length > 0, staleTime: 3600000, gcTime: 86400000, retry: 1, }); } /** * Get Relatr configuration */ export function getRelatrConfig(): RelatrConfig { return { ...RELATR_CONFIG }; } /** * Enable Relatr integration * Call this after confirming the service pubkey */ export function enableRelatr(servicePubkey: string, relay?: string) { RELATR_CONFIG.enabled = true; RELATR_CONFIG.servicePubkey = servicePubkey; if (relay) { RELATR_CONFIG.relay = relay; } console.log('[Relatr] Enabled with service pubkey:', servicePubkey.slice(0, 16) + '...'); } /** * Disable Relatr integration */ export function disableRelatr() { RELATR_CONFIG.enabled = false; console.log('[Relatr] Disabled'); }