Skip to content

Feat: Add multi-Mostro node data model and trusted nodes registry (P1)#436

Merged
grunch merged 7 commits into
mainfrom
multi-mostro-support
Feb 8, 2026
Merged

Feat: Add multi-Mostro node data model and trusted nodes registry (P1)#436
grunch merged 7 commits into
mainfrom
multi-mostro-support

Conversation

@grunch

@grunch grunch commented Feb 7, 2026

Copy link
Copy Markdown
Member

Phase 1

Introduce MostroNode model, MostroNodesNotifier, and provider for managing multiple Mostro instances. Includes trusted nodes config, custom node persistence via SharedPreferences, backward-compatible auto-import of unrecognized pubkeys, and phased implementation documentation.

Summary by CodeRabbit

  • New Features

    • Multi-node support: trusted + custom Mostro nodes, node selector, add/remove custom-node flows, metadata (name, picture, website, about), and resilient persistence with validation and backward-compatibility handling
    • Default trusted node entry and app initialization to load/manage nodes
  • Tests

    • Comprehensive unit tests for node model and notifier behaviors, persistence, selection, edge cases, and error tolerance
  • Documentation

    • Detailed Multi-Mostro architecture, data flow, implementation phases, and usage guide

…ase 1)

Introduce MostroNode model, MostroNodesNotifier, and provider for managing
multiple Mostro instances. Includes trusted nodes config, custom node
persistence via SharedPreferences, backward-compatible auto-import of
unrecognized pubkeys, and phased implementation documentation.
@coderabbitai

coderabbitai Bot commented Feb 7, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds Multi‑Mostro support: new MostroNode value object, MostroNodesNotifier + provider, trusted node registry and custom-node persistence/storage key, config default change, app-init wiring, documentation, and unit tests for model and notifier behavior.

Changes

Cohort / File(s) Summary
Documentation
docs/MULTI_MOSTRO_SUPPORT.md
New design and implementation doc covering architecture, data flow, storage mappings, implementation phases, backward-compat rules, and UI integration points.
Configuration & Storage
lib/core/config.dart, lib/data/models/enums/storage_keys.dart
Introduced _defaultMostroPubKey and trustedMostroNodes; switched mostroPubKey default to the new constant; added mostro_custom_nodes SharedPreferences key.
Core Data Model
lib/features/mostro/mostro_node.dart
Added MostroNode value object with pubkey, optional metadata (name, picture, website, about), isTrusted, addedAt, JSON (de)serialization, display helpers, equality, and toString.
State Management
lib/features/mostro/mostro_nodes_notifier.dart, lib/features/mostro/mostro_nodes_provider.dart
Added MostroNodesNotifier (StateNotifier<List>) with init(), selection, add/remove/update custom nodes, validation, backward-compat auto-import, persistence helpers, and mostroNodesProvider.
App Init Wiring
lib/shared/providers/app_init_provider.dart
Calls mostroNodesProvider.notifier.init() during application initialization sequence.
Tests
test/features/mostro/mostro_node_test.dart, test/features/mostro/mostro_nodes_notifier_test.dart
Comprehensive unit tests for MostroNode and MostroNodesNotifier: serialization, display helpers, selection, persistence, add/remove/update, auto-import, and corrupt prefs handling.

Sequence Diagram(s)

sequenceDiagram
    participant AppInit as App Init
    participant Config as Config
    participant SharedPrefs as SharedPreferences
    participant Notifier as MostroNodesNotifier
    participant Settings as SettingsProvider
    rect rgba(200,230,250,0.5)
    AppInit->>Notifier: init()
    Notifier->>Config: read trustedMostroNodes
    Notifier->>SharedPrefs: load `mostro_custom_nodes`
    Notifier->>Notifier: merge trusted + custom nodes
    Notifier->>Settings: read mostroPublicKey
    alt active pubkey unknown and non-empty
        Notifier->>SharedPrefs: persist auto-imported custom node
    end
    Notifier-->>AppInit: init complete
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • chebizarro
  • BraCR10

Poem

🐰
I nibbled through prefs and config bright,
Gathered trusted friends beneath the light,
Notifier hums as nodes hop into line,
Metadata sparkles — each pubkey will shine,
A rabbit cheers — the network feels fine! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: adding a multi-Mostro node data model and trusted nodes registry, which is the primary focus of this Phase 1 implementation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch multi-mostro-support

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
lib/features/mostro/mostro_nodes_notifier.dart (1)

194-209: One corrupt entry in storage discards all custom nodes.

If any single element in the JSON array fails to parse (e.g., missing pubkey), the catch block returns [], silently dropping all valid custom nodes. Consider parsing entries individually and skipping invalid ones.

🛡️ Suggested resilient parsing
     try {
       final json = await _prefs.getString(
         SharedPreferencesKeys.mostroCustomNodes.value,
       );
       if (json == null) return [];

       final list = jsonDecode(json) as List<dynamic>;
-      return list
-          .map((e) => MostroNode.fromJson(e as Map<String, dynamic>))
-          .toList();
+      final nodes = <MostroNode>[];
+      for (final e in list) {
+        try {
+          nodes.add(MostroNode.fromJson(e as Map<String, dynamic>));
+        } catch (e) {
+          logger.w('Skipping corrupt custom node entry: $e');
+        }
+      }
+      return nodes;
     } catch (e) {
       logger.e('Failed to load custom nodes: $e');
       return [];
     }

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@docs/MULTI_MOSTRO_SUPPORT.md`:
- Around line 20-35: The fenced ASCII diagram block is missing a language
identifier which triggers markdownlint MD040; update the opening fence to
include a plain-text language specifier (e.g., change the triple backticks to
"```text") for the diagram that begins with "Config.trustedMostroNodes" so the
block containing Config.trustedMostroNodes, MostroNodesNotifier,
SettingsNotifier.updateMostroInstance, Settings.mostroPublicKey, RelaysNotifier,
NostrService and SubscriptionManager is explicitly marked as text.

In `@lib/features/mostro/mostro_node.dart`:
- Around line 22-23: The getter truncatedPubkey crashes for short values of
pubkey because substring(pubkey.length - 5) will throw when pubkey.length < 10;
update truncatedPubkey to defensively handle short/invalid pubkey strings by
checking pubkey.length (or using Dart's characters package) and returning either
the full pubkey when it's too short or a safe truncated form (e.g., pubkey when
length <= 10, otherwise the current head...tail logic) while referencing the
truncatedPubkey getter and the pubkey field.

In `@lib/features/mostro/mostro_nodes_notifier.dart`:
- Around line 137-156: The updateNodeMetadata method updates node metadata but
does not persist changes for custom nodes, so metadata will be lost on restart;
modify updateNodeMetadata (the method shown) to persist custom-node changes the
same way updateCustomNodeName does by calling _saveCustomNodes(...) after
mutating state (only when the node is identified as custom, e.g., using the same
custom check used in updateCustomNodeName or the node's isCustom flag), or add a
TODO/comment if persistence isn't desired yet.
- Around line 163-170: Remove the duplicate private getter _customNodes and use
the existing public getter customNodes everywhere: delete the _customNodes
declaration and replace all references to _customNodes with customNodes (e.g.,
in methods that currently call _customNodes), ensuring the code continues to
create a new List from state.where((n) => !n.isTrusted).toList() via the single
customNodes getter.
🧹 Nitpick comments (3)
lib/core/config.dart (1)

12-26: Pubkey is duplicated, not derived — risk of silent divergence.

The comment on Line 21 says "derived from first trusted node," but mostroPubKey uses a hardcoded string identical to the one in trustedMostroNodes. Dart const context prevents runtime derivation from the list. If someone updates one location and forgets the other, the values silently diverge.

Consider adding a comment explicitly noting this coupling, or extracting the hex string into a single private constant referenced by both.

Suggested approach
+  // Canonical pubkey for the primary Mostro node — used in both
+  // trustedMostroNodes and as the default for mostroPubKey.
+  static const String _defaultMostroPubKey =
+      '82fa8cb978b43c79b2156585bac2c011176a21d2aead6d9f7c575c005be88390';
+
   // Trusted Mostro nodes registry
   static const List<Map<String, String>> trustedMostroNodes = [
     {
-      'pubkey':
-          '82fa8cb978b43c79b2156585bac2c011176a21d2aead6d9f7c575c005be88390',
+      'pubkey': _defaultMostroPubKey,
       'name': 'Mostro P2P',
     },
   ];

-  // Mostro hexkey (backward compatible, derived from first trusted node)
+  // Mostro hexkey (backward compatible)
   static const String mostroPubKey = String.fromEnvironment(
     'MOSTRO_PUB_KEY',
-    defaultValue:
-        '82fa8cb978b43c79b2156585bac2c011176a21d2aead6d9f7c575c005be88390',
+    defaultValue: _defaultMostroPubKey,
   );
lib/features/mostro/mostro_nodes_provider.dart (1)

1-10: File placement doesn't match feature folder convention.

The provider file is at lib/features/mostro/mostro_nodes_provider.dart but the coding guidelines specify organizing providers under features/{feature}/providers/. Similarly, the notifier and model would go under their respective subdirectories. Consider moving to:

  • lib/features/mostro/providers/mostro_nodes_provider.dart
  • lib/features/mostro/notifiers/mostro_nodes_notifier.dart
  • lib/features/mostro/models/mostro_node.dart

This can be deferred but worth tracking. As per coding guidelines: "Use feature-based folder structure with organization as features/{feature}/{screens|providers|notifiers|widgets}/".

lib/features/mostro/mostro_node.dart (1)

1-8: Consider making metadata fields final for a true value-object.

name, picture, website, and about are non-final, yet the class already provides withMetadata() for immutable-style updates, and the notifier always replaces nodes in state via withMetadata(). Making these fields final would enforce the immutable pattern consistently and prevent accidental in-place mutation.

♻️ Suggested diff
 class MostroNode {
   final String pubkey;
-  String? name;
-  String? picture;
-  String? website;
-  String? about;
+  final String? name;
+  final String? picture;
+  final String? website;
+  final String? about;
   final bool isTrusted;
   final DateTime? addedAt;

Comment thread docs/MULTI_MOSTRO_SUPPORT.md
Comment thread lib/features/mostro/mostro_node.dart Outdated
Comment thread lib/features/mostro/mostro_nodes_notifier.dart
Comment thread lib/features/mostro/mostro_nodes_notifier.dart Outdated
…re-state

- Deduplicate default pubkey via shared constant in Config
- Guard truncatedPubkey against short strings (RangeError)
- Validate 64-char hex format before auto-importing unrecognized pubkeys
- Persist to SharedPreferences before updating in-memory state
- Add error handling to _saveCustomNodes for consistency with load
- Remove duplicate _customNodes getter
- Update docs: relay management per node, phase reordering, naming clarity

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/features/mostro/mostro_nodes_notifier.dart`:
- Around line 84-101: addCustomNode currently accepts and persists any pubkey
string; add validation similar to init by calling the existing _isValidHexPubkey
before creating MostroNode. If validation fails, log a warning (use logger.w)
and return false without saving; otherwise proceed with creating newNode,
updating _saveCustomNodes, and updating state. Update references in
addCustomNode and ensure behavior matches init’s validation flow (use
_isValidHexPubkey, logger.w, and same return semantics).
🧹 Nitpick comments (5)
test/features/mostro/mostro_node_test.dart (1)

93-105: withMetadata cannot clear a field back to null — consider whether a test should document that.

Because withMetadata uses name: name ?? this.name, there's no way for a caller to explicitly reset a field to null. If that's intentional, a brief test documenting the behavior (e.g., "calling withMetadata(name: null) preserves the existing name") would make the contract explicit. If clearing is needed in Phase 2 for kind 0 metadata, the signature will need a sentinel or a separate method.

lib/features/mostro/mostro_node.dart (1)

1-8: Consider making metadata fields final for a fully immutable value object.

name, picture, website, and about are non-final, yet the class provides withMetadata for creating updated copies and the MostroNodesNotifier always replaces nodes in state (never mutating in place). Mutable fields invite accidental direct mutation that would bypass Riverpod state notifications.

♻️ Suggested change
 class MostroNode {
   final String pubkey;
-  String? name;
-  String? picture;
-  String? website;
-  String? about;
+  final String? name;
+  final String? picture;
+  final String? website;
+  final String? about;
   final bool isTrusted;
   final DateTime? addedAt;
lib/features/mostro/mostro_nodes_notifier.dart (3)

200-211: _saveCustomNodes rethrows — callers propagate unhandled errors.

addCustomNode, removeCustomNode, and updateCustomNodeName all await _saveCustomNodes(...) without try-catch. If persistence fails, the rethrown exception bubbles up to the caller (likely UI code). This is arguably the correct design (persist-before-state ensures consistency), but the public method signatures return Future<bool> / Future<void> with no documentation that they can throw on I/O failure. Consider either:

  1. Catching in the public methods, logging, and returning false / failing gracefully, or
  2. Documenting the throwing behavior so callers know to handle it.

65-81: selectNode doesn't handle errors from updateMostroInstance.

If updateMostroInstance throws (e.g., SharedPreferences failure), the error propagates unhandled. A try-catch with logging would keep this consistent with the defensive pattern used elsewhere.

🛡️ Suggested fix
   Future<void> selectNode(String pubkey) async {
     MostroNode? node;
     for (final n in state) {
       if (n.pubkey == pubkey) {
         node = n;
         break;
       }
     }
     if (node == null) {
       logger.w('Cannot select unknown node: $pubkey');
       return;
     }
 
-    await _ref.read(settingsProvider.notifier).updateMostroInstance(pubkey);
-    logger.i('Selected Mostro node: ${node.displayName}');
+    try {
+      await _ref.read(settingsProvider.notifier).updateMostroInstance(pubkey);
+      logger.i('Selected Mostro node: ${node.displayName}');
+    } catch (e) {
+      logger.e('Failed to select node ${node.displayName}: $e');
+      rethrow;
+    }
   }

171-175: Getter allocations on every access — acceptable but worth noting.

trustedNodes and customNodes create a new filtered list on every call. Methods like addCustomNode (line 96-98) and removeCustomNode (line 123-125) call these getters and then immediately spread them into a new state list. With small node counts this is fine, but if this pattern is reused in hot paths later, consider caching.

Comment thread lib/features/mostro/mostro_nodes_notifier.dart
addCustomNode now validates pubkeys via _isValidHexPubkey before
persisting, matching the validation already used in init() for
auto-imported nodes. This prevents storing entries with invalid
pubkey formats.

Update existing tests to use valid 64-char hex pubkeys and add a
dedicated test for invalid pubkey rejection.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/features/mostro/mostro_nodes_notifier.dart`:
- Around line 188-216: The error handling for persistence is inconsistent:
_loadCustomNodes swallows errors and returns an empty list while
_saveCustomNodes logs and rethrows, causing callers like addCustomNode,
removeCustomNode, and updateCustomNodeName to potentially throw on write
failures but init to silently degrade on load failure; fix by making behavior
consistent — either make _saveCustomNodes catch/log and not rethrow (so callers
don't unexpectedly throw) or make _loadCustomNodes rethrow after logging (so
failures surface); locate the methods _loadCustomNodes and _saveCustomNodes and
apply the chosen symmetric strategy across them and update callers
(addCustomNode/removeCustomNode/updateCustomNodeName) to handle or propagate
errors accordingly.
🧹 Nitpick comments (2)
lib/features/mostro/mostro_nodes_notifier.dart (2)

1-14: File should live under notifiers/ subfolder per project conventions.

The coding guidelines require the feature-based folder structure features/{feature}/{screens|providers|notifiers|widgets}/. This notifier should be at lib/features/mostro/notifiers/mostro_nodes_notifier.dart.

As per coding guidelines: lib/features/**/*.dart: Use feature-based folder structure with organization as features/{feature}/{screens|providers|notifiers|widgets}/


135-148: updateCustomNodeName silently no-ops for trusted nodes.

If called with a trusted node's pubkey, customNodes won't include it, so the map produces no change — yet _saveCustomNodes and a full state rebuild still run. Consider either guarding early (like removeCustomNode does) or documenting this is by-design.

🛡️ Optional early guard
  Future<void> updateCustomNodeName(String pubkey, String newName) async {
+   if (!customNodes.any((n) => n.pubkey == pubkey)) {
+     logger.w('Cannot update name: $pubkey is not a custom node');
+     return;
+   }
    final updatedCustom = customNodes.map((n) {

Comment thread lib/features/mostro/mostro_nodes_notifier.dart
Change _saveCustomNodes to return bool instead of rethrowing on failure,
consistent with how _loadCustomNodes already handles errors. Callers now
check save success before updating in-memory state, preventing divergence
between memory and disk while avoiding unexpected crashes on save failures.
Add MostroNodesNotifier API section with full method signatures, document
the resilient error handling strategy (persist-before-state pattern),
pubkey validation, and test coverage summary for Phase 1.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@docs/MULTI_MOSTRO_SUPPORT.md`:
- Around line 74-76: The "Test Coverage" total is wrong: the breakdown lists 14
tests in mostro_node_test.dart and 18 in mostro_nodes_notifier_test.dart (total
32) but the header says "22 tests"; update the header under "**Test Coverage**"
(the line showing "22 tests") to the correct total "32 tests" or adjust the
individual counts so they sum to the stated total, ensuring consistency with the
entries for mostro_node_test.dart and mostro_nodes_notifier_test.dart.

In `@lib/features/mostro/mostro_nodes_notifier.dart`:
- Around line 138-152: The method updateCustomNodeName currently maps and saves
even when the given pubkey isn't present in customNodes (or is actually a
trusted node), causing it to return true incorrectly; modify
updateCustomNodeName to first check existence by using customNodes.any((n) =>
n.pubkey == pubkey) (and additionally ensure the pubkey is not present in
trustedNodes if applicable), return false immediately if no matching custom node
is found, and only then perform the mapping, call
_saveCustomNodes(updatedCustom) and update state; keep references to
updateCustomNodeName, customNodes, trustedNodes, _saveCustomNodes, and state
when making this change.

Comment thread docs/MULTI_MOSTRO_SUPPORT.md Outdated
Comment thread lib/features/mostro/mostro_nodes_notifier.dart
Correct total from 22 to 37 tests and notifier count from 18 to 23,
matching actual test files (14 + 23 = 37).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant