- Introduced comprehensive documentation for the Nostr Development Kit (NDK) including an overview, quick reference, and troubleshooting guide. - Added detailed examples covering initialization, authentication, event publishing, querying, and user profile management. - Structured the documentation to facilitate quick lookups and deep learning, based on real-world usage patterns from the Plebeian Market application. - Created an index for examples to enhance usability and navigation. - Bumped version to 1.0.0 to reflect the addition of this new skill set.
405 lines
9.9 KiB
TypeScript
405 lines
9.9 KiB
TypeScript
/**
|
|
* 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
|
|
}
|
|
|