Compare commits
No commits in common. 'master' and '0.10.2' have entirely different histories.
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,5 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet" ]
|
||||
|
||||
[patch.crates-io]
|
||||
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
|
||||
jsonrpc_client = { git = "https://github.com/delta1/rust-jsonrpc-client.git", rev = "3b6081697cd616c952acb9c2f02d546357d35506" }
|
||||
monero = { git = "https://github.com/comit-network/monero-rs", rev = "818f38b" }
|
||||
|
@ -0,0 +1,22 @@
|
||||
status = [
|
||||
"static_analysis",
|
||||
"build (x86_64-unknown-linux-gnu, ubuntu-latest)",
|
||||
"build (armv7-unknown-linux-gnueabihf, ubuntu-latest)",
|
||||
"build (x86_64-apple-darwin, macos-latest)",
|
||||
"build (x86_64-pc-windows-msvc, windows-latest)",
|
||||
"test (ubuntu-latest)",
|
||||
"test (macos-latest)",
|
||||
"docker_tests (happy_path)",
|
||||
"docker_tests (happy_path_restart_bob_after_xmr_locked)",
|
||||
"docker_tests (happy_path_restart_alice_after_xmr_locked)",
|
||||
"docker_tests (happy_path_restart_bob_before_xmr_locked)",
|
||||
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command)",
|
||||
"docker_tests (alice_and_bob_refund_using_cancel_and_refund_command_timelock_not_expired)",
|
||||
"docker_tests (punish)",
|
||||
"docker_tests (alice_punishes_after_restart_bob_dead)",
|
||||
"docker_tests (alice_manually_punishes_after_bob_dead)",
|
||||
"docker_tests (alice_refunds_after_restart_bob_refunded)",
|
||||
"docker_tests (ensure_same_swap_id)",
|
||||
"docker_tests (concurrent_bobs_before_xmr_lock_proof_sent)",
|
||||
"docker_tests (alice_manually_redeems_after_enc_sig_learned)"
|
||||
]
|
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
Before Width: | Height: | Size: 182 KiB |
Binary file not shown.
Before Width: | Height: | Size: 109 KiB |
@ -1,4 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.74" # also update this in the readme, changelog, and github actions
|
||||
channel = "1.53"
|
||||
components = ["clippy"]
|
||||
targets = ["armv7-unknown-linux-gnueabihf"]
|
||||
|
@ -1,9 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use vergen::EmitBuilder;
|
||||
use vergen::{vergen, Config, SemverKind};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
EmitBuilder::builder()
|
||||
.git_describe(true, true, None)
|
||||
.emit()?;
|
||||
Ok(())
|
||||
let mut config = Config::default();
|
||||
*config.git_mut().semver_kind_mut() = SemverKind::Lightweight;
|
||||
|
||||
vergen(config)
|
||||
}
|
||||
|
@ -1,15 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# run this script from the swap dir
|
||||
# make sure you have sqlx-cli installed: cargo install --version 0.6.3 sqlx-cli
|
||||
# it's advised for the sqlx-cli to be the same version as specified in cargo.toml
|
||||
|
||||
# this script creates a temporary sqlite database
|
||||
# then runs the migration scripts to create the tables (migrations folder)
|
||||
# then it prepares the offline sqlx-data.json rust mappings
|
||||
# crated temporary DB
|
||||
# run the migration scripts to create the tables
|
||||
# prepare the sqlx-data.json rust mappings
|
||||
DATABASE_URL=sqlite:tempdb cargo sqlx database create
|
||||
DATABASE_URL=sqlite:tempdb cargo sqlx migrate run
|
||||
# needs the absolute path here
|
||||
# https://github.com/launchbadge/sqlx/issues/1399
|
||||
DB_PATH=$(readlink -f tempdb)
|
||||
DATABASE_URL="sqlite:$DB_PATH" cargo sqlx prepare -- --bin swap
|
||||
DATABASE_URL=sqlite:./swap/tempdb cargo sqlx prepare -- --bin swap
|
@ -1,460 +0,0 @@
|
||||
pub mod request;
|
||||
use crate::cli::command::{Bitcoin, Monero, Tor};
|
||||
use crate::database::open_db;
|
||||
use crate::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet};
|
||||
use crate::fs::system_data_dir;
|
||||
use crate::network::rendezvous::XmrBtcNamespace;
|
||||
use crate::protocol::Database;
|
||||
use crate::seed::Seed;
|
||||
use crate::{bitcoin, cli, monero};
|
||||
use anyhow::{bail, Context as AnyContext, Error, Result};
|
||||
use futures::future::try_join_all;
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Once};
|
||||
use tokio::sync::{broadcast, broadcast::Sender, Mutex, RwLock};
|
||||
use tokio::task::JoinHandle;
|
||||
use url::Url;
|
||||
|
||||
static START: Once = Once::new();
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct Config {
|
||||
tor_socks5_port: u16,
|
||||
namespace: XmrBtcNamespace,
|
||||
server_address: Option<SocketAddr>,
|
||||
pub env_config: EnvConfig,
|
||||
seed: Option<Seed>,
|
||||
debug: bool,
|
||||
json: bool,
|
||||
data_dir: PathBuf,
|
||||
is_testnet: bool,
|
||||
}
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PendingTaskList(Mutex<Vec<JoinHandle<()>>>);
|
||||
|
||||
impl PendingTaskList {
|
||||
pub async fn spawn<F, T>(&self, future: F)
|
||||
where
|
||||
F: Future<Output = T> + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = future.await;
|
||||
});
|
||||
|
||||
self.0.lock().await.push(handle);
|
||||
}
|
||||
|
||||
pub async fn wait_for_tasks(&self) -> Result<()> {
|
||||
let tasks = {
|
||||
// Scope for the lock, to avoid holding it for the entire duration of the async block
|
||||
let mut guard = self.0.lock().await;
|
||||
guard.drain(..).collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
try_join_all(tasks).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SwapLock {
|
||||
current_swap: RwLock<Option<Uuid>>,
|
||||
suspension_trigger: Sender<()>,
|
||||
}
|
||||
|
||||
impl SwapLock {
|
||||
pub fn new() -> Self {
|
||||
let (suspension_trigger, _) = broadcast::channel(10);
|
||||
SwapLock {
|
||||
current_swap: RwLock::new(None),
|
||||
suspension_trigger,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn listen_for_swap_force_suspension(&self) -> Result<(), Error> {
|
||||
let mut listener = self.suspension_trigger.subscribe();
|
||||
let event = listener.recv().await;
|
||||
match event {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
tracing::error!("Error receiving swap suspension signal: {}", e);
|
||||
bail!(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn acquire_swap_lock(&self, swap_id: Uuid) -> Result<(), Error> {
|
||||
let mut current_swap = self.current_swap.write().await;
|
||||
if current_swap.is_some() {
|
||||
bail!("There already exists an active swap lock");
|
||||
}
|
||||
|
||||
tracing::debug!(swap_id = %swap_id, "Acquiring swap lock");
|
||||
*current_swap = Some(swap_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_current_swap_id(&self) -> Option<Uuid> {
|
||||
*self.current_swap.read().await
|
||||
}
|
||||
|
||||
/// Sends a signal to suspend all ongoing swap processes.
|
||||
///
|
||||
/// This function performs the following steps:
|
||||
/// 1. Triggers the suspension by sending a unit `()` signal to all listeners via `self.suspension_trigger`.
|
||||
/// 2. Polls the `current_swap` state every 50 milliseconds to check if it has been set to `None`, indicating that the swap processes have been suspended and the lock released.
|
||||
/// 3. If the lock is not released within 10 seconds, the function returns an error.
|
||||
///
|
||||
/// If we send a suspend signal while no swap is in progress, the function will not fail, but will return immediately.
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(())` if the swap lock is successfully released.
|
||||
/// - `Err(Error)` if the function times out waiting for the swap lock to be released.
|
||||
///
|
||||
/// # Notes
|
||||
/// The 50ms polling interval is considered negligible overhead compared to the typical time required to suspend ongoing swap processes.
|
||||
pub async fn send_suspend_signal(&self) -> Result<(), Error> {
|
||||
const TIMEOUT: u64 = 10_000;
|
||||
const INTERVAL: u64 = 50;
|
||||
|
||||
let _ = self.suspension_trigger.send(())?;
|
||||
|
||||
for _ in 0..(TIMEOUT / INTERVAL) {
|
||||
if self.get_current_swap_id().await.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(INTERVAL)).await;
|
||||
}
|
||||
|
||||
bail!("Timed out waiting for swap lock to be released");
|
||||
}
|
||||
|
||||
pub async fn release_swap_lock(&self) -> Result<Uuid, Error> {
|
||||
let mut current_swap = self.current_swap.write().await;
|
||||
if let Some(swap_id) = current_swap.as_ref() {
|
||||
tracing::debug!(swap_id = %swap_id, "Releasing swap lock");
|
||||
|
||||
let prev_swap_id = *swap_id;
|
||||
*current_swap = None;
|
||||
drop(current_swap);
|
||||
Ok(prev_swap_id)
|
||||
} else {
|
||||
bail!("There is no current swap lock to release");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SwapLock {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
// workaround for warning over monero_rpc_process which we must own but not read
|
||||
#[allow(dead_code)]
|
||||
pub struct Context {
|
||||
pub db: Arc<dyn Database + Send + Sync>,
|
||||
bitcoin_wallet: Option<Arc<bitcoin::Wallet>>,
|
||||
monero_wallet: Option<Arc<monero::Wallet>>,
|
||||
monero_rpc_process: Option<monero::WalletRpcProcess>,
|
||||
pub swap_lock: Arc<SwapLock>,
|
||||
pub config: Config,
|
||||
pub tasks: Arc<PendingTaskList>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl Context {
|
||||
pub async fn build(
|
||||
bitcoin: Option<Bitcoin>,
|
||||
monero: Option<Monero>,
|
||||
tor: Option<Tor>,
|
||||
data: Option<PathBuf>,
|
||||
is_testnet: bool,
|
||||
debug: bool,
|
||||
json: bool,
|
||||
server_address: Option<SocketAddr>,
|
||||
) -> Result<Context> {
|
||||
let data_dir = data::data_dir_from(data, is_testnet)?;
|
||||
let env_config = env_config_from(is_testnet);
|
||||
|
||||
START.call_once(|| {
|
||||
let _ = cli::tracing::init(debug, json, data_dir.join("logs"));
|
||||
});
|
||||
|
||||
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||
.context("Failed to read seed in file")?;
|
||||
|
||||
let bitcoin_wallet = {
|
||||
if let Some(bitcoin) = bitcoin {
|
||||
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
|
||||
bitcoin.apply_defaults(is_testnet)?;
|
||||
Some(Arc::new(
|
||||
init_bitcoin_wallet(
|
||||
bitcoin_electrum_rpc_url,
|
||||
&seed,
|
||||
data_dir.clone(),
|
||||
env_config,
|
||||
bitcoin_target_block,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let (monero_wallet, monero_rpc_process) = {
|
||||
if let Some(monero) = monero {
|
||||
let monero_daemon_address = monero.apply_defaults(is_testnet);
|
||||
let (wlt, prc) =
|
||||
init_monero_wallet(data_dir.clone(), monero_daemon_address, env_config).await?;
|
||||
(Some(Arc::new(wlt)), Some(prc))
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
};
|
||||
|
||||
let tor_socks5_port = tor.map_or(9050, |tor| tor.tor_socks5_port);
|
||||
|
||||
let context = Context {
|
||||
db: open_db(data_dir.join("sqlite")).await?,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
monero_rpc_process,
|
||||
config: Config {
|
||||
tor_socks5_port,
|
||||
namespace: XmrBtcNamespace::from_is_testnet(is_testnet),
|
||||
env_config,
|
||||
seed: Some(seed),
|
||||
server_address,
|
||||
debug,
|
||||
json,
|
||||
is_testnet,
|
||||
data_dir,
|
||||
},
|
||||
swap_lock: Arc::new(SwapLock::new()),
|
||||
tasks: Arc::new(PendingTaskList::default()),
|
||||
};
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
pub async fn for_harness(
|
||||
seed: Seed,
|
||||
env_config: EnvConfig,
|
||||
db_path: PathBuf,
|
||||
bob_bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bob_monero_wallet: Arc<monero::Wallet>,
|
||||
) -> Self {
|
||||
let config = Config::for_harness(seed, env_config);
|
||||
|
||||
Self {
|
||||
bitcoin_wallet: Some(bob_bitcoin_wallet),
|
||||
monero_wallet: Some(bob_monero_wallet),
|
||||
config,
|
||||
db: open_db(db_path)
|
||||
.await
|
||||
.expect("Could not open sqlite database"),
|
||||
monero_rpc_process: None,
|
||||
swap_lock: Arc::new(SwapLock::new()),
|
||||
tasks: Arc::new(PendingTaskList::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Context {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "")
|
||||
}
|
||||
}
|
||||
|
||||
async fn init_bitcoin_wallet(
|
||||
electrum_rpc_url: Url,
|
||||
seed: &Seed,
|
||||
data_dir: PathBuf,
|
||||
env_config: EnvConfig,
|
||||
bitcoin_target_block: usize,
|
||||
) -> Result<bitcoin::Wallet> {
|
||||
let wallet_dir = data_dir.join("wallet");
|
||||
|
||||
let wallet = bitcoin::Wallet::new(
|
||||
electrum_rpc_url.clone(),
|
||||
&wallet_dir,
|
||||
seed.derive_extended_private_key(env_config.bitcoin_network)?,
|
||||
env_config,
|
||||
bitcoin_target_block,
|
||||
)
|
||||
.await
|
||||
.context("Failed to initialize Bitcoin wallet")?;
|
||||
|
||||
wallet.sync().await?;
|
||||
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
async fn init_monero_wallet(
|
||||
data_dir: PathBuf,
|
||||
monero_daemon_address: String,
|
||||
env_config: EnvConfig,
|
||||
) -> Result<(monero::Wallet, monero::WalletRpcProcess)> {
|
||||
let network = env_config.monero_network;
|
||||
|
||||
const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-monitoring-wallet";
|
||||
|
||||
let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?;
|
||||
|
||||
let monero_wallet_rpc_process = monero_wallet_rpc
|
||||
.run(network, Some(monero_daemon_address))
|
||||
.await?;
|
||||
|
||||
let monero_wallet = monero::Wallet::open_or_create(
|
||||
monero_wallet_rpc_process.endpoint(),
|
||||
MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME.to_string(),
|
||||
env_config,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((monero_wallet, monero_wallet_rpc_process))
|
||||
}
|
||||
|
||||
mod data {
|
||||
use super::*;
|
||||
|
||||
pub fn data_dir_from(arg_dir: Option<PathBuf>, testnet: bool) -> Result<PathBuf> {
|
||||
let base_dir = match arg_dir {
|
||||
Some(custom_base_dir) => custom_base_dir,
|
||||
None => os_default()?,
|
||||
};
|
||||
|
||||
let sub_directory = if testnet { "testnet" } else { "mainnet" };
|
||||
|
||||
Ok(base_dir.join(sub_directory))
|
||||
}
|
||||
|
||||
fn os_default() -> Result<PathBuf> {
|
||||
Ok(system_data_dir()?.join("cli"))
|
||||
}
|
||||
}
|
||||
|
||||
fn env_config_from(testnet: bool) -> EnvConfig {
|
||||
if testnet {
|
||||
Testnet::get_config()
|
||||
} else {
|
||||
Mainnet::get_config()
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn for_harness(seed: Seed, env_config: EnvConfig) -> Self {
|
||||
let data_dir = data::data_dir_from(None, false).expect("Could not find data directory");
|
||||
|
||||
Self {
|
||||
tor_socks5_port: 9050,
|
||||
namespace: XmrBtcNamespace::from_is_testnet(false),
|
||||
server_address: None,
|
||||
env_config,
|
||||
seed: Some(seed),
|
||||
debug: false,
|
||||
json: false,
|
||||
is_testnet: false,
|
||||
data_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod api_test {
|
||||
use super::*;
|
||||
use crate::api::request::{Method, Request};
|
||||
|
||||
use libp2p::Multiaddr;
|
||||
use std::str::FromStr;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub const MULTI_ADDRESS: &str =
|
||||
"/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi";
|
||||
pub const MONERO_STAGENET_ADDRESS: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a";
|
||||
pub const BITCOIN_TESTNET_ADDRESS: &str = "tb1qr3em6k3gfnyl8r7q0v7t4tlnyxzgxma3lressv";
|
||||
pub const MONERO_MAINNET_ADDRESS: &str = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa";
|
||||
pub const BITCOIN_MAINNET_ADDRESS: &str = "bc1qe4epnfklcaa0mun26yz5g8k24em5u9f92hy325";
|
||||
pub const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b";
|
||||
|
||||
impl Config {
|
||||
pub fn default(
|
||||
is_testnet: bool,
|
||||
data_dir: Option<PathBuf>,
|
||||
debug: bool,
|
||||
json: bool,
|
||||
) -> Self {
|
||||
let data_dir = data::data_dir_from(data_dir, is_testnet).unwrap();
|
||||
|
||||
let seed = Seed::from_file_or_generate(data_dir.as_path()).unwrap();
|
||||
|
||||
let env_config = env_config_from(is_testnet);
|
||||
Self {
|
||||
tor_socks5_port: 9050,
|
||||
namespace: XmrBtcNamespace::from_is_testnet(is_testnet),
|
||||
server_address: None,
|
||||
env_config,
|
||||
seed: Some(seed),
|
||||
debug,
|
||||
json,
|
||||
is_testnet,
|
||||
data_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn buy_xmr(is_testnet: bool) -> Request {
|
||||
let seller = Multiaddr::from_str(MULTI_ADDRESS).unwrap();
|
||||
let bitcoin_change_address = {
|
||||
if is_testnet {
|
||||
bitcoin::Address::from_str(BITCOIN_TESTNET_ADDRESS).unwrap()
|
||||
} else {
|
||||
bitcoin::Address::from_str(BITCOIN_MAINNET_ADDRESS).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let monero_receive_address = {
|
||||
if is_testnet {
|
||||
monero::Address::from_str(MONERO_STAGENET_ADDRESS).unwrap()
|
||||
} else {
|
||||
monero::Address::from_str(MONERO_MAINNET_ADDRESS).unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
Request::new(Method::BuyXmr {
|
||||
seller,
|
||||
bitcoin_change_address,
|
||||
monero_receive_address,
|
||||
swap_id: Uuid::new_v4(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resume() -> Request {
|
||||
Request::new(Method::Resume {
|
||||
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel() -> Request {
|
||||
Request::new(Method::CancelAndRefund {
|
||||
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refund() -> Request {
|
||||
Request::new(Method::CancelAndRefund {
|
||||
swap_id: Uuid::from_str(SWAP_ID).unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -1,935 +0,0 @@
|
||||
use crate::api::Context;
|
||||
use crate::bitcoin::{Amount, ExpiredTimelocks, TxLock};
|
||||
use crate::cli::{list_sellers, EventLoop, SellerStatus};
|
||||
use crate::libp2p_ext::MultiAddrExt;
|
||||
use crate::network::quote::{BidQuote, ZeroQuoteReceived};
|
||||
use crate::network::swarm;
|
||||
use crate::protocol::bob::{BobState, Swap};
|
||||
use crate::protocol::{bob, State};
|
||||
use crate::{bitcoin, cli, monero, rpc};
|
||||
use anyhow::{bail, Context as AnyContext, Result};
|
||||
use libp2p::core::Multiaddr;
|
||||
use qrcode::render::unicode;
|
||||
use qrcode::QrCode;
|
||||
use serde_json::json;
|
||||
use std::cmp::min;
|
||||
use std::convert::TryInto;
|
||||
use std::future::Future;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug_span, field, Instrument, Span};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub struct Request {
|
||||
pub cmd: Method,
|
||||
pub log_reference: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Method {
|
||||
BuyXmr {
|
||||
seller: Multiaddr,
|
||||
bitcoin_change_address: bitcoin::Address,
|
||||
monero_receive_address: monero::Address,
|
||||
swap_id: Uuid,
|
||||
},
|
||||
Resume {
|
||||
swap_id: Uuid,
|
||||
},
|
||||
CancelAndRefund {
|
||||
swap_id: Uuid,
|
||||
},
|
||||
MoneroRecovery {
|
||||
swap_id: Uuid,
|
||||
},
|
||||
History,
|
||||
Config,
|
||||
WithdrawBtc {
|
||||
amount: Option<Amount>,
|
||||
address: bitcoin::Address,
|
||||
},
|
||||
Balance {
|
||||
force_refresh: bool,
|
||||
},
|
||||
ListSellers {
|
||||
rendezvous_point: Multiaddr,
|
||||
},
|
||||
ExportBitcoinWallet,
|
||||
SuspendCurrentSwap,
|
||||
StartDaemon {
|
||||
server_address: Option<SocketAddr>,
|
||||
},
|
||||
GetCurrentSwap,
|
||||
GetSwapInfo {
|
||||
swap_id: Uuid,
|
||||
},
|
||||
GetRawStates,
|
||||
}
|
||||
|
||||
impl Method {
|
||||
fn get_tracing_span(&self, log_reference_id: Option<String>) -> Span {
|
||||
let span = match self {
|
||||
Method::Balance { .. } => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "Balance",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::BuyXmr { swap_id, .. } => {
|
||||
debug_span!("method", method_name="BuyXmr", swap_id=%swap_id, log_reference_id=field::Empty)
|
||||
}
|
||||
Method::CancelAndRefund { swap_id } => {
|
||||
debug_span!("method", method_name="CancelAndRefund", swap_id=%swap_id, log_reference_id=field::Empty)
|
||||
}
|
||||
Method::Resume { swap_id } => {
|
||||
debug_span!("method", method_name="Resume", swap_id=%swap_id, log_reference_id=field::Empty)
|
||||
}
|
||||
Method::Config => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "Config",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::ExportBitcoinWallet => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "ExportBitcoinWallet",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::GetCurrentSwap => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "GetCurrentSwap",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::GetSwapInfo { .. } => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "GetSwapInfo",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::History => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "History",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::ListSellers { .. } => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "ListSellers",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::MoneroRecovery { .. } => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "MoneroRecovery",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::GetRawStates => debug_span!(
|
||||
"method",
|
||||
method_name = "RawHistory",
|
||||
log_reference_id = field::Empty
|
||||
),
|
||||
Method::StartDaemon { .. } => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "StartDaemon",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::SuspendCurrentSwap => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "SuspendCurrentSwap",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
Method::WithdrawBtc { .. } => {
|
||||
debug_span!(
|
||||
"method",
|
||||
method_name = "WithdrawBtc",
|
||||
log_reference_id = field::Empty
|
||||
)
|
||||
}
|
||||
};
|
||||
if let Some(log_reference_id) = log_reference_id {
|
||||
span.record("log_reference_id", log_reference_id.as_str());
|
||||
}
|
||||
span
|
||||
}
|
||||
}
|
||||
|
||||
impl Request {
|
||||
pub fn new(cmd: Method) -> Request {
|
||||
Request {
|
||||
cmd,
|
||||
log_reference: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_id(cmd: Method, id: Option<String>) -> Request {
|
||||
Request {
|
||||
cmd,
|
||||
log_reference: id,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_cmd(self, context: Arc<Context>) -> Result<serde_json::Value> {
|
||||
match self.cmd {
|
||||
Method::SuspendCurrentSwap => {
|
||||
let swap_id = context.swap_lock.get_current_swap_id().await;
|
||||
|
||||
if let Some(id_value) = swap_id {
|
||||
context.swap_lock.send_suspend_signal().await?;
|
||||
|
||||
Ok(json!({ "swapId": id_value }))
|
||||
} else {
|
||||
bail!("No swap is currently running")
|
||||
}
|
||||
}
|
||||
Method::GetSwapInfo { swap_id } => {
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
|
||||
let state = context.db.get_state(swap_id).await?;
|
||||
let is_completed = state.swap_finished();
|
||||
|
||||
let peerId = context
|
||||
.db
|
||||
.get_peer_id(swap_id)
|
||||
.await
|
||||
.with_context(|| "Could not get PeerID")?;
|
||||
|
||||
let addresses = context
|
||||
.db
|
||||
.get_addresses(peerId)
|
||||
.await
|
||||
.with_context(|| "Could not get addressess")?;
|
||||
|
||||
let start_date = context.db.get_swap_start_date(swap_id).await?;
|
||||
|
||||
let swap_state: BobState = state.try_into()?;
|
||||
let state_name = format!("{}", swap_state);
|
||||
|
||||
let (
|
||||
xmr_amount,
|
||||
btc_amount,
|
||||
tx_lock_id,
|
||||
tx_cancel_fee,
|
||||
tx_refund_fee,
|
||||
tx_lock_fee,
|
||||
btc_refund_address,
|
||||
cancel_timelock,
|
||||
punish_timelock,
|
||||
) = context
|
||||
.db
|
||||
.get_states(swap_id)
|
||||
.await?
|
||||
.iter()
|
||||
.find_map(|state| {
|
||||
if let State::Bob(BobState::SwapSetupCompleted(state2)) = state {
|
||||
let xmr_amount = state2.xmr;
|
||||
let btc_amount = state2.tx_lock.lock_amount().to_sat();
|
||||
let tx_cancel_fee = state2.tx_cancel_fee.to_sat();
|
||||
let tx_refund_fee = state2.tx_refund_fee.to_sat();
|
||||
let tx_lock_id = state2.tx_lock.txid();
|
||||
let btc_refund_address = state2.refund_address.to_string();
|
||||
|
||||
if let Ok(tx_lock_fee) = state2.tx_lock.fee() {
|
||||
let tx_lock_fee = tx_lock_fee.to_sat();
|
||||
|
||||
Some((
|
||||
xmr_amount,
|
||||
btc_amount,
|
||||
tx_lock_id,
|
||||
tx_cancel_fee,
|
||||
tx_refund_fee,
|
||||
tx_lock_fee,
|
||||
btc_refund_address,
|
||||
state2.cancel_timelock,
|
||||
state2.punish_timelock,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.with_context(|| "Did not find SwapSetupCompleted state for swap")?;
|
||||
|
||||
let timelock = match swap_state {
|
||||
BobState::Started { .. }
|
||||
| BobState::SafelyAborted
|
||||
| BobState::SwapSetupCompleted(_) => None,
|
||||
BobState::BtcLocked { state3: state, .. }
|
||||
| BobState::XmrLockProofReceived { state, .. } => {
|
||||
Some(state.expired_timelock(bitcoin_wallet).await)
|
||||
}
|
||||
BobState::XmrLocked(state) | BobState::EncSigSent(state) => {
|
||||
Some(state.expired_timelock(bitcoin_wallet).await)
|
||||
}
|
||||
BobState::CancelTimelockExpired(state) | BobState::BtcCancelled(state) => {
|
||||
Some(state.expired_timelock(bitcoin_wallet).await)
|
||||
}
|
||||
BobState::BtcPunished { .. } => Some(Ok(ExpiredTimelocks::Punish)),
|
||||
BobState::BtcRefunded(_)
|
||||
| BobState::BtcRedeemed(_)
|
||||
| BobState::XmrRedeemed { .. } => None,
|
||||
};
|
||||
|
||||
Ok(json!({
|
||||
"swapId": swap_id,
|
||||
"seller": {
|
||||
"peerId": peerId.to_string(),
|
||||
"addresses": addresses
|
||||
},
|
||||
"completed": is_completed,
|
||||
"startDate": start_date,
|
||||
"stateName": state_name,
|
||||
"xmrAmount": xmr_amount,
|
||||
"btcAmount": btc_amount,
|
||||
"txLockId": tx_lock_id,
|
||||
"txCancelFee": tx_cancel_fee,
|
||||
"txRefundFee": tx_refund_fee,
|
||||
"txLockFee": tx_lock_fee,
|
||||
"btcRefundAddress": btc_refund_address.to_string(),
|
||||
"cancelTimelock": cancel_timelock,
|
||||
"punishTimelock": punish_timelock,
|
||||
// If the timelock is None, it means that the swap is in a state where the timelock is not accessible to us.
|
||||
// If that is the case, we return null. Otherwise, we return the timelock.
|
||||
"timelock": timelock.map(|tl| tl.map(|tl| json!(tl)).unwrap_or(json!(null))).unwrap_or(json!(null)),
|
||||
}))
|
||||
}
|
||||
Method::BuyXmr {
|
||||
seller,
|
||||
bitcoin_change_address,
|
||||
monero_receive_address,
|
||||
swap_id,
|
||||
} => {
|
||||
let bitcoin_wallet = Arc::clone(
|
||||
context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.expect("Could not find Bitcoin wallet"),
|
||||
);
|
||||
let monero_wallet = Arc::clone(
|
||||
context
|
||||
.monero_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Monero wallet")?,
|
||||
);
|
||||
let env_config = context.config.env_config;
|
||||
let seed = context.config.seed.clone().context("Could not get seed")?;
|
||||
|
||||
let seller_peer_id = seller
|
||||
.extract_peer_id()
|
||||
.context("Seller address must contain peer ID")?;
|
||||
context
|
||||
.db
|
||||
.insert_address(seller_peer_id, seller.clone())
|
||||
.await?;
|
||||
|
||||
let behaviour = cli::Behaviour::new(
|
||||
seller_peer_id,
|
||||
env_config,
|
||||
bitcoin_wallet.clone(),
|
||||
(seed.derive_libp2p_identity(), context.config.namespace),
|
||||
);
|
||||
let mut swarm = swarm::cli(
|
||||
seed.derive_libp2p_identity(),
|
||||
context.config.tor_socks5_port,
|
||||
behaviour,
|
||||
)
|
||||
.await?;
|
||||
|
||||
swarm.behaviour_mut().add_address(seller_peer_id, seller);
|
||||
|
||||
context
|
||||
.db
|
||||
.insert_monero_address(swap_id, monero_receive_address)
|
||||
.await?;
|
||||
|
||||
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
|
||||
|
||||
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
||||
|
||||
let initialize_swap = tokio::select! {
|
||||
biased;
|
||||
_ = context.swap_lock.listen_for_swap_force_suspension() => {
|
||||
tracing::debug!("Shutdown signal received, exiting");
|
||||
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
||||
bail!("Shutdown signal received");
|
||||
},
|
||||
result = async {
|
||||
let (event_loop, mut event_loop_handle) =
|
||||
EventLoop::new(swap_id, swarm, seller_peer_id)?;
|
||||
let event_loop = tokio::spawn(event_loop.run().in_current_span());
|
||||
|
||||
let bid_quote = event_loop_handle.request_quote().await?;
|
||||
|
||||
Ok::<_, anyhow::Error>((event_loop, event_loop_handle, bid_quote))
|
||||
} => {
|
||||
result
|
||||
},
|
||||
};
|
||||
|
||||
let (event_loop, event_loop_handle, bid_quote) = match initialize_swap {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
tracing::error!(%swap_id, "Swap initialization failed: {:#}", error);
|
||||
context
|
||||
.swap_lock
|
||||
.release_swap_lock()
|
||||
.await
|
||||
.expect("Could not release swap lock");
|
||||
bail!(error);
|
||||
}
|
||||
};
|
||||
|
||||
context.tasks.clone().spawn(async move {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = context.swap_lock.listen_for_swap_force_suspension() => {
|
||||
tracing::debug!("Shutdown signal received, exiting");
|
||||
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
||||
bail!("Shutdown signal received");
|
||||
},
|
||||
event_loop_result = event_loop => {
|
||||
match event_loop_result {
|
||||
Ok(_) => {
|
||||
tracing::debug!(%swap_id, "EventLoop completed")
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(%swap_id, "EventLoop failed: {:#}", error)
|
||||
}
|
||||
}
|
||||
},
|
||||
swap_result = async {
|
||||
let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size());
|
||||
let estimate_fee = |amount| bitcoin_wallet.estimate_fee(TxLock::weight(), amount);
|
||||
|
||||
let determine_amount = determine_btc_to_swap(
|
||||
context.config.json,
|
||||
bid_quote,
|
||||
bitcoin_wallet.new_address(),
|
||||
|| bitcoin_wallet.balance(),
|
||||
max_givable,
|
||||
|| bitcoin_wallet.sync(),
|
||||
estimate_fee,
|
||||
);
|
||||
|
||||
let (amount, fees) = match determine_amount.await {
|
||||
Ok(val) => val,
|
||||
Err(error) => match error.downcast::<ZeroQuoteReceived>() {
|
||||
Ok(_) => {
|
||||
bail!("Seller's XMR balance is currently too low to initiate a swap, please try again later")
|
||||
}
|
||||
Err(other) => bail!(other),
|
||||
},
|
||||
};
|
||||
|
||||
tracing::info!(%amount, %fees, "Determined swap amount");
|
||||
|
||||
context.db.insert_peer_id(swap_id, seller_peer_id).await?;
|
||||
|
||||
let swap = Swap::new(
|
||||
Arc::clone(&context.db),
|
||||
swap_id,
|
||||
Arc::clone(&bitcoin_wallet),
|
||||
monero_wallet,
|
||||
env_config,
|
||||
event_loop_handle,
|
||||
monero_receive_address,
|
||||
bitcoin_change_address,
|
||||
amount,
|
||||
);
|
||||
|
||||
bob::run(swap).await
|
||||
} => {
|
||||
match swap_result {
|
||||
Ok(state) => {
|
||||
tracing::debug!(%swap_id, state=%state, "Swap completed")
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(%swap_id, "Failed to complete swap: {:#}", error)
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
tracing::debug!(%swap_id, "Swap completed");
|
||||
|
||||
context
|
||||
.swap_lock
|
||||
.release_swap_lock()
|
||||
.await
|
||||
.expect("Could not release swap lock");
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}.in_current_span()).await;
|
||||
|
||||
Ok(json!({
|
||||
"swapId": swap_id.to_string(),
|
||||
"quote": bid_quote,
|
||||
}))
|
||||
}
|
||||
Method::Resume { swap_id } => {
|
||||
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
||||
|
||||
let seller_peer_id = context.db.get_peer_id(swap_id).await?;
|
||||
let seller_addresses = context.db.get_addresses(seller_peer_id).await?;
|
||||
|
||||
let seed = context
|
||||
.config
|
||||
.seed
|
||||
.as_ref()
|
||||
.context("Could not get seed")?
|
||||
.derive_libp2p_identity();
|
||||
|
||||
let behaviour = cli::Behaviour::new(
|
||||
seller_peer_id,
|
||||
context.config.env_config,
|
||||
Arc::clone(
|
||||
context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?,
|
||||
),
|
||||
(seed.clone(), context.config.namespace),
|
||||
);
|
||||
let mut swarm =
|
||||
swarm::cli(seed.clone(), context.config.tor_socks5_port, behaviour).await?;
|
||||
let our_peer_id = swarm.local_peer_id();
|
||||
|
||||
tracing::debug!(peer_id = %our_peer_id, "Network layer initialized");
|
||||
|
||||
for seller_address in seller_addresses {
|
||||
swarm
|
||||
.behaviour_mut()
|
||||
.add_address(seller_peer_id, seller_address);
|
||||
}
|
||||
|
||||
let (event_loop, event_loop_handle) =
|
||||
EventLoop::new(swap_id, swarm, seller_peer_id)?;
|
||||
let monero_receive_address = context.db.get_monero_address(swap_id).await?;
|
||||
let swap = Swap::from_db(
|
||||
Arc::clone(&context.db),
|
||||
swap_id,
|
||||
Arc::clone(
|
||||
context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?,
|
||||
),
|
||||
Arc::clone(
|
||||
context
|
||||
.monero_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Monero wallet")?,
|
||||
),
|
||||
context.config.env_config,
|
||||
event_loop_handle,
|
||||
monero_receive_address,
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.tasks.clone().spawn(
|
||||
async move {
|
||||
let handle = tokio::spawn(event_loop.run().in_current_span());
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = context.swap_lock.listen_for_swap_force_suspension() => {
|
||||
tracing::debug!("Shutdown signal received, exiting");
|
||||
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
||||
bail!("Shutdown signal received");
|
||||
},
|
||||
|
||||
event_loop_result = handle => {
|
||||
match event_loop_result {
|
||||
Ok(_) => {
|
||||
tracing::debug!(%swap_id, "EventLoop completed during swap resume")
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(%swap_id, "EventLoop failed during swap resume: {:#}", error)
|
||||
}
|
||||
}
|
||||
},
|
||||
swap_result = bob::run(swap) => {
|
||||
match swap_result {
|
||||
Ok(state) => {
|
||||
tracing::debug!(%swap_id, state=%state, "Swap completed after resuming")
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(%swap_id, "Failed to resume swap: {:#}", error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
context
|
||||
.swap_lock
|
||||
.release_swap_lock()
|
||||
.await
|
||||
.expect("Could not release swap lock");
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}
|
||||
.in_current_span(),
|
||||
).await;
|
||||
Ok(json!({
|
||||
"result": "ok",
|
||||
}))
|
||||
}
|
||||
Method::CancelAndRefund { swap_id } => {
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
|
||||
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
||||
|
||||
let state = cli::cancel_and_refund(
|
||||
swap_id,
|
||||
Arc::clone(bitcoin_wallet),
|
||||
Arc::clone(&context.db),
|
||||
)
|
||||
.await;
|
||||
|
||||
context
|
||||
.swap_lock
|
||||
.release_swap_lock()
|
||||
.await
|
||||
.expect("Could not release swap lock");
|
||||
|
||||
state.map(|state| {
|
||||
json!({
|
||||
"result": state,
|
||||
})
|
||||
})
|
||||
}
|
||||
Method::History => {
|
||||
let swaps = context.db.all().await?;
|
||||
let mut vec: Vec<(Uuid, String)> = Vec::new();
|
||||
for (swap_id, state) in swaps {
|
||||
let state: BobState = state.try_into()?;
|
||||
vec.push((swap_id, state.to_string()));
|
||||
}
|
||||
|
||||
Ok(json!({ "swaps": vec }))
|
||||
}
|
||||
Method::GetRawStates => {
|
||||
let raw_history = context.db.raw_all().await?;
|
||||
|
||||
Ok(json!({ "raw_states": raw_history }))
|
||||
}
|
||||
Method::Config => {
|
||||
let data_dir_display = context.config.data_dir.display();
|
||||
tracing::info!(path=%data_dir_display, "Data directory");
|
||||
tracing::info!(path=%format!("{}/logs", data_dir_display), "Log files directory");
|
||||
tracing::info!(path=%format!("{}/sqlite", data_dir_display), "Sqlite file location");
|
||||
tracing::info!(path=%format!("{}/seed.pem", data_dir_display), "Seed file location");
|
||||
tracing::info!(path=%format!("{}/monero", data_dir_display), "Monero-wallet-rpc directory");
|
||||
tracing::info!(path=%format!("{}/wallet", data_dir_display), "Internal bitcoin wallet directory");
|
||||
|
||||
Ok(json!({
|
||||
"log_files": format!("{}/logs", data_dir_display),
|
||||
"sqlite": format!("{}/sqlite", data_dir_display),
|
||||
"seed": format!("{}/seed.pem", data_dir_display),
|
||||
"monero-wallet-rpc": format!("{}/monero", data_dir_display),
|
||||
"bitcoin_wallet": format!("{}/wallet", data_dir_display),
|
||||
}))
|
||||
}
|
||||
Method::WithdrawBtc { address, amount } => {
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
|
||||
let amount = match amount {
|
||||
Some(amount) => amount,
|
||||
None => {
|
||||
bitcoin_wallet
|
||||
.max_giveable(address.script_pubkey().len())
|
||||
.await?
|
||||
}
|
||||
};
|
||||
let psbt = bitcoin_wallet
|
||||
.send_to_address(address, amount, None)
|
||||
.await?;
|
||||
let signed_tx = bitcoin_wallet.sign_and_finalize(psbt).await?;
|
||||
|
||||
bitcoin_wallet
|
||||
.broadcast(signed_tx.clone(), "withdraw")
|
||||
.await?;
|
||||
|
||||
Ok(json!({
|
||||
"signed_tx": signed_tx,
|
||||
"amount": amount.to_sat(),
|
||||
"txid": signed_tx.txid(),
|
||||
}))
|
||||
}
|
||||
Method::StartDaemon { server_address } => {
|
||||
// Default to 127.0.0.1:1234
|
||||
let server_address = server_address.unwrap_or("127.0.0.1:1234".parse()?);
|
||||
|
||||
let (addr, server_handle) =
|
||||
rpc::run_server(server_address, Arc::clone(&context)).await?;
|
||||
|
||||
tracing::info!(%addr, "Started RPC server");
|
||||
|
||||
server_handle.stopped().await;
|
||||
|
||||
tracing::info!("Stopped RPC server");
|
||||
|
||||
Ok(json!({}))
|
||||
}
|
||||
Method::Balance { force_refresh } => {
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
|
||||
if force_refresh {
|
||||
bitcoin_wallet.sync().await?;
|
||||
}
|
||||
|
||||
let bitcoin_balance = bitcoin_wallet.balance().await?;
|
||||
|
||||
if force_refresh {
|
||||
tracing::info!(
|
||||
balance = %bitcoin_balance,
|
||||
"Checked Bitcoin balance",
|
||||
);
|
||||
} else {
|
||||
tracing::debug!(
|
||||
balance = %bitcoin_balance,
|
||||
"Current Bitcoin balance as of last sync",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"balance": bitcoin_balance.to_sat()
|
||||
}))
|
||||
}
|
||||
Method::ListSellers { rendezvous_point } => {
|
||||
let rendezvous_node_peer_id = rendezvous_point
|
||||
.extract_peer_id()
|
||||
.context("Rendezvous node address must contain peer ID")?;
|
||||
|
||||
let identity = context
|
||||
.config
|
||||
.seed
|
||||
.as_ref()
|
||||
.context("Cannot extract seed")?
|
||||
.derive_libp2p_identity();
|
||||
|
||||
let sellers = list_sellers(
|
||||
rendezvous_node_peer_id,
|
||||
rendezvous_point,
|
||||
context.config.namespace,
|
||||
context.config.tor_socks5_port,
|
||||
identity,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for seller in &sellers {
|
||||
match seller.status {
|
||||
SellerStatus::Online(quote) => {
|
||||
tracing::info!(
|
||||
price = %quote.price.to_string(),
|
||||
min_quantity = %quote.min_quantity.to_string(),
|
||||
max_quantity = %quote.max_quantity.to_string(),
|
||||
status = "Online",
|
||||
address = %seller.multiaddr.to_string(),
|
||||
"Fetched peer status"
|
||||
);
|
||||
}
|
||||
SellerStatus::Unreachable => {
|
||||
tracing::info!(
|
||||
status = "Unreachable",
|
||||
address = %seller.multiaddr.to_string(),
|
||||
"Fetched peer status"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(json!({ "sellers": sellers }))
|
||||
}
|
||||
Method::ExportBitcoinWallet => {
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
|
||||
let wallet_export = bitcoin_wallet.wallet_export("cli").await?;
|
||||
tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet");
|
||||
Ok(json!({
|
||||
"descriptor": wallet_export.to_string(),
|
||||
}))
|
||||
}
|
||||
Method::MoneroRecovery { swap_id } => {
|
||||
let swap_state: BobState = context.db.get_state(swap_id).await?.try_into()?;
|
||||
|
||||
if let BobState::BtcRedeemed(state5) = swap_state {
|
||||
let (spend_key, view_key) = state5.xmr_keys();
|
||||
let restore_height = state5.monero_wallet_restore_blockheight.height;
|
||||
|
||||
let address = monero::Address::standard(
|
||||
context.config.env_config.monero_network,
|
||||
monero::PublicKey::from_private_key(&spend_key),
|
||||
monero::PublicKey::from(view_key.public()),
|
||||
);
|
||||
|
||||
tracing::info!(restore_height=%restore_height, address=%address, spend_key=%spend_key, view_key=%view_key, "Monero recovery information");
|
||||
|
||||
Ok(json!({
|
||||
"address": address,
|
||||
"spend_key": spend_key.to_string(),
|
||||
"view_key": view_key.to_string(),
|
||||
"restore_height": state5.monero_wallet_restore_blockheight.height,
|
||||
}))
|
||||
} else {
|
||||
bail!(
|
||||
"Cannot print monero recovery information in state {}, only possible for BtcRedeemed",
|
||||
swap_state
|
||||
)
|
||||
}
|
||||
}
|
||||
Method::GetCurrentSwap => Ok(json!({
|
||||
"swap_id": context.swap_lock.get_current_swap_id().await
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call(self, context: Arc<Context>) -> Result<serde_json::Value> {
|
||||
let method_span = self.cmd.get_tracing_span(self.log_reference.clone());
|
||||
|
||||
self.handle_cmd(context)
|
||||
.instrument(method_span.clone())
|
||||
.await
|
||||
.map_err(|err| {
|
||||
method_span.in_scope(|| {
|
||||
tracing::debug!(err = format!("{:?}", err), "API call resulted in an error");
|
||||
});
|
||||
err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn qr_code(value: &impl ToString) -> Result<String> {
|
||||
let code = QrCode::new(value.to_string())?;
|
||||
let qr_code = code
|
||||
.render::<unicode::Dense1x2>()
|
||||
.dark_color(unicode::Dense1x2::Light)
|
||||
.light_color(unicode::Dense1x2::Dark)
|
||||
.build();
|
||||
Ok(qr_code)
|
||||
}
|
||||
|
||||
pub async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS, FFE, TFE>(
|
||||
json: bool,
|
||||
bid_quote: BidQuote,
|
||||
get_new_address: impl Future<Output = Result<bitcoin::Address>>,
|
||||
balance: FB,
|
||||
max_giveable_fn: FMG,
|
||||
sync: FS,
|
||||
estimate_fee: FFE,
|
||||
) -> Result<(Amount, Amount)>
|
||||
where
|
||||
TB: Future<Output = Result<Amount>>,
|
||||
FB: Fn() -> TB,
|
||||
TMG: Future<Output = Result<Amount>>,
|
||||
FMG: Fn() -> TMG,
|
||||
TS: Future<Output = Result<()>>,
|
||||
FS: Fn() -> TS,
|
||||
FFE: Fn(Amount) -> TFE,
|
||||
TFE: Future<Output = Result<Amount>>,
|
||||
{
|
||||
if bid_quote.max_quantity == Amount::ZERO {
|
||||
bail!(ZeroQuoteReceived)
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
price = %bid_quote.price,
|
||||
minimum_amount = %bid_quote.min_quantity,
|
||||
maximum_amount = %bid_quote.max_quantity,
|
||||
"Received quote",
|
||||
);
|
||||
|
||||
sync().await?;
|
||||
let mut max_giveable = max_giveable_fn().await?;
|
||||
|
||||
if max_giveable == Amount::ZERO || max_giveable < bid_quote.min_quantity {
|
||||
let deposit_address = get_new_address.await?;
|
||||
let minimum_amount = bid_quote.min_quantity;
|
||||
let maximum_amount = bid_quote.max_quantity;
|
||||
|
||||
if !json {
|
||||
eprintln!("{}", qr_code(&deposit_address)?);
|
||||
}
|
||||
|
||||
loop {
|
||||
let min_outstanding = bid_quote.min_quantity - max_giveable;
|
||||
let min_bitcoin_lock_tx_fee = estimate_fee(min_outstanding).await?;
|
||||
let min_deposit_until_swap_will_start = min_outstanding + min_bitcoin_lock_tx_fee;
|
||||
let max_deposit_until_maximum_amount_is_reached =
|
||||
maximum_amount - max_giveable + min_bitcoin_lock_tx_fee;
|
||||
|
||||
tracing::info!(
|
||||
"Deposit at least {} to cover the min quantity with fee!",
|
||||
min_deposit_until_swap_will_start
|
||||
);
|
||||
tracing::info!(
|
||||
%deposit_address,
|
||||
%min_deposit_until_swap_will_start,
|
||||
%max_deposit_until_maximum_amount_is_reached,
|
||||
%max_giveable,
|
||||
%minimum_amount,
|
||||
%maximum_amount,
|
||||
%min_bitcoin_lock_tx_fee,
|
||||
price = %bid_quote.price,
|
||||
"Waiting for Bitcoin deposit",
|
||||
);
|
||||
|
||||
max_giveable = loop {
|
||||
sync().await?;
|
||||
let new_max_givable = max_giveable_fn().await?;
|
||||
|
||||
if new_max_givable > max_giveable {
|
||||
break new_max_givable;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
};
|
||||
|
||||
let new_balance = balance().await?;
|
||||
tracing::info!(%new_balance, %max_giveable, "Received Bitcoin");
|
||||
|
||||
if max_giveable < bid_quote.min_quantity {
|
||||
tracing::info!("Deposited amount is not enough to cover `min_quantity` when accounting for network fees");
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let balance = balance().await?;
|
||||
let fees = balance - max_giveable;
|
||||
let max_accepted = bid_quote.max_quantity;
|
||||
let btc_swap_amount = min(max_giveable, max_accepted);
|
||||
|
||||
Ok((btc_swap_amount, fees))
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Txid, Wallet};
|
||||
use crate::protocol::bob::BobState;
|
||||
use crate::protocol::Database;
|
||||
use anyhow::{bail, Result};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn cancel(
|
||||
swap_id: Uuid,
|
||||
bitcoin_wallet: Arc<Wallet>,
|
||||
db: Arc<dyn Database>,
|
||||
) -> Result<(Txid, BobState)> {
|
||||
let state = db.get_state(swap_id).await?.try_into()?;
|
||||
|
||||
let state6 = match state {
|
||||
BobState::BtcLocked { state3, .. } => state3.cancel(),
|
||||
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
||||
BobState::XmrLocked(state4) => state4.cancel(),
|
||||
BobState::EncSigSent(state4) => state4.cancel(),
|
||||
BobState::CancelTimelockExpired(state6) => state6,
|
||||
BobState::BtcRefunded(state6) => state6,
|
||||
BobState::BtcCancelled(state6) => state6,
|
||||
|
||||
BobState::Started { .. }
|
||||
| BobState::SwapSetupCompleted(_)
|
||||
| BobState::BtcRedeemed(_)
|
||||
| BobState::XmrRedeemed { .. }
|
||||
| BobState::BtcPunished { .. }
|
||||
| BobState::SafelyAborted => bail!(
|
||||
"Cannot cancel swap {} because it is in state {} which is not refundable.",
|
||||
swap_id,
|
||||
state
|
||||
),
|
||||
};
|
||||
|
||||
tracing::info!(%swap_id, "Manually cancelling swap");
|
||||
|
||||
let txid = match state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await {
|
||||
Ok(txid) => txid,
|
||||
Err(err) => {
|
||||
if let Ok(code) = parse_rpc_error_code(&err) {
|
||||
if code == i64::from(RpcErrorCode::RpcVerifyAlreadyInChain) {
|
||||
tracing::info!("Cancel transaction has already been confirmed on chain")
|
||||
}
|
||||
}
|
||||
bail!(err);
|
||||
}
|
||||
};
|
||||
|
||||
let state = BobState::BtcCancelled(state6);
|
||||
db.insert_latest_state(swap_id, state.clone().into())
|
||||
.await?;
|
||||
|
||||
Ok((txid, state))
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
use crate::bitcoin::wallet::Subscription;
|
||||
use crate::bitcoin::{parse_rpc_error_code, RpcErrorCode, Wallet};
|
||||
use crate::protocol::bob::BobState;
|
||||
use crate::protocol::Database;
|
||||
use anyhow::{bail, Result};
|
||||
use bitcoin::Txid;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn cancel_and_refund(
|
||||
swap_id: Uuid,
|
||||
bitcoin_wallet: Arc<Wallet>,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
) -> Result<BobState> {
|
||||
if let Err(err) = cancel(swap_id, bitcoin_wallet.clone(), db.clone()).await {
|
||||
tracing::info!(%err, "Could not submit cancel transaction");
|
||||
};
|
||||
|
||||
let state = match refund(swap_id, bitcoin_wallet, db).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => bail!(e),
|
||||
};
|
||||
|
||||
tracing::info!("Refund transaction submitted");
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
pub async fn cancel(
|
||||
swap_id: Uuid,
|
||||
bitcoin_wallet: Arc<Wallet>,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
) -> Result<(Txid, Subscription, BobState)> {
|
||||
let state = db.get_state(swap_id).await?.try_into()?;
|
||||
|
||||
let state6 = match state {
|
||||
BobState::BtcLocked { state3, .. } => state3.cancel(),
|
||||
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
||||
BobState::XmrLocked(state4) => state4.cancel(),
|
||||
BobState::EncSigSent(state4) => state4.cancel(),
|
||||
BobState::CancelTimelockExpired(state6) => state6,
|
||||
BobState::BtcRefunded(state6) => state6,
|
||||
BobState::BtcCancelled(state6) => state6,
|
||||
|
||||
BobState::Started { .. }
|
||||
| BobState::SwapSetupCompleted(_)
|
||||
| BobState::BtcRedeemed(_)
|
||||
| BobState::XmrRedeemed { .. }
|
||||
| BobState::BtcPunished { .. }
|
||||
| BobState::SafelyAborted => bail!(
|
||||
"Cannot cancel swap {} because it is in state {} which is not refundable.",
|
||||
swap_id,
|
||||
state
|
||||
),
|
||||
};
|
||||
|
||||
tracing::info!(%swap_id, "Manually cancelling swap");
|
||||
|
||||
let (txid, subscription) = match state6.submit_tx_cancel(bitcoin_wallet.as_ref()).await {
|
||||
Ok(txid) => txid,
|
||||
Err(err) => {
|
||||
if let Ok(error_code) = parse_rpc_error_code(&err) {
|
||||
tracing::debug!(%error_code, "parse rpc error");
|
||||
if error_code == i64::from(RpcErrorCode::RpcVerifyAlreadyInChain) {
|
||||
tracing::info!("Cancel transaction has already been confirmed on chain");
|
||||
} else if error_code == i64::from(RpcErrorCode::RpcVerifyError) {
|
||||
tracing::info!("General error trying to submit cancel transaction");
|
||||
}
|
||||
}
|
||||
bail!(err);
|
||||
}
|
||||
};
|
||||
|
||||
let state = BobState::BtcCancelled(state6);
|
||||
db.insert_latest_state(swap_id, state.clone().into())
|
||||
.await?;
|
||||
|
||||
Ok((txid, subscription, state))
|
||||
}
|
||||
|
||||
pub async fn refund(
|
||||
swap_id: Uuid,
|
||||
bitcoin_wallet: Arc<Wallet>,
|
||||
db: Arc<dyn Database + Send + Sync>,
|
||||
) -> Result<BobState> {
|
||||
let state = db.get_state(swap_id).await?.try_into()?;
|
||||
|
||||
let state6 = match state {
|
||||
BobState::BtcLocked { state3, .. } => state3.cancel(),
|
||||
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
||||
BobState::XmrLocked(state4) => state4.cancel(),
|
||||
BobState::EncSigSent(state4) => state4.cancel(),
|
||||
BobState::CancelTimelockExpired(state6) => state6,
|
||||
BobState::BtcCancelled(state6) => state6,
|
||||
BobState::Started { .. }
|
||||
| BobState::SwapSetupCompleted(_)
|
||||
| BobState::BtcRedeemed(_)
|
||||
| BobState::BtcRefunded(_)
|
||||
| BobState::XmrRedeemed { .. }
|
||||
| BobState::BtcPunished { .. }
|
||||
| BobState::SafelyAborted => bail!(
|
||||
"Cannot refund swap {} because it is in state {} which is not refundable.",
|
||||
swap_id,
|
||||
state
|
||||
),
|
||||
};
|
||||
|
||||
tracing::info!(%swap_id, "Manually refunding swap");
|
||||
state6.publish_refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||
|
||||
let state = BobState::BtcRefunded(state6);
|
||||
db.insert_latest_state(swap_id, state.clone().into())
|
||||
.await?;
|
||||
|
||||
Ok(state)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
||||
use crate::bitcoin::Wallet;
|
||||
use crate::protocol::bob::BobState;
|
||||
use crate::protocol::Database;
|
||||
use anyhow::{bail, Result};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn refund(
|
||||
swap_id: Uuid,
|
||||
bitcoin_wallet: Arc<Wallet>,
|
||||
db: Arc<dyn Database>,
|
||||
) -> Result<BobState> {
|
||||
let state = db.get_state(swap_id).await?.try_into()?;
|
||||
|
||||
let state6 = match state {
|
||||
BobState::BtcLocked { state3, .. } => state3.cancel(),
|
||||
BobState::XmrLockProofReceived { state, .. } => state.cancel(),
|
||||
BobState::XmrLocked(state4) => state4.cancel(),
|
||||
BobState::EncSigSent(state4) => state4.cancel(),
|
||||
BobState::CancelTimelockExpired(state6) => state6,
|
||||
BobState::BtcCancelled(state6) => state6,
|
||||
BobState::Started { .. }
|
||||
| BobState::SwapSetupCompleted(_)
|
||||
| BobState::BtcRedeemed(_)
|
||||
| BobState::BtcRefunded(_)
|
||||
| BobState::XmrRedeemed { .. }
|
||||
| BobState::BtcPunished { .. }
|
||||
| BobState::SafelyAborted => bail!(
|
||||
"Cannot refund swap {} because it is in state {} which is not refundable.",
|
||||
swap_id,
|
||||
state
|
||||
),
|
||||
};
|
||||
|
||||
state6.publish_refund_btc(bitcoin_wallet.as_ref()).await?;
|
||||
|
||||
let state = BobState::BtcRefunded(state6);
|
||||
db.insert_latest_state(swap_id, state.clone().into())
|
||||
.await?;
|
||||
|
||||
Ok(state)
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
|
||||
const LATEST_RELEASE_URL: &str = "https://github.com/comit-network/xmr-btc-swap/releases/latest";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Version {
|
||||
Current,
|
||||
Available,
|
||||
}
|
||||
|
||||
/// Check the latest release from GitHub API.
|
||||
pub async fn check_latest_version(current_version: &str) -> anyhow::Result<Version> {
|
||||
let response = reqwest::get(LATEST_RELEASE_URL).await?;
|
||||
let e = "Failed to get latest release.";
|
||||
let download_url = response.url();
|
||||
let segments = download_url.path_segments().ok_or_else(|| anyhow!(e))?;
|
||||
let latest_version = segments.last().ok_or_else(|| anyhow!(e))?;
|
||||
|
||||
let result = if is_latest_version(current_version, latest_version) {
|
||||
Version::Current
|
||||
} else {
|
||||
tracing::warn!(%current_version, %latest_version, %download_url,
|
||||
"You are not on the latest version",
|
||||
);
|
||||
Version::Available
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// todo: naive implementation can be improved using semver
|
||||
fn is_latest_version(current: &str, latest: &str) -> bool {
|
||||
current == latest
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_compares_versions() {
|
||||
assert!(is_latest_version("0.10.2", "0.10.2"));
|
||||
assert!(!is_latest_version("0.10.2", "0.10.3"));
|
||||
assert!(!is_latest_version("0.10.2", "0.11.0"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "For local testing, makes http requests to github."]
|
||||
async fn it_compares_with_github() {
|
||||
let result = check_latest_version("0.11.0").await.unwrap();
|
||||
assert_eq!(result, Version::Available);
|
||||
|
||||
let result = check_latest_version("0.11.1").await.unwrap();
|
||||
assert_eq!(result, Version::Current);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
use crate::api::Context;
|
||||
use jsonrpsee::server::{RpcModule, ServerBuilder, ServerHandle};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod methods;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Could not parse key value from params")]
|
||||
ParseError,
|
||||
}
|
||||
|
||||
pub async fn run_server(
|
||||
server_address: SocketAddr,
|
||||
context: Arc<Context>,
|
||||
) -> anyhow::Result<(SocketAddr, ServerHandle)> {
|
||||
let server = ServerBuilder::default().build(server_address).await?;
|
||||
let mut modules = RpcModule::new(());
|
||||
{
|
||||
modules
|
||||
.merge(methods::register_modules(Arc::clone(&context))?)
|
||||
.expect("Could not register RPC modules")
|
||||
}
|
||||
|
||||
let addr = server.local_addr()?;
|
||||
let server_handle = server.start(modules)?;
|
||||
|
||||
Ok((addr, server_handle))
|
||||
}
|
@ -1,236 +0,0 @@
|
||||
use crate::api::request::{Method, Request};
|
||||
use crate::api::Context;
|
||||
use crate::bitcoin::bitcoin_address;
|
||||
use crate::monero::monero_address;
|
||||
use crate::{bitcoin, monero};
|
||||
use anyhow::Result;
|
||||
use jsonrpsee::server::RpcModule;
|
||||
use jsonrpsee::types::Params;
|
||||
use libp2p::core::Multiaddr;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn register_modules(context: Arc<Context>) -> Result<RpcModule<Arc<Context>>> {
|
||||
let mut module = RpcModule::new(context);
|
||||
|
||||
module.register_async_method("suspend_current_swap", |params, context| async move {
|
||||
execute_request(params, Method::SuspendCurrentSwap, &context).await
|
||||
})?;
|
||||
|
||||
module.register_async_method("get_swap_info", |params_raw, context| async move {
|
||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
||||
|
||||
let swap_id = params
|
||||
.get("swap_id")
|
||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()))?;
|
||||
|
||||
let swap_id = as_uuid(swap_id)
|
||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?;
|
||||
|
||||
execute_request(params_raw, Method::GetSwapInfo { swap_id }, &context).await
|
||||
})?;
|
||||
|
||||
module.register_async_method("get_bitcoin_balance", |params_raw, context| async move {
|
||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
||||
|
||||
let force_refresh = params
|
||||
.get("force_refresh")
|
||||
.ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("Does not contain force_refresh".to_string())
|
||||
})?
|
||||
.as_bool()
|
||||
.ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("force_refesh is not a boolean".to_string())
|
||||
})?;
|
||||
|
||||
execute_request(params_raw, Method::Balance { force_refresh }, &context).await
|
||||
})?;
|
||||
|
||||
module.register_async_method("get_history", |params, context| async move {
|
||||
execute_request(params, Method::History, &context).await
|
||||
})?;
|
||||
|
||||
module.register_async_method("get_raw_states", |params, context| async move {
|
||||
execute_request(params, Method::GetRawStates, &context).await
|
||||
})?;
|
||||
|
||||
module.register_async_method("resume_swap", |params_raw, context| async move {
|
||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
||||
|
||||
let swap_id = params
|
||||
.get("swap_id")
|
||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()))?;
|
||||
|
||||
let swap_id = as_uuid(swap_id)
|
||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?;
|
||||
|
||||
execute_request(params_raw, Method::Resume { swap_id }, &context).await
|
||||
})?;
|
||||
|
||||
module.register_async_method("cancel_refund_swap", |params_raw, context| async move {
|
||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
||||
|
||||
let swap_id = params
|
||||
.get("swap_id")
|
||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string()))?;
|
||||
|
||||
let swap_id = as_uuid(swap_id)
|
||||
.ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?;
|
||||
|
||||
execute_request(params_raw, Method::CancelAndRefund { swap_id }, &context).await
|
||||
})?;
|
||||
|
||||
module.register_async_method(
|
||||
"get_monero_recovery_info",
|
||||
|params_raw, context| async move {
|
||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
||||
|
||||
let swap_id = params.get("swap_id").ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("Does not contain swap_id".to_string())
|
||||
})?;
|
||||
|
||||
let swap_id = as_uuid(swap_id).ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string())
|
||||
})?;
|
||||
|
||||
execute_request(params_raw, Method::MoneroRecovery { swap_id }, &context).await
|
||||
},
|
||||
)?;
|
||||
|
||||
module.register_async_method("withdraw_btc", |params_raw, context| async move {
|
||||
let params: HashMap<String, String> = params_raw.parse()?;
|
||||
|
||||
let amount = if let Some(amount_str) = params.get("amount") {
|
||||
Some(
|
||||
::bitcoin::Amount::from_str_in(amount_str, ::bitcoin::Denomination::Bitcoin)
|
||||
.map_err(|_| {
|
||||
jsonrpsee_core::Error::Custom("Unable to parse amount".to_string())
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let withdraw_address =
|
||||
bitcoin::Address::from_str(params.get("address").ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("Does not contain address".to_string())
|
||||
})?)
|
||||
.map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?;
|
||||
let withdraw_address =
|
||||
bitcoin_address::validate(withdraw_address, context.config.env_config.bitcoin_network)?;
|
||||
|
||||
execute_request(
|
||||
params_raw,
|
||||
Method::WithdrawBtc {
|
||||
amount,
|
||||
address: withdraw_address,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await
|
||||
})?;
|
||||
|
||||
module.register_async_method("buy_xmr", |params_raw, context| async move {
|
||||
let params: HashMap<String, String> = params_raw.parse()?;
|
||||
|
||||
let bitcoin_change_address =
|
||||
bitcoin::Address::from_str(params.get("bitcoin_change_address").ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("Does not contain bitcoin_change_address".to_string())
|
||||
})?)
|
||||
.map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?;
|
||||
|
||||
let bitcoin_change_address = bitcoin_address::validate(
|
||||
bitcoin_change_address,
|
||||
context.config.env_config.bitcoin_network,
|
||||
)?;
|
||||
|
||||
let monero_receive_address =
|
||||
monero::Address::from_str(params.get("monero_receive_address").ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("Does not contain monero_receiveaddress".to_string())
|
||||
})?)
|
||||
.map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?;
|
||||
|
||||
let monero_receive_address = monero_address::validate(
|
||||
monero_receive_address,
|
||||
context.config.env_config.monero_network,
|
||||
)?;
|
||||
|
||||
let seller =
|
||||
Multiaddr::from_str(params.get("seller").ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("Does not contain seller".to_string())
|
||||
})?)
|
||||
.map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?;
|
||||
|
||||
execute_request(
|
||||
params_raw,
|
||||
Method::BuyXmr {
|
||||
bitcoin_change_address,
|
||||
monero_receive_address,
|
||||
seller,
|
||||
swap_id: Uuid::new_v4(),
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await
|
||||
})?;
|
||||
|
||||
module.register_async_method("list_sellers", |params_raw, context| async move {
|
||||
let params: HashMap<String, serde_json::Value> = params_raw.parse()?;
|
||||
|
||||
let rendezvous_point = params.get("rendezvous_point").ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("Does not contain rendezvous_point".to_string())
|
||||
})?;
|
||||
|
||||
let rendezvous_point = rendezvous_point
|
||||
.as_str()
|
||||
.and_then(|addr_str| Multiaddr::from_str(addr_str).ok())
|
||||
.ok_or_else(|| {
|
||||
jsonrpsee_core::Error::Custom("Could not parse valid multiaddr".to_string())
|
||||
})?;
|
||||
|
||||
execute_request(
|
||||
params_raw,
|
||||
Method::ListSellers {
|
||||
rendezvous_point: rendezvous_point.clone(),
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await
|
||||
})?;
|
||||
|
||||
module.register_async_method("get_current_swap", |params, context| async move {
|
||||
execute_request(params, Method::GetCurrentSwap, &context).await
|
||||
})?;
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
|
||||
fn as_uuid(json_value: &serde_json::Value) -> Option<Uuid> {
|
||||
if let Some(uuid_str) = json_value.as_str() {
|
||||
Uuid::parse_str(uuid_str).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_request(
|
||||
params: Params<'static>,
|
||||
cmd: Method,
|
||||
context: &Arc<Context>,
|
||||
) -> Result<serde_json::Value, jsonrpsee_core::Error> {
|
||||
// If we fail to parse the params as a String HashMap, it's most likely because its an empty object
|
||||
// In that case, we want to make sure not to fail the request, so we set the log_reference_id to None
|
||||
// and swallow the error
|
||||
let reference_id = params
|
||||
.parse::<HashMap<String, serde_json::Value>>()
|
||||
.ok()
|
||||
.and_then(|params_parsed| params_parsed.get("log_reference_id").cloned());
|
||||
|
||||
let request = Request::with_id(cmd, reference_id.map(|log_ref| log_ref.to_string()));
|
||||
request
|
||||
.call(Arc::clone(context))
|
||||
.await
|
||||
.map_err(|err| jsonrpsee_core::Error::Custom(format!("{:#}", err)))
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
pub mod harness;
|
||||
|
||||
use harness::alice_run_until::is_xmr_lock_transaction_sent;
|
||||
use harness::bob_run_until::is_btc_locked;
|
||||
use harness::FastCancelConfig;
|
||||
use swap::asb::FixedRate;
|
||||
use swap::protocol::alice::AliceState;
|
||||
use swap::protocol::bob::BobState;
|
||||
use swap::protocol::{alice, bob};
|
||||
use swap::{asb, cli};
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_alice_and_bob_manually_cancel_and_refund_after_funds_locked_both_refund() {
|
||||
harness::setup_test(FastCancelConfig, |mut ctx| async move {
|
||||
let (bob_swap, bob_join_handle) = ctx.bob_swap().await;
|
||||
let bob_swap_id = bob_swap.id;
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_btc_locked));
|
||||
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
let alice_swap = tokio::spawn(alice::run_until(
|
||||
alice_swap,
|
||||
is_xmr_lock_transaction_sent,
|
||||
FixedRate::default(),
|
||||
));
|
||||
|
||||
let bob_state = bob_swap.await??;
|
||||
assert!(matches!(bob_state, BobState::BtcLocked { .. }));
|
||||
|
||||
let alice_state = alice_swap.await??;
|
||||
assert!(matches!(
|
||||
alice_state,
|
||||
AliceState::XmrLockTransactionSent { .. }
|
||||
));
|
||||
|
||||
let (bob_swap, bob_join_handle) = ctx
|
||||
.stop_and_resume_bob_from_db(bob_join_handle, bob_swap_id)
|
||||
.await;
|
||||
|
||||
// Ensure cancel timelock is expired
|
||||
if let BobState::BtcLocked { state3, .. } = bob_swap.state.clone() {
|
||||
bob_swap
|
||||
.bitcoin_wallet
|
||||
.subscribe_to(state3.tx_lock)
|
||||
.await
|
||||
.wait_until_confirmed_with(state3.cancel_timelock)
|
||||
.await?;
|
||||
} else {
|
||||
panic!("Bob in unexpected state {}", bob_swap.state);
|
||||
}
|
||||
|
||||
// Bob manually cancels and refunds
|
||||
bob_join_handle.abort();
|
||||
let bob_state =
|
||||
cli::cancel_and_refund(bob_swap.id, bob_swap.bitcoin_wallet, bob_swap.db).await?;
|
||||
|
||||
ctx.assert_bob_refunded(bob_state).await;
|
||||
|
||||
// manually refund Alice's swap
|
||||
ctx.restart_alice().await;
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
let alice_state = asb::refund(
|
||||
alice_swap.swap_id,
|
||||
alice_swap.bitcoin_wallet,
|
||||
alice_swap.monero_wallet,
|
||||
alice_swap.db,
|
||||
)
|
||||
.await?;
|
||||
|
||||
ctx.assert_alice_refunded(alice_state).await;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
VERSION=0.11.1
|
||||
|
||||
mkdir bdk
|
||||
stat ./target/debug/swap || exit 1
|
||||
cp ./target/debug/swap bdk/swap-current
|
||||
pushd bdk
|
||||
|
||||
echo "download swap $VERSION"
|
||||
curl -L "https://github.com/comit-network/xmr-btc-swap/releases/download/${VERSION}/swap_${VERSION}_Linux_x86_64.tar" | tar xv
|
||||
|
||||
echo "create testnet wallet with $VERSION"
|
||||
./swap --testnet --data-base-dir . --debug balance || exit 1
|
||||
echo "check testnet wallet with this version"
|
||||
./swap-current --testnet --data-base-dir . --debug balance || exit 1
|
||||
|
||||
echo "create mainnet wallet with $VERSION"
|
||||
./swap --version || exit 1
|
||||
./swap --data-base-dir . --debug balance || exit 1
|
||||
echo "check mainnet wallet with this version"
|
||||
./swap-current --version || exit 1
|
||||
./swap-current --data-base-dir . --debug balance || exit 1
|
||||
|
||||
exit 0
|
@ -1,44 +0,0 @@
|
||||
pub mod harness;
|
||||
|
||||
use crate::harness::bob_run_until::is_encsig_sent;
|
||||
use swap::asb::FixedRate;
|
||||
use swap::protocol::bob::BobState;
|
||||
use swap::protocol::{alice, bob};
|
||||
use tokio::join;
|
||||
|
||||
#[tokio::test]
|
||||
async fn given_bob_restarts_while_alice_redeems_btc() {
|
||||
harness::setup_test(harness::SlowCancelConfig, |mut ctx| async move {
|
||||
let (bob_swap, bob_handle) = ctx.bob_swap().await;
|
||||
let swap_id = bob_swap.id;
|
||||
|
||||
let bob_swap = tokio::spawn(bob::run_until(bob_swap, is_encsig_sent));
|
||||
|
||||
let alice_swap = ctx.alice_next_swap().await;
|
||||
let alice_swap = tokio::spawn(alice::run(alice_swap, FixedRate::default()));
|
||||
|
||||
let (bob_state, alice_state) = join!(bob_swap, alice_swap);
|
||||
ctx.assert_alice_redeemed(alice_state??).await;
|
||||
assert!(matches!(bob_state??, BobState::EncSigSent { .. }));
|
||||
|
||||
let (bob_swap, _) = ctx.stop_and_resume_bob_from_db(bob_handle, swap_id).await;
|
||||
|
||||
if let BobState::EncSigSent(state4) = bob_swap.state.clone() {
|
||||
bob_swap
|
||||
.bitcoin_wallet
|
||||
.subscribe_to(state4.tx_lock)
|
||||
.await
|
||||
.wait_until_confirmed_with(state4.cancel_timelock)
|
||||
.await?;
|
||||
} else {
|
||||
panic!("Bob in unexpected state {}", bob_swap.state);
|
||||
}
|
||||
|
||||
// Restart Bob
|
||||
let bob_state = bob::run(bob_swap).await?;
|
||||
ctx.assert_bob_redeemed(bob_state).await;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue