Skip to content

Commit

Permalink
Merge pull request #2590 from get10101/chore/set-timedout-invoices-to…
Browse files Browse the repository at this point in the history
…-canceled

feat: Cancel hodl invoice if dlc proposal fails
  • Loading branch information
holzeis authored Jun 4, 2024
2 parents 55e873a + b113b56 commit 1b9e50b
Show file tree
Hide file tree
Showing 12 changed files with 94 additions and 93 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

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

Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TYPE "InvoiceState_Type" ADD VALUE IF NOT EXISTS 'Canceled';
15 changes: 15 additions & 0 deletions coordinator/src/bin/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions coordinator/src/db/custom_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ impl ToSql<InvoiceStateType, Pg> 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)
}
Expand All @@ -287,6 +288,7 @@ impl FromSql<InvoiceStateType, Pg> 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()),
}
}
Expand Down
33 changes: 18 additions & 15 deletions coordinator/src/db/hodl_invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,6 +23,7 @@ pub enum InvoiceState {
Accepted,
Settled,
Failed,
Canceled,
}

impl QueryId for InvoiceStateType {
Expand All @@ -33,6 +35,13 @@ impl QueryId for InvoiceStateType {
}
}

pub fn cancel_pending_hodl_invoices(conn: &mut PgConnection) -> QueryResult<usize> {
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,
Expand All @@ -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<String> {
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,
Expand Down Expand Up @@ -87,28 +103,15 @@ pub fn update_hodl_invoice_to_settled(
.get_result(conn)
}

pub fn update_hodl_invoice_to_failed(
conn: &mut PgConnection,
order_id: Uuid,
) -> QueryResult<usize> {
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<usize> {
diesel::update(hodl_invoices::table)
.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)
}
28 changes: 15 additions & 13 deletions coordinator/src/node/invoice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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;
}
Expand Down
73 changes: 29 additions & 44 deletions coordinator/src/trade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:#}");
}
}

Expand All @@ -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({
Expand Down Expand Up @@ -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.
///
Expand Down
4 changes: 2 additions & 2 deletions crates/lnd-bridge/examples/cancel_invoice_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
6 changes: 4 additions & 2 deletions crates/lnd-bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand All @@ -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?;

Expand Down
1 change: 0 additions & 1 deletion mobile/native/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 3 additions & 15 deletions mobile/native/src/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:#}");
}
}
});
}
Expand Down

0 comments on commit 1b9e50b

Please sign in to comment.