Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub async fn init_db() -> Result<SqlitePool> {
trade_keys TEXT,
counterparty_pubkey TEXT,
order_chat_shared_key_hex TEXT,
dispute_id TEXT,
solver_pubkey TEXT,
dispute_chat_shared_key_hex TEXT,
is_mine INTEGER NOT NULL,
buyer_invoice TEXT,
request_id INTEGER,
Expand Down Expand Up @@ -167,6 +170,10 @@ async fn migrate_db(pool: &SqlitePool) -> Result<()> {
let has_last_seen_dm_ts = check_column_exists(pool, "orders", "last_seen_dm_ts").await?;
let has_order_chat_shared_key_hex =
check_column_exists(pool, "orders", "order_chat_shared_key_hex").await?;
let has_order_dispute_id = check_column_exists(pool, "orders", "dispute_id").await?;
let has_solver_pubkey = check_column_exists(pool, "orders", "solver_pubkey").await?;
let has_dispute_chat_shared_key_hex =
check_column_exists(pool, "orders", "dispute_chat_shared_key_hex").await?;

// Only run migration if at least one column is missing
if !has_initiator_info
Expand All @@ -181,8 +188,11 @@ async fn migrate_db(pool: &SqlitePool) -> Result<()> {
|| !has_trade_index
|| !has_last_seen_dm_ts
|| !has_order_chat_shared_key_hex
|| !has_order_dispute_id
|| !has_solver_pubkey
|| !has_dispute_chat_shared_key_hex
{
log::info!("Running migration: Adding missing columns to admin_disputes table");
log::info!("Running migration: adding missing database columns");

// Wrap all ALTER TABLE statements in a transaction for atomicity
let mut tx = pool.begin().await?;
Expand Down Expand Up @@ -317,6 +327,24 @@ async fn migrate_db(pool: &SqlitePool) -> Result<()> {
.await?;
}

if !has_order_dispute_id {
sqlx::query("ALTER TABLE orders ADD COLUMN dispute_id TEXT")
.execute(&mut *tx)
.await?;
}

if !has_solver_pubkey {
sqlx::query("ALTER TABLE orders ADD COLUMN solver_pubkey TEXT")
.execute(&mut *tx)
.await?;
}

if !has_dispute_chat_shared_key_hex {
sqlx::query("ALTER TABLE orders ADD COLUMN dispute_chat_shared_key_hex TEXT")
.execute(&mut *tx)
.await?;
}

tx.commit().await?;
log::info!("Migration completed successfully");
}
Expand Down Expand Up @@ -377,6 +405,9 @@ async fn orders_table_rebuild_without_suppress_column(pool: &SqlitePool) -> Resu
trade_keys TEXT,
counterparty_pubkey TEXT,
order_chat_shared_key_hex TEXT,
dispute_id TEXT,
solver_pubkey TEXT,
dispute_chat_shared_key_hex TEXT,
is_mine INTEGER NOT NULL,
buyer_invoice TEXT,
request_id INTEGER,
Expand All @@ -393,12 +424,14 @@ async fn orders_table_rebuild_without_suppress_column(pool: &SqlitePool) -> Resu
r#"
INSERT INTO orders_new (
id, kind, status, amount, fiat_code, min_amount, max_amount, fiat_amount,
payment_method, premium, trade_keys, counterparty_pubkey, order_chat_shared_key_hex, is_mine, buyer_invoice,
payment_method, premium, trade_keys, counterparty_pubkey, order_chat_shared_key_hex,
dispute_id, solver_pubkey, dispute_chat_shared_key_hex, is_mine, buyer_invoice,
request_id, trade_index, created_at, expires_at, last_seen_dm_ts
)
SELECT
id, kind, status, amount, fiat_code, min_amount, max_amount, fiat_amount,
payment_method, premium, trade_keys, counterparty_pubkey, order_chat_shared_key_hex, is_mine, buyer_invoice,
payment_method, premium, trade_keys, counterparty_pubkey, order_chat_shared_key_hex,
dispute_id, solver_pubkey, dispute_chat_shared_key_hex, is_mine, buyer_invoice,
request_id, trade_index, created_at, expires_at, last_seen_dm_ts
FROM orders;
"#,
Expand Down
60 changes: 57 additions & 3 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ pub struct Order {
pub counterparty_pubkey: Option<String>,
/// ECDH shared secret for P2P order chat (hex), derived once when both trade pubkeys are known.
pub order_chat_shared_key_hex: Option<String>,
/// Dispute UUID assigned by Mostro for this order.
pub dispute_id: Option<String>,
/// Trade pubkey of the solver that took the dispute.
pub solver_pubkey: Option<String>,
/// ECDH shared secret for the user-to-solver dispute chat.
pub dispute_chat_shared_key_hex: Option<String>,
/// Maker (`true`) vs taker (`false`). Matches `orders.is_mine` INTEGER NOT NULL (0/1).
pub is_mine: bool,
pub buyer_invoice: Option<String>,
Expand Down Expand Up @@ -262,6 +268,9 @@ impl Order {
trade_keys: Some(trade_keys_hex),
counterparty_pubkey: None,
order_chat_shared_key_hex: None,
dispute_id: None,
solver_pubkey: None,
dispute_chat_shared_key_hex: None,
is_mine: is_maker,
buyer_invoice: order.buyer_invoice,
request_id: _request_id,
Expand Down Expand Up @@ -299,8 +308,10 @@ impl Order {
r#"
INSERT INTO orders (id, kind, status, amount, min_amount, max_amount,
fiat_code, fiat_amount, payment_method, premium, is_mine,
trade_keys, counterparty_pubkey, order_chat_shared_key_hex, buyer_invoice, request_id, trade_index, created_at, expires_at, last_seen_dm_ts)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
trade_keys, counterparty_pubkey, order_chat_shared_key_hex,
dispute_id, solver_pubkey, dispute_chat_shared_key_hex,
buyer_invoice, request_id, trade_index, created_at, expires_at, last_seen_dm_ts)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(&self.id)
Expand All @@ -317,6 +328,9 @@ impl Order {
.bind(&self.trade_keys)
.bind(&self.counterparty_pubkey)
.bind(&self.order_chat_shared_key_hex)
.bind(&self.dispute_id)
.bind(&self.solver_pubkey)
.bind(&self.dispute_chat_shared_key_hex)
.bind(&self.buyer_invoice)
.bind(self.request_id)
.bind(self.trade_index)
Expand All @@ -334,7 +348,8 @@ impl Order {
UPDATE orders
SET kind = ?, status = ?, amount = ?, min_amount = ?, max_amount = ?,
fiat_code = ?, fiat_amount = ?, payment_method = ?, premium = ?,
is_mine = ?, trade_keys = ?, counterparty_pubkey = ?, order_chat_shared_key_hex = ?, buyer_invoice = ?,
is_mine = ?, trade_keys = ?, counterparty_pubkey = ?, order_chat_shared_key_hex = ?,
dispute_id = ?, solver_pubkey = ?, dispute_chat_shared_key_hex = ?, buyer_invoice = ?,
request_id = ?, trade_index = ?, created_at = ?, expires_at = ?, last_seen_dm_ts = ?
WHERE id = ?
"#,
Expand All @@ -352,6 +367,9 @@ impl Order {
.bind(&self.trade_keys)
.bind(&self.counterparty_pubkey)
.bind(&self.order_chat_shared_key_hex)
.bind(&self.dispute_id)
.bind(&self.solver_pubkey)
.bind(&self.dispute_chat_shared_key_hex)
.bind(&self.buyer_invoice)
.bind(self.request_id)
.bind(self.trade_index)
Expand Down Expand Up @@ -398,6 +416,10 @@ impl Order {
trade_keys: Some(trade_keys_hex),
counterparty_pubkey,
order_chat_shared_key_hex,
dispute_id: existing.and_then(|e| e.dispute_id.clone()),
solver_pubkey: existing.and_then(|e| e.solver_pubkey.clone()),
dispute_chat_shared_key_hex: existing
.and_then(|e| e.dispute_chat_shared_key_hex.clone()),
is_mine: existing.map(|e| e.is_mine).unwrap_or(true),
buyer_invoice: small_order.buyer_invoice.clone(),
request_id: message_request_id.or_else(|| existing.and_then(|e| e.request_id)),
Expand Down Expand Up @@ -535,6 +557,38 @@ impl Order {
Ok(())
}

/// Persist the dispute id announced by Mostro for a user order.
pub async fn update_dispute_id(
pool: &SqlitePool,
order_id: &str,
dispute_id: &str,
) -> Result<()> {
sqlx::query("UPDATE orders SET dispute_id = ? WHERE id = ?")
.bind(dispute_id)
.bind(order_id)
.execute(pool)
.await?;
Ok(())
}

/// Persist the assigned solver and the derived user-to-solver chat secret.
pub async fn update_solver_chat(
pool: &SqlitePool,
order_id: &str,
solver_pubkey: &str,
shared_key_hex: &str,
) -> Result<()> {
sqlx::query(
"UPDATE orders SET solver_pubkey = ?, dispute_chat_shared_key_hex = ? WHERE id = ?",
)
.bind(solver_pubkey)
.bind(shared_key_hex)
.bind(order_id)
.execute(pool)
.await?;
Ok(())
}

pub async fn get_startup_active_orders(
pool: &SqlitePool,
) -> Result<Vec<StartupActiveOrderRecord>> {
Expand Down
12 changes: 10 additions & 2 deletions src/ui/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::models::AdminDispute;
use crate::ui::admin_state::AdminMode;
use crate::ui::chat::{
AdminChatLastSeen, ChatParty, DisputeChatMessage, DisputeFilter, OrderChatLastSeen,
UserOrderChatMessage,
UserChatChannel, UserOrderChatMessage,
};
use crate::ui::helpers::OrderChatListItem;
use crate::ui::navigation::{Tab, UserRole};
Expand Down Expand Up @@ -207,11 +207,16 @@ pub struct AppState {
/// Maker `pending` listings on the book without a trade-DM row in Messages (refreshed on events).
pub my_trades_maker_book: Vec<OrderChatListItem>,
pub order_chats: HashMap<String, Vec<UserOrderChatMessage>>, // Chat messages per order id
/// User-to-solver dispute messages per order id.
pub user_dispute_chats: HashMap<String, Vec<UserOrderChatMessage>>,
/// Active My Trades conversation for the selected order.
pub active_user_chat_channel: UserChatChannel,
pub order_chat_scrollview_state: tui_scrollview::ScrollViewState,
pub order_chat_selected_message_idx: Option<usize>,
pub order_chat_line_starts: Vec<usize>,
pub order_chat_scroll_tracker: Option<(String, usize)>,
pub order_chat_scroll_tracker: Option<(String, UserChatChannel, usize)>,
pub order_chat_last_seen: HashMap<String, OrderChatLastSeen>,
pub user_dispute_chat_last_seen: HashMap<String, OrderChatLastSeen>,
pub pending_notifications: Arc<Mutex<usize>>, // Count of pending notifications (non-critical)
pub admin_disputes_in_progress: Vec<AdminDispute>, // Taken disputes
pub dispute_filter: DisputeFilter, // Filter for viewing InProgress or Finalized disputes
Expand Down Expand Up @@ -303,11 +308,14 @@ impl AppState {
order_chat_static: HashMap::new(),
my_trades_maker_book: Vec::new(),
order_chats: HashMap::new(),
user_dispute_chats: HashMap::new(),
active_user_chat_channel: UserChatChannel::Peer,
order_chat_scrollview_state: tui_scrollview::ScrollViewState::default(),
order_chat_selected_message_idx: None,
order_chat_line_starts: Vec::new(),
order_chat_scroll_tracker: None,
order_chat_last_seen: HashMap::new(),
user_dispute_chat_last_seen: HashMap::new(),
pending_notifications: Arc::new(Mutex::new(0)),
admin_disputes_in_progress: Vec::new(),
dispute_filter: DisputeFilter::InProgress, // Default to InProgress view
Expand Down
21 changes: 21 additions & 0 deletions src/ui/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ pub enum UserChatSender {
Peer,
}

/// User-facing chat channel selected in My Trades.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum UserChatChannel {
/// Chat with the order counterparty.
#[default]
Peer,
/// Chat with the solver assigned to the dispute.
Solver,
}

impl Display for UserChatChannel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Peer => write!(f, "Peer"),
Self::Solver => write!(f, "Solver"),
}
}
}

/// Type of file attachment (Mostro Mobile image_encrypted / file_encrypted).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChatAttachmentType {
Expand Down Expand Up @@ -114,6 +133,8 @@ pub struct OrderChatLastSeen {
#[derive(Clone, Debug)]
pub struct OrderChatUpdate {
pub order_id: String,
/// Conversation that should receive this batch.
pub channel: UserChatChannel,
/// Local trade public key for this order; used to skip relay echoes of our own sends.
pub local_trade_pubkey: PublicKey,
pub messages: Vec<DecodedChatMessage>,
Expand Down
2 changes: 2 additions & 0 deletions src/ui/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub const HELP_ORDERS_CANCEL_PENDING_MSG: &str =
// Help popup lines (My Trades)
pub const HELP_MY_TRADES_NAV: &str = "↑↓: Select order";
pub const HELP_MY_TRADES_ENTER_SEND: &str = "Enter: Send message (when input enabled)";
pub const HELP_MY_TRADES_TAB_CHAT: &str = "Tab: Switch Peer/Solver chat (after solver assignment)";
pub const HELP_MY_TRADES_SHIFT_I: &str = "Shift+I: Enable/disable message input";
pub const HELP_MY_TRADES_SHIFT_C_CANCEL: &str = "Shift+C: Cancel order (cooperative cancel)";
pub const HELP_MY_TRADES_SHIFT_F_FIAT_SENT: &str = "Shift+F: Mark fiat as sent (FiatSent message)";
Expand Down Expand Up @@ -174,6 +175,7 @@ pub const FOOTER_PGUP_PGDN_SCROLL_CHAT: &str = "PgUp/PgDn: Scroll Chat";
// --- Footer (My Trades / Order Chat) ---

pub const FOOTER_MYTRADES_SELECT_ORDER: &str = "↑↓: Select order";
pub const FOOTER_MYTRADES_TAB_CHAT: &str = "Tab: Peer/Solver chat";
pub const FOOTER_MYTRADES_ENTER_SEND: &str = "Enter: Send";
pub const FOOTER_MYTRADES_SHIFT_I_DISABLE: &str = "Shift+I: Disable input";
pub const FOOTER_MYTRADES_SHIFT_I_ENABLE: &str = "Shift+I: Enable input";
Expand Down
Loading