You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
404 lines
9.9 KiB
404 lines
9.9 KiB
/** |
|
* 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<NDKEvent[]>([]) |
|
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<NDKEvent | null> => { |
|
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 |
|
} |
|
|
|
|