Alice sweeps refunded funds into default wallet

Since Alice's refund scenario starts with generating the temporary wallet
from keys to claim the XMR which results in Alice' unloading the wallet.
Alice then loads her original wallet to be able to handle more swaps.
Since Alice is in the role of the long running daemon handling concurrent
swaps, the operation to close, claim and re-open her default wallet must
be atomic.
This PR adds an additional step, that sweeps all the refunded XMR back into
the default wallet. In order to ensure that this is possible, Alice has to
ensure that the locked XMR got enough confirmations.
These changes allow us to assert Alice's balance after refunding.
This commit is contained in:
Daniel Karzel 2021-03-16 19:24:41 +11:00
parent 16dfea035b
commit 396c4177a6
7 changed files with 119 additions and 53 deletions

View File

@ -164,18 +164,16 @@ async fn init_wallets(
bitcoin_balance bitcoin_balance
); );
let monero_wallet = monero::Wallet::new( let monero_wallet = monero::Wallet::open_or_create(
config.monero.wallet_rpc_url.clone(), config.monero.wallet_rpc_url.clone(),
DEFAULT_WALLET_NAME.to_string(), DEFAULT_WALLET_NAME.to_string(),
env_config, env_config,
); )
.await?;
// Setup the Monero wallet
monero_wallet.open_or_create().await?;
let balance = monero_wallet.get_balance().await?; let balance = monero_wallet.get_balance().await?;
if balance == Amount::ZERO { if balance == Amount::ZERO {
let deposit_address = monero_wallet.get_main_address().await?; let deposit_address = monero_wallet.get_main_address();
warn!( warn!(
"The Monero balance is 0, make sure to deposit funds at: {}", "The Monero balance is 0, make sure to deposit funds at: {}",
deposit_address deposit_address

View File

@ -295,18 +295,12 @@ async fn init_monero_wallet(
.run(network, monero_daemon_host.as_str()) .run(network, monero_daemon_host.as_str())
.await?; .await?;
let monero_wallet = monero::Wallet::new( let monero_wallet = monero::Wallet::open_or_create(
monero_wallet_rpc_process.endpoint(), monero_wallet_rpc_process.endpoint(),
MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(), MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(),
env_config, env_config,
); )
.await?;
monero_wallet.open_or_create().await?;
let _test_wallet_connection = monero_wallet
.block_height()
.await
.context("Failed to validate connection to monero-wallet-rpc")?;
Ok((monero_wallet, monero_wallet_rpc_process)) Ok((monero_wallet, monero_wallet_rpc_process))
} }

View File

@ -5,7 +5,7 @@ use crate::monero::{
use ::monero::{Address, Network, PrivateKey, PublicKey}; use ::monero::{Address, Network, PrivateKey, PublicKey};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use monero_rpc::wallet; use monero_rpc::wallet;
use monero_rpc::wallet::{BlockHeight, CheckTxKey, Client, Refreshed}; use monero_rpc::wallet::{BlockHeight, CheckTxKey, Refreshed};
use std::future::Future; use std::future::Future;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
@ -19,24 +19,44 @@ pub struct Wallet {
inner: Mutex<wallet::Client>, inner: Mutex<wallet::Client>,
network: Network, network: Network,
name: String, name: String,
main_address: monero::Address,
sync_interval: Duration, sync_interval: Duration,
} }
impl Wallet { impl Wallet {
pub fn new(url: Url, name: String, env_config: Config) -> Self { /// Connect to a wallet RPC and load the given wallet by name.
Self::new_with_client(Client::new(url), name, env_config) pub async fn open_or_create(url: Url, name: String, env_config: Config) -> Result<Self> {
let client = wallet::Client::new(url);
let open_wallet_response = client.open_wallet(name.as_str()).await;
if open_wallet_response.is_err() {
client.create_wallet(name.as_str()).await.context(
"Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available",
)?;
debug!("Created Monero wallet {}", name);
} else {
debug!("Opened Monero wallet {}", name);
} }
pub fn new_with_client(client: wallet::Client, name: String, env_config: Config) -> Self { Self::connect(client, name, env_config).await
Self { }
/// Connects to a wallet RPC where a wallet is already loaded.
pub async fn connect(client: wallet::Client, name: String, env_config: Config) -> Result<Self> {
let main_address =
monero::Address::from_str(client.get_address(0).await?.address.as_str())?;
Ok(Self {
inner: Mutex::new(client), inner: Mutex::new(client),
network: env_config.monero_network, network: env_config.monero_network,
name, name,
main_address,
sync_interval: env_config.monero_sync_interval(), sync_interval: env_config.monero_sync_interval(),
} })
} }
pub async fn open(&self) -> Result<()> { /// Re-open the wallet using the internally stored name.
pub async fn re_open(&self) -> Result<()> {
self.inner self.inner
.lock() .lock()
.await .await
@ -45,21 +65,8 @@ impl Wallet {
Ok(()) Ok(())
} }
pub async fn open_or_create(&self) -> Result<()> { /// Close the wallet and open (load) another wallet by generating it from
let open_wallet_response = self.open().await; /// keys. The generated wallet will remain loaded.
if open_wallet_response.is_err() {
self.inner.lock().await.create_wallet(self.name.as_str()).await.context(
"Unable to create Monero wallet, please ensure that the monero-wallet-rpc is available",
)?;
debug!("Created Monero wallet {}", self.name);
} else {
debug!("Opened Monero wallet {}", self.name);
}
Ok(())
}
pub async fn create_from_and_load( pub async fn create_from_and_load(
&self, &self,
private_spend_key: PrivateKey, private_spend_key: PrivateKey,
@ -89,6 +96,10 @@ impl Wallet {
Ok(()) Ok(())
} }
/// Close the wallet and open (load) another wallet by generating it from
/// 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(
&self, &self,
private_spend_key: PrivateKey, private_spend_key: PrivateKey,
@ -98,23 +109,48 @@ impl Wallet {
let public_spend_key = PublicKey::from_private_key(&private_spend_key); let public_spend_key = PublicKey::from_private_key(&private_spend_key);
let public_view_key = PublicKey::from_private_key(&private_view_key.into()); let public_view_key = PublicKey::from_private_key(&private_view_key.into());
let address = Address::standard(self.network, public_spend_key, public_view_key); let temp_wallet_address =
Address::standard(self.network, public_spend_key, public_view_key);
let wallet = self.inner.lock().await; let wallet = self.inner.lock().await;
// Properly close the wallet before generating the other wallet to ensure that // Close the default wallet before generating the other wallet to ensure that
// it saves its state correctly // it saves its state correctly
let _ = wallet.close_wallet().await?; let _ = wallet.close_wallet().await?;
let _ = wallet let _ = wallet
.generate_from_keys( .generate_from_keys(
&address.to_string(), &temp_wallet_address.to_string(),
&private_spend_key.to_string(), &private_spend_key.to_string(),
&PrivateKey::from(private_view_key).to_string(), &PrivateKey::from(private_view_key).to_string(),
restore_height.height, restore_height.height,
) )
.await?; .await?;
// Try to send all the funds from the generated wallet to the default wallet
match wallet.refresh().await {
Ok(_) => match wallet
.sweep_all(self.main_address.to_string().as_str())
.await
{
Ok(sweep_all) => {
for tx in sweep_all.tx_hash_list {
tracing::info!(%tx, "Monero transferred back to default wallet {}", self.main_address);
}
}
Err(e) => {
tracing::warn!(
"Transferring Monero back to default wallet {} failed with {:#}",
self.main_address,
e
);
}
},
Err(e) => {
tracing::warn!("Refreshing the generated wallet failed with {:#}", e);
}
}
let _ = wallet.open_wallet(self.name.as_str()).await?; let _ = wallet.open_wallet(self.name.as_str()).await?;
Ok(()) Ok(())
@ -209,9 +245,8 @@ impl Wallet {
self.inner.lock().await.block_height().await self.inner.lock().await.block_height().await
} }
pub async fn get_main_address(&self) -> Result<Address> { pub fn get_main_address(&self) -> Address {
let address = self.inner.lock().await.get_address(0).await?; self.main_address
Ok(Address::from_str(address.address.as_str())?)
} }
pub async fn refresh(&self) -> Result<Refreshed> { pub async fn refresh(&self) -> Result<Refreshed> {

View File

@ -2,7 +2,8 @@ use crate::bitcoin::{
current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, TxCancel, TxPunish, TxRefund,
}; };
use crate::env::Config; use crate::env::Config;
use crate::monero::wallet::TransferRequest; use crate::monero::wallet::{TransferRequest, WatchRequest};
use crate::monero::TransferProof;
use crate::protocol::alice::{Message1, Message3}; use crate::protocol::alice::{Message1, Message3};
use crate::protocol::bob::{Message0, Message2, Message4}; use crate::protocol::bob::{Message0, Message2, Message4};
use crate::protocol::CROSS_CURVE_PROOF_SYSTEM; use crate::protocol::CROSS_CURVE_PROOF_SYSTEM;
@ -357,6 +358,24 @@ impl State3 {
} }
} }
pub fn lock_xmr_watch_request(
&self,
transfer_proof: TransferProof,
conf_target: u32,
) -> WatchRequest {
let S_a = monero::PublicKey::from_private_key(&monero::PrivateKey { scalar: self.s_a });
let public_spend_key = S_a + self.S_b_monero;
let public_view_key = self.v.public();
WatchRequest {
public_spend_key,
public_view_key,
transfer_proof,
conf_target,
expected: self.xmr,
}
}
pub fn tx_cancel(&self) -> TxCancel { pub fn tx_cancel(&self) -> TxCancel {
TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B) TxCancel::new(&self.tx_lock, self.cancel_timelock, self.a.public(), self.B)
} }

View File

@ -97,13 +97,20 @@ async fn run_until_internal(
.transfer(state3.lock_xmr_transfer_request()) .transfer(state3.lock_xmr_transfer_request())
.await?; .await?;
// TODO(Franck): Wait for Monero to be confirmed once monero_wallet
// Waiting for XMR confirmations should not be done in here, but in a separate .watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof.clone(), 1))
.await?;
// TODO: Waiting for XMR confirmations should be done in a separate
// state! We have to record that Alice has already sent the transaction. // state! We have to record that Alice has already sent the transaction.
// Otherwise Alice might publish the lock tx twice! // Otherwise Alice might publish the lock tx twice!
event_loop_handle event_loop_handle
.send_transfer_proof(transfer_proof) .send_transfer_proof(transfer_proof.clone())
.await?;
monero_wallet
.watch_for_transfer(state3.lock_xmr_watch_request(transfer_proof, 10))
.await?; .await?;
AliceState::XmrLocked { AliceState::XmrLocked {

View File

@ -12,7 +12,7 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked)); let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
let alice_swap = ctx.alice_next_swap().await; let alice_swap = ctx.alice_next_swap().await;
let _ = tokio::spawn(alice::run(alice_swap)); let alice_swap = tokio::spawn(alice::run(alice_swap));
let bob_state = bob_swap.await??; let bob_state = bob_swap.await??;
assert!(matches!(bob_state, BobState::BtcLocked { .. })); assert!(matches!(bob_state, BobState::BtcLocked { .. }));
@ -56,6 +56,9 @@ async fn given_bob_manually_refunds_after_btc_locked_bob_refunds() {
ctx.assert_bob_refunded(bob_state).await; ctx.assert_bob_refunded(bob_state).await;
let alice_state = alice_swap.await??;
ctx.assert_alice_refunded(alice_state).await;
Ok(()) Ok(())
}) })
.await .await

View File

@ -57,7 +57,7 @@ struct BobParams {
impl BobParams { impl BobParams {
pub async fn builder(&self, event_loop_handle: bob::EventLoopHandle) -> Result<bob::Builder> { pub async fn builder(&self, event_loop_handle: bob::EventLoopHandle) -> Result<bob::Builder> {
let receive_address = self.monero_wallet.get_main_address().await?; let receive_address = self.monero_wallet.get_main_address();
Ok(bob::Builder::new( Ok(bob::Builder::new(
Database::open(&self.db_path.clone().as_path()).unwrap(), Database::open(&self.db_path.clone().as_path()).unwrap(),
@ -191,7 +191,15 @@ impl TestContext {
.get_balance() .get_balance()
.await .await
.unwrap(); .unwrap();
assert_eq!(xmr_balance_after_swap, self.xmr_amount);
// Alice pays fees - comparison does not take exact lock fee into account
assert!(
xmr_balance_after_swap > self.alice_starting_balances.xmr - self.xmr_amount,
"{} > {} - {}",
xmr_balance_after_swap,
self.alice_starting_balances.xmr,
self.xmr_amount
);
} }
pub async fn assert_alice_punished(&self, state: AliceState) { pub async fn assert_alice_punished(&self, state: AliceState) {
@ -237,7 +245,7 @@ impl TestContext {
); );
// unload the generated wallet by opening the original wallet // unload the generated wallet by opening the original wallet
self.bob_monero_wallet.open().await.unwrap(); self.bob_monero_wallet.re_open().await.unwrap();
// refresh the original wallet to make sure the balance is caught up // refresh the original wallet to make sure the balance is caught up
self.bob_monero_wallet.refresh().await.unwrap(); self.bob_monero_wallet.refresh().await.unwrap();
@ -587,11 +595,13 @@ async fn init_test_wallets(
.await .await
.unwrap(); .unwrap();
let xmr_wallet = swap::monero::Wallet::new_with_client( let xmr_wallet = swap::monero::Wallet::connect(
monero.wallet(name).unwrap().client(), monero.wallet(name).unwrap().client(),
name.to_string(), name.to_string(),
env_config, env_config,
); )
.await
.unwrap();
let electrum_rpc_url = { let electrum_rpc_url = {
let input = format!("tcp://@localhost:{}", electrum_rpc_port); let input = format!("tcp://@localhost:{}", electrum_rpc_port);