Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Cancel hodl invoice if dlc proposal fails #2590

Merged
merged 4 commits into from
Jun 4, 2024
Merged
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
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
Loading