Skip to content

Latest commit

 

History

History
executable file
·
921 lines (711 loc) · 33.1 KB

File metadata and controls

executable file
·
921 lines (711 loc) · 33.1 KB

Mostro Mobile App Initialization Process

Overview

This document provides a comprehensive technical guide to the Mostro Mobile app initialization process, detailing how the application bootstraps its core systems, establishes Nostr connectivity, and prepares for trading operations.

Purpose: Understanding the complete initialization flow enables developers to maintain, extend, and troubleshoot the app's startup sequence effectively.

Scope: Covers the entire startup process from app launch to ready-for-trading state, including system dependencies, architecture patterns, and integration points.


App Initialization Sequence

Core Initialization Flow

The Mostro Mobile app follows a carefully orchestrated initialization sequence managed by appInitializerProvider. This sequence ensures all systems are properly configured and connected before user interaction begins.

// lib/shared/providers/app_init_provider.dart
final appInitializerProvider = FutureProvider<void>((ref) async {
  // 1. Initialize NostrService
  final nostrService = ref.read(nostrServiceProvider);
  await nostrService.init(ref.read(settingsProvider));

  // 2. Initialize KeyManager  
  final keyManager = ref.read(keyManagerProvider);
  await keyManager.init();

  // 3. Initialize SessionNotifier - Load active trading sessions
  final sessionManager = ref.read(sessionNotifierProvider.notifier);
  await sessionManager.init();
  
  // 4. Create SubscriptionManager - Setup event subscriptions based on sessions
  ref.read(subscriptionManagerProvider);

  // 5. Configure background services for notifications and sync
  ref.listen<Settings>(settingsProvider, (previous, next) {
    ref.read(backgroundServiceProvider).updateSettings(next);
  });

  // 6. Initialize order notifiers for existing sessions
  final cutoff = DateTime.now().subtract(const Duration(hours: Config.sessionExpirationHours));
  for (final session in sessionManager.sessions) {
    if(session.orderId == null || session.startTime.isBefore(cutoff)) continue;
    
    ref.read(orderNotifierProvider(session.orderId!).notifier);
    
    if (session.peer != null) {
      ref.read(chatRoomsProvider(session.orderId!).notifier).subscribe();
    }
  }
});

First-Run Detection and User Experience

First-Run Detection System

The app implements a sophisticated first-run detection system that determines whether to show the walkthrough or proceed directly to the main app.

First-Run Detection Process:

// lib/features/walkthrough/providers/first_run_provider.dart:23-28
Future<bool> _checkIfFirstRun() async {
  final firstRunComplete = await _sharedPreferences.getBool(
    SharedPreferencesKeys.firstRunComplete.value,
  );
  return firstRunComplete != true; // Returns true if first run
}

Navigation Flow:

// lib/core/app_routes.dart:44-59
return firstRunState.when(
  data: (isFirstRun) {
    if (isFirstRun && state.matchedLocation != '/walkthrough') {
      return '/walkthrough'; // Redirect to walkthrough
    }
    return null; // Proceed to requested route
  },
  loading: () {
    return state.matchedLocation == '/walkthrough' ? null : '/walkthrough';
  },
  error: (_, __) => null,
);

First-Time User Experience

Complete First-Run Flow:

  1. App Launch: User opens app for the first time
  2. First-Run Detection: firstRunProvider detects no firstRunComplete flag
  3. Walkthrough Display: App redirects to /walkthrough route
  4. Key Generation: During appInitializerProvider, KeyManager.init() creates new mnemonic
  5. Walkthrough Completion: User completes or skips walkthrough
  6. Flag Setting: markFirstRunComplete() sets firstRunComplete = true
  7. Navigation: App redirects to home screen (/)
// lib/features/walkthrough/screens/walkthrough_screen.dart:167-172
Future<void> _onIntroEnd(BuildContext context) async {
  await ref.read(firstRunProvider.notifier).markFirstRunComplete();
  if (context.mounted) {
    context.go('/'); // Navigate to home
  }
}

Returning User Experience

Returning User Flow:

  1. App Launch: User opens app (not first time)
  2. First-Run Detection: firstRunProvider detects firstRunComplete = true
  3. Direct Navigation: App proceeds directly to requested route (usually /)
  4. Key Loading: During appInitializerProvider, KeyManager.init() loads existing keys
  5. Session Restoration: Active trading sessions are restored from storage
  6. Ready State: App is immediately ready for trading operations

Key Generation Timing

When Mnemonic is Created:

  • First-time users: Mnemonic is generated during KeyManager.init() in appInitializerProvider
  • Timing: Happens before the walkthrough is shown to the user
  • Storage: Immediately stored in Flutter Secure Storage
  • User awareness: User is not explicitly shown the mnemonic during first run

Mnemonic Access:

  • User can view: Through Settings → Key Management screen
  • User can regenerate: Through Key Management → Generate New Key
  • User can import: Through Key Management → Import Key
  • Backup responsibility: User must manually backup their mnemonic

Security Considerations

First-Run Security:

  • Automatic generation: No user input required for key generation
  • Secure storage: Uses Flutter Secure Storage (encrypted at rest)
  • No network transmission: Mnemonic never leaves the device
  • Immediate availability: Keys ready for use without additional setup

Key Recovery:

  • Mnemonic backup: User's responsibility to backup 12/24 word phrase
  • Import capability: Users can restore from existing mnemonic
  • Reset functionality: Users can generate new keys (clears all data)

Detailed Component Analysis

1. NostrService Initialization

Purpose: Establishes WebSocket connections to Nostr relays.

Process:

// lib/services/nostr_service.dart:35
await nostrService.init(settings);

What Happens:

  • Reads relay configuration from settings
  • Establishes WebSocket connections to configured relays
  • Sets up connection pool for Nostr protocol communication
  • Validates relay connectivity

Dependencies: Settings (relay configuration) Duration: ~200ms (network dependent)

2. KeyManager Initialization

Purpose: Initializes cryptographic key management system and handles mnemonic seed creation for first-time users.

Process:

// lib/features/key_manager/key_manager_provider.dart
await keyManager.init();

What Happens:

First-Time User Flow:

// lib/features/key_manager/key_manager.dart:16-23
Future<void> init() async {
  if (!await hasMasterKey()) {
    await generateAndStoreMasterKey(); // Creates new mnemonic seed
  } else {
    masterKeyPair = await _getMasterKey(); // Loads existing keys
    tradeKeyIndex = await getCurrentKeyIndex();
  }
}

For First-Time Users:

  1. Check for existing master key: hasMasterKey() returns false
  2. Generate new mnemonic: Uses BIP-39 to create 12/24 word seed phrase
  3. Derive master key: Converts mnemonic to BIP-32 extended private key
  4. Store securely: Saves both mnemonic and master key in Flutter Secure Storage
  5. Initialize key index: Sets current trade key index to 1
  6. Create key pair: Generates NostrKeyPairs for immediate use
// lib/features/key_manager/key_manager.dart:31-46
Future<void> generateAndStoreMasterKey() async {
  final mnemonic = _derivator.generateMnemonic(); // BIP-39 generation
  await generateAndStoreMasterKeyFromMnemonic(mnemonic);
}

Future<void> generateAndStoreMasterKeyFromMnemonic(String mnemonic) async {
  final masterKeyHex = _derivator.extendedKeyFromMnemonic(mnemonic);
  
  await _storage.clear(); // Clear any existing data
  await _storage.storeMnemonic(mnemonic); // Store in secure storage
  await _storage.storeMasterKey(masterKeyHex); // Store derived master key
  await setCurrentKeyIndex(1); // Initialize trade key index
  masterKeyPair = await _getMasterKey(); // Create NostrKeyPairs
  tradeKeyIndex = await getCurrentKeyIndex();
}

Returning User Flow:

For Returning Users:

  1. Check for existing master key: hasMasterKey() returns true
  2. Load master key: Retrieves stored master key from secure storage
  3. Load key index: Gets current trade key index from SharedPreferences
  4. Create key pair: Generates NostrKeyPairs from stored master key
  5. Ready for trading: System is immediately ready for trade operations
// lib/features/key_manager/key_manager.dart:54-61
Future<NostrKeyPairs> _getMasterKey() async {
  final masterKeyHex = await _storage.readMasterKey();
  if (masterKeyHex == null) {
    throw MasterKeyNotFoundException('No master key found in secure storage');
  }
  final privKey = _derivator.derivePrivateKey(masterKeyHex, 0);
  return NostrKeyPairs(private: privKey);
}

Key Storage Architecture:

  • Mnemonic: Stored in Flutter Secure Storage (encrypted at rest)
  • Master Key: Extended private key stored in Flutter Secure Storage
  • Key Index: Current trade key index stored in SharedPreferences
  • Trade Keys: Derived on-demand, never persisted

Dependencies: Secure storage access, BIP-39/BIP-32 libraries Duration: ~50ms (first-time), ~20ms (returning users)

3. SessionNotifier Initialization (CRITICAL)

Purpose: Loads all active trading sessions from local storage.

Process:

// lib/shared/notifiers/session_notifier.dart:32
await sessionManager.init();

What Happens:

Future<void> init() async {
  final allSessions = await _storage.getAllSessions();
  final cutoff = DateTime.now()
      .subtract(const Duration(hours: Config.sessionExpirationHours));
  
  for (final session in allSessions) {
    if (session.startTime.isAfter(cutoff)) {
      _sessions[session.orderId!] = session;
    } else {
      await _storage.deleteSession(session.orderId!);
      _sessions.remove(session.orderId!);
    }
  }
  
  state = sessions; // This triggers listeners
  _scheduleCleanup();
}

Critical Aspects:

  • Loads sessions from Sembast database
  • Filters expired sessions (older than 72 hours)
  • Updates state which triggers all listeners
  • Must complete before SubscriptionManager setup

Dependencies: Session storage (Sembast database) Duration: ~100ms (database I/O dependent)

4. SubscriptionManager Creation

Purpose: Manages Nostr event subscriptions based on active sessions.

Process:

// lib/features/subscriptions/subscription_manager.dart:32
SubscriptionManager(this.ref) {
  _initSessionListener();
  _initializeExistingSessions(); // CRITICAL: DO NOT REMOVE - Prevents stuck orders bug
}

void _initSessionListener() {
  _sessionListener = ref.listen<List<Session>>(
    sessionNotifierProvider,
    (previous, current) {
      _updateAllSubscriptions(current);
    },
    fireImmediately: false, // Wait for proper initialization
    onError: (error, stackTrace) {
      _logger.e('Error in session listener', error: error, stackTrace: stackTrace);
    },
  );
}

Manual Initialization for Existing Sessions

The SubscriptionManager includes manual initialization to prevent orders from getting stuck in previous states after app restart:

void _initializeExistingSessions() {
  try {
    final existingSessions = ref.read(sessionNotifierProvider);
    if (existingSessions.isNotEmpty) {
      _logger.i('Initializing subscriptions for ${existingSessions.length} existing sessions');
      _updateAllSubscriptions(existingSessions);
    }
  } catch (e, stackTrace) {
    _logger.e('Error initializing existing sessions', error: e, stackTrace: stackTrace);
  }
}

Why This Implementation is Required:

  • fireImmediately: false prevents automatic subscription creation for existing sessions on app startup
  • Without this implementation, orders remain stuck in previous states (like waiting-buyer-invoice) when they should show current states (like waiting-payment) after app restart
  • The implementation creates subscriptions for existing sessions manually during constructor execution
  • Protected by regression test: test/features/subscriptions/subscription_manager_initialization_test.dart

Why fireImmediately: false is Used:

  • Prevents the listener from executing before SessionNotifier.init() completes
  • Ensures subscriptions are created with valid session data
  • Avoids creating subscriptions with empty session lists
  • Maintains proper initialization order dependencies

Historical Context:

  • Commit 63dc124e set fireImmediately: false to fix relay switching issues where changing between Mostro instances would lose orders
  • This created a new bug where existing sessions wouldn't get subscriptions on app restart, causing orders to appear stuck in old states
  • Manual initialization pattern was implemented to solve both problems: preserve relay switching fix while ensuring existing sessions get proper subscriptions

How This Works:

Initialization Sequence with Manual Implementation:

1. SubscriptionManager constructor called
2. _initSessionListener() runs → Listener registered with fireImmediately: false
3. _initializeExistingSessions() runs → Creates subscriptions for loaded sessions immediately
4. SessionNotifier.init() runs and loads sessions (already handled by step 3)
5. Future session changes trigger listener normally
6. Both existing and new sessions handled correctly
7. UI shows orders in correct states after app restart

Dual-Path Architecture:

  • Listener Path: Handles future session changes (new trades, session deletions)
  • Manual Path: Handles existing sessions during app initialization
  • Result: Complete coverage of all session lifecycle scenarios

Dependencies: SessionNotifier state
Duration: ~10ms (manual initialization) + instantaneous (listener registration)

5. Background Services Setup

Purpose: Configures notification and background processing services.

Process:

ref.listen<Settings>(settingsProvider, (previous, next) {
  ref.read(backgroundServiceProvider).updateSettings(next);
});

What Happens:

  • Sets up listener for settings changes
  • Configures notification delivery
  • Initializes background sync processes

Dependencies: Settings provider Duration: ~10ms

6. Order Notifier Initialization

Purpose: Creates individual order management notifiers for each active session.

Process:

for (final session in sessionManager.sessions) {
  if(session.orderId == null || session.startTime.isBefore(cutoff)) continue;
  
  // Create order notifier for this session
  ref.read(orderNotifierProvider(session.orderId!).notifier);
  
  // Initialize chat if peer exists
  if (session.peer != null) {
    ref.read(chatRoomsProvider(session.orderId!).notifier).subscribe();
  }
}

What Happens:

  • Iterates through all loaded sessions
  • Creates OrderNotifier for each active order
  • Sets up chat room subscriptions for orders with assigned peers
  • Establishes timeout detection and reversal systems

Dependencies: SessionNotifier state, ChatRoomProvider Duration: ~50ms per session


Initialization Dependencies & Timing

Why Order Matters

The initialization sequence has critical timing dependencies that must be respected:

  • SessionNotifier must complete before SubscriptionManager: Event subscriptions require loaded session data to create proper filters
  • NostrService must initialize first: Other components depend on established relay connectivity
  • KeyManager initializes early: Required for session restoration and cryptographic operations

Common Timing Patterns

Dependent Provider Initialization

// Correct pattern for dependent initialization
_sessionListener = ref.listen<List<Session>>(
  sessionNotifierProvider,
  (previous, current) => _updateSubscriptions(current),
  fireImmediately: false, // Wait for proper initialization
);

Why this pattern matters: Using fireImmediately: true would cause the listener to execute immediately with potentially empty session data, before SessionNotifier.init() completes. This would result in subscriptions being created with incorrect filters, causing UI inconsistencies like missing orders in "My Trades" screen.

Sequential Async Initialization

// Proper async sequence in appInitializerProvider
await nostrService.init(settings);     // Must complete first
await keyManager.init();               // Can run after NostrService
await sessionManager.init();           // Requires KeyManager
ref.read(subscriptionManagerProvider); // Requires SessionNotifier

Subscription Creation Process

The SubscriptionManager follows a standardized pattern for creating event subscriptions:

void _updateAllSubscriptions(List<Session> sessions) {
  if (sessions.isEmpty) {
    _clearAllSubscriptions();
    return;
  }

  for (final type in SubscriptionType.values) {
    _updateSubscription(type, sessions);
  }
}

This pattern ensures subscriptions are only created when valid session data is available.

Filter Creation for Private Sessions

NostrFilter? _createFilterForType(SubscriptionType type, List<Session> sessions) {
  switch (type) {
    case SubscriptionType.orders:
      return NostrFilter(
        kinds: [1059], // Private gift-wrapped messages for active trading sessions
        p: sessions.map((s) => s.tradeKey.public).toList(), // Messages to my trade keys
      );
    case SubscriptionType.chat:
      return NostrFilter(
        kinds: [1059], // Private gift-wrapped chat messages
        p: sessions
            .where((s) => s.sharedKey?.public != null)
            .map((s) => s.sharedKey!.public)
            .toList(), // Messages to my shared keys
      );
    case SubscriptionType.relayList:
      return null; // Handled separately via subscribeToMostroRelayList()
  }
}

Dual-Channel Nostr Architecture

Overview

The Mostro Mobile app uses a dual-channel architecture with two completely separate subscription systems, each handling different types of Nostr events for different purposes:

  1. Private Channel (SubscriptionManager): Handles encrypted user sessions
  2. Public Channel (OpenOrdersRepository + OrderNotifier): Handles public order discovery

Channel 1: Private Sessions - SubscriptionManager

Purpose: Manages private encrypted communications for active trading sessions.

Events Handled:

// lib/features/subscriptions/subscription_manager.dart
case SubscriptionType.orders:
  return NostrFilter(
    kinds: [1059], // Private gift-wrapped messages
    p: sessions.map((s) => s.tradeKey.public).toList(), // To user's trade keys
  );

case SubscriptionType.chat:
  return NostrFilter(
    kinds: [1059], // Private gift-wrapped chat messages  
    p: sessions
        .where((s) => s.sharedKey?.public != null)
        .map((s) => s.sharedKey!.public)
        .toList(), // To user's shared keys
  );

// Relay synchronization
case SubscriptionType.relayList:
  return NostrFilter(
    kinds: [10002], // Relay list events
    authors: [mostroPublicKey], // From Mostro instance
  );

Responsibilities:

  • My Trades screen data
  • Private chat messages with trading partners
  • Session state management and updates
  • Relay synchronization from Mostro instances
  • Trade notifications and status changes

Data Flow:

Trading Partner → Kind 1059 (encrypted) → SubscriptionManager → MostroService → UI Updates

Channel 2: Public Orders - OpenOrdersRepository + OrderNotifier

Purpose: Handles public order discovery and timeout detection.

Events Handled:

// lib/data/repositories/open_orders_repository.dart  
final filter = NostrFilter(
  kinds: [38383], // Public Mostro order events
  since: filterTime, // Last 48 hours
  authors: [_settings.mostroPublicKey], // Only from Mostro instance
);

// lib/features/order/notifiers/order_notifier.dart
// Uses 38383 events for timeout detection by comparing public state vs local state

Responsibilities:

  • Order Book (home screen) - all available orders
  • Order discovery - finding orders to take
  • Timeout detection - comparing public events vs local session state
  • Cancellation detection - detecting when orders are canceled
  • Market data - public order information

Data Flow:

Mostro → Kind 38383 (public) → OpenOrdersRepository → Order Book UI
Mostro → Kind 38383 (public) → OrderNotifier → Timeout Detection

Architectural Separation: Dynamic vs Static Subscriptions

The real architectural principle behind this separation is subscription lifecycle management, not privacy levels.

Dynamic Subscriptions (SubscriptionManager)

Principle: Subscriptions that change based on user context

// These subscriptions RECONFIGURE when context changes
Kind 1059 (Orders): p: [myActiveTradeKeys]     // Updates with active sessions
Kind 10002 (RelayList): authors: [currentMostro]  // Updates with Mostro instance

Characteristics:

  • Context-dependent: Change when user sessions or settings change
  • Dynamic reconfiguration: Uses _updateAllSubscriptions() logic
  • Shared lifecycle: Both require same subscribe/unsubscribe patterns
  • State listeners: React to sessionNotifierProvider and settingsProvider

Why Kind 10002 is here: Relay lists need the same dynamic reconfiguration logic as session-based subscriptions, not because they're "private".

Static Subscriptions (OpenOrdersRepository)

Principle: Subscriptions that remain constant during app lifecycle

// This subscription is CONSTANT throughout app session
Kind 38383: authors: [mostroInstance] // Always same filter, independent of user context

Characteristics:

  • Context-independent: Not affected by user's trading sessions
  • Static configuration: Initialize once at startup
  • Global platform data: Market information for all users
  • Simple lifecycle: No dynamic updates needed

Separation by Functional Domain

Aspect SubscriptionManager (Dynamic) OpenOrdersRepository (Static)
Update Trigger Session/Settings changes App startup only
Reconfiguration Frequent, context-based None after initialization
State Dependency Depends on user context Independent of user state
Complexity High (dynamic management) Low (simple subscription)
Purpose "Events that change with user context" "Global platform data"

Why This Architecture Works

  1. State Management: Different update patterns require different architectures
  2. Performance: Static subscriptions avoid unnecessary reconnections
  3. Complexity Isolation: Dynamic logic separated from simple global subscriptions
  4. Maintainability: Clear separation of concerns by update frequency
  5. Shared Logic Reuse: Kind 1059 and 10002 share the same reconfiguration system

Legacy Benefits (Still Valid)

  • Security: Private events encrypted, public events accessible
  • Performance: Focused subscriptions reduce network overhead
  • Scalability: Independent scaling of personal vs market data
  • Fault Tolerance: Failure isolation between systems

Key Architectural Points

SubscriptionManager Does NOT Handle Kind 38383

// NEVER in SubscriptionManager - it only handles private events
❌ kinds: [38383] // This would be wrong - public events not handled here
✅ kinds: [1059]  // Correct - only private encrypted events

Public Events Handled Separately

// OpenOrdersRepository handles all 38383 events
✅ kinds: [38383] // Public order announcements from Mostro
✅ authors: [mostroPublicKey] // Only from configured Mostro instance
✅ since: filterTime // Recent orders only

Different Filtering Strategies

// Private events: Filter by recipient (who can decrypt)
p: [myTradeKeys] // Only messages I can decrypt

// Public events: Filter by author and time  
authors: [mostroPublicKey] // Only from Mostro
since: filterTime // Recent orders only

Related Systems

1. MostroService Integration

Purpose: Processes private encrypted messages (Kind 1059) from active trading sessions.

Integration:

// lib/services/mostro_service.dart:27
_ordersSubscription = ref.read(subscriptionManagerProvider).orders.listen(
  _onData,
  onError: (error, stackTrace) {
    _logger.e('Error in orders subscription', error: error, stackTrace: stackTrace);
  },
  cancelOnError: false,
);

What MostroService Actually Processes:

Future<void> _onData(NostrEvent event) async {
  // 1. Event deduplication
  if (await eventStore.hasItem(event.id!)) return;
  
  // 2. Find matching session by trade key
  final matchingSession = sessions.firstWhereOrNull(
    (s) => s.tradeKey.public == event.recipient,
  );
  
  // 3. Decrypt NIP-59 gift-wrapped message
  final decryptedEvent = await event.unWrap(privateKey);
  
  // 4. Parse Mostro protocol message
  final msg = MostroMessage.fromJson(result[0]);
  
  // 5. Store in local database
  await messageStorage.addMessage(decryptedEvent.id!, msg);
}

Integration Result: ✅ Proper Event Handling - MostroService receives private encrypted events (Kind 1059) from properly initialized subscriptions, NOT public events (Kind 38383).

2. Relay Synchronization System

Purpose: Automatically syncs relay lists from Mostro instances.

Integration:

// lib/features/relays/relays_notifier.dart:488
_subscriptionManager?.subscribeToMostroRelayList(mostroPubkey);

Integration Result: ✅ Independent Operation - Relay sync uses separate subscription methods and operates independently.

3. Public Order Systems (Separate from SubscriptionManager)

Purpose: Handle public order discovery and timeout detection using Kind 38383 events.

3.1 OpenOrdersRepository

// lib/data/repositories/open_orders_repository.dart
final filter = NostrFilter(
  kinds: [38383], // Public Mostro order events
  since: filterTime, // Last 48 hours
  authors: [_settings.mostroPublicKey], // Only from Mostro instance
);

Integration:

// lib/shared/providers/order_repository_provider.dart
final orderEventsProvider = StreamProvider<List<NostrEvent>>((ref) {
  final orderRepository = ref.read(orderRepositoryProvider);
  return orderRepository.eventsStream; // Streams public 38383 events
});

Powers: Order Book (home screen), market discovery, order taking

3.2 OrderNotifier Timeout Detection

// lib/features/order/notifiers/order_notifier.dart
// Compares public 38383 events vs local session state to detect:
// - Order timeouts (waitingPayment → pending)
// - Order cancellations (active → canceled)
// - State synchronization between public announcements and private sessions

Integration Result: ✅ Independent Operation - Public order systems operate independently of SubscriptionManager initialization.

4. Chat System

Purpose: Handles encrypted peer-to-peer messaging via Kind 1059 events.

Integration: Uses SubscriptionManager chat stream for private messages between trading partners.

Integration Result: ✅ Proper Initialization - Chat messages are properly initialized with session data.


Testing the Initialization Process

Cold Start Testing

Testing the complete app initialization from a clean state:

# Clean app state and dependencies
flutter clean
flutter pub get
dart run build_runner build -d

# Test cold start
flutter run --release

Key verification points:

  • All components initialize without errors
  • UI is fully responsive after initialization
  • No race conditions or timing issues occur

Dependency Verification

Verifying proper component initialization order:

// Monitor initialization sequence in logs
NostrService → KeyManager → SessionNotifier → SubscriptionManager → Background Services

Log patterns to verify:

  • NostrService initialized successfully with X relays
  • KeyManager: Master keys loaded from secure storage
  • SessionNotifier: Loaded X sessions from storage
  • Subscription created for SubscriptionType.orders with X sessions

Component Integration Testing

Verify that systems integrate correctly:

  • Session Restoration: Active sessions load properly after app restart
  • Subscription Setup: Event subscriptions match loaded sessions
  • Background Services: Notifications and sync services activate correctly
  • Relay Connectivity: All configured relays establish connections

Performance Characteristics

⚠️ Note: These are rough estimates based on typical Flutter operations. Actual performance varies significantly based on network conditions, device performance, UI complexity, and relay responsiveness.

Current Implementation Performance:

  • Single subscription creation: ~10-50ms (network dependent)
  • No recreation needed: Eliminates secondary overhead
  • Total: ~10-50ms + no UI flickering

Architectural Benefits:

  • Eliminates UI flickering during initialization
  • Reduces initialization complexity
  • Prevents race conditions between components
  • Maintains proper dependency order

The exact performance characteristics vary by environment, but the architectural approach ensures consistent behavior across different conditions.


Development Guidelines

Extending the Initialization Process

When adding new components to the app initialization sequence:

  1. Identify Dependencies: Determine which existing systems your component requires
  2. Placement in Sequence: Add initialization calls in the correct order within appInitializerProvider
  3. Async Patterns: Use await for components that other systems depend on
  4. Error Handling: Implement proper error handling and recovery mechanisms
// Example: Adding a new component
final appInitializerProvider = FutureProvider<void>((ref) async {
  // ... existing initialization ...
  
  // Add new component after its dependencies
  final newComponent = ref.read(newComponentProvider);
  await newComponent.init(); // If other systems depend on this
  
  // Or without await if independent
  ref.read(independentComponentProvider);
});

Provider Dependencies Best Practices

  • Listen Pattern: Use fireImmediately: false when depending on other providers
  • Explicit Dependencies: Clearly document what each provider requires
  • State Validation: Check that dependencies are initialized before using them
  • Error Propagation: Handle dependency initialization failures gracefully

Architecture Evolution

Scaling Considerations

  • Monitor initialization time as components are added
  • Consider parallel initialization for independent systems
  • Implement lazy loading for non-critical components
  • Use dependency injection patterns to manage complexity

Performance Optimization

  • Profile initialization performance regularly
  • Identify bottlenecks in the startup sequence
  • Consider background initialization for heavy operations
  • Implement progressive enhancement patterns

Monitoring and Observability

  • Maintain comprehensive logging throughout initialization
  • Add performance metrics for each initialization phase
  • Implement health checks for critical components
  • Use tracing to understand initialization flow in production

Conclusion

The Mostro Mobile app initialization process represents a sophisticated bootstrap sequence that ensures all critical systems are properly configured before user interaction begins. This comprehensive initialization flow demonstrates several key architectural principles:

Core Design Principles

Dependency Management

  • Sequential Initialization: Critical components initialize in dependency order
  • Proper Timing: fireImmediately: false patterns prevent race conditions
  • Error Handling: Graceful failure handling at each initialization stage

Dual-Channel Architecture

  • Dynamic Subscriptions: SubscriptionManager handles context-dependent events (Kind 1059, 10002)
  • Static Subscriptions: OpenOrdersRepository manages global platform data (Kind 38383)
  • Separation of Concerns: Clear boundaries between private trading data and public market information

Extensible Design

  • Clear Patterns: Well-defined approaches for adding new components
  • Provider Integration: Consistent use of Riverpod for dependency injection
  • Scalable Structure: Architecture supports growth without major refactoring

Key Takeaways for Developers

  1. Initialization Order Matters: Carefully consider dependencies when adding new components
  2. Timing Patterns: Use appropriate fireImmediately settings based on dependency requirements
  3. Architecture Separation: Maintain clear boundaries between different data channels and responsibilities
  4. Documentation Value: Technical documentation must accurately reflect implementation details

This initialization system provides a robust foundation for the app's trading operations while maintaining clear architectural boundaries and extensible patterns for future development.


Last Updated: September 17, 2025