use crate::app::bond;
use crate::app::bond::TakerContext;
use crate::app::context::AppContext;
use crate::db::{buyer_has_pending_order, update_user_trade_index};
use crate::util::{
enqueue_order_msg, get_dev_fee, get_fiat_amount_requested, get_market_amount_and_fee,
get_order, set_waiting_invoice_status, show_hold_invoice, update_order_event, validate_invoice,
};
use mostro_core::prelude::*;
use nostr_sdk::prelude::*;
use sqlx::{Pool, Sqlite};
use sqlx_crud::Crud;
async fn update_order_status(
order: &mut Order,
my_keys: &Keys,
pool: &Pool<Sqlite>,
request_id: Option<u64>,
) -> Result<(), MostroError> {
let buyer_pubkey = order.get_buyer_pubkey().map_err(MostroInternalErr)?;
match set_waiting_invoice_status(order, buyer_pubkey, request_id).await {
Ok(_) => {
match update_order_event(my_keys, Status::WaitingBuyerInvoice, order).await {
Ok(order_updated) => {
let _ = order_updated.update(pool).await;
Ok(())
}
Err(_) => Err(MostroInternalErr(ServiceError::UpdateOrderStatusError)),
}
}
Err(_) => Err(MostroInternalErr(ServiceError::UpdateOrderStatusError)),
}
}
pub async fn take_sell_action(
ctx: &AppContext,
msg: Message,
event: &UnwrappedMessage,
my_keys: &Keys,
) -> Result<(), MostroError> {
let pool = ctx.pool();
let mut order = get_order(&msg, pool).await?;
let request_id = msg.get_inner_message_kind().request_id;
if buyer_has_pending_order(pool, event.identity.to_string()).await? {
return Err(MostroCantDo(CantDoReason::PendingOrderExists));
}
if let Err(cause) = order.is_sell_order() {
return Err(MostroCantDo(cause));
};
if order.check_status(Status::Pending).is_err()
&& order.check_status(Status::WaitingTakerBond).is_err()
{
return Err(MostroCantDo(CantDoReason::InvalidOrderStatus));
}
order
.not_sent_from_maker(event.sender)
.map_err(MostroCantDo)?;
let bond_required = bond::taker_bond_required();
if bond_required {
let active = crate::app::bond::db::find_active_bonds_for_order(pool, order.id).await?;
if active
.iter()
.any(|b| b.state == crate::app::bond::BondState::Locked.to_string())
{
return Err(MostroCantDo(CantDoReason::PendingOrderExists));
}
let sender_str = event.sender.to_string();
if let Some(existing) = active.iter().find(|b| b.pubkey == sender_str) {
if let Some(bolt11) = existing.payment_request.clone() {
let order_kind = order.get_order_kind().map_err(MostroInternalErr)?;
let bond_small = SmallOrder::new(
Some(order.id),
Some(order_kind),
Some(Status::Pending),
existing.amount_sats,
order.fiat_code.clone(),
order.min_amount,
order.max_amount,
existing.taker_fiat_amount.unwrap_or(order.fiat_amount),
order.payment_method.clone(),
order.premium,
None,
None,
None,
None,
None,
);
enqueue_order_msg(
request_id,
Some(order.id),
Action::PayBondInvoice,
Some(Payload::PaymentRequest(Some(bond_small), bolt11, None)),
event.sender,
existing.taker_trade_index,
)
.await;
}
return Ok(());
}
}
let seller_pubkey = order.get_seller_pubkey().map_err(MostroInternalErr)?;
if let Some(am) = get_fiat_amount_requested(&order, &msg) {
order.fiat_amount = am;
} else {
return Err(MostroCantDo(CantDoReason::OutOfRangeSatsAmount));
}
if order.has_no_amount() {
match get_market_amount_and_fee(order.fiat_amount, &order.fiat_code, order.premium).await {
Ok(amount_fees) => {
order.amount = amount_fees.0;
order.fee = amount_fees.1;
let total_mostro_fee = order.fee * 2;
order.dev_fee = get_dev_fee(total_mostro_fee);
}
Err(_) => return Err(MostroInternalErr(ServiceError::WrongAmountError)),
};
} else {
let total_mostro_fee = order.fee * 2;
order.dev_fee = get_dev_fee(total_mostro_fee);
}
let payment_request = validate_invoice(&msg, &order).await?;
let trade_index = match msg.get_inner_message_kind().trade_index {
Some(trade_index) => trade_index,
None => {
if event.identity == event.sender {
0
} else {
return Err(MostroInternalErr(ServiceError::InvalidPayload));
}
}
};
update_user_trade_index(pool, event.identity.to_string(), trade_index)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
if bond_required {
let taker_ctx = TakerContext {
identity: event.identity.to_string(),
trade_index,
buyer_invoice: payment_request.clone(),
fiat_amount: order.fiat_amount,
amount: order.amount,
fee: order.fee,
dev_fee: order.dev_fee,
};
bond::request_taker_bond(pool, &order, event.sender, request_id, taker_ctx).await?;
return Ok(());
}
order.buyer_pubkey = Some(event.sender.to_string());
order.master_buyer_pubkey = Some(event.identity.to_string());
order.trade_index_buyer = Some(trade_index);
order.set_timestamp_now();
if payment_request.is_none() {
update_order_status(&mut order, my_keys, pool, request_id).await?;
}
else {
show_hold_invoice(
my_keys,
payment_request,
&event.sender,
&seller_pubkey,
order,
request_id,
)
.await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use mostro_core::order::{Kind as OrderKind, Status};
use nostr_sdk::{Keys, Timestamp};
use sqlx::SqlitePool;
async fn create_test_pool() -> SqlitePool {
SqlitePool::connect(":memory:").await.unwrap()
}
fn create_test_keys() -> Keys {
Keys::generate()
}
fn build_test_context(pool: SqlitePool) -> AppContext {
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool))
.with_settings(test_settings())
.build()
}
fn create_test_message(trade_index: Option<u32>) -> Message {
Message::new_order(
Some(uuid::Uuid::new_v4()),
Some(1),
trade_index.map(|i| i as i64),
Action::TakeSell,
None, )
}
fn create_test_unwrapped_message() -> UnwrappedMessage {
let identity = create_test_keys();
let trade = create_test_keys();
UnwrappedMessage {
message: create_test_message(None),
signature: None,
sender: trade.public_key(),
identity: identity.public_key(),
created_at: Timestamp::now(),
}
}
#[tokio::test]
async fn test_update_order_status_structure() {
}
#[tokio::test]
async fn test_take_sell_action_pending_order_exists() {
let pool = create_test_pool().await;
let ctx = build_test_context(pool.clone());
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg = create_test_message(Some(1));
let result = take_sell_action(&ctx, msg, &event, &keys).await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_take_sell_action_order_validation() {
let pool = create_test_pool().await;
let ctx = build_test_context(pool.clone());
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg = create_test_message(Some(1));
let result = take_sell_action(&ctx, msg, &event, &keys).await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_take_sell_action_trade_index_logic() {
let pool = create_test_pool().await;
let ctx = build_test_context(pool.clone());
let keys = create_test_keys();
let mut event = create_test_unwrapped_message();
event.identity = event.sender;
let msg = create_test_message(None);
let result = take_sell_action(&ctx, msg, &event, &keys).await;
assert!(result.is_ok() || result.is_err());
let event2 = create_test_unwrapped_message();
let msg2 = create_test_message(None);
let result2 = take_sell_action(&ctx, msg2, &event2, &keys).await;
if let Err(MostroInternalErr(ServiceError::InvalidPayload)) = result2 {}
let msg3 = create_test_message(Some(1));
let result3 = take_sell_action(&ctx, msg3, &event2, &keys).await;
assert!(result3.is_ok() || result3.is_err());
}
#[tokio::test]
async fn test_take_sell_action_market_price_calculation() {
let pool = create_test_pool().await;
let ctx = build_test_context(pool.clone());
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg = create_test_message(Some(1));
let result = take_sell_action(&ctx, msg, &event, &keys).await;
assert!(result.is_ok() || result.is_err());
}
#[tokio::test]
async fn test_take_sell_action_payment_request_flows() {
let pool = create_test_pool().await;
let ctx = build_test_context(pool.clone());
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg1 = create_test_message(Some(1));
let result1 = take_sell_action(&ctx, msg1, &event, &keys).await;
assert!(result1.is_ok() || result1.is_err());
let msg2 = create_test_message(Some(1));
let result2 = take_sell_action(&ctx, msg2, &event, &keys).await;
assert!(result2.is_ok() || result2.is_err());
}
mod order_validation_tests {
use super::*;
#[test]
fn test_order_validation_logic() {
let order_kind = OrderKind::Sell;
assert!(matches!(order_kind, OrderKind::Sell));
let order_status = Status::Pending;
assert!(matches!(order_status, Status::Pending));
let maker_pubkey = create_test_keys().public_key();
let taker_pubkey = create_test_keys().public_key();
assert_ne!(maker_pubkey, taker_pubkey);
}
#[test]
fn test_fiat_amount_range_logic() {
let requested_amount = 100i64;
let min_amount = 50i64;
let max_amount = 200i64;
assert!(requested_amount >= min_amount && requested_amount <= max_amount);
let too_small = 25i64;
let too_large = 300i64;
assert!(too_small < min_amount);
assert!(too_large > max_amount);
}
}
mod market_price_tests {
#[test]
fn test_market_price_calculation_logic() {
let fiat_amount = 100i64;
let premium = 5;
let mock_btc_price = 50000.0;
let base_amount = (fiat_amount as f64 / mock_btc_price) * 1e8;
let premium_multiplier = 1.0 + (premium as f64 / 100.0);
let final_amount = (base_amount * premium_multiplier) as i64;
assert!(final_amount > 0);
assert!(final_amount > base_amount as i64); }
#[test]
fn test_fee_calculation_logic() {
let amount = 1_000_000i64; let fee_rate = 0.005; let expected_fee = (amount as f64 * fee_rate) as i64;
assert_eq!(expected_fee, 5_000); assert!(expected_fee < amount); }
}
}