use crate::app::bond::{self, BondSlashReason};
use crate::app::context::AppContext;
use crate::db::{
ensure_dispute_finalize_permission, find_dispute_by_order_id, is_assigned_solver,
is_dispute_taken_by_admin,
};
use crate::lightning::LndConnector;
use crate::nip33::{create_platform_tag_values, new_dispute_event};
use crate::util::{enqueue_order_msg, get_order, settle_seller_hold_invoice, update_order_event};
use mostro_core::prelude::*;
use nostr_sdk::prelude::*;
use sqlx_crud::Crud;
use std::str::FromStr;
use tracing::error;
use super::release::do_payment;
pub async fn admin_settle_action(
ctx: &AppContext,
msg: Message,
event: &UnwrappedMessage,
my_keys: &Keys,
ln_client: &mut LndConnector,
) -> Result<(), MostroError> {
let pool = ctx.pool();
let request_id = msg.get_inner_message_kind().request_id;
let order = get_order(&msg, pool).await?;
match is_assigned_solver(pool, &event.identity.to_string(), order.id).await {
Ok(false) => {
if is_dispute_taken_by_admin(pool, order.id, &my_keys.public_key().to_string()).await? {
return Err(MostroCantDo(
mostro_core::error::CantDoReason::DisputeTakenByAdmin,
));
} else {
return Err(MostroCantDo(
mostro_core::error::CantDoReason::IsNotYourDispute,
));
}
}
Err(e) => {
return Err(MostroInternalErr(ServiceError::DbAccessError(
e.to_string(),
)));
}
_ => {}
}
ensure_dispute_finalize_permission(
pool,
&event.identity.to_string(),
&my_keys.public_key().to_string(),
order.id,
)
.await?;
if order.check_status(Status::CooperativelyCanceled).is_ok() {
enqueue_order_msg(
request_id,
Some(order.id),
Action::CooperativeCancelAccepted,
None,
event.identity,
msg.get_inner_message_kind().trade_index,
)
.await;
return Ok(());
}
if let Err(cause) = order.check_status(Status::Dispute) {
return Err(MostroCantDo(cause));
}
let bond_resolution = bond::extract_bond_resolution(&msg);
bond::validate_bond_resolution(pool, &order, &bond_resolution).await?;
settle_seller_hold_invoice(event, ln_client, Action::AdminSettled, true, &order)
.await
.map_err(|e| MostroInternalErr(ServiceError::LnNodeError(e.to_string())))?;
let order_updated = update_order_event(my_keys, Status::SettledHoldInvoice, &order)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
let result =
sqlx::query("UPDATE orders SET status = ?, event_id = ? WHERE id = ? AND status = ?")
.bind(&order_updated.status)
.bind(&order_updated.event_id)
.bind(order_updated.id)
.bind(Status::Dispute.to_string())
.execute(pool)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
if result.rows_affected() == 0 {
tracing::warn!(
"Order {} not transitioned to settled-hold-invoice: status changed concurrently",
order_updated.id
);
return Ok(());
}
let dispute = find_dispute_by_order_id(pool, order.id).await;
if let Ok(mut d) = dispute {
let dispute_id = d.id;
d.status = DisputeStatus::Settled.to_string();
d.update(pool)
.await
.map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
let dispute_initiator = match (order.seller_dispute, order.buyer_dispute) {
(true, false) => "seller",
(false, true) => "buyer",
(_, _) => return Err(MostroInternalErr(ServiceError::DisputeEventError)),
};
let tags: Tags = Tags::from_list(vec![
Tag::custom(
TagKind::Custom(std::borrow::Cow::Borrowed("s")),
vec![DisputeStatus::Settled.to_string()],
),
Tag::custom(
TagKind::Custom(std::borrow::Cow::Borrowed("initiator")),
vec![dispute_initiator],
),
Tag::custom(
TagKind::Custom(std::borrow::Cow::Borrowed("y")),
create_platform_tag_values(ctx.settings().mostro.name.as_deref()),
),
Tag::custom(
TagKind::Custom(std::borrow::Cow::Borrowed("z")),
vec!["dispute".to_string()],
),
]);
let event = new_dispute_event(my_keys, "", dispute_id.to_string(), tags)
.map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
tracing::info!("Dispute event to be published: {event:#?}");
let client = ctx.nostr_client();
if let Err(e) = client.send_event(&event).await {
error!("Failed to send dispute settlement event: {}", e);
}
}
enqueue_order_msg(
request_id,
Some(order_updated.id),
Action::AdminSettled,
None,
event.sender,
msg.get_inner_message_kind().trade_index,
)
.await;
if let Some(ref seller_pubkey) = order_updated.seller_pubkey {
enqueue_order_msg(
None,
Some(order_updated.id),
Action::AdminSettled,
None,
PublicKey::from_str(seller_pubkey)
.map_err(|_| MostroInternalErr(ServiceError::InvalidPubkey))?,
msg.get_inner_message_kind().trade_index,
)
.await;
}
if let Some(ref buyer_pubkey) = order_updated.buyer_pubkey {
enqueue_order_msg(
None,
Some(order_updated.id),
Action::AdminSettled,
None,
PublicKey::from_str(buyer_pubkey)
.map_err(|_| MostroInternalErr(ServiceError::InvalidPubkey))?,
msg.get_inner_message_kind().trade_index,
)
.await;
}
if let Err(e) = bond::apply_bond_resolution(
pool,
ln_client,
&order_updated,
&bond_resolution,
BondSlashReason::LostDispute,
)
.await
{
tracing::warn!(
order_id = %order_updated.id,
"admin_settle: bond resolution apply failed: {}", e
);
}
if let Err(e) = bond::resolve_range_maker_bond_at_close(pool, ln_client, &order_updated).await {
tracing::warn!(
order_id = %order_updated.id,
"admin_settle: maker bond close failed: {}", e
);
}
let _ = do_payment(ctx, order_updated, request_id).await;
Ok(())
}
#[cfg(test)]
mod tests {
use mostro_core::error::CantDoReason;
#[test]
fn test_dispute_error_types() {
let regular_error = CantDoReason::IsNotYourDispute;
assert_eq!(format!("{:?}", regular_error), "IsNotYourDispute");
let admin_error = CantDoReason::DisputeTakenByAdmin;
assert_eq!(format!("{:?}", admin_error), "DisputeTakenByAdmin");
let unauthorized_error = CantDoReason::NotAuthorized;
assert_eq!(format!("{:?}", unauthorized_error), "NotAuthorized");
assert_ne!(regular_error, admin_error);
assert_ne!(admin_error, unauthorized_error);
}
}