The entire skate-spots codebase was accidentally copied into src/src/ and public/public/ instead of src/ and public/ directly. This left the real src/ directory as blank MKStack template stubs, explaining why the landing page showed the default template instead of SkateSpots. Changes made: - Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx, all pages, components, hooks, contexts, lib, i18n, test) - Copied all files from public/public/ up to public/ (images, manifest, etc.) - Deleted src/src/ and public/public/ nested directories - Removed HelloWorld.tsx template stub from pages/ - Retained the previously fixed index.css (Tailwind v3 directives) Full skate-spots feature set is now active: - All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.) - NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler - Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.) - Outfit Variable font, Amber pre-check in main.tsx - All hooks (useSkateSpots, useCoaches, useZaps, etc.) - i18n support (en/es) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
243 lines
7.6 KiB
TypeScript
243 lines
7.6 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { useNostr } from '@nostrify/react';
|
|
|
|
/**
|
|
* 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
|
|
const events = 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 = 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');
|
|
}
|