refactor(swap, tauri_bindings): Overhaul API architecture and introduce Tauri events

- Implement trait-based request handling in api/request.rs
- Add Tauri bindings and event system in api/tauri_bindings.rs
- Refactor CLI command parsing and execution in cli/command.rs
- Update RPC methods to use new request handling approach
- Emit Tauri events in swap/src/protocol/bob/swap.rs
- Add typescript type bindings use typeshare crate
This commit is contained in:
binarybaron 2024-08-26 15:18:11 +02:00
parent fea1e66c64
commit 4939d63524
No known key found for this signature in database
GPG Key ID: 99B75D3E1476A26E
14 changed files with 796 additions and 329 deletions

View File

@ -51,6 +51,7 @@ libp2p = { version = "0.42.2", default-features = false, features = [
"ping",
"rendezvous",
"identify",
"serde",
] }
monero = { version = "0.12", features = [ "serde_support" ] }
monero-rpc = { path = "../monero-rpc" }
@ -67,7 +68,7 @@ reqwest = { version = "0.12", features = [
], default-features = false }
rust_decimal = { version = "1", features = [ "serde-float" ] }
rust_decimal_macros = "1"
serde = { version = "1", features = [ "derive" ] }
serde = { version = "1.0", features = [ "derive" ] }
serde_cbor = "0.11"
serde_json = "1"
serde_with = { version = "1", features = [ "macros" ] }
@ -85,6 +86,7 @@ sqlx = { version = "0.6.3", features = [
] }
structopt = "0.3"
strum = { version = "0.26", features = [ "derive" ] }
tauri = { version = "2.0.0-rc.1", features = [ "config-json5" ] }
thiserror = "1"
time = "0.3"
tokio = { version = "1", features = [
@ -118,6 +120,7 @@ tracing-subscriber = { version = "0.3", default-features = false, features = [
"tracing-log",
"json",
] }
typeshare = "1.0.3"
url = { version = "2", features = [ "serde" ] }
uuid = { version = "1.9", features = [ "serde", "v4" ] }
void = "1"

View File

@ -1,4 +1,5 @@
pub mod request;
pub mod tauri_bindings;
use crate::cli::command::{Bitcoin, Monero, Tor};
use crate::database::open_db;
use crate::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet};
@ -13,10 +14,13 @@ use std::fmt;
use std::future::Future;
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, Once};
use std::sync::{Arc, Mutex as SyncMutex, Once};
use tauri::AppHandle;
use tauri_bindings::TauriHandle;
use tokio::sync::{broadcast, broadcast::Sender, Mutex as TokioMutex, RwLock};
use tokio::task::JoinHandle;
use url::Url;
use uuid::Uuid;
static START: Once = Once::new();
@ -33,8 +37,6 @@ pub struct Config {
is_testnet: bool,
}
use uuid::Uuid;
#[derive(Default)]
pub struct PendingTaskList(TokioMutex<Vec<JoinHandle<()>>>);
@ -64,6 +66,13 @@ impl PendingTaskList {
}
}
/// The `SwapLock` manages the state of the current swap, ensuring that only one swap can be active at a time.
/// It includes:
/// - A lock for the current swap (`current_swap`)
/// - A broadcast channel for suspension signals (`suspension_trigger`)
///
/// The `SwapLock` provides methods to acquire and release the swap lock, and to listen for suspension signals.
/// This ensures that swap operations do not overlap and can be safely suspended if needed.
pub struct SwapLock {
current_swap: RwLock<Option<Uuid>>,
suspension_trigger: Sender<()>,
@ -157,17 +166,22 @@ impl Default for SwapLock {
}
}
// workaround for warning over monero_rpc_process which we must own but not read
#[allow(dead_code)]
/// Holds shared data for different parts of the CLI.
///
/// Some components are optional, allowing initialization of only necessary parts.
/// For example, the `history` command doesn't require wallet initialization.
///
/// Many fields are wrapped in `Arc` for thread-safe sharing.
#[derive(Clone)]
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<Arc<Mutex<monero::WalletRpcProcess>>>,
pub swap_lock: Arc<SwapLock>,
pub config: Config,
pub tasks: Arc<PendingTaskList>,
tauri_handle: Option<TauriHandle>,
bitcoin_wallet: Option<Arc<bitcoin::Wallet>>,
monero_wallet: Option<Arc<monero::Wallet>>,
monero_rpc_process: Option<Arc<SyncMutex<monero::WalletRpcProcess>>>,
}
#[allow(clippy::too_many_arguments)]
@ -216,7 +230,7 @@ impl Context {
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))
(Some(Arc::new(wlt)), Some(Arc::new(SyncMutex::new(prc))))
} else {
(None, None)
}
@ -228,7 +242,7 @@ impl Context {
db: open_db(data_dir.join("sqlite")).await?,
bitcoin_wallet,
monero_wallet,
monero_rpc_process: monero_rpc_process.map(|prc| Arc::new(Mutex::new(prc))),
monero_rpc_process,
config: Config {
tor_socks5_port,
namespace: XmrBtcNamespace::from_is_testnet(is_testnet),
@ -242,11 +256,18 @@ impl Context {
},
swap_lock: Arc::new(SwapLock::new()),
tasks: Arc::new(PendingTaskList::default()),
tauri_handle: None,
};
Ok(context)
}
pub fn with_tauri_handle(mut self, tauri_handle: AppHandle) -> Self {
self.tauri_handle = Some(TauriHandle::new(tauri_handle));
self
}
pub async fn for_harness(
seed: Seed,
env_config: EnvConfig,
@ -266,6 +287,7 @@ impl Context {
monero_rpc_process: None,
swap_lock: Arc::new(SwapLock::new()),
tasks: Arc::new(PendingTaskList::default()),
tauri_handle: None,
}
}
@ -386,12 +408,6 @@ impl Config {
#[cfg(test)]
pub mod api_test {
use super::*;
use crate::api::request::{Method, Request};
use libp2p::Multiaddr;
use request::BuyXmrArgs;
use std::str::FromStr;
use uuid::Uuid;
pub const MULTI_ADDRESS: &str =
"/ip4/127.0.0.1/tcp/9939/p2p/12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi";
@ -426,50 +442,4 @@ pub mod api_test {
}
}
}
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(BuyXmrArgs {
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(),
})
}
}
}

View File

@ -1,5 +1,7 @@
use super::tauri_bindings::TauriHandle;
use crate::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent};
use crate::api::Context;
use crate::bitcoin::{Amount, ExpiredTimelocks, TxLock};
use crate::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock, TxLock};
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus};
use crate::libp2p_ext::MultiAddrExt;
use crate::network::quote::{BidQuote, ZeroQuoteReceived};
@ -10,11 +12,11 @@ use crate::{bitcoin, cli, monero, rpc};
use ::bitcoin::Txid;
use anyhow::{bail, Context as AnyContext, Result};
use libp2p::core::Multiaddr;
use libp2p::PeerId;
use qrcode::render::unicode;
use qrcode::QrCode;
use serde::{Deserialize, Serialize};
use serde_json::json;
use serde_json::Value as JsonValue;
use std::cmp::min;
use std::convert::TryInto;
use std::future::Future;
@ -22,144 +24,334 @@ use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tracing::Instrument;
use typeshare::typeshare;
use uuid::Uuid;
#[derive(PartialEq, Debug)]
pub struct Request {
pub cmd: Method,
pub log_reference: Option<String>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct BuyXmrArgs {
pub seller: Multiaddr,
pub bitcoin_change_address: bitcoin::Address,
pub monero_receive_address: monero::Address,
pub swap_id: Uuid,
}
#[derive(Debug, Eq, PartialEq)]
pub struct ResumeArgs {
pub swap_id: Uuid,
}
#[derive(Debug, Eq, PartialEq)]
pub struct CancelAndRefundArgs {
pub swap_id: Uuid,
}
#[derive(Debug, Eq, PartialEq)]
pub struct MoneroRecoveryArgs {
pub swap_id: Uuid,
/// This trait is implemented by all types of request args that
/// the CLI can handle.
/// It provides a unified abstraction that can be useful for generics.
#[allow(async_fn_in_trait)]
pub trait Request {
type Response: Serialize;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response>;
}
// BuyXmr
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct WithdrawBtcArgs {
pub amount: Option<u64>,
pub address: bitcoin::Address,
pub struct BuyXmrArgs {
#[typeshare(serialized_as = "string")]
pub seller: Multiaddr,
#[typeshare(serialized_as = "string")]
pub bitcoin_change_address: bitcoin::Address,
#[typeshare(serialized_as = "string")]
pub monero_receive_address: monero::Address,
}
#[derive(Debug, Eq, PartialEq)]
pub struct BalanceArgs {
pub force_refresh: bool,
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct BuyXmrResponse {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
pub quote: BidQuote,
}
#[derive(Debug, Eq, PartialEq)]
pub struct ListSellersArgs {
pub rendezvous_point: Multiaddr,
impl Request for BuyXmrArgs {
type Response = BuyXmrResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
buy_xmr(self, ctx).await
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct StartDaemonArgs {
pub server_address: Option<SocketAddr>,
}
#[derive(Debug, Eq, PartialEq)]
pub struct GetSwapInfoArgs {
// ResumeSwap
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ResumeSwapArgs {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct ResumeSwapResponse {
pub result: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BalanceResponse {
pub balance: u64, // in satoshis
impl Request for ResumeSwapArgs {
type Response = ResumeSwapResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
resume_swap(self, ctx).await
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BuyXmrResponse {
pub swap_id: String,
pub quote: BidQuote, // You'll need to import or define BidQuote
// CancelAndRefund
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CancelAndRefundArgs {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetHistoryResponse {
swaps: Vec<(Uuid, String)>,
impl Request for CancelAndRefundArgs {
type Response = serde_json::Value;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
cancel_and_refund(self, ctx).await
}
}
// MoneroRecovery
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct MoneroRecoveryArgs {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
}
impl Request for MoneroRecoveryArgs {
type Response = serde_json::Value;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
monero_recovery(self, ctx).await
}
}
// WithdrawBtc
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct WithdrawBtcArgs {
#[typeshare(serialized_as = "number")]
#[serde(default, with = "::bitcoin::util::amount::serde::as_sat::opt")]
pub amount: Option<bitcoin::Amount>,
#[typeshare(serialized_as = "string")]
pub address: bitcoin::Address,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct WithdrawBtcResponse {
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub amount: bitcoin::Amount,
pub txid: String,
}
impl Request for WithdrawBtcArgs {
type Response = WithdrawBtcResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
withdraw_btc(self, ctx).await
}
}
// ListSellers
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ListSellersArgs {
#[typeshare(serialized_as = "string")]
pub rendezvous_point: Multiaddr,
}
impl Request for ListSellersArgs {
type Response = serde_json::Value;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
list_sellers(self, ctx).await
}
}
// StartDaemon
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct StartDaemonArgs {
#[typeshare(serialized_as = "string")]
pub server_address: Option<SocketAddr>,
}
impl Request for StartDaemonArgs {
type Response = serde_json::Value;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
start_daemon(self, (*ctx).clone()).await
}
}
// GetSwapInfo
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct GetSwapInfoArgs {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
}
#[typeshare]
#[derive(Serialize)]
pub struct GetSwapInfoResponse {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
pub seller: Seller,
pub completed: bool,
pub start_date: String,
#[typeshare(serialized_as = "string")]
pub state_name: String,
pub xmr_amount: u64,
pub btc_amount: u64,
#[typeshare(serialized_as = "number")]
pub xmr_amount: monero::Amount,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub btc_amount: bitcoin::Amount,
#[typeshare(serialized_as = "string")]
pub tx_lock_id: Txid,
pub tx_cancel_fee: u64,
pub tx_refund_fee: u64,
pub tx_lock_fee: u64,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub tx_cancel_fee: bitcoin::Amount,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub tx_refund_fee: bitcoin::Amount,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub tx_lock_fee: bitcoin::Amount,
pub btc_refund_address: String,
pub cancel_timelock: u32,
pub punish_timelock: u32,
pub cancel_timelock: CancelTimelock,
pub punish_timelock: PunishTimelock,
pub timelock: Option<ExpiredTimelocks>,
}
impl Request for GetSwapInfoArgs {
type Response = GetSwapInfoResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
get_swap_info(self, ctx).await
}
}
// Balance
#[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct BalanceArgs {
pub force_refresh: bool,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct WithdrawBtcResponse {
amount: u64,
txid: String,
pub struct BalanceResponse {
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub balance: bitcoin::Amount,
}
#[derive(Serialize, Deserialize)]
impl Request for BalanceArgs {
type Response = BalanceResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
get_balance(self, ctx).await
}
}
// GetHistory
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct GetHistoryArgs;
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetHistoryEntry {
#[typeshare(serialized_as = "string")]
swap_id: Uuid,
state: String,
}
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct GetHistoryResponse {
pub swaps: Vec<GetHistoryEntry>,
}
impl Request for GetHistoryArgs {
type Response = GetHistoryResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
get_history(ctx).await
}
}
// Additional structs
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct Seller {
pub peer_id: String,
pub addresses: Vec<Multiaddr>,
#[typeshare(serialized_as = "string")]
pub peer_id: PeerId,
pub addresses: Vec<String>,
}
// TODO: We probably dont even need this.
// We can just call the method directly from the RPC server, the CLI and the Tauri connector
#[derive(Debug, PartialEq)]
pub enum Method {
BuyXmr(BuyXmrArgs),
Resume(ResumeArgs),
CancelAndRefund(CancelAndRefundArgs),
MoneroRecovery(MoneroRecoveryArgs),
History,
Config,
WithdrawBtc(WithdrawBtcArgs),
Balance(BalanceArgs),
ListSellers(ListSellersArgs),
ExportBitcoinWallet,
SuspendCurrentSwap,
StartDaemon(StartDaemonArgs),
GetCurrentSwap,
GetSwapInfo(GetSwapInfoArgs),
GetRawStates,
// Suspend current swap
#[derive(Debug, Deserialize)]
pub struct SuspendCurrentSwapArgs;
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct SuspendCurrentSwapResponse {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
}
impl Request for SuspendCurrentSwapArgs {
type Response = SuspendCurrentSwapResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
suspend_current_swap(ctx).await
}
}
pub struct GetCurrentSwap;
impl Request for GetCurrentSwap {
type Response = serde_json::Value;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
get_current_swap(ctx).await
}
}
pub struct GetConfig;
impl Request for GetConfig {
type Response = serde_json::Value;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
get_config(ctx).await
}
}
pub struct ExportBitcoinWalletArgs;
impl Request for ExportBitcoinWalletArgs {
type Response = serde_json::Value;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
export_bitcoin_wallet(ctx).await
}
}
pub struct GetConfigArgs;
impl Request for GetConfigArgs {
type Response = serde_json::Value;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
get_config(ctx).await
}
}
#[tracing::instrument(fields(method = "suspend_current_swap"), skip(context))]
pub async fn suspend_current_swap(context: Arc<Context>) -> Result<serde_json::Value> {
pub async fn suspend_current_swap(context: Arc<Context>) -> Result<SuspendCurrentSwapResponse> {
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 }))
Ok(SuspendCurrentSwapResponse { swap_id: id_value })
} else {
bail!("No swap is currently running")
}
@ -191,7 +383,7 @@ pub async fn get_swap_info(
let state = context.db.get_state(args.swap_id).await?;
let is_completed = state.swap_finished();
let peerId = context
let peer_id = context
.db
.get_peer_id(args.swap_id)
.await
@ -199,14 +391,13 @@ pub async fn get_swap_info(
let addresses = context
.db
.get_addresses(peerId)
.get_addresses(peer_id)
.await
.with_context(|| "Could not get addressess")?;
let start_date = context.db.get_swap_start_date(args.swap_id).await?;
let swap_state: BobState = state.try_into()?;
let state_name = format!("{}", swap_state);
let (
xmr_amount,
@ -226,15 +417,13 @@ pub async fn get_swap_info(
.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 btc_amount = state2.tx_lock.lock_amount();
let tx_cancel_fee = state2.tx_cancel_fee;
let tx_refund_fee = state2.tx_refund_fee;
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,
@ -255,7 +444,7 @@ pub async fn get_swap_info(
})
.with_context(|| "Did not find SwapSetupCompleted state for swap")?;
let timelock = match swap_state {
let timelock = match swap_state.clone() {
BobState::Started { .. } | BobState::SafelyAborted | BobState::SwapSetupCompleted(_) => {
None
}
@ -276,21 +465,21 @@ pub async fn get_swap_info(
Ok(GetSwapInfoResponse {
swap_id: args.swap_id,
seller: Seller {
peer_id: peerId.to_string(),
addresses,
peer_id,
addresses: addresses.iter().map(|a| a.to_string()).collect(),
},
completed: is_completed,
start_date,
state_name,
xmr_amount: xmr_amount.as_piconero(),
state_name: format!("{}", swap_state),
xmr_amount,
btc_amount,
tx_lock_id,
tx_cancel_fee,
tx_refund_fee,
tx_lock_fee,
btc_refund_address: btc_refund_address.to_string(),
cancel_timelock: cancel_timelock.into(),
punish_timelock: punish_timelock.into(),
cancel_timelock,
punish_timelock,
timelock,
})
}
@ -299,13 +488,15 @@ pub async fn get_swap_info(
pub async fn buy_xmr(
buy_xmr: BuyXmrArgs,
context: Arc<Context>,
) -> Result<serde_json::Value, anyhow::Error> {
) -> Result<BuyXmrResponse, anyhow::Error> {
let BuyXmrArgs {
seller,
bitcoin_change_address,
monero_receive_address,
swap_id,
} = buy_xmr;
let swap_id = Uuid::new_v4();
let bitcoin_wallet = Arc::clone(
context
.bitcoin_wallet
@ -358,6 +549,9 @@ pub async fn buy_xmr(
_ = 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.");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
bail!("Shutdown signal received");
},
result = async {
@ -382,16 +576,28 @@ pub async fn buy_xmr(
.release_swap_lock()
.await
.expect("Could not release swap lock");
context
.tauri_handle
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
bail!(error);
}
};
context
.tauri_handle
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(bid_quote));
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.");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
bail!("Shutdown signal received");
},
event_loop_result = event_loop => {
@ -416,6 +622,7 @@ pub async fn buy_xmr(
max_givable,
|| bitcoin_wallet.sync(),
estimate_fee,
context.tauri_handle.clone(),
);
let (amount, fees) = match determine_amount.await {
@ -442,7 +649,7 @@ pub async fn buy_xmr(
monero_receive_address,
bitcoin_change_address,
amount,
);
).with_event_emitter(context.tauri_handle.clone());
bob::run(swap).await
} => {
@ -463,18 +670,24 @@ pub async fn buy_xmr(
.release_swap_lock()
.await
.expect("Could not release swap lock");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
Ok::<_, anyhow::Error>(())
}.in_current_span()).await;
Ok(json!({
"swapId": swap_id.to_string(),
"quote": bid_quote,
}))
Ok(BuyXmrResponse {
swap_id,
quote: bid_quote,
})
}
#[tracing::instrument(fields(method = "resume_swap"), skip(context))]
pub async fn resume_swap(resume: ResumeArgs, context: Arc<Context>) -> Result<serde_json::Value> {
let ResumeArgs { swap_id } = resume;
pub async fn resume_swap(
resume: ResumeSwapArgs,
context: Arc<Context>,
) -> Result<ResumeSwapResponse> {
let ResumeSwapArgs { swap_id } = resume;
context.swap_lock.acquire_swap_lock(swap_id).await?;
let seller_peer_id = context.db.get_peer_id(swap_id).await?;
@ -531,7 +744,8 @@ pub async fn resume_swap(resume: ResumeArgs, context: Arc<Context>) -> Result<se
event_loop_handle,
monero_receive_address,
)
.await?;
.await?
.with_event_emitter(context.tauri_handle.clone());
context.tasks.clone().spawn(
async move {
@ -541,6 +755,9 @@ pub async fn resume_swap(resume: ResumeArgs, context: Arc<Context>) -> Result<se
_ = 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.");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
bail!("Shutdown signal received");
},
@ -571,13 +788,17 @@ pub async fn resume_swap(resume: ResumeArgs, context: Arc<Context>) -> Result<se
.release_swap_lock()
.await
.expect("Could not release swap lock");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
Ok::<(), anyhow::Error>(())
}
.in_current_span(),
).await;
Ok(json!({
"result": "ok",
}))
Ok(ResumeSwapResponse {
result: "OK".to_string(),
})
}
#[tracing::instrument(fields(method = "cancel_and_refund"), skip(context))]
@ -602,6 +823,10 @@ pub async fn cancel_and_refund(
.await
.expect("Could not release swap lock");
context
.tauri_handle
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
state.map(|state| {
json!({
"result": state,
@ -612,10 +837,13 @@ pub async fn cancel_and_refund(
#[tracing::instrument(fields(method = "get_history"), skip(context))]
pub async fn get_history(context: Arc<Context>) -> Result<GetHistoryResponse> {
let swaps = context.db.all().await?;
let mut vec: Vec<(Uuid, String)> = Vec::new();
let mut vec: Vec<GetHistoryEntry> = Vec::new();
for (swap_id, state) in swaps {
let state: BobState = state.try_into()?;
vec.push((swap_id, state.to_string()));
vec.push(GetHistoryEntry {
swap_id,
state: state.to_string(),
})
}
Ok(GetHistoryResponse { swaps: vec })
@ -659,7 +887,7 @@ pub async fn withdraw_btc(
.context("Could not get Bitcoin wallet")?;
let amount = match amount {
Some(amount) => Amount::from_sat(amount),
Some(amount) => amount,
None => {
bitcoin_wallet
.max_giveable(address.script_pubkey().len())
@ -677,7 +905,7 @@ pub async fn withdraw_btc(
Ok(WithdrawBtcResponse {
txid: signed_tx.txid().to_string(),
amount: amount.to_sat(),
amount,
})
}
@ -728,7 +956,7 @@ pub async fn get_balance(balance: BalanceArgs, context: Arc<Context>) -> Result<
}
Ok(BalanceResponse {
balance: bitcoin_balance.to_sat(),
balance: bitcoin_balance,
})
}
@ -838,26 +1066,6 @@ pub async fn get_current_swap(context: Arc<Context>) -> Result<serde_json::Value
}))
}
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,
}
}
pub async fn call(self, _: Arc<Context>) -> Result<JsonValue> {
unreachable!("This function should never be called")
}
}
fn qr_code(value: &impl ToString) -> Result<String> {
let code = QrCode::new(value.to_string())?;
let qr_code = code
@ -868,6 +1076,7 @@ fn qr_code(value: &impl ToString) -> Result<String> {
Ok(qr_code)
}
#[allow(clippy::too_many_arguments)]
pub async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS, FFE, TFE>(
json: bool,
bid_quote: BidQuote,
@ -876,18 +1085,19 @@ pub async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS, FFE, TFE>(
max_giveable_fn: FMG,
sync: FS,
estimate_fee: FFE,
) -> Result<(Amount, Amount)>
event_emitter: Option<TauriHandle>,
) -> Result<(bitcoin::Amount, bitcoin::Amount)>
where
TB: Future<Output = Result<Amount>>,
TB: Future<Output = Result<bitcoin::Amount>>,
FB: Fn() -> TB,
TMG: Future<Output = Result<Amount>>,
TMG: Future<Output = Result<bitcoin::Amount>>,
FMG: Fn() -> TMG,
TS: Future<Output = Result<()>>,
FS: Fn() -> TS,
FFE: Fn(Amount) -> TFE,
TFE: Future<Output = Result<Amount>>,
FFE: Fn(bitcoin::Amount) -> TFE,
TFE: Future<Output = Result<bitcoin::Amount>>,
{
if bid_quote.max_quantity == Amount::ZERO {
if bid_quote.max_quantity == bitcoin::Amount::ZERO {
bail!(ZeroQuoteReceived)
}
@ -901,7 +1111,7 @@ where
sync().await?;
let mut max_giveable = max_giveable_fn().await?;
if max_giveable == Amount::ZERO || max_giveable < bid_quote.min_quantity {
if max_giveable == bitcoin::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;
@ -933,6 +1143,19 @@ where
"Waiting for Bitcoin deposit",
);
// TODO: Use the real swap id here
event_emitter.emit_swap_progress_event(
Uuid::new_v4(),
TauriSwapProgressEvent::WaitingForBtcDeposit {
deposit_address: deposit_address.clone(),
max_giveable,
min_deposit_until_swap_will_start,
max_deposit_until_maximum_amount_is_reached,
min_bitcoin_lock_tx_fee,
quote: bid_quote.clone(),
},
);
max_giveable = loop {
sync().await?;
let new_max_givable = max_giveable_fn().await?;

View File

@ -0,0 +1,141 @@
/**
* TOOD: Perhaps we should move this to the `src-tauri` package.
*/
use anyhow::Result;
use bitcoin::Txid;
use serde::Serialize;
use std::sync::Arc;
use tauri::{AppHandle, Emitter};
use typeshare::typeshare;
use uuid::Uuid;
use crate::{monero, network::quote::BidQuote};
static SWAP_PROGRESS_EVENT_NAME: &str = "swap-progress-update";
#[derive(Debug, Clone)]
pub struct TauriHandle(Arc<AppHandle>);
impl TauriHandle {
pub fn new(tauri_handle: AppHandle) -> Self {
Self(Arc::new(tauri_handle))
}
pub fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
self.0.emit(event, payload).map_err(|e| e.into())
}
}
pub trait TauriEmitter {
fn emit_tauri_event<S: Serialize + Clone>(
&self,
event: &str,
payload: S,
) -> Result<()>;
fn emit_swap_progress_event(&self, swap_id: Uuid, event: TauriSwapProgressEvent) {
let _ = self.emit_tauri_event(
SWAP_PROGRESS_EVENT_NAME,
TauriSwapProgressEventWrapper { swap_id, event },
);
}
}
impl TauriEmitter for TauriHandle {
fn emit_tauri_event<S: Serialize + Clone>(
&self,
event: &str,
payload: S,
) -> Result<()> {
self.emit_tauri_event(event, payload)
}
}
impl TauriEmitter for Option<TauriHandle> {
fn emit_tauri_event<S: Serialize + Clone>(
&self,
event: &str,
payload: S,
) -> Result<()> {
match self {
Some(tauri) => tauri.emit_tauri_event(event, payload),
None => Ok(()),
}
}
}
#[derive(Serialize, Clone)]
#[typeshare]
pub struct TauriSwapProgressEventWrapper {
#[typeshare(serialized_as = "string")]
swap_id: Uuid,
event: TauriSwapProgressEvent,
}
#[derive(Serialize, Clone)]
#[serde(tag = "type", content = "content")]
#[typeshare]
pub enum TauriSwapProgressEvent {
Initiated,
ReceivedQuote(BidQuote),
WaitingForBtcDeposit {
#[typeshare(serialized_as = "string")]
deposit_address: bitcoin::Address,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
max_giveable: bitcoin::Amount,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
min_deposit_until_swap_will_start: bitcoin::Amount,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
max_deposit_until_maximum_amount_is_reached: bitcoin::Amount,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
min_bitcoin_lock_tx_fee: bitcoin::Amount,
quote: BidQuote,
},
Started {
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
btc_lock_amount: bitcoin::Amount,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
btc_tx_lock_fee: bitcoin::Amount,
},
BtcLockTxInMempool {
#[typeshare(serialized_as = "string")]
btc_lock_txid: bitcoin::Txid,
#[typeshare(serialized_as = "number")]
btc_lock_confirmations: u64,
},
XmrLockTxInMempool {
#[typeshare(serialized_as = "string")]
xmr_lock_txid: monero::TxHash,
#[typeshare(serialized_as = "number")]
xmr_lock_tx_confirmations: u64,
},
XmrLocked,
BtcRedeemed,
XmrRedeemInMempool {
#[typeshare(serialized_as = "string")]
xmr_redeem_txid: monero::TxHash,
#[typeshare(serialized_as = "string")]
xmr_redeem_address: monero::Address,
},
BtcCancelled {
#[typeshare(serialized_as = "string")]
btc_cancel_txid: Txid,
},
BtcRefunded {
#[typeshare(serialized_as = "string")]
btc_refund_txid: Txid,
},
BtcPunished,
AttemptingCooperativeRedeem,
CooperativeRedeemAccepted,
CooperativeRedeemRejected {
reason: String,
},
Released,
}

View File

@ -11,10 +11,10 @@
#![forbid(unsafe_code)]
#![allow(non_snake_case)]
use swap::cli::command::{parse_args_and_apply_defaults, ParseResult};
use swap::common::check_latest_version;
use anyhow::Result;
use std::env;
use swap::cli::command::{parse_args_and_apply_defaults, ParseResult};
use swap::common::check_latest_version;
#[tokio::main]
pub async fn main() -> Result<()> {
@ -23,7 +23,9 @@ pub async fn main() -> Result<()> {
}
match parse_args_and_apply_defaults(env::args_os()).await? {
ParseResult::Success => {}
ParseResult::Success(context) => {
context.tasks.wait_for_tasks().await?;
}
ParseResult::PrintAndExitZero { message } => {
println!("{}", message);
std::process::exit(0);

View File

@ -16,6 +16,7 @@ use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
use std::ops::Add;
use typeshare::typeshare;
/// Represent a timelock, expressed in relative block height as defined in
/// [BIP68](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki).
@ -23,6 +24,7 @@ use std::ops::Add;
/// mined.
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(transparent)]
#[typeshare]
pub struct CancelTimelock(u32);
impl From<CancelTimelock> for u32 {
@ -69,6 +71,7 @@ impl fmt::Display for CancelTimelock {
/// mined.
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(transparent)]
#[typeshare]
pub struct PunishTimelock(u32);
impl From<PunishTimelock> for u32 {

View File

@ -3,6 +3,7 @@ use bdk::electrum_client::HeaderNotification;
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use std::ops::Add;
use typeshare::typeshare;
/// Represent a block height, or block number, expressed in absolute block
/// count. E.g. The transaction was included in block #655123, 655123 block
@ -37,7 +38,9 @@ impl Add<u32> for BlockHeight {
}
}
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[typeshare]
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(tag = "type", content = "content")]
pub enum ExpiredTimelocks {
None { blocks_left: u32 },
Cancel { blocks_left: u32 },

View File

@ -1,8 +1,8 @@
use crate::api::request::{
buy_xmr, cancel_and_refund, export_bitcoin_wallet, get_balance, get_config, get_history,
list_sellers, monero_recovery, resume_swap, start_daemon, withdraw_btc, BalanceArgs,
BuyXmrArgs, CancelAndRefundArgs, ListSellersArgs, MoneroRecoveryArgs, ResumeArgs,
StartDaemonArgs, WithdrawBtcArgs,
BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs, GetHistoryArgs,
ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, StartDaemonArgs, WithdrawBtcArgs,
};
use crate::api::Context;
use crate::bitcoin::{bitcoin_address, Amount};
@ -38,7 +38,7 @@ const DEFAULT_TOR_SOCKS5_PORT: &str = "9050";
#[derive(Debug)]
pub enum ParseResult {
/// The arguments we were invoked in.
Success,
Success(Arc<Context>),
/// A flag or command was given that does not need further processing other
/// than printing the provided message.
///
@ -65,7 +65,7 @@ where
let json = args.json;
let is_testnet = args.testnet;
let data = args.data;
let result = match args.cmd {
let result: Result<Arc<Context>> = match args.cmd {
CliCommand::BuyXmr {
seller: Seller { seller },
bitcoin,
@ -93,36 +93,33 @@ where
.await?,
);
buy_xmr(
BuyXmrArgs {
seller,
bitcoin_change_address,
monero_receive_address,
swap_id: Uuid::new_v4(),
},
context,
)
BuyXmrArgs {
seller,
bitcoin_change_address,
monero_receive_address,
}
.request(context.clone())
.await?;
Ok(()) as Result<(), anyhow::Error>
Ok(context)
}
CliCommand::History => {
let context = Arc::new(
Context::build(None, None, None, data, is_testnet, debug, json, None).await?,
);
get_history(context).await?;
GetHistoryArgs {}.request(context.clone()).await?;
Ok(())
Ok(context)
}
CliCommand::Config => {
let context = Arc::new(
Context::build(None, None, None, data, is_testnet, debug, json, None).await?,
);
get_config(context).await?;
GetConfigArgs {}.request(context.clone()).await?;
Ok(())
Ok(context)
}
CliCommand::Balance { bitcoin } => {
let context = Arc::new(
@ -139,15 +136,13 @@ where
.await?,
);
get_balance(
BalanceArgs {
force_refresh: true,
},
context,
)
BalanceArgs {
force_refresh: true,
}
.request(context.clone())
.await?;
Ok(())
Ok(context)
}
CliCommand::StartDaemon {
server_address,
@ -155,21 +150,25 @@ where
monero,
tor,
} => {
let context = Context::build(
Some(bitcoin),
Some(monero),
Some(tor),
data,
is_testnet,
debug,
json,
server_address,
)
.await?;
let context = Arc::new(
Context::build(
Some(bitcoin),
Some(monero),
Some(tor),
data,
is_testnet,
debug,
json,
server_address,
)
.await?,
);
start_daemon(StartDaemonArgs { server_address }, context).await?;
StartDaemonArgs { server_address }
.request(context.clone())
.await?;
Ok(())
Ok(context)
}
CliCommand::WithdrawBtc {
bitcoin,
@ -192,16 +191,11 @@ where
.await?,
);
withdraw_btc(
WithdrawBtcArgs {
amount: amount.map(Amount::to_sat),
address,
},
context,
)
.await?;
WithdrawBtcArgs { amount, address }
.request(context.clone())
.await?;
Ok(())
Ok(context)
}
CliCommand::Resume {
swap_id: SwapId { swap_id },
@ -223,9 +217,9 @@ where
.await?,
);
resume_swap(ResumeArgs { swap_id }, context).await?;
ResumeSwapArgs { swap_id }.request(context.clone()).await?;
Ok(())
Ok(context)
}
CliCommand::CancelAndRefund {
swap_id: SwapId { swap_id },
@ -246,9 +240,11 @@ where
.await?,
);
cancel_and_refund(CancelAndRefundArgs { swap_id }, context).await?;
CancelAndRefundArgs { swap_id }
.request(context.clone())
.await?;
Ok(())
Ok(context)
}
CliCommand::ListSellers {
rendezvous_point,
@ -258,9 +254,11 @@ where
Context::build(None, None, Some(tor), data, is_testnet, debug, json, None).await?,
);
list_sellers(ListSellersArgs { rendezvous_point }, context).await?;
ListSellersArgs { rendezvous_point }
.request(context.clone())
.await?;
Ok(())
Ok(context)
}
CliCommand::ExportBitcoinWallet { bitcoin } => {
let context = Arc::new(
@ -277,9 +275,9 @@ where
.await?,
);
export_bitcoin_wallet(context).await?;
ExportBitcoinWalletArgs {}.request(context.clone()).await?;
Ok(())
Ok(context)
}
CliCommand::MoneroRecovery {
swap_id: SwapId { swap_id },
@ -288,15 +286,15 @@ where
Context::build(None, None, None, data, is_testnet, debug, json, None).await?,
);
monero_recovery(MoneroRecoveryArgs { swap_id }, context).await?;
MoneroRecoveryArgs { swap_id }
.request(context.clone())
.await?;
Ok(())
Ok(context)
}
};
result?;
Ok(ParseResult::Success)
Ok(ParseResult::Success(result?))
}
#[derive(structopt::StructOpt, Debug)]
@ -1067,18 +1065,14 @@ mod tests {
let args = parse_args_and_apply_defaults(raw_ars).await.unwrap();
let (is_testnet, debug, json) = (true, true, false);
let (expected_config, expected_request) = (
Config::default(is_testnet, None, debug, json),
Request::resume(),
);
let expected_config = Config::default(is_testnet, None, debug, json);
let (actual_config, actual_request) = match args {
ParseResult::Context(context, request) => (context.config.clone(), request),
let actual_config = match args {
ParseResult::Context(context, request) => context.config.clone(),
_ => panic!("Couldn't parse result"),
};
assert_eq!(actual_config, expected_config);
assert_eq!(actual_request, Box::new(expected_request));
// given_buy_xmr_on_mainnet_with_json_then_json_set
let raw_ars = vec![

View File

@ -4,6 +4,7 @@ mod wallet_rpc;
pub use ::monero::network::Network;
pub use ::monero::{Address, PrivateKey, PublicKey};
pub use curve25519_dalek::scalar::Scalar;
use typeshare::typeshare;
pub use wallet::Wallet;
pub use wallet_rpc::{WalletRpc, WalletRpcProcess};
@ -86,6 +87,7 @@ impl From<PublicViewKey> for PublicKey {
pub struct PublicViewKey(PublicKey);
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)]
#[typeshare(serialized_as = "number")]
pub struct Amount(u64);
// Median tx fees on Monero as found here: https://www.monero.how/monero-transaction-fees, XMR 0.000_008 * 2 (to be on the safe side)

View File

@ -7,6 +7,7 @@ use libp2p::request_response::{
};
use libp2p::PeerId;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0";
pub type OutEvent = RequestResponseEvent<(), BidQuote>;
@ -25,15 +26,20 @@ impl ProtocolName for BidQuoteProtocol {
/// Represents a quote for buying XMR.
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[typeshare]
pub struct BidQuote {
/// The price at which the maker is willing to buy at.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
#[typeshare(serialized_as = "number")]
pub price: bitcoin::Amount,
/// The minimum quantity the maker is willing to buy.
/// #[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
#[typeshare(serialized_as = "number")]
pub min_quantity: bitcoin::Amount,
/// The maximum quantity the maker is willing to buy.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
#[typeshare(serialized_as = "number")]
pub max_quantity: bitcoin::Amount,
}

View File

@ -3,6 +3,7 @@ use std::sync::Arc;
use anyhow::Result;
use uuid::Uuid;
use crate::api::tauri_bindings::TauriHandle;
use crate::protocol::Database;
use crate::{bitcoin, cli, env, monero};
@ -22,6 +23,7 @@ pub struct Swap {
pub env_config: env::Config,
pub id: Uuid,
pub monero_receive_address: monero::Address,
pub event_emitter: Option<TauriHandle>,
}
impl Swap {
@ -49,6 +51,7 @@ impl Swap {
env_config,
id,
monero_receive_address,
event_emitter: None,
}
}
@ -73,6 +76,12 @@ impl Swap {
env_config,
id,
monero_receive_address,
event_emitter: None,
})
}
pub fn with_event_emitter(mut self, event_emitter: Option<TauriHandle>) -> Self {
self.event_emitter = event_emitter;
self
}
}

View File

@ -1,3 +1,4 @@
use crate::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent};
use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund};
use crate::cli::EventLoopHandle;
use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected};
@ -48,6 +49,7 @@ pub async fn run_until(
swap.bitcoin_wallet.as_ref(),
swap.monero_wallet.as_ref(),
swap.monero_receive_address,
swap.event_emitter.clone(),
)
.await?;
@ -73,6 +75,7 @@ async fn next_state(
bitcoin_wallet: &bitcoin::Wallet,
monero_wallet: &monero::Wallet,
monero_receive_address: monero::Address,
event_emitter: Option<TauriHandle>,
) -> Result<BobState> {
tracing::debug!(%state, "Advancing state");
@ -120,6 +123,16 @@ async fn next_state(
.sign_and_finalize(tx_lock.clone().into())
.await
.context("Failed to sign Bitcoin lock transaction")?;
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::Started {
btc_lock_amount: tx_lock.lock_amount(),
// TODO: Replace this with the actual fee
btc_tx_lock_fee: bitcoin::Amount::ZERO,
},
);
let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?;
BobState::BtcLocked {
@ -133,6 +146,15 @@ async fn next_state(
state3,
monero_wallet_restore_blockheight,
} => {
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::BtcLockTxInMempool {
btc_lock_txid: state3.tx_lock_id(),
// TODO: Replace this with the actual confirmations
btc_lock_confirmations: 0,
},
);
let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await;
if let ExpiredTimelocks::None { .. } = state3.expired_timelock(bitcoin_wallet).await? {
@ -188,6 +210,14 @@ async fn next_state(
lock_transfer_proof,
monero_wallet_restore_blockheight,
} => {
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::XmrLockTxInMempool {
xmr_lock_txid: lock_transfer_proof.tx_hash(),
xmr_lock_tx_confirmations: 0,
},
);
let tx_lock_status = bitcoin_wallet.subscribe_to(state.tx_lock.clone()).await;
if let ExpiredTimelocks::None { .. } = state.expired_timelock(bitcoin_wallet).await? {
@ -207,6 +237,7 @@ async fn next_state(
},
}
}
// TODO: Send Tauri event here everytime we receive a new confirmation
result = tx_lock_status.wait_until_confirmed_with(state.cancel_timelock) => {
result?;
BobState::CancelTimelockExpired(state.cancel(monero_wallet_restore_blockheight))
@ -217,6 +248,8 @@ async fn next_state(
}
}
BobState::XmrLocked(state) => {
event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::XmrLocked);
// In case we send the encrypted signature to Alice, but she doesn't give us a confirmation
// We need to check if she still published the Bitcoin redeem transaction
// Otherwise we risk staying stuck in "XmrLocked"
@ -272,10 +305,21 @@ async fn next_state(
}
}
BobState::BtcRedeemed(state) => {
event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::BtcRedeemed);
state
.redeem_xmr(monero_wallet, swap_id.to_string(), monero_receive_address)
.await?;
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::XmrRedeemInMempool {
// TODO: Replace this with the actual txid
xmr_redeem_txid: monero::TxHash("placeholder".to_string()),
xmr_redeem_address: monero_receive_address,
},
);
BobState::XmrRedeemed {
tx_lock_id: state.tx_lock_id(),
}
@ -288,6 +332,13 @@ async fn next_state(
BobState::BtcCancelled(state4)
}
BobState::BtcCancelled(state) => {
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::BtcCancelled {
btc_cancel_txid: state.construct_tx_cancel()?.txid(),
},
);
// Bob has cancelled the swap
match state.expired_timelock(bitcoin_wallet).await? {
ExpiredTimelocks::None { .. } => {
@ -308,8 +359,24 @@ async fn next_state(
}
}
}
BobState::BtcRefunded(state4) => BobState::BtcRefunded(state4),
BobState::BtcRefunded(state4) => {
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::BtcRefunded {
btc_refund_txid: state4.signed_refund_transaction()?.txid(),
},
);
BobState::BtcRefunded(state4)
}
BobState::BtcPunished { state, tx_lock_id } => {
event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::BtcPunished);
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::AttemptingCooperativeRedeem,
);
tracing::info!("Attempting to cooperatively redeem XMR after being punished");
let response = event_loop_handle
.request_cooperative_xmr_redeem(swap_id)
@ -321,7 +388,13 @@ async fn next_state(
"Alice has accepted our request to cooperatively redeem the XMR"
);
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::CooperativeRedeemAccepted,
);
let s_a = monero::PrivateKey { scalar: s_a };
let state5 = state.attempt_cooperative_redeem(s_a);
match state5
@ -329,33 +402,78 @@ async fn next_state(
.await
{
Ok(_) => {
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::XmrRedeemInMempool {
xmr_redeem_txid: monero::TxHash("placeholder".to_string()),
xmr_redeem_address: monero_receive_address,
},
);
return Ok(BobState::XmrRedeemed { tx_lock_id });
}
Err(error) => {
return Err(error)
.context("Failed to redeem XMR with revealed XMR key");
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::CooperativeRedeemRejected {
reason: error.to_string(),
},
);
let err: std::result::Result<_, anyhow::Error> =
Err(error).context("Failed to redeem XMR with revealed XMR key");
return err;
}
}
}
Ok(Rejected { reason, .. }) => {
let err = Err(reason.clone())
.context("Alice rejected our request for cooperative XMR redeem");
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::CooperativeRedeemRejected {
reason: reason.to_string(),
},
);
tracing::error!(
?reason,
"Alice rejected our request for cooperative XMR redeem"
);
return Err(reason)
.context("Alice rejected our request for cooperative XMR redeem");
return err;
}
Err(error) => {
tracing::error!(
?error,
"Failed to request cooperative XMR redeem from Alice"
);
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::CooperativeRedeemRejected {
reason: error.to_string(),
},
);
return Err(error)
.context("Failed to request cooperative XMR redeem from Alice");
}
};
}
BobState::SafelyAborted => BobState::SafelyAborted,
BobState::XmrRedeemed { tx_lock_id } => BobState::XmrRedeemed { tx_lock_id },
BobState::XmrRedeemed { tx_lock_id } => {
// TODO: Replace this with the actual txid
event_emitter.emit_swap_progress_event(
swap_id,
TauriSwapProgressEvent::XmrRedeemInMempool {
xmr_redeem_txid: monero::TxHash("placeholder".to_string()),
xmr_redeem_address: monero_receive_address,
},
);
BobState::XmrRedeemed { tx_lock_id }
}
})
}

View File

@ -1,8 +1,8 @@
use crate::api::request::{
buy_xmr, cancel_and_refund, get_balance, get_current_swap, get_history, get_raw_states,
get_swap_info, list_sellers, monero_recovery, resume_swap, suspend_current_swap, withdraw_btc,
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs, Method,
MoneroRecoveryArgs, ResumeArgs, WithdrawBtcArgs,
get_current_swap, get_history, get_raw_states,
suspend_current_swap,
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, GetSwapInfoArgs, ListSellersArgs,
MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs,
};
use crate::api::Context;
use crate::bitcoin::bitcoin_address;
@ -42,7 +42,8 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
let swap_id = as_uuid(swap_id)
.ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?;
get_swap_info(GetSwapInfoArgs { swap_id }, context)
GetSwapInfoArgs { swap_id }
.request(context)
.await
.to_jsonrpsee_result()
})?;
@ -60,7 +61,8 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
jsonrpsee_core::Error::Custom("force_refesh is not a boolean".to_string())
})?;
get_balance(BalanceArgs { force_refresh }, context)
BalanceArgs { force_refresh }
.request(context)
.await
.to_jsonrpsee_result()
})?;
@ -83,7 +85,8 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
let swap_id = as_uuid(swap_id)
.ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?;
resume_swap(ResumeArgs { swap_id }, context)
ResumeSwapArgs { swap_id }
.request(context)
.await
.to_jsonrpsee_result()
})?;
@ -98,7 +101,8 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
let swap_id = as_uuid(swap_id)
.ok_or_else(|| jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string()))?;
cancel_and_refund(CancelAndRefundArgs { swap_id }, context)
CancelAndRefundArgs { swap_id }
.request(context)
.await
.to_jsonrpsee_result()
})?;
@ -116,7 +120,8 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
jsonrpsee_core::Error::Custom("Could not parse swap_id".to_string())
})?;
monero_recovery(MoneroRecoveryArgs { swap_id }, context)
MoneroRecoveryArgs { swap_id }
.request(context)
.await
.to_jsonrpsee_result()
},
@ -130,8 +135,7 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
::bitcoin::Amount::from_str_in(amount_str, ::bitcoin::Denomination::Bitcoin)
.map_err(|_| {
jsonrpsee_core::Error::Custom("Unable to parse amount".to_string())
})?
.to_sat(),
})?,
)
} else {
None
@ -145,13 +149,11 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
let withdraw_address =
bitcoin_address::validate(withdraw_address, context.config.env_config.bitcoin_network)?;
withdraw_btc(
WithdrawBtcArgs {
amount,
address: withdraw_address,
},
context,
)
WithdrawBtcArgs {
amount,
address: withdraw_address,
}
.request(context)
.await
.to_jsonrpsee_result()
})?;
@ -187,15 +189,12 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
})?)
.map_err(|err| jsonrpsee_core::Error::Custom(err.to_string()))?;
buy_xmr(
BuyXmrArgs {
seller,
bitcoin_change_address,
monero_receive_address,
swap_id: Uuid::new_v4(),
},
context,
)
BuyXmrArgs {
seller,
bitcoin_change_address,
monero_receive_address,
}
.request(context)
.await
.to_jsonrpsee_result()
})?;
@ -214,12 +213,10 @@ pub fn register_modules(outer_context: Context) -> Result<RpcModule<Context>> {
jsonrpsee_core::Error::Custom("Could not parse valid multiaddr".to_string())
})?;
list_sellers(
ListSellersArgs {
rendezvous_point: rendezvous_point.clone(),
},
context,
)
ListSellersArgs {
rendezvous_point: rendezvous_point.clone(),
}
.request(context)
.await
.to_jsonrpsee_result()
})?;

View File

@ -15,7 +15,7 @@ mod test {
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use swap::api::request::{Method, Request};
use swap::api::request::{start_daemon, StartDaemonArgs};
use swap::api::Context;
use crate::harness::alice_run_until::is_xmr_lock_transaction_sent;
@ -39,18 +39,14 @@ mod test {
harness_ctx: TestContext,
) -> (Client, MakeCapturingWriter, Arc<Context>) {
let writer = capture_logs(LevelFilter::DEBUG);
let server_address: SocketAddr = SERVER_ADDRESS.parse().unwrap();
let request = Request::new(Method::StartDaemon {
server_address: Some(server_address),
});
let server_address: Option<SocketAddr> = SERVER_ADDRESS.parse().unwrap().into();
let context = Arc::new(harness_ctx.get_bob_context().await);
let context_clone = context.clone();
tokio::spawn(async move {
if let Err(err) = request.call(context_clone).await {
if let Err(err) = start_daemon(StartDaemonArgs { server_address }, context).await {
println!("Failed to initialize daemon for testing: {}", err);
}
});