- 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.
11 KiB
11 KiB
NDK Common Patterns & Troubleshooting
Quick reference for common patterns and solutions to frequent NDK issues.
Common Patterns
Store-Based NDK Management
// Store pattern (recommended for React apps)
import { Store } from '@tanstack/store'
interface NDKState {
ndk: NDK | null
isConnected: boolean
signer?: NDKSigner
}
const ndkStore = new Store<NDKState>({
ndk: null,
isConnected: false
})
export const ndkActions = {
initialize: () => {
const ndk = new NDK({ explicitRelayUrls: relays })
ndkStore.setState({ ndk })
return ndk
},
getNDK: () => ndkStore.state.ndk,
setSigner: (signer: NDKSigner) => {
const ndk = ndkStore.state.ndk
if (ndk) {
ndk.signer = signer
ndkStore.setState({ signer })
}
}
}
Query + Subscription Pattern
// Initial data load + real-time updates
function useOrdersWithRealtime(orderId: string) {
const queryClient = useQueryClient()
const ndk = ndkActions.getNDK()
// Fetch initial data
const query = useQuery({
queryKey: ['orders', orderId],
queryFn: () => fetchOrders(orderId),
})
// Subscribe to updates
useEffect(() => {
if (!ndk || !orderId) return
const sub = ndk.subscribe(
{ kinds: [16], '#order': [orderId] },
{ closeOnEose: false }
)
sub.on('event', () => {
queryClient.invalidateQueries(['orders', orderId])
})
return () => sub.stop()
}, [ndk, orderId])
return query
}
Event Parsing Pattern
// Parse event tags into structured data
function parseProductEvent(event: NDKEvent) {
const getTag = (name: string) =>
event.tags.find(t => t[0] === name)?.[1]
const getAllTags = (name: string) =>
event.tags.filter(t => t[0] === name).map(t => t[1])
return {
id: event.id,
slug: getTag('d'),
title: getTag('title'),
price: parseFloat(getTag('price') || '0'),
currency: event.tags.find(t => t[0] === 'price')?.[2] || 'USD',
images: getAllTags('image'),
shipping: getAllTags('shipping'),
description: event.content,
createdAt: event.created_at,
author: event.pubkey
}
}
Relay Pool Pattern
// Separate NDK instances for different purposes
const mainNdk = new NDK({
explicitRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol']
})
const zapNdk = new NDK({
explicitRelayUrls: ['wss://relay.damus.io'] // Zap-optimized relays
})
const blossomNdk = new NDK({
explicitRelayUrls: ['wss://blossom.server.com'] // Media server
})
await Promise.all([
mainNdk.connect(),
zapNdk.connect(),
blossomNdk.connect()
])
Troubleshooting
Problem: Events Not Received
Symptoms: Subscription doesn't receive events, fetchEvents returns empty Set
Solutions:
- Check relay connection:
const status = ndk.pool?.connectedRelays()
console.log('Connected relays:', status?.length)
if (status?.length === 0) {
await ndk.connect()
}
- Verify filter syntax (especially tags):
// ❌ Wrong
{ kinds: [16], 'order': [orderId] }
// ✅ Correct (note the # prefix for tags)
{ kinds: [16], '#order': [orderId] }
- Check timestamps:
// Events might be too old/new
const now = Math.floor(Date.now() / 1000)
const filter = {
kinds: [1],
since: now - 86400, // Last 24 hours
until: now
}
- Ensure closeOnEose is correct:
// For real-time updates
ndk.subscribe(filter, { closeOnEose: false })
// For one-time historical fetch
ndk.subscribe(filter, { closeOnEose: true })
Problem: "NDK not initialized"
Symptoms: ndk is null/undefined
Solutions:
- Initialize before use:
// In app entry point
const ndk = new NDK({ explicitRelayUrls: relays })
await ndk.connect()
- Add null checks:
const ndk = ndkActions.getNDK()
if (!ndk) throw new Error('NDK not initialized')
- Use initialization guard:
const ensureNDK = () => {
let ndk = ndkActions.getNDK()
if (!ndk) {
ndk = ndkActions.initialize()
}
return ndk
}
Problem: "No active signer" / Cannot Sign Events
Symptoms: Event signing fails, publishing throws error
Solutions:
- Check signer is set:
if (!ndk.signer) {
throw new Error('Please login first')
}
- Ensure blockUntilReady called:
const signer = new NDKNip07Signer()
await signer.blockUntilReady() // ← Critical!
ndk.signer = signer
- Handle NIP-07 unavailable:
try {
const signer = new NDKNip07Signer()
await signer.blockUntilReady()
ndk.signer = signer
} catch (error) {
console.error('Browser extension not available')
// Fallback to other auth method
}
Problem: Duplicate Events in Subscriptions
Symptoms: Same event received multiple times
Solutions:
- Track processed event IDs:
const processedIds = new Set<string>()
sub.on('event', (event) => {
if (processedIds.has(event.id)) return
processedIds.add(event.id)
handleEvent(event)
})
- Use Map for event storage:
const [events, setEvents] = useState<Map<string, NDKEvent>>(new Map())
sub.on('event', (event) => {
setEvents(prev => new Map(prev).set(event.id, event))
})
Problem: Connection Timeout
Symptoms: connect() hangs, never resolves
Solutions:
- Use timeout wrapper:
const connectWithTimeout = async (ndk: NDK, ms = 10000) => {
await Promise.race([
ndk.connect(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
])
}
- Try fewer relays:
// Start with reliable relays only
const reliableRelays = ['wss://relay.damus.io']
const ndk = new NDK({ explicitRelayUrls: reliableRelays })
- Add connection retry:
const connectWithRetry = async (ndk: NDK, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
await connectWithTimeout(ndk, 10000)
return
} catch (error) {
console.log(`Retry ${i + 1}/${maxRetries}`)
if (i === maxRetries - 1) throw error
}
}
}
Problem: Subscription Memory Leak
Symptoms: App gets slower, memory usage increases
Solutions:
- Always stop subscriptions:
useEffect(() => {
const sub = ndk.subscribe(filter, { closeOnEose: false })
// ← CRITICAL: cleanup
return () => {
sub.stop()
}
}, [dependencies])
- Track active subscriptions:
const activeSubscriptions = new Set<NDKSubscription>()
const createSub = (filter: NDKFilter) => {
const sub = ndk.subscribe(filter, { closeOnEose: false })
activeSubscriptions.add(sub)
return sub
}
const stopAllSubs = () => {
activeSubscriptions.forEach(sub => sub.stop())
activeSubscriptions.clear()
}
Problem: Profile Not Found
Symptoms: fetchProfile() returns null/undefined
Solutions:
- Check different relays:
// Add more relay URLs
const ndk = new NDK({
explicitRelayUrls: [
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol'
]
})
- Verify pubkey format:
// Ensure correct format
if (pubkey.startsWith('npub')) {
const user = ndk.getUser({ npub: pubkey })
} else if (/^[0-9a-f]{64}$/.test(pubkey)) {
const user = ndk.getUser({ hexpubkey: pubkey })
}
- Handle missing profiles gracefully:
const profile = await user.fetchProfile()
const displayName = profile?.name || profile?.displayName || 'Anonymous'
const avatar = profile?.picture || '/default-avatar.png'
Problem: Events Published But Not Visible
Symptoms: publish() succeeds but event not found in queries
Solutions:
- Verify event was signed:
await event.sign()
console.log('Event ID:', event.id) // Should be set
console.log('Signature:', event.sig) // Should exist
- Check relay acceptance:
const relays = await event.publish()
console.log('Published to relays:', relays)
- Query immediately after publish:
await event.publish()
// Wait a moment for relay propagation
await new Promise(resolve => setTimeout(resolve, 1000))
const found = await ndk.fetchEvents({ ids: [event.id] })
console.log('Event found:', found.size > 0)
Problem: NIP-46 Connection Fails
Symptoms: Remote signer connection times out or fails
Solutions:
- Verify bunker URL format:
// Correct format: bunker://<remote-pubkey>?relay=wss://...
const isValidBunkerUrl = (url: string) => {
return url.startsWith('bunker://') && url.includes('?relay=')
}
- Ensure local signer is ready:
const localSigner = new NDKPrivateKeySigner(privateKey)
await localSigner.blockUntilReady()
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
await remoteSigner.blockUntilReady()
- Store credentials for reconnection:
// Save for future sessions
localStorage.setItem('local-signer-key', localSigner.privateKey)
localStorage.setItem('bunker-url', bunkerUrl)
Performance Tips
Optimize Queries
// ❌ Slow: Multiple sequential queries
const products = await ndk.fetchEvents({ kinds: [30402], authors: [pk1] })
const orders = await ndk.fetchEvents({ kinds: [16], authors: [pk1] })
const profiles = await ndk.fetchEvents({ kinds: [0], authors: [pk1] })
// ✅ Fast: Parallel queries
const [products, orders, profiles] = await Promise.all([
ndk.fetchEvents({ kinds: [30402], authors: [pk1] }),
ndk.fetchEvents({ kinds: [16], authors: [pk1] }),
ndk.fetchEvents({ kinds: [0], authors: [pk1] })
])
Cache Profile Lookups
const profileCache = new Map<string, NDKUserProfile>()
const getCachedProfile = async (ndk: NDK, pubkey: string) => {
if (profileCache.has(pubkey)) {
return profileCache.get(pubkey)!
}
const user = ndk.getUser({ hexpubkey: pubkey })
const profile = await user.fetchProfile()
if (profile) {
profileCache.set(pubkey, profile)
}
return profile
}
Limit Result Sets
// Always use limit to prevent over-fetching
const filter: NDKFilter = {
kinds: [1],
authors: [pubkey],
limit: 50 // ← Important!
}
Debounce Subscription Updates
import { debounce } from 'lodash'
const debouncedUpdate = debounce((event: NDKEvent) => {
handleEvent(event)
}, 300)
sub.on('event', debouncedUpdate)
Testing Tips
Mock NDK in Tests
const mockNDK = {
fetchEvents: vi.fn().mockResolvedValue(new Set()),
subscribe: vi.fn().mockReturnValue({
on: vi.fn(),
stop: vi.fn()
}),
signer: {
user: vi.fn().mockResolvedValue({ pubkey: 'test-pubkey' })
}
} as unknown as NDK
Test Event Creation
const createTestEvent = (overrides?: Partial<NDKEvent>): NDKEvent => {
return {
id: 'test-id',
kind: 1,
content: 'test content',
tags: [],
created_at: Math.floor(Date.now() / 1000),
pubkey: 'test-pubkey',
sig: 'test-sig',
...overrides
} as NDKEvent
}
For more detailed information, see:
ndk-skill.md- Complete referencequick-reference.md- Quick lookupexamples/- Code examples