Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
42 changes: 42 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ tokio = { version = "1.47.1", features = ["full", "test-util", "macros"] }
axum = "0.8.4"
tower-http = { version = "0.6.6", features = ["cors"] }
bech32 = "0.11.0"
serial_test = "3.2"

[build-dependencies]
tonic-prost-build = "0.14.1"
6 changes: 6 additions & 0 deletions src/lightning/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
pub mod invoice;
pub mod operations;

// Re-export operations for easier access
pub use operations::{
get_market_amount_and_fee, invoice_subscribe, settle_seller_hold_invoice, show_hold_invoice,
};

use crate::config::settings::Settings;
use crate::lightning::invoice::decode_invoice;
Expand Down
193 changes: 193 additions & 0 deletions src/lightning/operations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
use crate::config::settings::get_db_pool;
use crate::db;
use crate::flow;
use crate::lightning::LndConnector;
use crate::messages;
use crate::util::helpers::bytes_to_string;
use crate::util::orders::update_order_event;
use crate::util::pricing::{get_fee, get_market_quote};
use crate::util::queues::enqueue_order_msg;
use fedimint_tonic_lnd::lnrpc::invoice::InvoiceState;
use mostro_core::prelude::*;
use nostr::nips::nip59::UnwrappedGift;
use nostr_sdk::prelude::*;
use sqlx_crud::Crud;
use tokio::sync::mpsc::channel;
use tracing::info;

pub async fn show_hold_invoice(
my_keys: &Keys,
payment_request: Option<String>,
buyer_pubkey: &PublicKey,
seller_pubkey: &PublicKey,
mut order: Order,
request_id: Option<u64>,
) -> Result<(), MostroError> {
let mut ln_client = LndConnector::new().await?;
// Seller pays only the order amount and their Mostro fee
// Dev fee is NOT charged to seller - it's paid by mostrod from its earnings
let new_amount = order.amount + order.fee;

// Now we generate the hold invoice that seller should pay
let (invoice_response, preimage, hash) = ln_client
.create_hold_invoice(
&messages::hold_invoice_description(
&order.id.to_string(),
&order.fiat_code,
&order.fiat_amount.to_string(),
)
.map_err(|e| MostroInternalErr(ServiceError::HoldInvoiceError(e.to_string())))?,
new_amount,
)
.await
.map_err(|e| MostroInternalErr(ServiceError::HoldInvoiceError(e.to_string())))?;
if let Some(invoice) = payment_request {
order.buyer_invoice = Some(invoice);
};

// Using CRUD to update all fiels
order.preimage = Some(bytes_to_string(&preimage));
order.hash = Some(bytes_to_string(&hash));
order.status = Status::WaitingPayment.to_string();
order.buyer_pubkey = Some(buyer_pubkey.to_string());
order.seller_pubkey = Some(seller_pubkey.to_string());

// We need to publish a new event with the new status
let pool = db::connect()
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
let order_updated = update_order_event(my_keys, Status::WaitingPayment, &order)
.await
.map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
order_updated
.update(&pool)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;

let mut new_order = order.as_new_order();
new_order.status = Some(Status::WaitingPayment);
new_order.amount = new_amount;

// We create a Message to send the hold invoice to seller
enqueue_order_msg(
request_id,
Some(order.id),
Action::PayInvoice,
Some(Payload::PaymentRequest(
Some(new_order),
invoice_response.payment_request,
None,
)),
*seller_pubkey,
order.trade_index_seller,
)
.await;

// We notify the buyer (maker) that their order was taken and seller must pay the hold invoice
enqueue_order_msg(
request_id,
Some(order.id),
Action::WaitingSellerToPay,
None,
*buyer_pubkey,
order.trade_index_buyer,
)
.await;

let _ = invoice_subscribe(hash, request_id).await;

Ok(())
}

// Create function to reuse in case of resubscription
pub async fn invoice_subscribe(hash: Vec<u8>, request_id: Option<u64>) -> Result<(), MostroError> {
let mut ln_client_invoices = LndConnector::new().await?;
let (tx, mut rx) = channel(100);

let invoice_task = {
async move {
let _ = ln_client_invoices
.subscribe_invoice(hash, tx)
.await
.map_err(|e| e.to_string());
}
};
tokio::spawn(invoice_task);

// Arc clone db pool to safe use across threads
let pool = get_db_pool();

let subs = {
async move {
// Receiving msgs from the invoice subscription.
while let Some(msg) = rx.recv().await {
let hash = bytes_to_string(msg.hash.as_ref());
// If this invoice was paid by the seller
if msg.state == InvoiceState::Accepted {
if let Err(e) = flow::hold_invoice_paid(&hash, request_id, &pool).await {
info!("Invoice flow error {e}");
} else {
info!("Invoice with hash {hash} accepted!");
}
} else if msg.state == InvoiceState::Settled {
// If the payment was settled
if let Err(e) = flow::hold_invoice_settlement(&hash, &pool).await {
info!("Invoice flow error {e}");
}
} else if msg.state == InvoiceState::Canceled {
// If the payment was canceled
if let Err(e) = flow::hold_invoice_canceled(&hash, &pool).await {
info!("Invoice flow error {e}");
}
} else {
info!("Invoice with hash: {hash} subscribed!");
}
}
}
};
tokio::spawn(subs);
Ok(())
}

pub async fn get_market_amount_and_fee(
fiat_amount: i64,
fiat_code: &str,
premium: i64,
) -> Result<(i64, i64)> {
// Update amount order
let new_sats_amount = get_market_quote(&fiat_amount, fiat_code, premium).await?;
let fee = get_fee(new_sats_amount);

Ok((new_sats_amount, fee))
}

/// Settle a seller hold invoice
#[allow(clippy::too_many_arguments)]
pub async fn settle_seller_hold_invoice(
event: &UnwrappedGift,
ln_client: &mut LndConnector,
action: Action,
is_admin: bool,
order: &Order,
) -> Result<(), MostroError> {
// Get seller pubkey
let seller_pubkey = order
.get_seller_pubkey()
.map_err(|_| MostroCantDo(CantDoReason::InvalidPubkey))?
.to_string();
// Get sender pubkey
let sender_pubkey = event.rumor.pubkey.to_string();
// Check if the pubkey is right
if !is_admin && sender_pubkey != seller_pubkey {
return Err(MostroCantDo(CantDoReason::InvalidPubkey));
}

// Settling the hold invoice
if let Some(preimage) = order.preimage.as_ref() {
ln_client.settle_hold_invoice(preimage).await?;
info!("{action}: Order Id {}: hold invoice settled", order.id);
} else {
return Err(MostroCantDo(CantDoReason::InvalidInvoice));
}
Ok(())
}
File renamed without changes.
36 changes: 36 additions & 0 deletions src/util/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::fmt::Write;

/// Converts a byte slice to a hex string representation
///
/// # Examples
///
/// ```
/// let bytes = vec![0xde, 0xad, 0xbe, 0xef];
/// let result = bytes_to_string(&bytes);
/// assert_eq!(result, "deadbeef");
/// ```
pub fn bytes_to_string(bytes: &[u8]) -> String {
bytes.iter().fold(String::new(), |mut output, b| {
let _ = write!(output, "{:02x}", b);
output
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_bytes_to_string() {
let bytes = vec![0xde, 0xad, 0xbe, 0xef];
let result = bytes_to_string(&bytes);
assert_eq!(result, "deadbeef");
}

#[test]
fn test_bytes_to_string_empty() {
let bytes: Vec<u8> = vec![];
let result = bytes_to_string(&bytes);
assert_eq!(result, "");
}
}
40 changes: 40 additions & 0 deletions src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Submódulos
pub mod helpers;
pub mod nostr;
pub mod orders;
pub mod pricing;
pub mod queues;
pub mod reputation;

// Re-exportaciones para compatibilidad con imports existentes
// HELPERS
pub use helpers::bytes_to_string;

// PRICING
pub use pricing::{
calculate_dev_fee, get_bitcoin_price, get_dev_fee, get_expiration_date, get_fee,
get_market_quote, retries_yadio_request, FiatNames,
};

// ORDERS
pub use orders::{
get_fiat_amount_requested, get_order, get_tags_for_new_order, get_user_orders_by_id,
publish_order, set_waiting_invoice_status, update_order_event, validate_invoice,
};

// NOSTR
pub use nostr::{
connect_nostr, get_keys, get_nostr_client, get_nostr_relays, publish_dev_fee_audit_event,
send_dm, update_user_rating_event,
};

// QUEUES
pub use queues::{enqueue_cant_do_msg, enqueue_order_msg, enqueue_restore_session_msg};

// REPUTATION
pub use reputation::{get_dispute, notify_taker_reputation, rate_counterpart};

// LIGHTNING operations (now in lightning/operations.rs)
pub use crate::lightning::operations::{
get_market_amount_and_fee, invoice_subscribe, settle_seller_hold_invoice, show_hold_invoice,
};
Loading