use crate::app::context::AppContext;
use crate::db::update_user_trade_index;
use crate::util::{get_bitcoin_price, publish_order, validate_invoice};
use mostro_core::prelude::*;
use nostr_sdk::prelude::*;
use nostr_sdk::Keys;
async fn calculate_and_check_quote(
ctx: &AppContext,
order: &SmallOrder,
fiat_amount: &i64,
) -> Result<(), MostroError> {
let mostro_settings = &ctx.settings().mostro;
let quote = match order.amount {
0 => match get_bitcoin_price(&order.fiat_code) {
Ok(price) => {
let quote = *fiat_amount as f64 / price;
(quote * 1E8) as i64
}
Err(_) => {
return Err(MostroInternalErr(ServiceError::NoAPIResponse));
}
},
_ => order.amount,
};
if quote < 0 {
return Err(MostroCantDo(CantDoReason::InvalidAmount));
}
if quote > mostro_settings.max_order_amount as i64
|| quote < mostro_settings.min_payment_amount as i64
{
return Err(MostroCantDo(CantDoReason::OutOfRangeSatsAmount));
}
Ok(())
}
pub async fn order_action(
ctx: &AppContext,
msg: Message,
event: &UnwrappedMessage,
my_keys: &Keys,
) -> Result<(), MostroError> {
let pool = ctx.pool();
let request_id = msg.get_inner_message_kind().request_id;
if let Some(order) = msg.get_inner_message_kind().get_order() {
let _invoice = validate_invoice(&msg, &Order::from(order.clone())).await?;
let mostro_settings = &ctx.settings().mostro;
if let Err(cause) = order.check_fiat_currency(&mostro_settings.fiat_currencies_accepted) {
return Err(MostroCantDo(cause));
}
if order.min_amount.is_none() && order.max_amount.is_none() {
if let Err(cause) = order.check_fiat_amount() {
return Err(MostroCantDo(cause));
}
}
if let Err(cause) = order.check_amount() {
return Err(MostroCantDo(cause));
}
let mut amount_vec = vec![order.fiat_amount];
if let Err(cause) = order.check_range_order_limits(&mut amount_vec) {
return Err(MostroCantDo(cause));
}
if let Err(cause) = order.check_zero_amount_with_premium() {
return Err(MostroCantDo(cause));
}
for fiat_amount in amount_vec.iter() {
calculate_and_check_quote(ctx, order, fiat_amount).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())))?;
publish_order(
pool,
my_keys,
order,
event.sender,
event.identity,
event.sender,
request_id,
msg.get_inner_message_kind().trade_index,
)
.await?
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use mostro_core::message::MessageKind;
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 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::NewOrder,
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(),
}
}
fn create_test_order_message(fiat_amount: i64, amount: i64) -> Message {
let order = mostro_core::order::SmallOrder::new(
Some(uuid::Uuid::new_v4()),
Some(mostro_core::order::Kind::Sell),
Some(mostro_core::order::Status::Pending),
amount,
"USD".to_string(),
None,
None,
fiat_amount,
"BANK".to_string(),
0,
None,
None,
None,
None,
None,
);
Message::new_order(
Some(uuid::Uuid::new_v4()),
Some(1),
None,
Action::NewOrder,
Some(Payload::Order(order)),
)
}
#[tokio::test]
async fn test_order_action_no_order() {
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg = Message::Order(MessageKind {
version: 1,
request_id: Some(1),
trade_index: None,
id: Some(uuid::Uuid::new_v4()),
action: Action::NewOrder,
payload: None,
});
let result = order_action(&ctx, msg, &event, &keys).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_order_action_invalid_fiat_amount() {
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg = create_test_order_message(0, 50000);
let result = order_action(&ctx, msg, &event, &keys).await;
let err = result.unwrap_err();
assert!(
matches!(err, MostroCantDo(CantDoReason::InvalidAmount)),
"expected InvalidAmount, got: {:?}",
err
);
let msg = create_test_order_message(-100, 50000);
let result = order_action(&ctx, msg, &event, &keys).await;
let err = result.unwrap_err();
assert!(
matches!(err, MostroCantDo(CantDoReason::InvalidAmount)),
"expected InvalidAmount for negative, got: {:?}",
err
);
}
#[tokio::test]
async fn test_order_action_invalid_amount() {
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg = create_test_order_message(100, -50000);
let result = order_action(&ctx, msg, &event, &keys).await;
let err = result.unwrap_err();
assert!(
matches!(err, MostroCantDo(CantDoReason::InvalidAmount)),
"expected InvalidAmount, got: {:?}",
err
);
}
#[tokio::test]
async fn test_order_action_with_valid_order() {
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg = create_test_message(Some(1));
let _ = order_action(&ctx, msg, &event, &keys).await;
}
#[tokio::test]
async fn test_order_action_range_order_validation() {
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg = create_test_message(Some(1));
let _ = order_action(&ctx, msg, &event, &keys).await;
}
#[tokio::test]
async fn test_order_action_zero_amount_with_premium() {
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let event = create_test_unwrapped_message();
let msg = create_test_message(Some(1));
let _ = order_action(&ctx, msg, &event, &keys).await;
}
#[tokio::test]
async fn test_order_action_trade_index_logic() {
let pool = create_test_pool().await;
use crate::app::context::test_utils::{test_settings, TestContextBuilder};
let ctx = TestContextBuilder::new()
.with_pool(std::sync::Arc::new(pool.clone()))
.with_settings(test_settings())
.build();
let keys = create_test_keys();
let mut event = create_test_unwrapped_message();
event.identity = event.sender;
let msg = create_test_message(None);
let _ = order_action(&ctx, msg, &event, &keys).await;
let event2 = create_test_unwrapped_message();
let msg2 = create_test_message(None);
let _ = order_action(&ctx, msg2, &event2, &keys).await;
let msg3 = create_test_message(Some(1));
let _ = order_action(&ctx, msg3, &event2, &keys).await;
}
mod quote_calculation_tests {
#[test]
fn test_quote_calculation_logic() {
let fiat_amount = 100i64;
let price = 50000.0;
let expected_quote = (fiat_amount as f64 / price * 1E8) as i64;
assert_eq!(expected_quote, 200_000);
let fiat_amount2 = 1000i64;
let price2 = 25000.0; let expected_quote2 = (fiat_amount2 as f64 / price2 * 1E8) as i64;
assert_eq!(expected_quote2, 4_000_000); }
#[test]
fn test_amount_limits_validation() {
let quote = 1000i64;
let max_order = 100_000_000i64; let min_payment = 1_000i64;
assert!(quote >= min_payment && quote <= max_order);
let small_quote = 500i64;
assert!(small_quote < min_payment);
let large_quote = 200_000_000i64; assert!(large_quote > max_order);
}
}
}