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:
286
.claude/skills/ndk/INDEX.md
Normal file
286
.claude/skills/ndk/INDEX.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# NDK (Nostr Development Kit) Claude Skill
|
||||
|
||||
> **Comprehensive knowledge base for working with NDK in production applications**
|
||||
|
||||
This Claude skill provides deep expertise in the Nostr Development Kit based on real-world usage patterns from the Plebeian Market application.
|
||||
|
||||
## 📚 Documentation Structure
|
||||
|
||||
```
|
||||
.claude/skills/ndk/
|
||||
├── README.md # This file - Overview and getting started
|
||||
├── ndk-skill.md # Complete reference guide (18KB)
|
||||
├── quick-reference.md # Fast lookup for common tasks (7KB)
|
||||
├── troubleshooting.md # Common problems and solutions
|
||||
└── examples/ # Production code examples
|
||||
├── README.md
|
||||
├── 01-initialization.ts # NDK setup and connection
|
||||
├── 02-authentication.ts # NIP-07, NIP-46, private keys
|
||||
├── 03-publishing-events.ts # Creating and publishing events
|
||||
├── 04-querying-subscribing.ts # Fetching and real-time subs
|
||||
└── 05-users-profiles.ts # User and profile management
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### For Quick Lookups
|
||||
Start with **`quick-reference.md`** for:
|
||||
- Common code snippets
|
||||
- Quick syntax reminders
|
||||
- Frequently used patterns
|
||||
|
||||
### For Deep Learning
|
||||
Read **`ndk-skill.md`** for:
|
||||
- Complete API documentation
|
||||
- Best practices
|
||||
- Integration patterns
|
||||
- Performance optimization
|
||||
|
||||
### For Problem Solving
|
||||
Check **`troubleshooting.md`** for:
|
||||
- Common error solutions
|
||||
- Performance tips
|
||||
- Testing strategies
|
||||
- Debug techniques
|
||||
|
||||
### For Code Examples
|
||||
Browse **`examples/`** directory for:
|
||||
- Real production code
|
||||
- Full implementations
|
||||
- React integration patterns
|
||||
- Error handling examples
|
||||
|
||||
## 📖 Core Topics Covered
|
||||
|
||||
### 1. Initialization & Setup
|
||||
- Basic NDK initialization
|
||||
- Multiple instance patterns (main + zap relays)
|
||||
- Connection management with timeouts
|
||||
- Relay pool configuration
|
||||
- Connection status monitoring
|
||||
|
||||
### 2. Authentication
|
||||
- **NIP-07**: Browser extension signers (Alby, nos2x)
|
||||
- **NIP-46**: Remote signers (Bunker)
|
||||
- **Private Keys**: Direct key management
|
||||
- Auto-login with localStorage
|
||||
- Multi-account session management
|
||||
|
||||
### 3. Event Publishing
|
||||
- Basic text notes
|
||||
- Parameterized replaceable events (products, profiles)
|
||||
- Order and payment events
|
||||
- Batch publishing
|
||||
- Error handling patterns
|
||||
|
||||
### 4. Querying & Subscriptions
|
||||
- One-time fetches with `fetchEvents()`
|
||||
- Real-time subscriptions
|
||||
- Tag filtering patterns
|
||||
- Time-range queries
|
||||
- Event monitoring
|
||||
- React Query integration
|
||||
|
||||
### 5. User & Profile Management
|
||||
- Fetch profiles (npub, hex, NIP-05)
|
||||
- Update user profiles
|
||||
- Follow/unfollow operations
|
||||
- Batch profile loading
|
||||
- Profile caching strategies
|
||||
|
||||
### 6. Advanced Patterns
|
||||
- Store-based NDK management
|
||||
- Query + subscription combination
|
||||
- Event parsing utilities
|
||||
- Memory leak prevention
|
||||
- Performance optimization
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
### Building a Nostr Client
|
||||
```typescript
|
||||
// Initialize
|
||||
const { ndk, isConnected } = await initializeNDK({
|
||||
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
||||
timeoutMs: 10000
|
||||
})
|
||||
|
||||
// Authenticate
|
||||
const { user } = await loginWithExtension(ndk)
|
||||
|
||||
// Publish
|
||||
await publishBasicNote(ndk, 'Hello Nostr!')
|
||||
|
||||
// Subscribe
|
||||
const sub = subscribeToNotes(ndk, user.pubkey, (event) => {
|
||||
console.log('New note:', event.content)
|
||||
})
|
||||
```
|
||||
|
||||
### Building a Marketplace
|
||||
```typescript
|
||||
// Publish product
|
||||
await publishProduct(ndk, {
|
||||
slug: 'bitcoin-shirt',
|
||||
title: 'Bitcoin T-Shirt',
|
||||
price: 25,
|
||||
currency: 'USD',
|
||||
images: ['https://...']
|
||||
})
|
||||
|
||||
// Create order
|
||||
await createOrder(ndk, {
|
||||
orderId: uuidv4(),
|
||||
sellerPubkey: merchant.pubkey,
|
||||
productRef: '30402:pubkey:bitcoin-shirt',
|
||||
quantity: 1,
|
||||
totalAmount: '25.00'
|
||||
})
|
||||
|
||||
// Monitor payment
|
||||
monitorPaymentReceipt(ndk, orderId, invoiceId, (preimage) => {
|
||||
console.log('Payment confirmed!')
|
||||
})
|
||||
```
|
||||
|
||||
### React Integration
|
||||
```typescript
|
||||
function Feed() {
|
||||
const ndk = useNDK()
|
||||
const { user } = useAuth()
|
||||
|
||||
// Query with real-time updates
|
||||
const { data: notes } = useNotesWithSubscription(
|
||||
ndk,
|
||||
user.pubkey
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{notes?.map(note => (
|
||||
<NoteCard key={note.id} note={note} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 Common Patterns Quick Reference
|
||||
|
||||
### Safe NDK Access
|
||||
```typescript
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
```
|
||||
|
||||
### Subscription Cleanup
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const sub = ndk.subscribe(filter, { closeOnEose: false })
|
||||
sub.on('event', handleEvent)
|
||||
return () => sub.stop() // Critical!
|
||||
}, [ndk])
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
```typescript
|
||||
try {
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
} catch (error) {
|
||||
console.error('Publishing failed:', error)
|
||||
throw new Error('Failed to publish. Check connection.')
|
||||
}
|
||||
```
|
||||
|
||||
### Tag Filtering
|
||||
```typescript
|
||||
// ✅ Correct (note the # prefix for tag filters)
|
||||
{ kinds: [16], '#order': [orderId] }
|
||||
|
||||
// ❌ Wrong
|
||||
{ kinds: [16], 'order': [orderId] }
|
||||
```
|
||||
|
||||
## 🛠 Development Tools
|
||||
|
||||
### VS Code Integration
|
||||
These skill files work with:
|
||||
- Cursor AI for code completion
|
||||
- Claude for code assistance
|
||||
- GitHub Copilot with context
|
||||
|
||||
### Debugging Tips
|
||||
```typescript
|
||||
// Check connection
|
||||
console.log('Connected relays:',
|
||||
Array.from(ndk.pool?.relays.values() || [])
|
||||
.filter(r => r.status === 1)
|
||||
.map(r => r.url)
|
||||
)
|
||||
|
||||
// Verify signer
|
||||
console.log('Signer:', ndk.signer)
|
||||
console.log('Active user:', ndk.activeUser)
|
||||
|
||||
// Event inspection
|
||||
console.log('Event:', {
|
||||
id: event.id,
|
||||
kind: event.kind,
|
||||
tags: event.tags,
|
||||
sig: event.sig
|
||||
})
|
||||
```
|
||||
|
||||
## 📊 Statistics
|
||||
|
||||
- **Total Documentation**: ~50KB
|
||||
- **Code Examples**: 5 complete modules
|
||||
- **Patterns Documented**: 50+
|
||||
- **Common Issues Covered**: 15+
|
||||
- **Based On**: Real production code
|
||||
|
||||
## 🔗 Additional Resources
|
||||
|
||||
### Official NDK Resources
|
||||
- **GitHub**: https://github.com/nostr-dev-kit/ndk
|
||||
- **Documentation**: https://ndk.fyi
|
||||
- **NPM**: `@nostr-dev-kit/ndk`
|
||||
|
||||
### Nostr Protocol
|
||||
- **NIPs**: https://github.com/nostr-protocol/nips
|
||||
- **Nostr**: https://nostr.com
|
||||
|
||||
### Related Tools
|
||||
- **TanStack Query**: React state management
|
||||
- **TanStack Router**: Type-safe routing
|
||||
- **Radix UI**: Accessible components
|
||||
|
||||
## 💡 Tips for Using This Skill
|
||||
|
||||
1. **Start Small**: Begin with quick-reference.md for syntax
|
||||
2. **Go Deep**: Read ndk-skill.md section by section
|
||||
3. **Copy Examples**: Use examples/ as templates
|
||||
4. **Debug Issues**: Check troubleshooting.md first
|
||||
5. **Stay Updated**: Patterns based on production usage
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
This skill is maintained based on the Plebeian Market codebase. To improve it:
|
||||
|
||||
1. Document new patterns you discover
|
||||
2. Add solutions to common problems
|
||||
3. Update examples with better approaches
|
||||
4. Keep synchronized with NDK updates
|
||||
|
||||
## 📝 Version Info
|
||||
|
||||
- **Skill Version**: 1.0.0
|
||||
- **NDK Version**: Latest (based on production usage)
|
||||
- **Last Updated**: November 2025
|
||||
- **Codebase**: Plebeian Market
|
||||
|
||||
---
|
||||
|
||||
**Ready to build with NDK?** Start with `quick-reference.md` or dive into `examples/01-initialization.ts`!
|
||||
|
||||
38
.claude/skills/ndk/README.md
Normal file
38
.claude/skills/ndk/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# NDK (Nostr Development Kit) Claude Skill
|
||||
|
||||
This skill provides comprehensive knowledge about working with the Nostr Development Kit (NDK) library.
|
||||
|
||||
## Files
|
||||
|
||||
- **ndk-skill.md** - Complete reference documentation with patterns from production usage
|
||||
- **quick-reference.md** - Quick lookup guide for common NDK tasks
|
||||
- **examples/** - Code examples extracted from the Plebeian Market codebase
|
||||
|
||||
## Usage
|
||||
|
||||
When working with NDK-related code, reference these documents to:
|
||||
- Understand initialization patterns
|
||||
- Learn authentication flows (NIP-07, NIP-46, private keys)
|
||||
- Implement event creation and publishing
|
||||
- Set up subscriptions for real-time updates
|
||||
- Query events with filters
|
||||
- Handle users and profiles
|
||||
- Integrate with TanStack Query
|
||||
|
||||
## Key Topics Covered
|
||||
|
||||
1. NDK Initialization & Configuration
|
||||
2. Authentication & Signers
|
||||
3. Event Creation & Publishing
|
||||
4. Querying Events
|
||||
5. Real-time Subscriptions
|
||||
6. User & Profile Management
|
||||
7. Tag Handling
|
||||
8. Replaceable Events
|
||||
9. Relay Management
|
||||
10. Integration with React/TanStack Query
|
||||
11. Error Handling & Best Practices
|
||||
12. Performance Optimization
|
||||
|
||||
All examples are based on real production code from the Plebeian Market application.
|
||||
|
||||
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
|
||||
|
||||
701
.claude/skills/ndk/ndk-skill.md
Normal file
701
.claude/skills/ndk/ndk-skill.md
Normal file
@@ -0,0 +1,701 @@
|
||||
# NDK (Nostr Development Kit) - Claude Skill Reference
|
||||
|
||||
## Overview
|
||||
|
||||
NDK is the primary Nostr development kit with outbox-model support, designed for building Nostr applications with TypeScript/JavaScript. This reference is based on analyzing production usage in the Plebeian Market codebase.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. NDK Initialization
|
||||
|
||||
**Basic Pattern:**
|
||||
```typescript
|
||||
import NDK from '@nostr-dev-kit/ndk'
|
||||
|
||||
// Simple initialization
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band']
|
||||
})
|
||||
|
||||
await ndk.connect()
|
||||
```
|
||||
|
||||
**Store-based Pattern (Production):**
|
||||
```typescript
|
||||
// From src/lib/stores/ndk.ts
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: relays || defaultRelaysUrls,
|
||||
})
|
||||
|
||||
// Separate NDK for zaps on specialized relays
|
||||
const zapNdk = new NDK({
|
||||
explicitRelayUrls: ZAP_RELAYS,
|
||||
})
|
||||
|
||||
// Connect with timeout protection
|
||||
const connectPromise = ndk.connect()
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Connection timeout')), timeoutMs)
|
||||
)
|
||||
await Promise.race([connectPromise, timeoutPromise])
|
||||
```
|
||||
|
||||
### 2. Authentication & Signers
|
||||
|
||||
NDK supports multiple signer types for different authentication methods:
|
||||
|
||||
#### NIP-07 (Browser Extension)
|
||||
```typescript
|
||||
import { NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const signer = new NDKNip07Signer()
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
|
||||
const user = await signer.user()
|
||||
```
|
||||
|
||||
#### Private Key Signer
|
||||
```typescript
|
||||
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const signer = new NDKPrivateKeySigner(privateKeyHex)
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
|
||||
const user = await signer.user()
|
||||
```
|
||||
|
||||
#### NIP-46 (Remote Signer / Bunker)
|
||||
```typescript
|
||||
import { NDKNip46Signer } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const localSigner = new NDKPrivateKeySigner(localPrivateKey)
|
||||
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
|
||||
await remoteSigner.blockUntilReady()
|
||||
ndk.signer = remoteSigner
|
||||
|
||||
const user = await remoteSigner.user()
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Always call `blockUntilReady()` before using a signer
|
||||
- Store signer reference in your state management
|
||||
- Set `ndk.signer` to enable signing operations
|
||||
- Use `await signer.user()` to get the authenticated user
|
||||
|
||||
### 3. Event Creation & Publishing
|
||||
|
||||
#### Basic Event Pattern
|
||||
```typescript
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
// Create event
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 1 // Kind 1 = text note
|
||||
event.content = "Hello Nostr!"
|
||||
event.tags = [
|
||||
['t', 'nostr'],
|
||||
['p', recipientPubkey]
|
||||
]
|
||||
|
||||
// Sign and publish
|
||||
await event.sign() // Uses ndk.signer automatically
|
||||
await event.publish()
|
||||
|
||||
// Get event ID after signing
|
||||
console.log(event.id)
|
||||
```
|
||||
|
||||
#### Production Pattern with Error Handling
|
||||
```typescript
|
||||
// From src/publish/orders.tsx
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = ORDER_PROCESS_KIND
|
||||
event.content = orderNotes || ''
|
||||
event.tags = [
|
||||
['p', sellerPubkey],
|
||||
['subject', `Order for ${productName}`],
|
||||
['type', 'order-creation'],
|
||||
['order', orderId],
|
||||
['amount', totalAmount],
|
||||
['item', productRef, quantity.toString()],
|
||||
]
|
||||
|
||||
// Optional tags
|
||||
if (shippingRef) {
|
||||
event.tags.push(['shipping', shippingRef])
|
||||
}
|
||||
|
||||
try {
|
||||
await event.sign(signer) // Can pass explicit signer
|
||||
await event.publish()
|
||||
return event.id
|
||||
} catch (error) {
|
||||
console.error('Failed to publish event:', error)
|
||||
throw error
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Create event with `new NDKEvent(ndk)`
|
||||
- Set `kind`, `content`, and `tags` properties
|
||||
- Optional: Set `created_at` timestamp (defaults to now)
|
||||
- Call `await event.sign()` before publishing
|
||||
- Call `await event.publish()` to broadcast to relays
|
||||
- Access `event.id` after signing for the event hash
|
||||
|
||||
### 4. Querying Events with Filters
|
||||
|
||||
#### fetchEvents() - One-time Fetch
|
||||
```typescript
|
||||
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||
|
||||
// Simple filter
|
||||
const filter: NDKFilter = {
|
||||
kinds: [30402], // Product listings
|
||||
authors: [merchantPubkey],
|
||||
limit: 50
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
// Returns Set<NDKEvent>
|
||||
|
||||
// Convert to array and process
|
||||
const eventArray = Array.from(events)
|
||||
const sortedEvents = eventArray.sort((a, b) =>
|
||||
(b.created_at || 0) - (a.created_at || 0)
|
||||
)
|
||||
```
|
||||
|
||||
#### Advanced Filters
|
||||
```typescript
|
||||
// Multiple kinds
|
||||
const filter: NDKFilter = {
|
||||
kinds: [16, 17], // Orders and payment receipts
|
||||
'#order': [orderId], // Tag filter (# prefix)
|
||||
since: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours
|
||||
limit: 100
|
||||
}
|
||||
|
||||
// Event ID lookup
|
||||
const filter: NDKFilter = {
|
||||
ids: [eventIdHex],
|
||||
}
|
||||
|
||||
// Tag filtering
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1],
|
||||
'#p': [pubkey], // Events mentioning pubkey
|
||||
'#t': ['nostr'], // Events with hashtag 'nostr'
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Subscriptions (Real-time)
|
||||
|
||||
#### Basic Subscription
|
||||
```typescript
|
||||
// From src/queries/blacklist.tsx
|
||||
const filter = {
|
||||
kinds: [10000],
|
||||
authors: [appPubkey],
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(filter, {
|
||||
closeOnEose: false, // Keep open for real-time updates
|
||||
})
|
||||
|
||||
subscription.on('event', (event: NDKEvent) => {
|
||||
console.log('New event received:', event)
|
||||
// Process event
|
||||
})
|
||||
|
||||
subscription.on('eose', () => {
|
||||
console.log('End of stored events')
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
subscription.stop()
|
||||
```
|
||||
|
||||
#### Production Pattern with React Query
|
||||
```typescript
|
||||
// From src/queries/orders.tsx
|
||||
useEffect(() => {
|
||||
if (!orderId || !ndk) return
|
||||
|
||||
const filter = {
|
||||
kinds: [ORDER_PROCESS_KIND, PAYMENT_RECEIPT_KIND],
|
||||
'#order': [orderId],
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(filter, {
|
||||
closeOnEose: false,
|
||||
})
|
||||
|
||||
subscription.on('event', (newEvent) => {
|
||||
// Invalidate React Query cache to trigger refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orderKeys.details(orderId)
|
||||
})
|
||||
})
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
subscription.stop()
|
||||
}
|
||||
}, [orderId, ndk, queryClient])
|
||||
```
|
||||
|
||||
#### Monitoring Specific Events
|
||||
```typescript
|
||||
// From src/queries/payment.tsx - Payment receipt monitoring
|
||||
const receiptFilter = {
|
||||
kinds: [17], // Payment receipts
|
||||
'#order': [orderId],
|
||||
'#payment-request': [invoiceId],
|
||||
since: sessionStartTime - 30, // Clock skew buffer
|
||||
}
|
||||
|
||||
const subscription = ndk.subscribe(receiptFilter, {
|
||||
closeOnEose: false,
|
||||
})
|
||||
|
||||
subscription.on('event', (receiptEvent: NDKEvent) => {
|
||||
// Verify this is the correct invoice
|
||||
const paymentRequestTag = receiptEvent.tags.find(
|
||||
tag => tag[0] === 'payment-request'
|
||||
)
|
||||
|
||||
if (paymentRequestTag?.[1] === invoiceId) {
|
||||
const paymentTag = receiptEvent.tags.find(tag => tag[0] === 'payment')
|
||||
const preimage = paymentTag?.[3] || 'external-payment'
|
||||
|
||||
// Stop subscription after finding payment
|
||||
subscription.stop()
|
||||
handlePaymentReceived(preimage)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Key Subscription Patterns:**
|
||||
- Use `closeOnEose: false` for real-time monitoring
|
||||
- Use `closeOnEose: true` for one-time historical fetch
|
||||
- Always call `subscription.stop()` in cleanup
|
||||
- Listen to both `'event'` and `'eose'` events
|
||||
- Filter events in the handler for specific conditions
|
||||
- Integrate with React Query for reactive UI updates
|
||||
|
||||
### 6. User & Profile Handling
|
||||
|
||||
#### Fetching User Profiles
|
||||
```typescript
|
||||
// From src/queries/profiles.tsx
|
||||
|
||||
// By npub
|
||||
const user = ndk.getUser({ npub })
|
||||
const profile = await user.fetchProfile()
|
||||
// Returns NDKUserProfile with name, picture, about, etc.
|
||||
|
||||
// By hex pubkey
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
const profile = await user.fetchProfile()
|
||||
|
||||
// By NIP-05 identifier
|
||||
const user = await ndk.getUserFromNip05('user@domain.com')
|
||||
if (user) {
|
||||
const profile = await user.fetchProfile()
|
||||
}
|
||||
|
||||
// Profile fields
|
||||
const name = profile?.name || profile?.displayName
|
||||
const avatar = profile?.picture || profile?.image
|
||||
const bio = profile?.about
|
||||
const nip05 = profile?.nip05
|
||||
const lud16 = profile?.lud16 // Lightning address
|
||||
```
|
||||
|
||||
#### Getting Current User
|
||||
```typescript
|
||||
// Active user (authenticated)
|
||||
const user = ndk.activeUser
|
||||
|
||||
// From signer
|
||||
const user = await ndk.signer?.user()
|
||||
|
||||
// User properties
|
||||
const pubkey = user.pubkey // Hex format
|
||||
const npub = user.npub // NIP-19 encoded
|
||||
```
|
||||
|
||||
### 7. NDK Event Object
|
||||
|
||||
#### Essential Properties
|
||||
```typescript
|
||||
interface NDKEvent {
|
||||
id: string // Event hash (after signing)
|
||||
kind: number // Event kind
|
||||
content: string // Event content
|
||||
tags: NDKTag[] // Array of tag arrays
|
||||
created_at?: number // Unix timestamp
|
||||
pubkey?: string // Author pubkey (after signing)
|
||||
sig?: string // Signature (after signing)
|
||||
|
||||
// Methods
|
||||
sign(signer?: NDKSigner): Promise<void>
|
||||
publish(): Promise<void>
|
||||
tagValue(tagName: string): string | undefined
|
||||
}
|
||||
|
||||
type NDKTag = string[] // e.g., ['p', pubkey, relay, petname]
|
||||
```
|
||||
|
||||
#### Tag Helpers
|
||||
```typescript
|
||||
// Get first value of a tag
|
||||
const orderId = event.tagValue('order')
|
||||
const recipientPubkey = event.tagValue('p')
|
||||
|
||||
// Find specific tag
|
||||
const paymentTag = event.tags.find(tag => tag[0] === 'payment')
|
||||
const preimage = paymentTag?.[3]
|
||||
|
||||
// Get all tags of a type
|
||||
const pTags = event.tags.filter(tag => tag[0] === 'p')
|
||||
const allPubkeys = pTags.map(tag => tag[1])
|
||||
|
||||
// Common tag patterns
|
||||
event.tags.push(['p', pubkey]) // Mention
|
||||
event.tags.push(['e', eventId]) // Reference event
|
||||
event.tags.push(['t', 'nostr']) // Hashtag
|
||||
event.tags.push(['d', identifier]) // Replaceable event ID
|
||||
event.tags.push(['a', '30402:pubkey:d-tag']) // Addressable event reference
|
||||
```
|
||||
|
||||
### 8. Parameterized Replaceable Events (NIP-33)
|
||||
|
||||
Used for products, collections, profiles that need updates:
|
||||
|
||||
```typescript
|
||||
// Product listing (kind 30402)
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 30402
|
||||
event.content = JSON.stringify(productDetails)
|
||||
event.tags = [
|
||||
['d', productSlug], // Unique identifier
|
||||
['title', productName],
|
||||
['price', price, currency],
|
||||
['image', imageUrl],
|
||||
['shipping', shippingRef],
|
||||
]
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
// Querying replaceable events
|
||||
const filter = {
|
||||
kinds: [30402],
|
||||
authors: [merchantPubkey],
|
||||
'#d': [productSlug], // Specific product
|
||||
}
|
||||
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
// Returns only the latest version due to replaceable nature
|
||||
```
|
||||
|
||||
### 9. Relay Management
|
||||
|
||||
#### Getting Relay Status
|
||||
```typescript
|
||||
// From src/lib/stores/ndk.ts
|
||||
const connectedRelays = Array.from(ndk.pool?.relays.values() || [])
|
||||
.filter(relay => relay.status === 1) // 1 = connected
|
||||
.map(relay => relay.url)
|
||||
|
||||
const outboxRelays = Array.from(ndk.outboxPool?.relays.values() || [])
|
||||
```
|
||||
|
||||
#### Adding Relays
|
||||
```typescript
|
||||
// Add explicit relays
|
||||
ndk.addExplicitRelay('wss://relay.example.com')
|
||||
|
||||
// Multiple relays
|
||||
const relays = ['wss://relay1.com', 'wss://relay2.com']
|
||||
relays.forEach(url => ndk.addExplicitRelay(url))
|
||||
```
|
||||
|
||||
### 10. Common Patterns & Best Practices
|
||||
|
||||
#### Null Safety
|
||||
```typescript
|
||||
// Always check NDK initialization
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
|
||||
// Check signer before operations requiring auth
|
||||
const signer = ndk.signer
|
||||
if (!signer) throw new Error('No active signer')
|
||||
|
||||
// Check user authentication
|
||||
const user = ndk.activeUser
|
||||
if (!user) throw new Error('Not authenticated')
|
||||
```
|
||||
|
||||
#### Error Handling
|
||||
```typescript
|
||||
try {
|
||||
const events = await ndk.fetchEvents(filter)
|
||||
if (events.size === 0) {
|
||||
return null // No results found
|
||||
}
|
||||
return Array.from(events)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch events:', error)
|
||||
throw new Error('Could not fetch data from relays')
|
||||
}
|
||||
```
|
||||
|
||||
#### Connection Lifecycle
|
||||
```typescript
|
||||
// Initialize once at app startup
|
||||
const ndk = new NDK({ explicitRelayUrls: relays })
|
||||
|
||||
// Connect with timeout
|
||||
await Promise.race([
|
||||
ndk.connect(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), 10000)
|
||||
)
|
||||
])
|
||||
|
||||
// Check connection status
|
||||
const isConnected = ndk.pool?.connectedRelays().length > 0
|
||||
|
||||
// Reconnect if needed
|
||||
if (!isConnected) {
|
||||
await ndk.connect()
|
||||
}
|
||||
```
|
||||
|
||||
#### Subscription Cleanup
|
||||
```typescript
|
||||
// In React components
|
||||
useEffect(() => {
|
||||
if (!ndk) return
|
||||
|
||||
const sub = ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
sub.on('event', handleEvent)
|
||||
sub.on('eose', handleEose)
|
||||
|
||||
// Critical: cleanup on unmount
|
||||
return () => {
|
||||
sub.stop()
|
||||
}
|
||||
}, [dependencies])
|
||||
```
|
||||
|
||||
#### Event Validation
|
||||
```typescript
|
||||
// Check required fields before processing
|
||||
if (!event.pubkey) {
|
||||
console.error('Event missing pubkey')
|
||||
return
|
||||
}
|
||||
|
||||
if (!event.created_at) {
|
||||
console.error('Event missing timestamp')
|
||||
return
|
||||
}
|
||||
|
||||
// Verify event age
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const eventAge = now - (event.created_at || 0)
|
||||
if (eventAge > 86400) { // Older than 24 hours
|
||||
console.log('Event is old, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
// Validate specific tags exist
|
||||
const orderId = event.tagValue('order')
|
||||
if (!orderId) {
|
||||
console.error('Order event missing order ID')
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Common Event Kinds
|
||||
|
||||
```typescript
|
||||
// NIP-01: Basic Events
|
||||
const KIND_METADATA = 0 // User profile
|
||||
const KIND_TEXT_NOTE = 1 // Short text note
|
||||
const KIND_RECOMMEND_RELAY = 2 // Relay recommendation
|
||||
|
||||
// NIP-04: Encrypted Direct Messages
|
||||
const KIND_ENCRYPTED_DM = 4
|
||||
|
||||
// NIP-25: Reactions
|
||||
const KIND_REACTION = 7
|
||||
|
||||
// NIP-51: Lists
|
||||
const KIND_MUTE_LIST = 10000
|
||||
const KIND_PIN_LIST = 10001
|
||||
const KIND_RELAY_LIST = 10002
|
||||
|
||||
// NIP-57: Lightning Zaps
|
||||
const KIND_ZAP_REQUEST = 9734
|
||||
const KIND_ZAP_RECEIPT = 9735
|
||||
|
||||
// Marketplace (Plebeian/Gamma spec)
|
||||
const ORDER_PROCESS_KIND = 16 // Order processing
|
||||
const PAYMENT_RECEIPT_KIND = 17 // Payment receipts
|
||||
const DIRECT_MESSAGE_KIND = 14 // Direct messages
|
||||
const ORDER_GENERAL_KIND = 27 // General order events
|
||||
const SHIPPING_KIND = 30405 // Shipping options
|
||||
const PRODUCT_KIND = 30402 // Product listings
|
||||
const COLLECTION_KIND = 30401 // Product collections
|
||||
const REVIEW_KIND = 30407 // Product reviews
|
||||
|
||||
// Application Handlers
|
||||
const APP_HANDLER_KIND = 31990 // NIP-89 app handlers
|
||||
```
|
||||
|
||||
## Integration with TanStack Query
|
||||
|
||||
NDK works excellently with TanStack Query for reactive data fetching:
|
||||
|
||||
### Query Functions
|
||||
```typescript
|
||||
// From src/queries/products.tsx
|
||||
export const fetchProductsByPubkey = async (pubkey: string) => {
|
||||
const ndk = ndkActions.getNDK()
|
||||
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).map(parseProductEvent)
|
||||
}
|
||||
|
||||
export const useProductsByPubkey = (pubkey: string) => {
|
||||
return useQuery({
|
||||
queryKey: productKeys.byAuthor(pubkey),
|
||||
queryFn: () => fetchProductsByPubkey(pubkey),
|
||||
enabled: !!pubkey,
|
||||
staleTime: 30000,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Combining Queries with Subscriptions
|
||||
```typescript
|
||||
// Query for initial data
|
||||
const { data: order, refetch } = useQuery({
|
||||
queryKey: orderKeys.details(orderId),
|
||||
queryFn: () => fetchOrderById(orderId),
|
||||
enabled: !!orderId,
|
||||
})
|
||||
|
||||
// Subscription for real-time updates
|
||||
useEffect(() => {
|
||||
if (!orderId || !ndk) return
|
||||
|
||||
const sub = ndk.subscribe(
|
||||
{ kinds: [16, 17], '#order': [orderId] },
|
||||
{ closeOnEose: false }
|
||||
)
|
||||
|
||||
sub.on('event', () => {
|
||||
// Invalidate query to trigger refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: orderKeys.details(orderId)
|
||||
})
|
||||
})
|
||||
|
||||
return () => sub.stop()
|
||||
}, [orderId, ndk, queryClient])
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Events Not Received
|
||||
- Check relay connections: `ndk.pool?.connectedRelays()`
|
||||
- Verify filter syntax (especially tag filters with `#` prefix)
|
||||
- Check event timestamps match filter's `since`/`until`
|
||||
- Ensure `closeOnEose: false` for real-time subscriptions
|
||||
|
||||
### Signing Errors
|
||||
- Verify signer is initialized: `await signer.blockUntilReady()`
|
||||
- Check signer is set: `ndk.signer !== undefined`
|
||||
- For NIP-07, ensure browser extension is installed and enabled
|
||||
- For NIP-46, verify bunker URL and local signer are correct
|
||||
|
||||
### Connection Timeouts
|
||||
- Implement connection timeout pattern shown above
|
||||
- Try connecting to fewer, more reliable relays initially
|
||||
- Use fallback relays in production
|
||||
|
||||
### Duplicate Events
|
||||
- NDK deduplicates by event ID automatically
|
||||
- For subscriptions, track processed event IDs if needed
|
||||
- Use replaceable events (kinds 10000-19999, 30000-39999) when appropriate
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Batching Queries
|
||||
```typescript
|
||||
// Instead of multiple fetchEvents calls
|
||||
const [products, orders, profiles] = await Promise.all([
|
||||
ndk.fetchEvents(productFilter),
|
||||
ndk.fetchEvents(orderFilter),
|
||||
ndk.fetchEvents(profileFilter),
|
||||
])
|
||||
```
|
||||
|
||||
### Limiting Results
|
||||
```typescript
|
||||
const filter = {
|
||||
kinds: [1],
|
||||
authors: [pubkey],
|
||||
limit: 50, // Limit results
|
||||
since: recentTimestamp, // Only recent events
|
||||
}
|
||||
```
|
||||
|
||||
### Caching with React Query
|
||||
```typescript
|
||||
export const useProfile = (npub: string) => {
|
||||
return useQuery({
|
||||
queryKey: profileKeys.byNpub(npub),
|
||||
queryFn: () => fetchProfileByNpub(npub),
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
cacheTime: 30 * 60 * 1000, // 30 minutes
|
||||
enabled: !!npub,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- **NDK GitHub**: https://github.com/nostr-dev-kit/ndk
|
||||
- **NDK Documentation**: https://ndk.fyi
|
||||
- **Nostr NIPs**: https://github.com/nostr-protocol/nips
|
||||
- **Production Example**: Plebeian Market codebase
|
||||
|
||||
## Key Files in This Codebase
|
||||
|
||||
- `src/lib/stores/ndk.ts` - NDK store and initialization
|
||||
- `src/lib/stores/auth.ts` - Authentication with NDK signers
|
||||
- `src/queries/*.tsx` - Query patterns with NDK
|
||||
- `src/publish/*.tsx` - Event publishing patterns
|
||||
- `scripts/gen_*.ts` - Event creation examples
|
||||
|
||||
---
|
||||
|
||||
*This reference is based on NDK version used in production and real-world patterns from the Plebeian Market application.*
|
||||
|
||||
351
.claude/skills/ndk/quick-reference.md
Normal file
351
.claude/skills/ndk/quick-reference.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# NDK Quick Reference
|
||||
|
||||
Fast lookup guide for common NDK tasks.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import NDK from '@nostr-dev-kit/ndk'
|
||||
|
||||
const ndk = new NDK({ explicitRelayUrls: ['wss://relay.damus.io'] })
|
||||
await ndk.connect()
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### Browser Extension (NIP-07)
|
||||
```typescript
|
||||
import { NDKNip07Signer } from '@nostr-dev-kit/ndk'
|
||||
const signer = new NDKNip07Signer()
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
```
|
||||
|
||||
### Private Key
|
||||
```typescript
|
||||
import { NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'
|
||||
const signer = new NDKPrivateKeySigner(privateKeyHex)
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
```
|
||||
|
||||
### Remote Signer (NIP-46)
|
||||
```typescript
|
||||
import { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'
|
||||
const localSigner = new NDKPrivateKeySigner()
|
||||
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
|
||||
await remoteSigner.blockUntilReady()
|
||||
ndk.signer = remoteSigner
|
||||
```
|
||||
|
||||
## Publish Event
|
||||
|
||||
```typescript
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 1
|
||||
event.content = "Hello Nostr!"
|
||||
event.tags = [['t', 'nostr']]
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
```
|
||||
|
||||
## Query Events (One-time)
|
||||
|
||||
```typescript
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [1],
|
||||
authors: [pubkey],
|
||||
limit: 50
|
||||
})
|
||||
|
||||
// Convert Set to Array
|
||||
const eventArray = Array.from(events)
|
||||
```
|
||||
|
||||
## Subscribe (Real-time)
|
||||
|
||||
```typescript
|
||||
const sub = ndk.subscribe(
|
||||
{ kinds: [1], authors: [pubkey] },
|
||||
{ closeOnEose: false }
|
||||
)
|
||||
|
||||
sub.on('event', (event) => {
|
||||
console.log('New event:', event.content)
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
sub.stop()
|
||||
```
|
||||
|
||||
## Get User Profile
|
||||
|
||||
```typescript
|
||||
// By npub
|
||||
const user = ndk.getUser({ npub })
|
||||
const profile = await user.fetchProfile()
|
||||
|
||||
// By hex pubkey
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
const profile = await user.fetchProfile()
|
||||
|
||||
// By NIP-05
|
||||
const user = await ndk.getUserFromNip05('user@domain.com')
|
||||
const profile = await user?.fetchProfile()
|
||||
```
|
||||
|
||||
## Common Filters
|
||||
|
||||
```typescript
|
||||
// By author
|
||||
{ kinds: [1], authors: [pubkey] }
|
||||
|
||||
// By tag
|
||||
{ kinds: [1], '#p': [pubkey] }
|
||||
{ kinds: [30402], '#d': [productSlug] }
|
||||
|
||||
// By time
|
||||
{
|
||||
kinds: [1],
|
||||
since: Math.floor(Date.now() / 1000) - 86400, // Last 24h
|
||||
until: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
// By event ID
|
||||
{ ids: [eventId] }
|
||||
|
||||
// Multiple conditions
|
||||
{
|
||||
kinds: [16, 17],
|
||||
'#order': [orderId],
|
||||
since: timestamp,
|
||||
limit: 100
|
||||
}
|
||||
```
|
||||
|
||||
## Tag Helpers
|
||||
|
||||
```typescript
|
||||
// Get first tag value
|
||||
const orderId = event.tagValue('order')
|
||||
|
||||
// Find specific tag
|
||||
const tag = event.tags.find(t => t[0] === 'payment')
|
||||
const value = tag?.[1]
|
||||
|
||||
// Get all of one type
|
||||
const pTags = event.tags.filter(t => t[0] === 'p')
|
||||
|
||||
// Common tag formats
|
||||
['p', pubkey] // Mention
|
||||
['e', eventId] // Event reference
|
||||
['t', 'nostr'] // Hashtag
|
||||
['d', identifier] // Replaceable ID
|
||||
['a', '30402:pubkey:d-tag'] // Addressable reference
|
||||
```
|
||||
|
||||
## Error Handling Pattern
|
||||
|
||||
```typescript
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
|
||||
const signer = ndk.signer
|
||||
if (!signer) throw new Error('No active signer')
|
||||
|
||||
try {
|
||||
await event.publish()
|
||||
} catch (error) {
|
||||
console.error('Publish failed:', error)
|
||||
throw error
|
||||
}
|
||||
```
|
||||
|
||||
## React Integration
|
||||
|
||||
```typescript
|
||||
// Query function
|
||||
export const fetchProducts = async (pubkey: string) => {
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [30402],
|
||||
authors: [pubkey]
|
||||
})
|
||||
|
||||
return Array.from(events)
|
||||
}
|
||||
|
||||
// React Query hook
|
||||
export const useProducts = (pubkey: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['products', pubkey],
|
||||
queryFn: () => fetchProducts(pubkey),
|
||||
enabled: !!pubkey,
|
||||
})
|
||||
}
|
||||
|
||||
// Subscription in useEffect
|
||||
useEffect(() => {
|
||||
if (!ndk || !orderId) return
|
||||
|
||||
const sub = ndk.subscribe(
|
||||
{ kinds: [16], '#order': [orderId] },
|
||||
{ closeOnEose: false }
|
||||
)
|
||||
|
||||
sub.on('event', () => {
|
||||
queryClient.invalidateQueries(['order', orderId])
|
||||
})
|
||||
|
||||
return () => sub.stop()
|
||||
}, [ndk, orderId, queryClient])
|
||||
```
|
||||
|
||||
## Common Event Kinds
|
||||
|
||||
```typescript
|
||||
0 // Metadata (profile)
|
||||
1 // Text note
|
||||
4 // Encrypted DM (NIP-04)
|
||||
7 // Reaction
|
||||
9735 // Zap receipt
|
||||
10000 // Mute list
|
||||
10002 // Relay list
|
||||
30402 // Product listing (Marketplace)
|
||||
31990 // App handler (NIP-89)
|
||||
```
|
||||
|
||||
## Relay Management
|
||||
|
||||
```typescript
|
||||
// Check connection
|
||||
const connected = ndk.pool?.connectedRelays().length > 0
|
||||
|
||||
// Get connected relays
|
||||
const relays = Array.from(ndk.pool?.relays.values() || [])
|
||||
.filter(r => r.status === 1)
|
||||
|
||||
// Add relay
|
||||
ndk.addExplicitRelay('wss://relay.example.com')
|
||||
```
|
||||
|
||||
## Connection with Timeout
|
||||
|
||||
```typescript
|
||||
const connectWithTimeout = async (timeoutMs = 10000) => {
|
||||
const connectPromise = ndk.connect()
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
|
||||
)
|
||||
|
||||
await Promise.race([connectPromise, timeoutPromise])
|
||||
}
|
||||
```
|
||||
|
||||
## Current User
|
||||
|
||||
```typescript
|
||||
// Active user
|
||||
const user = ndk.activeUser
|
||||
|
||||
// From signer
|
||||
const user = await ndk.signer?.user()
|
||||
|
||||
// User info
|
||||
const pubkey = user.pubkey // hex
|
||||
const npub = user.npub // NIP-19
|
||||
```
|
||||
|
||||
## Parameterized Replaceable Events
|
||||
|
||||
```typescript
|
||||
// Create
|
||||
const event = new NDKEvent(ndk)
|
||||
event.kind = 30402
|
||||
event.content = JSON.stringify(data)
|
||||
event.tags = [
|
||||
['d', uniqueIdentifier], // Required for replaceable
|
||||
['title', 'Product Name'],
|
||||
]
|
||||
|
||||
await event.sign()
|
||||
await event.publish()
|
||||
|
||||
// Query (returns latest only)
|
||||
const events = await ndk.fetchEvents({
|
||||
kinds: [30402],
|
||||
authors: [pubkey],
|
||||
'#d': [identifier]
|
||||
})
|
||||
```
|
||||
|
||||
## Validation Checks
|
||||
|
||||
```typescript
|
||||
// Event age check
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const age = now - (event.created_at || 0)
|
||||
if (age > 86400) console.log('Event older than 24h')
|
||||
|
||||
// Required fields
|
||||
if (!event.pubkey || !event.created_at || !event.sig) {
|
||||
throw new Error('Invalid event')
|
||||
}
|
||||
|
||||
// Tag existence
|
||||
const orderId = event.tagValue('order')
|
||||
if (!orderId) throw new Error('Missing order tag')
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
```typescript
|
||||
// Batch queries
|
||||
const [products, orders] = await Promise.all([
|
||||
ndk.fetchEvents(productFilter),
|
||||
ndk.fetchEvents(orderFilter)
|
||||
])
|
||||
|
||||
// Limit results
|
||||
const filter = {
|
||||
kinds: [1],
|
||||
limit: 50,
|
||||
since: recentTimestamp
|
||||
}
|
||||
|
||||
// Cache with React Query
|
||||
const { data } = useQuery({
|
||||
queryKey: ['profile', npub],
|
||||
queryFn: () => fetchProfile(npub),
|
||||
staleTime: 5 * 60 * 1000, // 5 min
|
||||
})
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```typescript
|
||||
// Check NDK state
|
||||
console.log('Connected:', ndk.pool?.connectedRelays())
|
||||
console.log('Signer:', ndk.signer)
|
||||
console.log('Active user:', ndk.activeUser)
|
||||
|
||||
// Event inspection
|
||||
console.log('Event ID:', event.id)
|
||||
console.log('Tags:', event.tags)
|
||||
console.log('Content:', event.content)
|
||||
console.log('Author:', event.pubkey)
|
||||
|
||||
// Subscription events
|
||||
sub.on('event', e => console.log('Event:', e))
|
||||
sub.on('eose', () => console.log('End of stored events'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For detailed explanations and advanced patterns, see `ndk-skill.md`.
|
||||
|
||||
530
.claude/skills/ndk/troubleshooting.md
Normal file
530
.claude/skills/ndk/troubleshooting.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# NDK Common Patterns & Troubleshooting
|
||||
|
||||
Quick reference for common patterns and solutions to frequent NDK issues.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Store-Based NDK Management
|
||||
|
||||
```typescript
|
||||
// Store pattern (recommended for React apps)
|
||||
import { Store } from '@tanstack/store'
|
||||
|
||||
interface NDKState {
|
||||
ndk: NDK | null
|
||||
isConnected: boolean
|
||||
signer?: NDKSigner
|
||||
}
|
||||
|
||||
const ndkStore = new Store<NDKState>({
|
||||
ndk: null,
|
||||
isConnected: false
|
||||
})
|
||||
|
||||
export const ndkActions = {
|
||||
initialize: () => {
|
||||
const ndk = new NDK({ explicitRelayUrls: relays })
|
||||
ndkStore.setState({ ndk })
|
||||
return ndk
|
||||
},
|
||||
|
||||
getNDK: () => ndkStore.state.ndk,
|
||||
|
||||
setSigner: (signer: NDKSigner) => {
|
||||
const ndk = ndkStore.state.ndk
|
||||
if (ndk) {
|
||||
ndk.signer = signer
|
||||
ndkStore.setState({ signer })
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Query + Subscription Pattern
|
||||
|
||||
```typescript
|
||||
// Initial data load + real-time updates
|
||||
function useOrdersWithRealtime(orderId: string) {
|
||||
const queryClient = useQueryClient()
|
||||
const ndk = ndkActions.getNDK()
|
||||
|
||||
// Fetch initial data
|
||||
const query = useQuery({
|
||||
queryKey: ['orders', orderId],
|
||||
queryFn: () => fetchOrders(orderId),
|
||||
})
|
||||
|
||||
// Subscribe to updates
|
||||
useEffect(() => {
|
||||
if (!ndk || !orderId) return
|
||||
|
||||
const sub = ndk.subscribe(
|
||||
{ kinds: [16], '#order': [orderId] },
|
||||
{ closeOnEose: false }
|
||||
)
|
||||
|
||||
sub.on('event', () => {
|
||||
queryClient.invalidateQueries(['orders', orderId])
|
||||
})
|
||||
|
||||
return () => sub.stop()
|
||||
}, [ndk, orderId])
|
||||
|
||||
return query
|
||||
}
|
||||
```
|
||||
|
||||
### Event Parsing Pattern
|
||||
|
||||
```typescript
|
||||
// Parse event tags into structured data
|
||||
function parseProductEvent(event: NDKEvent) {
|
||||
const getTag = (name: string) =>
|
||||
event.tags.find(t => t[0] === name)?.[1]
|
||||
|
||||
const getAllTags = (name: string) =>
|
||||
event.tags.filter(t => t[0] === name).map(t => t[1])
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
slug: getTag('d'),
|
||||
title: getTag('title'),
|
||||
price: parseFloat(getTag('price') || '0'),
|
||||
currency: event.tags.find(t => t[0] === 'price')?.[2] || 'USD',
|
||||
images: getAllTags('image'),
|
||||
shipping: getAllTags('shipping'),
|
||||
description: event.content,
|
||||
createdAt: event.created_at,
|
||||
author: event.pubkey
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Relay Pool Pattern
|
||||
|
||||
```typescript
|
||||
// Separate NDK instances for different purposes
|
||||
const mainNdk = new NDK({
|
||||
explicitRelayUrls: ['wss://relay.damus.io', 'wss://nos.lol']
|
||||
})
|
||||
|
||||
const zapNdk = new NDK({
|
||||
explicitRelayUrls: ['wss://relay.damus.io'] // Zap-optimized relays
|
||||
})
|
||||
|
||||
const blossomNdk = new NDK({
|
||||
explicitRelayUrls: ['wss://blossom.server.com'] // Media server
|
||||
})
|
||||
|
||||
await Promise.all([
|
||||
mainNdk.connect(),
|
||||
zapNdk.connect(),
|
||||
blossomNdk.connect()
|
||||
])
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Events Not Received
|
||||
|
||||
**Symptoms:** Subscription doesn't receive events, fetchEvents returns empty Set
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check relay connection:
|
||||
```typescript
|
||||
const status = ndk.pool?.connectedRelays()
|
||||
console.log('Connected relays:', status?.length)
|
||||
if (status?.length === 0) {
|
||||
await ndk.connect()
|
||||
}
|
||||
```
|
||||
|
||||
2. Verify filter syntax (especially tags):
|
||||
```typescript
|
||||
// ❌ Wrong
|
||||
{ kinds: [16], 'order': [orderId] }
|
||||
|
||||
// ✅ Correct (note the # prefix for tags)
|
||||
{ kinds: [16], '#order': [orderId] }
|
||||
```
|
||||
|
||||
3. Check timestamps:
|
||||
```typescript
|
||||
// Events might be too old/new
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const filter = {
|
||||
kinds: [1],
|
||||
since: now - 86400, // Last 24 hours
|
||||
until: now
|
||||
}
|
||||
```
|
||||
|
||||
4. Ensure closeOnEose is correct:
|
||||
```typescript
|
||||
// For real-time updates
|
||||
ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
// For one-time historical fetch
|
||||
ndk.subscribe(filter, { closeOnEose: true })
|
||||
```
|
||||
|
||||
### Problem: "NDK not initialized"
|
||||
|
||||
**Symptoms:** `ndk` is null/undefined
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Initialize before use:
|
||||
```typescript
|
||||
// In app entry point
|
||||
const ndk = new NDK({ explicitRelayUrls: relays })
|
||||
await ndk.connect()
|
||||
```
|
||||
|
||||
2. Add null checks:
|
||||
```typescript
|
||||
const ndk = ndkActions.getNDK()
|
||||
if (!ndk) throw new Error('NDK not initialized')
|
||||
```
|
||||
|
||||
3. Use initialization guard:
|
||||
```typescript
|
||||
const ensureNDK = () => {
|
||||
let ndk = ndkActions.getNDK()
|
||||
if (!ndk) {
|
||||
ndk = ndkActions.initialize()
|
||||
}
|
||||
return ndk
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: "No active signer" / Cannot Sign Events
|
||||
|
||||
**Symptoms:** Event signing fails, publishing throws error
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check signer is set:
|
||||
```typescript
|
||||
if (!ndk.signer) {
|
||||
throw new Error('Please login first')
|
||||
}
|
||||
```
|
||||
|
||||
2. Ensure blockUntilReady called:
|
||||
```typescript
|
||||
const signer = new NDKNip07Signer()
|
||||
await signer.blockUntilReady() // ← Critical!
|
||||
ndk.signer = signer
|
||||
```
|
||||
|
||||
3. Handle NIP-07 unavailable:
|
||||
```typescript
|
||||
try {
|
||||
const signer = new NDKNip07Signer()
|
||||
await signer.blockUntilReady()
|
||||
ndk.signer = signer
|
||||
} catch (error) {
|
||||
console.error('Browser extension not available')
|
||||
// Fallback to other auth method
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: Duplicate Events in Subscriptions
|
||||
|
||||
**Symptoms:** Same event received multiple times
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Track processed event IDs:
|
||||
```typescript
|
||||
const processedIds = new Set<string>()
|
||||
|
||||
sub.on('event', (event) => {
|
||||
if (processedIds.has(event.id)) return
|
||||
processedIds.add(event.id)
|
||||
handleEvent(event)
|
||||
})
|
||||
```
|
||||
|
||||
2. Use Map for event storage:
|
||||
```typescript
|
||||
const [events, setEvents] = useState<Map<string, NDKEvent>>(new Map())
|
||||
|
||||
sub.on('event', (event) => {
|
||||
setEvents(prev => new Map(prev).set(event.id, event))
|
||||
})
|
||||
```
|
||||
|
||||
### Problem: Connection Timeout
|
||||
|
||||
**Symptoms:** connect() hangs, never resolves
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Use timeout wrapper:
|
||||
```typescript
|
||||
const connectWithTimeout = async (ndk: NDK, ms = 10000) => {
|
||||
await Promise.race([
|
||||
ndk.connect(),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), ms)
|
||||
)
|
||||
])
|
||||
}
|
||||
```
|
||||
|
||||
2. Try fewer relays:
|
||||
```typescript
|
||||
// Start with reliable relays only
|
||||
const reliableRelays = ['wss://relay.damus.io']
|
||||
const ndk = new NDK({ explicitRelayUrls: reliableRelays })
|
||||
```
|
||||
|
||||
3. Add connection retry:
|
||||
```typescript
|
||||
const connectWithRetry = async (ndk: NDK, maxRetries = 3) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await connectWithTimeout(ndk, 10000)
|
||||
return
|
||||
} catch (error) {
|
||||
console.log(`Retry ${i + 1}/${maxRetries}`)
|
||||
if (i === maxRetries - 1) throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: Subscription Memory Leak
|
||||
|
||||
**Symptoms:** App gets slower, memory usage increases
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Always stop subscriptions:
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const sub = ndk.subscribe(filter, { closeOnEose: false })
|
||||
|
||||
// ← CRITICAL: cleanup
|
||||
return () => {
|
||||
sub.stop()
|
||||
}
|
||||
}, [dependencies])
|
||||
```
|
||||
|
||||
2. Track active subscriptions:
|
||||
```typescript
|
||||
const activeSubscriptions = new Set<NDKSubscription>()
|
||||
|
||||
const createSub = (filter: NDKFilter) => {
|
||||
const sub = ndk.subscribe(filter, { closeOnEose: false })
|
||||
activeSubscriptions.add(sub)
|
||||
return sub
|
||||
}
|
||||
|
||||
const stopAllSubs = () => {
|
||||
activeSubscriptions.forEach(sub => sub.stop())
|
||||
activeSubscriptions.clear()
|
||||
}
|
||||
```
|
||||
|
||||
### Problem: Profile Not Found
|
||||
|
||||
**Symptoms:** fetchProfile() returns null/undefined
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check different relays:
|
||||
```typescript
|
||||
// Add more relay URLs
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: [
|
||||
'wss://relay.damus.io',
|
||||
'wss://relay.nostr.band',
|
||||
'wss://nos.lol'
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
2. Verify pubkey format:
|
||||
```typescript
|
||||
// Ensure correct format
|
||||
if (pubkey.startsWith('npub')) {
|
||||
const user = ndk.getUser({ npub: pubkey })
|
||||
} else if (/^[0-9a-f]{64}$/.test(pubkey)) {
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
}
|
||||
```
|
||||
|
||||
3. Handle missing profiles gracefully:
|
||||
```typescript
|
||||
const profile = await user.fetchProfile()
|
||||
const displayName = profile?.name || profile?.displayName || 'Anonymous'
|
||||
const avatar = profile?.picture || '/default-avatar.png'
|
||||
```
|
||||
|
||||
### Problem: Events Published But Not Visible
|
||||
|
||||
**Symptoms:** publish() succeeds but event not found in queries
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify event was signed:
|
||||
```typescript
|
||||
await event.sign()
|
||||
console.log('Event ID:', event.id) // Should be set
|
||||
console.log('Signature:', event.sig) // Should exist
|
||||
```
|
||||
|
||||
2. Check relay acceptance:
|
||||
```typescript
|
||||
const relays = await event.publish()
|
||||
console.log('Published to relays:', relays)
|
||||
```
|
||||
|
||||
3. Query immediately after publish:
|
||||
```typescript
|
||||
await event.publish()
|
||||
|
||||
// Wait a moment for relay propagation
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const found = await ndk.fetchEvents({ ids: [event.id] })
|
||||
console.log('Event found:', found.size > 0)
|
||||
```
|
||||
|
||||
### Problem: NIP-46 Connection Fails
|
||||
|
||||
**Symptoms:** Remote signer connection times out or fails
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Verify bunker URL format:
|
||||
```typescript
|
||||
// Correct format: bunker://<remote-pubkey>?relay=wss://...
|
||||
const isValidBunkerUrl = (url: string) => {
|
||||
return url.startsWith('bunker://') && url.includes('?relay=')
|
||||
}
|
||||
```
|
||||
|
||||
2. Ensure local signer is ready:
|
||||
```typescript
|
||||
const localSigner = new NDKPrivateKeySigner(privateKey)
|
||||
await localSigner.blockUntilReady()
|
||||
|
||||
const remoteSigner = new NDKNip46Signer(ndk, bunkerUrl, localSigner)
|
||||
await remoteSigner.blockUntilReady()
|
||||
```
|
||||
|
||||
3. Store credentials for reconnection:
|
||||
```typescript
|
||||
// Save for future sessions
|
||||
localStorage.setItem('local-signer-key', localSigner.privateKey)
|
||||
localStorage.setItem('bunker-url', bunkerUrl)
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
### Optimize Queries
|
||||
|
||||
```typescript
|
||||
// ❌ Slow: Multiple sequential queries
|
||||
const products = await ndk.fetchEvents({ kinds: [30402], authors: [pk1] })
|
||||
const orders = await ndk.fetchEvents({ kinds: [16], authors: [pk1] })
|
||||
const profiles = await ndk.fetchEvents({ kinds: [0], authors: [pk1] })
|
||||
|
||||
// ✅ Fast: Parallel queries
|
||||
const [products, orders, profiles] = await Promise.all([
|
||||
ndk.fetchEvents({ kinds: [30402], authors: [pk1] }),
|
||||
ndk.fetchEvents({ kinds: [16], authors: [pk1] }),
|
||||
ndk.fetchEvents({ kinds: [0], authors: [pk1] })
|
||||
])
|
||||
```
|
||||
|
||||
### Cache Profile Lookups
|
||||
|
||||
```typescript
|
||||
const profileCache = new Map<string, NDKUserProfile>()
|
||||
|
||||
const getCachedProfile = async (ndk: NDK, pubkey: string) => {
|
||||
if (profileCache.has(pubkey)) {
|
||||
return profileCache.get(pubkey)!
|
||||
}
|
||||
|
||||
const user = ndk.getUser({ hexpubkey: pubkey })
|
||||
const profile = await user.fetchProfile()
|
||||
if (profile) {
|
||||
profileCache.set(pubkey, profile)
|
||||
}
|
||||
|
||||
return profile
|
||||
}
|
||||
```
|
||||
|
||||
### Limit Result Sets
|
||||
|
||||
```typescript
|
||||
// Always use limit to prevent over-fetching
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1],
|
||||
authors: [pubkey],
|
||||
limit: 50 // ← Important!
|
||||
}
|
||||
```
|
||||
|
||||
### Debounce Subscription Updates
|
||||
|
||||
```typescript
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const debouncedUpdate = debounce((event: NDKEvent) => {
|
||||
handleEvent(event)
|
||||
}, 300)
|
||||
|
||||
sub.on('event', debouncedUpdate)
|
||||
```
|
||||
|
||||
## Testing Tips
|
||||
|
||||
### Mock NDK in Tests
|
||||
|
||||
```typescript
|
||||
const mockNDK = {
|
||||
fetchEvents: vi.fn().mockResolvedValue(new Set()),
|
||||
subscribe: vi.fn().mockReturnValue({
|
||||
on: vi.fn(),
|
||||
stop: vi.fn()
|
||||
}),
|
||||
signer: {
|
||||
user: vi.fn().mockResolvedValue({ pubkey: 'test-pubkey' })
|
||||
}
|
||||
} as unknown as NDK
|
||||
```
|
||||
|
||||
### Test Event Creation
|
||||
|
||||
```typescript
|
||||
const createTestEvent = (overrides?: Partial<NDKEvent>): NDKEvent => {
|
||||
return {
|
||||
id: 'test-id',
|
||||
kind: 1,
|
||||
content: 'test content',
|
||||
tags: [],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: 'test-pubkey',
|
||||
sig: 'test-sig',
|
||||
...overrides
|
||||
} as NDKEvent
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For more detailed information, see:
|
||||
- `ndk-skill.md` - Complete reference
|
||||
- `quick-reference.md` - Quick lookup
|
||||
- `examples/` - Code examples
|
||||
|
||||
Reference in New Issue
Block a user