Skip to content

docs: add Core Services specs (MostroService, ExchangeService, EncryptionService)#35

Merged
grunch merged 2 commits intomainfrom
docs/core-services
Mar 25, 2026
Merged

docs: add Core Services specs (MostroService, ExchangeService, EncryptionService)#35
grunch merged 2 commits intomainfrom
docs/core-services

Conversation

@mostronatorcoder
Copy link

@mostronatorcoder mostronatorcoder bot commented Mar 25, 2026

Comprehensive documentation for Core Services layer (point 14).

New Specs

MOSTRO_SERVICE.md

  • Message reception pipeline (event deduplication, session matching, gift wrap decryption)
  • Restore payload filtering (RestoreData, LastTradeIndexResponse, OrdersResponse)
  • Child order linking for range orders
  • Message publishing with PoW
  • Integration points (SubscriptionManager, SessionNotifier, NostrService)
  • Security considerations (NIP-59 privacy, PoW anti-spam)

EXCHANGE_SERVICE.md

  • Abstract base class contract (getExchangeRate, getCurrencyCodes)
  • HTTP utilities (timeout, error handling, validation)
  • YadioExchangeService implementation details
  • Riverpod integration (ExchangeRateNotifier)
  • Error scenarios and resolution strategies
  • Extension point for fallback providers
  • Performance considerations (caching recommendations)

ENCRYPTION_SERVICE.md

  • ChaCha20-Poly1305 AEAD implementation
  • EncryptionResult model with blob serialization
  • Secure random generation (Fortuna CSPRNG)
  • Security properties (confidentiality, authenticity, integrity)
  • Nonce uniqueness rules
  • Usage examples (encrypted image uploads)
  • Limitations and future enhancements

Updated Files

README.md

  • Added MOSTRO_SERVICE.md with migration note
  • Added EXCHANGE_SERVICE.md
  • Added ENCRYPTION_SERVICE.md with Rust migration recommendation

All specs include comprehensive cross-references to related documentation.

Summary by CodeRabbit

Documentation

  • Added comprehensive service documentation for the encryption infrastructure protecting user data in storage and secure messaging.
  • Added service documentation for the exchange rate service providing Bitcoin to fiat currency conversion capabilities.
  • Added service documentation for the Mostro trading protocol communication layer handling order routing and session management.

…tionService)

New specs:

**MOSTRO_SERVICE.md**
- Central Mostro protocol communication service
- Gift wrap event handling (NIP-59 decryption)
- Message reception pipeline (deduplication, session matching, restore filtering)
- Child order linking for range orders
- Message publishing with PoW
- Integration with SubscriptionManager, SessionNotifier, OrderNotifier

**EXCHANGE_SERVICE.md**
- Abstract base class for exchange rate providers
- HTTP utilities (timeout, error handling, validation)
- YadioExchangeService implementation
- Riverpod integration (ExchangeRateNotifier)
- Currency code discovery
- Performance considerations (caching, rate limiting)

**ENCRYPTION_SERVICE.md**
- ChaCha20-Poly1305 AEAD encryption
- Secure random generation (Fortuna CSPRNG)
- Blob serialization (nonce + ciphertext + auth tag)
- EncryptionResult model
- Convenience methods (encryptToBlob, decryptFromBlob)
- Security properties and nonce uniqueness rules
- Usage examples for encrypted image uploads

Updated README.md with all three specs and migration notes.
@grunch
Copy link
Member

grunch commented Mar 25, 2026

@coderabbitai review

2 similar comments
@grunch
Copy link
Member

grunch commented Mar 25, 2026

@coderabbitai review

@grunch
Copy link
Member

grunch commented Mar 25, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31ccdd85-d917-453b-bf48-08c2edc8798d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Added comprehensive reference documentation for three core services: EncryptionService (ChaCha20-Poly1305 AEAD encryption), ExchangeService (exchange-rate abstraction with Riverpod integration), and MostroService (Mostro protocol communication layer). Updated the v1-reference index accordingly.

Changes

Cohort / File(s) Summary
New Service Documentation
``.specify/v1-reference/ENCRYPTION_SERVICE.md, .specify/v1-reference/EXCHANGE_SERVICE.md`, `.specify/v1-reference/MOSTRO_SERVICE.md`
Three new specification documents detailing service designs, component interactions, data models, error handling, and security considerations for encryption, exchange rates, and Mostro communication respectively.
Reference Index
.specify/v1-reference/README.md
Added three new entries to the "Platform & Infrastructure" documentation index with cross-references and implementation migration notes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Three specifications tucked in neat,
Encryption, exchange, and services complete,
ChaCha20 whispers, rates dance high,
Mostro messages encrypted and spry,
Documentation blooming, architecture clean!

🚥 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 summarizes the main change: adding comprehensive documentation specs for three core services (MostroService, ExchangeService, EncryptionService).
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
  • Commit unit tests in branch docs/core-services

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
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
.specify/v1-reference/README.md (1)

77-79: Consider making the bridge version explicit in these migration notes.

Since these rows are now key migration pointers, adding “flutter_rust_bridge 2.x” in the V2 phrasing would reduce ambiguity during implementation planning.

Based on learnings: Use flutter_rust_bridge 2.x for Rust-Flutter interop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.specify/v1-reference/README.md around lines 77 - 79, Update the three
migration-note rows (MOSTRO_SERVICE.md, EXCHANGE_SERVICE.md,
ENCRYPTION_SERVICE.md) to explicitly mention the bridge version by appending
"flutter_rust_bridge 2.x" in the V2 phrasing for each entry; specifically edit
the V2 migration text for MOSTRO_SERVICE.md, EXCHANGE_SERVICE.md, and
ENCRYPTION_SERVICE.md so they read something like "Move core logic to Rust
(flutter_rust_bridge 2.x; Flutter handles UI integration)", "Keep in Flutter
(HTTP API integration; use flutter_rust_bridge 2.x where applicable)" and "Move
to Rust (crypto should be platform-agnostic; flutter_rust_bridge 2.x)" to remove
ambiguity during implementation planning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.specify/v1-reference/ENCRYPTION_SERVICE.md:
- Around line 39-41: Update the EncryptionResult.fromBlob spec to explicitly
validate and parse the blob: require blob.length >= 28 bytes (12-byte nonce + at
least 0-byte ciphertext + 16-byte authTag) and throw a clear error if shorter;
extract nonce as the first 12 bytes, authTag as the last 16 bytes, and
ciphertext as the bytes in between, then construct the EncryptionResult from
those three components; also state exact error behavior for malformed input
(e.g., raise/return a parse/validation error).

In @.specify/v1-reference/EXCHANGE_SERVICE.md:
- Around line 184-190: The UI example shows "1 BTC = ... USD" but
exchangeRateProvider('USD') (which uses getExchangeRate(currency, 'BTC'))
returns a USD→BTC rate, so the direction is reversed; fix by making the example
consistent: either call exchangeRateProvider('BTC') to obtain a BTC→USD rate
when displaying "1 BTC = ... USD", or keep exchangeRateProvider('USD') and
change the text to "1 USD = ${value.toStringAsFixed(2)} BTC"; update the snippet
where exchangeRateProvider('USD') is referenced to use the chosen correction
(exchangeRateProvider('BTC') or adjusted display string) so the provider and
displayed direction match.
- Around line 72-79: The getRequest() error handling currently re-wraps
HttpException in the generic catch block, losing its specificity; add an
explicit on HttpException { rethrow; } clause before the final generic catch to
let HttpException propagate unchanged (retain existing TimeoutException and
FormatException handlers and the thrown HttpException instances for non-200
responses).

In @.specify/v1-reference/MOSTRO_SERVICE.md:
- Around line 98-109: In _isRestorePayload, make the payload null- and
type-safe: first ensure payload is non-null and a Map (e.g., check payload !=
null && payload is Map) before calling containsKey; when testing 'orders' verify
payload['orders'] is a List and isNotEmpty before accessing index 0, and guard
the buyer_trade_pubkey check with a null-safe lookup (e.g.,
payload['orders'][0]?['buyer_trade_pubkey'] != null); also ensure the initial
payload assignment from json['restore']['payload'] ?? json['order']['payload']
handles missing keys safely (avoid direct indexing on null). Ensure all
containsKey and list-index checks use these guards in _isRestorePayload.
- Around line 168-178: publishOrder() currently assumes
sessions.firstWhereOrNull(...) returns a non-null session and directly accesses
session.tradeKey.private when calling MostroMessage.wrap, which can crash;
update the code to guard against a missing session by checking if session ==
null after the firstWhereOrNull call and handle it (log/error/return or throw a
clear exception) before using session.tradeKey.private, then pass the private
key to MostroMessage.wrap only when the session exists and has a tradeKey.

---

Nitpick comments:
In @.specify/v1-reference/README.md:
- Around line 77-79: Update the three migration-note rows (MOSTRO_SERVICE.md,
EXCHANGE_SERVICE.md, ENCRYPTION_SERVICE.md) to explicitly mention the bridge
version by appending "flutter_rust_bridge 2.x" in the V2 phrasing for each
entry; specifically edit the V2 migration text for MOSTRO_SERVICE.md,
EXCHANGE_SERVICE.md, and ENCRYPTION_SERVICE.md so they read something like "Move
core logic to Rust (flutter_rust_bridge 2.x; Flutter handles UI integration)",
"Keep in Flutter (HTTP API integration; use flutter_rust_bridge 2.x where
applicable)" and "Move to Rust (crypto should be platform-agnostic;
flutter_rust_bridge 2.x)" to remove ambiguity during implementation planning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 937b0a06-ce61-47fb-b3a6-3458b0237d8f

📥 Commits

Reviewing files that changed from the base of the PR and between 8a279a7 and 5ba9eed.

📒 Files selected for processing (4)
  • .specify/v1-reference/ENCRYPTION_SERVICE.md
  • .specify/v1-reference/EXCHANGE_SERVICE.md
  • .specify/v1-reference/MOSTRO_SERVICE.md
  • .specify/v1-reference/README.md

Comment on lines +39 to +41
static EncryptionResult fromBlob(Uint8List blob) {
// Extract: nonce (first 12), authTag (last 16), data (middle)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

fromBlob() is underspecified for a reference spec.

This section currently leaves core parsing behavior implicit. Please document explicit validation and extraction rules (min length 28 bytes, nonce first 12, tag last 16, middle ciphertext) to make this implementable without guesswork.

Suggested doc snippet completion
 static EncryptionResult fromBlob(Uint8List blob) {
-  // Extract: nonce (first 12), authTag (last 16), data (middle)
+  if (blob.length < 28) {
+    throw ArgumentError('Blob too short: must include nonce (12) + authTag (16)');
+  }
+  final nonce = blob.sublist(0, 12);
+  final authTag = blob.sublist(blob.length - 16);
+  final encryptedData = blob.sublist(12, blob.length - 16);
+  return EncryptionResult(
+    encryptedData: encryptedData,
+    nonce: nonce,
+    authTag: authTag,
+  );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static EncryptionResult fromBlob(Uint8List blob) {
// Extract: nonce (first 12), authTag (last 16), data (middle)
}
static EncryptionResult fromBlob(Uint8List blob) {
if (blob.length < 28) {
throw ArgumentError('Blob too short: must include nonce (12) + authTag (16)');
}
final nonce = blob.sublist(0, 12);
final authTag = blob.sublist(blob.length - 16);
final encryptedData = blob.sublist(12, blob.length - 16);
return EncryptionResult(
encryptedData: encryptedData,
nonce: nonce,
authTag: authTag,
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.specify/v1-reference/ENCRYPTION_SERVICE.md around lines 39 - 41, Update the
EncryptionResult.fromBlob spec to explicitly validate and parse the blob:
require blob.length >= 28 bytes (12-byte nonce + at least 0-byte ciphertext +
16-byte authTag) and throw a clear error if shorter; extract nonce as the first
12 bytes, authTag as the last 16 bytes, and ciphertext as the bytes in between,
then construct the EncryptionResult from those three components; also state
exact error behavior for malformed input (e.g., raise/return a parse/validation
error).

Comment on lines +72 to +79
throw HttpException('Failed to load data: ${response.statusCode}', uri: url);
} on TimeoutException {
throw HttpException('Request timed out', uri: url);
} on FormatException catch (e) {
throw HttpException('Invalid response format: ${e.message}', uri: url);
} catch (e) {
throw HttpException('Request failed: $e', uri: url);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid re-wrapping your own HttpException in getRequest().

The generic catch (e) will also catch the HttpException you throw for non-200 responses, which loses error specificity and contradicts the documented error mapping table. Add a dedicated on HttpException { rethrow; } before the generic catch.

Suggested doc snippet fix
   if (response.statusCode == 200) {
     return json.decode(response.body) as Map<String, dynamic>;
   }

   throw HttpException('Failed to load data: ${response.statusCode}', uri: url);
+} on HttpException {
+  rethrow;
 } on TimeoutException {
   throw HttpException('Request timed out', uri: url);
 } on FormatException catch (e) {
   throw HttpException('Invalid response format: ${e.message}', uri: url);
 } catch (e) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw HttpException('Failed to load data: ${response.statusCode}', uri: url);
} on TimeoutException {
throw HttpException('Request timed out', uri: url);
} on FormatException catch (e) {
throw HttpException('Invalid response format: ${e.message}', uri: url);
} catch (e) {
throw HttpException('Request failed: $e', uri: url);
}
throw HttpException('Failed to load data: ${response.statusCode}', uri: url);
} on HttpException {
rethrow;
} on TimeoutException {
throw HttpException('Request timed out', uri: url);
} on FormatException catch (e) {
throw HttpException('Invalid response format: ${e.message}', uri: url);
} catch (e) {
throw HttpException('Request failed: $e', uri: url);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.specify/v1-reference/EXCHANGE_SERVICE.md around lines 72 - 79, The
getRequest() error handling currently re-wraps HttpException in the generic
catch block, losing its specificity; add an explicit on HttpException { rethrow;
} clause before the final generic catch to let HttpException propagate unchanged
(retain existing TimeoutException and FormatException handlers and the thrown
HttpException instances for non-200 responses).

Comment on lines +184 to +190
final rate = ref.watch(exchangeRateProvider('USD'));

rate.when(
data: (value) => Text('1 BTC = ${value.toStringAsFixed(2)} USD'),
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Rate direction in UI example is reversed.

exchangeRateProvider('USD') (with getExchangeRate(currency, 'BTC')) yields a USD→BTC rate, but the UI text says 1 BTC = ... USD. This is misleading for integrators.

Suggested doc snippet fix
-rate.when(
-  data: (value) => Text('1 BTC = ${value.toStringAsFixed(2)} USD'),
+rate.when(
+  data: (value) => Text('1 USD = ${value.toStringAsFixed(8)} BTC'),
   loading: () => CircularProgressIndicator(),
   error: (err, stack) => Text('Error: $err'),
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final rate = ref.watch(exchangeRateProvider('USD'));
rate.when(
data: (value) => Text('1 BTC = ${value.toStringAsFixed(2)} USD'),
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
final rate = ref.watch(exchangeRateProvider('USD'));
rate.when(
data: (value) => Text('1 USD = ${value.toStringAsFixed(8)} BTC'),
loading: () => CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.specify/v1-reference/EXCHANGE_SERVICE.md around lines 184 - 190, The UI
example shows "1 BTC = ... USD" but exchangeRateProvider('USD') (which uses
getExchangeRate(currency, 'BTC')) returns a USD→BTC rate, so the direction is
reversed; fix by making the example consistent: either call
exchangeRateProvider('BTC') to obtain a BTC→USD rate when displaying "1 BTC =
... USD", or keep exchangeRateProvider('USD') and change the text to "1 USD =
${value.toStringAsFixed(2)} BTC"; update the snippet where
exchangeRateProvider('USD') is referenced to use the chosen correction
(exchangeRateProvider('BTC') or adjusted display string) so the provider and
displayed direction match.

Comment on lines +168 to +178
final session = sessions.firstWhereOrNull(
(s) => s.orderId == message.id || s.requestId == message.requestId,
);

final pow = ref.read(mostroInstanceProvider)?.pow ?? 0;

final wrappedEvent = await MostroMessage.wrap(
message,
mostroPubkey,
session.tradeKey.private,
pow: pow,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard for missing session before accessing trade key in publishOrder().

firstWhereOrNull can return null; dereferencing session.tradeKey.private without a check turns this into a crash path.

Suggested doc snippet fix
 final session = sessions.firstWhereOrNull(
   (s) => s.orderId == message.id || s.requestId == message.requestId,
 );
+if (session == null) {
+  throw StateError(
+    'No session found for orderId=${message.id} requestId=${message.requestId}',
+  );
+}
 
 final pow = ref.read(mostroInstanceProvider)?.pow ?? 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
final session = sessions.firstWhereOrNull(
(s) => s.orderId == message.id || s.requestId == message.requestId,
);
final pow = ref.read(mostroInstanceProvider)?.pow ?? 0;
final wrappedEvent = await MostroMessage.wrap(
message,
mostroPubkey,
session.tradeKey.private,
pow: pow,
final session = sessions.firstWhereOrNull(
(s) => s.orderId == message.id || s.requestId == message.requestId,
);
if (session == null) {
throw StateError(
'No session found for orderId=${message.id} requestId=${message.requestId}',
);
}
final pow = ref.read(mostroInstanceProvider)?.pow ?? 0;
final wrappedEvent = await MostroMessage.wrap(
message,
mostroPubkey,
session.tradeKey.private,
pow: pow,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.specify/v1-reference/MOSTRO_SERVICE.md around lines 168 - 178,
publishOrder() currently assumes sessions.firstWhereOrNull(...) returns a
non-null session and directly accesses session.tradeKey.private when calling
MostroMessage.wrap, which can crash; update the code to guard against a missing
session by checking if session == null after the firstWhereOrNull call and
handle it (log/error/return or throw a clear exception) before using
session.tradeKey.private, then pass the private key to MostroMessage.wrap only
when the session exists and has a tradeKey.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- ENCRYPTION_SERVICE.md: Added explicit validation in fromBlob (requires >=28 bytes, extracts nonce/authTag/ciphertext with clear error on malformed input)
- EXCHANGE_SERVICE.md: Added 'on HttpException { rethrow; }' to preserve specificity in error handling
- EXCHANGE_SERVICE.md: Fixed UI example to show correct rate direction (1 USD = X BTC, not 1 BTC = X USD)
- MOSTRO_SERVICE.md: Made _isRestorePayload null- and type-safe (guards all containsKey and list accesses)
- MOSTRO_SERVICE.md: Added session null-check in publishOrder before accessing tradeKey.private
- README.md: Added flutter_rust_bridge 2.x mention to all three Core Services migration notes
@grunch grunch merged commit 6fc49c4 into main Mar 25, 2026
1 check passed
@grunch grunch deleted the docs/core-services branch March 25, 2026 10:25
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