/** * NDK Query and Subscription Patterns * * Examples from: src/queries/orders.tsx, src/queries/payment.tsx */ import NDK, { NDKEvent, NDKFilter, NDKSubscription } from '@nostr-dev-kit/ndk' // ============================================================ // BASIC FETCH (ONE-TIME QUERY) // ============================================================ const fetchNotes = async (ndk: NDK, authorPubkey: string, limit: number = 50) => { const filter: NDKFilter = { kinds: [1], // Text notes authors: [authorPubkey], limit } // Fetch returns a Set const events = await ndk.fetchEvents(filter) // Convert to array and sort by timestamp const eventArray = Array.from(events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0) ) return eventArray } // ============================================================ // FETCH WITH MULTIPLE FILTERS // ============================================================ const fetchProductsByMultipleAuthors = async ( ndk: NDK, pubkeys: string[] ) => { const filter: NDKFilter = { kinds: [30402], // Product listings authors: pubkeys, limit: 100 } const events = await ndk.fetchEvents(filter) return Array.from(events) } // ============================================================ // FETCH WITH TAG FILTERS // ============================================================ const fetchOrderEvents = async (ndk: NDK, orderId: string) => { const filter: NDKFilter = { kinds: [16, 17], // Order and payment receipt '#order': [orderId], // Tag filter (note the # prefix) } const events = await ndk.fetchEvents(filter) return Array.from(events) } // ============================================================ // FETCH WITH TIME RANGE // ============================================================ const fetchRecentEvents = async ( ndk: NDK, kind: number, hoursAgo: number = 24 ) => { const now = Math.floor(Date.now() / 1000) const since = now - (hoursAgo * 3600) const filter: NDKFilter = { kinds: [kind], since, until: now, limit: 100 } const events = await ndk.fetchEvents(filter) return Array.from(events) } // ============================================================ // FETCH BY EVENT ID // ============================================================ const fetchEventById = async (ndk: NDK, eventId: string) => { const filter: NDKFilter = { ids: [eventId] } const events = await ndk.fetchEvents(filter) if (events.size === 0) { return null } return Array.from(events)[0] } // ============================================================ // BASIC SUBSCRIPTION (REAL-TIME) // ============================================================ const subscribeToNotes = ( ndk: NDK, authorPubkey: string, onEvent: (event: NDKEvent) => void ): NDKSubscription => { const filter: NDKFilter = { kinds: [1], authors: [authorPubkey] } const subscription = ndk.subscribe(filter, { closeOnEose: false // Keep open for real-time updates }) // Event handler subscription.on('event', (event: NDKEvent) => { onEvent(event) }) // EOSE (End of Stored Events) handler subscription.on('eose', () => { console.log('✅ Received all stored events') }) return subscription } // ============================================================ // SUBSCRIPTION WITH CLEANUP // ============================================================ const createManagedSubscription = ( ndk: NDK, filter: NDKFilter, handlers: { onEvent: (event: NDKEvent) => void onEose?: () => void onClose?: () => void } ) => { const subscription = ndk.subscribe(filter, { closeOnEose: false }) subscription.on('event', handlers.onEvent) if (handlers.onEose) { subscription.on('eose', handlers.onEose) } if (handlers.onClose) { subscription.on('close', handlers.onClose) } // Return cleanup function return () => { subscription.stop() console.log('✅ Subscription stopped') } } // ============================================================ // MONITORING SPECIFIC EVENT // ============================================================ const monitorPaymentReceipt = ( ndk: NDK, orderId: string, invoiceId: string, onPaymentReceived: (preimage: string) => void ): NDKSubscription => { const sessionStart = Math.floor(Date.now() / 1000) const filter: NDKFilter = { kinds: [17], // Payment receipt '#order': [orderId], '#payment-request': [invoiceId], since: sessionStart - 30 // 30 second buffer for clock skew } const subscription = ndk.subscribe(filter, { closeOnEose: false }) subscription.on('event', (event: NDKEvent) => { // Verify event is recent if (event.created_at && event.created_at < sessionStart - 30) { console.log('⏰ Ignoring old receipt') return } // Verify it's the correct invoice const paymentRequestTag = event.tags.find(tag => tag[0] === 'payment-request') if (paymentRequestTag?.[1] !== invoiceId) { return } // Extract preimage const paymentTag = event.tags.find(tag => tag[0] === 'payment') const preimage = paymentTag?.[3] || 'external-payment' console.log('✅ Payment received!') subscription.stop() onPaymentReceived(preimage) }) return subscription } // ============================================================ // REACT INTEGRATION PATTERN // ============================================================ import { useEffect, useState } from 'react' function useOrderSubscription(ndk: NDK | null, orderId: string) { const [events, setEvents] = useState([]) const [eosed, setEosed] = useState(false) useEffect(() => { if (!ndk || !orderId) return const filter: NDKFilter = { kinds: [16, 17], '#order': [orderId] } const subscription = ndk.subscribe(filter, { closeOnEose: false }) subscription.on('event', (event: NDKEvent) => { setEvents(prev => { // Avoid duplicates if (prev.some(e => e.id === event.id)) { return prev } return [...prev, event].sort((a, b) => (a.created_at || 0) - (b.created_at || 0) ) }) }) subscription.on('eose', () => { setEosed(true) }) // Cleanup on unmount return () => { subscription.stop() } }, [ndk, orderId]) return { events, eosed } } // ============================================================ // REACT QUERY INTEGRATION // ============================================================ import { useQuery, useQueryClient } from '@tanstack/react-query' // Query function const fetchProducts = async (ndk: NDK, pubkey: string) => { if (!ndk) throw new Error('NDK not initialized') const filter: NDKFilter = { kinds: [30402], authors: [pubkey] } const events = await ndk.fetchEvents(filter) return Array.from(events) } // Hook with subscription for real-time updates function useProductsWithSubscription(ndk: NDK | null, pubkey: string) { const queryClient = useQueryClient() // Initial query const query = useQuery({ queryKey: ['products', pubkey], queryFn: () => fetchProducts(ndk!, pubkey), enabled: !!ndk && !!pubkey, staleTime: 30000 }) // Real-time subscription useEffect(() => { if (!ndk || !pubkey) return const filter: NDKFilter = { kinds: [30402], authors: [pubkey] } const subscription = ndk.subscribe(filter, { closeOnEose: false }) subscription.on('event', () => { // Invalidate query to trigger refetch queryClient.invalidateQueries({ queryKey: ['products', pubkey] }) }) return () => { subscription.stop() } }, [ndk, pubkey, queryClient]) return query } // ============================================================ // ADVANCED: WAITING FOR SPECIFIC EVENT // ============================================================ const waitForEvent = ( ndk: NDK, filter: NDKFilter, condition: (event: NDKEvent) => boolean, timeoutMs: number = 30000 ): Promise => { return new Promise((resolve) => { const subscription = ndk.subscribe(filter, { closeOnEose: false }) // Timeout const timeout = setTimeout(() => { subscription.stop() resolve(null) }, timeoutMs) // Event handler subscription.on('event', (event: NDKEvent) => { if (condition(event)) { clearTimeout(timeout) subscription.stop() resolve(event) } }) }) } // Usage example async function waitForPayment(ndk: NDK, orderId: string, invoiceId: string) { const paymentEvent = await waitForEvent( ndk, { kinds: [17], '#order': [orderId], since: Math.floor(Date.now() / 1000) }, (event) => { const tag = event.tags.find(t => t[0] === 'payment-request') return tag?.[1] === invoiceId }, 60000 // 60 second timeout ) if (paymentEvent) { console.log('✅ Payment confirmed!') return paymentEvent } else { console.log('⏰ Payment timeout') return null } } // ============================================================ // USAGE EXAMPLES // ============================================================ async function queryExample(ndk: NDK) { // Fetch notes const notes = await fetchNotes(ndk, 'pubkey123', 50) console.log(`Found ${notes.length} notes`) // Subscribe to new notes const cleanup = subscribeToNotes(ndk, 'pubkey123', (event) => { console.log('New note:', event.content) }) // Clean up after 60 seconds setTimeout(cleanup, 60000) // Monitor payment monitorPaymentReceipt(ndk, 'order-123', 'invoice-456', (preimage) => { console.log('Payment received:', preimage) }) } export { fetchNotes, fetchProductsByMultipleAuthors, fetchOrderEvents, fetchRecentEvents, fetchEventById, subscribeToNotes, createManagedSubscription, monitorPaymentReceipt, useOrderSubscription, useProductsWithSubscription, waitForEvent }