import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import { MARKETPLACE_ITEM_KIND } from '@/lib/types'; import { useAppContext } from '@/hooks/useAppContext'; export interface SellerInfo { pubkey: string; accountAge: number; // Days since first event listingCount: number; activeListingCount: number; hasNip05: boolean; nip05?: string; firstSeen?: number; // Unix timestamp of first event } /** * Hook to fetch seller information including account age and listing count. */ export function useSellerInfo(pubkey: string | undefined) { const { nostr } = useNostr(); const { config } = useAppContext(); const relayUrls = config.relayMetadata.relays .filter(r => r.read) .map(r => r.url) .sort() .join(','); return useQuery({ queryKey: ['seller-info', pubkey, relayUrls], queryFn: async (c) => { if (!pubkey) return null; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]); try { const readRelays = config.relayMetadata.relays .filter(r => r.read) .map(r => r.url); // Query profile (kind 0) and marketplace listings in parallel const profileEventsPromise: Promise[] = []; const listingEventsPromise: Promise[] = []; for (const relayUrl of readRelays) { try { const relay = nostr.relay(relayUrl); profileEventsPromise.push( relay.query( [{ kinds: [0], authors: [pubkey], limit: 1 }], { signal } ).catch(() => []) ); listingEventsPromise.push( relay.query( [{ kinds: [MARKETPLACE_ITEM_KIND], authors: [pubkey], '#t': ['skateboarding'], limit: 100 }], { signal } ).catch(() => []) ); } catch (error) { // Ignore individual relay errors } } const [profileResults, listingResults] = await Promise.all([ Promise.all(profileEventsPromise), Promise.all(listingEventsPromise), ]); // Flatten results const profileEvents = profileResults.flat(); const listingEvents = listingResults.flat(); // Get profile metadata let hasNip05 = false; let nip05: string | undefined; let profileCreatedAt: number | undefined; if (profileEvents.length > 0) { const profile = profileEvents.reduce((latest, current) => current.created_at > latest.created_at ? current : latest ); try { const metadata = JSON.parse(profile.content); hasNip05 = !!metadata.nip05; nip05 = metadata.nip05; } catch (e) { // Ignore JSON parse errors } profileCreatedAt = profile.created_at; } // Deduplicate listings by d-tag const listingMap = new Map(); for (const event of listingEvents) { const d = event.tags.find(([name]: [string]) => name === 'd')?.[1]; if (d) { const key = `${event.pubkey}:${event.kind}:${d}`; const existing = listingMap.get(key); if (!existing || event.created_at > existing.created_at) { listingMap.set(key, event); } } } const uniqueListings = Array.from(listingMap.values()); // Count active listings (not expired, status = active) const now = Math.floor(Date.now() / 1000); const activeListings = uniqueListings.filter(event => { const status = event.tags.find(([name]: [string]) => name === 'status')?.[1]; const expiration = event.tags.find(([name]: [string]) => name === 'expiration')?.[1]; if (status && status !== 'active') return false; if (expiration && parseInt(expiration) < now) return false; return true; }); // Find oldest event timestamp (approximation of account age) const allTimestamps = [ ...uniqueListings.map(e => e.created_at), ...(profileCreatedAt ? [profileCreatedAt] : []), ]; const firstSeen = allTimestamps.length > 0 ? Math.min(...allTimestamps) : undefined; const accountAge = firstSeen ? Math.floor((now - firstSeen) / (24 * 60 * 60)) // Days : 0; return { pubkey, accountAge, listingCount: uniqueListings.length, activeListingCount: activeListings.length, hasNip05, nip05, firstSeen, } as SellerInfo; } catch (error) { console.warn('[useSellerInfo] Error fetching seller info:', error); return null; } }, enabled: !!pubkey, staleTime: 300000, // Cache for 5 minutes gcTime: 600000, // Keep in cache for 10 minutes }); }