diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 916a94f..ec0de08 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -370,7 +370,7 @@ checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567" [[package]] name = "boltz-client" version = "0.1.3" -source = "git+https://github.com/ok300/boltz-rust?branch=ok300-breez-latest-05-21#a6254147a0d00756880f5de38ac6000e49f61560" +source = "git+https://github.com/ok300/boltz-rust?branch=ok300-breez-latest-05-27#faecead854e8b0803f744b25bd0c47853cc487da" dependencies = [ "bip39", "bitcoin 0.31.2", diff --git a/lib/Cargo.lock b/lib/Cargo.lock index a7965c8..a191a15 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -490,7 +490,7 @@ checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567" [[package]] name = "boltz-client" version = "0.1.3" -source = "git+https://github.com/ok300/boltz-rust?branch=ok300-breez-latest-05-21#a6254147a0d00756880f5de38ac6000e49f61560" +source = "git+https://github.com/ok300/boltz-rust?branch=ok300-breez-latest-05-27#faecead854e8b0803f744b25bd0c47853cc487da" dependencies = [ "bip39", "bitcoin 0.31.2", diff --git a/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h b/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h index c0e8dfb..690e8c0 100644 --- a/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h +++ b/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h @@ -68,6 +68,8 @@ typedef struct wire_cst_payment { uint64_t amount_sat; uint64_t *fees_sat; struct wire_cst_list_prim_u_8_strict *preimage; + struct wire_cst_list_prim_u_8_strict *refund_tx_id; + uint64_t *refund_tx_amount_sat; int32_t payment_type; int32_t status; } wire_cst_payment; diff --git a/lib/bindings/src/breez_liquid_sdk.udl b/lib/bindings/src/breez_liquid_sdk.udl index 29f6784..fc9b569 100644 --- a/lib/bindings/src/breez_liquid_sdk.udl +++ b/lib/bindings/src/breez_liquid_sdk.udl @@ -84,6 +84,8 @@ dictionary Payment { u64 amount_sat; u64? fees_sat = null; string? preimage = null; + string? refund_tx_id = null; + u64? refund_tx_amount_sat = null; PaymentType payment_type; PaymentState status; }; diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 75e7012..4ec6bf6 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -15,7 +15,7 @@ frb = ["dep:flutter_rust_bridge"] anyhow = { workspace = true } bip39 = { version = "2.0.0", features = ["serde"] } #boltz-client = { git = "https://github.com/SatoshiPortal/boltz-rust", rev = "a05731cc33030ada9ae14afcafe0cded22842ba6" } -boltz-client = { git = "https://github.com/ok300/boltz-rust", branch = "ok300-breez-latest-05-21" } +boltz-client = { git = "https://github.com/ok300/boltz-rust", branch = "ok300-breez-latest-05-27" } flutter_rust_bridge = { version = "=2.0.0-dev.35", features = ["chrono"], optional = true } log = "0.4.20" lwk_common = "0.5.1" diff --git a/lib/core/src/frb/bridge.io.rs b/lib/core/src/frb/bridge.io.rs index 7127214..9bee2fd 100644 --- a/lib/core/src/frb/bridge.io.rs +++ b/lib/core/src/frb/bridge.io.rs @@ -254,6 +254,8 @@ impl CstDecode for wire_cst_payment { amount_sat: self.amount_sat.cst_decode(), fees_sat: self.fees_sat.cst_decode(), preimage: self.preimage.cst_decode(), + refund_tx_id: self.refund_tx_id.cst_decode(), + refund_tx_amount_sat: self.refund_tx_amount_sat.cst_decode(), payment_type: self.payment_type.cst_decode(), status: self.status.cst_decode(), } @@ -453,6 +455,8 @@ impl NewWithNullPtr for wire_cst_payment { amount_sat: Default::default(), fees_sat: core::ptr::null_mut(), preimage: core::ptr::null_mut(), + refund_tx_id: core::ptr::null_mut(), + refund_tx_amount_sat: core::ptr::null_mut(), payment_type: Default::default(), status: Default::default(), } @@ -893,6 +897,8 @@ pub struct wire_cst_payment { amount_sat: u64, fees_sat: *mut u64, preimage: *mut wire_cst_list_prim_u_8_strict, + refund_tx_id: *mut wire_cst_list_prim_u_8_strict, + refund_tx_amount_sat: *mut u64, payment_type: i32, status: i32, } diff --git a/lib/core/src/frb/bridge.rs b/lib/core/src/frb/bridge.rs index 5fab990..8a1b44d 100644 --- a/lib/core/src/frb/bridge.rs +++ b/lib/core/src/frb/bridge.rs @@ -778,6 +778,8 @@ impl SseDecode for crate::model::Payment { let mut var_amountSat = ::sse_decode(deserializer); let mut var_feesSat = >::sse_decode(deserializer); let mut var_preimage = >::sse_decode(deserializer); + let mut var_refundTxId = >::sse_decode(deserializer); + let mut var_refundTxAmountSat = >::sse_decode(deserializer); let mut var_paymentType = ::sse_decode(deserializer); let mut var_status = ::sse_decode(deserializer); return crate::model::Payment { @@ -787,6 +789,8 @@ impl SseDecode for crate::model::Payment { amount_sat: var_amountSat, fees_sat: var_feesSat, preimage: var_preimage, + refund_tx_id: var_refundTxId, + refund_tx_amount_sat: var_refundTxAmountSat, payment_type: var_paymentType, status: var_status, }; @@ -1172,6 +1176,8 @@ impl flutter_rust_bridge::IntoDart for crate::model::Payment { self.amount_sat.into_into_dart().into_dart(), self.fees_sat.into_into_dart().into_dart(), self.preimage.into_into_dart().into_dart(), + self.refund_tx_id.into_into_dart().into_dart(), + self.refund_tx_amount_sat.into_into_dart().into_dart(), self.payment_type.into_into_dart().into_dart(), self.status.into_into_dart().into_dart(), ] @@ -1577,6 +1583,8 @@ impl SseEncode for crate::model::Payment { ::sse_encode(self.amount_sat, serializer); >::sse_encode(self.fees_sat, serializer); >::sse_encode(self.preimage, serializer); + >::sse_encode(self.refund_tx_id, serializer); + >::sse_encode(self.refund_tx_amount_sat, serializer); ::sse_encode(self.payment_type, serializer); ::sse_encode(self.status, serializer); } diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 5f6c8a9..590d371 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -3,7 +3,7 @@ use boltz_client::network::Chain; use boltz_client::swaps::boltzv2::{ CreateReverseResponse, CreateSubmarineResponse, Leaf, SwapTree, }; -use boltz_client::SwapType; +use boltz_client::{Keypair, SwapType}; use lwk_signer::SwSigner; use lwk_wollet::{ElectrumUrl, ElementsNetwork, WolletDescriptor}; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; @@ -201,8 +201,13 @@ pub(crate) struct SendSwap { pub(crate) refund_tx_id: Option, pub(crate) created_at: u32, pub(crate) state: PaymentState, + pub(crate) refund_private_key: String, } impl SendSwap { + pub(crate) fn get_refund_keypair(&self) -> Result { + utils::decode_keypair(&self.refund_private_key).map_err(Into::into) + } + pub(crate) fn get_boltz_create_response( &self, ) -> Result { @@ -460,6 +465,9 @@ pub struct PaymentSwapData { /// Amount received by the swap receiver pub receiver_amount_sat: u64, + pub refund_tx_id: Option, + pub refund_tx_amount_sat: Option, + /// Payment status derived from the swap status pub status: PaymentState, } @@ -495,6 +503,12 @@ pub struct Payment { /// In case of a Send swap, this is the preimage of the paid invoice (proof of payment). pub preimage: Option, + /// For a Send swap which was refunded, this is the refund tx id + pub refund_tx_id: Option, + + /// For a Send swap which was refunded, this is the refund amount + pub refund_tx_amount_sat: Option, + pub payment_type: PaymentType, /// Composite status representing the overall status of the payment. @@ -518,6 +532,8 @@ impl Payment { .as_ref() .map(|s| s.payer_amount_sat - s.receiver_amount_sat), preimage: swap.as_ref().and_then(|s| s.preimage.clone()), + refund_tx_id: swap.as_ref().and_then(|s| s.refund_tx_id.clone()), + refund_tx_amount_sat: swap.as_ref().and_then(|s| s.refund_tx_amount_sat), payment_type: tx.payment_type, status: match swap { Some(swap) => swap.status, diff --git a/lib/core/src/persist/migrations.rs b/lib/core/src/persist/migrations.rs index 5e3ee22..353d5bd 100644 --- a/lib/core/src/persist/migrations.rs +++ b/lib/core/src/persist/migrations.rs @@ -19,6 +19,7 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { payer_amount_sat INTEGER NOT NULL, receiver_amount_sat INTEGER NOT NULL, create_response_json TEXT NOT NULL, + refund_private_key TEXT NOT NULL, lockup_tx_id TEXT, refund_tx_id TEXT, created_at INTEGER NOT NULL, diff --git a/lib/core/src/persist/mod.rs b/lib/core/src/persist/mod.rs index 1b9e923..7ab9f8f 100644 --- a/lib/core/src/persist/mod.rs +++ b/lib/core/src/persist/mod.rs @@ -109,15 +109,21 @@ impl Persister { ss.id, ss.created_at, ss.preimage, + ss.refund_tx_id, ss.payer_amount_sat, ss.receiver_amount_sat, - ss.state - FROM payment_tx_data AS ptx - LEFT JOIN receive_swaps AS rs + ss.state, + rtx.amount_sat + FROM payment_tx_data AS ptx -- Payment tx (each tx results in a Payment) + LEFT JOIN receive_swaps AS rs -- Receive Swap data ON ptx.tx_id = rs.claim_tx_id - LEFT JOIN send_swaps AS ss + LEFT JOIN send_swaps AS ss -- Send Swap data ON ptx.tx_id = ss.lockup_tx_id - WHERE {} + LEFT JOIN payment_tx_data AS rtx -- Refund tx data + ON rtx.tx_id = ss.refund_tx_id + WHERE -- Filter out refund txs from Payment tx list + ptx.tx_id NOT IN (SELECT refund_tx_id FROM send_swaps WHERE refund_tx_id NOT NULL) + AND {} ", where_clause.unwrap_or("true") ) @@ -137,12 +143,15 @@ impl Persister { let maybe_receive_swap_payer_amount_sat: Option = row.get(7)?; let maybe_receive_swap_receiver_amount_sat: Option = row.get(8)?; let maybe_receive_swap_receiver_state: Option = row.get(9)?; + let maybe_send_swap_id: Option = row.get(10)?; let maybe_send_swap_created_at: Option = row.get(11)?; let maybe_send_swap_preimage: Option = row.get(12)?; - let maybe_send_swap_payer_amount_sat: Option = row.get(13)?; - let maybe_send_swap_receiver_amount_sat: Option = row.get(14)?; - let maybe_send_swap_state: Option = row.get(15)?; + let maybe_send_swap_refund_tx_id: Option = row.get(13)?; + let maybe_send_swap_payer_amount_sat: Option = row.get(14)?; + let maybe_send_swap_receiver_amount_sat: Option = row.get(15)?; + let maybe_send_swap_state: Option = row.get(16)?; + let maybe_send_swap_refund_tx_amount_sat: Option = row.get(17)?; let swap = match maybe_receive_swap_id { Some(receive_swap_id) => Some(PaymentSwapData { @@ -151,6 +160,8 @@ impl Persister { preimage: None, payer_amount_sat: maybe_receive_swap_payer_amount_sat.unwrap_or(0), receiver_amount_sat: maybe_receive_swap_receiver_amount_sat.unwrap_or(0), + refund_tx_id: None, + refund_tx_amount_sat: None, status: maybe_receive_swap_receiver_state.unwrap_or(PaymentState::Created), }), None => maybe_send_swap_id.map(|send_swap_id| PaymentSwapData { @@ -159,6 +170,8 @@ impl Persister { preimage: maybe_send_swap_preimage, payer_amount_sat: maybe_send_swap_payer_amount_sat.unwrap_or(0), receiver_amount_sat: maybe_send_swap_receiver_amount_sat.unwrap_or(0), + refund_tx_id: maybe_send_swap_refund_tx_id, + refund_tx_amount_sat: maybe_send_swap_refund_tx_amount_sat, status: maybe_send_swap_state.unwrap_or(PaymentState::Created), }), }; diff --git a/lib/core/src/persist/receive.rs b/lib/core/src/persist/receive.rs index 1e38956..7fa1039 100644 --- a/lib/core/src/persist/receive.rs +++ b/lib/core/src/persist/receive.rs @@ -11,7 +11,7 @@ use rusqlite::{named_params, params, Connection, Row}; use serde::{Deserialize, Serialize}; impl Persister { - pub(crate) fn insert_receive_swap(&self, receive_swap: ReceiveSwap) -> Result<()> { + pub(crate) fn insert_receive_swap(&self, receive_swap: &ReceiveSwap) -> Result<()> { let con = self.get_connection()?; let mut stmt = con.prepare( @@ -31,16 +31,16 @@ impl Persister { VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", )?; _ = stmt.execute(( - receive_swap.id, - receive_swap.preimage, - receive_swap.create_response_json, - receive_swap.invoice, - receive_swap.payer_amount_sat, - receive_swap.receiver_amount_sat, - receive_swap.created_at, - receive_swap.claim_fees_sat, - receive_swap.claim_tx_id, - receive_swap.state, + &receive_swap.id, + &receive_swap.preimage, + &receive_swap.create_response_json, + &receive_swap.invoice, + &receive_swap.payer_amount_sat, + &receive_swap.receiver_amount_sat, + &receive_swap.created_at, + &receive_swap.claim_fees_sat, + &receive_swap.claim_tx_id, + &receive_swap.state, ))?; Ok(()) diff --git a/lib/core/src/persist/send.rs b/lib/core/src/persist/send.rs index 8d5884e..aaf5796 100644 --- a/lib/core/src/persist/send.rs +++ b/lib/core/src/persist/send.rs @@ -11,7 +11,7 @@ use crate::model::*; use crate::persist::Persister; impl Persister { - pub(crate) fn insert_send_swap(&self, send_swap: SendSwap) -> Result<()> { + pub(crate) fn insert_send_swap(&self, send_swap: &SendSwap) -> Result<()> { let con = self.get_connection()?; let mut stmt = con.prepare( @@ -22,23 +22,25 @@ impl Persister { payer_amount_sat, receiver_amount_sat, create_response_json, + refund_private_key, lockup_tx_id, refund_tx_id, created_at, state ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", )?; _ = stmt.execute(( - send_swap.id, - send_swap.invoice, - send_swap.payer_amount_sat, - send_swap.receiver_amount_sat, - send_swap.create_response_json, - send_swap.lockup_tx_id, - send_swap.refund_tx_id, - send_swap.created_at, - send_swap.state, + &send_swap.id, + &send_swap.invoice, + &send_swap.payer_amount_sat, + &send_swap.receiver_amount_sat, + &send_swap.create_response_json, + &send_swap.refund_private_key, + &send_swap.lockup_tx_id, + &send_swap.refund_tx_id, + &send_swap.created_at, + &send_swap.state, ))?; Ok(()) @@ -59,6 +61,7 @@ impl Persister { payer_amount_sat, receiver_amount_sat, create_response_json, + refund_private_key, lockup_tx_id, refund_tx_id, created_at, @@ -85,10 +88,11 @@ impl Persister { payer_amount_sat: row.get(2)?, receiver_amount_sat: row.get(3)?, create_response_json: row.get(4)?, - lockup_tx_id: row.get(5)?, - refund_tx_id: row.get(6)?, - created_at: row.get(7)?, - state: row.get(8)?, + refund_private_key: row.get(5)?, + lockup_tx_id: row.get(6)?, + refund_tx_id: row.get(7)?, + created_at: row.get(8)?, + state: row.get(9)?, }) } diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 2d9ff35..032e913 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -1,4 +1,6 @@ use anyhow::{anyhow, Result}; +use boltz_client::network::Chain; +use boltz_client::ToHex; use boltz_client::{ network::electrum::ElectrumConfig, swaps::{ @@ -13,7 +15,7 @@ use log::{debug, error, info, warn}; use lwk_common::{singlesig_desc, Signer, Singlesig}; use lwk_signer::{AnySigner, SwSigner}; use lwk_wollet::bitcoin::Witness; -use lwk_wollet::elements::hex::ToHex; +use lwk_wollet::elements::{LockTime, LockTime::*}; use lwk_wollet::hashes::{sha256, Hash}; use lwk_wollet::{ elements::{Address, Transaction}, @@ -148,23 +150,6 @@ impl LiquidSdk { Ok(descriptor_str.parse()?) } - fn get_submarine_keys(&self, derivation_index: i32) -> Result { - let mnemonic = self - .lwk_signer - .mnemonic() - .ok_or(PaymentError::SignerError { - err: "Could not claim: Mnemonic not found".to_string(), - })?; - let swap_key = SwapKey::from_submarine_account( - &mnemonic.to_string(), - "", - self.network.into(), - derivation_index as u64, - )?; - let lsk = LiquidSwapKey::try_from(swap_key)?; - Ok(lsk.keypair) - } - fn validate_state_transition( from_state: PaymentState, to_state: PaymentState, @@ -406,20 +391,12 @@ impl LiquidSdk { .persister .fetch_send_swap(id)? .ok_or(anyhow!("No ongoing Send Swap found for ID {id}"))?; - let create_response: CreateSubmarineResponse = - ongoing_send_swap.get_boltz_create_response()?; - - let receiver_amount_sat = get_invoice_amount!(ongoing_send_swap.invoice); - let keypair = self.get_submarine_keys(0)?; match swap_state { SubSwapStates::TransactionClaimPending => { - let lockup_tx_id = ongoing_send_swap.lockup_tx_id.ok_or(anyhow!( - "Swap-in {id} is pending but no lockup txid is present" - ))?; - + let keypair = ongoing_send_swap.get_refund_keypair()?; let swap_script = LBtcSwapScriptV2::submarine_from_swap_resp( - &create_response, + &ongoing_send_swap.get_boltz_create_response()?, keypair.public_key().into(), ) .map_err(|e| anyhow!("Could not rebuild refund details for swap-in {id}: {e:?}"))?; @@ -445,21 +422,13 @@ impl LiquidSdk { Ok(()) } + // If swap state is unrecoverable, try refunding SubSwapStates::TransactionLockupFailed | SubSwapStates::InvoiceFailedToPay | SubSwapStates::SwapExpired => { warn!("Swap-in {id} is in an unrecoverable state: {swap_state:?}"); - // If swap state is unrecoverable, try refunding - let swap_script = LBtcSwapScriptV2::submarine_from_swap_resp( - &create_response, - keypair.public_key().into(), - ) - .map_err(|e| anyhow!("Could not rebuild refund details for swap-in {id}: {e:?}"))?; - - let refund_tx_id = self - .try_refund(id, &swap_script, &keypair, receiver_amount_sat) - .await?; + let refund_tx_id = self.try_refund(&ongoing_send_swap).await?; info!("Broadcast refund tx for Swap-in {id}. Tx id: {refund_tx_id}"); self.try_handle_send_swap_update(id, Pending, None, None, Some(&refund_tx_id)) .await?; @@ -495,13 +464,23 @@ impl LiquidSdk { for p in self.list_payments()? { match p.payment_type { PaymentType::Send => match p.status { - PaymentState::Complete => confirmed_sent_sat += p.amount_sat, - PaymentState::Failed => {} - _ => pending_send_sat += p.amount_sat, + Complete => confirmed_sent_sat += p.amount_sat, + Failed => { + confirmed_sent_sat += p.amount_sat; + confirmed_received_sat += p.refund_tx_amount_sat.unwrap_or_default(); + } + Pending => match p.refund_tx_amount_sat { + Some(refund_tx_amount_sat) => { + confirmed_sent_sat += p.amount_sat; + pending_receive_sat += refund_tx_amount_sat; + } + None => pending_send_sat += p.amount_sat, + }, + Created => pending_send_sat += p.amount_sat, }, PaymentType::Receive => match p.status { - PaymentState::Complete => confirmed_received_sat += p.amount_sat, - PaymentState::Failed => {} + Complete => confirmed_received_sat += p.amount_sat, + Failed => {} _ => pending_receive_sat += p.amount_sat, }, } @@ -652,50 +631,118 @@ impl LiquidSdk { async fn new_refund_tx( &self, + swap_id: &str, swap_script: &LBtcSwapScriptV2, ) -> Result { - let wallet = self.lwk_wollet.lock().await; - let output_address = wallet.address(Some(0))?.address().to_string(); + let output_address = self.next_unused_address().await?.to_string(); let network_config = self.network_config(); Ok(LBtcSwapTxV2::new_refund( swap_script.clone(), &output_address, &network_config, + self.boltz_url_v2().to_string(), + swap_id.to_string(), )?) } - async fn try_refund( + async fn try_refund_cooperative( &self, - swap_id: &str, - swap_script: &LBtcSwapScriptV2, - keypair: &Keypair, - amount_sat: u64, + swap: &SendSwap, + refund_tx: &LBtcSwapTxV2, + broadcast_fees_sat: Amount, + is_lowball: Option<(&BoltzApiClientV2, Chain)>, ) -> Result { - let refund_tx = self.new_refund_tx(swap_script).await?; + info!("Initiating cooperative refund for Send Swap {}", &swap.id); + let tx = refund_tx.sign_refund( + &swap.get_refund_keypair()?, + broadcast_fees_sat, + Some((&self.boltz_client_v2(), &swap.id)), + )?; + let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?; + info!( + "Successfully broadcast cooperative refund for Send Swap {}", + &swap.id + ); + Ok(refund_tx_id.clone()) + } + + async fn try_refund_non_cooperative( + &self, + swap: &SendSwap, + swap_script: &LBtcSwapScriptV2, + refund_tx: LBtcSwapTxV2, + broadcast_fees_sat: Amount, + is_lowball: Option<(&BoltzApiClientV2, Chain)>, + ) -> Result { + info!( + "Initiating non-cooperative refund for Send Swap {}", + &swap.id + ); + + let current_height = self.lwk_wollet.lock().await.tip().height(); + let locktime_from_height = + LockTime::from_height(current_height).map_err(|e| PaymentError::Generic { + err: format!("Cannot convert current block height to lock time: {e:?}"), + })?; + + info!("locktime info: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap_script.locktime); + let is_locktime_satisfied = match (locktime_from_height, swap_script.locktime) { + (Blocks(n), Blocks(lock_time)) => n >= lock_time, + (Seconds(n), Seconds(lock_time)) => n >= lock_time, + _ => false, // Not using the same units + }; + if !is_locktime_satisfied { + return Err(PaymentError::Generic { + err: format!( + "Cannot refund non-cooperatively. Lock time not elapsed yet. Current tip: {:?}. Script lock time: {:?}", + locktime_from_height, swap_script.locktime + ) + }); + } + + let tx = refund_tx.sign_refund(&swap.get_refund_keypair()?, broadcast_fees_sat, None)?; + let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?; + info!( + "Successfully broadcast non-cooperative refund for swap-in {}", + swap.id + ); + Ok(refund_tx_id) + } + + async fn try_refund(&self, swap: &SendSwap) -> Result { + let id = &swap.id; + let swap_script = LBtcSwapScriptV2::submarine_from_swap_resp( + &swap.get_boltz_create_response()?, + swap.get_refund_keypair()?.public_key().into(), + ) + .map_err(|e| anyhow!("Could not rebuild refund details for swap-in {id}: {e:?}"))?; + + let refund_tx = self.new_refund_tx(&swap.id, &swap_script).await?; + let amount_sat = get_invoice_amount!(swap.invoice); let broadcast_fees_sat = Amount::from_sat(self.get_broadcast_fee_estimation(amount_sat).await?); let client = self.boltz_client_v2(); - let is_lowball = Some((&client, boltz_client::network::Chain::from(self.network))); + let is_lowball = match self.network { + Network::Liquid => None, + Network::LiquidTestnet => Some((&client, boltz_client::network::Chain::LiquidTestnet)), + }; - match refund_tx.sign_refund( - keypair, - broadcast_fees_sat, - Some((&client, &swap_id.to_string())), - ) { - // Try with cooperative refund - Ok(tx) => { - let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?; - debug!("Successfully broadcast cooperative refund for swap-in {swap_id}"); - Ok(refund_tx_id) - } - // Try with non-cooperative refund + match self + .try_refund_cooperative(swap, &refund_tx, broadcast_fees_sat, is_lowball) + .await + { + Ok(res) => Ok(res), Err(e) => { - debug!("Cooperative refund failed: {:?}", e); - let tx = refund_tx.sign_refund(keypair, broadcast_fees_sat, None)?; - let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?; - debug!("Successfully broadcast non-cooperative refund for swap-in {swap_id}"); - Ok(refund_tx_id) + warn!("Cooperative refund failed: {:?}", e); + self.try_refund_non_cooperative( + swap, + &swap_script, + refund_tx, + broadcast_fees_sat, + is_lowball, + ) + .await } } } @@ -723,7 +770,7 @@ impl LiquidSdk { ) -> Result<(), PaymentError> { debug!("Claim is pending for swap-in {swap_id}. Initiating cooperative claim"); let client = self.boltz_client_v2(); - let refund_tx = self.new_refund_tx(swap_script).await?; + let refund_tx = self.new_refund_tx(swap_id, swap_script).await?; let claim_tx_response = client.get_claim_tx_details(&swap_id.to_string())?; debug!("Received claim tx details: {:?}", &claim_tx_response); @@ -783,7 +830,7 @@ impl LiquidSdk { PaymentError::InvalidOrExpiredFees ); - let keypair = self.get_submarine_keys(0)?; + let keypair = utils::generate_keypair(); let refund_public_key = boltz_client::PublicKey { compressed: true, inner: keypair.public_key(), @@ -820,7 +867,7 @@ impl LiquidSdk { BoltzStatusStream::mark_swap_as_tracked(swap_id, SwapType::Submarine); let payer_amount_sat = req.fees_sat + receiver_amount_sat; - self.persister.insert_send_swap(SendSwap { + let swap = SendSwap { id: swap_id.clone(), invoice: req.invoice.clone(), payer_amount_sat, @@ -830,7 +877,9 @@ impl LiquidSdk { refund_tx_id: None, created_at: utils::now(), state: PaymentState::Created, - })?; + refund_private_key: keypair.display_secret().to_string(), + }; + self.persister.insert_send_swap(&swap)?; let result; let mut lockup_tx_id = String::new(); @@ -913,9 +962,16 @@ impl LiquidSdk { SubSwapStates::InvoiceFailedToPay | SubSwapStates::SwapExpired | SubSwapStates::TransactionLockupFailed => { - let refund_tx_id = self - .try_refund(swap_id, &swap_script, &keypair, receiver_amount_sat) - .await?; + let refund_tx_id = self.try_refund(&swap).await?; + + self.try_handle_send_swap_update( + swap_id, + Pending, + None, + None, + Some(&refund_tx_id), + ) + .await?; result = Err(PaymentError::Refunded { err: format!( @@ -1080,7 +1136,7 @@ impl LiquidSdk { &invoice.to_string(), )?; self.persister - .insert_receive_swap(ReceiveSwap { + .insert_receive_swap(&ReceiveSwap { id: swap_id.clone(), preimage: preimage_str, create_response_json, @@ -1153,7 +1209,7 @@ impl LiquidSdk { info!("Retrieving preimage from non-cooperative claim tx"); let id = &swap.id; - let keypair = self.get_submarine_keys(0)?; + let keypair = swap.get_refund_keypair()?; let create_response = swap.get_boltz_create_response()?; let electrum_client = ElectrumClient::new(&self.electrum_url)?; diff --git a/lib/core/src/utils.rs b/lib/core/src/utils.rs index 6749bd3..1fd2c50 100644 --- a/lib/core/src/utils.rs +++ b/lib/core/src/utils.rs @@ -44,11 +44,13 @@ pub(crate) fn get_swap_status_v2( return match args.first() { Some(update) if update.id == swap_id => { - info!("Got new swap status: {}", update.status); + info!("Got new swap status for {swap_id}: {}", update.status); Ok(update.status.clone()) } - Some(update) => Err(anyhow!("WS reply has wrong swap ID {update:?}")), + Some(update) => Err(anyhow!( + "WS reply has wrong swap ID {update:?}. Should be {swap_id}" + )), None => Err(anyhow!("WS reply contains no update")), }; } @@ -64,7 +66,9 @@ pub(crate) fn get_swap_status_v2( for e in &args { error!("Got error: {} for swap: {}", e.error, e.id); } - return Err(anyhow!("Got SwapUpdate errors: {args:?}")); + return Err(anyhow!( + "Got SwapUpdate errors for swap {swap_id}: {args:?}" + )); } } } @@ -82,3 +86,16 @@ pub(crate) fn json_to_pubkey(json: &str) -> Result boltz_client::Keypair { + let secp = boltz_client::Secp256k1::new(); + let mut rng = bip39::rand::rngs::OsRng; + let secret_key = lwk_wollet::secp256k1::SecretKey::new(&mut rng); + boltz_client::Keypair::from_secret_key(&secp, &secret_key) +} + +pub(crate) fn decode_keypair(secret_key: &str) -> Result { + let secp = boltz_client::Secp256k1::new(); + let secret_key = lwk_wollet::secp256k1::SecretKey::from_str(secret_key)?; + Ok(boltz_client::Keypair::from_secret_key(&secp, &secret_key)) +} diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index 7a7a922..6022a42 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -669,7 +669,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { Payment dco_decode_payment(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 8) throw Exception('unexpected arr length: expect 8 but see ${arr.length}'); + if (arr.length != 10) throw Exception('unexpected arr length: expect 10 but see ${arr.length}'); return Payment( txId: dco_decode_String(arr[0]), swapId: dco_decode_opt_String(arr[1]), @@ -677,8 +677,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { amountSat: dco_decode_u_64(arr[3]), feesSat: dco_decode_opt_box_autoadd_u_64(arr[4]), preimage: dco_decode_opt_String(arr[5]), - paymentType: dco_decode_payment_type(arr[6]), - status: dco_decode_payment_state(arr[7]), + refundTxId: dco_decode_opt_String(arr[6]), + refundTxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[7]), + paymentType: dco_decode_payment_type(arr[8]), + status: dco_decode_payment_state(arr[9]), ); } @@ -1090,6 +1092,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_amountSat = sse_decode_u_64(deserializer); var var_feesSat = sse_decode_opt_box_autoadd_u_64(deserializer); var var_preimage = sse_decode_opt_String(deserializer); + var var_refundTxId = sse_decode_opt_String(deserializer); + var var_refundTxAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); var var_paymentType = sse_decode_payment_type(deserializer); var var_status = sse_decode_payment_state(deserializer); return Payment( @@ -1099,6 +1103,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { amountSat: var_amountSat, feesSat: var_feesSat, preimage: var_preimage, + refundTxId: var_refundTxId, + refundTxAmountSat: var_refundTxAmountSat, paymentType: var_paymentType, status: var_status); } @@ -1546,6 +1552,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_64(self.amountSat, serializer); sse_encode_opt_box_autoadd_u_64(self.feesSat, serializer); sse_encode_opt_String(self.preimage, serializer); + sse_encode_opt_String(self.refundTxId, serializer); + sse_encode_opt_box_autoadd_u_64(self.refundTxAmountSat, serializer); sse_encode_payment_type(self.paymentType, serializer); sse_encode_payment_state(self.status, serializer); } diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index 4c8bbca..ec4c9e2 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -569,6 +569,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { wireObj.amount_sat = cst_encode_u_64(apiObj.amountSat); wireObj.fees_sat = cst_encode_opt_box_autoadd_u_64(apiObj.feesSat); wireObj.preimage = cst_encode_opt_String(apiObj.preimage); + wireObj.refund_tx_id = cst_encode_opt_String(apiObj.refundTxId); + wireObj.refund_tx_amount_sat = cst_encode_opt_box_autoadd_u_64(apiObj.refundTxAmountSat); wireObj.payment_type = cst_encode_payment_type(apiObj.paymentType); wireObj.status = cst_encode_payment_state(apiObj.status); } @@ -1369,6 +1371,10 @@ final class wire_cst_payment extends ffi.Struct { external ffi.Pointer preimage; + external ffi.Pointer refund_tx_id; + + external ffi.Pointer refund_tx_amount_sat; + @ffi.Int32() external int payment_type; diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index 86e311f..caf0e81 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -161,6 +161,12 @@ class Payment { /// In case of a Send swap, this is the preimage of the paid invoice (proof of payment). final String? preimage; + + /// For a Send swap which was refunded, this is the refund tx id + final String? refundTxId; + + /// For a Send swap which was refunded, this is the refund amount + final int? refundTxAmountSat; final PaymentType paymentType; /// Composite status representing the overall status of the payment. @@ -177,6 +183,8 @@ class Payment { required this.amountSat, this.feesSat, this.preimage, + this.refundTxId, + this.refundTxAmountSat, required this.paymentType, required this.status, }); @@ -189,6 +197,8 @@ class Payment { amountSat.hashCode ^ feesSat.hashCode ^ preimage.hashCode ^ + refundTxId.hashCode ^ + refundTxAmountSat.hashCode ^ paymentType.hashCode ^ status.hashCode; @@ -203,6 +213,8 @@ class Payment { amountSat == other.amountSat && feesSat == other.feesSat && preimage == other.preimage && + refundTxId == other.refundTxId && + refundTxAmountSat == other.refundTxAmountSat && paymentType == other.paymentType && status == other.status; } diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index d20fac5..cb78654 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -539,6 +539,10 @@ final class wire_cst_payment extends ffi.Struct { external ffi.Pointer preimage; + external ffi.Pointer refund_tx_id; + + external ffi.Pointer refund_tx_amount_sat; + @ffi.Int32() external int payment_type; diff --git a/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKMapper.kt b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKMapper.kt index ac50800..44243f8 100644 --- a/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKMapper.kt +++ b/packages/react-native/android/src/main/java/com/breezliquidsdk/BreezLiquidSDKMapper.kt @@ -172,6 +172,8 @@ fun asPayment(payment: ReadableMap): Payment? { val amountSat = payment.getDouble("amountSat").toULong() val feesSat = if (hasNonNullKey(payment, "feesSat")) payment.getDouble("feesSat").toULong() else null val preimage = if (hasNonNullKey(payment, "preimage")) payment.getString("preimage") else null + val refundTxId = if (hasNonNullKey(payment, "refundTxId")) payment.getString("refundTxId") else null + val refundTxAmountSat = if (hasNonNullKey(payment, "refundTxAmountSat")) payment.getDouble("refundTxAmountSat").toULong() else null val paymentType = payment.getString("paymentType")?.let { asPaymentType(it) }!! val status = payment.getString("status")?.let { asPaymentState(it) }!! return Payment( @@ -181,6 +183,8 @@ fun asPayment(payment: ReadableMap): Payment? { amountSat, feesSat, preimage, + refundTxId, + refundTxAmountSat, paymentType, status, ) @@ -194,6 +198,8 @@ fun readableMapOf(payment: Payment): ReadableMap { "amountSat" to payment.amountSat, "feesSat" to payment.feesSat, "preimage" to payment.preimage, + "refundTxId" to payment.refundTxId, + "refundTxAmountSat" to payment.refundTxAmountSat, "paymentType" to payment.paymentType.name.lowercase(), "status" to payment.status.name.lowercase(), ) diff --git a/packages/react-native/ios/BreezLiquidSDKMapper.swift b/packages/react-native/ios/BreezLiquidSDKMapper.swift index 206f4fa..640ab38 100644 --- a/packages/react-native/ios/BreezLiquidSDKMapper.swift +++ b/packages/react-native/ios/BreezLiquidSDKMapper.swift @@ -198,6 +198,20 @@ enum BreezLiquidSDKMapper { } preimage = preimageTmp } + var refundTxId: String? + if hasNonNilKey(data: payment, key: "refundTxId") { + guard let refundTxIdTmp = payment["refundTxId"] as? String else { + throw LiquidSdkError.Generic(message: errUnexpectedValue(fieldName: "refundTxId")) + } + refundTxId = refundTxIdTmp + } + var refundTxAmountSat: UInt64? + if hasNonNilKey(data: payment, key: "refundTxAmountSat") { + guard let refundTxAmountSatTmp = payment["refundTxAmountSat"] as? UInt64 else { + throw LiquidSdkError.Generic(message: errUnexpectedValue(fieldName: "refundTxAmountSat")) + } + refundTxAmountSat = refundTxAmountSatTmp + } guard let paymentTypeTmp = payment["paymentType"] as? String else { throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "paymentType", typeName: "Payment")) } @@ -215,6 +229,8 @@ enum BreezLiquidSDKMapper { amountSat: amountSat, feesSat: feesSat, preimage: preimage, + refundTxId: refundTxId, + refundTxAmountSat: refundTxAmountSat, paymentType: paymentType, status: status ) @@ -228,6 +244,8 @@ enum BreezLiquidSDKMapper { "amountSat": payment.amountSat, "feesSat": payment.feesSat == nil ? nil : payment.feesSat, "preimage": payment.preimage == nil ? nil : payment.preimage, + "refundTxId": payment.refundTxId == nil ? nil : payment.refundTxId, + "refundTxAmountSat": payment.refundTxAmountSat == nil ? nil : payment.refundTxAmountSat, "paymentType": valueOf(paymentType: payment.paymentType), "status": valueOf(paymentState: payment.status), ] diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index bf2bc89..f45248a 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -47,6 +47,8 @@ export interface Payment { amountSat: number feesSat?: number preimage?: string + refundTxId?: string + refundTxAmountSat?: number paymentType: PaymentType status: PaymentState }