diff --git a/Cargo.lock b/Cargo.lock index b6a98d483..5948d67f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2569,7 +2569,6 @@ version = "2.4.5" dependencies = [ "aes-gcm-siv", "anyhow", - "base64 0.21.7", "bdk", "bdk_file_store", "bip21", diff --git a/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/down.sql b/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/down.sql new file mode 100644 index 000000000..79aacce09 --- /dev/null +++ b/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/down.sql @@ -0,0 +1,5 @@ +-- Postgres does not allow removing enum type values. One can only re-create an enum type with fewer values and replace the references. +-- However, there is no proper way to replace the values to be removed where they are used (i.e. referenced in `hodl_invoice` table) +-- We opt to NOT remove enum values that were added at a later point. + +select 1; diff --git a/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/up.sql b/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/up.sql new file mode 100644 index 000000000..d0124bf86 --- /dev/null +++ b/coordinator/migrations/2024-05-31-144618_add-canceled-invoice-state/up.sql @@ -0,0 +1 @@ +ALTER TYPE "InvoiceState_Type" ADD VALUE IF NOT EXISTS 'Canceled'; \ No newline at end of file diff --git a/coordinator/src/bin/coordinator.rs b/coordinator/src/bin/coordinator.rs index eae15e487..cad9bda12 100644 --- a/coordinator/src/bin/coordinator.rs +++ b/coordinator/src/bin/coordinator.rs @@ -3,6 +3,7 @@ use anyhow::Result; use bitcoin::key::XOnlyPublicKey; use coordinator::backup::SledBackup; use coordinator::cli::Opts; +use coordinator::db; use coordinator::dlc_handler; use coordinator::dlc_handler::DlcHandler; use coordinator::logger; @@ -356,6 +357,20 @@ async fn main() -> Result<()> { } }); + if let Err(e) = spawn_blocking({ + let pool = pool.clone(); + move || { + let mut conn = pool.get()?; + db::hodl_invoice::cancel_pending_hodl_invoices(&mut conn)?; + anyhow::Ok(()) + } + }) + .await + .expect("task to finish") + { + tracing::error!("Failed to set expired hodl invoices to canceled. Error: {e:#}"); + } + tracing::debug!("Listening on http://{}", http_address); match axum::Server::bind(&http_address) diff --git a/coordinator/src/db/custom_types.rs b/coordinator/src/db/custom_types.rs index 633dbffc2..57b91d3a4 100644 --- a/coordinator/src/db/custom_types.rs +++ b/coordinator/src/db/custom_types.rs @@ -275,6 +275,7 @@ impl ToSql for InvoiceState { InvoiceState::Accepted => out.write_all(b"Accepted")?, InvoiceState::Settled => out.write_all(b"Settled")?, InvoiceState::Failed => out.write_all(b"Failed")?, + InvoiceState::Canceled => out.write_all(b"Canceled")?, } Ok(IsNull::No) } @@ -287,6 +288,7 @@ impl FromSql for InvoiceState { b"Accepted" => Ok(InvoiceState::Accepted), b"Settled" => Ok(InvoiceState::Settled), b"Failed" => Ok(InvoiceState::Failed), + b"Canceled" => Ok(InvoiceState::Canceled), _ => Err("Unrecognized enum variant".into()), } } diff --git a/coordinator/src/db/hodl_invoice.rs b/coordinator/src/db/hodl_invoice.rs index ad06a7f82..8f82edabe 100644 --- a/coordinator/src/db/hodl_invoice.rs +++ b/coordinator/src/db/hodl_invoice.rs @@ -9,6 +9,7 @@ use diesel::AsExpression; use diesel::ExpressionMethods; use diesel::FromSqlRow; use diesel::PgConnection; +use diesel::QueryDsl; use diesel::QueryResult; use diesel::RunQueryDsl; use std::any::TypeId; @@ -22,6 +23,7 @@ pub enum InvoiceState { Accepted, Settled, Failed, + Canceled, } impl QueryId for InvoiceStateType { @@ -33,6 +35,13 @@ impl QueryId for InvoiceStateType { } } +pub fn cancel_pending_hodl_invoices(conn: &mut PgConnection) -> QueryResult { + diesel::update(hodl_invoices::table) + .filter(hodl_invoices::invoice_state.eq_any([InvoiceState::Open, InvoiceState::Accepted])) + .set(hodl_invoices::invoice_state.eq(InvoiceState::Canceled)) + .execute(conn) +} + pub fn create_hodl_invoice( conn: &mut PgConnection, r_hash: &str, @@ -53,6 +62,13 @@ pub fn create_hodl_invoice( Ok(()) } +pub fn get_r_hash_by_order_id(conn: &mut PgConnection, order_id: Uuid) -> QueryResult { + hodl_invoices::table + .filter(hodl_invoices::order_id.eq(order_id)) + .select(hodl_invoices::r_hash) + .get_result(conn) +} + pub fn update_hodl_invoice_to_accepted( conn: &mut PgConnection, hash: &str, @@ -87,20 +103,7 @@ pub fn update_hodl_invoice_to_settled( .get_result(conn) } -pub fn update_hodl_invoice_to_failed( - conn: &mut PgConnection, - order_id: Uuid, -) -> QueryResult { - diesel::update(hodl_invoices::table) - .filter(hodl_invoices::order_id.eq(order_id)) - .set(( - hodl_invoices::updated_at.eq(OffsetDateTime::now_utc()), - hodl_invoices::invoice_state.eq(InvoiceState::Failed), - )) - .execute(conn) -} - -pub fn update_hodl_invoice_to_failed_by_r_hash( +pub fn update_hodl_invoice_to_canceled( conn: &mut PgConnection, r_hash: String, ) -> QueryResult { @@ -108,7 +111,7 @@ pub fn update_hodl_invoice_to_failed_by_r_hash( .filter(hodl_invoices::r_hash.eq(r_hash)) .set(( hodl_invoices::updated_at.eq(OffsetDateTime::now_utc()), - hodl_invoices::invoice_state.eq(InvoiceState::Failed), + hodl_invoices::invoice_state.eq(InvoiceState::Canceled), )) .execute(conn) } diff --git a/coordinator/src/node/invoice.rs b/coordinator/src/node/invoice.rs index b43672f4a..b20262228 100644 --- a/coordinator/src/node/invoice.rs +++ b/coordinator/src/node/invoice.rs @@ -28,22 +28,24 @@ pub fn spawn_invoice_watch( match stream.try_next().await { Ok(Some(invoice)) => match invoice.state { InvoiceState::Open => { - tracing::debug!(%trader_pubkey, invoice.r_hash, "Watching hodl invoice."); + tracing::debug!(%trader_pubkey, r_hash, "Watching hodl invoice."); continue; } InvoiceState::Settled => { - tracing::info!(%trader_pubkey, invoice.r_hash, "Accepted hodl invoice has been settled."); + tracing::info!(%trader_pubkey, r_hash, "Accepted hodl invoice has been settled."); break; } InvoiceState::Canceled => { - tracing::warn!(%trader_pubkey, invoice.r_hash, "Pending hodl invoice has been canceled."); - if let Err(e) = spawn_blocking(move || { - let mut conn = pool.get()?; - db::hodl_invoice::update_hodl_invoice_to_failed_by_r_hash( - &mut conn, - invoice.r_hash, - )?; - anyhow::Ok(()) + tracing::warn!(%trader_pubkey, r_hash, "Pending hodl invoice has been canceled."); + if let Err(e) = spawn_blocking({ + let r_hash = r_hash.clone(); + move || { + let mut conn = pool.get()?; + db::hodl_invoice::update_hodl_invoice_to_canceled( + &mut conn, r_hash, + )?; + anyhow::Ok(()) + } }) .await .expect("task to finish") @@ -56,12 +58,12 @@ pub fn spawn_invoice_watch( break; } InvoiceState::Accepted => { - tracing::info!(%trader_pubkey, invoice.r_hash, "Pending hodl invoice has been accepted."); + tracing::info!(%trader_pubkey, r_hash, "Pending hodl invoice has been accepted."); if let Err(e) = trader_sender.send(Message::LnPaymentReceived { - r_hash: invoice.r_hash.clone(), + r_hash: r_hash.clone(), amount: Amount::from_sat(invoice.amt_paid_sat), }) { - tracing::error!(%trader_pubkey, r_hash = invoice.r_hash, "Failed to send payment received event to app. Error: {e:#}") + tracing::error!(%trader_pubkey, r_hash, "Failed to send payment received event to app. Error: {e:#}") } continue; } diff --git a/coordinator/src/trade/mod.rs b/coordinator/src/trade/mod.rs index 5cd23f288..a490fb56d 100644 --- a/coordinator/src/trade/mod.rs +++ b/coordinator/src/trade/mod.rs @@ -149,7 +149,17 @@ impl TradeExecutor { if params.external_funding.is_some() { // The channel was funded externally. We need to post process the dlc channel // offer. - if let Err(e) = self.post_process_proposal(trader_id, order_id).await { + if let Err(e) = self.settle_invoice(trader_id, order_id).await { + tracing::error!(%trader_id, %order_id, "Failed to settle invoice with provided pre_image. Cancelling offer. Error: {e:#}"); + + if let Err(e) = self.cancel_offer(trader_id).await { + tracing::error!(%trader_id, %order_id, "Failed to cancel offer. Error: {e:#}"); + } + + if let Err(e) = self.cancel_hodl_invoice(order_id).await { + tracing::error!(%trader_id, %order_id, "Failed to cancel hodl invoice. Error: {e:#}"); + } + let message = OrderbookMessage::TraderMessage { trader_id, message: Message::TradeError { @@ -183,20 +193,8 @@ impl TradeExecutor { tracing::error!(%trader_id, %order_id, "Failed to cancel offer. Error: {e:#}"); } - // if the order was externally funded we need to set the hodl invoice to failed. - if let Err(e) = spawn_blocking({ - let pool = self.node.pool.clone(); - move || { - let mut conn = pool.get()?; - db::hodl_invoice::update_hodl_invoice_to_failed(&mut conn, order_id)?; - - anyhow::Ok(()) - } - }) - .await - .expect("task to finish") - { - tracing::error!(%trader_id, %order_id, "Failed to set hodl invoice to failed. Error: {e:#}"); + if let Err(e) = self.cancel_hodl_invoice(order_id).await { + tracing::error!(%trader_id, %order_id, "Failed to cancel hodl_invoice. Error: {e:#}"); } } @@ -221,35 +219,6 @@ impl TradeExecutor { }; } - async fn post_process_proposal(&self, trader: PublicKey, order_id: Uuid) -> Result<()> { - match self.settle_invoice(trader, order_id).await { - Ok(()) => Ok(()), - Err(e) => { - tracing::error!(%trader, %order_id, "Failed to settle invoice with provided pre_image. Cancelling offer. Error: {e:#}"); - - if let Err(e) = self.cancel_offer(trader).await { - tracing::error!(%trader, %order_id, "Failed to cancel offer. Error: {e:#}"); - } - - if let Err(e) = spawn_blocking({ - let pool = self.node.pool.clone(); - move || { - let mut conn = pool.get()?; - db::hodl_invoice::update_hodl_invoice_to_failed(&mut conn, order_id)?; - - anyhow::Ok(()) - } - }) - .await - .expect("task to finish") - { - tracing::error!(%trader, %order_id, "Failed to set hodl invoice to failed. Error: {e:#}"); - } - Err(e) - } - } - } - /// Settles the accepted invoice for the given trader async fn settle_invoice(&self, trader: PublicKey, order_id: Uuid) -> Result<()> { let pre_image = spawn_blocking({ @@ -305,6 +274,22 @@ impl TradeExecutor { Ok(()) } + pub async fn cancel_hodl_invoice(&self, order_id: Uuid) -> Result<()> { + // if the order was externally funded we need to set the hodl invoice to failed. + let r_hash = spawn_blocking({ + let pool = self.node.pool.clone(); + move || { + let mut conn = pool.get()?; + let r_hash = db::hodl_invoice::get_r_hash_by_order_id(&mut conn, order_id)?; + + anyhow::Ok(r_hash) + } + }) + .await??; + + self.node.lnd_bridge.cancel_invoice(r_hash).await + } + /// Execute a trade action according to the coordinator's current trading status with the /// trader. /// diff --git a/crates/lnd-bridge/examples/cancel_invoice_api.rs b/crates/lnd-bridge/examples/cancel_invoice_api.rs index 9395d1b55..860ff9b52 100644 --- a/crates/lnd-bridge/examples/cancel_invoice_api.rs +++ b/crates/lnd-bridge/examples/cancel_invoice_api.rs @@ -9,8 +9,8 @@ async fn main() -> Result<()> { let macaroon = "[enter macroon here]".to_string(); let lnd_bridge = lnd_bridge::LndBridge::new("localhost:18080".to_string(), macaroon, false); - let payment_hash = "".to_string(); - lnd_bridge.cancel_invoice(payment_hash).await?; + let r_hash = "".to_string(); + lnd_bridge.cancel_invoice(r_hash).await?; Ok(()) } diff --git a/crates/lnd-bridge/src/lib.rs b/crates/lnd-bridge/src/lib.rs index 3f67a40a9..089ee9799 100644 --- a/crates/lnd-bridge/src/lib.rs +++ b/crates/lnd-bridge/src/lib.rs @@ -150,7 +150,7 @@ impl LndBridge { Ok(invoice) } - pub async fn cancel_invoice(&self, payment_hash: String) -> Result<()> { + pub async fn cancel_invoice(&self, r_hash: String) -> Result<()> { let builder = self.client.request( Method::POST, format!( @@ -163,7 +163,9 @@ impl LndBridge { let resp = builder .header("content-type", "application/json") .header("Grpc-Metadata-macaroon", self.macaroon.clone()) - .json(&CancelInvoice { payment_hash }) + .json(&CancelInvoice { + payment_hash: r_hash, + }) .send() .await?; diff --git a/mobile/native/Cargo.toml b/mobile/native/Cargo.toml index cbbcc6532..84300ed4a 100644 --- a/mobile/native/Cargo.toml +++ b/mobile/native/Cargo.toml @@ -9,7 +9,6 @@ crate-type = ["rlib", "cdylib", "staticlib"] [dependencies] aes-gcm-siv = { version = "0.11.1", features = ["heapless"] } anyhow = "1" -base64 = "0.21.0" bdk = { version = "1.0.0-alpha.6", features = ["std"] } bdk_file_store = "0.6" bip21 = "0.3.0" diff --git a/mobile/native/src/watcher.rs b/mobile/native/src/watcher.rs index 268a9a66d..c6bea08e6 100644 --- a/mobile/native/src/watcher.rs +++ b/mobile/native/src/watcher.rs @@ -3,8 +3,6 @@ use crate::event::EventInternal; use crate::event::EventType; use crate::state; use anyhow::Result; -use base64::engine::general_purpose; -use base64::Engine; use bitcoin::Address; use bitcoin::Amount; use std::time::Duration; @@ -33,19 +31,9 @@ impl Subscriber for InvoiceWatcher { let r_hash = r_hash.clone(); let sender = self.sender.clone(); async move { - // We receive the r_hash in base64 standard encoding - match general_purpose::STANDARD.decode(&r_hash) { - Ok(hash) => { - // but we watch for the r_has in base64 url safe encoding. - let r_hash = general_purpose::URL_SAFE.encode(hash); - if let Err(e) = sender.send(r_hash.clone()) { - tracing::error!(%r_hash, "Failed to send accepted invoice event. Error: {e:#}"); - } - }, - Err(e) => { - tracing::error!(r_hash, "Failed to decode. Error: {e:#}"); - } - }; + if let Err(e) = sender.send(r_hash.clone()) { + tracing::error!(%r_hash, "Failed to send accepted invoice event. Error: {e:#}"); + } } }); }