diff --git a/lib/features/disputes/screens/dispute_chat_screen.dart b/lib/features/disputes/screens/dispute_chat_screen.dart index 1c703540..fb33e0fe 100644 --- a/lib/features/disputes/screens/dispute_chat_screen.dart +++ b/lib/features/disputes/screens/dispute_chat_screen.dart @@ -43,7 +43,9 @@ class _DisputeChatScreenState extends ConsumerState { } void _onSendText(String text) { - // TODO(bridge): Encrypt with adminSharedKey and publish via Rust bridge. + // Dispute messaging requires an adminSharedKey derived from the trade key + // and the admin's pubkey (Phase 12). The Rust bridge will expose + // `send_dispute_message(dispute_id, text, admin_shared_key)` when ready. final l10n = AppLocalizations.of(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -54,7 +56,8 @@ class _DisputeChatScreenState extends ConsumerState { } void _onAttachFile() { - // TODO(bridge): Open file picker, encrypt with adminSharedKey, upload. + // File attachments in disputes require the same adminSharedKey as text + // messages (Phase 12). Will use file picker + encrypt + upload flow. final l10n = AppLocalizations.of(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/features/settings/providers/log_provider.dart b/lib/features/settings/providers/log_provider.dart new file mode 100644 index 00000000..b4ea8974 --- /dev/null +++ b/lib/features/settings/providers/log_provider.dart @@ -0,0 +1,23 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:mostro/src/rust/api/logging.dart' as logging_api; +import 'package:mostro/src/rust/api/types.dart'; + +/// Live log entries from the Rust backend, newest first. +/// +/// Caps at 500 entries to bound memory usage. The stream is cancelled +/// when the provider is disposed (e.g. when LogReportScreen is popped). +final logEntriesProvider = StreamProvider.autoDispose>((ref) async* { + var cancelled = false; + ref.onDispose(() => cancelled = true); + + final stream = await logging_api.onLogEntry(); + final entries = []; + while (!cancelled) { + final entry = await stream.next(); + if (entry == null || cancelled) break; + entries.insert(0, entry); // newest first + if (entries.length > 500) entries.removeLast(); + yield List.unmodifiable(entries); + } +}); diff --git a/lib/features/settings/screens/log_report_screen.dart b/lib/features/settings/screens/log_report_screen.dart index 45be729c..5af6ddfc 100644 --- a/lib/features/settings/screens/log_report_screen.dart +++ b/lib/features/settings/screens/log_report_screen.dart @@ -3,29 +3,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:share_plus/share_plus.dart'; import 'package:mostro/core/app_theme.dart'; +import 'package:mostro/features/settings/providers/log_provider.dart'; import 'package:mostro/features/settings/providers/settings_provider.dart'; - -// ── Local log level enum (mirrors Rust LogLevel) ────────────────────────────── - -enum _LogLevel { debug, info, warning, error } - -// ── Local log entry model ───────────────────────────────────────────────────── - -class _LogEntry { - const _LogEntry({ - required this.id, - required this.level, - required this.tag, - required this.message, - required this.timestamp, - }); - - final int id; - final _LogLevel level; - final String tag; - final String message; - final int timestamp; // Unix seconds -} +import 'package:mostro/src/rust/api/types.dart'; // ── Screen ──────────────────────────────────────────────────────────────────── @@ -37,36 +17,6 @@ class LogReportScreen extends ConsumerStatefulWidget { } class _LogReportScreenState extends ConsumerState { - // Sample data shown when no bridge is connected. - // TODO(bridge): replace _mockEntries with a live stream from the Rust log - // sink once the log bridge API is implemented (Phase 18+). The bridge - // should expose an on_log_entry() stream that emits LogEntry structs; consume - // it here via a StreamBuilder or a Riverpod StreamProvider and remove the - // static list below. - static final List<_LogEntry> _mockEntries = List.unmodifiable([ - _LogEntry( - id: 1, - level: _LogLevel.info, - tag: 'App', - message: 'Application started successfully', - timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000 - 120, - ), - _LogEntry( - id: 2, - level: _LogLevel.warning, - tag: 'Relay', - message: 'Connection to wss://nos.lol timed out, retrying…', - timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000 - 60, - ), - _LogEntry( - id: 3, - level: _LogLevel.debug, - tag: 'Order', - message: 'Fetched 42 orders from relay', - timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000 - 10, - ), - ]); - @override Widget build(BuildContext context) { final loggingEnabled = ref.watch(settingsProvider).loggingEnabled; @@ -74,6 +24,9 @@ class _LogReportScreenState extends ConsumerState { if (colorsRaw == null) throw StateError('AppColors theme extension must be registered'); final colors = colorsRaw; + final logAsync = ref.watch(logEntriesProvider); + final entries = logAsync.valueOrNull ?? const []; + return Scaffold( appBar: AppBar( title: const Text('Log Report'), @@ -81,8 +34,8 @@ class _LogReportScreenState extends ConsumerState { // Share logs — disabled when no entries exist. IconButton( icon: const Icon(Icons.share_outlined), - tooltip: _mockEntries.isNotEmpty ? 'Share logs' : 'No logs to share', - onPressed: _mockEntries.isNotEmpty ? _shareLogs : null, + tooltip: entries.isNotEmpty ? 'Share logs' : 'No logs to share', + onPressed: entries.isNotEmpty ? () => _shareLogs(entries) : null, ), // Toggle logging IconButton( @@ -138,7 +91,7 @@ class _LogReportScreenState extends ConsumerState { ), // Log entries Expanded( - child: _mockEntries.isEmpty + child: entries.isEmpty ? Center( child: Text( 'No log entries', @@ -147,10 +100,10 @@ class _LogReportScreenState extends ConsumerState { ) : ListView.builder( padding: const EdgeInsets.all(AppSpacing.md), - itemCount: _mockEntries.length, + itemCount: entries.length, itemBuilder: (context, index) { return _LogEntryTile( - entry: _mockEntries[index], + entry: entries[index], colors: colors, ); }, @@ -161,8 +114,8 @@ class _LogReportScreenState extends ConsumerState { ); } - Future _shareLogs() async { - final lines = _mockEntries.map((e) { + Future _shareLogs(List entries) async { + final lines = entries.map((e) { final time = _formatTimestamp(e.timestamp); final level = e.level.name.toUpperCase().padRight(7); final tag = _sanitizeForShare(e.tag); @@ -192,7 +145,7 @@ class _LogReportScreenState extends ConsumerState { class _LogEntryTile extends StatelessWidget { const _LogEntryTile({required this.entry, required this.colors}); - final _LogEntry entry; + final LogEntry entry; final AppColors colors; @override @@ -258,12 +211,12 @@ class _LogEntryTile extends StatelessWidget { ); } - (Color, Color) _levelColors(_LogLevel level) { + (Color, Color) _levelColors(LogLevel level) { return switch (level) { - _LogLevel.debug => (const Color(0xFF374151), const Color(0xFFD1D5DB)), - _LogLevel.info => (const Color(0xFF1E3A8A), const Color(0xFF93C5FD)), - _LogLevel.warning => (const Color(0xFF854D0E), const Color(0xFFFCD34D)), - _LogLevel.error => (const Color(0xFF7F1D1D), const Color(0xFFFCA5A5)), + LogLevel.debug => (const Color(0xFF374151), const Color(0xFFD1D5DB)), + LogLevel.info => (const Color(0xFF1E3A8A), const Color(0xFF93C5FD)), + LogLevel.warning => (const Color(0xFF854D0E), const Color(0xFFFCD34D)), + LogLevel.error => (const Color(0xFF7F1D1D), const Color(0xFFFCA5A5)), }; } } diff --git a/lib/features/settings/widgets/mostro_node_selector.dart b/lib/features/settings/widgets/mostro_node_selector.dart index de00407f..77ffcc58 100644 --- a/lib/features/settings/widgets/mostro_node_selector.dart +++ b/lib/features/settings/widgets/mostro_node_selector.dart @@ -3,16 +3,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro/core/app_theme.dart'; import 'package:mostro/core/mostro_defaults.dart'; +import 'package:mostro/src/rust/api/settings.dart' as settings_api; // ── Provider for current Mostro node pubkey ─────────────────────────────────── const _defaultMostroPubkey = defaultMostroPubkey; -/// In-memory override of the Mostro node pubkey. -/// -/// **UI-only placeholder** — this value is not yet passed to the Rust bridge. -/// TODO(bridge): read mostroPubkeyProvider when constructing outgoing Nostr -/// events so order routing uses the selected node (Phase 18+). +/// Active Mostro node pubkey — synced to the Rust bridge so outgoing events +/// are routed to the selected node. final mostroPubkeyProvider = StateProvider( (ref) => _defaultMostroPubkey, ); @@ -61,25 +59,46 @@ class _MostroNodeSelectorState extends ConsumerState { super.dispose(); } - void _useDefault() { + Future _useDefault() async { + final previous = ref.read(mostroPubkeyProvider); ref.read(mostroPubkeyProvider.notifier).state = _defaultMostroPubkey; - _controller.clear(); - setState(() => _errorText = null); - Navigator.of(context).pop(); + try { + await settings_api.setMostroPubkey(pubkey: null); + if (!mounted) return; + _controller.clear(); + setState(() => _errorText = null); + Navigator.of(context).pop(); + } catch (e) { + debugPrint('[MostroNodeSelector] setMostroPubkey(null) failed: $e'); + ref.read(mostroPubkeyProvider.notifier).state = previous; + if (!mounted) return; + setState(() => _errorText = 'Failed to reset node'); + } } - void _confirm() { + Future _confirm() async { final input = _controller.text.trim(); if (input.isEmpty) { - _useDefault(); + await _useDefault(); return; } if (!_hexRegex.hasMatch(input)) { setState(() => _errorText = 'Must be a 64-character hex string'); return; } - ref.read(mostroPubkeyProvider.notifier).state = input.toLowerCase(); - Navigator.of(context).pop(); + final pubkey = input.toLowerCase(); + final previous = ref.read(mostroPubkeyProvider); + ref.read(mostroPubkeyProvider.notifier).state = pubkey; + try { + await settings_api.setMostroPubkey(pubkey: pubkey); + if (!mounted) return; + Navigator.of(context).pop(); + } catch (e) { + debugPrint('[MostroNodeSelector] setMostroPubkey failed: $e'); + ref.read(mostroPubkeyProvider.notifier).state = previous; + if (!mounted) return; + setState(() => _errorText = 'Invalid pubkey or bridge error'); + } } @override diff --git a/lib/features/settings/widgets/relay_management_card.dart b/lib/features/settings/widgets/relay_management_card.dart index 2fdc7b27..ce248669 100644 --- a/lib/features/settings/widgets/relay_management_card.dart +++ b/lib/features/settings/widgets/relay_management_card.dart @@ -38,9 +38,6 @@ class _RelayManagementCardState extends ConsumerState { // Defaults mirror rust/src/config.rs — imported from core/mostro_defaults.dart. static const _defaultRelays = defaultMostroRelays; - // TODO(bridge): replace _relays local state with a Riverpod provider backed - // by the Rust bridge (get_relays / add_relay / remove_relay) so configuration - // persists across navigations and stays in sync with the backend (Phase 18+). late List<_RelayEntry> _relays; bool _loading = false; @@ -73,8 +70,29 @@ class _RelayManagementCardState extends ConsumerState { } } - void _toggleRelay(int index, bool value) { + Future _toggleRelay(int index, bool value) async { + final url = _relays[index].url; setState(() => _relays[index].isActive = value); + try { + if (value) { + await nostr_api.addRelay(url: url); + } else { + await nostr_api.removeRelay(url: url); + } + } catch (e) { + debugPrint('[RelayManagement] toggleRelay failed: $e'); + if (!mounted) return; + setState(() { + final currentIndex = _relays.indexWhere((r) => r.url == url); + if (currentIndex != -1) { + _relays[currentIndex].isActive = !value; + } + }); + final l10n = AppLocalizations.of(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(value ? l10n.relayAddFailed : l10n.relayRemoveFailed)), + ); + } } Future _removeRelay(int index) async { diff --git a/rust/src/api/disputes.rs b/rust/src/api/disputes.rs index cb0f7550..0bad37d2 100644 --- a/rust/src/api/disputes.rs +++ b/rust/src/api/disputes.rs @@ -134,8 +134,8 @@ pub async fn open_dispute(trade_id: String, reason: Option) -> Result> = OnceLock::new(); + +fn log_sender() -> &'static broadcast::Sender { + LOG_TX.get_or_init(|| { + let (tx, _) = broadcast::channel(512); + tx + }) +} + +// ── Forwarding bridge (sync → async) ──────────────────────────────────────── + +static LOG_STD_TX: OnceLock>> = OnceLock::new(); + +/// Monotonically increasing ID for log entries. +static COUNTER: AtomicU32 = AtomicU32::new(0); + +/// Install the log capture bridge. +/// +/// Must be called once during app initialization (from `init_app()`). +/// After this call, every `log::info!()` / `log::warn!()` etc. in Rust +/// is forwarded to the Flutter `on_log_entry()` stream. +pub fn install_log_bridge() { + static INSTALLED: std::sync::Once = std::sync::Once::new(); + INSTALLED.call_once(|| { + let tx = log_sender().clone(); + let (std_tx, std_rx) = mpsc::channel::(); + + // Background thread: reads from the std mpsc channel and broadcasts + // to the tokio broadcast channel. + std::thread::spawn(move || { + while let Ok(entry) = std_rx.recv() { + let _ = tx.send(entry); + } + }); + + LOG_STD_TX.get_or_init(|| Mutex::new(std_tx)); + + // Install a custom log::Log that forwards every record. + // max_level is set to Debug so Info/Warn/Error all flow through. + let _ = log::set_logger(&BRIDGE_LOGGER); + log::set_max_level(log::LevelFilter::Debug); + }); +} + +/// Global logger instance that forwards records to the Flutter stream +/// and prints to stderr (logcat on Android). +static BRIDGE_LOGGER: BridgeLogger = BridgeLogger; + +struct BridgeLogger; + +impl log::Log for BridgeLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::Level::Debug + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + // Forward to Flutter stream. + forward_log(record.level(), record.target(), &record.args().to_string()); + + // Also print to stderr so adb logcat / terminal output still works. + eprintln!( + "[{level}] {target}: {msg}", + level = record.level(), + target = record.target(), + msg = record.args(), + ); + } + + fn flush(&self) {} +} + +/// Forward a log record to the Flutter stream. +pub(crate) fn forward_log(level: log::Level, target: &str, message: &str) { + if let Some(tx) = LOG_STD_TX.get() { + let entry = LogEntry { + id: COUNTER.fetch_add(1, Ordering::Relaxed), + level: match level { + log::Level::Error => LogLevel::Error, + log::Level::Warn => LogLevel::Warning, + log::Level::Info => LogLevel::Info, + _ => LogLevel::Debug, + }, + tag: target.split("::").last().unwrap_or(target).to_string(), + message: message.to_string(), + timestamp: std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64, + }; + if let Ok(g) = tx.lock() { + let _ = g.send(entry); + } + } +} + +// ── FRB stream ─────────────────────────────────────────────────────────────── + +/// Stream of log entries for consumption by Flutter. +pub struct LogEntryStream { + rx: broadcast::Receiver, +} + +impl LogEntryStream { + /// Poll for the next log entry. + pub async fn next(&mut self) -> Option { + loop { + match self.rx.recv().await { + Ok(entry) => return Some(entry), + Err(broadcast::error::RecvError::Lagged(_)) => continue, + Err(broadcast::error::RecvError::Closed) => return None, + } + } + } +} + +/// Subscribe to the live log stream. +/// +/// Each call returns a new independent stream. Call [`install_log_bridge`] +/// during initialization or no entries will be emitted. +pub fn on_log_entry() -> LogEntryStream { + LogEntryStream { + rx: log_sender().subscribe(), + } +} + +// ── Tests ──────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn forward_log_does_not_panic() { + // Calling forward_log should not panic regardless of bridge state. + // If bridge is installed, this forwards; if not, it's a no-op. + forward_log(log::Level::Info, "test", "should not panic"); + } + + #[tokio::test] + async fn install_and_receive_log() { + install_log_bridge(); + let mut stream = on_log_entry(); + + forward_log(log::Level::Warn, "nwc::client", "test warning"); + + let entry = tokio::time::timeout( + std::time::Duration::from_secs(2), + stream.next(), + ) + .await + .expect("timed out waiting for log entry") + .expect("stream closed unexpectedly"); + + assert_eq!(entry.level, LogLevel::Warning); + assert_eq!(entry.tag, "client"); + assert_eq!(entry.message, "test warning"); + } +} diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs index 5186f1cf..6a0d7655 100644 --- a/rust/src/api/mod.rs +++ b/rust/src/api/mod.rs @@ -1,5 +1,6 @@ pub mod disputes; pub mod identity; +pub mod logging; pub mod messages; pub mod nostr; pub mod nwc; diff --git a/rust/src/api/orders.rs b/rust/src/api/orders.rs index ace16021..1dff7fb6 100644 --- a/rust/src/api/orders.rs +++ b/rust/src/api/orders.rs @@ -9,7 +9,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use tokio::sync::{broadcast, RwLock}; use crate::api::types::{NewOrderParams, OrderInfo, OrderKind, OrderStatus}; -use crate::config::DEFAULT_MOSTRO_PUBKEY; +use crate::config::active_mostro_pubkey; use crate::db::Storage; use crate::mostro::actions; use crate::nostr::order_events::parse_order_event; @@ -436,7 +436,7 @@ pub async fn create_order(params: NewOrderParams) -> Result { store_pending_local_id(&ck, &order.id); order_book().upsert_order(order.clone()).await; - let mostro_pubkey = nostr_sdk::PublicKey::from_hex(DEFAULT_MOSTRO_PUBKEY)?; + let mostro_pubkey = nostr_sdk::PublicKey::from_hex(&active_mostro_pubkey())?; let event_json = actions::new_order(&sender_keys, &mostro_pubkey, ¶ms_for_dispatch, trade_index) .await?; @@ -535,7 +535,7 @@ pub async fn take_order( // Dispatch the take action to Mostro using the trade key for signing. match crate::api::identity::get_active_trade_keys(trade_index).await { Ok(sender_keys) => { - match nostr_sdk::PublicKey::from_hex(DEFAULT_MOSTRO_PUBKEY) { + match nostr_sdk::PublicKey::from_hex(&active_mostro_pubkey()) { Ok(mostro_pubkey) => { // Read default LN address from settings (take-sell-ln-address flow). let ln_address: Option = @@ -628,7 +628,7 @@ pub async fn send_invoice( let trade_index = get_trade_key_index(&order_id).await .ok_or_else(|| anyhow::anyhow!("no persisted trade key for order {order_id}"))?; let sender_keys = crate::api::identity::get_active_trade_keys(trade_index).await?; - let mostro_pubkey = nostr_sdk::PublicKey::from_hex(DEFAULT_MOSTRO_PUBKEY)?; + let mostro_pubkey = nostr_sdk::PublicKey::from_hex(&active_mostro_pubkey())?; let event_json = actions::add_invoice( &sender_keys, &mostro_pubkey, @@ -656,7 +656,7 @@ pub async fn send_fiat_sent(order_id: String) -> Result<()> { let trade_index = get_trade_key_index(&order_id).await .ok_or_else(|| anyhow::anyhow!("no persisted trade key for order {order_id}"))?; let sender_keys = crate::api::identity::get_active_trade_keys(trade_index).await?; - let mostro_pubkey = nostr_sdk::PublicKey::from_hex(DEFAULT_MOSTRO_PUBKEY)?; + let mostro_pubkey = nostr_sdk::PublicKey::from_hex(&active_mostro_pubkey())?; let event_json = actions::fiat_sent(&sender_keys, &mostro_pubkey, &order_id, trade_index).await?; publish_event_json(&event_json).await?; log::info!("[orders] fiat_sent published for order={order_id} trade_index={trade_index}"); @@ -671,7 +671,7 @@ pub async fn release_order(order_id: String) -> Result<()> { let trade_index = get_trade_key_index(&order_id).await .ok_or_else(|| anyhow::anyhow!("no persisted trade key for order {order_id}"))?; let sender_keys = crate::api::identity::get_active_trade_keys(trade_index).await?; - let mostro_pubkey = nostr_sdk::PublicKey::from_hex(DEFAULT_MOSTRO_PUBKEY)?; + let mostro_pubkey = nostr_sdk::PublicKey::from_hex(&active_mostro_pubkey())?; let event_json = actions::release(&sender_keys, &mostro_pubkey, &order_id, trade_index).await?; publish_event_json(&event_json).await?; log::info!("[orders] release published for order={order_id} trade_index={trade_index}"); @@ -687,7 +687,7 @@ pub async fn cancel_order(order_id: String) -> Result<()> { let trade_index = get_trade_key_index(&order_id).await .ok_or_else(|| anyhow::anyhow!("no persisted trade key for order {order_id}"))?; let sender_keys = crate::api::identity::get_active_trade_keys(trade_index).await?; - let mostro_pubkey = nostr_sdk::PublicKey::from_hex(DEFAULT_MOSTRO_PUBKEY)?; + let mostro_pubkey = nostr_sdk::PublicKey::from_hex(&active_mostro_pubkey())?; let event_json = actions::cancel(&sender_keys, &mostro_pubkey, &order_id, trade_index).await?; publish_event_json(&event_json).await?; log::info!("[orders] cancel published for order={order_id} trade_index={trade_index}"); @@ -884,7 +884,7 @@ async fn subscribe_single_order(order_id: &str) { }; let client = pool.client(); let mostro_pubkey = - match nostr_sdk::PublicKey::from_hex(crate::config::DEFAULT_MOSTRO_PUBKEY) { + match nostr_sdk::PublicKey::from_hex(&crate::config::active_mostro_pubkey()) { Ok(pk) => pk, Err(e) => { log::error!("[orders] subscribe_single_order: invalid pubkey: {e}"); @@ -1011,10 +1011,10 @@ async fn _run_order_subscription() { // The Mostro daemon is the author of all Kind 38383 events. // Use the compiled-in default pubkey (mirrors config.rs / settings screen). - let mostro_pubkey = match nostr_sdk::PublicKey::from_hex(crate::config::DEFAULT_MOSTRO_PUBKEY) { + let mostro_pubkey = match nostr_sdk::PublicKey::from_hex(&crate::config::active_mostro_pubkey()) { Ok(pk) => pk, Err(e) => { - log::error!("[orders] invalid DEFAULT_MOSTRO_PUBKEY: {e}"); + log::error!("[orders] invalid mostro pubkey: {e}"); return; } }; diff --git a/rust/src/api/reputation.rs b/rust/src/api/reputation.rs index 00de5402..ab4f3ddd 100644 --- a/rust/src/api/reputation.rs +++ b/rust/src/api/reputation.rs @@ -161,8 +161,8 @@ pub async fn submit_rating(trade_id: String, score: u8) -> Result<()> { let sender_keys = crate::api::identity::get_active_trade_keys(trade_index).await?; let mostro_pubkey = - nostr_sdk::PublicKey::from_hex(crate::config::DEFAULT_MOSTRO_PUBKEY) - .map_err(|e| anyhow::anyhow!("invalid DEFAULT_MOSTRO_PUBKEY: {e}"))?; + nostr_sdk::PublicKey::from_hex(&crate::config::active_mostro_pubkey()) + .map_err(|e| anyhow::anyhow!("invalid mostro pubkey: {e}"))?; let event_json = crate::mostro::actions::rate_user( &sender_keys, &mostro_pubkey, diff --git a/rust/src/api/settings.rs b/rust/src/api/settings.rs index 1ab569ca..d1d9414e 100644 --- a/rust/src/api/settings.rs +++ b/rust/src/api/settings.rs @@ -180,6 +180,26 @@ pub async fn set_default_lightning_address(address: Option) -> Result<() Ok(()) } +/// Set or clear the active Mostro node pubkey. +/// +/// Pass `Some("hex...")` to route orders to a custom Mostro node, or `None` +/// to revert to the compiled-in default. +/// +/// **Errors**: `InvalidPubkey` if `pubkey` is Some but not a valid 64-char hex key. +pub fn set_mostro_pubkey(pubkey: Option) -> Result<()> { + if let Some(ref pk) = pubkey { + nostr_sdk::PublicKey::from_hex(pk) + .map_err(|e| anyhow::anyhow!("InvalidPubkey: {e}"))?; + } + crate::config::set_active_mostro_pubkey(pubkey); + Ok(()) +} + +/// Return the currently active Mostro node pubkey (override or default). +pub fn get_mostro_pubkey() -> String { + crate::config::active_mostro_pubkey() +} + /// Toggle the in-memory logging flag (not persisted to disk). /// /// When a Tokio runtime is available the update is dispatched asynchronously diff --git a/rust/src/config.rs b/rust/src/config.rs index 443d9fbf..0af02992 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -3,6 +3,8 @@ //! These are compiled into the app and used on first launch when no //! user-configured relays or Mostro node exist in the database. +use std::sync::RwLock; + /// Default relay URLs seeded on first launch. pub const DEFAULT_RELAYS: &[&str] = &[ "wss://relay.mostro.network", @@ -15,3 +17,22 @@ pub const DEFAULT_MOSTRO_PUBKEY: &str = /// Default Mostro daemon display name. pub const DEFAULT_MOSTRO_NAME: &str = "Mostro"; + +// ── Runtime pubkey override ────────────────────────────────────────────────── + +static ACTIVE_MOSTRO_PUBKEY: RwLock> = RwLock::new(None); + +/// Returns the active Mostro pubkey — either the user-selected override or +/// the compiled-in default. +pub fn active_mostro_pubkey() -> String { + ACTIVE_MOSTRO_PUBKEY + .read() + .unwrap() + .clone() + .unwrap_or_else(|| DEFAULT_MOSTRO_PUBKEY.to_string()) +} + +/// Set (or clear) the active Mostro pubkey override. +pub fn set_active_mostro_pubkey(pubkey: Option) { + *ACTIVE_MOSTRO_PUBKEY.write().unwrap() = pubkey; +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index c5eba5c9..1b20885c 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -26,6 +26,7 @@ // Section: imports use crate::api::disputes::*; +use crate::api::logging::*; use crate::api::messages::*; use crate::api::nostr::*; use crate::api::nwc::*; @@ -44,7 +45,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1150097857; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -327029727; // Section: executor @@ -226,6 +227,63 @@ fn wire__crate__api__disputes__DisputeStream_next_impl( }, ) } +fn wire__crate__api__logging__LogEntryStream_next_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "LogEntryStream_next", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, true, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref_mut().await) + } + _ => unreachable!(), + } + } + let mut api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok( + crate::api::logging::LogEntryStream::next(&mut *api_that_guard).await, + )?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__messages__MessageStream_next_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1674,6 +1732,38 @@ fn wire__crate__api__messages__get_messages_impl( }, ) } +fn wire__crate__api__settings__get_mostro_pubkey_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_mostro_pubkey", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::settings::get_mostro_pubkey())?; + Ok(output_ok) + })()) + } + }, + ) +} fn wire__crate__api__identity__get_nym_identity_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -2369,6 +2459,40 @@ fn wire__crate__api__nostr__initialize_impl( }, ) } +fn wire__crate__api__logging__install_log_bridge_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "install_log_bridge", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok({ + crate::api::logging::install_log_bridge(); + })?; + Ok(output_ok) + })()) + } + }, + ) +} fn wire__crate__api__orders__list_trades_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -2632,6 +2756,38 @@ fn wire__crate__api__disputes__on_dispute_updated_impl( }, ) } +fn wire__crate__api__logging__on_log_entry_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "on_log_entry", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::api::logging::on_log_entry())?; + Ok(output_ok) + })()) + } + }, + ) +} fn wire__crate__api__messages__on_new_message_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -3402,6 +3558,41 @@ fn wire__crate__api__settings__set_logging_enabled_impl( }, ) } +fn wire__crate__api__settings__set_mostro_pubkey_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "set_mostro_pubkey", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_pubkey = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = crate::api::settings::set_mostro_pubkey(api_pubkey)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} fn wire__crate__api__reputation__set_privacy_mode_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -3638,6 +3829,9 @@ flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::for_generated::RustAutoOpaqueInner ); +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); flutter_rust_bridge::frb_generated_moi_arc_impl_value!( flutter_rust_bridge::for_generated::RustAutoOpaqueInner ); @@ -3703,6 +3897,16 @@ impl SseDecode for DisputeStream { } } +impl SseDecode for LogEntryStream { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + impl SseDecode for MessageStream { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -3817,6 +4021,16 @@ impl SseDecode } } +impl SseDecode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + impl SseDecode for RustOpaqueMoi> { @@ -4256,6 +4470,38 @@ impl SseDecode for Vec { } } +impl SseDecode for crate::api::types::LogEntry { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_id = ::sse_decode(deserializer); + let mut var_level = ::sse_decode(deserializer); + let mut var_tag = ::sse_decode(deserializer); + let mut var_message = ::sse_decode(deserializer); + let mut var_timestamp = ::sse_decode(deserializer); + return crate::api::types::LogEntry { + id: var_id, + level: var_level, + tag: var_tag, + message: var_message, + timestamp: var_timestamp, + }; + } +} + +impl SseDecode for crate::api::types::LogLevel { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::types::LogLevel::Debug, + 1 => crate::api::types::LogLevel::Info, + 2 => crate::api::types::LogLevel::Warning, + 3 => crate::api::types::LogLevel::Error, + _ => unreachable!("Invalid variant for LogLevel: {}", inner), + }; + } +} + impl SseDecode for crate::api::types::MessageType { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4458,6 +4704,17 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -4987,200 +5244,205 @@ fn pde_ffi_dispatcher_primary_impl( data_len, ), 3 => wire__crate__api__disputes__DisputeStream_next_impl(port, ptr, rust_vec_len, data_len), - 4 => wire__crate__api__messages__MessageStream_next_impl(port, ptr, rust_vec_len, data_len), - 5 => wire__crate__api__orders__OrderBook_default_impl(port, ptr, rust_vec_len, data_len), - 6 => wire__crate__api__orders__OrderBook_get_order_impl(port, ptr, rust_vec_len, data_len), - 7 => wire__crate__api__orders__OrderBook_get_orders_impl(port, ptr, rust_vec_len, data_len), - 8 => wire__crate__api__orders__OrderBook_new_impl(port, ptr, rust_vec_len, data_len), - 9 => { + 4 => wire__crate__api__logging__LogEntryStream_next_impl(port, ptr, rust_vec_len, data_len), + 5 => wire__crate__api__messages__MessageStream_next_impl(port, ptr, rust_vec_len, data_len), + 6 => wire__crate__api__orders__OrderBook_default_impl(port, ptr, rust_vec_len, data_len), + 7 => wire__crate__api__orders__OrderBook_get_order_impl(port, ptr, rust_vec_len, data_len), + 8 => wire__crate__api__orders__OrderBook_get_orders_impl(port, ptr, rust_vec_len, data_len), + 9 => wire__crate__api__orders__OrderBook_new_impl(port, ptr, rust_vec_len, data_len), + 10 => { wire__crate__api__orders__OrderBook_remove_order_impl(port, ptr, rust_vec_len, data_len) } - 10 => { + 11 => { wire__crate__api__orders__OrderBook_set_orders_impl(port, ptr, rust_vec_len, data_len) } - 11 => { + 12 => { wire__crate__api__orders__OrderBook_upsert_order_impl(port, ptr, rust_vec_len, data_len) } - 12 => wire__crate__api__orders__OrdersStream_next_impl(port, ptr, rust_vec_len, data_len), - 13 => { + 13 => wire__crate__api__orders__OrdersStream_next_impl(port, ptr, rust_vec_len, data_len), + 14 => { wire__crate__api__reputation__RatingStream_next_impl(port, ptr, rust_vec_len, data_len) } - 14 => { + 15 => { wire__crate__api__nostr__RelayStatusStream_next_impl(port, ptr, rust_vec_len, data_len) } - 15 => { + 16 => { wire__crate__api__settings__SettingsStream_next_impl(port, ptr, rust_vec_len, data_len) } - 16 => wire__crate__api__messages__UnreadCountStream_next_impl( + 17 => wire__crate__api__messages__UnreadCountStream_next_impl( port, ptr, rust_vec_len, data_len, ), - 17 => { + 18 => { wire__crate__api__nwc__WalletStatusStream_next_impl(port, ptr, rust_vec_len, data_len) } - 18 => wire__crate__api__nostr__add_relay_impl(port, ptr, rust_vec_len, data_len), - 19 => wire__crate__api__orders__cancel_order_impl(port, ptr, rust_vec_len, data_len), - 20 => wire__crate__api__nwc__connect_wallet_impl(port, ptr, rust_vec_len, data_len), - 21 => wire__crate__api__identity__create_identity_impl(port, ptr, rust_vec_len, data_len), - 22 => wire__crate__api__orders__create_order_impl(port, ptr, rust_vec_len, data_len), - 23 => wire__crate__api__identity__delete_identity_impl(port, ptr, rust_vec_len, data_len), - 24 => wire__crate__api__identity__derive_trade_key_impl(port, ptr, rust_vec_len, data_len), - 25 => wire__crate__api__nwc__disconnect_wallet_impl(port, ptr, rust_vec_len, data_len), - 26 => { + 19 => wire__crate__api__nostr__add_relay_impl(port, ptr, rust_vec_len, data_len), + 20 => wire__crate__api__orders__cancel_order_impl(port, ptr, rust_vec_len, data_len), + 21 => wire__crate__api__nwc__connect_wallet_impl(port, ptr, rust_vec_len, data_len), + 22 => wire__crate__api__identity__create_identity_impl(port, ptr, rust_vec_len, data_len), + 23 => wire__crate__api__orders__create_order_impl(port, ptr, rust_vec_len, data_len), + 24 => wire__crate__api__identity__delete_identity_impl(port, ptr, rust_vec_len, data_len), + 25 => wire__crate__api__identity__derive_trade_key_impl(port, ptr, rust_vec_len, data_len), + 26 => wire__crate__api__nwc__disconnect_wallet_impl(port, ptr, rust_vec_len, data_len), + 27 => { wire__crate__api__messages__download_attachment_impl(port, ptr, rust_vec_len, data_len) } - 27 => wire__crate__api__identity__export_encrypted_backup_impl( + 28 => wire__crate__api__identity__export_encrypted_backup_impl( port, ptr, rust_vec_len, data_len, ), - 28 => wire__crate__api__nostr__fetch_mostro_instance_tags_impl( + 29 => wire__crate__api__nostr__fetch_mostro_instance_tags_impl( port, ptr, rust_vec_len, data_len, ), - 29 => wire__crate__api__nostr__flush_message_queue_impl(port, ptr, rust_vec_len, data_len), - 30 => wire__crate__api__get_app_version_impl(port, ptr, rust_vec_len, data_len), - 31 => wire__crate__api__messages__get_attachment_status_impl( + 30 => wire__crate__api__nostr__flush_message_queue_impl(port, ptr, rust_vec_len, data_len), + 31 => wire__crate__api__get_app_version_impl(port, ptr, rust_vec_len, data_len), + 32 => wire__crate__api__messages__get_attachment_status_impl( port, ptr, rust_vec_len, data_len, ), - 32 => wire__crate__api__nwc__get_balance_impl(port, ptr, rust_vec_len, data_len), - 33 => wire__crate__api__nostr__get_connection_state_impl(port, ptr, rust_vec_len, data_len), - 34 => wire__crate__api__disputes__get_dispute_impl(port, ptr, rust_vec_len, data_len), - 35 => wire__crate__api__identity__get_identity_impl(port, ptr, rust_vec_len, data_len), - 36 => wire__crate__api__messages__get_messages_impl(port, ptr, rust_vec_len, data_len), - 37 => wire__crate__api__identity__get_nym_identity_impl(port, ptr, rust_vec_len, data_len), - 38 => wire__crate__api__orders__get_order_impl(port, ptr, rust_vec_len, data_len), - 39 => wire__crate__api__orders__get_orders_impl(port, ptr, rust_vec_len, data_len), - 40 => { + 33 => wire__crate__api__nwc__get_balance_impl(port, ptr, rust_vec_len, data_len), + 34 => wire__crate__api__nostr__get_connection_state_impl(port, ptr, rust_vec_len, data_len), + 35 => wire__crate__api__disputes__get_dispute_impl(port, ptr, rust_vec_len, data_len), + 36 => wire__crate__api__identity__get_identity_impl(port, ptr, rust_vec_len, data_len), + 37 => wire__crate__api__messages__get_messages_impl(port, ptr, rust_vec_len, data_len), + 38 => wire__crate__api__settings__get_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), + 39 => wire__crate__api__identity__get_nym_identity_impl(port, ptr, rust_vec_len, data_len), + 40 => wire__crate__api__orders__get_order_impl(port, ptr, rust_vec_len, data_len), + 41 => wire__crate__api__orders__get_orders_impl(port, ptr, rust_vec_len, data_len), + 42 => { wire__crate__api__reputation__get_privacy_mode_impl(port, ptr, rust_vec_len, data_len) } - 41 => wire__crate__api__reputation__get_rating_for_trade_impl( + 43 => wire__crate__api__reputation__get_rating_for_trade_impl( port, ptr, rust_vec_len, data_len, ), - 42 => wire__crate__api__nostr__get_relays_impl(port, ptr, rust_vec_len, data_len), - 43 => wire__crate__api__settings__get_settings_impl(port, ptr, rust_vec_len, data_len), - 44 => wire__crate__api__identity__get_trade_key_impl(port, ptr, rust_vec_len, data_len), - 45 => wire__crate__api__orders__get_trade_role_impl(port, ptr, rust_vec_len, data_len), - 46 => wire__crate__api__messages__get_unread_count_impl(port, ptr, rust_vec_len, data_len), - 47 => wire__crate__api__nwc__get_wallet_impl(port, ptr, rust_vec_len, data_len), - 48 => wire__crate__api__disputes__handle_admin_canceled_impl( + 44 => wire__crate__api__nostr__get_relays_impl(port, ptr, rust_vec_len, data_len), + 45 => wire__crate__api__settings__get_settings_impl(port, ptr, rust_vec_len, data_len), + 46 => wire__crate__api__identity__get_trade_key_impl(port, ptr, rust_vec_len, data_len), + 47 => wire__crate__api__orders__get_trade_role_impl(port, ptr, rust_vec_len, data_len), + 48 => wire__crate__api__messages__get_unread_count_impl(port, ptr, rust_vec_len, data_len), + 49 => wire__crate__api__nwc__get_wallet_impl(port, ptr, rust_vec_len, data_len), + 50 => wire__crate__api__disputes__handle_admin_canceled_impl( port, ptr, rust_vec_len, data_len, ), - 49 => { + 51 => { wire__crate__api__disputes__handle_admin_settled_impl(port, ptr, rust_vec_len, data_len) } - 50 => wire__crate__api__disputes__handle_admin_took_dispute_impl( + 52 => wire__crate__api__disputes__handle_admin_took_dispute_impl( port, ptr, rust_vec_len, data_len, ), - 51 => wire__crate__api__reputation__handle_rating_received_impl( + 53 => wire__crate__api__reputation__handle_rating_received_impl( port, ptr, rust_vec_len, data_len, ), - 52 => { + 54 => { wire__crate__api__identity__import_from_mnemonic_impl(port, ptr, rust_vec_len, data_len) } - 53 => wire__crate__api__identity__import_from_nsec_impl(port, ptr, rust_vec_len, data_len), - 54 => wire__crate__api__init_db_impl(port, ptr, rust_vec_len, data_len), - 55 => wire__crate__api__nostr__initialize_impl(port, ptr, rust_vec_len, data_len), - 56 => wire__crate__api__orders__list_trades_impl(port, ptr, rust_vec_len, data_len), - 57 => wire__crate__api__identity__load_identity_from_mnemonic_impl( + 55 => wire__crate__api__identity__import_from_nsec_impl(port, ptr, rust_vec_len, data_len), + 56 => wire__crate__api__init_db_impl(port, ptr, rust_vec_len, data_len), + 57 => wire__crate__api__nostr__initialize_impl(port, ptr, rust_vec_len, data_len), + 58 => wire__crate__api__logging__install_log_bridge_impl(port, ptr, rust_vec_len, data_len), + 59 => wire__crate__api__orders__list_trades_impl(port, ptr, rust_vec_len, data_len), + 60 => wire__crate__api__identity__load_identity_from_mnemonic_impl( port, ptr, rust_vec_len, data_len, ), - 58 => wire__crate__api__nwc__make_invoice_impl(port, ptr, rust_vec_len, data_len), - 59 => wire__crate__api__messages__mark_as_read_impl(port, ptr, rust_vec_len, data_len), - 60 => wire__crate__api__messages__on_attachment_progress_impl( + 61 => wire__crate__api__nwc__make_invoice_impl(port, ptr, rust_vec_len, data_len), + 62 => wire__crate__api__messages__mark_as_read_impl(port, ptr, rust_vec_len, data_len), + 63 => wire__crate__api__messages__on_attachment_progress_impl( port, ptr, rust_vec_len, data_len, ), - 61 => wire__crate__api__nostr__on_connection_state_changed_impl( + 64 => wire__crate__api__nostr__on_connection_state_changed_impl( port, ptr, rust_vec_len, data_len, ), - 62 => { + 65 => { wire__crate__api__disputes__on_dispute_updated_impl(port, ptr, rust_vec_len, data_len) } - 63 => wire__crate__api__messages__on_new_message_impl(port, ptr, rust_vec_len, data_len), - 64 => wire__crate__api__orders__on_orders_updated_impl(port, ptr, rust_vec_len, data_len), - 65 => { + 66 => wire__crate__api__logging__on_log_entry_impl(port, ptr, rust_vec_len, data_len), + 67 => wire__crate__api__messages__on_new_message_impl(port, ptr, rust_vec_len, data_len), + 68 => wire__crate__api__orders__on_orders_updated_impl(port, ptr, rust_vec_len, data_len), + 69 => { wire__crate__api__reputation__on_rating_received_impl(port, ptr, rust_vec_len, data_len) } - 66 => { + 70 => { wire__crate__api__nostr__on_relay_status_changed_impl(port, ptr, rust_vec_len, data_len) } - 67 => { + 71 => { wire__crate__api__settings__on_settings_changed_impl(port, ptr, rust_vec_len, data_len) } - 68 => wire__crate__api__messages__on_unread_count_changed_impl( + 72 => wire__crate__api__messages__on_unread_count_changed_impl( port, ptr, rust_vec_len, data_len, ), - 69 => { + 73 => { wire__crate__api__nwc__on_wallet_status_changed_impl(port, ptr, rust_vec_len, data_len) } - 70 => wire__crate__api__disputes__open_dispute_impl(port, ptr, rust_vec_len, data_len), - 71 => { + 74 => wire__crate__api__disputes__open_dispute_impl(port, ptr, rust_vec_len, data_len), + 75 => { wire__crate__api__orders__order_filters_default_impl(port, ptr, rust_vec_len, data_len) } - 72 => wire__crate__api__nwc__pay_invoice_impl(port, ptr, rust_vec_len, data_len), - 73 => wire__crate__api__orders__release_order_impl(port, ptr, rust_vec_len, data_len), - 74 => wire__crate__api__nostr__remove_relay_impl(port, ptr, rust_vec_len, data_len), - 75 => wire__crate__api__orders__resolve_maker_order_impl(port, ptr, rust_vec_len, data_len), - 76 => wire__crate__api__orders__send_fiat_sent_impl(port, ptr, rust_vec_len, data_len), - 77 => wire__crate__api__messages__send_file_impl(port, ptr, rust_vec_len, data_len), - 78 => wire__crate__api__orders__send_invoice_impl(port, ptr, rust_vec_len, data_len), - 79 => wire__crate__api__messages__send_message_impl(port, ptr, rust_vec_len, data_len), - 80 => wire__crate__api__settings__set_default_fiat_code_impl( + 76 => wire__crate__api__nwc__pay_invoice_impl(port, ptr, rust_vec_len, data_len), + 77 => wire__crate__api__orders__release_order_impl(port, ptr, rust_vec_len, data_len), + 78 => wire__crate__api__nostr__remove_relay_impl(port, ptr, rust_vec_len, data_len), + 79 => wire__crate__api__orders__resolve_maker_order_impl(port, ptr, rust_vec_len, data_len), + 80 => wire__crate__api__orders__send_fiat_sent_impl(port, ptr, rust_vec_len, data_len), + 81 => wire__crate__api__messages__send_file_impl(port, ptr, rust_vec_len, data_len), + 82 => wire__crate__api__orders__send_invoice_impl(port, ptr, rust_vec_len, data_len), + 83 => wire__crate__api__messages__send_message_impl(port, ptr, rust_vec_len, data_len), + 84 => wire__crate__api__settings__set_default_fiat_code_impl( port, ptr, rust_vec_len, data_len, ), - 81 => wire__crate__api__settings__set_default_lightning_address_impl( + 85 => wire__crate__api__settings__set_default_lightning_address_impl( port, ptr, rust_vec_len, data_len, ), - 82 => wire__crate__api__settings__set_language_impl(port, ptr, rust_vec_len, data_len), - 83 => { + 86 => wire__crate__api__settings__set_language_impl(port, ptr, rust_vec_len, data_len), + 87 => { wire__crate__api__settings__set_logging_enabled_impl(port, ptr, rust_vec_len, data_len) } - 84 => { + 88 => wire__crate__api__settings__set_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), + 89 => { wire__crate__api__reputation__set_privacy_mode_impl(port, ptr, rust_vec_len, data_len) } - 85 => wire__crate__api__settings__set_theme_impl(port, ptr, rust_vec_len, data_len), - 86 => wire__crate__api__disputes__submit_evidence_impl(port, ptr, rust_vec_len, data_len), - 87 => wire__crate__api__reputation__submit_rating_impl(port, ptr, rust_vec_len, data_len), - 88 => wire__crate__api__orders__subscribe_orders_impl(port, ptr, rust_vec_len, data_len), - 89 => wire__crate__api__orders__take_order_impl(port, ptr, rust_vec_len, data_len), + 90 => wire__crate__api__settings__set_theme_impl(port, ptr, rust_vec_len, data_len), + 91 => wire__crate__api__disputes__submit_evidence_impl(port, ptr, rust_vec_len, data_len), + 92 => wire__crate__api__reputation__submit_rating_impl(port, ptr, rust_vec_len, data_len), + 93 => wire__crate__api__orders__subscribe_orders_impl(port, ptr, rust_vec_len, data_len), + 94 => wire__crate__api__orders__take_order_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -5254,6 +5516,21 @@ impl flutter_rust_bridge::IntoIntoDart> for DisputeStr } } +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for LogEntryStream { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + // Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for FrbWrapper { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { @@ -5701,6 +5978,47 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::types::LogEntry { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.id.into_into_dart().into_dart(), + self.level.into_into_dart().into_dart(), + self.tag.into_into_dart().into_dart(), + self.message.into_into_dart().into_dart(), + self.timestamp.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::types::LogEntry {} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::types::LogEntry +{ + fn into_into_dart(self) -> crate::api::types::LogEntry { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::types::LogLevel { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Debug => 0.into_dart(), + Self::Info => 1.into_dart(), + Self::Warning => 2.into_dart(), + Self::Error => 3.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::api::types::LogLevel {} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::types::LogLevel +{ + fn into_into_dart(self) -> crate::api::types::LogLevel { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::types::MessageType { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { @@ -6253,6 +6571,13 @@ impl SseEncode for DisputeStream { } } +impl SseEncode for LogEntryStream { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + impl SseEncode for MessageStream { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6346,6 +6671,17 @@ impl SseEncode } } +impl SseEncode + for RustOpaqueMoi> +{ + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + impl SseEncode for RustOpaqueMoi> { @@ -6747,6 +7083,35 @@ impl SseEncode for Vec { } } +impl SseEncode for crate::api::types::LogEntry { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.id, serializer); + ::sse_encode(self.level, serializer); + ::sse_encode(self.tag, serializer); + ::sse_encode(self.message, serializer); + ::sse_encode(self.timestamp, serializer); + } +} + +impl SseEncode for crate::api::types::LogLevel { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::types::LogLevel::Debug => 0, + crate::api::types::LogLevel::Info => 1, + crate::api::types::LogLevel::Warning => 2, + crate::api::types::LogLevel::Error => 3, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + impl SseEncode for crate::api::types::MessageType { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -6909,6 +7274,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -7384,6 +7759,7 @@ mod io { use super::*; use crate::api::disputes::*; + use crate::api::logging::*; use crate::api::messages::*; use crate::api::nostr::*; use crate::api::nwc::*; @@ -7442,6 +7818,20 @@ mod io { MoiArc::>::decrement_strong_count(ptr as _); } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_mostro_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLogEntryStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_mostro_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLogEntryStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_mostro_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMessageStream( ptr: *const std::ffi::c_void, @@ -7567,6 +7957,7 @@ mod web { use super::*; use crate::api::disputes::*; + use crate::api::logging::*; use crate::api::messages::*; use crate::api::nostr::*; use crate::api::nwc::*; @@ -7627,6 +8018,20 @@ mod web { MoiArc::>::decrement_strong_count(ptr as _); } + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLogEntryStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerLogEntryStream( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } + #[wasm_bindgen] pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerMessageStream( ptr: *const std::ffi::c_void, diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f2ca41b8..d8f8a6a4 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -14,15 +14,15 @@ pub mod nwc; pub mod queue; /// Called once by Flutter during `RustLib.init()` — sets up logging so Rust -/// messages appear in `adb logcat` under the tag `mostro_rust`. +/// messages appear in `adb logcat` / stderr and are forwarded to the Flutter +/// log stream via `BridgeLogger`. #[flutter_rust_bridge::frb(init)] pub fn init_app() { - #[cfg(target_os = "android")] - android_logger::init_once( - android_logger::Config::default() - .with_max_level(log::LevelFilter::Debug) - .with_tag("mostro_rust"), - ); + // Install the log bridge so every log::info!/warn!/error! is forwarded + // to the Flutter on_log_entry() stream AND printed to stderr (logcat on + // Android). Must be called before any other log::set_logger() call. + api::logging::install_log_bridge(); + log::info!("[init] Rust core initialized"); } diff --git a/rust/src/nostr/relay_pool.rs b/rust/src/nostr/relay_pool.rs index d3a8e29b..c702c758 100644 --- a/rust/src/nostr/relay_pool.rs +++ b/rust/src/nostr/relay_pool.rs @@ -154,8 +154,8 @@ impl RelayPool { &self, trade_keys: Vec<(PublicKey, u32)>, ) -> Result<()> { - let mostro_pubkey = nostr_sdk::PublicKey::from_hex(crate::config::DEFAULT_MOSTRO_PUBKEY) - .map_err(|e| anyhow!("invalid DEFAULT_MOSTRO_PUBKEY: {e}"))?; + let mostro_pubkey = nostr_sdk::PublicKey::from_hex(&crate::config::active_mostro_pubkey()) + .map_err(|e| anyhow!("invalid mostro pubkey: {e}"))?; let order_filter = pending_orders_filter(&mostro_pubkey); self.client .subscribe(order_filter, None)