Monitor chain swap addresses (#322)

* Check chain swap addresses for unspent outputs

* Monitoring expired swaps up to 4320 blocks after expiration

* Refactor chain swap monitoring

* Handle the error to prevent the loop exiting

* Add RefundPending state

* Check if RefundPendingbefore setting to Refundable

* Use script_get_balance to determine spent state

* Use unconfirmed balance to check if RefundPending should be reset to Refundable
This commit is contained in:
Ross Savage
2024-06-26 16:53:41 +02:00
committed by GitHub
parent ef5cd28fa5
commit e7844473cd
26 changed files with 614 additions and 229 deletions

View File

@@ -70,6 +70,8 @@ pub(crate) enum Command {
// Fee rate to use
sat_per_vbyte: u32,
},
/// Rescan onchain swaps
RescanOnchainSwaps,
/// Get the balance and general info of the current instance
GetInfo,
/// Sync local data with mempool and onchain data
@@ -294,6 +296,10 @@ pub(crate) async fn handle_command(
.await?;
command_result!(res)
}
Command::RescanOnchainSwaps => {
sdk.rescan_onchain_swaps().await?;
command_result!("Rescanned successfully")
}
Command::Sync => {
sdk.sync().await?;
command_result!("Synced successfully")

View File

@@ -28,6 +28,11 @@ typedef struct _Dart_Handle* Dart_Handle;
*/
#define DEFAULT_ZERO_CONF_MAX_SAT 100000
/**
* Number of blocks to monitor a swap after its timeout block height
*/
#define CHAIN_SWAP_MONTIORING_PERIOD_BITCOIN_BLOCKS 4320
typedef struct wire_cst_list_prim_u_8_strict {
uint8_t *ptr;
int32_t len;
@@ -704,8 +709,9 @@ typedef struct wire_cst_payment_error {
} wire_cst_payment_error;
typedef struct wire_cst_prepare_refund_response {
uint32_t refund_tx_vsize;
uint64_t refund_tx_fee_sat;
uint32_t tx_vsize;
uint64_t tx_fee_sat;
struct wire_cst_list_prim_u_8_strict *refund_tx_id;
} wire_cst_prepare_refund_response;
typedef struct wire_cst_receive_onchain_response {
@@ -801,6 +807,9 @@ void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_refund(int64_t
uintptr_t that,
struct wire_cst_refund_request *req);
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps(int64_t port_,
uintptr_t that);
WireSyncRust2DartDco frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_restore(uintptr_t that,
struct wire_cst_restore_request *req);
@@ -991,6 +1000,7 @@ static int64_t dummy_method_to_enforce_bundling(void) {
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_receive_onchain);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_receive_payment);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_refund);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_restore);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_send_payment);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_sync);

View File

@@ -401,6 +401,7 @@ enum PaymentState {
"Failed",
"TimedOut",
"Refundable",
"RefundPending",
};
dictionary RefundableSwap {
@@ -416,8 +417,9 @@ dictionary PrepareRefundRequest {
};
dictionary PrepareRefundResponse {
u32 refund_tx_vsize;
u64 refund_tx_fee_sat;
u32 tx_vsize;
u64 tx_fee_sat;
string? refund_tx_id = null;
};
dictionary RefundRequest {
@@ -516,6 +518,9 @@ interface BindingLiquidSdk {
[Throws=PaymentError]
RefundResponse refund(RefundRequest req);
[Throws=LiquidSdkError]
void rescan_onchain_swaps();
[Throws=LiquidSdkError]
void sync();

View File

@@ -193,6 +193,10 @@ impl BindingLiquidSdk {
rt().block_on(self.sdk.refund(&req))
}
pub fn rescan_onchain_swaps(&self) -> LiquidSdkResult<()> {
rt().block_on(self.sdk.rescan_onchain_swaps())
}
pub fn sync(&self) -> LiquidSdkResult<()> {
rt().block_on(self.sdk.sync()).map_err(Into::into)
}

View File

@@ -210,6 +210,10 @@ impl BindingLiquidSdk {
self.sdk.refund(&req).await
}
pub async fn rescan_onchain_swaps(&self) -> Result<(), LiquidSdkError> {
self.sdk.rescan_onchain_swaps().await
}
pub async fn sync(&self) -> Result<(), LiquidSdkError> {
self.sdk.sync().await.map_err(Into::into)
}

View File

@@ -1,10 +1,9 @@
use std::collections::HashMap;
use anyhow::Result;
use electrum_client::{Client, ElectrumApi, HeaderNotification};
use electrum_client::{Client, ElectrumApi, GetBalanceRes, HeaderNotification};
use lwk_wollet::{
bitcoin::{
self,
block::Header,
consensus::{deserialize, serialize},
BlockHash, Script, Transaction, Txid,
@@ -37,6 +36,9 @@ pub trait BitcoinChainService: Send + Sync {
/// Get the transactions involved in a list of scripts
fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>>;
/// Return the confirmed and unconfirmed balances of a script hash
fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes>;
}
pub(crate) struct ElectrumClient {
@@ -91,13 +93,8 @@ impl BitcoinChainService for ElectrumClient {
}
fn get_transactions(&self, txids: &[Txid]) -> Result<Vec<Transaction>> {
let txids: Vec<bitcoin::Txid> = txids
.iter()
.map(|t| bitcoin::Txid::from_raw_hash(t.to_raw_hash()))
.collect();
let mut result = vec![];
for tx in self.client.batch_transaction_get_raw(&txids)? {
for tx in self.client.batch_transaction_get_raw(txids)? {
let tx: Transaction = deserialize(&tx)?;
result.push(tx);
}
@@ -118,16 +115,15 @@ impl BitcoinChainService for ElectrumClient {
}
fn get_scripts_history(&self, scripts: &[&Script]) -> Result<Vec<Vec<History>>> {
let scripts: Vec<&bitcoin::Script> = scripts
.iter()
.map(|t| bitcoin::Script::from_bytes(t.as_bytes()))
.collect();
Ok(self
.client
.batch_script_get_history(&scripts)?
.batch_script_get_history(scripts)?
.into_iter()
.map(|e| e.into_iter().map(Into::into).collect())
.collect())
}
fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes> {
Ok(self.client.script_get_balance(script)?)
}
}

View File

@@ -1,3 +1,4 @@
use std::time::Duration;
use std::{str::FromStr, sync::Arc};
use anyhow::{anyhow, Result};
@@ -5,18 +6,23 @@ use boltz_client::swaps::boltzv2;
use boltz_client::swaps::{boltz::ChainSwapStates, boltzv2::CreateChainResponse};
use log::{debug, error, info, warn};
use lwk_wollet::elements::Transaction;
use tokio::sync::{broadcast, Mutex};
use tokio::sync::{broadcast, watch, Mutex};
use tokio::time::MissedTickBehavior;
use crate::chain::bitcoin::BitcoinChainService;
use crate::chain::liquid::LiquidChainService;
use crate::error::{LiquidSdkError, LiquidSdkResult};
use crate::model::PaymentState::{Complete, Created, Failed, Pending, Refundable, TimedOut};
use crate::model::{ChainSwap, Direction, PaymentTxData, PaymentType};
use crate::model::PaymentState::{
Complete, Created, Failed, Pending, RefundPending, Refundable, TimedOut,
};
use crate::model::{ChainSwap, Config, Direction, PaymentTxData, PaymentType};
use crate::sdk::CHAIN_SWAP_MONTIORING_PERIOD_BITCOIN_BLOCKS;
use crate::swapper::Swapper;
use crate::wallet::OnchainWallet;
use crate::{error::PaymentError, model::PaymentState, persist::Persister};
pub(crate) struct ChainSwapStateHandler {
config: Config,
onchain_wallet: Arc<dyn OnchainWallet>,
persister: Arc<Persister>,
swapper: Arc<dyn Swapper>,
@@ -27,6 +33,7 @@ pub(crate) struct ChainSwapStateHandler {
impl ChainSwapStateHandler {
pub(crate) fn new(
config: Config,
onchain_wallet: Arc<dyn OnchainWallet>,
persister: Arc<Persister>,
swapper: Arc<dyn Swapper>,
@@ -35,6 +42,7 @@ impl ChainSwapStateHandler {
) -> Result<Self> {
let (subscription_notifier, _) = broadcast::channel::<String>(30);
Ok(Self {
config,
onchain_wallet,
persister,
swapper,
@@ -44,6 +52,28 @@ impl ChainSwapStateHandler {
})
}
pub(crate) async fn start(self: Arc<Self>, mut shutdown: watch::Receiver<()>) {
let cloned = self.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60 * 10));
interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
loop {
tokio::select! {
_ = interval.tick() => {
if let Err(e) = cloned.rescan_incoming_chain_swaps().await {
error!("Error checking chain swaps: {e:?}");
}
},
_ = shutdown.changed() => {
info!("Received shutdown signal, exiting chain swap loop");
return;
}
}
}
});
}
pub(crate) fn subscribe_payment_updates(&self) -> broadcast::Receiver<String> {
self.subscription_notifier.subscribe()
}
@@ -62,6 +92,86 @@ impl ChainSwapStateHandler {
}
}
pub(crate) async fn rescan_incoming_chain_swaps(&self) -> Result<()> {
let current_height = self.bitcoin_chain_service.lock().await.tip()?.height as u32;
let chain_swaps: Vec<ChainSwap> = self
.persister
.list_chain_swaps()?
.into_iter()
.filter(|s| s.direction == Direction::Incoming)
.collect();
info!(
"Rescanning {} Chain Swap(s) at height {}",
chain_swaps.len(),
current_height
);
for swap in chain_swaps {
if let Err(e) = self.rescan_incoming_chain_swap(&swap, current_height).await {
error!("Error rescanning Chain Swap {}: {e:?}", swap.id);
}
}
Ok(())
}
async fn rescan_incoming_chain_swap(
&self,
swap: &ChainSwap,
current_height: u32,
) -> Result<()> {
let monitoring_block_height =
swap.timeout_block_height + CHAIN_SWAP_MONTIORING_PERIOD_BITCOIN_BLOCKS;
let is_swap_expired = current_height > swap.timeout_block_height;
let is_monitoring_expired = current_height > monitoring_block_height;
if (is_swap_expired && !is_monitoring_expired) || swap.state == RefundPending {
let swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?;
let script_pubkey = swap_script
.to_address(self.config.network.as_bitcoin_chain())
.map_err(|e| anyhow!("Error getting script address: {e:?}"))?
.script_pubkey();
let script_balance = self
.bitcoin_chain_service
.lock()
.await
.script_get_balance(script_pubkey.as_script())?;
info!(
"Chain Swap {} has {} confirmed and {} unconfirmed sats",
swap.id, script_balance.confirmed, script_balance.unconfirmed
);
if script_balance.confirmed > 0
&& script_balance.unconfirmed == 0
&& swap.state != Refundable
{
// If there are unspent funds sent to the lockup script address then set
// the state to Refundable.
info!(
"Chain Swap {} has {} unspent sats. Setting the swap to refundable",
swap.id, script_balance.confirmed
);
self.update_swap_info(&swap.id, Refundable, None, None, None, None)
.await?;
} else if script_balance.confirmed == 0 {
// If the funds sent to the lockup script address are spent then set the
// state back to Complete/Failed.
let to_state = match swap.claim_tx_id {
Some(_) => Complete,
None => Failed,
};
if to_state != swap.state {
info!(
"Chain Swap {} has 0 unspent sats. Setting the swap to {:?}",
swap.id, to_state
);
self.update_swap_info(&swap.id, to_state, None, None, None, None)
.await?;
}
}
}
Ok(())
}
async fn on_new_incoming_status(
&self,
swap: &ChainSwap,
@@ -181,9 +291,12 @@ impl ChainSwapStateHandler {
match swap_state {
// The swap is created
ChainSwapStates::Created => {
match swap.user_lockup_tx_id.clone() {
// Create the user lockup tx when sending
None => {
match (swap.state, swap.user_lockup_tx_id.clone()) {
// The swap timed out before receiving this status
(TimedOut, _) => warn!("Chain Swap {id} timed out, do not broadcast a lockup tx"),
// Create the user lockup tx
(_, None) => {
let create_response = swap.get_boltz_create_response()?;
let user_lockup_tx = self.lockup_funds(id, &create_response).await?;
let lockup_tx_id = user_lockup_tx.txid().to_string();
@@ -205,8 +318,8 @@ impl ChainSwapStateHandler {
.await?;
},
// Lockup tx already exists when sending
Some(lockup_tx_id) => warn!("User lockup tx for Chain Swap {id} was already broadcast: txid {lockup_tx_id}"),
// Lockup tx already exists
(_, Some(lockup_tx_id)) => warn!("User lockup tx for Chain Swap {id} was already broadcast: txid {lockup_tx_id}"),
};
Ok(())
}
@@ -277,7 +390,7 @@ impl ChainSwapStateHandler {
info!("Broadcast refund tx for Chain Swap {id}. Tx id: {refund_tx_id}");
self.update_swap_info(
id,
Pending,
RefundPending,
None,
None,
None,
@@ -416,25 +529,23 @@ impl ChainSwapStateHandler {
lockup_address: &str,
output_address: &str,
sat_per_vbyte: u32,
) -> LiquidSdkResult<(u32, u64)> {
) -> LiquidSdkResult<(u32, u64, Option<String>)> {
let swap = self
.persister
.fetch_chain_swap_by_lockup_address(lockup_address)?
.ok_or(LiquidSdkError::Generic {
err: format!("Swap {} not found", lockup_address),
})?;
match swap.refund_tx_id {
Some(refund_tx_id) => Err(LiquidSdkError::Generic {
err: format!(
"Refund tx for Chain Swap {} was already broadcast: txid {refund_tx_id}",
if let Some(refund_tx_id) = swap.refund_tx_id.clone() {
warn!(
"A refund tx for Chain Swap {} was already broadcast: txid {refund_tx_id}",
swap.id
),
}),
None => {
);
}
let (tx_vsize, tx_fee_sat) =
self.swapper
.prepare_chain_swap_refund(&swap, output_address, sat_per_vbyte as f32)
}
}
.prepare_chain_swap_refund(&swap, output_address, sat_per_vbyte as f32)?;
Ok((tx_vsize, tx_fee_sat, swap.refund_tx_id))
}
pub(crate) async fn refund_incoming_swap(
@@ -449,30 +560,24 @@ impl ChainSwapStateHandler {
.ok_or(PaymentError::Generic {
err: format!("Swap {} not found", lockup_address),
})?;
match swap.refund_tx_id {
Some(refund_tx_id) => Err(PaymentError::Generic {
err: format!(
"Refund tx for Chain Swap {} was already broadcast: txid {refund_tx_id}",
if let Some(refund_tx_id) = swap.refund_tx_id.clone() {
warn!(
"A refund tx for Chain Swap {} was already broadcast: txid {refund_tx_id}",
swap.id
),
}),
None => {
let (_, broadcast_fees_sat) = self.swapper.prepare_chain_swap_refund(
&swap,
output_address,
sat_per_vbyte as f32,
)?;
let refund_res = self.swapper.refund_chain_swap_cooperative(
&swap,
output_address,
broadcast_fees_sat,
);
}
let (_, broadcast_fees_sat) =
self.swapper
.prepare_chain_swap_refund(&swap, output_address, sat_per_vbyte as f32)?;
let refund_res =
self.swapper
.refund_chain_swap_cooperative(&swap, output_address, broadcast_fees_sat);
let refund_tx_id = match refund_res {
Ok(res) => Ok(res),
Err(e) => {
warn!("Cooperative refund failed: {:?}", e);
let current_height =
self.bitcoin_chain_service.lock().await.tip()?.height as u32;
let current_height = self.bitcoin_chain_service.lock().await.tip()?.height as u32;
self.swapper.refund_chain_swap_non_cooperative(
&swap,
broadcast_fees_sat,
@@ -486,12 +591,17 @@ impl ChainSwapStateHandler {
"Broadcast refund tx for Chain Swap {}. Tx id: {refund_tx_id}",
swap.id
);
self.update_swap_info(&swap.id, Pending, None, None, None, Some(&refund_tx_id))
self.update_swap_info(
&swap.id,
RefundPending,
None,
None,
None,
Some(&refund_tx_id),
)
.await?;
Ok(refund_tx_id)
}
}
}
pub(crate) async fn refund_outgoing_swap(
&self,
@@ -532,7 +642,14 @@ impl ChainSwapStateHandler {
"Broadcast refund tx for Chain Swap {}. Tx id: {refund_tx_id}",
swap.id
);
self.update_swap_info(&swap.id, Pending, None, None, None, Some(&refund_tx_id))
self.update_swap_info(
&swap.id,
RefundPending,
None,
None,
None,
Some(&refund_tx_id),
)
.await?;
Ok(refund_tx_id)
}
@@ -548,12 +665,12 @@ impl ChainSwapStateHandler {
err: "Cannot transition to Created state".to_string(),
}),
(Created | Pending | Refundable, Pending) => Ok(()),
(Created | Pending, Pending) => Ok(()),
(_, Pending) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Pending state"),
}),
(Created | Pending, Complete) => Ok(()),
(Created | Pending | RefundPending, Complete) => Ok(()),
(_, Complete) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Complete state"),
}),
@@ -563,11 +680,16 @@ impl ChainSwapStateHandler {
err: format!("Cannot transition from {from_state:?} to TimedOut state"),
}),
(Created | Pending, Refundable) => Ok(()),
(Created | Pending | RefundPending | Failed | Complete, Refundable) => Ok(()),
(_, Refundable) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Refundable state"),
}),
(Pending | Refundable, RefundPending) => Ok(()),
(_, RefundPending) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to RefundPending state"),
}),
(Complete, Failed) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Failed state"),
}),
@@ -607,12 +729,13 @@ mod tests {
),
(
Pending,
HashSet::from([Pending, Complete, Refundable, Failed]),
HashSet::from([Pending, Complete, Refundable, RefundPending, Failed]),
),
(TimedOut, HashSet::from([Failed])),
(Complete, HashSet::from([])),
(Refundable, HashSet::from([Pending, Failed])),
(Failed, HashSet::from([Failed])),
(Complete, HashSet::from([Refundable])),
(Refundable, HashSet::from([RefundPending, Failed])),
(RefundPending, HashSet::from([Refundable, Complete, Failed])),
(Failed, HashSet::from([Failed, Refundable])),
]);
for (first_state, allowed_states) in valid_combinations.iter() {

View File

@@ -1195,8 +1195,9 @@ impl CstDecode<crate::model::PrepareRefundResponse> for wire_cst_prepare_refund_
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::PrepareRefundResponse {
crate::model::PrepareRefundResponse {
refund_tx_vsize: self.refund_tx_vsize.cst_decode(),
refund_tx_fee_sat: self.refund_tx_fee_sat.cst_decode(),
tx_vsize: self.tx_vsize.cst_decode(),
tx_fee_sat: self.tx_fee_sat.cst_decode(),
refund_tx_id: self.refund_tx_id.cst_decode(),
}
}
}
@@ -1969,8 +1970,9 @@ impl Default for wire_cst_prepare_refund_request {
impl NewWithNullPtr for wire_cst_prepare_refund_response {
fn new_with_null_ptr() -> Self {
Self {
refund_tx_vsize: Default::default(),
refund_tx_fee_sat: Default::default(),
tx_vsize: Default::default(),
tx_fee_sat: Default::default(),
refund_tx_id: core::ptr::null_mut(),
}
}
}
@@ -2371,6 +2373,14 @@ pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_re
wire__crate__bindings__BindingLiquidSdk_refund_impl(port_, that, req)
}
#[no_mangle]
pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps(
port_: i64,
that: usize,
) {
wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps_impl(port_, that)
}
#[no_mangle]
pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_restore(
that: usize,
@@ -3570,8 +3580,9 @@ pub struct wire_cst_prepare_refund_request {
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_prepare_refund_response {
refund_tx_vsize: u32,
refund_tx_fee_sat: u64,
tx_vsize: u32,
tx_fee_sat: u64,
refund_tx_id: *mut wire_cst_list_prim_u_8_strict,
}
#[repr(C)]
#[derive(Clone, Copy)]

View File

@@ -39,7 +39,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueNom,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.0.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1268203752;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1515195984;
// Section: executor
@@ -1023,6 +1023,52 @@ fn wire__crate__bindings__BindingLiquidSdk_refund_impl(
},
)
}
fn wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
that: impl CstDecode<
RustOpaqueNom<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<BindingLiquidSdk>>,
>,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::DcoCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "BindingLiquidSdk_rescan_onchain_swaps",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let api_that = that.cst_decode();
move |context| async move {
transform_result_dco::<_, _, crate::error::LiquidSdkError>(
(move || async move {
let mut api_that_guard = None;
let decode_indices_ =
flutter_rust_bridge::for_generated::lockable_compute_decode_order(
vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new(
&api_that, 0, false,
)],
);
for i in decode_indices_ {
match i {
0 => {
api_that_guard =
Some(api_that.lockable_decode_async_ref().await)
}
_ => unreachable!(),
}
}
let api_that_guard = api_that_guard.unwrap();
let output_ok = crate::bindings::BindingLiquidSdk::rescan_onchain_swaps(
&*api_that_guard,
)
.await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bindings__BindingLiquidSdk_restore_impl(
that: impl CstDecode<
RustOpaqueNom<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<BindingLiquidSdk>>,
@@ -1534,6 +1580,7 @@ impl CstDecode<crate::model::PaymentState> for i32 {
3 => crate::model::PaymentState::Failed,
4 => crate::model::PaymentState::TimedOut,
5 => crate::model::PaymentState::Refundable,
6 => crate::model::PaymentState::RefundPending,
_ => unreachable!("Invalid variant for PaymentState: {}", self),
}
}
@@ -2702,6 +2749,7 @@ impl SseDecode for crate::model::PaymentState {
3 => crate::model::PaymentState::Failed,
4 => crate::model::PaymentState::TimedOut,
5 => crate::model::PaymentState::Refundable,
6 => crate::model::PaymentState::RefundPending,
_ => unreachable!("Invalid variant for PaymentState: {}", inner),
};
}
@@ -2802,11 +2850,13 @@ impl SseDecode for crate::model::PrepareRefundRequest {
impl SseDecode for crate::model::PrepareRefundResponse {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_refundTxVsize = <u32>::sse_decode(deserializer);
let mut var_refundTxFeeSat = <u64>::sse_decode(deserializer);
let mut var_txVsize = <u32>::sse_decode(deserializer);
let mut var_txFeeSat = <u64>::sse_decode(deserializer);
let mut var_refundTxId = <Option<String>>::sse_decode(deserializer);
return crate::model::PrepareRefundResponse {
refund_tx_vsize: var_refundTxVsize,
refund_tx_fee_sat: var_refundTxFeeSat,
tx_vsize: var_txVsize,
tx_fee_sat: var_txFeeSat,
refund_tx_id: var_refundTxId,
};
}
}
@@ -4079,6 +4129,7 @@ impl flutter_rust_bridge::IntoDart for crate::model::PaymentState {
Self::Failed => 3.into_dart(),
Self::TimedOut => 4.into_dart(),
Self::Refundable => 5.into_dart(),
Self::RefundPending => 6.into_dart(),
_ => unreachable!(),
}
}
@@ -4245,8 +4296,9 @@ impl flutter_rust_bridge::IntoIntoDart<crate::model::PrepareRefundRequest>
impl flutter_rust_bridge::IntoDart for crate::model::PrepareRefundResponse {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.refund_tx_vsize.into_into_dart().into_dart(),
self.refund_tx_fee_sat.into_into_dart().into_dart(),
self.tx_vsize.into_into_dart().into_dart(),
self.tx_fee_sat.into_into_dart().into_dart(),
self.refund_tx_id.into_into_dart().into_dart(),
]
.into_dart()
}
@@ -5481,6 +5533,7 @@ impl SseEncode for crate::model::PaymentState {
crate::model::PaymentState::Failed => 3,
crate::model::PaymentState::TimedOut => 4,
crate::model::PaymentState::Refundable => 5,
crate::model::PaymentState::RefundPending => 6,
_ => {
unimplemented!("");
}
@@ -5563,8 +5616,9 @@ impl SseEncode for crate::model::PrepareRefundRequest {
impl SseEncode for crate::model::PrepareRefundResponse {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<u32>::sse_encode(self.refund_tx_vsize, serializer);
<u64>::sse_encode(self.refund_tx_fee_sat, serializer);
<u32>::sse_encode(self.tx_vsize, serializer);
<u64>::sse_encode(self.tx_fee_sat, serializer);
<Option<String>>::sse_encode(self.refund_tx_id, serializer);
}
}

View File

@@ -250,8 +250,9 @@ pub struct PrepareRefundRequest {
#[derive(Debug, Serialize)]
pub struct PrepareRefundResponse {
pub refund_tx_vsize: u32,
pub refund_tx_fee_sat: u64,
pub tx_vsize: u32,
pub tx_fee_sat: u64,
pub refund_tx_id: Option<String>,
}
#[derive(Debug, Serialize)]
@@ -681,11 +682,11 @@ pub enum PaymentState {
///
/// ## Send Swaps
///
/// Covers the cases when
/// - our lockup tx was broadcast or
/// - a refund was initiated and our refund tx was broadcast
/// This is the status when our lockup tx was broadcast
///
/// When the refund tx is broadcast, `refund_tx_id` is set in the swap.
/// ## Chain Swaps
///
/// This is the status when the user lockup tx was broadcast
///
/// ## No swap data available
///
@@ -696,7 +697,7 @@ pub enum PaymentState {
///
/// Covers the case when the claim tx is confirmed.
///
/// ## Send Swaps
/// ## Send and Chain Swaps
///
/// This is the status when the claim tx is broadcast and we see it in the mempool.
///
@@ -709,12 +710,12 @@ pub enum PaymentState {
///
/// This is the status when the swap failed for any reason and the Receive could not complete.
///
/// ## Send Swaps
/// ## Send and Chain Swaps
///
/// This is the status when a swap refund was initiated and the refund tx is confirmed.
Failed = 3,
/// ## Send Swaps
/// ## Send and Outgoing Chain Swaps
///
/// This covers the case when the swap state is still Created and the swap fails to reach the
/// Pending state in time. The TimedOut state indicates the lockup tx should never be broadcast.
@@ -725,6 +726,13 @@ pub enum PaymentState {
/// This covers the case when the swap failed for any reason and there is a user lockup tx.
/// The swap in this case has to be manually refunded with a provided Bitcoin address
Refundable = 5,
/// ## Send and Chain Swaps
///
/// This is the status when a refund was initiated and our refund tx was broadcast
///
/// When the refund tx is broadcast, `refund_tx_id` is set in the swap.
RefundPending = 6,
}
impl ToSql for PaymentState {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
@@ -741,6 +749,7 @@ impl FromSql for PaymentState {
3 => Ok(PaymentState::Failed),
4 => Ok(PaymentState::TimedOut),
5 => Ok(PaymentState::Refundable),
6 => Ok(PaymentState::RefundPending),
_ => Err(FromSqlError::OutOfRange(i)),
},
_ => Err(FromSqlError::InvalidType),

View File

@@ -155,11 +155,16 @@ impl Persister {
})
}
pub(crate) fn list_chain_swaps(
pub(crate) fn list_chain_swaps(&self) -> Result<Vec<ChainSwap>> {
let con: Connection = self.get_connection()?;
self.list_chain_swaps_where(&con, vec![])
}
pub(crate) fn list_chain_swaps_where(
&self,
con: &Connection,
where_clauses: Vec<String>,
) -> rusqlite::Result<Vec<ChainSwap>> {
) -> Result<Vec<ChainSwap>> {
let query = Self::list_chain_swaps_query(where_clauses);
let chain_swaps = con
.prepare(&query)?
@@ -169,32 +174,36 @@ impl Persister {
Ok(chain_swaps)
}
pub(crate) fn list_ongoing_chain_swaps(
pub(crate) fn list_chain_swaps_by_state(
&self,
con: &Connection,
) -> rusqlite::Result<Vec<ChainSwap>> {
states: Vec<PaymentState>,
) -> Result<Vec<ChainSwap>> {
let mut where_clause: Vec<String> = Vec::new();
where_clause.push(format!(
"state in ({})",
[PaymentState::Created, PaymentState::Pending]
states
.iter()
.map(|t| format!("'{}'", *t as i8))
.collect::<Vec<_>>()
.join(", ")
));
self.list_chain_swaps(con, where_clause)
self.list_chain_swaps_where(con, where_clause)
}
pub(crate) fn list_ongoing_chain_swaps(&self, con: &Connection) -> Result<Vec<ChainSwap>> {
self.list_chain_swaps_by_state(con, vec![PaymentState::Created, PaymentState::Pending])
}
pub(crate) fn list_pending_chain_swaps(&self) -> Result<Vec<ChainSwap>> {
let con: Connection = self.get_connection()?;
let query = Self::list_chain_swaps_query(vec!["state = ?1".to_string()]);
let res = con
.prepare(&query)?
.query_map(params![PaymentState::Pending], Self::sql_row_to_chain_swap)?
.map(|i| i.unwrap())
.collect();
Ok(res)
self.list_chain_swaps_by_state(&con, vec![PaymentState::Pending])
}
pub(crate) fn list_refundable_chain_swaps(&self) -> Result<Vec<ChainSwap>> {
let con: Connection = self.get_connection()?;
self.list_chain_swaps_by_state(&con, vec![PaymentState::Refundable])
}
/// Pending Chain swaps, indexed by refund tx id
@@ -214,20 +223,6 @@ impl Persister {
Ok(res)
}
pub(crate) fn list_refundable_chain_swaps(&self) -> Result<Vec<ChainSwap>> {
let con: Connection = self.get_connection()?;
let query = Self::list_chain_swaps_query(vec!["state = ?1".to_string()]);
let res = con
.prepare(&query)?
.query_map(
params![PaymentState::Refundable],
Self::sql_row_to_chain_swap,
)?
.map(|i| i.unwrap())
.collect();
Ok(res)
}
pub(crate) fn update_chain_swap_accept_zero_conf(
&self,
swap_id: &str,

View File

@@ -12,7 +12,9 @@ use lwk_wollet::History;
use tokio::sync::{broadcast, Mutex};
use crate::chain::liquid::LiquidChainService;
use crate::model::PaymentState::{Complete, Created, Failed, Pending, Refundable, TimedOut};
use crate::model::PaymentState::{
Complete, Created, Failed, Pending, RefundPending, Refundable, TimedOut,
};
use crate::model::{Config, PaymentTxData, PaymentType, ReceiveSwap};
use crate::{ensure_sdk, utils};
use crate::{
@@ -305,6 +307,10 @@ impl ReceiveSwapStateHandler {
err: format!("Cannot transition from {from_state:?} to Refundable state"),
}),
(_, RefundPending) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to RefundPending state"),
}),
(Complete, Failed) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Failed state"),
}),
@@ -421,6 +427,7 @@ mod tests {
(TimedOut, HashSet::from([TimedOut, Failed])),
(Complete, HashSet::from([])),
(Refundable, HashSet::from([Failed])),
(RefundPending, HashSet::from([Failed])),
(Failed, HashSet::from([Failed])),
]);

View File

@@ -39,6 +39,8 @@ use crate::{
};
pub const DEFAULT_DATA_DIR: &str = ".data";
/// Number of blocks to monitor a swap after its timeout block height
pub const CHAIN_SWAP_MONTIORING_PERIOD_BITCOIN_BLOCKS: u32 = 4320;
pub struct LiquidSdk {
config: Config,
@@ -55,7 +57,7 @@ pub struct LiquidSdk {
shutdown_receiver: watch::Receiver<()>,
send_swap_state_handler: SendSwapStateHandler,
receive_swap_state_handler: ReceiveSwapStateHandler,
chain_swap_state_handler: ChainSwapStateHandler,
chain_swap_state_handler: Arc<ChainSwapStateHandler>,
}
impl LiquidSdk {
@@ -120,13 +122,14 @@ impl LiquidSdk {
liquid_chain_service.clone(),
);
let chain_swap_state_handler = ChainSwapStateHandler::new(
let chain_swap_state_handler = Arc::new(ChainSwapStateHandler::new(
config.clone(),
onchain_wallet.clone(),
persister.clone(),
swapper.clone(),
liquid_chain_service.clone(),
bitcoin_chain_service.clone(),
)?;
)?);
let breez_server = BreezServer::new(PRODUCTION_BREEZSERVER_URL.into(), None)?;
@@ -173,7 +176,6 @@ impl LiquidSdk {
/// Internal method. Should only be used as part of [LiquidSdk::start].
async fn start_background_tasks(self: &Arc<LiquidSdk>) -> LiquidSdkResult<()> {
// Periodically run sync() in the background
// TODO: Check the bitcoin chain for confirmed refund txs
let sdk_clone = self.clone();
let mut shutdown_rx_sync_loop = self.shutdown_receiver.clone();
tokio::spawn(async move {
@@ -198,8 +200,12 @@ impl LiquidSdk {
.clone()
.start(reconnect_handler, self.shutdown_receiver.clone())
.await;
self.chain_swap_state_handler
.clone()
.start(self.shutdown_receiver.clone())
.await;
self.track_swap_updates().await;
self.track_refundable_swaps().await;
self.track_pending_swaps().await;
Ok(())
}
@@ -285,7 +291,7 @@ impl LiquidSdk {
});
}
async fn track_refundable_swaps(self: &Arc<LiquidSdk>) {
async fn track_pending_swaps(self: &Arc<LiquidSdk>) {
let cloned = self.clone();
tokio::spawn(async move {
let mut shutdown_receiver = cloned.shutdown_receiver.clone();
@@ -308,15 +314,15 @@ impl LiquidSdk {
Ok(pending_chain_swaps) => {
for swap in pending_chain_swaps {
if let Err(e) = cloned.check_chain_swap_expiration(&swap).await {
error!("Error checking expiration for Send Swap {}: {e:?}", swap.id);
error!("Error checking expiration for Chain Swap {}: {e:?}", swap.id);
}
}
}
Err(e) => error!("Error listing pending send swaps: {e:?}"),
Err(e) => error!("Error listing pending chain swaps: {e:?}"),
}
},
_ = shutdown_receiver.changed() => {
info!("Received shutdown signal, exiting refundable swaps loop");
info!("Received shutdown signal, exiting pending swaps loop");
return;
}
}
@@ -440,32 +446,24 @@ impl LiquidSdk {
}
}
}
Swap::Send(SendSwap { refund_tx_id, .. }) => {
match refund_tx_id {
Some(_) => {
// The refund tx has now been broadcast
self.notify_event_listeners(
LiquidSdkEvent::PaymentRefundPending {
details: payment,
},
)
.await?
}
None => {
Swap::Send(_) => {
// The lockup tx is in the mempool/confirmed
self.notify_event_listeners(
LiquidSdkEvent::PaymentPending {
details: payment,
},
LiquidSdkEvent::PaymentPending { details: payment },
)
.await?
}
}
}
},
None => debug!("Payment has no swap id"),
}
}
RefundPending => {
// The swap state has changed to RefundPending
self.notify_event_listeners(LiquidSdkEvent::PaymentRefundPending {
details: payment,
})
.await?
}
Failed => match payment.payment_type {
PaymentType::Receive => {
self.notify_event_listeners(LiquidSdkEvent::PaymentFailed {
@@ -518,12 +516,12 @@ impl LiquidSdk {
None => pending_send_sat += p.amount_sat,
},
Created => pending_send_sat += p.amount_sat,
Refundable | TimedOut => {}
Refundable | RefundPending | TimedOut => {}
},
PaymentType::Receive => match p.status {
Complete => confirmed_received_sat += p.amount_sat,
Pending => pending_receive_sat += p.amount_sat,
Created | Refundable | Failed | TimedOut => {}
Created | Refundable | RefundPending | Failed | TimedOut => {}
},
}
}
@@ -824,7 +822,7 @@ impl LiquidSdk {
Some(swap) => match swap.state {
Pending => return Err(PaymentError::PaymentInProgress),
Complete => return Err(PaymentError::AlreadyPaid),
Failed => {
RefundPending | Failed => {
return Err(PaymentError::InvalidInvoice {
err: "Payment has already failed. Please try with another invoice."
.to_string(),
@@ -1310,14 +1308,15 @@ impl LiquidSdk {
&self,
req: &PrepareRefundRequest,
) -> LiquidSdkResult<PrepareRefundResponse> {
let (refund_tx_vsize, refund_tx_fee_sat) = self.chain_swap_state_handler.prepare_refund(
let (tx_vsize, tx_fee_sat, refund_tx_id) = self.chain_swap_state_handler.prepare_refund(
&req.swap_address,
&req.refund_address,
req.sat_per_vbyte,
)?;
Ok(PrepareRefundResponse {
refund_tx_vsize,
refund_tx_fee_sat,
tx_vsize,
tx_fee_sat,
refund_tx_id,
})
}
@@ -1329,6 +1328,13 @@ impl LiquidSdk {
Ok(RefundResponse { refund_tx_id })
}
pub async fn rescan_onchain_swaps(&self) -> LiquidSdkResult<()> {
self.chain_swap_state_handler
.rescan_incoming_chain_swaps()
.await?;
Ok(())
}
/// This method fetches the chain tx data (onchain and mempool) using LWK. For every wallet tx,
/// it inserts or updates a corresponding entry in our Payments table.
async fn sync_payments_with_chain_data(&self, with_scan: bool) -> Result<()> {

View File

@@ -12,7 +12,9 @@ use lwk_wollet::hashes::{sha256, Hash};
use tokio::sync::{broadcast, Mutex};
use crate::chain::liquid::LiquidChainService;
use crate::model::PaymentState::{Complete, Created, Failed, Pending, Refundable, TimedOut};
use crate::model::PaymentState::{
Complete, Created, Failed, Pending, RefundPending, Refundable, TimedOut,
};
use crate::model::{Config, SendSwap};
use crate::swapper::Swapper;
use crate::wallet::OnchainWallet;
@@ -157,7 +159,13 @@ impl SendSwapStateHandler {
let refund_tx_id = self.refund(&swap).await?;
info!("Broadcast refund tx for Send Swap {id}. Tx id: {refund_tx_id}");
self.update_swap_info(id, Pending, None, None, Some(&refund_tx_id))
self.update_swap_info(
id,
RefundPending,
None,
None,
Some(&refund_tx_id),
)
.await?;
}
},
@@ -428,6 +436,11 @@ impl SendSwapStateHandler {
err: format!("Cannot transition from {from_state:?} to Refundable state"),
}),
(Pending, RefundPending) => Ok(()),
(_, RefundPending) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to RefundPending state"),
}),
(Complete, Failed) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Failed state"),
}),
@@ -476,7 +489,10 @@ mod tests {
Created,
HashSet::from([Pending, Complete, TimedOut, Failed]),
),
(Pending, HashSet::from([Pending, Complete, Failed])),
(
Pending,
HashSet::from([Pending, RefundPending, Complete, Failed]),
),
(TimedOut, HashSet::from([TimedOut, Failed])),
(Complete, HashSet::from([])),
(Refundable, HashSet::from([Failed])),

View File

@@ -71,6 +71,7 @@ pub(crate) fn new_chain_swap_state_handler(
)?));
ChainSwapStateHandler::new(
config,
onchain_wallet,
persister,
swapper,

View File

@@ -73,6 +73,8 @@ abstract class BindingLiquidSdk implements RustOpaqueInterface {
Future<RefundResponse> refund({required RefundRequest req});
Future<void> rescanOnchainSwaps();
void restore({required RestoreRequest req});
Future<SendPaymentResponse> sendPayment({required PrepareSendResponse req});

View File

@@ -55,7 +55,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.0.0';
@override
int get rustContentHash => -1268203752;
int get rustContentHash => 1515195984;
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
stem: 'breez_liquid_sdk',
@@ -120,6 +120,8 @@ abstract class RustLibApi extends BaseApi {
Future<RefundResponse> crateBindingsBindingLiquidSdkRefund(
{required BindingLiquidSdk that, required RefundRequest req});
Future<void> crateBindingsBindingLiquidSdkRescanOnchainSwaps({required BindingLiquidSdk that});
void crateBindingsBindingLiquidSdkRestore({required BindingLiquidSdk that, required RestoreRequest req});
Future<SendPaymentResponse> crateBindingsBindingLiquidSdkSendPayment(
@@ -689,6 +691,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["that", "req"],
);
@override
Future<void> crateBindingsBindingLiquidSdkRescanOnchainSwaps({required BindingLiquidSdk that}) {
return handler.executeNormal(NormalTask(
callFfi: (port_) {
var arg0 =
cst_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk(
that);
return wire.wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps(port_, arg0);
},
codec: DcoCodec(
decodeSuccessData: dco_decode_unit,
decodeErrorData: dco_decode_liquid_sdk_error,
),
constMeta: kCrateBindingsBindingLiquidSdkRescanOnchainSwapsConstMeta,
argValues: [that],
apiImpl: this,
));
}
TaskConstMeta get kCrateBindingsBindingLiquidSdkRescanOnchainSwapsConstMeta => const TaskConstMeta(
debugName: "BindingLiquidSdk_rescan_onchain_swaps",
argNames: ["that"],
);
@override
void crateBindingsBindingLiquidSdkRestore({required BindingLiquidSdk that, required RestoreRequest req}) {
return handler.executeSync(SyncTask(
@@ -2043,10 +2069,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
PrepareRefundResponse dco_decode_prepare_refund_response(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
return PrepareRefundResponse(
refundTxVsize: dco_decode_u_32(arr[0]),
refundTxFeeSat: dco_decode_u_64(arr[1]),
txVsize: dco_decode_u_32(arr[0]),
txFeeSat: dco_decode_u_64(arr[1]),
refundTxId: dco_decode_opt_String(arr[2]),
);
}
@@ -3438,9 +3465,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected
PrepareRefundResponse sse_decode_prepare_refund_response(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_refundTxVsize = sse_decode_u_32(deserializer);
var var_refundTxFeeSat = sse_decode_u_64(deserializer);
return PrepareRefundResponse(refundTxVsize: var_refundTxVsize, refundTxFeeSat: var_refundTxFeeSat);
var var_txVsize = sse_decode_u_32(deserializer);
var var_txFeeSat = sse_decode_u_64(deserializer);
var var_refundTxId = sse_decode_opt_String(deserializer);
return PrepareRefundResponse(txVsize: var_txVsize, txFeeSat: var_txFeeSat, refundTxId: var_refundTxId);
}
@protected
@@ -4754,8 +4782,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
@protected
void sse_encode_prepare_refund_response(PrepareRefundResponse self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_u_32(self.refundTxVsize, serializer);
sse_encode_u_64(self.refundTxFeeSat, serializer);
sse_encode_u_32(self.txVsize, serializer);
sse_encode_u_64(self.txFeeSat, serializer);
sse_encode_opt_String(self.refundTxId, serializer);
}
@protected
@@ -5010,6 +5039,10 @@ class BindingLiquidSdkImpl extends RustOpaque implements BindingLiquidSdk {
Future<RefundResponse> refund({required RefundRequest req}) =>
RustLib.instance.api.crateBindingsBindingLiquidSdkRefund(that: this, req: req);
Future<void> rescanOnchainSwaps() => RustLib.instance.api.crateBindingsBindingLiquidSdkRescanOnchainSwaps(
that: this,
);
void restore({required RestoreRequest req}) =>
RustLib.instance.api.crateBindingsBindingLiquidSdkRestore(that: this, req: req);

View File

@@ -2178,8 +2178,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void cst_api_fill_to_wire_prepare_refund_response(
PrepareRefundResponse apiObj, wire_cst_prepare_refund_response wireObj) {
wireObj.refund_tx_vsize = cst_encode_u_32(apiObj.refundTxVsize);
wireObj.refund_tx_fee_sat = cst_encode_u_64(apiObj.refundTxFeeSat);
wireObj.tx_vsize = cst_encode_u_32(apiObj.txVsize);
wireObj.tx_fee_sat = cst_encode_u_64(apiObj.txFeeSat);
wireObj.refund_tx_id = cst_encode_opt_String(apiObj.refundTxId);
}
@protected
@@ -3190,6 +3191,22 @@ class RustLibWire implements BaseWire {
_wire__crate__bindings__BindingLiquidSdk_refundPtr
.asFunction<void Function(int, int, ffi.Pointer<wire_cst_refund_request>)>();
void wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps(
int port_,
int that,
) {
return _wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps(
port_,
that,
);
}
late final _wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swapsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.UintPtr)>>(
'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps');
late final _wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps =
_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swapsPtr.asFunction<void Function(int, int)>();
WireSyncRust2DartDco wire__crate__bindings__BindingLiquidSdk_restore(
int that,
ffi.Pointer<wire_cst_restore_request> req,
@@ -4795,10 +4812,12 @@ final class wire_cst_payment_error extends ffi.Struct {
final class wire_cst_prepare_refund_response extends ffi.Struct {
@ffi.Uint32()
external int refund_tx_vsize;
external int tx_vsize;
@ffi.Uint64()
external int refund_tx_fee_sat;
external int tx_fee_sat;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> refund_tx_id;
}
final class wire_cst_receive_onchain_response extends ffi.Struct {
@@ -4828,3 +4847,5 @@ const double DEFAULT_ZERO_CONF_MIN_FEE_RATE_TESTNET = 0.1;
const double DEFAULT_ZERO_CONF_MIN_FEE_RATE_MAINNET = 0.01;
const int DEFAULT_ZERO_CONF_MAX_SAT = 100000;
const int CHAIN_SWAP_MONTIORING_PERIOD_BITCOIN_BLOCKS = 4320;

View File

@@ -367,11 +367,11 @@ enum PaymentState {
///
/// ## Send Swaps
///
/// Covers the cases when
/// - our lockup tx was broadcast or
/// - a refund was initiated and our refund tx was broadcast
/// This is the status when our lockup tx was broadcast
///
/// When the refund tx is broadcast, `refund_tx_id` is set in the swap.
/// ## Chain Swaps
///
/// This is the status when the user lockup tx was broadcast
///
/// ## No swap data available
///
@@ -382,7 +382,7 @@ enum PaymentState {
///
/// Covers the case when the claim tx is confirmed.
///
/// ## Send Swaps
/// ## Send and Chain Swaps
///
/// This is the status when the claim tx is broadcast and we see it in the mempool.
///
@@ -395,12 +395,12 @@ enum PaymentState {
///
/// This is the status when the swap failed for any reason and the Receive could not complete.
///
/// ## Send Swaps
/// ## Send and Chain Swaps
///
/// This is the status when a swap refund was initiated and the refund tx is confirmed.
failed,
/// ## Send Swaps
/// ## Send and Outgoing Chain Swaps
///
/// This covers the case when the swap state is still Created and the swap fails to reach the
/// Pending state in time. The TimedOut state indicates the lockup tx should never be broadcast.
@@ -411,6 +411,13 @@ enum PaymentState {
/// This covers the case when the swap failed for any reason and there is a user lockup tx.
/// The swap in this case has to be manually refunded with a provided Bitcoin address
refundable,
/// ## Send and Chain Swaps
///
/// This is the status when a refund was initiated and our refund tx was broadcast
///
/// When the refund tx is broadcast, `refund_tx_id` is set in the swap.
refundPending,
;
}
@@ -560,24 +567,27 @@ class PrepareRefundRequest {
}
class PrepareRefundResponse {
final int refundTxVsize;
final BigInt refundTxFeeSat;
final int txVsize;
final BigInt txFeeSat;
final String? refundTxId;
const PrepareRefundResponse({
required this.refundTxVsize,
required this.refundTxFeeSat,
required this.txVsize,
required this.txFeeSat,
this.refundTxId,
});
@override
int get hashCode => refundTxVsize.hashCode ^ refundTxFeeSat.hashCode;
int get hashCode => txVsize.hashCode ^ txFeeSat.hashCode ^ refundTxId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PrepareRefundResponse &&
runtimeType == other.runtimeType &&
refundTxVsize == other.refundTxVsize &&
refundTxFeeSat == other.refundTxFeeSat;
txVsize == other.txVsize &&
txFeeSat == other.txFeeSat &&
refundTxId == other.refundTxId;
}
class PrepareSendRequest {

View File

@@ -435,6 +435,23 @@ class FlutterBreezLiquidBindings {
_frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_refundPtr
.asFunction<void Function(int, int, ffi.Pointer<wire_cst_refund_request>)>();
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps(
int port_,
int that,
) {
return _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps(
port_,
that,
);
}
late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swapsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.UintPtr)>>(
'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps');
late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps =
_frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swapsPtr
.asFunction<void Function(int, int)>();
WireSyncRust2DartDco frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_restore(
int that,
ffi.Pointer<wire_cst_restore_request> req,
@@ -2107,10 +2124,12 @@ final class wire_cst_payment_error extends ffi.Struct {
final class wire_cst_prepare_refund_response extends ffi.Struct {
@ffi.Uint32()
external int refund_tx_vsize;
external int tx_vsize;
@ffi.Uint64()
external int refund_tx_fee_sat;
external int tx_fee_sat;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> refund_tx_id;
}
final class wire_cst_receive_onchain_response extends ffi.Struct {
@@ -2143,3 +2162,5 @@ const double DEFAULT_ZERO_CONF_MIN_FEE_RATE_TESTNET = 0.1;
const double DEFAULT_ZERO_CONF_MIN_FEE_RATE_MAINNET = 0.01;
const int DEFAULT_ZERO_CONF_MAX_SAT = 100000;
const int CHAIN_SWAP_MONTIORING_PERIOD_BITCOIN_BLOCKS = 4320;

View File

@@ -1289,25 +1289,28 @@ fun asPrepareRefundResponse(prepareRefundResponse: ReadableMap): PrepareRefundRe
if (!validateMandatoryFields(
prepareRefundResponse,
arrayOf(
"refundTxVsize",
"refundTxFeeSat",
"txVsize",
"txFeeSat",
),
)
) {
return null
}
val refundTxVsize = prepareRefundResponse.getInt("refundTxVsize").toUInt()
val refundTxFeeSat = prepareRefundResponse.getDouble("refundTxFeeSat").toULong()
val txVsize = prepareRefundResponse.getInt("txVsize").toUInt()
val txFeeSat = prepareRefundResponse.getDouble("txFeeSat").toULong()
val refundTxId = if (hasNonNullKey(prepareRefundResponse, "refundTxId")) prepareRefundResponse.getString("refundTxId") else null
return PrepareRefundResponse(
refundTxVsize,
refundTxFeeSat,
txVsize,
txFeeSat,
refundTxId,
)
}
fun readableMapOf(prepareRefundResponse: PrepareRefundResponse): ReadableMap =
readableMapOf(
"refundTxVsize" to prepareRefundResponse.refundTxVsize,
"refundTxFeeSat" to prepareRefundResponse.refundTxFeeSat,
"txVsize" to prepareRefundResponse.txVsize,
"txFeeSat" to prepareRefundResponse.txFeeSat,
"refundTxId" to prepareRefundResponse.refundTxId,
)
fun asPrepareRefundResponseList(arr: ReadableArray): List<PrepareRefundResponse> {

View File

@@ -389,6 +389,18 @@ class BreezLiquidSDKModule(
}
}
@ReactMethod
fun rescanOnchainSwaps(promise: Promise) {
executor.execute {
try {
getBindingLiquidSdk().rescanOnchainSwaps()
promise.resolve(readableMapOf("status" to "ok"))
} catch (e: Exception) {
promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e)
}
}
}
@ReactMethod
fun sync(promise: Promise) {
executor.execute {

View File

@@ -1513,23 +1513,32 @@ enum BreezLiquidSDKMapper {
}
static func asPrepareRefundResponse(prepareRefundResponse: [String: Any?]) throws -> PrepareRefundResponse {
guard let refundTxVsize = prepareRefundResponse["refundTxVsize"] as? UInt32 else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "refundTxVsize", typeName: "PrepareRefundResponse"))
guard let txVsize = prepareRefundResponse["txVsize"] as? UInt32 else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "txVsize", typeName: "PrepareRefundResponse"))
}
guard let refundTxFeeSat = prepareRefundResponse["refundTxFeeSat"] as? UInt64 else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "refundTxFeeSat", typeName: "PrepareRefundResponse"))
guard let txFeeSat = prepareRefundResponse["txFeeSat"] as? UInt64 else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "txFeeSat", typeName: "PrepareRefundResponse"))
}
var refundTxId: String?
if hasNonNilKey(data: prepareRefundResponse, key: "refundTxId") {
guard let refundTxIdTmp = prepareRefundResponse["refundTxId"] as? String else {
throw LiquidSdkError.Generic(message: errUnexpectedValue(fieldName: "refundTxId"))
}
refundTxId = refundTxIdTmp
}
return PrepareRefundResponse(
refundTxVsize: refundTxVsize,
refundTxFeeSat: refundTxFeeSat
txVsize: txVsize,
txFeeSat: txFeeSat,
refundTxId: refundTxId
)
}
static func dictionaryOf(prepareRefundResponse: PrepareRefundResponse) -> [String: Any?] {
return [
"refundTxVsize": prepareRefundResponse.refundTxVsize,
"refundTxFeeSat": prepareRefundResponse.refundTxFeeSat,
"txVsize": prepareRefundResponse.txVsize,
"txFeeSat": prepareRefundResponse.txFeeSat,
"refundTxId": prepareRefundResponse.refundTxId == nil ? nil : prepareRefundResponse.refundTxId,
]
}
@@ -2794,6 +2803,9 @@ enum BreezLiquidSDKMapper {
case "refundable":
return PaymentState.refundable
case "refundPending":
return PaymentState.refundPending
default: throw LiquidSdkError.Generic(message: "Invalid variant \(paymentState) for enum PaymentState")
}
}
@@ -2817,6 +2829,9 @@ enum BreezLiquidSDKMapper {
case .refundable:
return "refundable"
case .refundPending:
return "refundPending"
}
}

View File

@@ -118,6 +118,11 @@ RCT_EXTERN_METHOD(
reject: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
rescanOnchainSwaps: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
sync: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject

View File

@@ -294,6 +294,16 @@ class RNBreezLiquidSDK: RCTEventEmitter {
}
}
@objc(rescanOnchainSwaps:reject:)
func rescanOnchainSwaps(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
do {
try getBindingLiquidSdk().rescanOnchainSwaps()
resolve(["status": "ok"])
} catch let err {
rejectErr(err: err, reject: reject)
}
}
@objc(sync:reject:)
func sync(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
do {

View File

@@ -219,8 +219,9 @@ export interface PrepareRefundRequest {
}
export interface PrepareRefundResponse {
refundTxVsize: number
refundTxFeeSat: number
txVsize: number
txFeeSat: number
refundTxId?: string
}
export interface PrepareSendRequest {
@@ -443,7 +444,8 @@ export enum PaymentState {
COMPLETE = "complete",
FAILED = "failed",
TIMED_OUT = "timedOut",
REFUNDABLE = "refundable"
REFUNDABLE = "refundable",
REFUND_PENDING = "refundPending"
}
export enum PaymentType {
@@ -579,6 +581,10 @@ export const refund = async (req: RefundRequest): Promise<RefundResponse> => {
return response
}
export const rescanOnchainSwaps = async (): Promise<void> => {
await BreezLiquidSDK.rescanOnchainSwaps()
}
export const sync = async (): Promise<void> => {
await BreezLiquidSDK.sync()
}