From 18839c1bed2dcd7e84c7ec2ffd9d291617606199 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:55:13 -0600 Subject: [PATCH 1/2] feat: finalize transport v2 migration (Phase C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove global Config.mostroVersion; message version now derived from the transport (toJson defaults to 1, wrapNip44 passes 2) - Extract testable buildOrdersFilter() from the subscription manager with unit tests (v1 kind 1059 / v2 kind 14 authors-pinned) - About screen shows the node's protocol_version with an explanatory dialog (l10n en/es/it/de/fr) - Docs: mark the v1 -> v2 migration complete (status, §4.2, §4.3, Phase C/D) - Tidy: import order in subscription_manager; drop duplicate publishEvent stub in mostro_service_test --- docs/architecture/TRANSPORT_V2_MIGRATION.md | 76 +++++++++++-------- lib/core/config.dart | 3 - lib/data/models/mostro_message.dart | 7 +- lib/features/settings/about_screen.dart | 8 ++ .../subscriptions/subscription_manager.dart | 52 +++++++++---- lib/l10n/intl_de.arb | 2 + lib/l10n/intl_en.arb | 2 + lib/l10n/intl_es.arb | 2 + lib/l10n/intl_fr.arb | 2 + lib/l10n/intl_it.arb | 2 + .../subscriptions/orders_filter_test.dart | 35 +++++++++ test/services/mostro_service_test.dart | 11 +-- 12 files changed, 141 insertions(+), 61 deletions(-) create mode 100644 test/features/subscriptions/orders_filter_test.dart diff --git a/docs/architecture/TRANSPORT_V2_MIGRATION.md b/docs/architecture/TRANSPORT_V2_MIGRATION.md index 6ff7988ba..2dc40eb62 100644 --- a/docs/architecture/TRANSPORT_V2_MIGRATION.md +++ b/docs/architecture/TRANSPORT_V2_MIGRATION.md @@ -18,10 +18,12 @@ during the migration window. > - **Reference client (CLI)**: `MostroP2P/mostro-cli` PRs #176, #177, #178 and > its `docs/TRANSPORT_V2_SPEC.md`. > -> **Status.** Living design specification. **Phase A (dual receive)** — -> including `protocol_version` auto-detection and per-node transport resolution -> on the receive path — is merged to `main`. **Phase B (dual send)** is -> implemented in this branch. Phases C–D (§5) are pending. +> **Status.** **Migration complete.** All phases (§5) are implemented and +> merged: dual receive (A), dual send (B), per-node `protocol_version` +> auto-detection and transport wiring (folded into A/B), the `version`-field +> cleanup (C), and the test suite (D). The app speaks both transports and +> selects per node; the v1 gift-wrap path is unchanged and the v2 NIP-44 path +> engages automatically against a node advertising `protocol_version=2`. --- @@ -233,16 +235,14 @@ this is the version-skew guard. The downgrade must be **logged explicitly** (at `warn`) so a misconfigured or unreachable node cannot silently leave the app in a degraded transport without anyone noticing. -### 4.2 `version` field stops being a global constant +### 4.2 `version` field is derived from the transport -`Config.mostroVersion` is currently a hardcoded `1` -(`lib/core/config.dart:56`) read by `MostroMessage.toJson()` -(`lib/data/models/mostro_message.dart:30`). The message `version` must instead -be **derived from the resolved `Transport`** (1 for `giftWrap`, 2 for `nip44` — -decision #2). The cleanest seam is to pass the `Transport` (not a raw int) into -the serialize/wrap path rather than reading a global, so the version number is -always a function of the transport and a single `MostroMessage` can be wrapped -for either transport without the two drifting apart. +The global `Config.mostroVersion` constant has been **removed**. The message +`version` is now a function of the wire transport: `MostroMessage.toJson({int? +version})` defaults to `1` (gift wrap — used by storage, logging and the v1 send +path), and `wrapNip44` passes `version: 2` explicitly. The same serialized +message JSON is reused for the tuple, the trade signature and the identity-proof +payload, so element 0, element 1 and element 2 can never drift apart. ### 4.3 Touch points (current v1 code → where v2 plugs in) @@ -258,7 +258,7 @@ for either transport without the two drifting apart. | Background receive decrypt (unWrap → `result[0]`) | `lib/features/notifications/services/background_notification_service.dart:199-322` | | NIP-59 unwrap | `lib/shared/utils/nostr_utils.dart:383-443` | | Node info / `protocol_version` parse | `lib/features/mostro/mostro_instance.dart:119-251` | -| Global `version` constant | `lib/core/config.dart:56` | +| `version` derived from transport (no global constant) | `lib/data/models/mostro_message.dart` (`toJson`) | --- @@ -328,25 +328,35 @@ unchanged. expiring before processing; adding a generous window later is a safe, optional hygiene improvement. -### Phase C — Send-side wiring - -> `protocol_version` parsing and per-node transport resolution already landed in -> Phase A (receive). This phase only threads that same resolved transport into -> the **send** path. - -- Thread the resolved `Transport` (§4.1) into `MostroService.publishOrder` so - outbound messages use the v2 `wrap` from Phase B against a v2 node, while v1 - nodes keep the gift-wrap path. -- Reuse the version-skew guard from Phase A: degrade to v1 on an unsupported / - unreachable node, keeping the existing explicit downgrade logging. - -### Phase D — Tests - -- v2 `wrap` → `unwrap` round-trip: produces a kind-14 event authored by the - trade key; decodes back to the same `MostroMessage`. -- `protocol_version` tag parse → transport mapping (including absent → v1). -- Identity proof: present in normal mode, `null` in full-privacy; domain string - is exactly `mostro-transport-v2-identity::`. +### Phase C — Wiring & finalization ✅ + +> Send-side transport wiring landed with Phase B: every outbound Mostro send +> already routes through `MostroMessage.wrapForTransport(protocolVersion: …)`, +> and receive-side detection landed with Phase A. This phase finalized the +> remaining loose ends. + +- Removed the global `Config.mostroVersion`; the message `version` is derived + from the transport (§4.2). +- Extracted the transport → orders-filter mapping into the testable top-level + `buildOrdersFilter` (`subscription_manager.dart`). +- The version-skew guard (`resolveTransport`) degrades to v1 on an unsupported + protocol version and logs it (§4.1). + +### Phase D — Tests ✅ + +- v2 `wrap` → `unwrap` round-trip, reputation and full-privacy + (`test/data/mostro_message_nip44_test.dart`): produces a kind-14 event + authored by the trade key and decodes back to the same message; cryptographically + verifies the trade signature and the identity proof, including the exact domain + string `mostro-transport-v2-identity::` (`null` in + full-privacy). +- `protocol_version` tag parse → transport mapping, including absent → v1 + (`test/features/mostro/transport_test.dart`, + `test/features/mostro/mostro_instance_test.dart`). +- Orders subscription filter is transport-aware + (`test/features/subscriptions/orders_filter_test.dart`). +- Restore decode handles both transports and yields identical results + (`test/features/restore/restore_decode_test.dart`). - Regression: the v1 gift-wrap path is byte-for-byte unchanged (existing serialization tests stay green). diff --git a/lib/core/config.dart b/lib/core/config.dart index c655ff360..a6b4d9d4c 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -52,9 +52,6 @@ class Config { // Debug mode static bool get isDebug => !kReleaseMode; - // Mostro version - static int mostroVersion = 1; - // Key derivation configuration static const String keyDerivationPath = "m/44'/1237'/38383'/0"; diff --git a/lib/data/models/mostro_message.dart b/lib/data/models/mostro_message.dart index 2fa09bf79..7d8a93232 100644 --- a/lib/data/models/mostro_message.dart +++ b/lib/data/models/mostro_message.dart @@ -3,7 +3,6 @@ import 'package:convert/convert.dart'; import 'package:crypto/crypto.dart'; import 'package:dart_nostr/nostr/core/key_pairs.dart'; import 'package:dart_nostr/nostr/model/event/event.dart'; -import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/data/models/enums/action.dart'; import 'package:mostro_mobile/data/models/payload.dart'; import 'package:mostro_mobile/features/mostro/transport.dart'; @@ -28,7 +27,11 @@ class MostroMessage { Map toJson({int? version}) { Map json = { - 'version': version ?? Config.mostroVersion, + // The message version is derived from the wire transport: 1 for gift wrap + // (the default here, used by storage/logging and the v1 send path) and 2 + // for NIP-44 direct (passed explicitly by `wrapNip44`). See §4.2 of + // docs/architecture/TRANSPORT_V2_MIGRATION.md. + 'version': version ?? 1, 'request_id': requestId, 'trade_index': tradeIndex, }; diff --git a/lib/features/settings/about_screen.dart b/lib/features/settings/about_screen.dart index ce9acc4c3..df5ff5cab 100644 --- a/lib/features/settings/about_screen.dart +++ b/lib/features/settings/about_screen.dart @@ -335,6 +335,14 @@ class AboutScreen extends ConsumerWidget { ), const SizedBox(height: 16), + _buildInfoRowWithDialog( + context, + S.of(context)!.protocolVersion, + instance.protocolVersion.toString(), + S.of(context)!.protocolVersionExplanation, + ), + const SizedBox(height: 16), + _buildInfoRowWithDialog( context, S.of(context)!.mostroCommitId, diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index 47bf5b658..24103b685 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:dart_nostr/dart_nostr.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/core/models/relay_list_event.dart'; @@ -186,22 +187,11 @@ class SubscriptionManager { // and re-subscribe when the node info arrives after this subscription. final transport = _resolveOrdersTransport(); _appliedOrdersTransport = transport; - switch (transport) { - case Transport.giftWrap: - return NostrFilter( - kinds: [1059], - p: tradeKeys, - ); - case Transport.nip44: - // v2 Mostro replies are kind 14 authored by the node and addressed - // (p) to the trade key; the authors pin disambiguates them from - // NIP-17 peer chat, which is also kind 14 (§3.4). - return NostrFilter( - kinds: [14], - authors: [ref.read(settingsProvider).mostroPublicKey], - p: tradeKeys, - ); - } + return buildOrdersFilter( + transport, + tradeKeys, + ref.read(settingsProvider).mostroPublicKey, + ); case SubscriptionType.chat: if (sessions.isEmpty) { return null; @@ -423,3 +413,33 @@ class SubscriptionManager { _relayListController.close(); } } + +/// Builds the orders subscription filter for the resolved [transport] (§4.1). +/// +/// - v1 ([Transport.giftWrap]): kind 1059 addressed (`p`) to the trade keys. +/// - v2 ([Transport.nip44]): kind 14 authored by the node ([mostroPubkey]) and +/// addressed to the trade keys — the `authors` pin disambiguates Mostro v2 +/// replies from NIP-17 peer chat, which is also kind 14 (§3.4). +/// +/// Top-level so the transport → filter mapping can be unit-tested without the +/// full [SubscriptionManager] / Riverpod orchestration. +@visibleForTesting +NostrFilter buildOrdersFilter( + Transport transport, + List tradeKeys, + String mostroPubkey, +) { + switch (transport) { + case Transport.giftWrap: + return NostrFilter( + kinds: [1059], + p: tradeKeys, + ); + case Transport.nip44: + return NostrFilter( + kinds: [14], + authors: [mostroPubkey], + p: tradeKeys, + ); + } +} diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 6c7689d9c..bd91d46f3 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -975,6 +975,8 @@ "read": "Lesen", "technicalDetails": "Technische Details", "mostroDaemonVersion": "Mostro-Version", + "protocolVersion": "Protokollversion", + "protocolVersionExplanation": "Das Transportprotokoll dieses Knotens: 1 = NIP-59 Gift Wrap, 2 = NIP-44 Direktnachrichten. Die App erkennt dies automatisch und verwendet den passenden Transport.", "mostroCommitId": "Mostro Commit-ID", "orderExpiration": "Order-Ablaufzeit", "holdInvoiceExpiration": "Hold-Invoice Ablaufzeit", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 13a58550b..e68dd7f64 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -975,6 +975,8 @@ "read": "Read", "technicalDetails": "Technical Details", "mostroDaemonVersion": "Mostro Version", + "protocolVersion": "Protocol Version", + "protocolVersionExplanation": "The wire transport protocol this node speaks: 1 = NIP-59 gift wrap, 2 = NIP-44 direct messages. The app detects this automatically and uses the matching transport.", "mostroCommitId": "Mostro Commit ID", "orderExpiration": "Order Expiration", "holdInvoiceExpiration": "Hold Invoice Expiration", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 4eb118eb7..5d77e12c1 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -952,6 +952,8 @@ "read": "Leer", "technicalDetails": "Detalles Técnicos", "mostroDaemonVersion": "Versión de Mostro", + "protocolVersion": "Versión del Protocolo", + "protocolVersionExplanation": "El protocolo de transporte que habla este nodo: 1 = gift wrap NIP-59, 2 = mensajes directos NIP-44. La app lo detecta automáticamente y usa el transporte correspondiente.", "mostroCommitId": "ID de Commit Mostro", "orderExpiration": "Expiración de Orden", "holdInvoiceExpiration": "Expiración de Factura de Retención", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 21cafb184..161f07b9a 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -975,6 +975,8 @@ "read": "Lire", "technicalDetails": "Détails techniques", "mostroDaemonVersion": "Version de Mostro", + "protocolVersion": "Version du Protocole", + "protocolVersionExplanation": "Le protocole de transport utilisé par ce nœud : 1 = gift wrap NIP-59, 2 = messages directs NIP-44. L'application le détecte automatiquement et utilise le transport correspondant.", "mostroCommitId": "ID de commit Mostro", "orderExpiration": "Expiration de commande", "holdInvoiceExpiration": "Expiration hold invoice", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 46e809a1d..03aed6d68 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1014,6 +1014,8 @@ "read": "Leggi", "technicalDetails": "Dettagli Tecnici", "mostroDaemonVersion": "Versione di Mostro", + "protocolVersion": "Versione del Protocollo", + "protocolVersionExplanation": "Il protocollo di trasporto utilizzato da questo nodo: 1 = gift wrap NIP-59, 2 = messaggi diretti NIP-44. L'app lo rileva automaticamente e usa il trasporto corrispondente.", "mostroCommitId": "ID Commit Mostro", "orderExpiration": "Scadenza Ordine", "holdInvoiceExpiration": "Scadenza Fattura di Blocco", diff --git a/test/features/subscriptions/orders_filter_test.dart b/test/features/subscriptions/orders_filter_test.dart new file mode 100644 index 000000000..3d715eac4 --- /dev/null +++ b/test/features/subscriptions/orders_filter_test.dart @@ -0,0 +1,35 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mostro_mobile/features/mostro/transport.dart'; +import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; + +void main() { + group('buildOrdersFilter (transport-aware orders subscription)', () { + final mostroPubkey = 'a' * 64; + final tradeKeys = ['b' * 64, 'c' * 64]; + + test('v1 (giftWrap) → kind 1059, no authors pin', () { + final filter = buildOrdersFilter( + Transport.giftWrap, + tradeKeys, + mostroPubkey, + ); + + expect(filter.kinds, [1059]); + expect(filter.p, tradeKeys); + expect(filter.authors, isNull); + }); + + test('v2 (nip44) → kind 14 authored by the node, addressed to trade keys', + () { + final filter = buildOrdersFilter( + Transport.nip44, + tradeKeys, + mostroPubkey, + ); + + expect(filter.kinds, [14]); + expect(filter.authors, [mostroPubkey]); + expect(filter.p, tradeKeys); + }); + }); +} diff --git a/test/services/mostro_service_test.dart b/test/services/mostro_service_test.dart index 251b9c11b..6a72045f2 100644 --- a/test/services/mostro_service_test.dart +++ b/test/services/mostro_service_test.dart @@ -216,7 +216,7 @@ void main() { final messageContent = { 'order': { - 'version': Config.mostroVersion, + 'version': 1, 'id': orderId, 'action': 'take-sell', 'payload': { @@ -277,7 +277,7 @@ void main() { userPubKey: userPubKey, messageContent: { 'order': { - 'version': Config.mostroVersion, + 'version': 1, 'id': orderId, 'action': 'take-sell', 'payload': { @@ -337,7 +337,7 @@ void main() { userPubKey: userPubKey, messageContent: { 'order': { - 'version': Config.mostroVersion, + 'version': 1, 'id': orderId, 'action': 'take-sell', 'payload': { @@ -385,9 +385,6 @@ void main() { when(mockNostrService.publishEvent(any)) .thenAnswer((_) async => Future.value()); - when(mockNostrService.publishEvent(any)) - .thenAnswer((_) async => Future.value()); - // Act await mostroService.takeSellOrder(orderId, 400, 'lnbc121314invoice'); @@ -395,7 +392,7 @@ void main() { // Simulate server-side verification final messageContent = { 'order': { - 'version': Config.mostroVersion, + 'version': 1, 'id': orderId, 'action': 'take-sell', 'payload': { From 9e6faa728bd4e71092f8c6900117606bc19114ce Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Wed, 1 Jul 2026 14:37:36 -0600 Subject: [PATCH 2/2] coderabbit suggestion --- docs/architecture/TRANSPORT_V2_MIGRATION.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/architecture/TRANSPORT_V2_MIGRATION.md b/docs/architecture/TRANSPORT_V2_MIGRATION.md index 2dc40eb62..c92154c94 100644 --- a/docs/architecture/TRANSPORT_V2_MIGRATION.md +++ b/docs/architecture/TRANSPORT_V2_MIGRATION.md @@ -264,9 +264,9 @@ payload, so element 0, element 1 and element 2 can never drift apart. ## 5. Implementation phases -The phases below define the code work for **subsequent branches**; this -document does not execute them. Each phase keeps the v1 path behaviourally -unchanged. +The phases below record the code work delivered across the migration branches +(all now merged — see the status above). Each phase kept the v1 path +behaviourally unchanged. ### Phase A — Dual receive (with receive-side auto-detection)