mostro 0.18.0

Lightning Network peer-to-peer nostr platform
# Nostr-Based Exchange Rates

## Overview

Mostro daemon publishes Bitcoin/fiat exchange rates to Nostr relays as NIP-33 addressable events (kind `30078`). This enables:

- **Censorship resistance** — Mobile clients in censored regions (Venezuela, Cuba, etc.) can fetch rates via Nostr
- **Zero scaling cost** — Relays distribute events; no per-request infrastructure needed
- **Backward compatibility** — HTTP API remains available as fallback

---

## Event Structure

### Kind 30078 (NIP-33 Addressable Event)

```json
{
  "kind": 30078,
  "pubkey": "82fa8cb978b43c79b2156585bac2c011176a21d2aead6d9f7c575c005be88390",
  "created_at": 1732546800,
  "tags": [
    ["d", "mostro-rates"],
    ["published_at", "1732546800"],
    ["source", "yadio"],
    ["expiration", "1732550400"]
  ],
  "content": "{\"BTC\": {\"USD\": 50000.0, \"EUR\": 45000.0, \"ARS\": 105000000.0, ...}}",
  "sig": "..."
}
```

### Fields

- **kind:** `30078` (application-specific data, NIP-33 replaceable)
- **pubkey:** Mostro daemon's public key (same key that signs orders)
- **d tag:** `"mostro-rates"` (NIP-33 identifier — replaces previous rate events)
- **published_at tag:** Unix timestamp when daemon published the event (not source timestamp)
- **source tag:** `"yadio"` (indicates rate source)
- **expiration tag:** Unix timestamp for event expiration (NIP-40) — prevents stale rates
- **content:** JSON-encoded rates in format `{"CURRENCY": price, ...}`

### Content Format

The `content` field contains the full Yadio API response structure:

```json
{
  "BTC": {
    "BTC": 1,
    "USD": 50000.0,
    "EUR": 45000.0,
    "VES": 850000000.0,
    "ARS": 105000000.0,
    "AED": 260491.35,
    "..."
  }
}
```

**Format:** Identical to Yadio API response (`/exrates/BTC`).

**Rate semantics:** Each value under `"BTC"` represents the price of 1 BTC in that currency.

**Example:** `"BTC": {"USD": 50000.0}` means 1 BTC = 50,000 USD.

---

## Configuration

### Enable/Disable Publishing

Add to `settings.toml`:

```toml
[mostro]
# ... existing config ...

# Publish exchange rates to Nostr (default: true)
publish_exchange_rates_to_nostr = true

# Exchange rates update interval in seconds (default: 300 = 5 minutes)
exchange_rates_update_interval_seconds = 300
```

**Defaults:**
- `publish_exchange_rates_to_nostr`: `true` (enabled for censorship resistance)
- `exchange_rates_update_interval_seconds`: `300` (5 minutes)

### Update Frequency

Exchange rates are fetched from Yadio API and published to Nostr based on the configured `exchange_rates_update_interval_seconds` value.

**Recommended values:**
- **Production:** `300` (5 minutes) — balances freshness with API rate limits
- **Development:** `60` (1 minute) — faster testing
- **Low-volume instances:** `600` (10 minutes) — reduces API calls

**Note:** Very short intervals (<60s) may hit Yadio API rate limits.

---

## Implementation Details

### Code Flow

1. **Scheduler** (`scheduler.rs`): `job_update_bitcoin_prices()` runs at intervals configured by `exchange_rates_update_interval_seconds` (default: 300 seconds)
2. **BitcoinPriceManager** (`bitcoin_price.rs`): 
   - Fetches rates from Yadio HTTP API
   - Updates in-memory cache
   - If `publish_exchange_rates_to_nostr == true`:
     - Transforms rates to expected JSON format
     - Creates NIP-33 event (kind `30078`)
     - Publishes to configured Nostr relays

3. **Event Creation** (`nip33.rs`): `new_exchange_rates_event()` creates the signed event

### Error Handling

- **Yadio API failure** → Logs warning, skips update (keeps previous rates valid)
- **Nostr publish failure** → Logs error but doesn't fail the update job
- **Event creation failure** → Logs error but doesn't crash daemon

**Philosophy:** Nostr publishing is best-effort; HTTP API remains the source of truth.

---

## Security Considerations

### Event Verification (Client-Side)

Mobile clients **MUST** verify the event `pubkey` matches the connected Mostro instance's pubkey to prevent price manipulation attacks.

**Attack scenario:** Malicious actor publishes fake rates to influence order creation.

**Mitigation:** Clients only accept rate events signed by their connected Mostro instance.

See: [Mobile client spec](https://github.com/MostroP2P/app/blob/main/.specify/NOSTR_EXCHANGE_RATES.md)

### Relay Security

- Events are signed with Mostro's private key (standard NIP-01 signature verification)
- NIP-33 addressable events: newer events replace older ones (prevents stale data)
- **NIP-40 expiration:** Events expire after 1 hour (relays should delete them)
- No sensitive data in events (all rates are public information)

---

## Testing

### Unit Tests

```bash
cargo test bitcoin_price
```

**Coverage:**
- Yadio API response deserialization
- Rate format transformation (`{"USD": 0.024}``{"USD": {"BTC": 0.024}}`)
- JSON serialization for Nostr event content

### Integration Testing

1. Start Mostro daemon with `publish_exchange_rates_to_nostr = true`
2. Wait 5 minutes (or trigger update manually)
3. Query relay for kind `30078` events from Mostro pubkey:

```bash
# Using nak CLI
nak req -k 30078 -a <mostro_pubkey> --tag d=mostro-rates wss://relay.mostro.network
```

**Expected output:** JSON event with current exchange rates

### Manual Testing

```bash
# Subscribe to rate updates
nostcat -sub -k 30078 -a <mostro_pubkey> wss://relay.mostro.network

# Verify content format
echo '<event_content>' | jq .
# Should output: {"BTC": {"USD": 50000.0, "EUR": 45000.0, ...}}
```

---

## Deployment

### Production Checklist

- [ ] Verify `publish_exchange_rates_to_nostr` config in `settings.toml`
- [ ] Set `exchange_rates_update_interval_seconds` (default: 300)
- [ ] Confirm Nostr relays are reachable from daemon
- [ ] Monitor logs for "Starting Bitcoin price update job (interval: Xs)" on startup
- [ ] Monitor logs for "Exchange rates published to Nostr" messages
- [ ] Test client-side rate fetching from Nostr
- [ ] Verify fallback to HTTP API works if Nostr unavailable

### Monitoring

**Success indicators:**
```text
INFO Exchange rates published to Nostr. Event ID: <id> (<N> currencies)
```

**Error indicators:**
```text
ERROR Failed to publish exchange rates to Nostr: <error>
ERROR Failed to send exchange rates event to relays: <error>
```

---

## Related Documentation

- [NIP-33: Parameterized Replaceable Events]https://github.com/nostr-protocol/nips/blob/master/33.md
- [Mobile Client Spec]https://github.com/MostroP2P/app/blob/main/.specify/NOSTR_EXCHANGE_RATES.md
- [Issue #684: Feature Proposal]https://github.com/MostroP2P/mostro/issues/684

---

## Future Enhancements

### Multi-Source Aggregation

Aggregate rates from multiple sources (Yadio, CoinGecko, Binance):

```toml
[mostro]
exchange_rate_sources = ["yadio", "coingecko", "binance"]
```

Publish average or median rates to reduce single-source dependency.

### Rate History

Store historical rates in database:

```sql
CREATE TABLE exchange_rate_history (
    timestamp INTEGER PRIMARY KEY,
    currency TEXT NOT NULL,
    btc_rate REAL NOT NULL,
    source TEXT NOT NULL
);
```

Publish daily/weekly summaries as separate NIP-33 events.

### Custom Event Kinds

Propose standardized Nostr event kind for exchange rates (currently using generic `30078`).

**Draft NIP:** "Exchange Rate Events" (kind TBD, e.g., `30400`)