From 29da23ea60f5b3c9e6b6cfd9724e93a70ea6b5aa Mon Sep 17 00:00:00 2001 From: binarybaron <86064887+binarybaron@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:01:57 +0100 Subject: [PATCH] fix(asb): Would silently fail if Monero refund transaction publish failed (#254) --- CHANGELOG.md | 2 + dev-docs/asb/README.md | 2 + swap/src/monero/wallet.rs | 73 +++++++++++++------------------- swap/src/protocol/alice/state.rs | 2 +- swap/src/protocol/alice/swap.rs | 40 +++++++++++++---- 5 files changed, 65 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4d2681..0791659b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- ASB: Fixed an issue where the ASB would silently fail if the publication of the Monero refund transaction failed. + ## [1.0.0-rc.12] - 2025-01-14 ## [1.0.0-rc.11] - 2024-12-22 diff --git a/dev-docs/asb/README.md b/dev-docs/asb/README.md index 14ebcf50..b22e0b1e 100644 --- a/dev-docs/asb/README.md +++ b/dev-docs/asb/README.md @@ -32,6 +32,7 @@ Consider joining the designated [Matrix chat](https://matrix.to/#/%23unstoppable ### Using Docker Running the ASB and its required services (Bitcoin node, Monero node, wallet RPC) can be complex to set up manually. We provide a Docker Compose solution that handles all of this automatically. See our [docker-compose repository](https://github.com/UnstoppableSwap/asb-docker-compose) for setup instructions and configuration details. + ## ASB Details The ASB is a long running daemon that acts as the trading partner to the swap CLI. @@ -212,6 +213,7 @@ Sparrow wallet import works as follows: ![image](transactions-tab.png) If the bitcoin amount in your wallet doesn't match "asb balance" output and you don't see (all) the transactions you need to increase the gap limit: + - go to Settings > Advanced... > Gap limit ![image](gap-limit.png) diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index 1017525a..7893fe38 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -116,64 +116,49 @@ impl Wallet { /// keys. The generated wallet will be opened, all funds sweeped to the /// main_address and then the wallet will be re-loaded using the internally /// stored name. - pub async fn create_from( + pub async fn create_from_keys_and_sweep( &self, file_name: String, private_spend_key: PrivateKey, private_view_key: PrivateViewKey, restore_height: BlockHeight, ) -> Result<()> { - let public_spend_key = PublicKey::from_private_key(&private_spend_key); - let public_view_key = PublicKey::from_private_key(&private_view_key.into()); + // Close the default wallet, generate the new wallet from the keys and load it + self.create_from_and_load( + file_name, + private_spend_key, + private_view_key, + restore_height, + ) + .await?; - let temp_wallet_address = - Address::standard(self.network, public_spend_key, public_view_key); + // Refresh the generated wallet + if let Err(error) = self.refresh(20).await { + return Err(anyhow::anyhow!(error) + .context("Failed to refresh generated wallet for sweeping to default wallet")); + } - // Close the default wallet before generating the other wallet to ensure that - // it saves its state correctly - let _ = self.inner.lock().await.close_wallet().await?; - - let _ = self + // Sweep all the funds from the generated wallet to the default wallet + let sweep_result = self .inner .lock() .await - .generate_from_keys( - file_name, - temp_wallet_address.to_string(), - private_spend_key.to_string(), - PrivateKey::from(private_view_key).to_string(), - restore_height.height, - String::from(""), - true, - ) - .await?; + .sweep_all(self.main_address.to_string()) + .await; - // Try to send all the funds from the generated wallet to the default wallet - match self.refresh(3).await { - Ok(_) => match self - .inner - .lock() - .await - .sweep_all(self.main_address.to_string()) - .await - { - Ok(sweep_all) => { - for tx in sweep_all.tx_hash_list { - tracing::info!( - %tx, - monero_address = %self.main_address, - "Monero transferred back to default wallet"); - } + match sweep_result { + Ok(sweep_all) => { + for tx in sweep_all.tx_hash_list { + tracing::info!( + %tx, + monero_address = %self.main_address, + "Monero transferred back to default wallet"); } - Err(error) => { - tracing::warn!( - address = %self.main_address, - "Failed to transfer Monero to default wallet: {:#}", error - ); - } - }, + } Err(error) => { - tracing::warn!("Failed to refresh generated wallet: {:#}", error); + return Err( + anyhow::anyhow!(error).context("Failed to transfer Monero to default wallet") + ); } } diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index 7627a529..74afe402 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -522,7 +522,7 @@ impl State3 { .await?; monero_wallet - .create_from( + .create_from_keys_and_sweep( file_name, spend_key, view_key, diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index d5cf3868..d8cf8b1d 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -427,15 +427,37 @@ where spend_key, state3, } => { - state3 - .refund_xmr( - monero_wallet, - monero_wallet_restore_blockheight, - swap_id.to_string(), - spend_key, - transfer_proof, - ) - .await?; + // We retry indefinitely to refund the Monero funds, until the refund transaction is confirmed + let backoff = backoff::ExponentialBackoffBuilder::new() + .with_max_elapsed_time(None) + .with_max_interval(Duration::from_secs(60)) + .build(); + + backoff::future::retry_notify( + backoff, + || async { + state3 + .refund_xmr( + monero_wallet, + monero_wallet_restore_blockheight, + swap_id.to_string(), + spend_key, + transfer_proof.clone(), + ) + .await + .map_err(backoff::Error::transient) + }, + |e, wait_time: Duration| { + tracing::warn!( + swap_id = %swap_id, + error = ?e, + "Failed to refund Monero. We will retry in {} seconds", + wait_time.as_secs() + ) + }, + ) + .await + .expect("We should never run out of retries while refunding Monero"); AliceState::XmrRefunded }