From 21a0390ed5574008339e57eb9c4381b91dcb3c85 Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sat, 26 Sep 2026 22:36:14 +0700 Subject: [PATCH] refactor(relays): route every relay consumer through useRelays() So Local relays are used and Blocked relays are excluded EVERYWHERE, not just in the NPool routers: - useListingReports, useSellerInfo, useSkateRelays (read+write), useZaps (NIP-57 relays tag), usePurchasePro, CollectionDetailPage, CrewForm, useDirectMessages write fallback - AppProvider's empty-relays defaults check is the only intentional direct reader - AGENTS.md updated Gate: 101 tests, 0 errors/warnings, boot smoke green. --- AGENTS.md | 10 ++++++---- src/components/CrewForm.tsx | 6 +++--- src/hooks/useDirectMessages.ts | 8 ++++---- src/hooks/useListingReports.ts | 26 +++++++------------------- src/hooks/usePurchasePro.ts | 8 ++++---- src/hooks/useSellerInfo.ts | 14 ++++---------- src/hooks/useSkateRelays.ts | 10 ++++------ src/hooks/useZaps.ts | 8 ++++---- src/pages/CollectionDetailPage.tsx | 8 +++----- 9 files changed, 39 insertions(+), 59 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4736d42..926f919 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,10 +66,12 @@ session with **no other memory of this project**. So: "Trusted" (no distinct behaviour — would be empty UI), and Amethyst's proxy/ broadcast/private-home (we don't have that relay-discovery model) — don't add UI for features we don't implement. - - ⚠️ A few hooks build relay lists directly from `config.relayMetadata.relays` - (listing reports, seller info, skate relays, zaps) and therefore do NOT yet - honour Local/Blocked — migrate them to `useRelays()` when touched. DMs are - migrated, but their DM relays aren't block-filtered either. + - All relay consumers now go through **`useRelays()`** (read/write — blocklist + applied, local included): listing reports, seller info, skate relays, zaps, + Sasquatch purchase, collections, crew form, and DMs. AppProvider's + empty-relays defaults check is the only intentional direct reader. + - ⚠️ DM **inbox** relays (kind 10050) are delivered-to/read-from but are not + yet block-filtered (they're a separate list from the NIP-65 `relays`). - **Group DMs**: NIP-17 (`useDirectMessages`, `GroupConversationPage`). - **Mints directory**: `/mints` page lists Cashu mints from the community **MintRadar** project. MintRadar has **no CORS**, so it's fetched same-origin diff --git a/src/components/CrewForm.tsx b/src/components/CrewForm.tsx index c4c2c18..5eeed5f 100644 --- a/src/components/CrewForm.tsx +++ b/src/components/CrewForm.tsx @@ -22,7 +22,7 @@ import { CrewSponsorPicker } from '@/components/crew/CrewSponsorPicker'; import { NofferField } from '@/components/NofferField'; import { SKATE_CREW_KIND, SKATE_SPOT_KIND, SKATE_SHOP_KIND, parseAddrCoord, ZAP_GOAL_KIND, CREW_VIBE_LABELS, CREW_FOCUS_LABELS, CREW_FOCUS_DESCRIPTIONS, CREW_FOCUS_TAG, CREW_LOOKING_FOR_OPTIONS, CREW_ROLE_META, CREW_ROLE_ORDER, SHOW_CREW_TREASURY, isValidNoffer, type SkateCrew, type CrewFocus, type CrewRole } from '@/lib/types'; import { useCrewGoal } from '@/hooks/useCrewGoal'; -import { useAppContext } from '@/hooks/useAppContext'; +import { useRelays } from '@/lib/relays'; import { cn } from '@/lib/utils'; import { Users, Upload, X, Loader2, MapPin, Plus, ShieldCheck, AtSign, HardHat, Camera, GraduationCap, Sparkles, Target, Megaphone, Music, Palette, Dumbbell, Eye, Store, Film, Check, ArrowRight, ArrowLeft as ArrowLeftIcon, Server } from 'lucide-react'; @@ -286,7 +286,7 @@ export function CrewForm({ existing }: CrewFormProps) { // Crew treasury (NIP-75 zap goal). Load existing goal to prefill when editing. const { data: existingGoal } = useCrewGoal(existing?.goalRef); - const { config } = useAppContext(); + const { write } = useRelays(); const [goalEnabled, setGoalEnabled] = useState(!!existing?.goalRef); const [goalTitle, setGoalTitle] = useState(''); const [goalTarget, setGoalTarget] = useState(''); @@ -420,7 +420,7 @@ export function CrewForm({ existing }: CrewFormProps) { let goalRef: string | undefined = existing?.goalRef; const targetSats = parseInt(goalTarget, 10); if (goalEnabled && goalTitle.trim() && targetSats > 0) { - const relayUrls = config.relayMetadata.relays.filter((r) => r.write).map((r) => r.url); + const relayUrls = write; const needsNewGoal = !existingGoal || existingGoal.description !== goalTitle.trim() || diff --git a/src/hooks/useDirectMessages.ts b/src/hooks/useDirectMessages.ts index fa5b688..f4e09e3 100644 --- a/src/hooks/useDirectMessages.ts +++ b/src/hooks/useDirectMessages.ts @@ -8,6 +8,7 @@ import { useNostrLogin } from '@nostrify/react/login'; import { useCurrentUser } from './useCurrentUser'; import { useAppContext } from './useAppContext'; import { hasNip44 as checkNip44, isInteractiveSigner } from '@/lib/signerCapabilities'; +import { useRelays } from '@/lib/relays'; import type { RelayMetadata } from '@/contexts/AppContext'; /** @@ -124,6 +125,7 @@ export function useSendDirectMessage() { const { nostr } = useNostr(); const { user } = useCurrentUser(); const { config } = useAppContext(); + const { write } = useRelays(); const queryClient = useQueryClient(); const sendMessage = useCallback(async ( @@ -189,9 +191,7 @@ export function useSendDirectMessage() { // 4. Deliver to NIP-17 DM relays (kind 10050): the recipients' inboxes plus // our own, so both sides can read from their DM relays. Fall back to the // NIP-65 write relays when nobody has published a DM relay list. - const writeRelays = config.relayMetadata.relays - .filter(r => r.write) - .map(r => r.url); + const writeRelays = write; const dmTargets = [...new Set([...recipients, user.pubkey])]; const deliverRelays = new Set(config.relayMetadata.dmRelays ?? []); @@ -230,7 +230,7 @@ export function useSendDirectMessage() { queryClient.invalidateQueries({ queryKey: ['group-conversations'] }); } - }, [user, nostr, config, queryClient]); + }, [user, nostr, config, write, queryClient]); return useMutation({ mutationFn: async ({ recipients, content, replyTo, subject }: { diff --git a/src/hooks/useListingReports.ts b/src/hooks/useListingReports.ts index a8b22a8..3b4f858 100644 --- a/src/hooks/useListingReports.ts +++ b/src/hooks/useListingReports.ts @@ -1,7 +1,7 @@ import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import type { NostrEvent } from '@nostrify/nostrify'; -import { useAppContext } from '@/hooks/useAppContext'; +import { useRelays } from '@/lib/relays'; const REPORT_KIND = 1984; // NIP-56 Report kind @@ -21,13 +21,9 @@ export interface ListingReport { */ export function useListingReports(eventId: string, pubkey: string) { const { nostr } = useNostr(); - const { config } = useAppContext(); + const { read } = useRelays(); - const relayUrls = config.relayMetadata.relays - .filter(r => r.read) - .map(r => r.url) - .sort() - .join(','); + const relayUrls = read.slice().sort().join(','); return useQuery({ queryKey: ['listing-reports', eventId, relayUrls], @@ -35,9 +31,7 @@ export function useListingReports(eventId: string, pubkey: string) { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(8000)]); try { - const readRelays = config.relayMetadata.relays - .filter(r => r.read) - .map(r => r.url); + const readRelays = read; const allEvents: NostrEvent[] = []; @@ -115,13 +109,9 @@ export function useListingReports(eventId: string, pubkey: string) { */ export function useBatchListingReports(eventIds: string[]) { const { nostr } = useNostr(); - const { config } = useAppContext(); + const { read } = useRelays(); - const relayUrls = config.relayMetadata.relays - .filter(r => r.read) - .map(r => r.url) - .sort() - .join(','); + const relayUrls = read.slice().sort().join(','); return useQuery({ queryKey: ['batch-listing-reports', eventIds.sort().join(','), relayUrls], @@ -137,9 +127,7 @@ export function useBatchListingReports(eventIds: string[]) { } try { - const readRelays = config.relayMetadata.relays - .filter(r => r.read) - .map(r => r.url); + const readRelays = read; const allEvents: NostrEvent[] = []; diff --git a/src/hooks/usePurchasePro.ts b/src/hooks/usePurchasePro.ts index 0f65624..ab4c7a7 100644 --- a/src/hooks/usePurchasePro.ts +++ b/src/hooks/usePurchasePro.ts @@ -19,7 +19,7 @@ import { useNostr } from '@nostrify/react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNWC } from '@/hooks/useNWCContext'; import { useWallet } from '@/hooks/useWallet'; -import { useAppContext } from '@/hooks/useAppContext'; +import { useRelays } from '@/lib/relays'; import { useSendDirectMessage } from '@/hooks/useDirectMessages'; import { PLATFORM_OWNER_PUBKEYS } from '@/hooks/useListingRestrictions'; import { @@ -46,7 +46,7 @@ export function usePurchasePro() { const { user } = useCurrentUser(); const { getActiveConnection, sendPayment } = useNWC(); const { webln } = useWallet(); - const { config } = useAppContext(); + const { read, write } = useRelays(); const { mutateAsync: sendDM } = useSendDirectMessage(); const [result, setResult] = useState({ invoice: null, state: 'idle', error: null }); @@ -121,7 +121,7 @@ export function usePurchasePro() { const now = Math.floor(Date.now() / 1000); const expiry = now + tier.days * DAY_SECONDS; const amountMsats = tier.sats * 1000; - const relays = config.relayMetadata.relays.map((r) => r.url); + const relays = [...new Set([...read, ...write])]; const comment = `Sasquatch Pro — ${tier.label} 🦧`; // Try a NIP-57 zap first (unforgeable, npub-bound). Fall back to a plain @@ -198,7 +198,7 @@ export function usePurchasePro() { }); } }, - [user, config, getActiveConnection, sendPayment, webln, publishReceipt, sendConfirmationDM], + [user, read, write, getActiveConnection, sendPayment, webln, publishReceipt, sendConfirmationDM], ); /** Called after a manual (QR) payment to record the pass locally. */ diff --git a/src/hooks/useSellerInfo.ts b/src/hooks/useSellerInfo.ts index 34a7b97..d4f7b35 100644 --- a/src/hooks/useSellerInfo.ts +++ b/src/hooks/useSellerInfo.ts @@ -2,7 +2,7 @@ import { useNostr } from '@nostrify/react'; import { useQuery } from '@tanstack/react-query'; import type { NostrEvent } from '@nostrify/nostrify'; import { MARKETPLACE_ITEM_KIND } from '@/lib/types'; -import { useAppContext } from '@/hooks/useAppContext'; +import { useRelays } from '@/lib/relays'; export interface SellerInfo { pubkey: string; @@ -19,13 +19,9 @@ export interface SellerInfo { */ export function useSellerInfo(pubkey: string | undefined) { const { nostr } = useNostr(); - const { config } = useAppContext(); + const { read } = useRelays(); - const relayUrls = config.relayMetadata.relays - .filter(r => r.read) - .map(r => r.url) - .sort() - .join(','); + const relayUrls = read.slice().sort().join(','); return useQuery({ queryKey: ['seller-info', pubkey, relayUrls], @@ -35,9 +31,7 @@ export function useSellerInfo(pubkey: string | undefined) { const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]); try { - const readRelays = config.relayMetadata.relays - .filter(r => r.read) - .map(r => r.url); + const readRelays = read; // Query profile (kind 0) and marketplace listings in parallel const profileEventsPromise: Promise[] = []; diff --git a/src/hooks/useSkateRelays.ts b/src/hooks/useSkateRelays.ts index a57f4d4..2eab58e 100644 --- a/src/hooks/useSkateRelays.ts +++ b/src/hooks/useSkateRelays.ts @@ -1,6 +1,7 @@ import { useNostr } from '@nostrify/react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAppContext } from '@/hooks/useAppContext'; +import { useRelays } from '@/lib/relays'; import { useCitrine } from '@/hooks/useCitrine'; import { SKATE_RELAY } from '@/lib/types'; import type { NostrEvent, NostrFilter } from '@nostrify/nostrify'; @@ -17,6 +18,7 @@ export function useSkateRelays() { const { nostr } = useNostr(); const { user } = useCurrentUser(); const { config } = useAppContext(); + const { read, write } = useRelays(); const citrine = useCitrine(); const isLoggedIn = !!user; @@ -43,9 +45,7 @@ export function useSkateRelays() { } // Logged in: check setting - const userRelays = config.relayMetadata.relays - .filter(r => r.read) - .map(r => r.url); + const userRelays = read; if (useSkateRelay) { // Include community relay + user relays (deduplicated) @@ -67,9 +67,7 @@ export function useSkateRelays() { return [SKATE_RELAY]; } - const userRelays = config.relayMetadata.relays - .filter(r => r.write) - .map(r => r.url); + const userRelays = write; if (useSkateRelay) { // Include community relay + user relays (deduplicated) diff --git a/src/hooks/useZaps.ts b/src/hooks/useZaps.ts index 3692933..6cb796e 100644 --- a/src/hooks/useZaps.ts +++ b/src/hooks/useZaps.ts @@ -1,7 +1,7 @@ import { useState, useMemo, useEffect, useCallback } from 'react'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; -import { useAppContext } from '@/hooks/useAppContext'; +import { useRelays } from '@/lib/relays'; import { useToast } from '@/hooks/useToast'; import { useNWC } from '@/hooks/useNWCContext'; import type { NWCConnection } from '@/hooks/useNWC'; @@ -23,7 +23,7 @@ export function useZaps( const { nostr } = useNostr(); const { toast } = useToast(); const { user } = useCurrentUser(); - const { config } = useAppContext(); + const { read, write } = useRelays(); const queryClient = useQueryClient(); // Handle the case where an empty array is passed (from ZapButton when external data is provided) @@ -162,7 +162,7 @@ export function useZaps( const isAddressable = actualTarget.kind >= 30000 && actualTarget.kind < 40000; const zapTags: string[][] = [ - ['relays', ...config.relayMetadata.relays.map((r) => r.url)], + ['relays', ...read, ...write], ['amount', String(zapAmount)], ['p', actualTarget.pubkey], ]; @@ -202,7 +202,7 @@ export function useZaps( assertInvoiceAmount(newInvoice, amount); return newInvoice; - }, [user, actualTarget, author.data, config.relayMetadata.relays]); + }, [user, actualTarget, author.data, read, write]); const zap = async (amount: number, comment: string) => { if (amount <= 0) { diff --git a/src/pages/CollectionDetailPage.tsx b/src/pages/CollectionDetailPage.tsx index 9a4c208..b0551a7 100644 --- a/src/pages/CollectionDetailPage.tsx +++ b/src/pages/CollectionDetailPage.tsx @@ -14,7 +14,7 @@ import { Badge } from '@/components/ui/badge'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; import { useToast } from '@/hooks/useToast'; import { useCurrentUser } from '@/hooks/useCurrentUser'; -import { useAppContext } from '@/hooks/useAppContext'; +import { useRelays } from '@/lib/relays'; import { useSpotCollection, useRemoveSpotFromCollection, useDeleteSpotCollection } from '@/hooks/useSpotCollections'; import type { NostrEvent } from '@nostrify/nostrify'; import { SKATE_SPOT_KIND, parseSkateSpot, type SkateSpot } from '@/lib/types'; @@ -46,7 +46,7 @@ export default function CollectionDetailPage() { const { data: collection, isLoading, error, refetch } = useSpotCollection(identifier || '', pubkey); const { user } = useCurrentUser(); const { nostr } = useNostr(); - const { config } = useAppContext(); + const { read } = useRelays(); const { removeSpotFromCollection, isPending: isRemoving } = useRemoveSpotFromCollection(); const { deleteCollection, isPending: isDeleting } = useDeleteSpotCollection(); const { toast } = useToast(); @@ -69,9 +69,7 @@ export default function CollectionDetailPage() { if (!collection || collection.spotRefs.length === 0) return []; const signal = AbortSignal.any([c.signal, AbortSignal.timeout(15000)]); - const readRelays = config.relayMetadata.relays - .filter(r => r.read) - .map(r => r.url); + const readRelays = read; // Parse spot references to build filters const spotFilters = collection.spotRefs.map((ref) => {