mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2026-01-06 15:44:24 +01:00
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:
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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])),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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])),
|
||||
|
||||
@@ -71,6 +71,7 @@ pub(crate) fn new_chain_swap_state_handler(
|
||||
)?));
|
||||
|
||||
ChainSwapStateHandler::new(
|
||||
config,
|
||||
onchain_wallet,
|
||||
persister,
|
||||
swapper,
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user