Add NDK skill documentation and examples

- 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.
This commit is contained in:
2025-11-06 14:34:06 +00:00
parent 29ab350eed
commit 27f92336ae
27 changed files with 11800 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
/**
* NDK Initialization Patterns
*
* Examples from: src/lib/stores/ndk.ts
*/
import NDK from '@nostr-dev-kit/ndk'
// ============================================================
// BASIC INITIALIZATION
// ============================================================
const basicInit = () => {
const ndk = new NDK({
explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band']
})
return ndk
}
// ============================================================
// PRODUCTION PATTERN - WITH MULTIPLE NDK INSTANCES
// ============================================================
const productionInit = (relays: string[], zapRelays: string[]) => {
// Main NDK instance for general operations
const ndk = new NDK({
explicitRelayUrls: relays
})
// Separate NDK for zap operations (performance optimization)
const zapNdk = new NDK({
explicitRelayUrls: zapRelays
})
return { ndk, zapNdk }
}
// ============================================================
// CONNECTION WITH TIMEOUT
// ============================================================
const connectWithTimeout = async (
ndk: NDK,
timeoutMs: number = 10000
): Promise<void> => {
// Create connection promise
const connectPromise = ndk.connect()
// Create timeout promise
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Connection timeout')), timeoutMs)
)
try {
// Race between connection and timeout
await Promise.race([connectPromise, timeoutPromise])
console.log('✅ NDK connected successfully')
} catch (error) {
if (error instanceof Error && error.message === 'Connection timeout') {
console.error('❌ Connection timed out after', timeoutMs, 'ms')
} else {
console.error('❌ Connection failed:', error)
}
throw error
}
}
// ============================================================
// FULL INITIALIZATION FLOW
// ============================================================
interface InitConfig {
relays?: string[]
zapRelays?: string[]
timeoutMs?: number
}
const defaultRelays = [
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol'
]
const defaultZapRelays = [
'wss://relay.damus.io',
'wss://nostr.wine'
]
const initializeNDK = async (config: InitConfig = {}) => {
const {
relays = defaultRelays,
zapRelays = defaultZapRelays,
timeoutMs = 10000
} = config
// Initialize instances
const ndk = new NDK({ explicitRelayUrls: relays })
const zapNdk = new NDK({ explicitRelayUrls: zapRelays })
// Connect with timeout protection
try {
await connectWithTimeout(ndk, timeoutMs)
await connectWithTimeout(zapNdk, timeoutMs)
return { ndk, zapNdk, isConnected: true }
} catch (error) {
return { ndk, zapNdk, isConnected: false, error }
}
}
// ============================================================
// CHECKING CONNECTION STATUS
// ============================================================
const getConnectionStatus = (ndk: NDK) => {
const connectedRelays = Array.from(ndk.pool?.relays.values() || [])
.filter(relay => relay.status === 1)
.map(relay => relay.url)
const isConnected = connectedRelays.length > 0
return {
isConnected,
connectedRelays,
totalRelays: ndk.pool?.relays.size || 0
}
}
// ============================================================
// USAGE EXAMPLE
// ============================================================
async function main() {
// Initialize
const { ndk, zapNdk, isConnected } = await initializeNDK({
relays: defaultRelays,
zapRelays: defaultZapRelays,
timeoutMs: 10000
})
if (!isConnected) {
console.error('Failed to connect to relays')
return
}
// Check status
const status = getConnectionStatus(ndk)
console.log('Connection status:', status)
// Ready to use
console.log('NDK ready for operations')
}
export {
basicInit,
productionInit,
connectWithTimeout,
initializeNDK,
getConnectionStatus
}

View File

@@ -0,0 +1,255 @@
/**
* NDK Authentication Patterns
*
* Examples from: src/lib/stores/auth.ts
*/
import NDK from '@nostr-dev-kit/ndk'
import { NDKNip07Signer, NDKPrivateKeySigner, NDKNip46Signer } from '@nostr-dev-kit/ndk'
// ============================================================
// NIP-07 - BROWSER EXTENSION SIGNER
// ============================================================
const loginWithExtension = async (ndk: NDK) => {
try {
// Create NIP-07 signer (browser extension like Alby, nos2x)
const signer = new NDKNip07Signer()
// Wait for signer to be ready
await signer.blockUntilReady()
// Set signer on NDK instance
ndk.signer = signer
// Get authenticated user
const user = await signer.user()
console.log('✅ Logged in via extension:', user.npub)
return { user, signer }
} catch (error) {
console.error('❌ Extension login failed:', error)
throw new Error('Failed to login with browser extension. Is it installed?')
}
}
// ============================================================
// PRIVATE KEY SIGNER
// ============================================================
const loginWithPrivateKey = async (ndk: NDK, privateKeyHex: string) => {
try {
// Validate private key format (64 hex characters)
if (!/^[0-9a-f]{64}$/.test(privateKeyHex)) {
throw new Error('Invalid private key format')
}
// Create private key signer
const signer = new NDKPrivateKeySigner(privateKeyHex)
// Wait for signer to be ready
await signer.blockUntilReady()
// Set signer on NDK instance
ndk.signer = signer
// Get authenticated user
const user = await signer.user()
console.log('✅ Logged in with private key:', user.npub)
return { user, signer }
} catch (error) {
console.error('❌ Private key login failed:', error)
throw error
}
}
// ============================================================
// NIP-46 - REMOTE SIGNER (BUNKER)
// ============================================================
const loginWithNip46 = async (
ndk: NDK,
bunkerUrl: string,
localPrivateKey?: string
) => {
try {
// Create or use existing local signer
const localSigner = localPrivateKey
? new NDKPrivateKeySigner(localPrivateKey)
: NDKPrivateKeySigner.generate()
// Create NIP-46 remote signer
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
// Wait for signer to be ready (may require user approval)
await remoteSigner.blockUntilReady()
// Set signer on NDK instance
ndk.signer = remoteSigner
// Get authenticated user
const user = await remoteSigner.user()
console.log('✅ Logged in via NIP-46:', user.npub)
// Store local signer key for reconnection
return {
user,
signer: remoteSigner,
localSignerKey: localSigner.privateKey
}
} catch (error) {
console.error('❌ NIP-46 login failed:', error)
throw error
}
}
// ============================================================
// AUTO-LOGIN FROM LOCAL STORAGE
// ============================================================
const STORAGE_KEYS = {
AUTO_LOGIN: 'nostr:auto-login',
LOCAL_SIGNER: 'nostr:local-signer',
BUNKER_URL: 'nostr:bunker-url',
ENCRYPTED_KEY: 'nostr:encrypted-key'
}
const getAuthFromStorage = async (ndk: NDK) => {
try {
// Check if auto-login is enabled
const autoLogin = localStorage.getItem(STORAGE_KEYS.AUTO_LOGIN)
if (autoLogin !== 'true') {
return null
}
// Try NIP-46 bunker connection
const privateKey = localStorage.getItem(STORAGE_KEYS.LOCAL_SIGNER)
const bunkerUrl = localStorage.getItem(STORAGE_KEYS.BUNKER_URL)
if (privateKey && bunkerUrl) {
return await loginWithNip46(ndk, bunkerUrl, privateKey)
}
// Try encrypted private key
const encryptedKey = localStorage.getItem(STORAGE_KEYS.ENCRYPTED_KEY)
if (encryptedKey) {
// Would need decryption password from user
return { needsPassword: true, encryptedKey }
}
// Fallback to extension
return await loginWithExtension(ndk)
} catch (error) {
console.error('Auto-login failed:', error)
return null
}
}
// ============================================================
// SAVE AUTH TO STORAGE
// ============================================================
const saveAuthToStorage = (
method: 'extension' | 'private-key' | 'nip46',
data?: {
privateKey?: string
bunkerUrl?: string
encryptedKey?: string
}
) => {
// Enable auto-login
localStorage.setItem(STORAGE_KEYS.AUTO_LOGIN, 'true')
if (method === 'nip46' && data?.privateKey && data?.bunkerUrl) {
localStorage.setItem(STORAGE_KEYS.LOCAL_SIGNER, data.privateKey)
localStorage.setItem(STORAGE_KEYS.BUNKER_URL, data.bunkerUrl)
} else if (method === 'private-key' && data?.encryptedKey) {
localStorage.setItem(STORAGE_KEYS.ENCRYPTED_KEY, data.encryptedKey)
}
// Extension doesn't need storage
}
// ============================================================
// LOGOUT
// ============================================================
const logout = (ndk: NDK) => {
// Remove signer from NDK
ndk.signer = undefined
// Clear all auth storage
Object.values(STORAGE_KEYS).forEach(key => {
localStorage.removeItem(key)
})
console.log('✅ Logged out successfully')
}
// ============================================================
// GET CURRENT USER
// ============================================================
const getCurrentUser = async (ndk: NDK) => {
if (!ndk.signer) {
return null
}
try {
const user = await ndk.signer.user()
return {
pubkey: user.pubkey,
npub: user.npub,
profile: await user.fetchProfile()
}
} catch (error) {
console.error('Failed to get current user:', error)
return null
}
}
// ============================================================
// USAGE EXAMPLE
// ============================================================
async function authExample(ndk: NDK) {
// Try auto-login first
let auth = await getAuthFromStorage(ndk)
if (!auth) {
// Manual login options
console.log('Choose login method:')
console.log('1. Browser Extension (NIP-07)')
console.log('2. Private Key')
console.log('3. Remote Signer (NIP-46)')
// Example: login with extension
auth = await loginWithExtension(ndk)
saveAuthToStorage('extension')
}
if (auth && 'needsPassword' in auth) {
// Handle encrypted key case
console.log('Password required for encrypted key')
return
}
// Get current user info
const currentUser = await getCurrentUser(ndk)
console.log('Current user:', currentUser)
// Logout when done
// logout(ndk)
}
export {
loginWithExtension,
loginWithPrivateKey,
loginWithNip46,
getAuthFromStorage,
saveAuthToStorage,
logout,
getCurrentUser
}

View File

@@ -0,0 +1,376 @@
/**
* NDK Event Publishing Patterns
*
* Examples from: src/publish/orders.tsx, scripts/gen_products.ts
*/
import NDK, { NDKEvent, NDKTag } from '@nostr-dev-kit/ndk'
// ============================================================
// BASIC EVENT PUBLISHING
// ============================================================
const publishBasicNote = async (ndk: NDK, content: string) => {
// Create event
const event = new NDKEvent(ndk)
event.kind = 1 // Text note
event.content = content
event.tags = []
// Sign and publish
await event.sign()
await event.publish()
console.log('✅ Published note:', event.id)
return event.id
}
// ============================================================
// EVENT WITH TAGS
// ============================================================
const publishNoteWithTags = async (
ndk: NDK,
content: string,
options: {
mentions?: string[] // pubkeys to mention
hashtags?: string[]
replyTo?: string // event ID
}
) => {
const event = new NDKEvent(ndk)
event.kind = 1
event.content = content
event.tags = []
// Add mentions
if (options.mentions) {
options.mentions.forEach(pubkey => {
event.tags.push(['p', pubkey])
})
}
// Add hashtags
if (options.hashtags) {
options.hashtags.forEach(tag => {
event.tags.push(['t', tag])
})
}
// Add reply
if (options.replyTo) {
event.tags.push(['e', options.replyTo, '', 'reply'])
}
await event.sign()
await event.publish()
return event.id
}
// ============================================================
// PRODUCT LISTING (PARAMETERIZED REPLACEABLE EVENT)
// ============================================================
interface ProductData {
slug: string // Unique identifier
title: string
description: string
price: number
currency: string
images: string[]
shippingRefs?: string[]
category?: string
}
const publishProduct = async (ndk: NDK, product: ProductData) => {
const event = new NDKEvent(ndk)
event.kind = 30402 // Product listing kind
event.content = product.description
// Build tags
event.tags = [
['d', product.slug], // Unique identifier (required for replaceable)
['title', product.title],
['price', product.price.toString(), product.currency],
]
// Add images
product.images.forEach(image => {
event.tags.push(['image', image])
})
// Add shipping options
if (product.shippingRefs) {
product.shippingRefs.forEach(ref => {
event.tags.push(['shipping', ref])
})
}
// Add category
if (product.category) {
event.tags.push(['t', product.category])
}
// Optional: set custom timestamp
event.created_at = Math.floor(Date.now() / 1000)
await event.sign()
await event.publish()
console.log('✅ Published product:', product.title)
return event.id
}
// ============================================================
// ORDER CREATION EVENT
// ============================================================
interface OrderData {
orderId: string
sellerPubkey: string
productRef: string
quantity: number
totalAmount: string
currency: string
shippingRef?: string
shippingAddress?: string
email?: string
phone?: string
notes?: string
}
const createOrder = async (ndk: NDK, order: OrderData) => {
const event = new NDKEvent(ndk)
event.kind = 16 // Order processing kind
event.content = order.notes || ''
// Required tags per spec
event.tags = [
['p', order.sellerPubkey],
['subject', `Order ${order.orderId.substring(0, 8)}`],
['type', 'order-creation'],
['order', order.orderId],
['amount', order.totalAmount],
['item', order.productRef, order.quantity.toString()],
]
// Optional tags
if (order.shippingRef) {
event.tags.push(['shipping', order.shippingRef])
}
if (order.shippingAddress) {
event.tags.push(['address', order.shippingAddress])
}
if (order.email) {
event.tags.push(['email', order.email])
}
if (order.phone) {
event.tags.push(['phone', order.phone])
}
try {
await event.sign()
await event.publish()
console.log('✅ Order created:', order.orderId)
return { success: true, eventId: event.id }
} catch (error) {
console.error('❌ Failed to create order:', error)
return { success: false, error }
}
}
// ============================================================
// STATUS UPDATE EVENT
// ============================================================
const publishStatusUpdate = async (
ndk: NDK,
orderId: string,
recipientPubkey: string,
status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled',
notes?: string
) => {
const event = new NDKEvent(ndk)
event.kind = 16
event.content = notes || `Order status updated to ${status}`
event.tags = [
['p', recipientPubkey],
['subject', 'order-info'],
['type', 'status-update'],
['order', orderId],
['status', status],
]
await event.sign()
await event.publish()
return event.id
}
// ============================================================
// BATCH PUBLISHING
// ============================================================
const publishMultipleEvents = async (
ndk: NDK,
events: Array<{ kind: number; content: string; tags: NDKTag[] }>
) => {
const results = []
for (const eventData of events) {
try {
const event = new NDKEvent(ndk)
event.kind = eventData.kind
event.content = eventData.content
event.tags = eventData.tags
await event.sign()
await event.publish()
results.push({ success: true, eventId: event.id })
} catch (error) {
results.push({ success: false, error })
}
}
return results
}
// ============================================================
// PUBLISH WITH CUSTOM SIGNER
// ============================================================
import { NDKSigner } from '@nostr-dev-kit/ndk'
const publishWithCustomSigner = async (
ndk: NDK,
signer: NDKSigner,
eventData: { kind: number; content: string; tags: NDKTag[] }
) => {
const event = new NDKEvent(ndk)
event.kind = eventData.kind
event.content = eventData.content
event.tags = eventData.tags
// Sign with specific signer (not ndk.signer)
await event.sign(signer)
await event.publish()
return event.id
}
// ============================================================
// ERROR HANDLING PATTERN
// ============================================================
const publishWithErrorHandling = async (
ndk: NDK,
eventData: { kind: number; content: string; tags: NDKTag[] }
) => {
// Validate NDK
if (!ndk) {
throw new Error('NDK not initialized')
}
// Validate signer
if (!ndk.signer) {
throw new Error('No active signer. Please login first.')
}
try {
const event = new NDKEvent(ndk)
event.kind = eventData.kind
event.content = eventData.content
event.tags = eventData.tags
// Sign
await event.sign()
// Verify signature
if (!event.sig) {
throw new Error('Event signing failed')
}
// Publish
await event.publish()
// Verify event ID
if (!event.id) {
throw new Error('Event ID not generated')
}
return {
success: true,
eventId: event.id,
pubkey: event.pubkey
}
} catch (error) {
console.error('Publishing failed:', error)
if (error instanceof Error) {
// Handle specific error types
if (error.message.includes('relay')) {
throw new Error('Failed to publish to relays. Check connection.')
}
if (error.message.includes('sign')) {
throw new Error('Failed to sign event. Check signer.')
}
}
throw error
}
}
// ============================================================
// USAGE EXAMPLE
// ============================================================
async function publishingExample(ndk: NDK) {
// Simple note
await publishBasicNote(ndk, 'Hello Nostr!')
// Note with tags
await publishNoteWithTags(ndk, 'Check out this product!', {
hashtags: ['marketplace', 'nostr'],
mentions: ['pubkey123...']
})
// Product listing
await publishProduct(ndk, {
slug: 'bitcoin-tshirt',
title: 'Bitcoin T-Shirt',
description: 'High quality Bitcoin t-shirt',
price: 25,
currency: 'USD',
images: ['https://example.com/image.jpg'],
category: 'clothing'
})
// Order
await createOrder(ndk, {
orderId: 'order-123',
sellerPubkey: 'seller-pubkey',
productRef: '30402:pubkey:bitcoin-tshirt',
quantity: 1,
totalAmount: '25.00',
currency: 'USD',
email: 'customer@example.com'
})
}
export {
publishBasicNote,
publishNoteWithTags,
publishProduct,
createOrder,
publishStatusUpdate,
publishMultipleEvents,
publishWithCustomSigner,
publishWithErrorHandling
}

View File

@@ -0,0 +1,404 @@
/**
* 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
}

View File

@@ -0,0 +1,423 @@
/**
* NDK User and Profile Handling
*
* Examples from: src/queries/profiles.tsx, src/components/Profile.tsx
*/
import NDK, { NDKUser, NDKUserProfile } from '@nostr-dev-kit/ndk'
import { nip19 } from 'nostr-tools'
// ============================================================
// FETCH PROFILE BY NPUB
// ============================================================
const fetchProfileByNpub = async (ndk: NDK, npub: string): Promise<NDKUserProfile | null> => {
try {
// Get user object from npub
const user = ndk.getUser({ npub })
// Fetch profile from relays
const profile = await user.fetchProfile()
return profile
} catch (error) {
console.error('Failed to fetch profile:', error)
return null
}
}
// ============================================================
// FETCH PROFILE BY HEX PUBKEY
// ============================================================
const fetchProfileByPubkey = async (ndk: NDK, pubkey: string): Promise<NDKUserProfile | null> => {
try {
const user = ndk.getUser({ hexpubkey: pubkey })
const profile = await user.fetchProfile()
return profile
} catch (error) {
console.error('Failed to fetch profile:', error)
return null
}
}
// ============================================================
// FETCH PROFILE BY NIP-05
// ============================================================
const fetchProfileByNip05 = async (ndk: NDK, nip05: string): Promise<NDKUserProfile | null> => {
try {
// Resolve NIP-05 identifier to user
const user = await ndk.getUserFromNip05(nip05)
if (!user) {
console.log('User not found for NIP-05:', nip05)
return null
}
// Fetch profile
const profile = await user.fetchProfile()
return profile
} catch (error) {
console.error('Failed to fetch profile by NIP-05:', error)
return null
}
}
// ============================================================
// FETCH PROFILE BY ANY IDENTIFIER
// ============================================================
const fetchProfileByIdentifier = async (
ndk: NDK,
identifier: string
): Promise<{ profile: NDKUserProfile | null; user: NDKUser | null }> => {
try {
// Check if it's a NIP-05 (contains @)
if (identifier.includes('@')) {
const user = await ndk.getUserFromNip05(identifier)
if (!user) return { profile: null, user: null }
const profile = await user.fetchProfile()
return { profile, user }
}
// Check if it's an npub
if (identifier.startsWith('npub')) {
const user = ndk.getUser({ npub: identifier })
const profile = await user.fetchProfile()
return { profile, user }
}
// Assume it's a hex pubkey
const user = ndk.getUser({ hexpubkey: identifier })
const profile = await user.fetchProfile()
return { profile, user }
} catch (error) {
console.error('Failed to fetch profile:', error)
return { profile: null, user: null }
}
}
// ============================================================
// GET CURRENT USER
// ============================================================
const getCurrentUser = async (ndk: NDK): Promise<NDKUser | null> => {
if (!ndk.signer) {
console.log('No signer set')
return null
}
try {
const user = await ndk.signer.user()
return user
} catch (error) {
console.error('Failed to get current user:', error)
return null
}
}
// ============================================================
// PROFILE DATA STRUCTURE
// ============================================================
interface ProfileData {
// Standard fields
name?: string
displayName?: string
display_name?: string
picture?: string
image?: string
banner?: string
about?: string
// Contact
nip05?: string
lud06?: string // LNURL
lud16?: string // Lightning address
// Social
website?: string
// Raw data
[key: string]: any
}
// ============================================================
// EXTRACT PROFILE INFO
// ============================================================
const extractProfileInfo = (profile: NDKUserProfile | null) => {
if (!profile) {
return {
displayName: 'Anonymous',
avatar: null,
bio: null,
lightningAddress: null,
nip05: null
}
}
return {
displayName: profile.displayName || profile.display_name || profile.name || 'Anonymous',
avatar: profile.picture || profile.image || null,
banner: profile.banner || null,
bio: profile.about || null,
lightningAddress: profile.lud16 || profile.lud06 || null,
nip05: profile.nip05 || null,
website: profile.website || null
}
}
// ============================================================
// UPDATE PROFILE
// ============================================================
import { NDKEvent } from '@nostr-dev-kit/ndk'
const updateProfile = async (ndk: NDK, profileData: Partial<ProfileData>) => {
if (!ndk.signer) {
throw new Error('No signer available')
}
// Get current profile
const currentUser = await ndk.signer.user()
const currentProfile = await currentUser.fetchProfile()
// Merge with new data
const updatedProfile = {
...currentProfile,
...profileData
}
// Create kind 0 (metadata) event
const event = new NDKEvent(ndk)
event.kind = 0
event.content = JSON.stringify(updatedProfile)
event.tags = []
await event.sign()
await event.publish()
console.log('✅ Profile updated')
return event.id
}
// ============================================================
// BATCH FETCH PROFILES
// ============================================================
const fetchMultipleProfiles = async (
ndk: NDK,
pubkeys: string[]
): Promise<Map<string, NDKUserProfile | null>> => {
const profiles = new Map<string, NDKUserProfile | null>()
// Fetch all profiles in parallel
await Promise.all(
pubkeys.map(async (pubkey) => {
try {
const user = ndk.getUser({ hexpubkey: pubkey })
const profile = await user.fetchProfile()
profiles.set(pubkey, profile)
} catch (error) {
console.error(`Failed to fetch profile for ${pubkey}:`, error)
profiles.set(pubkey, null)
}
})
)
return profiles
}
// ============================================================
// CONVERT BETWEEN FORMATS
// ============================================================
const convertPubkeyFormats = (identifier: string) => {
try {
// If it's npub, convert to hex
if (identifier.startsWith('npub')) {
const decoded = nip19.decode(identifier)
if (decoded.type === 'npub') {
return {
hex: decoded.data as string,
npub: identifier
}
}
}
// If it's hex, convert to npub
if (/^[0-9a-f]{64}$/.test(identifier)) {
return {
hex: identifier,
npub: nip19.npubEncode(identifier)
}
}
throw new Error('Invalid pubkey format')
} catch (error) {
console.error('Format conversion failed:', error)
return null
}
}
// ============================================================
// REACT HOOK FOR PROFILE
// ============================================================
import { useQuery } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
function useProfile(ndk: NDK | null, npub: string | undefined) {
return useQuery({
queryKey: ['profile', npub],
queryFn: async () => {
if (!ndk || !npub) throw new Error('NDK or npub missing')
return await fetchProfileByNpub(ndk, npub)
},
enabled: !!ndk && !!npub,
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 30 * 60 * 1000 // 30 minutes
})
}
// ============================================================
// REACT COMPONENT EXAMPLE
// ============================================================
interface ProfileDisplayProps {
ndk: NDK
pubkey: string
}
function ProfileDisplay({ ndk, pubkey }: ProfileDisplayProps) {
const [profile, setProfile] = useState<NDKUserProfile | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const loadProfile = async () => {
setLoading(true)
try {
const user = ndk.getUser({ hexpubkey: pubkey })
const fetchedProfile = await user.fetchProfile()
setProfile(fetchedProfile)
} catch (error) {
console.error('Failed to load profile:', error)
} finally {
setLoading(false)
}
}
loadProfile()
}, [ndk, pubkey])
if (loading) {
return <div>Loading profile...</div>
}
const info = extractProfileInfo(profile)
return (
<div className="profile">
{info.avatar && <img src={info.avatar} alt={info.displayName} />}
<h2>{info.displayName}</h2>
{info.bio && <p>{info.bio}</p>}
{info.nip05 && <span> {info.nip05}</span>}
{info.lightningAddress && <span> {info.lightningAddress}</span>}
</div>
)
}
// ============================================================
// FOLLOW/UNFOLLOW USER
// ============================================================
const followUser = async (ndk: NDK, pubkeyToFollow: string) => {
if (!ndk.signer) {
throw new Error('No signer available')
}
// Fetch current contact list (kind 3)
const currentUser = await ndk.signer.user()
const contactListFilter = {
kinds: [3],
authors: [currentUser.pubkey]
}
const existingEvents = await ndk.fetchEvents(contactListFilter)
const existingContactList = existingEvents.size > 0
? Array.from(existingEvents)[0]
: null
// Get existing p tags
const existingPTags = existingContactList
? existingContactList.tags.filter(tag => tag[0] === 'p')
: []
// Check if already following
const alreadyFollowing = existingPTags.some(tag => tag[1] === pubkeyToFollow)
if (alreadyFollowing) {
console.log('Already following this user')
return
}
// Create new contact list with added user
const event = new NDKEvent(ndk)
event.kind = 3
event.content = existingContactList?.content || ''
event.tags = [
...existingPTags,
['p', pubkeyToFollow]
]
await event.sign()
await event.publish()
console.log('✅ Now following user')
}
// ============================================================
// USAGE EXAMPLE
// ============================================================
async function profileExample(ndk: NDK) {
// Fetch by different identifiers
const profile1 = await fetchProfileByNpub(ndk, 'npub1...')
const profile2 = await fetchProfileByNip05(ndk, 'user@domain.com')
const profile3 = await fetchProfileByPubkey(ndk, 'hex pubkey...')
// Extract display info
const info = extractProfileInfo(profile1)
console.log('Display name:', info.displayName)
console.log('Avatar:', info.avatar)
// Update own profile
await updateProfile(ndk, {
name: 'My Name',
about: 'My bio',
picture: 'https://example.com/avatar.jpg',
lud16: 'me@getalby.com'
})
// Follow someone
await followUser(ndk, 'pubkey to follow')
}
export {
fetchProfileByNpub,
fetchProfileByPubkey,
fetchProfileByNip05,
fetchProfileByIdentifier,
getCurrentUser,
extractProfileInfo,
updateProfile,
fetchMultipleProfiles,
convertPubkeyFormats,
useProfile,
followUser
}

View File

@@ -0,0 +1,94 @@
# NDK Examples Index
Complete code examples extracted from the Plebeian Market production codebase.
## Available Examples
### 01-initialization.ts
- Basic NDK initialization
- Multiple NDK instances (main + zap relays)
- Connection with timeout protection
- Connection status checking
- Full initialization flow with error handling
### 02-authentication.ts
- NIP-07 browser extension login
- Private key signer
- NIP-46 remote signer (Bunker)
- Auto-login from localStorage
- Saving auth credentials
- Logout functionality
- Getting current user
### 03-publishing-events.ts
- Basic note publishing
- Events with tags (mentions, hashtags, replies)
- Product listings (parameterized replaceable events)
- Order creation events
- Status update events
- Batch publishing
- Custom signer usage
- Comprehensive error handling
### 04-querying-subscribing.ts
- Basic fetch queries
- Multiple author queries
- Tag filtering
- Time range filtering
- Event ID lookup
- Real-time subscriptions
- Subscription cleanup patterns
- React integration hooks
- React Query integration
- Waiting for specific events
- Payment monitoring
### 05-users-profiles.ts
- Fetch profile by npub
- Fetch profile by hex pubkey
- Fetch profile by NIP-05
- Universal identifier lookup
- Get current user
- Extract profile information
- Update user profile
- Batch fetch multiple profiles
- Convert between pubkey formats (hex/npub)
- React hooks for profiles
- Follow/unfollow users
## Usage
Each file contains:
- Fully typed TypeScript code
- JSDoc comments explaining the pattern
- Error handling examples
- Integration patterns with React/TanStack Query
- Real-world usage examples
All examples are based on actual production code from the Plebeian Market application.
## Running Examples
```typescript
import { initializeNDK } from './01-initialization'
import { loginWithExtension } from './02-authentication'
import { publishBasicNote } from './03-publishing-events'
// Initialize NDK
const { ndk, isConnected } = await initializeNDK()
if (isConnected) {
// Authenticate
const { user } = await loginWithExtension(ndk)
// Publish
await publishBasicNote(ndk, 'Hello Nostr!')
}
```
## Additional Resources
- See `../ndk-skill.md` for detailed documentation
- See `../quick-reference.md` for quick lookup
- Check the main codebase for more complex patterns