Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 181 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- Run `dart run build_runner build -d` after installing dependencies or updating localization files
- This generates files needed by `flutter_intl` and other code generators

### Essential Commands for Code Changes
- **`flutter analyze`** - ✅ **ALWAYS run after any code change** - Mandatory before commits
- **`flutter test`** - ✅ **ALWAYS run after any code change** - Mandatory before commits
- **`dart run build_runner build -d`** - 🟡 **Only when code generation needed** (models, providers, mocks, localization)
- **`flutter test integration_test/`** - 🟡 **Only for significant changes** (core services, main flows)

## Architecture Overview

### State Management: Riverpod
Expand Down Expand Up @@ -48,6 +54,60 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- Provider pattern for dependency injection
- FSM pattern for order lifecycle management

### Relay Management System
- **Automatic Sync**: Real-time synchronization with Mostro instance relay lists via kind 10002 events
- **Manual Addition**: Users can add custom relays with strict validation (wss://, domains only, connectivity required)
- **Instance Validation**: Author pubkey checking prevents relay contamination between Mostro instances
- **Two-tier Testing**: Nostr protocol + WebSocket connectivity validation
- **Memory Safety**: Isolated test instances protect main app connectivity during validation
- **Dual Storage Strategy**: Mostro/default relays stored in `settings.relays`, user relays stored in `settings.userRelays` with full metadata preservation
- **Source Tracking**: Relays tagged by source (user, mostro, default) for appropriate handling and storage strategy
- **Smart Re-enablement**: Manual relay addition automatically removes from blacklist, Mostro relay re-activation removes from blacklist during sync
- **URL Normalization**: All relay URLs normalized by removing trailing slashes to ensure consistent matching
- **Implementation**: Located in `features/relays/` with core logic in `RelaysNotifier`

#### Manual Relay Addition
- Users can manually add relays via `addRelayWithSmartValidation()` method
- Five sequential validations: URL normalization, duplicate check, domain validation, connectivity testing, blacklist management
- Security requirements: Only wss:// protocol, domain-only (no IP addresses), mandatory connectivity test
- Smart URL handling: Auto-adds "wss://" prefix if missing
- Source tracking: Manual relays marked as `RelaySource.user`
- Blacklist override: Manual addition automatically removes relay from blacklist

#### Dual Storage Architecture
- **Active Storage**: `settings.relays` contains active relay list used by NostrService
- **Metadata Storage**: `settings.userRelays` preserves complete JSON metadata for user relays
- **Lifecycle Management**: `removeRelayWithBlacklist()` adds Mostro/default relays to blacklist, `removeRelay()` permanently deletes user relays
- **Storage Synchronization**: `_saveRelays()` method synchronizes both storage locations

#### Instance Isolation
- Author pubkey validation prevents relay contamination between different Mostro instances
- Subscription cleanup on instance switching via `unsubscribeFromMostroRelayList()`
- State cleanup removes old Mostro relays when switching instances via `_cleanMostroRelaysFromState()`

#### Relay Validation System
- Two-tier connectivity testing: Primary Nostr protocol test (REQ/EVENT/EOSE), WebSocket fallback
- Domain-only policy: IP addresses completely rejected
- URL normalization: Trailing slash removal prevents duplicate entries
- Instance-isolated testing: Test connections don't affect main app connectivity

## App Initialization Process

### Initialization Sequence
The app follows a specific initialization order in `appInitializerProvider`:

1. **NostrService Initialization**: Establishes WebSocket connections to configured relays
2. **KeyManager Initialization**: Loads cryptographic keys from secure storage
3. **SessionNotifier Initialization**: Loads active trading sessions from Sembast database
4. **SubscriptionManager Creation**: Registers session listeners with `fireImmediately: false`
5. **Background Services Setup**: Configures notification and sync services
6. **Order Notifier Initialization**: Creates individual order managers for active sessions

### Critical Timing Requirements
- SessionNotifier must complete initialization before SubscriptionManager setup
- SubscriptionManager uses `fireImmediately: false` to prevent premature execution
- Proper sequence ensures orders appear consistently in UI across app restarts

## Timeout Detection & Reversal System

### Overview
Expand Down Expand Up @@ -96,11 +156,9 @@ When orders are canceled (status changes to `canceled` in public events):

### Mock Files Guidelines
- **Generated file**: `test/mocks.mocks.dart` is auto-generated by Mockito
- **File-level ignores**: Already contains `// ignore_for_file: must_be_immutable` at top
- **DO NOT add**: Individual `// ignore: must_be_immutable` comments to classes
- **Common issue**: Adding individual ignores causes `duplicate_ignore` analyzer warnings
- **MockSharedPreferencesAsync**: Specifically covered by existing file-level ignore
- **Regeneration**: Use `dart run build_runner build -d` to update mocks
- **File-level ignores**: Contains comprehensive ignore directives at file level
- **Regeneration**: Use `dart run build_runner build -d` to update mocks after changes
- **No manual editing**: Never manually modify generated mock files

## Development Guidelines

Expand All @@ -116,6 +174,12 @@ When orders are canceled (status changes to `canceled` in public events):
- Use `S.of(context).yourKey` for all user-facing strings
- Refer to existing features (order, chat, auth) for implementation patterns

### Code Comments and Documentation
- **All code comments must be in English** - No Spanish, Italian, or other languages
- Use clear, concise English for variable names, function names, and comments
- Documentation and technical explanations should be in English
- User-facing strings use localization system (`S.of(context).keyName`)

### Key Services and Components
- **MostroService** - Core business logic and Mostro protocol handling
- **NostrService** - Nostr protocol connectivity
Expand Down Expand Up @@ -147,6 +211,93 @@ When orders are canceled (status changes to `canceled` in public events):
- **Implementation**: Custom `timeAgoWithLocale()` method in NostrEvent extension
- **Usage**: Automatically uses app's current locale for "hace X horas" vs "hours ago"

## Relay Synchronization System

### Overview
Comprehensive system that automatically synchronizes the app's relay list with the configured Mostro instance while providing users full control through an intelligent blacklist mechanism.

### Core Components

#### **RelayListEvent Model** (`lib/core/models/relay_list_event.dart`)
- Parses NIP-65 (kind 10002) events from Mostro instances
- Validates relay URLs (WebSocket only)
- Robust handling of different timestamp formats
- Null-safe parsing for malformed events

#### **Enhanced Relay Model** (`lib/features/relays/relay.dart`)
```dart
enum RelaySource {
user, // Manually added by user
mostro, // Auto-discovered from Mostro
defaultConfig, // App default relay
}

class Relay {
final RelaySource source;
bool get canDelete; // User relays only
bool get canBlacklist; // Mostro/default relays
}
```

#### **Settings with Blacklist** (`lib/features/settings/settings.dart`)
- New `blacklistedRelays: List<String>` field
- Backward-compatible serialization
- Automatic migration for existing users

#### **RelaysNotifier** (`lib/features/relays/relays_notifier.dart`)
- **`syncWithMostroInstance()`**: Manual sync trigger
- **`removeRelayWithBlacklist(String url)`**: Smart removal with blacklisting
- **`addRelayWithSmartValidation(...)`**: Auto-removes from blacklist when user manually adds
- **`_handleMostroRelayListUpdate()`**: Filters blacklisted relays during sync

### Synchronization Flow

#### **Real-time Sync**
1. **App Launch**: Automatic subscription to kind 10002 events from configured Mostro
2. **Event Reception**: Parse relay list and filter against blacklist
3. **State Update**: Merge new relays while preserving user relays
4. **NostrService**: Automatic reconnection to updated relay list

#### **Blacklist System**
```
User removes Mostro relay → Added to blacklist → Never re-added during sync
User manually adds relay → Removed from blacklist → Works as user relay
```

### Key Features

#### **User Experience**
- **Transparent Operation**: Sync happens automatically in background
- **Full User Control**: Can permanently block problematic Mostro relays
- **Reversible Decisions**: Manual addition re-enables previously blocked relays
- **Preserved Preferences**: User relays always maintained across syncs

#### **Technical Robustness**
- **Real-time Updates**: WebSocket subscriptions for instant sync
- **Error Resilience**: Graceful fallbacks and comprehensive error handling
- **Race Protection**: Prevents concurrent sync operations
- **Logging**: Detailed logging for debugging and monitoring

#### **API Methods**
```dart
// SettingsNotifier blacklist management
Future<void> addToBlacklist(String relayUrl);
Future<void> removeFromBlacklist(String relayUrl);
bool isRelayBlacklisted(String relayUrl);

// RelaysNotifier smart operations
Future<void> removeRelayWithBlacklist(String url);
Future<void> clearBlacklistAndResync();
```

### Implementation Notes
- **Subscription Management**: Uses `SubscriptionManager` with dedicated relay list stream
- **State Persistence**: Blacklist automatically saved to SharedPreferences
- **Backward Compatibility**: Existing relay configurations preserved and migrated
- **Testing**: Comprehensive unit tests in `test/features/relays/` (currently disabled due to complex mocking requirements)

For complete technical documentation, see `RELAY_SYNC_IMPLEMENTATION.md`.

## Code Quality Standards

### Flutter Analyze
Expand Down Expand Up @@ -186,7 +337,7 @@ When orders are canceled (status changes to `canceled` in public events):
- **Card-Based Settings**: Clean, organized settings interface with visual hierarchy
- **Enhanced Account Screen**: Streamlined user profile and preferences
- **Currency Integration**: Visual currency flags for international trading
- **Relay Management**: Localized relay dialog strings and improved UX
- **Relay Management**: Enhanced relay synchronization with URL normalization and settings persistence mechanisms

#### 3. Code Quality Excellence
- **Zero Analyzer Issues**: Resolved 54+ Flutter analyze issues, maintaining clean codebase
Expand All @@ -202,6 +353,15 @@ When orders are canceled (status changes to `canceled` in public events):
- **Background Services**: Reliable notification processing
- **Mock File Management**: Comprehensive documentation to prevent generated file issues

#### 5. Relay Management System Architecture
- **Dual Storage Implementation**: Mostro/default relays persist in `settings.relays` and use blacklist for deactivation, user relays persist in `settings.userRelays` with complete JSON metadata via `toJson()`/`fromJson()`
- **Differentiated Lifecycle Management**: `removeRelayWithBlacklist()` adds Mostro/default relays to blacklist for potential restoration, `removeRelay()` permanently deletes user relays from both state and storage
- **Storage Synchronization**: `_saveRelays()` method saves all active relays to `settings.relays` while separately preserving user relay metadata in `settings.userRelays`
- **URL Normalization Process**: Relay URLs undergo normalization by trimming whitespace and removing trailing slashes using `_normalizeRelayUrl()` method throughout blacklist operations in `_handleMostroRelayListUpdate()`
- **Settings Persistence Mechanism**: The Settings `copyWith()` method uses null-aware operators (`??`) to preserve existing values for selectedLanguage and defaultLightningAddress when not explicitly overridden
- **Relay Validation Protocol**: Connectivity testing follows a two-tier approach: primary Nostr protocol test (sends REQ, waits for EVENT/EOSE) via `_testNostrProtocol()`, fallback WebSocket test via `_testBasicWebSocketConnectivity()`
- **Blacklist Matching Logic**: All blacklist operations normalize both stored blacklist URLs and incoming relay URLs to ensure consistent string matching regardless of format variations

### Recent File Modifications

#### Core Infrastructure
Expand All @@ -217,7 +377,7 @@ When orders are canceled (status changes to `canceled` in public events):
#### UI Components
- **`lib/shared/widgets/bottom_nav_bar.dart`**: Enhanced navigation with notification badges
- **`lib/features/home/screens/home_screen.dart`**: Modern order book interface
- **`lib/features/relays/widgets/relay_selector.dart`**: Localized relay management
- **`lib/features/relays/widgets/relay_selector.dart`**: Relay management interface with comprehensive validation protocol and localization support
- **Settings screens**: Card-based layout with improved accessibility

#### Notification System
Expand Down Expand Up @@ -279,6 +439,15 @@ When orders are canceled (status changes to `canceled` in public events):
- `lib/l10n/` - Internationalization files
- `test/` - Unit and integration tests

### Relay System Files
- `lib/core/models/relay_list_event.dart` - NIP-65 event parser for kind 10002
- `lib/features/relays/relay.dart` - Enhanced relay model with source tracking
- `lib/features/relays/relays_notifier.dart` - Core relay management and sync logic
- `lib/features/relays/relays_provider.dart` - Riverpod provider configuration
- `lib/features/settings/settings.dart` - Settings model with blacklist support
- `lib/features/subscriptions/subscription_manager.dart` - Extended with relay list subscriptions
- `RELAY_SYNC_IMPLEMENTATION.md` - Complete technical documentation

### Generated Files (Don't Edit Manually)
- `lib/generated/` - Generated localization files
- `*.g.dart` - Generated Riverpod and other code
Expand Down Expand Up @@ -315,10 +484,10 @@ When orders are canceled (status changes to `canceled` in public events):

---

**Last Updated**: 2025-07-20
**Last Updated**: 2025-08-22
**Flutter Version**: Latest stable
**Dart Version**: Latest stable
**Key Dependencies**: Riverpod, GoRouter, flutter_intl, timeago, dart_nostr
**Key Dependencies**: Riverpod, GoRouter, flutter_intl, timeago, dart_nostr, logger, shared_preferences

## Current Project Status

Expand All @@ -337,10 +506,12 @@ When orders are canceled (status changes to `canceled` in public events):
- ⚡ **Lightning**: Seamless Lightning Network integration
- 🌐 **Multi-Platform**: Android and iOS native performance
- 📱 **Real-Time**: Live updates via Nostr protocol
- 🔗 **Smart Relay Management**: Automatic sync with blacklist control

### Recent Achievements
- **UI Modernization**: Complete settings and account screen redesign
- **Icon Enhancement**: Improved app launcher and notification visibility
- **Localization Excellence**: 73+ new translation keys across 3 languages
- **Code Quality**: Zero analyzer issues with modern Flutter standards
- **Documentation**: Comprehensive NOSTR.md and updated README.md
- **Documentation**: Comprehensive NOSTR.md and updated README.md
- **Relay System Architecture**: URL normalization using trailing slash removal, Settings persistence with null-aware operators, two-tier validation protocol (Nostr + WebSocket), and comprehensive multilingual support
77 changes: 77 additions & 0 deletions lib/core/models/relay_list_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'package:dart_nostr/dart_nostr.dart';

/// Represents a NIP-65 relay list event (kind 10002) from a Mostro instance.
/// These events contain the list of relays where the Mostro instance publishes its events.
class RelayListEvent {
final List<String> relays;
final DateTime publishedAt;
final String authorPubkey;

const RelayListEvent({
required this.relays,
required this.publishedAt,
required this.authorPubkey,
});

/// Parses a kind 10002 Nostr event into a RelayListEvent.
/// Returns null if the event is not a valid kind 10002 event.
static RelayListEvent? fromEvent(NostrEvent event) {
if (event.kind != 10002) return null;

// Extract relay URLs from 'r' tags
final relays = event.tags
?.where((tag) => tag.isNotEmpty && tag[0] == 'r')
.where((tag) => tag.length >= 2)
.map((tag) => tag[1])
.where((url) => url.isNotEmpty)
.toList() ?? <String>[];

// Handle different possible types for createdAt
DateTime publishedAt;
if (event.createdAt is DateTime) {
publishedAt = event.createdAt as DateTime;
} else if (event.createdAt is int) {
publishedAt = DateTime.fromMillisecondsSinceEpoch((event.createdAt as int) * 1000);
} else {
publishedAt = DateTime.now(); // Fallback to current time
}

return RelayListEvent(
relays: relays,
publishedAt: publishedAt,
authorPubkey: event.pubkey,
);
}

/// Validates that all relay URLs are properly formatted WebSocket URLs
/// Also normalizes URLs by removing trailing slashes to prevent duplicates
List<String> get validRelays {
return relays
.where((url) => url.startsWith('wss://') || url.startsWith('ws://'))
.map((url) => url.trim())
.map((url) => url.endsWith('/') ? url.substring(0, url.length - 1) : url)
.toList();
}

@override
String toString() {
return 'RelayListEvent(relays: $relays, publishedAt: $publishedAt, author: $authorPubkey)';
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! RelayListEvent) return false;
final a = relays.toSet();
final b = other.relays.toSet();
return other.authorPubkey == authorPubkey &&
a.length == b.length &&
a.containsAll(b);
}

@override
int get hashCode => Object.hash(
authorPubkey,
Object.hashAllUnordered(relays.toSet()),
);
}
Loading