SkateSpots/src/hooks/useRelatrScore.ts

246 lines
7.9 KiB
TypeScript

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", "<target-pubkey>"],
* ["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<string, RelatrScore>();
}
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]);
const scores = new Map<string, RelatrScore>();
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<string, RelatrScore>();
}
},
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');
}