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.
This commit is contained in:
parent
5d1a0e0fb0
commit
21a0390ed5
9 changed files with 39 additions and 59 deletions
10
AGENTS.md
10
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
|
||||
|
|
|
|||
|
|
@ -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() ||
|
||||
|
|
|
|||
|
|
@ -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<string>(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 }: {
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -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<PurchaseResult>({ 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. */
|
||||
|
|
|
|||
|
|
@ -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<NostrEvent[]>[] = [];
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue