feat(gui): Refund swap in the background (#154)

Swaps will now be refunded as soon as the cancel timelock expires if the GUI is running but the swap dialog is not open.
This commit is contained in:
binarybaron 2024-11-14 14:20:22 +01:00 committed by GitHub
parent 4cf5cf719a
commit e46be4a9ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 210 additions and 27 deletions

View file

@ -301,6 +301,8 @@ impl ContextBuilder {
let seed = Seed::from_file_or_generate(data_dir.as_path())
.context("Failed to read seed in file")?;
let swap_lock = Arc::new(SwapLock::new());
// We initialize the Bitcoin wallet below
// To display the progress to the user, we emit events to the Tauri frontend
self.tauri_handle
@ -355,7 +357,12 @@ impl ContextBuilder {
// we start a background task to watch for timelock changes.
if let Some(wallet) = bitcoin_wallet.clone() {
if self.tauri_handle.is_some() {
let watcher = Watcher::new(wallet, db.clone(), self.tauri_handle.clone());
let watcher = Watcher::new(
wallet,
db.clone(),
self.tauri_handle.clone(),
swap_lock.clone(),
);
tokio::spawn(watcher.run());
}
}
@ -375,7 +382,7 @@ impl ContextBuilder {
is_testnet: self.is_testnet,
data_dir,
},
swap_lock: Arc::new(SwapLock::new()),
swap_lock,
tasks: Arc::new(PendingTaskList::default()),
tauri_handle: self.tauri_handle,
};

View file

@ -15,6 +15,8 @@ const SWAP_STATE_CHANGE_EVENT_NAME: &str = "swap-database-state-update";
const TIMELOCK_CHANGE_EVENT_NAME: &str = "timelock-change";
const CONTEXT_INIT_PROGRESS_EVENT_NAME: &str = "context-init-progress-update";
const BALANCE_CHANGE_EVENT_NAME: &str = "balance-change";
const BACKGROUND_REFUND_EVENT_NAME: &str = "background-refund";
#[derive(Debug, Clone)]
pub struct TauriHandle(
#[cfg(feature = "tauri")]
@ -82,6 +84,13 @@ pub trait TauriEmitter {
},
);
}
fn emit_background_refund_event(&self, swap_id: Uuid, state: BackgroundRefundState) {
let _ = self.emit_tauri_event(
BACKGROUND_REFUND_EVENT_NAME,
TauriBackgroundRefundEvent { swap_id, state },
);
}
}
impl TauriEmitter for TauriHandle {
@ -222,6 +231,23 @@ pub struct TauriTimelockChangeEvent {
timelock: Option<ExpiredTimelocks>,
}
#[derive(Serialize, Clone)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum BackgroundRefundState {
Started,
Failed { error: String },
Completed,
}
#[derive(Serialize, Clone)]
#[typeshare]
pub struct TauriBackgroundRefundEvent {
#[typeshare(serialized_as = "string")]
swap_id: Uuid,
state: BackgroundRefundState,
}
/// This struct contains the settings for the Context
#[typeshare]
#[derive(Debug, Serialize, Deserialize, Clone)]

View file

@ -1,4 +1,6 @@
use super::api::tauri_bindings::TauriEmitter;
use super::api::tauri_bindings::{BackgroundRefundState, TauriEmitter};
use super::api::SwapLock;
use super::cancel_and_refund;
use crate::bitcoin::{ExpiredTimelocks, Wallet};
use crate::cli::api::tauri_bindings::TauriHandle;
use crate::protocol::bob::BobState;
@ -15,6 +17,7 @@ pub struct Watcher {
wallet: Arc<Wallet>,
database: Arc<dyn Database + Send + Sync>,
tauri: Option<TauriHandle>,
swap_lock: Arc<SwapLock>,
/// This saves for every running swap the last known timelock status
cached_timelocks: HashMap<Uuid, Option<ExpiredTimelocks>>,
}
@ -28,12 +31,14 @@ impl Watcher {
wallet: Arc<Wallet>,
database: Arc<dyn Database + Send + Sync>,
tauri: Option<TauriHandle>,
swap_lock: Arc<SwapLock>,
) -> Self {
Self {
wallet,
database,
cached_timelocks: HashMap::new(),
tauri,
swap_lock,
}
}
@ -63,6 +68,7 @@ impl Watcher {
.balance()
.await
.context("Failed to fetch Bitcoin balance, retrying later")?;
// Emit a balance update event
self.tauri.emit_balance_update_event(new_balance);
@ -98,6 +104,49 @@ impl Watcher {
// Insert new status
self.cached_timelocks.insert(swap_id, new_timelock_status);
// If the swap has to be refunded, do it in the background
if let Some(ExpiredTimelocks::Cancel { .. }) = new_timelock_status {
// If the swap is already running, we can skip the refund
// The refund will be handled by the state machine
if let Some(current_swap_id) = self.swap_lock.get_current_swap_id().await {
if current_swap_id == swap_id {
continue;
}
}
if let Err(e) = self.swap_lock.acquire_swap_lock(swap_id).await {
tracing::error!(%e, %swap_id, "Watcher failed to refund a swap in the background because another swap is already running");
continue;
}
self.tauri
.emit_background_refund_event(swap_id, BackgroundRefundState::Started);
match cancel_and_refund(swap_id, self.wallet.clone(), self.database.clone()).await {
Err(e) => {
tracing::error!(%e, %swap_id, "Watcher failed to refund a swap in the background");
self.tauri.emit_background_refund_event(
swap_id,
BackgroundRefundState::Failed {
error: format!("{:?}", e),
},
);
}
Ok(_) => {
tracing::info!(%swap_id, "Watcher has refunded a swap in the background");
self.tauri.emit_background_refund_event(
swap_id,
BackgroundRefundState::Completed,
);
}
}
// We have to release the swap lock when we are done
self.swap_lock.release_swap_lock().await?;
}
}
Ok(())