POWR/docs/archive/NDK_Comprehensive_Guide.md

544 lines
14 KiB
Markdown
Raw Normal View History

# ARCHIVED: NDK Comprehensive Guide for POWR App
**ARCHIVED:** This document has been superseded by [docs/technical/ndk/comprehensive_guide.md](../technical/ndk/comprehensive_guide.md) as part of the documentation reorganization.
**UPDATED FOR MVP: This guide has been consolidated and aligned with our simplified social approach for the MVP release.**
This guide combines key information from our various NDK analysis documents and the ndk-mobile package to provide a comprehensive reference for implementing Nostr features in POWR app. It's organized to support our MVP strategy with a focus on the core NDK capabilities we need.
## Table of Contents
1. [NDK Core Concepts](#ndk-core-concepts)
2. [Proper State Management](#proper-state-management)
3. [Subscription Lifecycle](#subscription-lifecycle)
4. [NIP-19 and Encoding/Decoding](#nip-19-and-encodingdecoding)
5. [Context Provider Pattern](#context-provider-pattern)
6. [Mobile-Specific Considerations](#mobile-specific-considerations)
7. [Best Practices for POWR MVP](#best-practices-for-powr-mvp)
## NDK Core Concepts
NDK (Nostr Development Kit) provides a set of tools for working with the Nostr protocol. Key components include:
- **NDK Instance**: Central object for interacting with the Nostr network
- **NDKEvent**: Represents a Nostr event
- **NDKUser**: Represents a user (pubkey)
- **NDKFilter**: Defines criteria for querying events
- **NDKSubscription**: Manages subscriptions to event streams
- **NDKRelay**: Represents a connection to a relay
### Basic Setup
```typescript
import NDK from '@nostr-dev-kit/ndk';
// Initialize NDK with relays
const ndk = new NDK({
explicitRelayUrls: [
'wss://relay.damus.io',
'wss://relay.nostr.band'
]
});
// Connect to relays
await ndk.connect();
```
## Proper State Management
The ndk-mobile library emphasizes proper state management using Zustand. This approach is critical for avoiding the issues we've been seeing in our application.
### Zustand Store Pattern
```typescript
// Create a singleton store using Zustand
import { create } from 'zustand';
// Private store instance - not exported
let store: ReturnType<typeof createNDKStore> | undefined;
// Store creator function
const createNDKStore = () => create<NDKState>((set) => ({
ndk: undefined,
initialized: false,
connecting: false,
connectionError: null,
initialize: (config) => set((state) => {
if (state.initialized) return state;
const ndk = new NDK(config);
return { ndk, initialized: true };
}),
connect: async () => {
set({ connecting: true, connectionError: null });
try {
const ndk = store?.getState().ndk;
if (!ndk) throw new Error("NDK not initialized");
await ndk.connect();
set({ connecting: false });
} catch (error) {
set({ connecting: false, connectionError: error });
}
}
}));
// Getter for the singleton store
export const getNDKStore = () => {
if (!store) {
store = createNDKStore();
}
return store;
};
// Hook for components to use
export const useNDKStore = <T>(selector: (state: NDKState) => T) => {
// Ensure the store exists
getNDKStore();
// Return the selected state
return useStore(store)(selector);
};
```
### Critical State Management Points
1. **Single Store Instance**: Always use a singleton pattern for NDK stores
2. **Lazy Initialization**: Only create the store when first accessed
3. **Proper Selectors**: Select only what you need from the store
4. **Clear State Transitions**: Define explicit state transitions (connecting, connected, error)
## Subscription Lifecycle
Proper subscription management is crucial for app stability and performance. The subscribe.ts file in ndk-mobile provides advanced subscription handling.
### Basic Subscription Pattern
```typescript
import { useEffect } from 'react';
import { useNDK } from './hooks/useNDK';
function EventComponent({ filter }) {
const { ndk } = useNDK();
const [events, setEvents] = useState([]);
useEffect(() => {
if (!ndk) return;
// Create subscription
const sub = ndk.subscribe(filter, {
closeOnEose: false, // Keep connection open
});
// Handle incoming events
sub.on('event', (event) => {
setEvents(prev => [...prev, event]);
});
// Start subscription
sub.start();
// Critical: Clean up subscription when component unmounts
return () => {
sub.stop();
};
}, [ndk, JSON.stringify(filter)]);
return (/* render events */);
}
```
### Enhanced Subscription Hook
The ndk-mobile package includes an enhanced useSubscribe hook with additional features:
```typescript
// Example based on ndk-mobile implementation
function useEnhancedSubscribe(filter, options = {}) {
const { ndk } = useNDK();
const [events, setEvents] = useState([]);
const [eose, setEose] = useState(false);
const subRef = useRef(null);
useEffect(() => {
if (!ndk) return;
// Create subscription
const sub = ndk.subscribe(filter, {
closeOnEose: options.closeOnEose || false,
wrap: options.wrap || false
});
subRef.current = sub;
// Handle incoming events
sub.on('event', (event) => {
// Process events (filtering, wrapping, etc.)
// Add to state
setEvents(prev => {
// Check for duplicates using event.id
if (prev.some(e => e.id === event.id)) return prev;
return [...prev, event];
});
});
// Handle end of stored events
sub.on('eose', () => {
setEose(true);
});
// Start subscription
sub.start();
// Clean up
return () => {
if (subRef.current) {
subRef.current.stop();
}
};
}, [ndk, JSON.stringify(filter), options]);
return { events, eose };
}
```
## NIP-19 and Encoding/Decoding
NIP-19 functions are essential for handling Nostr identifiers like npub, note, and naddr.
### Decoding NIP-19 Entities
```typescript
import { nip19 } from '@nostr-dev-kit/ndk';
// Decode any NIP-19 entity (naddr, npub, nsec, note, etc.)
function decodeNIP19(encoded: string) {
try {
const decoded = nip19.decode(encoded);
// decoded.type will be 'npub', 'note', 'naddr', etc.
// decoded.data will contain the data specific to that type
return decoded;
} catch (error) {
console.error('Invalid NIP-19 format:', error);
return null;
}
}
// Convert npub to hex pubkey
function npubToHex(npub: string) {
try {
const decoded = nip19.decode(npub);
if (decoded.type === 'npub') {
return decoded.data as string; // This is the hex pubkey
}
return null;
} catch (error) {
console.error('Invalid npub format:', error);
return null;
}
}
```
### Encoding to NIP-19 Formats
```typescript
// Create an npub from a hex public key
function hexToNpub(hexPubkey: string) {
return nip19.npubEncode(hexPubkey);
}
// Create a note (event reference) from event ID
function eventIdToNote(eventId: string) {
return nip19.noteEncode(eventId);
}
// Create an naddr for addressable events
function createNaddr(pubkey: string, kind: number, identifier: string) {
return nip19.naddrEncode({
pubkey, // Hex pubkey
kind, // Event kind (number)
identifier // The 'd' tag value
});
}
```
## Context Provider Pattern
The ndk-mobile package emphasizes the Context Provider pattern for proper NDK integration:
```typescript
import { NDKProvider } from '@nostr-dev-kit/ndk-mobile';
import App from './App';
// Root component
export default function Root() {
return (
<NDKProvider
params={{
explicitRelayUrls: [
'wss://relay.damus.io',
'wss://relay.nostr.band'
]
}}
>
<App />
</NDKProvider>
);
}
```
This pattern ensures:
1. **Single NDK Instance**: The entire app shares one NDK instance
2. **Consistent State**: Auth state and relay connections are managed in one place
3. **Hooks Availability**: All NDK hooks (useNDK, useSubscribe, etc.) work correctly
4. **Proper Cleanup**: Connections and subscriptions are managed appropriately
## Mobile-Specific Considerations
The ndk-mobile package includes several mobile-specific optimizations:
### Caching Strategy
Mobile devices need efficient caching to reduce network usage and improve performance:
```typescript
import { SQLiteAdapter } from '@nostr-dev-kit/ndk-mobile/cache-adapter/sqlite';
// Initialize NDK with SQLite caching
const ndk = new NDK({
explicitRelayUrls: [...],
cacheAdapter: new SQLiteAdapter({
dbName: 'nostr-cache.db'
})
});
```
### Mobile Signers
For secure key management on mobile devices:
```typescript
import { SecureStorageSigner } from '@nostr-dev-kit/ndk-mobile/signers/securestorage';
// Use secure storage for private keys
const signer = new SecureStorageSigner({
storageKey: 'nostr-private-key'
});
// Add to NDK
ndk.signer = signer;
```
## Best Practices for POWR MVP
Based on our analysis and the ndk-mobile implementation, here are key best practices for our MVP:
### 1. State Management
- **Use singleton stores** for NDK and session state
- **Implement proper state machine** for auth transitions with Zustand
- **Add event listeners/callbacks** for components to respond to auth changes
```typescript
// Authentication state using Zustand (aligned with ndk-mobile patterns)
export const useAuthStore = create((set) => ({
state: 'unauthenticated', // 'unauthenticated', 'authenticating', 'authenticated', 'deauthenticating'
user: null,
error: null,
startAuthentication: async (npub) => {
set({ state: 'authenticating', error: null });
try {
// Authentication logic here
const user = await authenticateUser(npub);
set({ state: 'authenticated', user });
} catch (error) {
set({ state: 'unauthenticated', error });
}
},
logout: async () => {
set({ state: 'deauthenticating' });
// Cleanup logic
set({ state: 'unauthenticated', user: null });
},
}));
```
### 2. Subscription Management
- **Always clean up subscriptions** when components unmount
- **Keep subscription references** in useRef to ensure proper cleanup
- **Use appropriate cache strategies** to reduce relay load
- **Implement proper error handling** for subscription failures
```typescript
// Example component with proper subscription management
function WorkoutShareComponent({ workoutId }) {
const { ndk } = useNDK();
const [relatedPosts, setRelatedPosts] = useState([]);
const subRef = useRef(null);
useEffect(() => {
if (!ndk) return;
// Create subscription for posts mentioning this workout
const sub = ndk.subscribe({
kinds: [1],
'#e': [workoutId]
}, {
closeOnEose: true,
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
});
subRef.current = sub;
// Handle events
sub.on('event', (event) => {
setRelatedPosts(prev => [...prev, event]);
});
// Start subscription
sub.start();
// Clean up on unmount
return () => {
if (subRef.current) {
subRef.current.stop();
}
};
}, [ndk, workoutId]);
// Component JSX
}
```
### 3. Simplified Social Features
For the MVP, we're focusing on:
- **Workout sharing**: Publishing kind 1301 workout records and kind 1 notes quoting them
- **Official feed**: Display only POWR official account posts in the social tab
- **Static content**: Prefer static content over complex real-time feeds
```typescript
// Example function to share a workout
async function shareWorkout(ndk, workout, caption) {
// First, publish the workout record (kind 1301)
const workoutEvent = new NDKEvent(ndk);
workoutEvent.kind = 1301;
workoutEvent.content = JSON.stringify(workout);
// Add appropriate tags
workoutEvent.tags = [
['d', workout.id],
['title', workout.title],
['duration', workout.duration.toString()]
];
// Publish workout record
const workoutPub = await workoutEvent.publish();
// Then create a kind 1 note quoting the workout
const noteEvent = new NDKEvent(ndk);
noteEvent.kind = 1;
noteEvent.content = caption || `Just completed ${workout.title}!`;
// Add e tag to reference the workout event
noteEvent.tags = [
['e', workoutEvent.id, '', 'quote']
];
// Publish social note
await noteEvent.publish();
return { workoutEvent, noteEvent };
}
```
### 4. Error Handling & Offline Support
- **Implement graceful fallbacks** for network errors
- **Store pending publish operations** for later retry
- **Use SQLite caching** for offline access to previously loaded data
```typescript
// Publication queue service example (aligned with mobile patterns)
class PublicationQueue {
async queueForPublication(event) {
try {
// Try immediate publication
await event.publish();
return true;
} catch (error) {
// Store for later retry
await this.storeEventForLater(event);
return false;
}
}
async publishPendingEvents() {
const pendingEvents = await this.getPendingEvents();
for (const event of pendingEvents) {
try {
await event.publish();
await this.markAsPublished(event);
} catch (error) {
// Keep in queue for next retry
console.error("Failed to publish:", error);
}
}
}
}
```
### 5. Provider Pattern
- **Use NDKProvider** to wrap the application
- **Configure NDK once** at the app root level
- **Access NDK via hooks** rather than creating instances
```typescript
// App.tsx
export default function App() {
return (
<NDKProvider
params={{
explicitRelayUrls: [
'wss://relay.damus.io',
'wss://relay.nostr.band'
]
}}
>
<YourAppComponents />
</NDKProvider>
);
}
// Using NDK in components
function SomeComponent() {
const { ndk } = useNDK();
// Use ndk here
return (/* component JSX */);
}
```
### 6. MVP Implementation Focus
For the MVP, focus on implementing:
1. **Core Authentication**: Proper login/logout with state management
2. **Workout Sharing**: Publication of workouts to Nostr
3. **Limited Social Features**: Static feed of official POWR account
4. **User Profile**: Basic user information display
Defer these for post-MVP:
1. Full social feed implementation
2. Real-time following/interaction
3. Complex subscription patterns
By following these best practices, we can create a stable foundation for the POWR app's MVP release that addresses the current architectural issues while providing a simplified but functional social experience.