diff --git a/lib/features/home/providers/home_order_providers.dart b/lib/features/home/providers/home_order_providers.dart index fe4dfa87..c61f4eb8 100644 --- a/lib/features/home/providers/home_order_providers.dart +++ b/lib/features/home/providers/home_order_providers.dart @@ -157,7 +157,6 @@ final orderBookProvider = StreamProvider.autoDispose>((ref) asyn while (true) { final orders = await stream.next(); if (orders == null) break; - debugPrint('[orderBook] update: ${orders.length} orders'); yield orders.map(OrderItem.fromInfo).toList(); } }); diff --git a/lib/features/order/screens/add_order_screen.dart b/lib/features/order/screens/add_order_screen.dart index 772c7aaf..7afc5ffe 100644 --- a/lib/features/order/screens/add_order_screen.dart +++ b/lib/features/order/screens/add_order_screen.dart @@ -8,8 +8,10 @@ import 'package:mostro/features/order/widgets/currency_section.dart'; import 'package:mostro/features/settings/providers/settings_provider.dart'; import 'package:mostro/features/order/widgets/payment_method_section.dart'; import 'package:mostro/features/order/widgets/price_section.dart'; +import 'package:mostro/core/services/identity_service.dart'; import 'package:mostro/features/trades/providers/trades_providers.dart' show refreshTrades; +import 'package:mostro/src/rust/api/identity.dart' as identity_api; import 'package:mostro/src/rust/api/orders.dart' as rust_orders; import 'package:mostro/src/rust/api/types.dart'; @@ -125,14 +127,31 @@ class _AddOrderScreenState extends ConsumerState { ); await rust_orders.createOrder(params: params); + + // Persist the updated trade key index so it survives app restarts. + // Failures here are non-fatal — the order was already created. + try { + final identity = await identity_api.getIdentity(); + if (identity != null) { + await IdentityService.saveTradeKeyIndex(identity.tradeKeyIndex); + } + } catch (e) { + debugPrint('[orders] save tradeKeyIndex failed: $e'); + } + refreshTrades(ref); if (!mounted) return; context.go(AppRoute.orderBook); } catch (e) { if (!mounted) return; + // CantDo rejections from Mostro arrive as errors from createOrder. + // Strip the Rust error prefix for a cleaner message. + final raw = e.toString(); + final anyhowMatch = RegExp(r'^.*?AnyhowException\((.+)\)$').firstMatch(raw); + final msg = anyhowMatch != null ? anyhowMatch.group(1)! : raw; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to create order: $e')), + SnackBar(content: Text(msg)), ); } finally { if (mounted) setState(() => _submitting = false); diff --git a/lib/features/trades/providers/trades_providers.dart b/lib/features/trades/providers/trades_providers.dart index b40db68a..69b19ba7 100644 --- a/lib/features/trades/providers/trades_providers.dart +++ b/lib/features/trades/providers/trades_providers.dart @@ -12,6 +12,8 @@ import 'package:mostro/src/rust/api/types.dart' as rust_types; enum TradeStatusFilter { all('All'), pending('Pending'), + waitingInvoice('Waiting Invoice'), + waitingPayment('Waiting Payment'), active('Active'), fiatSent('Fiat Sent'), success('Success'), @@ -75,8 +77,8 @@ class TradeListItem { TradeStatusFilter orderStatusToFilter(rust_types.OrderStatus status) { return switch (status) { rust_types.OrderStatus.pending => TradeStatusFilter.pending, - rust_types.OrderStatus.waitingBuyerInvoice => TradeStatusFilter.pending, - rust_types.OrderStatus.waitingPayment => TradeStatusFilter.pending, + rust_types.OrderStatus.waitingBuyerInvoice => TradeStatusFilter.waitingInvoice, + rust_types.OrderStatus.waitingPayment => TradeStatusFilter.waitingPayment, rust_types.OrderStatus.active => TradeStatusFilter.active, rust_types.OrderStatus.inProgress => TradeStatusFilter.active, rust_types.OrderStatus.fiatSent => TradeStatusFilter.fiatSent, diff --git a/lib/features/trades/widgets/trades_list_item.dart b/lib/features/trades/widgets/trades_list_item.dart index dc0a4706..2ee06fa6 100644 --- a/lib/features/trades/widgets/trades_list_item.dart +++ b/lib/features/trades/widgets/trades_list_item.dart @@ -158,6 +158,8 @@ class TradesListItem extends ConsumerWidget { static (Color, Color) _statusColors(TradeStatusFilter status) { return switch (status) { TradeStatusFilter.pending => AppColors.statusPending, + TradeStatusFilter.waitingInvoice => AppColors.statusWaiting, + TradeStatusFilter.waitingPayment => AppColors.statusWaiting, TradeStatusFilter.active => AppColors.statusActive, TradeStatusFilter.fiatSent => AppColors.statusActive, TradeStatusFilter.success => AppColors.statusSuccess, diff --git a/lib/main.dart b/lib/main.dart index c3d01182..baca8069 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'package:mostro/src/rust/frb_generated.dart'; import 'package:mostro/src/rust/api.dart' as rust_api; import 'package:mostro/features/settings/providers/nwc_provider.dart'; import 'package:mostro/src/rust/api/nwc.dart' as nwc_api; +import 'package:mostro/src/rust/api/logging.dart' as logging_api; import 'package:mostro/src/rust/api/nostr.dart' as nostr_api; import 'package:mostro/src/rust/api/orders.dart' as orders_api; @@ -76,6 +77,9 @@ Future main() async { // Watch for connection state changes in background (logs appear in flutter output). _watchConnectionState(); + // Forward Rust log entries to debugPrint so they appear in `flutter run`. + _forwardRustLogs(); + final container = ProviderContainer( overrides: [ firstRunProvider.overrideWith( @@ -125,6 +129,31 @@ void _restoreNwcConnection(String nwcUri, ProviderContainer container) { }); } +/// Forward Rust log entries to debugPrint so they are visible in `flutter run`. +/// +/// Only active in debug builds. +void _forwardRustLogs() { + if (!kDebugMode) return; + debugPrint('[rust-log] starting Rust log forwarder...'); + Future.microtask(() async { + try { + debugPrint('[rust-log] subscribing to Rust log stream...'); + final stream = await logging_api.onLogEntry(); + debugPrint('[rust-log] subscribed — waiting for entries'); + while (true) { + final entry = await stream.next(); + if (entry == null) { + debugPrint('[rust-log] stream closed'); + break; + } + debugPrint('[rust/${entry.tag}] ${entry.message}'); + } + } catch (e, st) { + debugPrint('[rust-log] bridge error: $e\n$st'); + } + }); +} + /// Guards against overlapping diagnostic order polls on rapid reconnects. bool _isPollingOrders = false; diff --git a/rust/src/api/logging.rs b/rust/src/api/logging.rs index 2ba84264..47b2e752 100644 --- a/rust/src/api/logging.rs +++ b/rust/src/api/logging.rs @@ -53,8 +53,16 @@ pub fn install_log_bridge() { // 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); + // If a dependency already set a logger, this fails silently — + // the eprintln! fallback in BridgeLogger::log still works, but the + // Flutter stream won't receive entries via the log crate. + match log::set_logger(&BRIDGE_LOGGER) { + Ok(()) => log::set_max_level(log::LevelFilter::Debug), + Err(e) => eprintln!( + "[logging] WARN: set_logger failed ({e}), another logger is already active. \ + Using direct bridge for Flutter stream." + ), + } }); } @@ -113,6 +121,21 @@ pub(crate) fn forward_log(level: log::Level, target: &str, message: &str) { } } +/// Send a log entry directly to the Flutter stream, bypassing the `log` crate. +/// +/// Use this when the `log` crate logger may have been hijacked by a dependency. +/// The entry is also printed to stderr for terminal visibility. +pub(crate) fn bridge_log(level: log::Level, tag: &str, message: &str) { + eprintln!("[{level}] {tag}: {message}"); + forward_log(level, tag, message); +} + +/// Shorthand helpers — always reach both stderr and the Flutter log stream. +pub(crate) fn blog_info(tag: &str, msg: String) { bridge_log(log::Level::Info, tag, &msg); } +pub(crate) fn blog_warn(tag: &str, msg: String) { bridge_log(log::Level::Warn, tag, &msg); } +pub(crate) fn blog_debug(tag: &str, msg: String) { bridge_log(log::Level::Debug, tag, &msg); } + + // ── FRB stream ─────────────────────────────────────────────────────────────── /// Stream of log entries for consumption by Flutter. diff --git a/rust/src/api/orders.rs b/rust/src/api/orders.rs index e871956a..632c92c8 100644 --- a/rust/src/api/orders.rs +++ b/rust/src/api/orders.rs @@ -47,6 +47,23 @@ fn store_pending_maker_key(trade_pubkey_hex: &str, index: u32) { } } +// ── Daemon confirmation channel for create_order ───────────────────────────── + +/// Result sent by the gift-wrap handler to the waiting `create_order` call. +enum DaemonConfirmation { + /// Daemon accepted the order and assigned a UUID. + Confirmed { daemon_id: String }, + /// Daemon rejected the order with a CantDo reason. + Rejected { reason: String, message: String }, +} + +/// Maps `trade_pubkey_hex` → oneshot sender for the pending `create_order` call. +static PENDING_CONFIRMATIONS: OnceLock>>> = OnceLock::new(); + +fn pending_confirmations() -> &'static std::sync::Mutex>> { + PENDING_CONFIRMATIONS.get_or_init(|| std::sync::Mutex::new(HashMap::new())) +} + /// Maps `content_key` → `local_uuid` for newly created maker orders. /// /// At create time the order is added to the order book with a locally-generated @@ -232,6 +249,19 @@ impl OrderBook { let _ = self.tx.send(snapshot); } + /// Update the status of an existing cached order and notify listeners. + /// + /// No-op when the order is not in the cache (e.g. already removed). + pub async fn update_order_status(&self, order_id: &str, status: OrderStatus) { + let mut orders = self.orders.write().await; + if let Some(existing) = orders.iter_mut().find(|o| o.id == order_id) { + existing.status = status; + let snapshot = orders.clone(); + drop(orders); + let _ = self.tx.send(snapshot); + } + } + /// Get all cached orders, optionally filtered. pub async fn get_orders(&self, filters: Option) -> Vec { // Clone + filter under the read lock, then drop it before sorting. @@ -309,6 +339,31 @@ impl OrderBook { use tokio::sync::OnceCell; +// ── Gift-wrap deduplication ────────────────────────────────────────────────── + +/// Tracks recently processed gift-wrap event IDs to avoid duplicate processing +/// when both the per-trade and global subscriptions receive the same event. +static PROCESSED_GW: OnceLock>> = OnceLock::new(); + +/// Returns `true` if this event ID was already processed (duplicate). +/// Otherwise records it and returns `false`. +fn is_duplicate_gift_wrap(event_id: &str) -> bool { + const MAX_ENTRIES: usize = 128; + let deque = PROCESSED_GW.get_or_init(|| std::sync::Mutex::new(std::collections::VecDeque::new())); + let mut guard = match deque.lock() { + Ok(g) => g, + Err(_) => return false, + }; + if guard.iter().any(|id| id == event_id) { + return true; + } + guard.push_back(event_id.to_string()); + if guard.len() > MAX_ENTRIES { + guard.pop_front(); + } + false +} + static ORDER_BOOK: OnceCell = OnceCell::const_new(); fn order_book() -> &'static OrderBook { @@ -384,7 +439,7 @@ pub async fn create_order(params: NewOrderParams) -> Result { // Clone params before the struct takes ownership of its fields. let params_for_dispatch = params.clone(); - let order = OrderInfo { + let mut order = OrderInfo { id: uuid::Uuid::new_v4().to_string(), kind: params.kind, status: OrderStatus::Pending, @@ -407,11 +462,6 @@ pub async fn create_order(params: NewOrderParams) -> Result { let trade_index = trade_key_info.index; let sender_keys = crate::api::identity::get_active_trade_keys(trade_index).await?; - // Subscribe to gift-wrap (Kind 1059) responses from the daemon addressed to - // this trade key. This gives us the daemon-assigned order UUID faster and - // more reliably than waiting for the Kind 38383 content-fingerprint match. - subscribe_gift_wraps(sender_keys.public_key(), trade_index).await; - // Build the content fingerprint key BEFORE publishing so the subscription // loop never races against an empty TRADE_KEY_MAP when the daemon replies // faster than our post-publish bookkeeping runs. @@ -430,9 +480,12 @@ pub async fn create_order(params: NewOrderParams) -> Result { // the keys are written and miss the fingerprint match entirely. store_trade_key_index(&order.id, trade_index).await; // local UUID fallback store_trade_key_index(&ck, trade_index).await; // content fingerprint - store_pending_maker_key(&sender_keys.public_key().to_hex(), trade_index); + let trade_pk_hex = sender_keys.public_key().to_hex(); + store_pending_maker_key(&trade_pk_hex, trade_index); store_pending_local_id(&ck, &order.id); - order_book().upsert_order(order.clone()).await; + + // DO NOT add to order book or DB yet — wait for daemon confirmation first. + // This avoids a phantom "pending" order when the daemon rejects (CantDo). let mostro_pubkey = nostr_sdk::PublicKey::from_hex(&active_mostro_pubkey())?; let event_json = actions::new_order( @@ -442,10 +495,72 @@ pub async fn create_order(params: NewOrderParams) -> Result { trade_index, ) .await?; - publish_event_json(&event_json).await?; - // Persist a TradeInfo for the maker so the order appears in My Trades - // immediately after creation with status Pending / OrderPublished. + // Set up the confirmation channel AFTER building the event but BEFORE + // publishing, so the entry is in the map before any response can arrive. + let (conf_tx, conf_rx) = tokio::sync::oneshot::channel::(); + if let Ok(mut map) = pending_confirmations().lock() { + map.insert(trade_pk_hex.clone(), conf_tx); + } + + // Subscribe to gift-wrap responses AFTER registering the confirmation + // channel so that any events (including stale ones replayed by relays) + // find the entry and notify us instead of being silently discarded. + subscribe_gift_wraps(sender_keys.public_key(), trade_index).await; + + if let Err(e) = publish_event_json(&event_json).await { + // Rollback all in-memory bookkeeping on publish failure. + if let Ok(mut m) = trade_key_map().write() { m.remove(&order.id); m.remove(&ck); } + if let Ok(mut m) = pending_maker_keys().write() { m.remove(&trade_pk_hex); } + if let Ok(mut m) = pending_local_ids().write() { m.remove(&ck); } + if let Ok(mut m) = pending_confirmations().lock() { m.remove(&trade_pk_hex); } + return Err(e.into()); + } + + crate::api::logging::blog_info("orders", format!( + "create_order published id={} trade_index={trade_index} — waiting for daemon", + order.id + )); + + // Wait for daemon confirmation. The daemon typically responds within 1s. + // The 5s timeout is a safety net for network issues. + let confirmation = tokio::time::timeout( + std::time::Duration::from_secs(5), + conf_rx, + ).await; + + // Clean up the pending entry regardless of outcome. + if let Ok(mut map) = pending_confirmations().lock() { + map.remove(&trade_pk_hex); + } + + // Determine the final order ID (daemon UUID or local fallback). + let final_order_id = match confirmation { + Ok(Ok(DaemonConfirmation::Confirmed { daemon_id })) => { + crate::api::logging::blog_info("orders", format!( + "create_order confirmed by daemon: {daemon_id}" + )); + daemon_id + } + Ok(Ok(DaemonConfirmation::Rejected { reason, message })) => { + crate::api::logging::blog_warn("orders", format!( + "create_order rejected: {reason} — {message}" + )); + return Err(anyhow::anyhow!("{message}")); + } + _ => { + // Timeout — optimistic: use local UUID and add to book/DB. + crate::api::logging::blog_info("orders", format!( + "create_order: no daemon response within 5s, using local id={}", order.id + )); + order.id.clone() + } + }; + + // Order confirmed (or timeout) — now create the local state. + order.id = final_order_id.clone(); + order_book().upsert_order(order.clone()).await; + let maker_role = match order.kind { OrderKind::Sell => crate::api::types::TradeRole::Seller, OrderKind::Buy => crate::api::types::TradeRole::Buyer, @@ -479,11 +594,6 @@ pub async fn create_order(params: NewOrderParams) -> Result { } } - log::info!( - "[orders] create_order dispatched id={} trade_index={trade_index} ck={ck}", - order.id - ); - Ok(order) } @@ -780,39 +890,46 @@ pub async fn cancel_order(order_id: String) -> Result<()> { /// - `Action::NewOrder` — daemon confirmed the order; bridges daemon UUID into /// `TRADE_KEY_MAP` via `resolve_maker_order`. /// - All other actions are logged (full trade-session routing is Phase 7+). +/// +/// The relay subscription is established synchronously (awaited) before returning, +/// then the event loop is spawned as a background task. This guarantees the +/// subscription is active before the caller publishes the order event. pub(crate) async fn subscribe_gift_wraps(trade_pubkey: nostr_sdk::PublicKey, trade_index: u32) { - tokio::spawn(async move { - // Fetch keys first — if this fails there is no point subscribing and - // we avoid leaving an orphan relay subscription with no decryption path. - let recipient_keys = match crate::api::identity::get_active_trade_keys(trade_index).await { - Ok(k) => k, - Err(e) => { - log::error!("[orders] subscribe_gift_wraps: no trade keys: {e}"); - return; - } - }; - - let Ok(pool) = crate::api::nostr::get_pool() else { - log::warn!("[orders] subscribe_gift_wraps: relay pool not initialized"); + // ── Synchronous setup: awaited by the caller ── + let recipient_keys = match crate::api::identity::get_active_trade_keys(trade_index).await { + Ok(k) => k, + Err(e) => { + log::error!("[orders] subscribe_gift_wraps: no trade keys: {e}"); return; - }; - let client = pool.client(); + } + }; - // Obtain the notifications receiver BEFORE subscribing to avoid a - // window where daemon responses arrive but aren't captured. - let mut rx = client.notifications(); + let Ok(pool) = crate::api::nostr::get_pool() else { + log::warn!("[orders] subscribe_gift_wraps: relay pool not initialized"); + return; + }; + let client = pool.client(); - let filter = nostr_sdk::Filter::new() - .kind(nostr_sdk::Kind::from(1059u16)) - .pubkey(trade_pubkey); - if let Err(e) = client.subscribe(filter, None).await { - log::warn!("[orders] subscribe_gift_wraps subscribe failed: {e}"); - return; - } + // Obtain the notifications receiver BEFORE subscribing to avoid a + // window where daemon responses arrive but aren't captured. + let mut rx = client.notifications(); - let trade_pubkey_hex = trade_pubkey.to_hex(); - log::info!("[orders] gift-wrap subscription active for trade_pubkey={trade_pubkey_hex}"); + let filter = nostr_sdk::Filter::new() + .kind(nostr_sdk::Kind::from(1059u16)) + .pubkey(trade_pubkey); + if let Err(e) = client.subscribe(filter, None).await { + log::warn!("[orders] subscribe_gift_wraps subscribe failed: {e}"); + return; + } + let trade_pubkey_hex = trade_pubkey.to_hex(); + crate::api::logging::blog_info("orders", format!( + "gift-wrap subscription active for trade={}", + &trade_pubkey_hex[..8] + )); + + // ── Event loop: spawned as a background task ── + tokio::spawn(async move { use nostr_sdk::RelayPoolNotification; use tokio::time::{timeout, Duration}; @@ -823,7 +940,6 @@ pub(crate) async fn subscribe_gift_wraps(trade_pubkey: nostr_sdk::PublicKey, tra let remaining = Duration::from_secs(IDLE_TIMEOUT_SECS).saturating_sub(last_activity.elapsed()); if remaining.is_zero() { - log::debug!("[orders] gift-wrap idle timeout for trade={trade_pubkey_hex}"); break; } @@ -832,7 +948,6 @@ pub(crate) async fn subscribe_gift_wraps(trade_pubkey: nostr_sdk::PublicKey, tra if event.kind != nostr_sdk::Kind::from(1059u16) { continue; } - // Only process events addressed to our trade pubkey. let is_for_us = event.tags.iter().any(|t| { let s = t.as_slice(); s.first().map(|v| v.as_str()) == Some("p") @@ -842,10 +957,20 @@ pub(crate) async fn subscribe_gift_wraps(trade_pubkey: nostr_sdk::PublicKey, tra continue; } + let eid = event.id.to_hex(); + if is_duplicate_gift_wrap(&eid) { + continue; + } + crate::api::logging::blog_info("gift-wrap", format!( + "Kind 1059 received (per-trade) for trade={} from={} event_id={}", + &trade_pubkey_hex[..8], + &event.pubkey.to_hex()[..8], + &eid[..16], + )); let event_json = match serde_json::to_string(&*event) { Ok(j) => j, Err(e) => { - log::warn!("[orders] gift-wrap event serialize failed: {e}"); + crate::api::logging::blog_warn("gift-wrap", format!("event serialize failed: {e}")); continue; } }; @@ -854,7 +979,9 @@ pub(crate) async fn subscribe_gift_wraps(trade_pubkey: nostr_sdk::PublicKey, tra process_gift_wrap_rumor(&rumor_json, &trade_pubkey_hex).await; last_activity = tokio::time::Instant::now(); } - Err(e) => log::warn!("[orders] gift-wrap decrypt failed: {e}"), + Err(e) => crate::api::logging::blog_warn("gift-wrap", format!( + "decrypt failed for trade={}: {e}", &trade_pubkey_hex[..8] + )), } } Ok(Ok(RelayPoolNotification::Shutdown)) => break, @@ -867,8 +994,6 @@ pub(crate) async fn subscribe_gift_wraps(trade_pubkey: nostr_sdk::PublicKey, tra Ok(Ok(_)) => continue, } } - - log::debug!("[orders] gift-wrap subscription exiting for trade={trade_pubkey_hex}"); }); } @@ -881,12 +1006,16 @@ async fn process_gift_wrap_rumor(rumor_json: &str, trade_pubkey_hex: &str) { Ok(v) => match v.get("content").and_then(|c| c.as_str()) { Some(s) => s.to_string(), None => { - log::warn!("[orders] gift-wrap rumor has no content field"); + crate::api::logging::blog_warn("gift-wrap", format!( + "rumor has no content field for trade={}", &trade_pubkey_hex[..8] + )); return; } }, Err(e) => { - log::warn!("[orders] gift-wrap rumor JSON parse failed: {e}"); + crate::api::logging::blog_warn("gift-wrap", format!( + "rumor JSON parse failed for trade={}: {e}", &trade_pubkey_hex[..8] + )); return; } }; @@ -896,26 +1025,88 @@ async fn process_gift_wrap_rumor(rumor_json: &str, trade_pubkey_hex: &str) { match serde_json::from_str(&content) { Ok(p) => p, Err(e) => { - log::warn!("[orders] gift-wrap content deserialize failed: {e}"); + crate::api::logging::blog_warn("gift-wrap", format!( + "content deserialize failed for trade={}: {e}", &trade_pubkey_hex[..8] + )); return; } }; let kind = msg.get_inner_message_kind(); - log::info!( - "[orders] gift-wrap action={:?} order_id={:?} trade_pubkey={}", - kind.action, - kind.id, - trade_pubkey_hex - ); + let payload_desc = match &kind.payload { + Some(mostro_core::message::Payload::Order(o)) => format!( + "Order(status={:?}, amount={}, buyer_pk={}, seller_pk={})", + o.status, + o.amount, + o.buyer_trade_pubkey.as_deref().unwrap_or("-"), + o.seller_trade_pubkey.as_deref().unwrap_or("-"), + ), + Some(mostro_core::message::Payload::PaymentRequest(id, pr, amt)) => format!( + "PaymentRequest(id={id:?}, invoice_len={}, amount={amt:?})", + pr.len() + ), + Some(other) => format!("{other:?}"), + None => "None".to_string(), + }; + crate::api::logging::blog_info("gift-wrap", format!( + "action={:?} order_id={:?} trade_index={:?} trade_pubkey={} payload={}", + kind.action, kind.id, kind.trade_index, &trade_pubkey_hex[..8], payload_desc + )); match &kind.action { Action::NewOrder => { if let Some(order_id) = &kind.id { - let order_id_str = order_id.to_string(); - resolve_maker_order(&order_id_str, trade_pubkey_hex).await; - log::info!("[orders] gift-wrap NewOrder: daemon order={order_id_str} confirmed"); + let daemon_id = order_id.to_string(); + resolve_maker_order(&daemon_id, trade_pubkey_hex).await; + + // If create_order is waiting for confirmation, notify it. + // The caller handles UUID replacement. Otherwise (cold-start / + // reconnect), do the replacement here. + let conf_tx = pending_confirmations() + .lock() + .ok() + .and_then(|mut m| m.remove(trade_pubkey_hex)); + if let Some(tx) = conf_tx { + let _ = tx.send(DaemonConfirmation::Confirmed { + daemon_id: daemon_id.clone(), + }); + crate::api::logging::blog_info("gift-wrap", format!( + "NewOrder: notified waiting create_order daemon={daemon_id}" + )); + } else { + // No waiting caller — either cold start, reconnect, or + // create_order timed out. Reconcile the local UUID with + // the daemon UUID if the order was persisted under a local ID. + let local_id = pending_local_ids() + .write() + .ok() + .and_then(|mut m| { + // Find the entry whose local_uuid is in the order book. + let key = m.iter() + .find(|(_, uuid)| uuid.as_str() != daemon_id) + .map(|(k, _)| k.clone()); + key.and_then(|k| m.remove(&k)) + }); + if let Some(local_id) = local_id { + if order_book().get_order(&local_id).await.is_some() { + let mut info = order_book().get_order(&local_id).await.unwrap(); + order_book().remove_order(&local_id).await; + info.id = daemon_id.clone(); + order_book().upsert_order(info).await; + if let Some(db) = crate::db::app_db::db() { + let _ = db.update_trade_order_id(&local_id, &daemon_id).await; + } + crate::api::logging::blog_info("gift-wrap", format!( + "NewOrder: late reconciliation local={local_id} → daemon={daemon_id}" + )); + } + } else { + crate::api::logging::blog_info("gift-wrap", format!( + "NewOrder: daemon order={daemon_id} confirmed (no local order to reconcile)" + )); + } + } } else { log::warn!("[orders] gift-wrap NewOrder has no order id"); } @@ -993,6 +1184,27 @@ async fn process_gift_wrap_rumor(rumor_json: &str, trade_pubkey_hex: &str) { // Derive the ECDH shared key and store in session so the chat API // can encrypt/decrypt P2P messages and subscribe to the right p-tag. on_peer_pubkey_received(&order_id, trade_pubkey_hex, &peer_pubkey_hex).await; + + // Sync the order status from the payload so the trade doesn't stay + // stuck at Pending in the DB and in-memory order book. + if let Some(new_status) = small_order.status.and_then(map_core_status) { + log::info!( + "[orders] gift-wrap {:?}: syncing order={order_id} status={:?}", + kind.action, + new_status + ); + order_book().update_order_status(&order_id, new_status.clone()).await; + if let Some(db) = crate::db::app_db::db() { + if let Err(e) = db + .update_trade_fields(&order_id, Some(new_status), None, None) + .await + { + log::warn!( + "[orders] failed to sync status for order={order_id}: {e}" + ); + } + } + } } // Mostro sends PayInvoice to the seller with the hold invoice bolt11 // when a buyer takes a sell order (or a seller takes a buy order). @@ -1028,7 +1240,8 @@ async fn process_gift_wrap_rumor(rumor_json: &str, trade_pubkey_hex: &str) { bolt11.len(), amount ); - // Save the hold invoice and update status to WaitingPayment in the DB. + // Save the hold invoice and update status to WaitingPayment. + order_book().update_order_status(&order_id, crate::api::types::OrderStatus::WaitingPayment).await; if let Some(db) = crate::db::app_db::db() { if let Err(e) = db .update_trade_fields( @@ -1061,7 +1274,13 @@ async fn process_gift_wrap_rumor(rumor_json: &str, trade_pubkey_hex: &str) { | Action::DisputeInitiatedByYou | Action::DisputeInitiatedByPeer | Action::AdminSettled - | Action::AdminCanceled => { + | Action::AdminCanceled + // Rate/RateReceived/PaymentFailed do not change order status but are + // handled explicitly so they don't fall through to the catch-all. + | Action::Rate + | Action::RateUser + | Action::RateReceived + | Action::PaymentFailed => { let order_id = match &kind.id { Some(id) => id.to_string(), None => { @@ -1075,9 +1294,12 @@ async fn process_gift_wrap_rumor(rumor_json: &str, trade_pubkey_hex: &str) { Action::WaitingBuyerInvoice => { Some(crate::api::types::OrderStatus::WaitingBuyerInvoice) } - Action::BuyerInvoiceAccepted | Action::FiatSentOk => { + Action::BuyerInvoiceAccepted => { Some(crate::api::types::OrderStatus::Active) } + Action::FiatSentOk => { + Some(crate::api::types::OrderStatus::FiatSent) + } Action::HoldInvoicePaymentSettled | Action::Released | Action::PurchaseCompleted => { Some(crate::api::types::OrderStatus::SettledHoldInvoice) } @@ -1089,6 +1311,8 @@ async fn process_gift_wrap_rumor(rumor_json: &str, trade_pubkey_hex: &str) { } Action::CooperativeCancelInitiatedByPeer | Action::CooperativeCancelInitiatedByYou => None, // status doesn't change yet + Action::Rate | Action::RateUser | Action::RateReceived => None, + Action::PaymentFailed => None, // order stays at SettledHoldInvoice Action::DisputeInitiatedByYou | Action::DisputeInitiatedByPeer => { Some(crate::api::types::OrderStatus::Dispute) } @@ -1102,6 +1326,7 @@ async fn process_gift_wrap_rumor(rumor_json: &str, trade_pubkey_hex: &str) { kind.action, status ); + order_book().update_order_status(&order_id, status.clone()).await; if let Some(db) = crate::db::app_db::db() { if let Err(e) = db .update_trade_fields(&order_id, Some(status), None, None) @@ -1119,12 +1344,68 @@ async fn process_gift_wrap_rumor(rumor_json: &str, trade_pubkey_hex: &str) { ); } } + Action::CantDo => { + let reason = match &kind.payload { + Some(mostro_core::message::Payload::CantDo(Some(r))) => format!("{r:?}"), + Some(mostro_core::message::Payload::CantDo(None)) => "unknown".to_string(), + _ => "unknown".to_string(), + }; + let message = match reason.as_str() { + "OutOfRangeSatsAmount" => "Order rejected: sats amount is out of the allowed range.".to_string(), + "OutOfRangeFiatAmount" => "Order rejected: fiat amount is out of the allowed range.".to_string(), + "InvalidAmount" => "Order rejected: invalid amount.".to_string(), + "InvalidInvoice" => "Order rejected: invalid Lightning invoice.".to_string(), + "IsNotYourOrder" => "Order rejected: this order does not belong to you.".to_string(), + "NotAllowedByStatus" => "Action rejected: not allowed in the current order status.".to_string(), + "OrderAlreadyCanceled" => "Order is already canceled.".to_string(), + other => format!("Order rejected by Mostro: {other}"), + }; + + // If create_order is waiting, notify it — the caller handles + // cleanup and returns an error to Dart. Otherwise ignore stale events. + let conf_tx = pending_confirmations() + .lock() + .ok() + .and_then(|mut m| m.remove(trade_pubkey_hex)); + if let Some(tx) = conf_tx { + crate::api::logging::blog_warn("gift-wrap", format!( + "CantDo: reason={reason} — notifying waiting create_order" + )); + let _ = tx.send(DaemonConfirmation::Rejected { reason, message }); + } else { + crate::api::logging::blog_debug("gift-wrap", format!( + "CantDo: reason={reason} — no waiting caller, ignoring stale event" + )); + } + } action => { log::debug!("[orders] gift-wrap unhandled action={action:?}"); } } } +/// Maps a `mostro_core::order::Status` to the local [`OrderStatus`] enum. +fn map_core_status(s: mostro_core::order::Status) -> Option { + use mostro_core::order::Status as S; + Some(match s { + S::Pending => OrderStatus::Pending, + S::WaitingBuyerInvoice => OrderStatus::WaitingBuyerInvoice, + S::WaitingPayment => OrderStatus::WaitingPayment, + S::Active => OrderStatus::Active, + S::InProgress => OrderStatus::InProgress, + S::FiatSent => OrderStatus::FiatSent, + S::SettledHoldInvoice => OrderStatus::SettledHoldInvoice, + S::Success => OrderStatus::Success, + S::Canceled => OrderStatus::Canceled, + S::CooperativelyCanceled => OrderStatus::CooperativelyCanceled, + S::Expired => OrderStatus::Expired, + S::CanceledByAdmin => OrderStatus::CanceledByAdmin, + S::SettledByAdmin => OrderStatus::SettledByAdmin, + S::CompletedByAdmin => OrderStatus::CompletedByAdmin, + S::Dispute => OrderStatus::Dispute, + }) +} + // ── Peer-pubkey resolution ──────────────────────────────────────────────────── /// Called when the daemon sends `BuyerTookOrder` or `HoldInvoicePaymentAccepted`. @@ -1371,6 +1652,85 @@ pub async fn restart_orders_subscription() { subscribe_orders().await; } +/// Build a map of `trade_pubkey_hex → (Keys, trade_index)` for all derived +/// trade keys so the global subscription can decrypt any gift-wrap. +async fn build_trade_key_map() -> HashMap { + let mut map = HashMap::new(); + let max_index = match crate::api::identity::get_identity().await { + Ok(Some(info)) => info.trade_key_index, + _ => return map, + }; + for idx in 1..=max_index { + match crate::api::identity::get_active_trade_keys(idx).await { + Ok(keys) => { + let hex = keys.public_key().to_hex(); + map.insert(hex, (keys, idx)); + } + Err(e) => log::warn!("[orders] failed to derive trade key {idx}: {e}"), + } + } + map +} + +/// Handle a Kind 1059 event received on the global subscription. +/// +/// Finds which trade key the event is addressed to (via `p` tag), decrypts, +/// logs the full content, and dispatches to `process_gift_wrap_rumor`. +async fn handle_global_gift_wrap( + event: &nostr_sdk::Event, + trade_key_map: &HashMap, +) { + // Find the p-tag that matches one of our trade keys. + let (recipient_hex, recipient_keys) = { + let mut found = None; + for tag in event.tags.iter() { + let s = tag.as_slice(); + if s.first().map(|v| v.as_str()) == Some("p") { + if let Some(pk_hex) = s.get(1).map(|v| v.as_str()) { + if let Some((keys, _idx)) = trade_key_map.get(pk_hex) { + found = Some((pk_hex.to_string(), keys.clone())); + break; + } + } + } + } + match found { + Some(f) => f, + None => { + // Not addressed to any of our known trade keys — skip silently. + return; + } + } + }; + + let eid = event.id.to_hex(); + if is_duplicate_gift_wrap(&eid) { + return; + } + crate::api::logging::blog_info("gift-wrap", format!( + "Kind 1059 received (global) for trade={} from={} event_id={}", + &recipient_hex[..8], + &event.pubkey.to_hex()[..8], + &eid[..16], + )); + + let event_json = match serde_json::to_string(event) { + Ok(j) => j, + Err(e) => { + crate::api::logging::blog_warn("gift-wrap", format!("event serialize failed: {e}")); + return; + } + }; + match crate::nostr::gift_wrap::unwrap(&recipient_keys, &event_json).await { + Ok(rumor_json) => { + process_gift_wrap_rumor(&rumor_json, &recipient_hex).await; + } + Err(e) => crate::api::logging::blog_warn("gift-wrap", format!( + "decrypt failed for trade={}: {e}", &recipient_hex[..8] + )), + } +} + async fn _run_order_subscription() { let Ok(pool) = crate::api::nostr::get_pool() else { log::error!("[orders] subscription failed: relay pool not initialized"); @@ -1388,10 +1748,16 @@ async fn _run_order_subscription() { return; } }; - log::info!( - "[orders] subscribing to Kind 38383 from mostro={}", - mostro_pubkey.to_hex() - ); + crate::api::logging::blog_info("orders", format!("subscribing to Kind 38383 from mostro={}", mostro_pubkey.to_hex())); + + // Build a map of all known trade keys so we can decrypt ANY Kind 1059 + // gift-wrap from Mostro, not just those from the current session. + let trade_key_map = build_trade_key_map().await; + let trade_pubkeys: Vec = trade_key_map + .keys() + .filter_map(|hex| nostr_sdk::PublicKey::from_hex(hex).ok()) + .collect(); + crate::api::logging::blog_info("orders", format!("trade key map: {} keys derived for gift-wrap decryption", trade_pubkeys.len())); // Get notifications receiver before subscribing to avoid missing // events that arrive between the subscribe call and receiver creation. @@ -1400,19 +1766,40 @@ async fn _run_order_subscription() { // Subscribe to ALL orders (no status restriction) so we receive status-change // events (e.g. pending → canceled) and can remove them from the order book. // Display-level filtering (show only pending) is handled in the Dart layer. - let filter = crate::nostr::order_events::all_orders_filter(&mostro_pubkey); - if let Err(e) = client.subscribe(filter, None).await { + let order_filter = crate::nostr::order_events::all_orders_filter(&mostro_pubkey); + if let Err(e) = client.subscribe(order_filter, None).await { log::error!("[orders] subscribe failed: {e}"); return; } - log::info!("[orders] Kind 38383 subscription active — waiting for events"); + + // Subscribe to Kind 1059 (gift-wrap) for ALL known trade pubkeys so we + // capture daemon responses even for trades started in previous sessions. + if !trade_pubkeys.is_empty() { + let gw_filter = nostr_sdk::Filter::new() + .kind(nostr_sdk::Kind::from(1059u16)) + .pubkeys(trade_pubkeys); + if let Err(e) = client.subscribe(gw_filter, None).await { + crate::api::logging::blog_warn("orders", format!("gift-wrap bulk subscribe failed: {e}")); + } else { + crate::api::logging::blog_info("orders", format!("Kind 1059 bulk subscription active for {} trade keys", trade_key_map.len())); + } + } + + crate::api::logging::blog_info("orders", format!("subscriptions active — waiting for events")); use nostr_sdk::RelayPoolNotification; loop { match rx.recv().await { Ok(RelayPoolNotification::Event { event, .. }) => { - log::info!( + // ── Kind 1059 gift-wrap: decrypt and dispatch ── + if event.kind == nostr_sdk::Kind::from(1059u16) { + handle_global_gift_wrap(&event, &trade_key_map).await; + continue; + } + + // ── Kind 38383 order book event ── + log::debug!( "[orders] event kind={} author={}", event.kind, &event.pubkey.to_hex()[..8] diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 921258ff..b7bc2cd8 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -45,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 = 1165020131; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 424169504; // Section: executor @@ -651,6 +651,70 @@ fn wire__crate__api__orders__OrderBook_set_orders_impl( }, ) } +fn wire__crate__api__orders__OrderBook_update_order_status_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: "OrderBook_update_order_status", + 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); + let api_order_id = ::sse_decode(&mut deserializer); + let api_status = ::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, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::api::orders::OrderBook::update_order_status( + &*api_that_guard, + &api_order_id, + api_status, + ) + .await; + })?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__orders__OrderBook_upsert_order_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -5396,201 +5460,207 @@ fn pde_ffi_dispatcher_primary_impl( 11 => { wire__crate__api__orders__OrderBook_set_orders_impl(port, ptr, rust_vec_len, data_len) } - 12 => { + 12 => wire__crate__api__orders__OrderBook_update_order_status_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 13 => { wire__crate__api__orders__OrderBook_upsert_order_impl(port, ptr, rust_vec_len, data_len) } - 13 => wire__crate__api__orders__OrdersStream_next_impl(port, ptr, rust_vec_len, data_len), - 14 => { + 14 => wire__crate__api__orders__OrdersStream_next_impl(port, ptr, rust_vec_len, data_len), + 15 => { wire__crate__api__reputation__RatingStream_next_impl(port, ptr, rust_vec_len, data_len) } - 15 => { + 16 => { wire__crate__api__nostr__RelayStatusStream_next_impl(port, ptr, rust_vec_len, data_len) } - 16 => { + 17 => { wire__crate__api__settings__SettingsStream_next_impl(port, ptr, rust_vec_len, data_len) } - 17 => wire__crate__api__messages__UnreadCountStream_next_impl( + 18 => wire__crate__api__messages__UnreadCountStream_next_impl( port, ptr, rust_vec_len, data_len, ), - 18 => { + 19 => { wire__crate__api__nwc__WalletStatusStream_next_impl(port, ptr, rust_vec_len, data_len) } - 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 => { + 20 => wire__crate__api__nostr__add_relay_impl(port, ptr, rust_vec_len, data_len), + 21 => wire__crate__api__orders__cancel_order_impl(port, ptr, rust_vec_len, data_len), + 22 => wire__crate__api__nwc__connect_wallet_impl(port, ptr, rust_vec_len, data_len), + 23 => wire__crate__api__identity__create_identity_impl(port, ptr, rust_vec_len, data_len), + 24 => wire__crate__api__orders__create_order_impl(port, ptr, rust_vec_len, data_len), + 25 => wire__crate__api__identity__delete_identity_impl(port, ptr, rust_vec_len, data_len), + 26 => wire__crate__api__identity__derive_trade_key_impl(port, ptr, rust_vec_len, data_len), + 27 => wire__crate__api__nwc__disconnect_wallet_impl(port, ptr, rust_vec_len, data_len), + 28 => { wire__crate__api__messages__download_attachment_impl(port, ptr, rust_vec_len, data_len) } - 28 => wire__crate__api__identity__export_encrypted_backup_impl( + 29 => wire__crate__api__identity__export_encrypted_backup_impl( port, ptr, rust_vec_len, data_len, ), - 29 => wire__crate__api__nostr__fetch_mostro_instance_tags_impl( + 30 => wire__crate__api__nostr__fetch_mostro_instance_tags_impl( port, ptr, rust_vec_len, data_len, ), - 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( + 31 => wire__crate__api__nostr__flush_message_queue_impl(port, ptr, rust_vec_len, data_len), + 32 => wire__crate__api__get_app_version_impl(port, ptr, rust_vec_len, data_len), + 33 => wire__crate__api__messages__get_attachment_status_impl( port, ptr, rust_vec_len, data_len, ), - 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_node_impl(port, ptr, rust_vec_len, data_len), - 39 => wire__crate__api__settings__get_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), - 40 => wire__crate__api__identity__get_nym_identity_impl(port, ptr, rust_vec_len, data_len), - 41 => wire__crate__api__orders__get_order_impl(port, ptr, rust_vec_len, data_len), - 42 => wire__crate__api__orders__get_orders_impl(port, ptr, rust_vec_len, data_len), - 43 => { + 34 => wire__crate__api__nwc__get_balance_impl(port, ptr, rust_vec_len, data_len), + 35 => wire__crate__api__nostr__get_connection_state_impl(port, ptr, rust_vec_len, data_len), + 36 => wire__crate__api__disputes__get_dispute_impl(port, ptr, rust_vec_len, data_len), + 37 => wire__crate__api__identity__get_identity_impl(port, ptr, rust_vec_len, data_len), + 38 => wire__crate__api__messages__get_messages_impl(port, ptr, rust_vec_len, data_len), + 39 => wire__crate__api__settings__get_mostro_node_impl(port, ptr, rust_vec_len, data_len), + 40 => wire__crate__api__settings__get_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), + 41 => wire__crate__api__identity__get_nym_identity_impl(port, ptr, rust_vec_len, data_len), + 42 => wire__crate__api__orders__get_order_impl(port, ptr, rust_vec_len, data_len), + 43 => wire__crate__api__orders__get_orders_impl(port, ptr, rust_vec_len, data_len), + 44 => { wire__crate__api__reputation__get_privacy_mode_impl(port, ptr, rust_vec_len, data_len) } - 44 => wire__crate__api__reputation__get_rating_for_trade_impl( + 45 => wire__crate__api__reputation__get_rating_for_trade_impl( port, ptr, rust_vec_len, data_len, ), - 45 => wire__crate__api__nostr__get_relays_impl(port, ptr, rust_vec_len, data_len), - 46 => wire__crate__api__settings__get_settings_impl(port, ptr, rust_vec_len, data_len), - 47 => wire__crate__api__identity__get_trade_key_impl(port, ptr, rust_vec_len, data_len), - 48 => wire__crate__api__orders__get_trade_role_impl(port, ptr, rust_vec_len, data_len), - 49 => wire__crate__api__messages__get_unread_count_impl(port, ptr, rust_vec_len, data_len), - 50 => wire__crate__api__nwc__get_wallet_impl(port, ptr, rust_vec_len, data_len), - 51 => wire__crate__api__disputes__handle_admin_canceled_impl( + 46 => wire__crate__api__nostr__get_relays_impl(port, ptr, rust_vec_len, data_len), + 47 => wire__crate__api__settings__get_settings_impl(port, ptr, rust_vec_len, data_len), + 48 => wire__crate__api__identity__get_trade_key_impl(port, ptr, rust_vec_len, data_len), + 49 => wire__crate__api__orders__get_trade_role_impl(port, ptr, rust_vec_len, data_len), + 50 => wire__crate__api__messages__get_unread_count_impl(port, ptr, rust_vec_len, data_len), + 51 => wire__crate__api__nwc__get_wallet_impl(port, ptr, rust_vec_len, data_len), + 52 => wire__crate__api__disputes__handle_admin_canceled_impl( port, ptr, rust_vec_len, data_len, ), - 52 => { + 53 => { wire__crate__api__disputes__handle_admin_settled_impl(port, ptr, rust_vec_len, data_len) } - 53 => wire__crate__api__disputes__handle_admin_took_dispute_impl( + 54 => wire__crate__api__disputes__handle_admin_took_dispute_impl( port, ptr, rust_vec_len, data_len, ), - 54 => wire__crate__api__reputation__handle_rating_received_impl( + 55 => wire__crate__api__reputation__handle_rating_received_impl( port, ptr, rust_vec_len, data_len, ), - 55 => { + 56 => { wire__crate__api__identity__import_from_mnemonic_impl(port, ptr, rust_vec_len, data_len) } - 56 => wire__crate__api__identity__import_from_nsec_impl(port, ptr, rust_vec_len, data_len), - 57 => wire__crate__api__init_db_impl(port, ptr, rust_vec_len, data_len), - 58 => wire__crate__api__nostr__initialize_impl(port, ptr, rust_vec_len, data_len), - 59 => wire__crate__api__logging__install_log_bridge_impl(port, ptr, rust_vec_len, data_len), - 60 => wire__crate__api__orders__list_trades_impl(port, ptr, rust_vec_len, data_len), - 61 => wire__crate__api__identity__load_identity_from_mnemonic_impl( + 57 => wire__crate__api__identity__import_from_nsec_impl(port, ptr, rust_vec_len, data_len), + 58 => wire__crate__api__init_db_impl(port, ptr, rust_vec_len, data_len), + 59 => wire__crate__api__nostr__initialize_impl(port, ptr, rust_vec_len, data_len), + 60 => wire__crate__api__logging__install_log_bridge_impl(port, ptr, rust_vec_len, data_len), + 61 => wire__crate__api__orders__list_trades_impl(port, ptr, rust_vec_len, data_len), + 62 => wire__crate__api__identity__load_identity_from_mnemonic_impl( port, ptr, rust_vec_len, data_len, ), - 62 => wire__crate__api__nwc__make_invoice_impl(port, ptr, rust_vec_len, data_len), - 63 => wire__crate__api__messages__mark_as_read_impl(port, ptr, rust_vec_len, data_len), - 64 => wire__crate__api__messages__on_attachment_progress_impl( + 63 => wire__crate__api__nwc__make_invoice_impl(port, ptr, rust_vec_len, data_len), + 64 => wire__crate__api__messages__mark_as_read_impl(port, ptr, rust_vec_len, data_len), + 65 => wire__crate__api__messages__on_attachment_progress_impl( port, ptr, rust_vec_len, data_len, ), - 65 => wire__crate__api__nostr__on_connection_state_changed_impl( + 66 => wire__crate__api__nostr__on_connection_state_changed_impl( port, ptr, rust_vec_len, data_len, ), - 66 => { + 67 => { wire__crate__api__disputes__on_dispute_updated_impl(port, ptr, rust_vec_len, data_len) } - 67 => wire__crate__api__logging__on_log_entry_impl(port, ptr, rust_vec_len, data_len), - 68 => wire__crate__api__messages__on_new_message_impl(port, ptr, rust_vec_len, data_len), - 69 => wire__crate__api__orders__on_orders_updated_impl(port, ptr, rust_vec_len, data_len), - 70 => { + 68 => wire__crate__api__logging__on_log_entry_impl(port, ptr, rust_vec_len, data_len), + 69 => wire__crate__api__messages__on_new_message_impl(port, ptr, rust_vec_len, data_len), + 70 => wire__crate__api__orders__on_orders_updated_impl(port, ptr, rust_vec_len, data_len), + 71 => { wire__crate__api__reputation__on_rating_received_impl(port, ptr, rust_vec_len, data_len) } - 71 => { + 72 => { wire__crate__api__nostr__on_relay_status_changed_impl(port, ptr, rust_vec_len, data_len) } - 72 => { + 73 => { wire__crate__api__settings__on_settings_changed_impl(port, ptr, rust_vec_len, data_len) } - 73 => wire__crate__api__messages__on_unread_count_changed_impl( + 74 => wire__crate__api__messages__on_unread_count_changed_impl( port, ptr, rust_vec_len, data_len, ), - 74 => { + 75 => { wire__crate__api__nwc__on_wallet_status_changed_impl(port, ptr, rust_vec_len, data_len) } - 75 => wire__crate__api__disputes__open_dispute_impl(port, ptr, rust_vec_len, data_len), - 76 => { + 76 => wire__crate__api__disputes__open_dispute_impl(port, ptr, rust_vec_len, data_len), + 77 => { wire__crate__api__orders__order_filters_default_impl(port, ptr, rust_vec_len, data_len) } - 77 => wire__crate__api__nwc__pay_invoice_impl(port, ptr, rust_vec_len, data_len), - 78 => wire__crate__api__orders__release_order_impl(port, ptr, rust_vec_len, data_len), - 79 => wire__crate__api__nostr__remove_relay_impl(port, ptr, rust_vec_len, data_len), - 80 => wire__crate__api__orders__resolve_maker_order_impl(port, ptr, rust_vec_len, data_len), - 81 => wire__crate__api__orders__restart_orders_subscription_impl( + 78 => wire__crate__api__nwc__pay_invoice_impl(port, ptr, rust_vec_len, data_len), + 79 => wire__crate__api__orders__release_order_impl(port, ptr, rust_vec_len, data_len), + 80 => wire__crate__api__nostr__remove_relay_impl(port, ptr, rust_vec_len, data_len), + 81 => wire__crate__api__orders__resolve_maker_order_impl(port, ptr, rust_vec_len, data_len), + 82 => wire__crate__api__orders__restart_orders_subscription_impl( port, ptr, rust_vec_len, data_len, ), - 82 => wire__crate__api__orders__send_fiat_sent_impl(port, ptr, rust_vec_len, data_len), - 83 => wire__crate__api__messages__send_file_impl(port, ptr, rust_vec_len, data_len), - 84 => wire__crate__api__orders__send_invoice_impl(port, ptr, rust_vec_len, data_len), - 85 => wire__crate__api__messages__send_message_impl(port, ptr, rust_vec_len, data_len), - 86 => wire__crate__api__settings__set_default_fiat_code_impl( + 83 => wire__crate__api__orders__send_fiat_sent_impl(port, ptr, rust_vec_len, data_len), + 84 => wire__crate__api__messages__send_file_impl(port, ptr, rust_vec_len, data_len), + 85 => wire__crate__api__orders__send_invoice_impl(port, ptr, rust_vec_len, data_len), + 86 => wire__crate__api__messages__send_message_impl(port, ptr, rust_vec_len, data_len), + 87 => wire__crate__api__settings__set_default_fiat_code_impl( port, ptr, rust_vec_len, data_len, ), - 87 => wire__crate__api__settings__set_default_lightning_address_impl( + 88 => wire__crate__api__settings__set_default_lightning_address_impl( port, ptr, rust_vec_len, data_len, ), - 88 => wire__crate__api__settings__set_language_impl(port, ptr, rust_vec_len, data_len), - 89 => { + 89 => wire__crate__api__settings__set_language_impl(port, ptr, rust_vec_len, data_len), + 90 => { wire__crate__api__settings__set_logging_enabled_impl(port, ptr, rust_vec_len, data_len) } - 90 => wire__crate__api__settings__set_mostro_node_impl(port, ptr, rust_vec_len, data_len), - 91 => wire__crate__api__settings__set_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), - 92 => { + 91 => wire__crate__api__settings__set_mostro_node_impl(port, ptr, rust_vec_len, data_len), + 92 => wire__crate__api__settings__set_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), + 93 => { wire__crate__api__reputation__set_privacy_mode_impl(port, ptr, rust_vec_len, data_len) } - 93 => wire__crate__api__settings__set_theme_impl(port, ptr, rust_vec_len, data_len), - 94 => wire__crate__api__disputes__submit_evidence_impl(port, ptr, rust_vec_len, data_len), - 95 => wire__crate__api__reputation__submit_rating_impl(port, ptr, rust_vec_len, data_len), - 96 => wire__crate__api__orders__subscribe_orders_impl(port, ptr, rust_vec_len, data_len), - 97 => wire__crate__api__orders__take_order_impl(port, ptr, rust_vec_len, data_len), + 94 => wire__crate__api__settings__set_theme_impl(port, ptr, rust_vec_len, data_len), + 95 => wire__crate__api__disputes__submit_evidence_impl(port, ptr, rust_vec_len, data_len), + 96 => wire__crate__api__reputation__submit_rating_impl(port, ptr, rust_vec_len, data_len), + 97 => wire__crate__api__orders__subscribe_orders_impl(port, ptr, rust_vec_len, data_len), + 98 => wire__crate__api__orders__take_order_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } }