Feat: Add multi-Mostro node data model and trusted nodes registry (P1)#436
Conversation
…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.
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
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. Comment |
There was a problem hiding this comment.
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
mostroPubKeyuses a hardcoded string identical to the one intrustedMostroNodes. Dartconstcontext 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.dartbut the coding guidelines specify organizing providers underfeatures/{feature}/providers/. Similarly, the notifier and model would go under their respective subdirectories. Consider moving to:
lib/features/mostro/providers/mostro_nodes_provider.dartlib/features/mostro/notifiers/mostro_nodes_notifier.dartlib/features/mostro/models/mostro_node.dartThis 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 fieldsfinalfor a true value-object.
name,picture,website, andaboutare non-final, yet the class already provideswithMetadata()for immutable-style updates, and the notifier always replaces nodes in state viawithMetadata(). Making these fieldsfinalwould 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;
…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
There was a problem hiding this comment.
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:withMetadatacannot clear a field back tonull— consider whether a test should document that.Because
withMetadatausesname: name ?? this.name, there's no way for a caller to explicitly reset a field tonull. If that's intentional, a brief test documenting the behavior (e.g., "callingwithMetadata(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 fieldsfinalfor a fully immutable value object.
name,picture,website, andaboutare non-final, yet the class provideswithMetadatafor creating updated copies and theMostroNodesNotifieralways 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:_saveCustomNodesrethrows — callers propagate unhandled errors.
addCustomNode,removeCustomNode, andupdateCustomNodeNameallawait _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 returnFuture<bool>/Future<void>with no documentation that they can throw on I/O failure. Consider either:
- Catching in the public methods, logging, and returning
false/ failing gracefully, or- Documenting the throwing behavior so callers know to handle it.
65-81:selectNodedoesn't handle errors fromupdateMostroInstance.If
updateMostroInstancethrows (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.
trustedNodesandcustomNodescreate a new filtered list on every call. Methods likeaddCustomNode(line 96-98) andremoveCustomNode(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.
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.
There was a problem hiding this comment.
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 undernotifiers/subfolder per project conventions.The coding guidelines require the feature-based folder structure
features/{feature}/{screens|providers|notifiers|widgets}/. This notifier should be atlib/features/mostro/notifiers/mostro_nodes_notifier.dart.As per coding guidelines:
lib/features/**/*.dart: Use feature-based folder structure with organization asfeatures/{feature}/{screens|providers|notifiers|widgets}/
135-148:updateCustomNodeNamesilently no-ops for trusted nodes.If called with a trusted node's pubkey,
customNodeswon't include it, so the map produces no change — yet_saveCustomNodesand a full state rebuild still run. Consider either guarding early (likeremoveCustomNodedoes) 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) {
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.
There was a problem hiding this comment.
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.
Correct total from 22 to 37 tests and notifier count from 18 to 23, matching actual test files (14 + 23 = 37).
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
Tests
Documentation