From a4584ed9bd6d91960068a4c708005fb62785103f Mon Sep 17 00:00:00 2001 From: padreug Date: Wed, 13 Aug 2025 15:18:54 +0200 Subject: [PATCH] feat: Add NOSTR architecture documentation - Introduce a comprehensive documentation file detailing the singleton-based architecture of the Ario web app. - Outline the core singletons for authentication, relay management, chat, market, and Nostr store, along with their integration into components. - Provide insights into the benefits of the architecture, including resource management, event handling, and performance optimizations. - Include guidelines for configuration, error handling, and future extensibility to support ongoing development. --- NOSTR_ARCHITECTURE.md | 782 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 782 insertions(+) create mode 100644 NOSTR_ARCHITECTURE.md diff --git a/NOSTR_ARCHITECTURE.md b/NOSTR_ARCHITECTURE.md new file mode 100644 index 0000000..4ff0c7f --- /dev/null +++ b/NOSTR_ARCHITECTURE.md @@ -0,0 +1,782 @@ +# Ario Web App Architecture + +## Overview + +The Ario web app uses a **singleton-based architecture** to manage shared state and resources across components. This document explains how the core singletons work and how different components (chat, market, feed) plug into the system. + +## Core Singleton Architecture + +### 1. Authentication Singleton (`useAuth`) + +**Location**: `src/composables/useAuth.ts` + +**Purpose**: Manages user authentication state using LNBits integration. + +**Singleton Pattern**: +```typescript +export function useAuth() { + // ... implementation +} + +// Export singleton instance for global state +export const auth = useAuth() +``` + +**Usage**: Imported throughout the app for authentication state: +```typescript +import { auth } from '@/composables/useAuth' + +// All components see the same authentication state +console.log(auth.isAuthenticated.value) +``` + +### 2. Relay Hub Singleton (`relayHub`) + +**Location**: `src/lib/nostr/relayHub.ts` + +**Purpose**: Centralized management of Nostr relay connections, providing a single WebSocket connection pool for the entire application. + +**Singleton Pattern**: +```typescript +export class RelayHub extends EventEmitter { + // ... implementation +} + +// Export singleton instance +export const relayHub = new RelayHub() +``` + +**Key Features**: +- Single WebSocket connection pool +- Automatic reconnection and health checks +- Event broadcasting for connection state changes +- Mobile visibility handling for WebSocket management + +### 3. Relay Hub Composable Singleton (`useRelayHub`) + +**Location**: `src/composables/useRelayHub.ts` + +**Purpose**: Vue-reactive wrapper around the core `relayHub` instance, providing reactive state and Vue-specific functionality. + +**Singleton Pattern**: +```typescript +export function useRelayHub() { + // Uses the same relayHub instance + const initialize = async () => { + await relayHub.initialize(relayUrls) + } + + return { initialize, connect, disconnect, ... } +} + +// Export singleton instance +export const relayHubComposable = useRelayHub() +``` + +**Relationship**: This composable wraps the core `relayHub` instance, making it Vue-reactive while maintaining the singleton connection pool. + +### 4. Chat Singleton (`useNostrChat`) + +**Location**: `src/composables/useNostrChat.ts` + +**Purpose**: Manages Nostr chat functionality including message encryption/decryption, peer management, and real-time messaging. + +**Singleton Pattern**: +```typescript +export function useNostrChat() { + // ... implementation +} + +// Export singleton instance for global state +export const nostrChat = useNostrChat() +``` + +**Relay Hub Integration**: Uses `useRelayHub()` to connect to relays and manage subscriptions. + +### 5. Market Singleton (`useMarket`) + +**Location**: `src/composables/useMarket.ts` + +**Purpose**: Manages market functionality including stalls, products, and market data. + +**Singleton Pattern**: +```typescript +export function useMarket() { + // ... implementation +} + +// Export singleton instance +export const market = useMarket() +``` + +**Relay Hub Integration**: Uses `useRelayHub()` to fetch market data from Nostr relays. + +### 6. Nostr Store Singleton (`useNostrStore`) + +**Location**: `src/stores/nostr.ts` + +**Purpose**: Pinia store for Nostr-related state including push notifications and relay connection status. + +**Singleton Pattern**: +```typescript +export const useNostrStore = defineStore('nostr', () => { + // ... implementation +}) +``` + +**Relay Hub Integration**: Imports the `relayHub` singleton directly for connection management. + +## Component Integration Architecture + +### How Components Plug Into the System + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Component Layer │ +├─────────────────────────────────────────────────────────────┤ +│ ChatComponent │ NostrFeed │ Market.vue │ Navbar.vue │ +│ ↓ │ ↓ │ ↓ │ ↓ │ +├─────────────────────────────────────────────────────────────┤ +│ Composable Layer │ +├─────────────────────────────────────────────────────────────┤ +│ useNostrChat │ useRelayHub │ useMarket │ useAuth │ +│ ↓ │ ↓ │ ↓ │ ↓ │ +├─────────────────────────────────────────────────────────────┤ +│ Singleton Instance Layer │ +├─────────────────────────────────────────────────────────────┤ +│ nostrChat │ relayHub │ market │ auth │ +│ ↓ │ ↓ │ ↓ │ ↓ │ +├─────────────────────────────────────────────────────────────┤ +│ Core Implementation Layer │ +├─────────────────────────────────────────────────────────────┤ +│ RelayHub │ LNBits API │ Market API │ Chat API │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 1. Chat Component Integration + +**Component**: `src/components/nostr/ChatComponent.vue` + +**Integration Path**: +```typescript +// 1. Component imports the chat singleton +import { nostrChat } from '@/composables/useNostrChat' + +// 2. Chat singleton uses relay hub composable +import { useRelayHub } from './useRelayHub' + +// 3. Relay hub composable uses core relay hub singleton +import { relayHub } from '../lib/nostr/relayHub' +``` + +**Data Flow**: +``` +ChatComponent → nostrChat → useRelayHub → relayHub → Nostr Relays + ↓ ↓ ↓ ↓ + UI Updates Chat State Vue State WebSocket +``` + +**Key Benefits**: +- Single chat state across all components +- Shared relay connections +- Centralized message encryption/decryption +- Unified peer management + +### 2. Feed Component Integration + +**Component**: `src/components/nostr/NostrFeed.vue` + +**Integration Path**: +```typescript +// 1. Component imports relay hub composable +import { useRelayHub } from '@/composables/useRelayHub' + +// 2. Relay hub composable uses core relay hub singleton +import { relayHub } from '../lib/nostr/relayHub' +``` + +**Data Flow**: +``` +NostrFeed → useRelayHub → relayHub → Nostr Relays + ↓ ↓ ↓ + Feed UI Vue State WebSocket +``` + +**Key Benefits**: +- Real-time feed updates +- Shared relay connections +- Centralized admin filtering +- Efficient event subscription management + +### 3. Market Component Integration + +**Component**: `src/pages/Market.vue` + +**Integration Path**: +```typescript +// 1. Component uses market composable +import { useMarket } from '@/composables/useMarket' + +// 2. Market composable uses relay hub composable +import { useRelayHub } from './useRelayHub' + +// 3. Relay hub composable uses core relay hub singleton +import { relayHub } from '../lib/nostr/relayHub' +``` + +**Data Flow**: +``` +Market.vue → useMarket → useRelayHub → relayHub → Nostr Relays + ↓ ↓ ↓ ↓ + Market UI Market Vue State WebSocket + State +``` + +**Key Benefits**: +- Centralized market data +- Shared relay connections +- Efficient product/stall fetching +- Real-time market updates + +## Singleton Benefits in Practice + +### 1. **Shared Connection State** +```typescript +// Component A +const { isConnected } = useRelayHub() +console.log(isConnected.value) // true + +// Component B (different part of app) +const { isConnected } = useRelayHub() +console.log(isConnected.value) // true (same state!) +``` + +### 2. **Centralized Resource Management** +```typescript +// All components share the same WebSocket connections +relayHub.connectedRelayCount // Same value everywhere +relayHub.totalSubscriptionCount // Same value everywhere +``` + +### 3. **Event Broadcasting** +```typescript +// Component A listens to connection events +relayHub.on('connected', () => console.log('Connected!')) + +// Component B triggers connection +await relayHub.connect() // Component A gets notified! +``` + +### 4. **Efficient Resource Usage** +- **Before**: Each component creates its own relay connections +- **After**: Single connection pool shared across all components +- **Result**: Reduced memory usage, better performance, consistent state + +## Configuration and Environment Variables + +### Required Environment Variables +```bash +# Nostr relay configuration +VITE_NOSTR_RELAYS=["wss://relay1.example.com", "wss://relay2.example.com"] + +# Admin pubkeys for feed filtering +VITE_ADMIN_PUBKEYS=["npub1admin1...", "npub1admin2..."] + +# LNBits API configuration +VITE_LNBITS_BASE_URL=https://your-lnbits-instance.com +VITE_API_KEY=your-api-key +``` + +### Configuration Structure +```typescript +// src/lib/config/index.ts +export const config: AppConfig = { + nostr: { + relays: parseJsonEnv(import.meta.env.VITE_NOSTR_RELAYS, []), + adminPubkeys: parseJsonEnv(import.meta.env.VITE_ADMIN_PUBKEYS, []) + }, + api: { + baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || '', + key: import.meta.env.VITE_API_KEY || '' + } +} +``` + +## Testing and Development + +### Mocking Singletons for Testing +```typescript +// In test files, you can mock the singleton instances +import { auth } from '@/composables/useAuth' +import { nostrChat } from '@/composables/useNostrChat' + +// Mock the singletons +vi.mock('@/composables/useAuth', () => ({ + auth: { + isAuthenticated: ref(false), + currentUser: ref(null) + } +})) +``` + +### Development Benefits +- **Hot Reload**: Singleton state persists across component reloads +- **Debugging**: Single point of inspection for shared state +- **Performance**: No unnecessary re-initialization of expensive resources + +## Best Practices + +### 1. **Always Use the Singleton Instance** +```typescript +// ✅ Good: Use the exported singleton +import { auth } from '@/composables/useAuth' +import { nostrChat } from '@/composables/useNostrChat' + +// ❌ Bad: Don't create new instances +const { currentUser } = useAuth() // This creates a new instance! +``` + +### 2. **Access State Through Composables** +```typescript +// ✅ Good: Use the composable wrapper for reactive state +const { isConnected, connectionStatus } = useRelayHub() + +// ❌ Bad: Don't access relayHub directly in components +import { relayHub } from '@/lib/nostr/relayHub' +console.log(relayHub.isConnected) // Not reactive! +``` + +### 3. **Listen to Events for State Changes** +```typescript +// ✅ Good: Listen to relay hub events +relayHub.on('connected', () => { + console.log('Relay connected!') +}) + +// ❌ Bad: Don't poll for state changes +setInterval(() => { + console.log(relayHub.isConnected) // Inefficient! +}, 1000) +``` + +## Conclusion + +The singleton architecture in Ario provides: + +1. **Efficient Resource Management**: Single connection pools and shared state +2. **Consistent User Experience**: All components see the same data +3. **Simplified Development**: Clear patterns for state management +4. **Better Performance**: No duplicate connections or state +5. **Easier Testing**: Centralized mocking of shared resources + +This architecture makes the app more maintainable, performant, and user-friendly while providing a solid foundation for future features. + +## Shared Nostr Function Execution + +### Event Building and Publishing Pattern + +All components that need to create and publish Nostr events follow a consistent pattern through the relay hub singleton: + +#### 1. Event Creation Utilities + +**Direct Event Template Creation** (used in `useNostrChat`): +```typescript +import { nip04, finalizeEvent, type EventTemplate } from 'nostr-tools' + +// Create event template +const eventTemplate: EventTemplate = { + kind: 4, // Encrypted DM + created_at: Math.floor(Date.now() / 1000), + tags: [['p', peerPubkey]] + content: encryptedContent +} + +// Finalize and sign the event +const event = finalizeEvent(eventTemplate, hexToBytes(privateKey)) +``` + +**Market Event Creation** (used in `useMarket`): +```typescript +// Market events use specific kinds and tags +const MARKET_EVENT_KINDS = { + STALL: 30017, + PRODUCT: 30018, + MARKET: 30019 +} + +// Extract data from event tags +const stallId = event.tags.find((tag: any) => tag[0] === 'd')?.[1] +const productId = event.tags.find((tag: any) => tag[0] === 'd')?.[1] +``` + +#### 2. Centralized Publishing Through Relay Hub + +**All components publish through the same singleton**: +```typescript +// In useNostrChat.ts +await relayHub.publishEvent(event) + +// In useMarket.ts (if publishing) +await relayHub.publishEvent(event) + +// In any other component +await relayHub.publishEvent(event) +``` + +**Relay Hub Publishing Logic**: +```typescript +async publishEvent(event: Event): Promise<{ success: number; total: number }> { + if (!this._isConnected) { + throw new Error('Not connected to any relays') + } + + const relayUrls = Array.from(this.connectedRelays.keys()) + const results = await Promise.allSettled( + relayUrls.map(relay => this.pool.publish([relay], event)) + ) + + const successful = results.filter(result => result.status === 'fulfilled').length + const total = results.length + + this.emit('eventPublished', { eventId: event.id, success: successful, total }) + return { success: successful, total } +} +``` + +#### 3. Event Querying and Subscription + +**Consistent Query Interface**: +```typescript +// One-time event queries +const events = await relayHub.queryEvents(filters) + +// Real-time subscriptions +const unsubscribe = relayHub.subscribe({ + id: 'subscription-id', + filters: [{ kinds: [1], limit: 50 }], + onEvent: (event) => { + // Handle incoming event + } +}) +``` + +### Shared Dependencies + +**nostr-tools Package**: +- `finalizeEvent` - Event signing and finalization +- `nip04` - Encrypted message handling +- `SimplePool` - Relay connection management +- `EventTemplate` - Event structure typing + +**Common Event Patterns**: +- **Text Notes** (kind 1): Community posts and announcements +- **Encrypted DMs** (kind 4): Private chat messages +- **Market Events** (kinds 30017-30019): Stall, product, and market data +- **Reactions** (kind 7): Emoji reactions to posts + +## Configuration Management + +### Environment Variables and Configuration + +**Centralized Config** (`src/lib/config/index.ts`): +```typescript +interface NostrConfig { + relays: string[] + adminPubkeys: string[] +} + +interface MarketConfig { + defaultNaddr: string + supportedRelays: string[] + lightningEnabled: boolean + defaultCurrency: string +} + +// Environment variables +VITE_NOSTR_RELAYS: JSON array of relay URLs +VITE_ADMIN_PUBKEYS: JSON array of admin public keys +VITE_MARKET_RELAYS: JSON array of market-specific relays +VITE_LIGHTNING_ENABLED: Boolean for Lightning integration +``` + +**Default Relay Configuration**: +```typescript +// Fallback relay configuration +supportedRelays: [ + 'ws://127.0.0.1:7777', + 'wss://relay.damus.io', + 'wss://relay.snort.social', + 'wss://nostr-pub.wellorder.net', + 'wss://nostr.zebedee.cloud', + 'wss://nostr.walletofsatoshi.com' +] +``` + +### Configuration Utilities + +**Admin Pubkey Validation**: +```typescript +export const configUtils = { + isAdminPubkey(pubkey: string): boolean { + return config.nostr.adminPubkeys.includes(pubkey) + } +} +``` + +## Error Handling and Resilience + +### Connection Management + +**Automatic Reconnection**: +```typescript +// Relay hub automatically handles reconnection +private scheduleReconnect(): void { + if (this.reconnectInterval) { + clearTimeout(this.reconnectInterval) + } + + this.reconnectInterval = setTimeout(async () => { + await this.connect() + }, this.reconnectDelay) +} +``` + +**Health Monitoring**: +```typescript +// Continuous health checks +private startHealthCheck(): void { + this.healthCheckInterval = setInterval(() => { + this.performHealthCheck() + }, this.healthCheckIntervalMs) +} +``` + +### Error Handling Patterns + +**Graceful Degradation**: +```typescript +// Components handle connection failures gracefully +async function loadNotes() { + try { + if (!relayHub.isConnected.value) { + await relayHub.connect() + } + // ... load notes + } catch (err) { + error.value = err instanceof Error ? err : new Error('Failed to load notes') + console.error('Failed to load notes:', err) + } +} +``` + +**Retry Logic**: +```typescript +// Market app retry pattern +async def wait_for_nostr_events(nostr_client: NostrClient): + while True: + try: + await subscribe_to_all_merchants() + # ... process events + except Exception as e: + logger.warning(f"Subscription failed. Will retry in one minute: {e}") + await asyncio.sleep(10) +``` + +## Event Processing and Filtering + +### Filter Management + +**Dynamic Filter Creation**: +```typescript +// Feed component creates filters based on type +const filters: any[] = [{ + kinds: [1], // TEXT_NOTE + limit: 50 +}] + +// Filter by authors for announcements +if (props.feedType === 'announcements' && hasAdminPubkeys.value) { + filters[0].authors = adminPubkeys +} +``` + +**Market Event Filtering**: +```typescript +// Market-specific filters +const _filters_for_stall_events = (public_keys: List[str], since: int) -> List: + stall_filter = {"kinds": [30017], "authors": public_keys} + if since and since != 0: + stall_filter["since"] = since + return [stall_filter] +``` + +### Event Processing Pipeline + +**Event Processing Flow**: +```typescript +// 1. Receive event from relay +onEvent: (event) => { + // 2. Process and transform + const newNote = { + id: event.id, + pubkey: event.pubkey, + content: event.content, + created_at: event.created_at, + tags: event.tags || [], + mentions: event.tags?.filter(tag => tag[0] === 'p').map(tag => tag[1]) || [], + isReply: event.tags?.some(tag => tag[0] === 'e' && tag[3] === 'reply'), + replyTo: event.tags?.find(tag => tag[0] === 'e' && tag[3] === 'reply')?.[1] + } + + // 3. Apply business logic filters + let shouldInclude = true + if (props.feedType === 'announcements' && !isAdminPost(event.pubkey)) { + shouldInclude = false + } + + // 4. Add to state if approved + if (shouldInclude) { + notes.value.unshift(newNote) + // 5. Limit array size for memory management + if (notes.value.length > 100) { + notes.value = notes.value.slice(0, 100) + } + } +} +``` + +## Performance Optimizations + +### Memory Management + +**Array Size Limits**: +```typescript +// Prevent memory issues with large feeds +if (notes.value.length > 100) { + notes.value = notes.value.slice(0, 100) +} +``` + +**Subscription Cleanup**: +```typescript +// Proper cleanup prevents memory leaks +onUnmounted(() => { + if (unsubscribe) { + unsubscribe() + unsubscribe = null + } +}) +``` + +### Connection Pooling + +**Shared WebSocket Connections**: +```typescript +// Single pool shared across all components +private pool: SimplePool + +// Components don't create individual connections +// They all use the centralized relay hub +``` + +**Efficient Relay Usage**: +```typescript +// Query multiple relays simultaneously +const events = await this.pool.querySync(availableRelays, filter) + +// Publish to all relays in parallel +const results = await Promise.allSettled( + relayUrls.map(relay => this.pool.publish([relay], event)) +) +``` + +## Security Considerations + +### Encryption and Privacy + +**End-to-End Encryption**: +```typescript +// All chat messages are encrypted using nip04 +const encryptedContent = await nip04.encrypt( + privateKey, + publicKey, + content +) +``` + +**Private Key Management**: +```typescript +// Keys never leave the browser +// Validation of key format and length +if (privateKey.length !== 64) { + throw new Error(`Invalid private key length: ${privateKey.length} (expected 64)`) +} + +// Hex format validation +const hexRegex = /^[0-9a-fA-F]+$/ +if (!hexRegex.test(privateKey)) { + throw new Error(`Invalid private key format: contains non-hex characters`) +} +``` + +### Access Control + +**Admin Pubkey Validation**: +```typescript +// Only admin pubkeys can post announcements +function isAdminPost(pubkey: string): boolean { + return configUtils.isAdminPubkey(pubkey) +} + +// Filter content based on user permissions +if (props.feedType === 'announcements' && !isAdminPost(event.pubkey)) { + shouldInclude = false +} +``` + +## Benefits of This Architecture + +### 1. **Consistent Event Handling** +- All components use the same event creation and publishing patterns +- Centralized relay management ensures consistent behavior +- Shared error handling and retry logic + +### 2. **Efficient Resource Usage** +- Single WebSocket connection pool shared across all components +- No duplicate relay connections +- Centralized connection health monitoring + +### 3. **Simplified Component Logic** +- Components focus on business logic, not relay management +- Consistent API for all Nostr operations +- Easy to add new event types and functionality + +### 4. **Maintainable Codebase** +- Single source of truth for relay connections +- Centralized event publishing logic +- Easy to debug and monitor Nostr operations + +### 5. **Robust Error Handling** +- Automatic reconnection and health monitoring +- Graceful degradation when relays are unavailable +- Comprehensive error logging and user feedback + +### 6. **Performance Optimized** +- Memory management with array size limits +- Efficient connection pooling +- Parallel relay operations + +## Future Extensibility + +This architecture makes it easy to add new Nostr functionality: + +1. **New Event Types**: Add new event creation utilities following the existing pattern +2. **Additional Relays**: Configure new relays in the centralized relay hub +3. **New Components**: Import the existing singletons and use the established patterns +4. **Enhanced Features**: Extend the relay hub with new capabilities (e.g., event caching, filtering) +5. **Advanced Filtering**: Implement more sophisticated event filtering and processing +6. **Caching Layer**: Add event caching for improved performance +7. **Rate Limiting**: Implement rate limiting for relay operations +8. **Metrics and Monitoring**: Add comprehensive metrics for relay performance + +This architecture makes the app more maintainable, performant, and user-friendly while providing a solid foundation for future features.