mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-06 18:31:03 +00:00
544 lines
14 KiB
Markdown
544 lines
14 KiB
Markdown
# 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.
|