From 7ebabc50949ce24172e017d02395c7925cb8a4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Calder=C3=B3n?= Date: Wed, 19 Apr 2023 17:53:40 -0300 Subject: [PATCH] Implement cooperative cancel (#46) Also implements other cancel use cases --- Cargo.toml | 2 +- migrations/20221222153301_orders.sql | 1 + sqlx-data.json | 20 ++++ src/app/cancel.rs | 162 ++++++++++++++++++++++++--- src/db.rs | 46 ++++++++ 5 files changed, 217 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cd76ff27..c0320efd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ uuid = { version = "1.3.0", features = [ "serde", ] } reqwest = { version = "0.11", features = ["json"] } -mostro-core = "0.1.7" +mostro-core = "0.1.8" tokio-cron-scheduler = "*" tracing = "0.1.37" tracing-subscriber = "0.3.16" diff --git a/migrations/20221222153301_orders.sql b/migrations/20221222153301_orders.sql index d44ea308..f1993dec 100644 --- a/migrations/20221222153301_orders.sql +++ b/migrations/20221222153301_orders.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS orders ( hash char(64), preimage char(64), creator_pubkey char(64), + cancel_initiator_pubkey char(64), buyer_pubkey char(64), seller_pubkey char(64), status char(10) not null, diff --git a/sqlx-data.json b/sqlx-data.json index 57fb9ad2..c1c3b371 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -30,6 +30,16 @@ }, "query": "\n UPDATE orders\n SET\n buyer_pubkey = ?1\n WHERE id = ?2\n " }, + "878c89a9ccf7cf33910f32a9dd2c09f45364dd744dc9e5749a318825343be542": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 4 + } + }, + "query": "\n UPDATE orders\n SET\n cancel_initiator_pubkey = ?1,\n buyer_cooperativecancel = ?2,\n seller_cooperativecancel = ?3\n WHERE id = ?4\n " + }, "89253158dce7eb8ed1c4fce2120418909fde010398815c5e673fb7d406dc5b4f": { "describe": { "columns": [], @@ -49,5 +59,15 @@ } }, "query": "\n UPDATE orders\n SET\n status = ?1,\n amount = ?2,\n fee = ?3,\n hash = ?4,\n preimage = ?5,\n taken_at = ?6,\n invoice_held_at = ?7\n WHERE id = ?8\n " + }, + "da72f298a426b1c65f7c67bf44436ba0b0878e52694cbd4cbca8585afcc665df": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 2 + } + }, + "query": "\n UPDATE orders\n SET\n seller_pubkey = ?1\n WHERE id = ?2\n " } } \ No newline at end of file diff --git a/src/app/cancel.rs b/src/app/cancel.rs index 7efbd994..1efbd09b 100644 --- a/src/app/cancel.rs +++ b/src/app/cancel.rs @@ -1,5 +1,7 @@ -use crate::db::edit_buyer_pubkey_order; -use crate::db::update_order_to_initial_state; +use crate::db::{ + edit_buyer_pubkey_order, edit_seller_pubkey_order, init_cancel_order, + update_order_to_initial_state, +}; use crate::lightning::LndConnector; use crate::messages; use crate::util::{send_dm, update_order_event}; @@ -56,16 +58,90 @@ pub async fn cancel_action( cancel_add_invoice(ln_client, &mut order, event, pool, client, my_keys).await?; } - if order.status == "WaitingPayment" { - // TODO - unimplemented!() - } else if order.status == "Active" || order.status == "FiatSent" || order.status == "Dispute" { - // TODO - unimplemented!() - } else { - // TODO - Ok(()) + if order.kind == "Buy" && order.status == "WaitingPayment" { + cancel_pay_hold_invoice(ln_client, &mut order, event, pool, client, my_keys).await?; + } + + if order.status == "Active" || order.status == "FiatSent" || order.status == "Dispute" { + let user_pubkey = event.pubkey.to_bech32()?; + let buyer_pubkey_bech32 = order.buyer_pubkey.as_ref().unwrap(); + let seller_pubkey_bech32 = order.seller_pubkey.as_ref().unwrap(); + let counterparty_pubkey: String; + if buyer_pubkey_bech32 == &user_pubkey { + order.buyer_cooperativecancel = true; + counterparty_pubkey = seller_pubkey_bech32.to_string(); + } else { + order.seller_cooperativecancel = true; + counterparty_pubkey = buyer_pubkey_bech32.to_string(); + } + + match order.cancel_initiator_pubkey { + Some(ref initiator_pubkey) => { + if initiator_pubkey == &user_pubkey { + let text_message = messages::cant_do(); + // We create a Message + let message = Message::new( + 0, + Some(order.id), + Action::CantDo, + Some(Content::TextMessage(text_message)), + ); + let message = message.as_json()?; + send_dm(client, my_keys, &event.pubkey, message).await?; + + return Ok(()); + } else { + if order.hash.is_some() { + // We return funds to seller + let hash = order.hash.as_ref().unwrap(); + ln_client.cancel_hold_invoice(hash).await?; + info!( + "Cooperative cancel: Order Id {}: Funds returned to seller", + &order.id + ); + } + init_cancel_order(pool, &order).await?; + order.status = "Canceled".to_string(); + // We publish a new replaceable kind nostr event with the status updated + // and update on local database the status and new event id + update_order_event(pool, client, my_keys, Status::Canceled, &order, None) + .await?; + // We create a Message for an accepted cooperative cancel and send it to both parties + let message = + Message::new(0, Some(order.id), Action::CooperativeCancelAccepted, None); + let message = message.as_json()?; + send_dm(client, my_keys, &event.pubkey, message.clone()).await?; + let counterparty_pubkey = XOnlyPublicKey::from_bech32(counterparty_pubkey)?; + send_dm(client, my_keys, &counterparty_pubkey, message).await?; + info!("Cancel: Order Id {order_id} canceled cooperatively!"); + } + } + None => { + order.cancel_initiator_pubkey = Some(user_pubkey.clone()); + // update db + init_cancel_order(pool, &order).await?; + // We create a Message to start a cooperative cancel and send it to both parties + let message = Message::new( + 0, + Some(order.id), + Action::CooperativeCancelInitiatedByYou, + None, + ); + let message = message.as_json()?; + send_dm(client, my_keys, &event.pubkey, message).await?; + let message = Message::new( + 0, + Some(order.id), + Action::CooperativeCancelInitiatedByPeer, + None, + ); + let message = message.as_json()?; + let counterparty_pubkey = XOnlyPublicKey::from_bech32(counterparty_pubkey)?; + send_dm(client, my_keys, &counterparty_pubkey, message).await?; + } + } } + Ok(()) } pub async fn cancel_add_invoice( @@ -123,8 +199,68 @@ pub async fn cancel_add_invoice( update_order_event(pool, client, my_keys, Status::Pending, order, None).await?; info!( "Buyer: {}: Canceled order Id {} republishing order", - order.buyer_pubkey.as_ref().unwrap(), - &order.id + buyer_pubkey_bech32, order.id + ); + Ok(()) + } +} + +pub async fn cancel_pay_hold_invoice( + ln_client: &mut LndConnector, + order: &mut Order, + event: &Event, + pool: &Pool, + client: &Client, + my_keys: &Keys, +) -> Result<()> { + if order.hash.is_some() { + // We return funds to seller + let hash = order.hash.as_ref().unwrap(); + ln_client.cancel_hold_invoice(hash).await?; + info!("Cancel: Order Id {}: Funds returned to seller", &order.id); + } + let user_pubkey = event.pubkey.to_bech32()?; + let buyer_pubkey_bech32 = order.buyer_pubkey.as_ref().unwrap(); + let seller_pubkey_bech32 = order.seller_pubkey.as_ref().unwrap(); + let seller_pubkey = XOnlyPublicKey::from_bech32(seller_pubkey_bech32)?; + if seller_pubkey_bech32 != &user_pubkey { + let text_message = messages::cant_do(); + // We create a Message + let message = Message::new( + 0, + Some(order.id), + Action::CantDo, + Some(Content::TextMessage(text_message)), + ); + let message = message.as_json()?; + send_dm(client, my_keys, &event.pubkey, message).await?; + + return Ok(()); + } + + if &order.creator_pubkey == seller_pubkey_bech32 { + // We publish a new replaceable kind nostr event with the status updated + // and update on local database the status and new event id + update_order_event(pool, client, my_keys, Status::Canceled, order, None).await?; + // We create a Message for cancel + let message = Message::new(0, Some(order.id), Action::Cancel, None); + let message = message.as_json()?; + send_dm(client, my_keys, &event.pubkey, message.clone()).await?; + send_dm(client, my_keys, &seller_pubkey, message).await?; + Ok(()) + } else { + // We re-publish the event with Pending status + // and update on local database + if order.price_from_api { + order.amount = 0; + order.fee = 0; + } + edit_seller_pubkey_order(pool, order.id, None).await?; + update_order_to_initial_state(pool, order.id, order.amount, order.fee).await?; + update_order_event(pool, client, my_keys, Status::Pending, order, None).await?; + info!( + "Seller: {}: Canceled order Id {} republishing order", + buyer_pubkey_bech32, order.id ); Ok(()) } diff --git a/src/db.rs b/src/db.rs index 0f560ba9..d86b805d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -168,6 +168,29 @@ pub async fn edit_buyer_pubkey_order( Ok(rows_affected > 0) } +pub async fn edit_seller_pubkey_order( + pool: &SqlitePool, + order_id: Uuid, + seller_pubkey: Option, +) -> anyhow::Result { + let mut conn = pool.acquire().await?; + let rows_affected = sqlx::query!( + r#" + UPDATE orders + SET + seller_pubkey = ?1 + WHERE id = ?2 + "#, + seller_pubkey, + order_id + ) + .execute(&mut conn) + .await? + .rows_affected(); + + Ok(rows_affected > 0) +} + pub async fn update_order_event_id_status( pool: &SqlitePool, order_id: Uuid, @@ -287,3 +310,26 @@ pub async fn update_order_to_initial_state( Ok(rows_affected > 0) } + +pub async fn init_cancel_order(pool: &SqlitePool, order: &Order) -> anyhow::Result { + let mut conn = pool.acquire().await?; + let rows_affected = sqlx::query!( + r#" + UPDATE orders + SET + cancel_initiator_pubkey = ?1, + buyer_cooperativecancel = ?2, + seller_cooperativecancel = ?3 + WHERE id = ?4 + "#, + order.cancel_initiator_pubkey, + order.buyer_cooperativecancel, + order.seller_cooperativecancel, + order.id, + ) + .execute(&mut conn) + .await? + .rows_affected(); + + Ok(rows_affected > 0) +}