pub mod app;
mod bitcoin_price;
pub mod cli;
pub mod config;
pub mod db;
pub mod flow;
pub mod lightning;
pub mod lnurl;
pub mod messages;
pub mod models;
pub mod nip33;
pub mod price;
pub mod rpc;
pub mod scheduler;
pub mod util;
use crate::app::context::AppContext;
use crate::app::run;
use crate::cli::settings_init;
use crate::config::{
get_db_pool, Settings, DB_POOL, LN_STATUS, MESSAGE_QUEUES, MOSTRO_CONFIG, NOSTR_CLIENT,
};
use crate::db::find_held_invoices;
use crate::lightning::LnStatus;
use crate::lightning::LndConnector;
use crate::rpc::RpcServer;
use nostr_sdk::prelude::*;
use scheduler::start_scheduler;
use std::env;
use std::process::exit;
use std::sync::Arc;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
use util::{get_nostr_client, invoice_subscribe};
#[tokio::main]
async fn main() -> Result<()> {
clearscreen::clear().expect("Failed to clear screen");
if cfg!(debug_assertions) {
env::set_var("RUST_LOG", "error,mostro=info");
} else {
env::set_var("RUST_LOG", "none,mostro=info");
}
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_default_env())
.init();
settings_init()?;
install_price_manager()?;
if DB_POOL.set(db::connect().await?).is_err() {
tracing::error!("No connection to database - closing Mostro!");
exit(1);
};
if NOSTR_CLIENT.set(util::connect_nostr().await?).is_err() {
tracing::error!("No connection to nostr relay - closing Mostro!");
exit(1);
};
let mostro_keys = util::get_keys()?;
let subscription = Filter::new()
.pubkey(mostro_keys.public_key())
.kind(Kind::GiftWrap)
.limit(0);
let client = match get_nostr_client() {
Ok(client) => client,
Err(e) => {
tracing::error!("Failed to initialize Nostr client. Cannot proceed: {e}");
exit(1)
}
};
client.subscribe(subscription, None).await?;
let mostro_settings = Settings::get_mostro();
let mut has_metadata = false;
let mut metadata = nostr_sdk::Metadata::new();
if let Some(ref name) = mostro_settings.name {
metadata = metadata.name(name);
has_metadata = true;
}
if let Some(ref about) = mostro_settings.about {
metadata = metadata.about(about);
has_metadata = true;
}
if let Some(ref picture) = mostro_settings.picture {
if let Ok(url) = nostr_sdk::Url::parse(picture) {
metadata = metadata.picture(url);
has_metadata = true;
} else {
tracing::warn!("Invalid picture URL in settings: {}", picture);
}
}
if let Some(ref website) = mostro_settings.website {
if let Ok(url) = nostr_sdk::Url::parse(website) {
metadata = metadata.website(url);
has_metadata = true;
} else {
tracing::warn!("Invalid website URL in settings: {}", website);
}
}
if has_metadata {
if let Ok(metadata_ev) = EventBuilder::metadata(&metadata).sign_with_keys(&mostro_keys) {
let _ = client.send_event(&metadata_ev).await;
tracing::info!("Published NIP-01 kind 0 metadata event");
}
}
let mut ln_client = LndConnector::new().await?;
let ln_status = ln_client.get_node_info().await?;
let ln_status = LnStatus::from_get_info_response(ln_status);
if LN_STATUS.set(ln_status).is_err() {
panic!("No connection to LND node - shutting down Mostro!");
};
if let Ok(held_invoices) = find_held_invoices(get_db_pool().as_ref()).await {
for invoice in held_invoices.iter() {
if let Some(hash) = &invoice.hash {
tracing::info!("Resubscribing order id - {}", invoice.id);
if let Err(e) = invoice_subscribe(hash.as_bytes().to_vec(), None).await {
tracing::error!("Ln node error {e}")
}
}
}
}
let bond_pool = get_db_pool();
if let Err(e) = app::bond::resubscribe_active_bonds(&bond_pool).await {
tracing::warn!("Failed to resubscribe active bonds: {e}");
}
if RpcServer::is_enabled() {
let rpc_server = RpcServer::new();
let rpc_keys = mostro_keys.clone();
let rpc_pool = get_db_pool();
let rpc_ln_client = Arc::new(tokio::sync::Mutex::new(ln_client.clone()));
tokio::spawn(async move {
match rpc_server.start(rpc_keys, rpc_pool, rpc_ln_client).await {
Ok(_) => tracing::info!("RPC server started successfully"),
Err(e) => tracing::error!("RPC server failed to start: {}", e),
}
});
}
let settings = Arc::new(
MOSTRO_CONFIG
.get()
.expect("MOSTRO_CONFIG not initialized")
.clone(),
);
let ctx = AppContext::new(
get_db_pool(),
client.clone(),
settings,
MESSAGE_QUEUES.queue_order_msg.clone(),
mostro_keys.clone(),
);
start_scheduler(ctx.clone()).await;
run(ctx, &mut ln_client).await
}
fn install_price_manager() -> std::result::Result<(), Box<dyn std::error::Error>> {
use crate::price::{synthesise_legacy_price_settings, PriceManager};
let mostro_settings = Settings::get_mostro();
let price_settings = match Settings::get_price() {
Some(p) => p.clone(),
None => synthesise_legacy_price_settings(
&mostro_settings.bitcoin_price_api_url,
mostro_settings.exchange_rates_update_interval_seconds,
mostro_settings.publish_exchange_rates_to_nostr,
),
};
let manager = PriceManager::from_settings(price_settings)
.map_err(|e| -> Box<dyn std::error::Error> { format!("price: {e}").into() })?;
manager
.install_global()
.map_err(|e| -> Box<dyn std::error::Error> { format!("price: {e}").into() })?;
tracing::info!("PriceManager installed");
Ok(())
}
#[cfg(test)]
mod tests {
use mostro_core::message::Message;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_message_deserialize_serialize() {
let sample_message = r#"{"order":{"version":1,"request_id":1,"trade_index":null,"id":"7dd204d2-d06c-4406-a3d9-4415f4a8b9c9","action":"fiat-sent","payload":null}}"#;
let message = Message::from_json(sample_message).unwrap();
assert!(message.verify());
let json_message = message.as_json().unwrap();
assert_eq!(sample_message, json_message);
}
#[test]
fn test_wrong_message_should_fail() {
let sample_message = r#"{"order":{"version":1,"request_id":1,"action":"take-sell","payload":{"order":{"kind":"sell","status":"pending","amount":100,"fiat_code":"XXX","fiat_amount":10,"payment_method":"SEPA","premium":1,"buyer_invoice":null,"created_at":1640839235}}}}"#;
let message = Message::from_json(sample_message).unwrap();
assert!(!message.verify());
}
#[test]
fn test_fee_rounding() {
let fee = 0.003 / 2.0;
let mut amt = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.subsec_micros() as i64;
for _i in 1..=1000 {
let fee_calculated = fee * amt as f64;
let rounded_fee = fee_calculated.round();
let seller_total_amt = rounded_fee as i64 + amt;
assert_eq!(amt, seller_total_amt - rounded_fee as i64);
let buyer_total_amt = amt - rounded_fee as i64;
assert_eq!(amt, buyer_total_amt + rounded_fee as i64);
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.subsec_millis() as i64;
amt %= 100_000_i64;
amt *= (rounded_fee as i64) % 100_i64;
amt += nonce;
}
}
#[test]
fn test_debug_log_level_setting() {
let debug_log_setting = if cfg!(debug_assertions) {
"error,mostro=info"
} else {
"none,mostro=info"
};
assert!(!debug_log_setting.is_empty());
assert!(debug_log_setting.contains("mostro=info"));
if cfg!(debug_assertions) {
assert!(debug_log_setting.contains("error"));
} else {
assert!(debug_log_setting.contains("none"));
}
}
}