feat: add Nostr-based exchange rates spec for v2#36
Conversation
New spec: NOSTR_EXCHANGE_RATES.md **Overview:** Censorship-resistant exchange rate fetching via NIP-33 addressable events, with Yadio HTTP API fallback. **Key Features:** - Primary source: Nostr (kind 30078, d:rates) - Fallback: Yadio HTTP API - Security: MUST verify event signed by connected Mostro instance pubkey - Publisher: Mostro daemon (fetches from Yadio, publishes to Nostr) **Event Structure:** - Kind 30078 (NIP-33 addressable) - Content: JSON with all currency rates - Published every 5 minutes by Mostro daemon - Signature verification prevents price manipulation attacks **Implementation Plan:** - Phase 1: Mostro daemon publishing (src/exchange_rates.rs) - Phase 2: Mobile client integration (Rust core + Flutter UI) - Phase 3: Gradual rollout with metrics monitoring **Benefits:** - Censorship-resistant (works in Venezuela, Cuba, etc.) - Zero scaling cost for Yadio - Better reliability (multiple relays) - Aligns with Nostr/Bitcoin philosophy **Cross-references:** - v1-reference/EXCHANGE_SERVICE.md (HTTP-based implementation) - Issue #684 (original proposal) - NIP-33 (Parameterized Replaceable Events) Also created .specify/README.md for v2 spec directory overview.
- Removed Phase 1 (daemon) section - Removed mostro.toml config examples - Removed src/exchange_rates.rs references - Focused spec exclusively on client-side implementation - Updated cross-references to point to Issue #684 for daemon implementation - Kept only client architecture, security, testing, and UI concerns
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughTwo specification documents are added to establish the Mostro v2 framework. The Changes
Estimated code review effort🎯 1 (Trivial) | ⏱️ ~5 minutes Possibly related issues
Possibly related PRs
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)
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: 7
🧹 Nitpick comments (2)
.specify/README.md (2)
13-15: Clarify that referenced specs are planned for future implementation.Lines 13-15 reference
ARCHITECTURE.md,DESIGN_SYSTEM.md, andPROTOCOL.md, which are not included in this PR. To avoid confusion, consider adding a note that these are planned specifications.📝 Suggested clarification
### Core Architecture +_(The following specs are planned for future implementation)_ + - [ARCHITECTURE.md](./ARCHITECTURE.md) — System architecture and component design - [DESIGN_SYSTEM.md](./DESIGN_SYSTEM.md) — UI/UX design system and guidelines - [PROTOCOL.md](./PROTOCOL.md) — Mostro protocol specification🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.specify/README.md around lines 13 - 15, Add a short clarifying note in .specify/README.md near the list that ARCHITECTURE.md, DESIGN_SYSTEM.md, and PROTOCOL.md are planned/specs-to-be-added rather than included in this PR; update the list area to append or precede a single-line remark like "These specification documents are planned for future implementation and are not included in this PR" so readers understand the referenced files are pending.
43-49: Clarify SpecKit is planned/future tooling.The section references "SpecKit — the automated app generation tool" as if it exists, but there's no evidence of this tool in the repository. Consider clarifying this is planned infrastructure to avoid confusion.
📝 Suggested clarification
### For SpecKit Generation -These specs are written to be consumed by **SpecKit** — the automated app generation tool for Mostro v2. When a spec is ready for implementation: +These specs are written to be consumed by **SpecKit** — a planned automated app generation tool for Mostro v2. Once SpecKit is available, the workflow will be: 1. Tag the spec with implementation status (e.g., `status: ready`) 2. Run SpecKit generator: `speckit generate --spec FEATURE_NAME.md`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.specify/README.md around lines 43 - 49, The README currently refers to "SpecKit — the automated app generation tool" as if it exists; update the wording to clarify SpecKit is planned or future tooling (e.g., change "These specs are written to be consumed by SpecKit — the automated app generation tool" to "These specs are intended for a planned tool ('SpecKit') — an automated app generator to be added in future"), and add a short note under the bulleted steps that SpecKit is not yet available in this repository (and that developers should tag specs with status: ready and run speckit generate once the tool is published).
🤖 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/NOSTR_EXCHANGE_RATES.md:
- Around line 259-269: Update the spec to use a consistent event update
frequency across the document: pick either a specific interval (e.g., "every 5
minutes") or explicitly state the allowed range (e.g., "every 5–10 minutes
depending on market volatility"), and apply that choice to all mentions
including the "v1 (HTTP)"/ "v2 (Nostr)" bandwidth comparison block, the "5-10
minute TTL" reference, and the PR objectives section so "1 event pushed to all
subscribed clients every 5-10 minutes" and the stated PR objective ("updates
every 5 minutes") are aligned.
- Around line 166-170: The spec adds a persistent SQLite cache for
"exchange_rates" but omits the schema, invalidation policy, and how to map
persisted data into AsyncValue states; add a concrete table definition (columns:
id, currency_code, btc_rate, fetched_at, source, mostro_pubkey) and indexes for
currency_code and fetched_at, implement startup purge of rows older than 24
hours, treat entries older than 15 minutes as "stale" when used as a fallback,
and document that when loading fallback data you should return
AsyncValue.data(rates, staleness: duration) (not AsyncValue.error) while surface
an explicit staleness warning; reference the persistent cache handling around
the "exchange_rates" cache key and the AsyncValue state mapping logic so
reviewers can locate where to persist/read and apply the purge/staleness checks.
- Around line 160-164: The event handling flow that parses event.content needs
explicit error handling: when verifying pubkey (event.pubkey) and signature
(event.verify()) succeeds but JSON.parse(event.content) throws, catch the parse
error, log it with context (including event id/pubkey), do not update the
in-memory/persistent cache or notify Riverpod UI state, and instead trigger the
HTTP API fallback path to fetch/update rates; ensure the parse-failure branch is
clearly documented in the update flow and returns/flags the event as handled to
avoid crashes or duplicate processing.
- Around line 237-238: The UI currently logs an error message ("Could not fetch
exchange rates. Using cached data.") while the fallback path
self.load_from_cache() returns Ok(rates), causing a mismatch between
persistent-cache semantics and AsyncValue mapping; decide which behavior you
want and implement it consistently: either (Option A) treat cached fallback as a
successful AsyncValue.data(rates) plus a staleness flag (add a staleness
boolean/metadata on the rates and render a warning in ErrorView or replace
ErrorView with a StaleDataView), or (Option B) surface the network failure as
AsyncValue.error(err, cachedData) so the error handler (the error: (err, stack)
=> ErrorView(...) branch) can render both the error and the attached cached
rates; update the code paths around self.load_from_cache(), the AsyncValue
construction (AsyncValue.data vs AsyncValue.error), and the ErrorView usage to
reflect the chosen option so the UI message matches the actual returned
AsyncValue.
- Around line 353-374: The migration text is ambiguous about backwards
compatibility—explicitly state that the abstract ExchangeService interface
(methods getExchangeRate() and getCurrencyCodes()) must remain unchanged, add a
new NostrExchangeRateService implementation that internally attempts Nostr first
then falls back to YadioExchangeRateService and persistent cache, and confirm
Riverpod provider names/signatures (e.g. ExchangeRateNotifier,
StateNotifierProvider.family exchangeRateProvider and a
Provider<ExchangeService> exchangeServiceProvider) remain the same so the new
service is a drop‑in replacement for existing UI code.
- Line 122: The fenced code block for the architecture diagram is missing a
language identifier; update the opening fence to include one (e.g., change the
opening "```" to "```text") so the diagram lines like "ExchangeRateService (Rust
core)" and "NostrExchangeRateProvider (primary)" are rendered and recognized
correctly by tooling.
In @.specify/README.md:
- Around line 92-108: The fenced code block in .specify/README.md is missing a
language identifier which can break syntax highlighting; update the opening
fence for the directory-structure block to include a language (for example
"text") so the block becomes ```text and leave the rest unchanged — locate the
directory tree block in README.md (the triple-backtick section showing
".specify/ ├── README.md ...") and add the language identifier to the opening
fence.
---
Nitpick comments:
In @.specify/README.md:
- Around line 13-15: Add a short clarifying note in .specify/README.md near the
list that ARCHITECTURE.md, DESIGN_SYSTEM.md, and PROTOCOL.md are
planned/specs-to-be-added rather than included in this PR; update the list area
to append or precede a single-line remark like "These specification documents
are planned for future implementation and are not included in this PR" so
readers understand the referenced files are pending.
- Around line 43-49: The README currently refers to "SpecKit — the automated app
generation tool" as if it exists; update the wording to clarify SpecKit is
planned or future tooling (e.g., change "These specs are written to be consumed
by SpecKit — the automated app generation tool" to "These specs are intended for
a planned tool ('SpecKit') — an automated app generator to be added in future"),
and add a short note under the bulleted steps that SpecKit is not yet available
in this repository (and that developers should tag specs with status: ready and
run speckit generate once the tool is published).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d0b18955-7635-42eb-9f26-3a3b45ff548d
📒 Files selected for processing (2)
.specify/NOSTR_EXCHANGE_RATES.md.specify/README.md
|
|
||
| **v1 (HTTP):** | ||
| - 1 request every 5-10 minutes per active user | ||
| - ~5-10 KB per request (all currencies) | ||
|
|
||
| **v2 (Nostr):** | ||
| - 1 event pushed to all subscribed clients every 5-10 minutes | ||
| - ~5-10 KB per event (same payload) | ||
| - No per-user request overhead | ||
|
|
||
| **Conclusion:** Bandwidth roughly equivalent, but Nostr scales better (relays handle distribution). |
There was a problem hiding this comment.
Standardize event update frequency.
The spec mentions "1 event pushed to all subscribed clients every 5-10 minutes" (line 265), and line 168 mentions "5-10 minute TTL", but the PR objectives state "updates every 5 minutes". This should be consistent throughout the spec.
Recommendation: Use a specific value (e.g., "every 5 minutes") or document the variability (e.g., "every 5-10 minutes depending on market volatility").
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.specify/NOSTR_EXCHANGE_RATES.md around lines 259 - 269, Update the spec to
use a consistent event update frequency across the document: pick either a
specific interval (e.g., "every 5 minutes") or explicitly state the allowed
range (e.g., "every 5–10 minutes depending on market volatility"), and apply
that choice to all mentions including the "v1 (HTTP)"/ "v2 (Nostr)" bandwidth
comparison block, the "5-10 minute TTL" reference, and the PR objectives section
so "1 event pushed to all subscribed clients every 5-10 minutes" and the stated
PR objective ("updates every 5 minutes") are aligned.
| ## Migration from v1 → v2 | ||
|
|
||
| ### Step 1: Add Nostr as Primary Source | ||
|
|
||
| - Implement `NostrExchangeRateProvider` in Rust core | ||
| - Keep `YadioExchangeRateProvider` as-is (for fallback) | ||
| - Update `ExchangeService` to try Nostr first, then HTTP | ||
|
|
||
| ### Step 2: Test in Regtest/Testnet | ||
|
|
||
| - Connect to test Mostro instance with rate publishing enabled | ||
| - Verify mobile app receives and validates events | ||
| - Test fallback when Nostr unavailable | ||
|
|
||
| ### Step 3: Gradual Rollout | ||
|
|
||
| - **Beta users:** Nostr-first (with fallback) | ||
| - Monitor metrics: | ||
| - % of rate fetches from Nostr vs HTTP | ||
| - Latency (Nostr vs HTTP) | ||
| - Failure rate | ||
| - **Stable release:** Once Nostr proves reliable (>95% success rate) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Clarify backward compatibility and provider implementation pattern.
The migration step "Update ExchangeService to try Nostr first, then HTTP" (line 359) is vague about the implementation approach. Based on v1-reference/EXCHANGE_SERVICE.md context:
- v1 has an abstract
ExchangeServicebase class withgetExchangeRate()andgetCurrencyCodes()methods - Current implementation is
YadioExchangeService - Riverpod providers expose
StateNotifierProvider.family<ExchangeRateNotifier, AsyncValue<double>, String>
The spec should clarify:
- Does the
ExchangeServiceabstract interface stay unchanged (ensuring existing UI code works without modification)? - Is a new
NostrExchangeRateServiceimplementation created that wraps both Nostr and HTTP providers internally? - Do the Riverpod provider names and signatures remain the same?
This is critical for ensuring backward compatibility. As per context snippet 1, the v1 spec emphasized maintaining the public API surface while changing internal implementation.
📋 Recommended addition to migration section
Add after line 359:
**Backward Compatibility:**
- Keep `ExchangeService` abstract interface unchanged (same method signatures)
- Create new `NostrExchangeRateService` implementation that internally:
1. Tries Nostr subscription first
2. Falls back to HTTP API (reuses existing `YadioExchangeRateService` logic)
3. Uses persistent cache as last resort
- Riverpod provider setup remains the same:
```dart
final exchangeServiceProvider = Provider<ExchangeService>((ref) {
return NostrExchangeRateService(); // Drop-in replacement
});- Existing UI code requires no changes (same
ref.watch(exchangeRateProvider('USD'))pattern)
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
Verify each finding against the current code and only fix it if needed.
In @.specify/NOSTR_EXCHANGE_RATES.md around lines 353 - 374, The migration text
is ambiguous about backwards compatibility—explicitly state that the abstract
ExchangeService interface (methods getExchangeRate() and getCurrencyCodes())
must remain unchanged, add a new NostrExchangeRateService implementation that
internally attempts Nostr first then falls back to YadioExchangeRateService and
persistent cache, and confirm Riverpod provider names/signatures (e.g.
ExchangeRateNotifier, StateNotifierProvider.family exchangeRateProvider and a
Provider exchangeServiceProvider) remain the same so the new
service is a drop‑in replacement for existing UI code.
</details>
<!-- fingerprinting:phantom:triton:puma:f147a7a5-c61a-4047-8fb5-efac2b6cbce2 -->
<!-- This is an auto-generated comment by CodeRabbit -->
| ``` | ||
| .specify/ | ||
| ├── README.md # This file | ||
| ├── ARCHITECTURE.md # Overall system design | ||
| ├── DESIGN_SYSTEM.md # UI/UX guidelines | ||
| ├── PROTOCOL.md # Mostro protocol spec | ||
| ├── NOSTR_EXCHANGE_RATES.md # v2 feature: Nostr-based rates | ||
| ├── v1-reference/ # Flutter v1 implementation docs | ||
| │ ├── README.md | ||
| │ ├── EXCHANGE_SERVICE.md | ||
| │ ├── MOSTRO_SERVICE.md | ||
| │ └── ... | ||
| ├── memory/ # Spec evolution notes | ||
| │ └── constitution.md | ||
| ├── scripts/ # Tooling for spec generation | ||
| └── templates/ # Spec templates | ||
| ``` |
There was a problem hiding this comment.
Add language identifier to fenced code block.
The directory structure code block should have a language identifier for proper rendering.
📝 Proposed fix
-```
+```text
.specify/
├── README.md # This file📝 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.
| ``` | |
| .specify/ | |
| ├── README.md # This file | |
| ├── ARCHITECTURE.md # Overall system design | |
| ├── DESIGN_SYSTEM.md # UI/UX guidelines | |
| ├── PROTOCOL.md # Mostro protocol spec | |
| ├── NOSTR_EXCHANGE_RATES.md # v2 feature: Nostr-based rates | |
| ├── v1-reference/ # Flutter v1 implementation docs | |
| │ ├── README.md | |
| │ ├── EXCHANGE_SERVICE.md | |
| │ ├── MOSTRO_SERVICE.md | |
| │ └── ... | |
| ├── memory/ # Spec evolution notes | |
| │ └── constitution.md | |
| ├── scripts/ # Tooling for spec generation | |
| └── templates/ # Spec templates | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 92-92: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.specify/README.md around lines 92 - 108, The fenced code block in
.specify/README.md is missing a language identifier which can break syntax
highlighting; update the opening fence for the directory-structure block to
include a language (for example "text") so the block becomes ```text and leave
the rest unchanged — locate the directory tree block in README.md (the
triple-backtick section showing ".specify/ ├── README.md ...") and add the
language identifier to the opening fence.
Updated subscription filter and verification logic to use 'mostro-rates' instead of 'rates' to match daemon implementation.
Updated spec to match daemon fix: rates are published as
{"USD": 50000.0} (price of 1 BTC), not {"USD": {"BTC": 0.000024}}.
Matches daemon implementation and clarifies semantics: published_at = daemon publish timestamp (not source timestamp).
Content now published as: {"BTC": {"USD": 50000.0, ...}}
Matches Yadio API response exactly for easy client parsing.
Fixed all outstanding issues from CodeRabbit review:
**1. Architecture diagram language identifier**
- Added ```text to architecture diagram
- Updated d tag reference to 'mostro-rates'
- Fixed API endpoint to /exrates/BTC
**2. JSON malformed handling**
- Documented error handling for invalid JSON
- Added validation for BTC key structure
- Defined fallback behavior (log + ignore + keep cache)
**3. SQLite schema and cache invalidation (MAJOR)**
- Documented complete SQLite schema:
```sql
CREATE TABLE exchange_rates_cache (
id INTEGER PRIMARY KEY CHECK (id = 1),
content TEXT NOT NULL,
fetched_at INTEGER NOT NULL,
source TEXT NOT NULL
);
```
- Defined cache invalidation rules:
- TTL: 1 hour (fetched_at < now - 3600)
- Replacement strategy (Nostr > HTTP)
- App launch behavior
- Manual refresh bypasses cache
**4. Error message clarification**
- Improved ErrorView example with timestamp
- Shows cached data below error message
- Handles case where no cache available
- Added staleness warning for old cache (>15 min)
**5. Update frequency consistency**
- Changed "5-10 minutes" to "configured interval (default: 5 minutes)"
- References exchange_rates_update_interval_seconds config
- Clarified daemon publishes, relays distribute
**6. README.md non-existent specs**
- Added note that ARCHITECTURE.md, DESIGN_SYSTEM.md are planned
- Marked as *(planned)* and struck through
- Added link to mostro repo for PROTOCOL.md
All issues resolved. Ready for merge.
Add documentation for the new exchange rates publishing feature via Nostr.
## Overview
Mostro instances can now publish Bitcoin/fiat exchange rates as NIP-33
addressable events (kind 30078) to provide censorship-resistant price
data for clients.
## Key Features
- **Event kind**: 30078 (NIP-33 addressable, replaceable)
- **d tag**: "mostro-rates" (unique identifier)
- **Content**: Full Yadio API format {\BTC\: {\USD\: 50000.0, ...}}
- **Expiration**: NIP-40 tag (min(interval × 2, 3600))
- **Configurable**: Update interval (default 300s, min 60s)
- **Optional**: Can be disabled via config
## Why Nostr?
In censored regions (Venezuela, Cuba, Iran), HTTP exchange rate APIs
are often blocked. Nostr's decentralized relay network provides:
- Censorship resistance
- Zero scaling cost
- Geographic distribution
- No single point of failure
## Security
Clients MUST:
- Verify event.pubkey matches expected Mostro instance
- Verify signature (NIP-01)
- Check expiration timestamp
- Fall back to HTTP API if Nostr fails
## Implementation
- Daemon PR: MostroP2P/mostro#685
- Client spec: MostroP2P/app#36
Related to the Venezuela censorship resistance use case discussed in:
MostroP2P/mostro#684
Comprehensive specification for censorship-resistant exchange rate fetching via Nostr (NIP-33).
New Files
NOSTR_EXCHANGE_RATES.md
Complete v2 feature spec for Nostr-based exchange rates.
Protocol:
30078, d tag"rates"){"USD": {"BTC": 0.000024}, ...})Security (Critical):
event.pubkeymatches connected Mostro instanceArchitecture:
Implementation Plan:
Benefits:
Relays:
wss://relay.mostro.network(primary)wss://nos.lol(fast public fallback)wss://relay.nostr.band(archival)Testing Strategy:
Cross-References:
.specify/README.md
New directory-level README for v2 specs.
Contents:
Related
Next Steps
Summary by CodeRabbit