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:
162
.claude/skills/ndk/examples/01-initialization.ts
Normal file
162
.claude/skills/ndk/examples/01-initialization.ts
Normal 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
|
||||
}
|
||||
|
||||
255
.claude/skills/ndk/examples/02-authentication.ts
Normal file
255
.claude/skills/ndk/examples/02-authentication.ts
Normal 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
|
||||
}
|
||||
|
||||
376
.claude/skills/ndk/examples/03-publishing-events.ts
Normal file
376
.claude/skills/ndk/examples/03-publishing-events.ts
Normal 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
|
||||
}
|
||||
|
||||
404
.claude/skills/ndk/examples/04-querying-subscribing.ts
Normal file
404
.claude/skills/ndk/examples/04-querying-subscribing.ts
Normal 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
|
||||
}
|
||||
|
||||
423
.claude/skills/ndk/examples/05-users-profiles.ts
Normal file
423
.claude/skills/ndk/examples/05-users-profiles.ts
Normal 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
|
||||
}
|
||||
|
||||
94
.claude/skills/ndk/examples/README.md
Normal file
94
.claude/skills/ndk/examples/README.md
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user