/// Shared types exposed to Flutter via flutter_rust_bridge. /// These are the data structures that cross the Rust/Dart boundary. // ── Enums ──────────────────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum OrderKind { Buy, Sell, } /// Protocol-level order states. /// /// `PaymentFailed` is NOT a status — it is an Action notification sent when /// the Lightning payment to the buyer fails. The order remains in /// `SettledHoldInvoice` when that notification arrives. /// /// `CooperativelyCanceled` is a **client-side UI state only** — the protocol /// does not change the order status for cooperative cancellations. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum OrderStatus { Pending, WaitingBuyerInvoice, WaitingPayment, Active, FiatSent, SettledHoldInvoice, Success, Canceled, Expired, /// Client-side UI state only — not a protocol status change. CooperativelyCanceled, CanceledByAdmin, SettledByAdmin, CompletedByAdmin, Dispute, InProgress, /// The taker's anti-abuse bond is outstanding: the daemon matched the /// take but the trade flow has not started. Publicly the order is still /// `pending` (NIP-69 bucket), so it stays takeable by others until a bond /// locks. See `docs/ANTI_ABUSE_BOND.md` §2.7. WaitingTakerBond, /// The maker's anti-abuse bond is outstanding: the order exists on the /// daemon but has **no** kind 38383 event yet and is invisible in the /// order book until the bond locks. See `docs/ANTI_ABUSE_BOND.md` §2.8. WaitingMakerBond, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum TradeRole { Buyer, Seller, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum BuyerStep { OrderTaken, PayInvoice, PaymentLocked, FiatSent, AwaitingRelease, Complete, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum SellerStep { OrderPublished, TakerFound, InvoiceCreated, PaymentLocked, AwaitingFiat, Complete, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum TradeStep { Buyer(BuyerStep), Seller(SellerStep), Disputed, } /// Final trade outcomes. /// /// `PaymentFailed` is intentionally absent — LN payment failures are transient /// and retried; they are not a terminal trade outcome. The order stays in /// `SettledHoldInvoice` while retries are in flight. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum TradeOutcome { Success, Canceled, Expired, DisputeWon, DisputeLost, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum MessageType { Peer, Admin, System, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum DisputeStatus { Open, InReview, Resolved, } /// Who a dispute solver is, for the label the dispute chat shows (#637). /// Rust decides it; Dart only localizes it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SolverRole { /// The dispute assistant (Serbero) a known node announces in its info /// event: it helps the parties and hands the case to a person. Assistant, /// Anyone else: a person who can settle or cancel. Every solver of a node /// that announces no assistant is one. Human, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum DisputeResolution { /// Admin settled the dispute — sats released to the buyer. FundsToBuyer, /// Admin canceled the order — sats returned to the seller. FundsToSeller, CooperativeCancel, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum RelayStatus { Connected, Disconnected, Connecting, Error, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ConnectionState { Online, Offline, Reconnecting, } /// The device token's platform, as the push server wants it /// (docs/PUSH_NOTIFICATIONS.md §3.1, §3.5). #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum PushPlatform { Android, Ios, Web, } impl PushPlatform { /// The wire value of `platform`. pub fn as_wire(&self) -> &'static str { match self { PushPlatform::Android => "android", PushPlatform::Ios => "ios", PushPlatform::Web => "web", } } pub fn from_wire(value: &str) -> Option { match value { "android" => Some(PushPlatform::Android), "ios" => Some(PushPlatform::Ios), "web" => Some(PushPlatform::Web), _ => None, } } } /// How loudly an announcement is painted (specs/006-announcement-channel /// §3.4). The publisher declares a level, never a colour; Dart maps it onto /// the palette. A level this build does not know arrives as `Warning`. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum AnnouncementSeverity { /// Releases, new features, events. Info, /// Outages, a relay being retired, anything with a deadline. Warning, /// A security issue the user must act on now. Critical, } /// One locale's copy of an announcement, plain text (§3.2). #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct AnnouncementText { /// `en`, `es`, … — one of the app's locales. pub locale: String, pub title: String, pub body: String, } /// A project announcement the reader admitted (§5.2), with every locale: Rust /// does not translate, Dart picks the copy for the locale on screen. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct AnnouncementInfo { /// `38387::` — the announcement's identity across /// revisions, and the id of its notification card. pub address: String, /// `:`: changes with every correction published /// under the same address, which is what makes it unread again (§5.3). pub revision: String, /// Unix seconds, the event's `created_at`. pub created_at: i64, /// Unix seconds, the NIP-40 `expiration`: the announcement is gone once /// it passes, online or not (§5.5). pub expiration: i64, pub severity: AnnouncementSeverity, /// Every supported locale, in the app's order. pub texts: Vec, /// The one action link, `https` only, as parsed. pub url: Option, pub read: bool, /// Swiped away. A `Critical` stays listed, read, until it expires; the /// other levels leave the list (§3.4). pub dismissed: bool, } /// What the notification settings screen shows about push registration /// (docs/PUSH_NOTIFICATIONS.md §8.1, §9.1). Capability (can this platform /// push at all) and permission are Dart's to know; this is the token and /// what the server holds. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct PushStatus { /// The master toggle. pub enabled: bool, /// A device token is held (Dart handed one over). pub has_token: bool, /// Trade pubkeys the server currently holds a token for. pub registered: u32, /// Trade pubkeys that should be registered right now. pub wanted: u32, /// Unix seconds of the most recent accepted registration. pub last_success_at: Option, /// Stable marker of the last failure, never prose: `PushServerUnreachable`, /// `PushRateLimited`, `PushNodeRefused`, `PushBadRequest`. pub last_error: Option, /// Unix seconds until which the operator's `403` for the active node /// keeps its keys unregistered; `None` when not refused. pub node_refused_until: Option, } /// What one `resync` pass found and did (docs/PUSH_NOTIFICATIONS.md §10). #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ResyncOutcome { /// The pool reported `Online` once the reconnect nudge settled. pub online: bool, /// Queued outgoing events published by this pass. pub flushed: u32, /// This call did no work of its own: a pass that was already running /// when it arrived finished meanwhile, and its result is what it reports. pub coalesced: bool, } /// What a node's exchange-rates event (Kind 30078, `d` = `mostro-rates`) /// says about one currency (`api::nostr::lookup_exchange_rate`). #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ExchangeRateLookup { /// The node publishes no usable rates event: none on the relays, expired, /// or unreadable. Nothing can be told, so a market order is sent /// unchecked (#337). NoRates, /// The event is there and quotes no such currency. For a stablecoin that /// means the node will refuse a market order in it. NotQuoted, /// The price of one BTC in the currency. Quoted { rate: f64 }, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum QueuedMessageStatus { Pending, /// Currently being published — prevents duplicate flush attempts. InFlight, Sent, Failed, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum CooperativeCancelState { RequestedByMe, RequestedByPeer, Accepted, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum FileType { Image, Document, Video, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum DownloadStatus { Pending, Downloading, Downloaded, Failed, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum WalletStatus { Connected, Disconnected, Connecting, Error, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ThemeMode { System, Dark, Light, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum LogLevel { Debug, Info, Warning, Error, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum RelaySource { Default, MostroDiscovered, UserAdded, } // ── Structs ─────────────────────────────────────────────────────────────────── #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct OrderInfo { pub id: String, pub kind: OrderKind, pub status: OrderStatus, pub amount_sats: Option, /// Fiat amount for display/transmission only. /// **Do not use for precise financial calculations** — `f64` cannot /// represent all decimal values exactly. Use integer minor units or a /// decimal type (e.g. `rust_decimal`) wherever arithmetic is needed. pub fiat_amount: Option, /// Lower bound of a range order. Same precision caveat as `fiat_amount`. pub fiat_amount_min: Option, /// Upper bound of a range order. Same precision caveat as `fiat_amount`. pub fiat_amount_max: Option, pub fiat_code: String, pub payment_method: String, /// Market premium as a percentage, e.g. `1.5` means 1.5% above market. /// For display only — same `f64` precision caveat applies. pub premium: f64, /// The Mostro node that issued the order: the Kind 38383 author on a book /// order, the node that answered on a trade row. Empty when unknown. /// Rows persisted before the rename carry it as `creator_pubkey`. #[serde(alias = "creator_pubkey")] pub node_pubkey: String, /// Unix timestamp (seconds). pub created_at: i64, pub expires_at: Option, pub is_mine: bool, /// Maker reputation from the Kind 38383 `rating` tag (`total_rating` /// aggregate, 0–5). `0.0` when the maker has no reputation yet or /// publishes in full-privacy mode (`rating` = `"none"`). /// /// `serde(default)` on these three fields keeps rows persisted before /// they existed (orders table, `OrderInfo` nested in trades JSON) /// deserializable after an app upgrade. #[serde(default)] pub rating: f64, /// Number of reviews behind [`Self::rating`] (`total_reviews`). #[serde(default)] pub total_reviews: u32, /// Days the maker has been active on this Mostro node (`days`). /// Deprecated on the wire in favour of [`Self::maker_since`]; kept as the /// fallback for daemons that do not publish `since`. #[serde(default)] pub days_active: u32, /// Unix timestamp (seconds) of the maker's first trade, truncated to its /// UTC day start (the `rating` tag's `since`). `None` from daemons that /// predate it and for users without a date. The UI computes the age at /// display time (now − since) and falls back to [`Self::days_active`]. #[serde(default)] pub maker_since: Option, /// Mint the order's escrow is locked at, from the Kind 38383 /// `cashu_mint_url` tag (MostroP2P/mostro#1047): the maker picks it among /// the node's mints. `None` on a Lightning order, on a Cashu order from an /// older daemon, and on our own new order until its book event says. #[serde(default)] pub cashu_mint_url: Option, } /// Parameters for creating a new order via the Mostro protocol. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct NewOrderParams { pub kind: OrderKind, /// Fixed fiat amount (null if range order). pub fiat_amount: Option, /// Min fiat amount for range orders (null if fixed). pub fiat_amount_min: Option, /// Max fiat amount for range orders (null if fixed). pub fiat_amount_max: Option, /// Currency code: ISO 4217, or a stablecoin code from the protocol book's /// list (`USDT`, `USDC`, `DOC`). pub fiat_code: String, /// Comma-separated payment method descriptions. pub payment_method: String, /// Market premium/discount percentage. pub premium: f64, /// Optional fixed sat amount. pub amount_sats: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TradeInfo { /// This row's own id — **not** the order's, and not reliably either the /// same or different. /// /// A take made on this device mints a fresh UUID here (`take_order`) /// while `order.id` holds the id the daemon knows, so the two diverge. A /// row rebuilt instead of taken — from a replayed daemon message on a /// fresh device (`trade_row_from_small_order`), or from a restored bond /// (`restored_bond_row`) — reuses the order id for both. So neither /// equality nor inequality says whose row it is, and no code should ask: /// `order.is_mine` and `role` are what carry that. /// /// Nothing looks a trade up by this, whether or not it happens to match. /// Every accessor on [`crate::db::Storage`] keys on `order.id`, and so /// does the chat (`messages.trade_id`); its one job is to be the row's /// primary key, so `save_trade` replaces a row instead of inserting a /// second one. Carry it forward when rebuilding a row, and reach for /// `order.id` when looking one up (issue #395). pub id: String, pub order: OrderInfo, pub role: TradeRole, pub counterparty_pubkey: String, pub current_step: TradeStep, pub hold_invoice: Option, pub buyer_invoice: Option, pub trade_key_index: u32, pub cooperative_cancel_state: Option, pub timeout_at: Option, pub started_at: i64, pub completed_at: Option, /// Unix seconds (the node's clock) when the seller's hold invoice was /// accepted: the `created_at` of the daemon message that moved the order /// out of `waiting-payment`. The escrow deadline guardian counts from /// there. The oldest candidate is kept, so a replayed or re-sent message /// never moves it. `None` until recorded, and on rows written before the /// field existed (`#[serde(default)]`). #[serde(default)] pub escrow_locked_at: Option, /// The node (hex pubkey) that sent the message [`Self::escrow_locked_at`] /// was taken from: its info event says how far the deadline is, whichever /// node is active now. #[serde(default)] pub escrow_node: Option, pub outcome: Option, /// Counterparty (taker) reputation snapshot from the daemon's follow-up /// Peer DM (issue #305). All-zeros is ambiguous on the wire — a brand-new /// user and a full-privacy taker are indistinguishable — so the UI shows /// the raw numbers rather than guessing. `#[serde(default)]` keeps trade /// rows written before this field existed deserializable. #[serde(default)] pub peer_rating: Option, #[serde(default)] pub peer_reviews: Option, #[serde(default)] pub peer_days: Option, /// Unix timestamp (seconds) of the counterparty's first trade, truncated /// to its UTC day start (`UserInfo.since` in the Peer DM). `None` from /// daemons that predate it and for users without a date. The UI computes /// the age at display time (now − since) and falls back to /// [`Self::peer_days`]. #[serde(default)] pub peer_since: Option, /// Durable "the local user rated this trade" marker (unix seconds): set /// after `submit_rating` publishes (issue #339), and when the daemon's /// `rate-received` — sent to the rater alone — arrives or is replayed, /// dated by it: a rating made on another device closes the step here too. /// /// The daemon's kind 38383 tag carries the peer's *aggregate* reputation /// only, and `rate-received` lives only as long as the relays keep it, so /// the marker is persisted here: the closed state survives a restart and /// keeps the duplicate-rating guard armed. The score itself is /// deliberately not stored — the closed UI shows only a label, not the note. /// `#[serde(default)]` keeps trade rows written before this field existed /// deserializable. #[serde(default)] pub rated_at: Option, /// Anti-abuse bond attached to this trade, when the node required one /// (`docs/ANTI_ABUSE_BOND.md` §7.1). `None` on nodes without bonds and /// on rows written before the field existed (`#[serde(default)]`). #[serde(default)] pub bond: Option, /// The buyer's **per-order trade pubkey**, as the daemon stated it. /// /// Not the same as [`Self::counterparty_pubkey`], which holds the maker's /// order-book key for a taker and nothing at all for a maker. The Cashu /// escrow is locked to these keys, and the daemon re-derives them from the /// order and rejects a proof that names any others — so this is the only /// value that can be used to build one. /// /// `None` until the daemon sends a reply carrying an order payload. #[serde(default)] pub buyer_trade_pubkey: Option, /// The seller's per-order trade pubkey. See [`Self::buyer_trade_pubkey`]. #[serde(default)] pub seller_trade_pubkey: Option, // ── Cashu escrow (phase C5) ────────────────────────────────────────────── // // All `None` on a Lightning trade, and on every trade that predates this // field. `TradeInfo` is persisted as a JSON blob, so adding optional fields // needs no migration — but they are `#[serde(default)]` so a row written by // an older build still deserializes. /// Mint the escrow was locked at. Recorded per trade rather than read back /// from settings: a node may change its mint, and a trade must still be /// settleable at the mint its funds actually sit in. #[serde(default)] pub cashu_mint_url: Option, /// The 2-of-3 escrow token the seller locked. Kept so the seller can /// re-submit after an interrupted send, and so either party can settle or /// reclaim without asking the daemon for it again. #[serde(default)] pub cashu_escrow_token: Option, /// Unix timestamp (seconds) when the escrow was locked. The locktime /// refund window is counted from the node's advertised locktime, not from /// this — this is for display and for ordering. #[serde(default)] pub cashu_locked_at: Option, /// Escrow tokens the daemon rejected for good (`invalid_cashu_token`, /// `invalid_mint_url`): it did not store them, so a retry must build a new /// one. Kept, never dropped — each is the seller's money, reclaimable /// through the refund path once its locktime passes. #[serde(default)] pub cashu_rejected_escrow_tokens: Vec, } /// Who posted the bond — a *posting-timing* role, not the buyer/seller side /// (`docs/ANTI_ABUSE_BOND.md` §2.3). #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] pub enum BondRole { Maker, Taker, } /// Client-side view of a bond's lifecycle. The daemon owns the real state /// machine; this mirrors what the client can observe from the wire. #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] pub enum BondState { /// `pay-bond-invoice` received; the bolt11 has not been paid. Requested, /// Paid. Inferred from the first trade-flow message after the request — /// the daemon sends no explicit "bond locked" message. Locked, /// The trade ended without a slash notice: the HTLC was cancelled and the /// sats never left the user's wallet. Released, /// `bond-slashed` received for this order. Slashed, } /// The bond the daemon asked this user to lock for one trade. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct BondInfo { pub role: BondRole, /// Bond amount in satoshis, as sent by the daemon (never computed here). pub amount_sats: u64, /// The bond bolt11. Persisted so a restart lands back on the pay screen; /// `None` only after a fresh-device restore, which carries no invoice. pub invoice: Option, pub state: BondState, /// Unix seconds when `pay-bond-invoice` was received. pub requested_at: i64, /// Unix seconds when the bolt11 stops being payable, decoded from the /// invoice itself. `None` when it could not be decoded — then no local /// expiry runs. pub expires_at: Option, /// Unix seconds when the bond was inferred locked. pub locked_at: Option, } /// Whether the active node enforces anti-abuse bonds. Three states on /// purpose: an old daemon that publishes no `bond_enabled` tag is /// `Unsupported`, which is not the same as a node that turned the feature off. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] pub enum BondPolicy { /// No `bond_enabled` tag: the daemon predates the feature. #[default] Unsupported, /// `bond_enabled = false`. Disabled, /// `bond_enabled = true`; the other fields of [`BondPolicyInfo`] are live. Enabled, } /// Which side of a trade must lock a bond (`bond_apply_to` tag). #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum BondApplyTo { /// Only the taker, at take time. Take, /// Only the maker, before the order is published. Make, /// Both sides. Both, } /// The bond policy a node advertises in its kind 38385 info event /// (`docs/ANTI_ABUSE_BOND.md` §3.4). Every parameter is `None` unless /// `policy == Enabled` **and** the tag parsed within its valid range, so a /// consumer can key off nullability alone. #[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)] pub struct BondPolicyInfo { pub policy: BondPolicy, pub apply_to: Option, /// `bond_amount_pct` as the wire **fraction** (`0.01` = 1 %), `>= 0`. pub amount_pct: Option, /// `bond_base_amount_sats`: floor of the bond, in sats. pub base_amount_sats: Option, /// Whether a missed waiting-state timeout can slash a bond on this node. pub slash_on_waiting_timeout: Option, /// Fraction of a slashed bond the node keeps, in `[0, 1]`. pub slash_node_share_pct: Option, /// Days the winning counterparty has, from the slash, to claim its share. pub payout_claim_window_days: Option, } /// A trade lifecycle change pushed from Rust so the UI does not have to poll /// for it. Emitted on every daemon-driven status sync — cancellations /// (including the wipe of a never-active trade, whose DB row no longer /// exists by the time this arrives, so polling could never observe the /// transition) as well as progression statuses like `WaitingBuyerInvoice` /// and `WaitingPayment`, which screens use to react to the daemon's /// add-invoice / pay-invoice requests. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TradeUpdate { pub order_id: String, pub status: OrderStatus, /// Why the status changed, when the wire action alone is ambiguous /// (`docs/ANTI_ABUSE_BOND.md` §6.1), or what happened when it did not /// change at all (a cooperative-cancel request). `None` from every /// emitter that has nothing to add. #[serde(default)] pub reason: Option, /// When the change happened, in Unix seconds: the daemon message's own /// `created_at` for a Kind 14 dispatch, the local clock for everything /// else. A history replay after a restore re-emits old transitions, and /// this is what tells them apart from new ones (issue #474). #[serde(default)] pub occurred_at: i64, } /// One change to the order book, as `on_order_deltas` delivers it. /// /// **How to consume it:** subscribe first, then read /// `get_order_book_snapshot()`, then apply only deltas whose `revision` is /// greater than the snapshot's (and than the last one applied). A delta at or /// below it is already inside the snapshot; applying it could resurrect an /// order that was removed since. On [`OrderDelta::Resync`], read a fresh /// snapshot and carry on with the same rule. // Nearly every delta is an `Upserted`, so boxing the order would add an // allocation per delta without making the typical value any smaller. #[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum OrderDelta { /// `order` was added or changed. Upserted { revision: u32, order: OrderInfo }, /// The order with this id left the book. Removed { revision: u32, order_id: String }, /// What happened cannot be told order by order: the book was replaced or /// cleared (a node switch), or this subscriber fell behind and deltas /// were dropped. Resync, /// The relay finished replaying the node's stored pending orders: the /// book as the consumer has it is complete, so an empty one is really /// empty. Without this a quiet node never produces a delta, and a screen /// waiting for one to leave its loading state waits forever. Changes /// nothing in the book; may arrive more than once (one per relay). Loaded, } /// The whole book — every status, as the snapshot stream carries it — and the /// revision it was read at. See [`OrderDelta`]. #[derive(Debug, Clone)] pub struct OrderBookSnapshot { pub revision: u32, pub orders: Vec, /// Whether the relay already finished replaying the node's stored pending /// orders into this book — what [`OrderDelta::Loaded`] announces when it /// happens. A consumer created afterwards never hears that event, so it /// reads the fact here: with `loaded`, an empty `orders` is really empty. /// Back to `false` when the book is cleared for another node. pub loaded: bool, } /// A step of an account restore, pushed by `api::restore_progress` while /// `recover_trades` runs, so the restore sheet can show which stage is in /// flight. The outcome itself is `recover_trades`' result, not an event here. #[derive(Debug, Clone, PartialEq, Eq)] pub enum RestoreProgress { /// The restore request reached at least one relay. Connected, /// The node answered. `found` is every order and dispute it returned; /// `to_load` is how many of them the app fetches the details of. Found { found: u32, to_load: u32 }, /// `done` of the `to_load` orders have their details. A restore that /// ends with `done < to_load` recovered only part of them. Loaded { done: u32, to_load: u32 }, } /// "Read this trade again" — the doorbell of `api::trade_touch`. Unlike a /// [`TradeUpdate`] it says nothing about what changed and drives no /// notification; it only tells a screen its copy may be stale. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TradeTouch { /// The order whose book entry or trade row was written. `None` means the /// subscriber fell behind and touches were dropped: re-read every trade. pub order_id: Option, } /// The cause behind a `TradeUpdate` whose wire action carries none. /// /// A daemon `canceled` during the taker's bond window means one of three /// things and the message does not say which; the local order book and the /// pending-cancel registry do. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum TradeUpdateReason { /// This client sent the cancel itself. UserCanceled, /// The maker cancelled the order (its wire status is `canceled`). MakerCanceled, /// Another taker locked their bond first: the order left the `pending` /// bucket (or is still there for someone else to take). BondLostRace, /// The bond bolt11 expired unpaid; the local row was closed. BondExpired, /// This side asked to cancel an active trade; the status is unchanged /// until the counterparty also cancels (protocol `cancel.md`, "Cancel /// cooperatively"). Emitted on the daemon's /// `cooperative-cancel-initiated-by-you`. CooperativeCancelRequestedByMe, /// The counterparty asked to cancel; this side decides whether to /// cancel too. Emitted on `cooperative-cancel-initiated-by-peer`. CooperativeCancelRequestedByPeer, /// Not a step this client just learned of: a status Rust re-states so /// the screens read the trade again: a restore filing an old trade, a /// re-read after the peer's reputation arrived. A restore's is dated by /// the local clock, so its `occurred_at` cannot tell it from news; this /// does (#770). Not the startup sweep's cancel: that is the daemon's /// `Canceled` learned late, news like the message it stands in for. Replayed, /// The node could not pay the buyer and asks for a new invoice: an /// `add-invoice` on a settled escrow (mostrod `check_failure_retries`). /// Rides on `WaitingBuyerInvoice`, like the first request, so the card /// and the screens can tell the two apart (#823). PayoutFailed, /// The node published this client's order after `create_order` had /// returned: the maker's bond locked (`WaitingMakerBond → Pending`), or /// the confirmation came after the create timed out. Rides on `Pending`, /// which raises no card on its own (the book's bucket carries it too), /// so the card can tell the publication apart. OrderPublished, } /// An image or file sent in a chat (#589), as read from v1's JSON message. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AttachmentInfo { /// Sanitized: the last path component only, safe to show and save under. pub file_name: String, /// As declared by the sender; a label only. pub mime_type: String, /// Size of the file before encryption, in bytes. pub file_size: u64, pub file_type: FileType, pub download_status: DownloadStatus, /// Where the encrypted blob lives (`https://…/`). #[serde(default)] pub blossom_url: String, /// Hex SHA-256 of the encrypted blob, from the URL. #[serde(default)] pub sha256: String, #[serde(default)] pub encrypted_size: u64, /// Pixel size, for images: lets the bubble keep its shape before the /// image is decrypted. #[serde(default)] pub width: Option, #[serde(default)] pub height: Option, /// For a file we sent: the pubkey it was encrypted to — the peer, or the /// solver in the dispute chat. Never read from the wire. Kept because /// our own message names only us as its sender, and a resolved /// dispute's solver key is gone after a restart (PR #596 review). #[serde(default)] pub counterpart_pubkey: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ChatMessage { pub id: String, pub trade_id: String, pub sender_pubkey: String, pub content: String, pub message_type: MessageType, pub is_mine: bool, pub is_read: bool, pub has_attachment: bool, pub attachment: Option, pub created_at: i64, /// Reactions to this message (protocol chat.md, "Reactions"), at most one /// per party: the newest that party sent. One with an empty `emoji` was /// withdrawn; it is kept so a re-wrapped older reaction changes nothing. /// Older stored messages have none, hence the default. #[serde(default)] pub reactions: Vec, } /// A party's reaction to a chat message: an inner kind 7 event naming the /// message by its inner id. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct ChatReaction { /// Trade pubkey of the party who reacted, from the verified inner event. pub sender_pubkey: String, /// The emoji, or empty when the reaction was withdrawn. pub emoji: String, /// Inner `created_at`: of a party's reactions to one message, the newest /// holds. pub created_at: i64, /// Inner event id: breaks a tie between two reactions of the same second. pub event_id: String, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RelayInfo { pub url: String, pub is_active: bool, pub is_default: bool, pub source: RelaySource, pub is_blacklisted: bool, pub status: RelayStatus, pub last_connected_at: Option, pub last_error: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TradeHistoryEntry { pub id: String, pub order_kind: OrderKind, pub fiat_amount: Option, pub fiat_amount_min: Option, pub fiat_amount_max: Option, pub fiat_code: String, pub amount_sats: Option, pub payment_method: String, pub counterparty_pubkey: String, pub outcome: TradeOutcome, pub completed_at: i64, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct IdentityInfo { pub public_key: String, pub display_name: Option, /// Authoritative privacy mode flag. The Settings `privacy_mode` is a /// read-only mirror of this value. pub privacy_mode: bool, pub trade_key_index: u32, pub created_at: i64, } /// Deterministic pseudonymous identity derived from a public key. /// /// **Rendering contract**: The icon MUST always be rendered in white /// (`Colors.white`) over the HSV-colored background circle. The v1 /// implementation had a bug where the icon color matched the background, /// making it invisible. v2 MUST always use white icon color regardless of /// `color_hue` (FR-011c). #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct NymIdentity { /// Deterministic pseudonym in adjective-noun format. pub pseudonym: String, /// Icon selector (0–36). pub icon_index: u8, /// HSV hue (0–359) for the avatar background circle. pub color_hue: u16, } /// What a payment destination typed, pasted or scanned by the user turns out /// to be (`api::invoice::classify_payment_destination`). The one place that /// decides "is this an invoice or a Lightning address": the add-invoice /// screen, `send_invoice` and the NWC payer all ask it. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum PaymentDestination { /// Nothing but whitespace (or a bare `lightning:` scheme). Empty, /// A well-formed, correctly signed BOLT11 invoice. Bolt11(Bolt11Summary), /// Starts like an invoice (`lnbc…` / `lntb…`) but does not decode: a /// typo or a truncated copy. MalformedBolt11, /// `user@domain` (LUD-16), normalized to lower case. LightningAddress(String), /// Anything else — including an LNURL, which the submission path does /// not resolve. Unknown, } /// Why a buyer invoice would be refused, locally or by the daemon. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum InvoiceProblem { /// Neither an invoice nor a Lightning address. Unrecognized, /// Starts like an invoice but does not decode. Malformed, /// Decodes, but its amount is not the trade's. WrongAmount, /// Its expiry has already passed. Expired, /// Unexpired, but with less remaining lifetime than the node demands /// (`invoice_expiration_window`): mostrod refuses it as invalid. ExpiresTooSoon, /// Decodes, but for another chain than the node's. WrongNetwork, } /// The verdict on a buyer's payment destination before it is submitted /// (`api::invoice::check_buyer_invoice`). Mirrors what mostrod's /// `is_valid_invoice` will decide, so the user hears it here first. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum InvoiceVerdict { /// Nothing typed: nothing to say and nothing to submit. Empty, /// Nothing to say locally — an open-amount invoice, or the trade amount /// is not known yet — so submission is allowed and the daemon decides. Unverified, /// A Lightning address, resolved into an invoice on submission. Address, /// A BOLT11 invoice for exactly `sats`, unexpired, on the node's chain. /// `expires_at` (unix seconds) lets the caller re-judge it before the /// verdict goes stale: it stops being valid `min_remaining_secs` before /// that moment. `u64`, not `i64`: an `i64` inside a bridge enum is a /// Dart `int` in the generated union but a `BigInt` on the web, and /// dart2js refuses the mismatch — a `u64` is a `BigInt` everywhere. Valid { sats: u64, expires_at: u64 }, /// Refused. The optional fields carry what the copy needs to name. Rejected { problem: InvoiceProblem, /// `WrongAmount`: what the invoice asks for, in msat (a sub-sat /// remainder must not be rounded into a match). actual_msat: Option, /// `WrongAmount`: what the trade pays. expected_sats: Option, /// `WrongNetwork`: the invoice's chain, in LND naming. invoice_network: Option, /// `WrongNetwork`: the node's chain, in LND naming. node_network: Option, /// `ExpiresTooSoon`: the node's minimum remaining lifetime, seconds. min_remaining_secs: Option, }, } /// What the add-invoice screen needs from a BOLT11 invoice to validate it /// before submission (see `api::invoice::decode_bolt11`). #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Bolt11Summary { /// Millisatoshis, or `None` for an invoice that leaves the amount open. /// Kept in msat so a sub-sat remainder is never rounded into a match. pub amount_msat: Option, /// Unix seconds after which the invoice can no longer be paid. pub expires_at: i64, /// The chain the invoice is for, in LND's naming (`mainnet`, `testnet`, /// `regtest`, `signet`, `simnet`) so it compares against the node's /// `lnd_networks`. pub network: String, } /// Where a buyer taking a sell order asks the node to send the sats (#823). /// /// Chosen on the take screen. Without a choice `take_order` sends the default /// Lightning address from Settings, as it always did. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum TakePayout { /// The default Lightning address from Settings, when one is set. DefaultAddress, /// Another Lightning address, for this trade only. Address { address: String }, /// No address: the node asks for an invoice once the trade starts. AskForInvoice, } /// How a payout destination was chosen (#823). #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum PayoutSource { /// The default Lightning address from Settings, sent with the take. DefaultLightningAddress, /// An invoice the connected NWC wallet generated and the app submitted /// on its own. NwcWallet, /// An invoice or Lightning address the buyer entered for this trade. Entered, } /// What became of one payout destination (#823). #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum PayoutOutcome { /// Sent to the node; no news of the payout yet. Submitted, /// The node's first payment failed and it is retrying. Retrying, /// The node gave up on it and asked for a new invoice. Failed, /// The node refused it when it was submitted. Rejected, /// The node acknowledged a later destination in its place, before /// paying anything. Replaced, /// The node paid it: the trade completed. Paid, /// The node paid, but more than one destination was still open and its /// message does not say which one: this one or another marked the same. MaybePaid, } /// One destination the buyer's payout was sent to, as this device submitted /// it (#823). Read with `orders::payout_destinations`, oldest first. /// /// Recorded locally, so a trade restored on another device has none: the /// node does not say where it paid. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct PayoutDestination { /// The Lightning address or the BOLT11 invoice, as sent. pub destination: String, pub is_lightning_address: bool, pub source: PayoutSource, /// The NWC wallet's name, when the wallet gave one and made the invoice. pub wallet_name: Option, /// Unix seconds, local clock, when the destination was sent. pub submitted_at: i64, /// The sats the destination receives: the invoice's amount, or the /// payout amount once the node said it. `None` while unknown. pub amount_sats: Option, /// Hex payment hash of an invoice; `None` for a Lightning address, /// whose invoice only the node sees. pub payment_hash: Option, pub outcome: PayoutOutcome, /// Unix seconds of the node message that set [`Self::outcome`] (node /// clock), or of the rejection (local clock). `None` while submitted. pub outcome_at: Option, /// How many times the node tries to pay before it gives up, from its /// `payment-failed` notice. `None` until one arrives. pub attempts: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct LogEntry { pub id: u32, pub level: LogLevel, pub tag: String, pub message: String, pub timestamp: i64, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AppState { pub connection: ConnectionState, pub has_identity: bool, pub has_active_trade: bool, pub has_nwc_wallet: bool, pub unread_messages: u32, pub pending_queue_count: u32, pub theme: ThemeMode, /// Read-only mirror of `IdentityInfo.privacy_mode`. pub privacy_mode: bool, pub logging_enabled: bool, } /// Mostro daemon node information (name, version, fees, limits, currencies). /// /// Intentionally retained though currently unused: this models the daemon's /// kind 38385 *instance status*, which today reaches the UI as raw tags /// (`fetch_mostro_instance_tags`) parsed on the Dart side. The node registry /// shipped on [`MostroNodeEntry`] (operator profile from kind 0) instead; /// this stays as the Rust-side model for whenever that 38385 parsing moves /// behind the bridge. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MostroNodeInfo { pub pubkey: String, pub name: Option, pub version: Option, /// Pending order lifetime in hours. Defaults to 24 if omitted by daemon. #[serde(default = "default_expiration_hours")] pub expiration_hours: u32, /// Waiting-state timeout in seconds. Defaults to 900 if omitted by daemon. #[serde(default = "default_expiration_seconds")] pub expiration_seconds: u32, pub fee_pct: Option, pub max_order_amount: Option, pub min_order_amount: Option, pub supported_currencies: Option>, pub ln_node_id: Option, pub ln_node_alias: Option, pub is_active: bool, } fn default_expiration_hours() -> u32 { 24 } /// One entry of the Mostro node registry shown in Settings → Mostro Node. /// /// Identity (pubkey) and trust/region come from the compiled-in registry or /// user additions; display metadata (name, picture, about, website) comes from /// the node operator's Nostr kind 0 event and may lag or be absent. Distinct /// from [`MostroNodeInfo`], which models the daemon's kind 38385 instance /// status rather than the operator's profile. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MostroNodeEntry { /// Node pubkey, 64-char lowercase hex. pub pubkey: String, /// Region label (flag emoji + place name) for trusted nodes, `None` for /// user-added ones. pub region: Option, /// `true` when the entry comes from the compiled-in trusted registry. pub is_trusted: bool, /// `true` when this is the currently active node. pub is_active: bool, /// Display name: user-given (custom nodes) or from kind 0 metadata. pub name: Option, /// Avatar URL from kind 0 metadata (https only; anything else is dropped). pub picture: Option, /// Operator description from kind 0 metadata. pub about: Option, /// Website URL from kind 0 metadata. pub website: Option, } /// A node's kind 0 picture as the device keeps it (`crate::node_pictures`): /// the bytes as downloaded and the URL they came from, so the UI shows them /// only while the profile still names that URL. #[derive(Debug, Clone, PartialEq)] pub struct MostroNodePicture { pub url: String, pub bytes: Vec, } fn default_expiration_seconds() -> u32 { 900 } /// Rating submitted or received for a completed trade. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RatingInfo { /// The trade this rating belongs to. pub trade_id: String, /// Star score (1–5). pub score: u8, /// `true` if the local user submitted this rating. pub is_mine: bool, /// Unix timestamp (seconds) when the rating was submitted. pub created_at: i64, } /// Event emitted when the counterparty submits a rating for the local user. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RatingReceivedEvent { /// The trade this rating belongs to. pub trade_id: String, /// Star score submitted by the counterparty (1–5). pub score: u8, /// Nostr public key (hex) of the rater. pub from_pubkey: String, } /// Cause of an anti-abuse bond slash, inferred from the tracked order state. /// /// The wire message carries no `reason`, so the cause is inferred client-side /// (see `crate::api::bond::infer_slash_cause`). #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] pub enum SlashCause { /// The bonded party let a waiting-state timeout elapse. Timeout, /// A solver directed the slash while resolving a dispute. Dispute, } /// Event emitted when the local user's anti-abuse bond is slashed. /// /// Best-effort and informational only: the hold invoice is already settled and /// the user keeps no claim over the forfeited sats. `amount_sats` is the /// **slashed bond amount**, not the trade amount — the tracked order's real /// status and amount are deliberately left untouched. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BondSlashedEvent { /// Stable identity of the source daemon event. The daemon replays stored /// history on reconnect/restart, so consumers key the notification on this /// id to persist exactly one record per slash. pub event_id: String, /// The order whose bond was slashed. pub order_id: String, /// Slashed bond amount, in satoshis. pub amount_sats: u64, pub fiat_code: String, pub fiat_amount: i64, pub payment_method: String, /// Inferred cause (timeout vs dispute). pub cause: SlashCause, } /// Where a payout claim stands (docs/ANTI_ABUSE_BOND.md §6.4): the daemon /// asked for an invoice, the user sent one, the daemon accepted it, the /// share was paid — or the claim window closed first. #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum BondClaimPhase { /// `add-bond-invoice` received, no invoice sent (or the daemon re-prompted). Pending, /// The user's bolt11 was published; the daemon has not answered yet. Submitted, /// `bond-invoice-accepted`: the payout is in progress. Acknowledged, /// `bond-payout-completed`: the share was paid. Completed, /// The claim window closed unclaimed. Expired, } impl BondClaimPhase { /// A phase nothing follows: the daemon stops retrying and the kind-14 /// filter no longer needs the issuing node for this claim. pub fn is_terminal(&self) -> bool { matches!(self, BondClaimPhase::Completed | BondClaimPhase::Expired) } } /// The counterparty's share of a slashed bond this user may claim /// (docs/ANTI_ABUSE_BOND.md §6.4, §7.1). Independent of the trade row: the /// winner's trade may be completed, cancelled or wiped by the time the /// daemon asks for an invoice. Keyed by `(node_pubkey, order_id)`: the user /// can switch nodes while a claim is open, and the submission always /// addresses the daemon that issued it. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct BondClaim { pub order_id: String, /// The daemon that issued the claim; the submission target. pub node_pubkey: String, /// The trade key index the daemon addressed the request to: the slashed /// attempt's key, which the reply must come from even when the order was /// retaken on a newer key since. `None` only for a claim stored before it /// was recorded; the order's current key is used then. #[serde(default)] pub trade_index: Option, /// The share on offer, in satoshis; the invoice must be for exactly this. pub amount_sats: u64, /// Unix seconds when the daemon slashed the bond, from the request. pub slashed_at: i64, /// `slashed_at + claim window`, frozen when the claim is first persisted: /// a later policy change cannot move a deadline the user was shown. pub deadline_at: i64, pub phase: BondClaimPhase, /// The bolt11 sent, kept so the screen can show it while the daemon answers. pub submitted_invoice: Option, /// Display only, from the request's order. pub fiat_code: String, pub fiat_amount: Option, pub payment_method: String, /// Unix seconds of the last change, the list's sort key. pub updated_at: i64, } /// Why replacing the identity now would cost the user something (issue /// #533). A marker, not prose: Dart localizes it. #[derive(Debug, Clone, PartialEq, Eq)] pub enum FundsAtRiskReason { /// The user is the seller and the hold invoice is paid and held; only a /// `release` signed with this trade's key moves those sats. SellerEscrowLocked, /// An anti-abuse bond is locked; it is given back when its trade ends. BondLocked, /// A slashed-bond payout the user won and has not been paid yet. PayoutClaimOpen, /// A live trade with none of the user's sats locked — a buyer mid-trade, /// or either side before the escrow is funded. TradeInProgress, /// A bond invoice that can still be paid: nothing is locked yet. BondInvoicePending, /// Ecash in this identity's Cashu wallet. Its proof store opens only /// under this identity, so only these words bring it back. Not tied to an /// order: `order_id` is empty. CashuWalletBalance, } /// One thing the current identity still has in flight, as listed by /// `funds_at_risk()` before a new user is generated or a seed imported. #[derive(Debug, Clone, PartialEq)] pub struct FundsAtRisk { pub order_id: String, pub reason: FundsAtRiskReason, /// The sats concerned, when known: the escrow, the bond or the payout. pub amount_sats: Option, } impl BondClaim { /// The storage key: `:`. pub fn storage_id(&self) -> String { bond_claim_key(&self.node_pubkey, &self.order_id) } } /// The `bond_claims` key for a node / order pair. pub fn bond_claim_key(node_pubkey: &str, order_id: &str) -> String { format!("{node_pubkey}:{order_id}") } /// A claim's phase changed (new claim, submission, ack, payout, expiry). #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct BondClaimUpdate { pub order_id: String, /// The node that issued the claim: two nodes can hold a claim for the /// same order, and a consumer must read the one that changed. pub node_pubkey: String, pub phase: BondClaimPhase, } /// Connected wallet information returned by `connect_wallet` and `get_wallet`. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct NwcWalletInfo { /// Wallet service Nostr public key (hex). pub wallet_pubkey: String, /// Human-readable wallet name/alias, if provided by the service. pub wallet_name: Option, /// Current connection status. pub status: WalletStatus, /// Balance in satoshis; `None` if the wallet does not expose balance. pub balance_sats: Option, /// NWC relay URL(s). pub relay_urls: Vec, /// Unix timestamp of the last successful connection. pub last_connected_at: Option, } /// Result returned by `pay_invoice`. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentResult { /// Whether the payment succeeded. pub success: bool, /// BOLT-11 payment preimage (hex), present on success. pub preimage: Option, /// Human-readable error message, present on failure. pub error: Option, } /// An open or resolved dispute on a trade. /// /// Created locally when the user initiates a dispute or when a peer-initiated /// dispute notification arrives. Status updated as admin actions are received. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Dispute { /// Unique dispute identifier (generated locally or from protocol event). pub id: String, /// The trade this dispute belongs to. pub trade_id: String, /// Current dispute lifecycle status. pub status: DisputeStatus, /// `true` if the local user opened the dispute. pub initiated_by_me: bool, /// Optional free-text reason supplied when opening the dispute. pub reason: Option, /// Admin's Nostr public key (hex), populated when `adminTookDispute` /// is received and ECDH admin shared key is derived. pub admin_pubkey: Option, /// Resolution outcome, populated when status becomes `Resolved`. pub resolution: Option, /// Unix timestamp (seconds) when the dispute was opened. pub opened_at: i64, /// Unix timestamp (seconds) when the dispute was resolved; `None` while /// still open. pub resolved_at: Option, /// Whether the local user has seen the latest dispute update. pub is_read: bool, /// Whether this side already sent the current solver the chat key /// (#415). A takeover clears it: the new solver never got the key. pub chat_key_shared: bool, } /// State of the embedded Cashu wallet — phase C2 of `docs/cashu/README.md`. /// /// Reported for every node, including Lightning ones, where it is simply /// "not connected": the UI asks before it knows what the node runs. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CashuWalletStatus { /// Whether a wallet is bound to a mint right now. False on any Lightning /// node, and before the first successful connect on a Cashu one. pub connected: bool, /// The mint the wallet is bound to, when connected. pub mint_url: Option, /// Spendable balance in satoshis, or `None` when it could not be read. /// /// `None` and `Some(0)` are different facts and only one of them is /// alarming: ecash is bearer money, and showing a user "0 sat" because a /// store read failed invites exactly the wrong reaction. The UI must render /// the unknown case as unknown. pub balance_sats: Option, /// Stable markers for anything the mint failed to advertise (`"nut07"`, /// `"nut11"`, `"nut12"`, `"sat_keyset"`). Empty on a healthy connection — /// a mint missing any of them is refused at connect, so a non-empty list /// here means the wallet is bound to a mint that has since changed. pub missing_capabilities: Vec, /// Whether the mint melts to `bolt11` invoices (NUT-05), so the wallet /// can pay one — phase C3b-1. False when not connected. Optional: a mint /// without it is still usable, and the UI hides "to Lightning" instead. pub can_send_lightning: bool, } /// What paying a Lightning invoice from the Cashu wallet costs — phase C3b-1. /// /// Shown for confirmation before anything is spent. Nothing is reserved /// while it is on screen; [`crate::api::cashu::cashu_melt`] prices the quote /// again and refuses one that now costs more than `max_total_sats`. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CashuMeltQuote { /// The mint's quote id, handed back to pay it. pub quote_id: String, /// What the invoice asks for. pub amount_sats: u64, /// The most the mint may spend on Lightning routing; what it does not /// spend comes back to the wallet. pub fee_reserve_sats: u64, /// The mint's input fees on the ecash spent (a swap's included). pub input_fee_sats: u64, /// The most the payment can take from the balance: amount, fee reserve /// and input fees. "Up to", not exact: unspent reserve is returned. pub max_total_sats: u64, } /// How a confirmed payment to a Lightning invoice ended — phase C3b-1. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum CashuMeltResult { /// The invoice is paid. `fee_paid_sats` is what it actually cost beyond /// the invoice: routing plus the mint's input fees. Paid { amount_sats: u64, fee_paid_sats: u64, }, /// How it ended is not known yet: the mint is still routing it, or did /// not answer after the request left. Its ecash stays reserved until the /// mint says, at the next sync. Pending, } /// What a seller is about to lock into a Cashu escrow — phase C5. /// /// Shown before the seller commits anything. The amount comes from the order; /// the fee is derived from the node's advertised rate and must match what the /// daemon computed to the satoshi, so it is surfaced rather than hidden. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CashuEscrowQuote { pub order_id: String, /// The escrow itself: exactly the order amount. pub amount_sats: u64, /// The Mostro fee funded with the lock. Zero until the daemon collects a /// fee token (its TA-1f): today it ignores one, so building it would only /// cost the seller. pub fee_sats: u64, /// `amount_sats + fee_sats` — what the wallet must actually hold. pub total_sats: u64, /// Spendable balance right now, so the UI can say "fund your wallet" /// instead of failing at the mint. pub balance_sats: u64, /// Mint the escrow will be locked at. pub mint_url: String, /// Days the escrow stays locked before the seller can reclaim it alone. pub locktime_days: u32, /// An escrow is already locked for this trade and recorded, but the node /// has not confirmed it: the next `lock_escrow` re-sends that same token /// and swaps nothing. pub pending_submission: bool, } /// The settlement backend the active Mostro node runs, as resolved by /// [`crate::mostro::escrow_mode`] with the developer overrides applied. /// /// Phase C1b of `docs/cashu/README.md`. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct EscrowModeInfo { /// Stable marker — `"unknown"`, `"lightning"` or `"cashu"`. Rust does not /// translate; Dart maps this to a localized string. pub mode: String, /// The one mint every escrow on the node is locked at, override applied: /// set only when the node accepts exactly one (the wallet binds to it). /// `None` on a Lightning node, and on a Cashu node that accepts several /// mints or any. pub mint_url: Option, /// Every mint the node accepts (MostroP2P/mostro#1047), override applied. /// Meaningful only when [`Self::mode`] is `"cashu"`, where empty means the /// node accepts any mint. pub mint_urls: Vec, /// NUT-11 locktime the seller must set, in days. pub escrow_locktime_days: Option, /// How close to expiry the daemon stops accepting `fiat-sent`, in days. pub settlement_margin_days: Option, /// True when [`Self::mode`] came from the developer override rather than /// the node's own tags. pub is_overridden: bool, /// **The gate.** True only when the mode is Cashu *and* the node pins one /// mint for the wallet to bind to. `mode == "cashu"` alone is not enough — /// a node can accept several mints, or any. pub is_cashu_available: bool, /// Developer override state, mirrored so the dev-only settings surface can /// render its own controls without a second call. pub force_cashu_override: bool, /// Mint URL override as stored, independent of what the node advertises. pub mint_url_override: Option, } /// Aggregated user-facing application settings. /// /// `privacy_mode` is a read-only mirror of `IdentityInfo.privacy_mode` — /// the authoritative value lives in the Identity layer. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AppSettings { pub theme: ThemeMode, /// BCP-47 language tag, e.g. `"en"`, `"es"`. pub language: String, /// Currency code selected as the user's default, if any: ISO 4217 or a /// stablecoin code (3 to 5 upper-case letters). pub default_fiat_code: Option, /// Default Lightning Address in `user@domain` format, if set. pub default_lightning_address: Option, pub logging_enabled: bool, /// Read-only mirror of `IdentityInfo.privacy_mode`. pub privacy_mode: bool, }