xmr-btc-swap/swap/src/cli/api/tauri_bindings.rs
Mohan 4ae47e57f9
refactor: swap-core / swap-machine (#530)
* progress

* fix thread safety

* move monero types from swap into swap_core

* just fmt

* move non test code above test code

* revert removed tracing in bitcoin-wallet/src/primitives.rs

* Use existing private_key_from_secp256k1_scalar

* remove unused monero chose code

* fix some clippy warnings due to imports

* move state machine types into the new `swap-machine` crate

* remove monero_c orphan submodule

* rm bdk_test and sqlx_test from ci

* move proptest.rs into swap-proptest

* increase stack size to 12mb

* properly increase stack size

* fix merge conflict in ci.yml

* don't increase stack size on mac

* fix infinite recursion

* fix integration tests

* fix some compile errors

* fix compilation errors

* rustfmt

* ignore unstaged patches we applied to monero submodule when running git status

* fix some test compilation errors

* use BitcoinWallet trait instead of concrete type everywhere

* add just test command to run integration tests

* remove test_utils features from bdk in swap-core

---------

Co-authored-by: einliterflasche <einliterflasche@pm.me>
Co-authored-by: binarybaron <binarybaron@mail.mail>
2025-10-10 16:29:00 +02:00

1084 lines
34 KiB
Rust

use super::request::BalanceResponse;
use crate::cli::api::request::{
GetMoneroBalanceResponse, GetMoneroHistoryResponse, GetMoneroSyncProgressResponse,
};
use crate::cli::list_sellers::QuoteWithAddress;
use crate::monero::MoneroAddressPool;
use crate::{monero, network::quote::BidQuote};
use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use bitcoin::Txid;
use monero_rpc_pool::pool::PoolStatus;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Display;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use strum::Display;
use swap_core::bitcoin;
use swap_core::bitcoin::ExpiredTimelocks;
use tokio::sync::oneshot;
use typeshare::typeshare;
use uuid::Uuid;
const TAURI_UNIFIED_EVENT_NAME: &str = "tauri-unified-event";
#[typeshare]
#[derive(Clone, Serialize)]
#[serde(tag = "channelName", content = "event")]
pub enum TauriEvent {
SwapProgress(TauriSwapProgressEventWrapper),
CliLog(TauriLogEvent),
BalanceChange(BalanceResponse),
SwapDatabaseStateUpdate(TauriDatabaseStateEvent),
TimelockChange(TauriTimelockChangeEvent),
Approval(ApprovalRequest),
BackgroundProgress(TauriBackgroundProgressWrapper),
PoolStatusUpdate(PoolStatus),
MoneroWalletUpdate(MoneroWalletUpdate),
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "content")]
pub enum MoneroWalletUpdate {
BalanceChange(GetMoneroBalanceResponse),
SyncProgress(GetMoneroSyncProgressResponse),
HistoryUpdate(GetMoneroHistoryResponse),
}
#[typeshare]
#[derive(Clone, Debug, Serialize)]
pub struct ContextStatus {
pub bitcoin_wallet_available: bool,
pub monero_wallet_available: bool,
pub database_available: bool,
pub tor_available: bool,
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LockBitcoinDetails {
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::amount::serde::as_sat")]
pub btc_lock_amount: bitcoin::Amount,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::amount::serde::as_sat")]
pub btc_network_fee: bitcoin::Amount,
#[typeshare(serialized_as = "number")]
pub xmr_receive_amount: monero::Amount,
pub monero_receive_pool: MoneroAddressPool,
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SelectMakerDetails {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::amount::serde::as_sat")]
pub btc_amount_to_swap: bitcoin::Amount,
pub maker: QuoteWithAddress,
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SendMoneroDetails {
/// Destination address for the Monero transfer
#[typeshare(serialized_as = "string")]
pub address: String,
/// Amount to send
#[typeshare(serialized_as = "number")]
pub amount: monero::Amount,
/// Transaction fee
#[typeshare(serialized_as = "number")]
pub fee: monero::Amount,
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PasswordRequestDetails {
/// The wallet file path that requires a password
pub wallet_path: String,
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "content")]
pub enum SeedChoice {
RandomSeed,
FromSeed { seed: String },
FromWalletPath { wallet_path: String },
Legacy,
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SeedSelectionDetails {
/// List of recently used wallet paths
pub recent_wallets: Vec<String>,
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApprovalRequest {
request: ApprovalRequestType,
request_status: RequestStatus,
#[typeshare(serialized_as = "string")]
request_id: Uuid,
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "content")]
pub enum ApprovalRequestType {
/// Request approval before locking Bitcoin.
/// Contains specific details for review.
LockBitcoin(LockBitcoinDetails),
/// Request approval for maker selection.
/// Contains available makers and swap details.
SelectMaker(SelectMakerDetails),
/// Request seed selection from user.
/// User can choose between random seed, provide their own, or select wallet file.
SeedSelection(SeedSelectionDetails),
/// Request approval for publishing a Monero transaction.
SendMonero(SendMoneroDetails),
/// Request password for wallet file.
/// User must provide password to unlock the selected wallet.
PasswordRequest(PasswordRequestDetails),
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "state", content = "content")]
pub enum RequestStatus {
Pending {
#[typeshare(serialized_as = "number")]
expiration_ts: u64,
},
Resolved {
#[typeshare(serialized_as = "object")]
approve_input: serde_json::Value,
},
Rejected,
}
struct PendingApproval {
responder: Option<oneshot::Sender<serde_json::Value>>,
#[allow(dead_code)]
expiration_ts: u64,
request: ApprovalRequest,
}
impl Drop for PendingApproval {
fn drop(&mut self) {
if let Some(responder) = self.responder.take() {
tracing::debug!("Dropping pending approval because handle was dropped");
let _ = responder.send(serde_json::Value::Bool(false));
}
}
}
#[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TorBootstrapStatus {
pub frac: f32,
pub ready_for_traffic: bool,
pub blockage: Option<String>,
}
#[cfg(feature = "tauri")]
struct TauriHandleInner {
app_handle: tauri::AppHandle,
pending_approvals: Arc<Mutex<HashMap<Uuid, PendingApproval>>>,
}
#[derive(Clone)]
pub struct TauriHandle(
#[cfg(feature = "tauri")]
#[cfg_attr(feature = "tauri", allow(unused))]
Arc<TauriHandleInner>,
);
impl TauriHandle {
#[cfg(feature = "tauri")]
pub fn new(tauri_handle: tauri::AppHandle) -> Self {
use std::collections::HashMap;
Self(
#[cfg(feature = "tauri")]
Arc::new(TauriHandleInner {
app_handle: tauri_handle,
pending_approvals: Arc::new(Mutex::new(HashMap::new())),
}),
)
}
#[allow(unused_variables)]
pub fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
#[cfg(feature = "tauri")]
{
let inner = self.0.as_ref();
tauri::Emitter::emit(&inner.app_handle, event, payload).map_err(anyhow::Error::from)?;
}
Ok(())
}
/// Helper to emit a approval event via the unified event name
fn emit_approval(&self, event: ApprovalRequest) {
tracing::debug!(?event, "Emitting approval event");
self.emit_unified_event(TauriEvent::Approval(event))
}
pub async fn request_approval<Response>(
&self,
request_type: ApprovalRequestType,
timeout_secs: Option<u64>,
) -> Result<Response>
where
Response: serde::de::DeserializeOwned + Clone + Serialize,
{
#[cfg(not(feature = "tauri"))]
{
bail!("Tauri feature not enabled");
}
#[cfg(feature = "tauri")]
{
// Create the approval request
// Generate the UUID
// Set the expiration timestamp
let (responder, receiver) = oneshot::channel();
let request_id = Uuid::new_v4();
let timeout_secs = timeout_secs.unwrap_or(60 * 60 * 24 * 7);
let timeout_duration = Duration::from_secs(timeout_secs);
let expiration_ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| anyhow!("Failed to get current time: {}", e))?
.as_secs()
+ timeout_duration.as_secs();
let request = ApprovalRequest {
request: request_type,
request_status: RequestStatus::Pending { expiration_ts },
request_id,
};
// Emit the "pending" event
self.emit_approval(request.clone());
tracing::debug!(%request, "Emitted approval request event");
let pending = PendingApproval {
responder: Some(responder),
expiration_ts: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| anyhow!("Failed to get current time: {}", e))?
.as_secs()
+ timeout_secs,
request: request.clone(),
};
// Lock map and insert the pending approval
{
let mut pending_map = self
.0
.pending_approvals
.lock()
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
pending_map.insert(request_id, pending);
}
// Create cleanup guard to handle cancellation
let mut cleanup_guard = ApprovalCleanupGuard::new(
request_id,
self.clone(),
self.0.pending_approvals.clone(),
);
// Determine if the request will be accepted or rejected
// Either by being resolved by the user, or by timing out
let unparsed_response = tokio::select! {
res = receiver => Some(res.map_err(|_| anyhow!("Approval responder dropped"))?),
_ = tokio::time::sleep(timeout_duration) => {
None
},
};
let maybe_response: Option<Response> = match &unparsed_response {
Some(value) => serde_json::from_value(value.clone())
.inspect_err(|e| {
tracing::error!("Failed to parse approval response to expected type: {}", e)
})
.ok(),
None => None,
};
let mut map = self
.0
.pending_approvals
.lock()
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
if let Some(_pending) = map.remove(&request_id) {
let status = match &maybe_response {
Some(_) => RequestStatus::Resolved {
approve_input: unparsed_response.unwrap_or(serde_json::Value::Bool(false)),
},
None => RequestStatus::Rejected,
};
// Set the status and emit the event
let mut approval = request.clone();
approval.request_status = status;
self.emit_approval(approval.clone());
tracing::debug!(%approval, "Resolved approval request");
}
cleanup_guard.disarm();
tracing::debug!("Returning approval response");
maybe_response.context("Approval was rejected")
}
}
pub async fn resolve_approval(
&self,
request_id: Uuid,
response: serde_json::Value,
) -> Result<()> {
#[cfg(not(feature = "tauri"))]
{
Err(anyhow!(
"Cannot resolve approval: Tauri feature not enabled."
))
}
#[cfg(feature = "tauri")]
{
let mut pending_map = self
.0
.pending_approvals
.lock()
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
if let Some(mut pending) = pending_map.remove(&request_id) {
// Send response through oneshot channel
if let Some(responder) = pending.responder.take() {
let _ = responder.send(response);
Ok(())
} else {
Err(anyhow!("Approval responder was already consumed"))
}
} else {
Err(anyhow!("Approval not found or already handled"))
}
}
}
pub async fn reject_approval(&self, request_id: Uuid) -> Result<()> {
#[cfg(not(feature = "tauri"))]
{
Err(anyhow!(
"Cannot reject approval: Tauri feature not enabled."
))
}
#[cfg(feature = "tauri")]
{
let mut pending_map = self
.0
.pending_approvals
.lock()
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
if let Some(mut pending) = pending_map.remove(&request_id) {
// Send rejection through oneshot channel
if let Some(responder) = pending.responder.take() {
let _ = responder.send(serde_json::Value::Null);
// Emit the rejection event
let mut approval = pending.request.clone();
approval.request_status = RequestStatus::Rejected;
self.emit_approval(approval);
Ok(())
} else {
Err(anyhow!("Approval responder was already consumed"))
}
} else {
Err(anyhow!("Approval not found or already handled"))
}
}
}
}
impl Display for ApprovalRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.request {
ApprovalRequestType::LockBitcoin(..) => write!(f, "LockBitcoin()"),
ApprovalRequestType::SelectMaker(..) => write!(f, "SelectMaker()"),
ApprovalRequestType::SeedSelection(_) => write!(f, "SeedSelection()"),
ApprovalRequestType::SendMonero(_) => write!(f, "SendMonero()"),
ApprovalRequestType::PasswordRequest(_) => write!(f, "PasswordRequest()"),
}
}
}
#[async_trait]
pub trait TauriEmitter {
async fn request_bitcoin_approval(
&self,
details: LockBitcoinDetails,
timeout_secs: u64,
) -> Result<bool>;
async fn request_maker_selection(
&self,
details: SelectMakerDetails,
timeout_secs: u64,
) -> Result<bool>;
async fn request_seed_selection(&self) -> Result<SeedChoice>;
async fn request_seed_selection_with_recent_wallets(
&self,
recent_wallets: Vec<String>,
) -> Result<SeedChoice>;
async fn request_password(&self, wallet_path: String) -> Result<String>;
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()>;
fn emit_unified_event(&self, event: TauriEvent) {
let _ = self.emit_tauri_event(TAURI_UNIFIED_EVENT_NAME, event);
}
// Restore default implementations below
fn emit_swap_progress_event(&self, swap_id: Uuid, event: TauriSwapProgressEvent) {
self.emit_unified_event(TauriEvent::SwapProgress(TauriSwapProgressEventWrapper {
swap_id,
event,
}));
}
fn emit_cli_log_event(&self, event: TauriLogEvent) {
self.emit_unified_event(TauriEvent::CliLog(event));
}
fn emit_swap_state_change_event(&self, swap_id: Uuid) {
self.emit_unified_event(TauriEvent::SwapDatabaseStateUpdate(
TauriDatabaseStateEvent { swap_id },
));
}
fn emit_timelock_change_event(&self, swap_id: Uuid, timelock: Option<ExpiredTimelocks>) {
self.emit_unified_event(TauriEvent::TimelockChange(TauriTimelockChangeEvent {
swap_id,
timelock,
}));
}
fn emit_balance_update_event(&self, new_balance: bitcoin::Amount) {
self.emit_unified_event(TauriEvent::BalanceChange(BalanceResponse {
balance: new_balance,
}));
}
fn emit_background_progress(&self, id: Uuid, event: TauriBackgroundProgress) {
self.emit_unified_event(TauriEvent::BackgroundProgress(
TauriBackgroundProgressWrapper { id, event },
));
}
fn emit_pool_status_update(&self, status: PoolStatus) {
self.emit_unified_event(TauriEvent::PoolStatusUpdate(status));
}
/// Create a new background progress handle for tracking a specific type of progress
fn new_background_process<T: Clone>(
&self,
component: fn(PendingCompleted<T>) -> TauriBackgroundProgress,
) -> TauriBackgroundProgressHandle<T>;
fn new_background_process_with_initial_progress<T: Clone>(
&self,
component: fn(PendingCompleted<T>) -> TauriBackgroundProgress,
initial_progress: T,
) -> TauriBackgroundProgressHandle<T>;
}
#[async_trait]
impl TauriEmitter for TauriHandle {
async fn request_bitcoin_approval(
&self,
details: LockBitcoinDetails,
timeout_secs: u64,
) -> Result<bool> {
Ok(self
.request_approval(
ApprovalRequestType::LockBitcoin(details),
Some(timeout_secs),
)
.await
.unwrap_or(false))
}
async fn request_maker_selection(
&self,
details: SelectMakerDetails,
timeout_secs: u64,
) -> Result<bool> {
Ok(self
.request_approval(
ApprovalRequestType::SelectMaker(details),
Some(timeout_secs),
)
.await
.unwrap_or(false))
}
async fn request_seed_selection(&self) -> Result<SeedChoice> {
self.request_seed_selection_with_recent_wallets(vec![])
.await
}
async fn request_seed_selection_with_recent_wallets(
&self,
recent_wallets: Vec<String>,
) -> Result<SeedChoice> {
let details = SeedSelectionDetails { recent_wallets };
self.request_approval(ApprovalRequestType::SeedSelection(details), None)
.await
}
async fn request_password(&self, wallet_path: String) -> Result<String> {
let details = PasswordRequestDetails { wallet_path };
self.request_approval(ApprovalRequestType::PasswordRequest(details), None)
.await
}
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
self.emit_tauri_event(event, payload)
}
fn new_background_process<T: Clone>(
&self,
component: fn(PendingCompleted<T>) -> TauriBackgroundProgress,
) -> TauriBackgroundProgressHandle<T> {
let id = Uuid::new_v4();
TauriBackgroundProgressHandle {
id,
component,
emitter: Some(self.clone()),
is_finished: Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
fn new_background_process_with_initial_progress<T: Clone>(
&self,
component: fn(PendingCompleted<T>) -> TauriBackgroundProgress,
initial_progress: T,
) -> TauriBackgroundProgressHandle<T> {
let background_process_handle = self.new_background_process(component);
background_process_handle.update(initial_progress);
background_process_handle
}
}
#[async_trait]
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),
// If no TauriHandle is available, we just ignore the event and pretend as if it was emitted
None => Ok(()),
}
}
async fn request_bitcoin_approval(
&self,
details: LockBitcoinDetails,
timeout_secs: u64,
) -> Result<bool> {
match self {
Some(tauri) => tauri.request_bitcoin_approval(details, timeout_secs).await,
// If no TauriHandle is available, we just approve the request
None => Ok(true),
}
}
async fn request_maker_selection(
&self,
details: SelectMakerDetails,
timeout_secs: u64,
) -> Result<bool> {
match self {
Some(tauri) => tauri.request_maker_selection(details, timeout_secs).await,
None => bail!("No Tauri handle available"),
}
}
async fn request_seed_selection(&self) -> Result<SeedChoice> {
match self {
Some(tauri) => tauri.request_seed_selection().await,
None => bail!("No Tauri handle available"),
}
}
async fn request_seed_selection_with_recent_wallets(
&self,
recent_wallets: Vec<String>,
) -> Result<SeedChoice> {
match self {
Some(tauri) => {
tauri
.request_seed_selection_with_recent_wallets(recent_wallets)
.await
}
None => bail!("No Tauri handle available"),
}
}
async fn request_password(&self, wallet_path: String) -> Result<String> {
match self {
Some(tauri) => tauri.request_password(wallet_path).await,
None => bail!("No Tauri handle available"),
}
}
fn new_background_process<T: Clone>(
&self,
component: fn(PendingCompleted<T>) -> TauriBackgroundProgress,
) -> TauriBackgroundProgressHandle<T> {
let id = Uuid::new_v4();
TauriBackgroundProgressHandle {
id,
component,
emitter: self.clone(),
is_finished: Arc::new(std::sync::atomic::AtomicBool::new(false)),
}
}
fn new_background_process_with_initial_progress<T: Clone>(
&self,
component: fn(PendingCompleted<T>) -> TauriBackgroundProgress,
initial_progress: T,
) -> TauriBackgroundProgressHandle<T> {
let background_process_handle = self.new_background_process(component);
background_process_handle.update(initial_progress);
background_process_handle
}
}
impl TauriHandle {
#[cfg(feature = "tauri")]
pub async fn get_pending_approvals(&self) -> Result<Vec<ApprovalRequest>> {
let pending_map = self
.0
.pending_approvals
.lock()
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
let approvals: Vec<ApprovalRequest> = pending_map
.values()
.map(|pending| pending.request.clone())
.collect();
Ok(approvals)
}
#[cfg(not(feature = "tauri"))]
pub async fn get_pending_approvals(&self) -> Result<Vec<ApprovalRequest>> {
Ok(Vec::new())
}
}
/// A handle for updating a specific background progress's progress
///
/// # Examples
///
/// ```
/// // For Tor bootstrap progress
/// use self::{TauriHandle, TauriBackgroundProgress, TorBootstrapStatus};
///
/// // In a real scenario, tauri_handle would be properly initialized.
/// // For this example, we'll use Option<TauriHandle>::None,
/// // which allows calling new_background_process.
/// let tauri_handle: Option<TauriHandle> = None;
///
/// let tor_progress = tauri_handle.new_background_process(
/// |status| TauriBackgroundProgress::EstablishingTorCircuits(status)
/// );
///
/// // Define a sample TorBootstrapStatus
/// let tor_status = TorBootstrapStatus {
/// frac: 0.5,
/// ready_for_traffic: false,
/// blockage: None,
/// };
///
/// tor_progress.update(tor_status);
/// tor_progress.finish();
/// ```
#[derive(Clone)]
pub struct TauriBackgroundProgressHandle<T: Clone> {
id: Uuid,
component: fn(PendingCompleted<T>) -> TauriBackgroundProgress,
emitter: Option<TauriHandle>,
is_finished: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl<T: Clone> TauriBackgroundProgressHandle<T> {
/// Update the progress of this background process
/// Updates after finish() has been called will be ignored
#[cfg(feature = "tauri")]
pub fn update(&self, progress: T) {
// Silently fail if the background process has already been finished
if self.is_finished.load(std::sync::atomic::Ordering::Relaxed) {
return;
}
if let Some(emitter) = &self.emitter {
emitter.emit_background_progress(
self.id,
(self.component)(PendingCompleted::Pending(progress)),
);
}
}
#[cfg(not(feature = "tauri"))]
pub fn update(&self, _progress: T) {
// Do nothing when tauri is not enabled
}
/// Mark this background process as completed
/// All subsequent update() calls will be ignored
pub fn finish(&self) {
self.is_finished
.store(true, std::sync::atomic::Ordering::Relaxed);
if let Some(emitter) = &self.emitter {
emitter
.emit_background_progress(self.id, (self.component)(PendingCompleted::Completed));
}
}
}
impl<T: Clone> Drop for TauriBackgroundProgressHandle<T> {
fn drop(&mut self) {
(*self).finish();
}
}
#[typeshare]
#[derive(Display, Clone, Serialize)]
#[serde(tag = "type", content = "content")]
pub enum PendingCompleted<P> {
Pending(P),
Completed,
}
#[derive(Serialize, Clone)]
#[typeshare]
pub struct DownloadProgress {
// Progress of the download in percent (0-100)
#[typeshare(serialized_as = "number")]
pub progress: u64,
// Size of the download file in bytes
#[typeshare(serialized_as = "number")]
pub size: u64,
}
#[derive(Clone, Serialize)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum TauriBitcoinSyncProgress {
Known {
// Number of addresses processed
#[typeshare(serialized_as = "number")]
consumed: u64,
// Total number of addresses to process
#[typeshare(serialized_as = "number")]
total: u64,
},
Unknown,
}
#[derive(Clone, Serialize)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum TauriBitcoinFullScanProgress {
Known {
#[typeshare(serialized_as = "number")]
current_index: u64,
#[typeshare(serialized_as = "number")]
assumed_total: u64,
},
Unknown,
}
#[derive(Serialize, Clone)]
#[typeshare]
pub struct BackgroundRefundProgress {
#[typeshare(serialized_as = "string")]
pub swap_id: Uuid,
}
#[typeshare]
#[derive(Display, Clone, Serialize)]
#[serde(tag = "componentName", content = "progress")]
pub enum TauriBackgroundProgress {
OpeningBitcoinWallet(PendingCompleted<()>),
OpeningMoneroWallet(PendingCompleted<()>),
OpeningDatabase(PendingCompleted<()>),
EstablishingTorCircuits(PendingCompleted<TorBootstrapStatus>),
SyncingBitcoinWallet(PendingCompleted<TauriBitcoinSyncProgress>),
FullScanningBitcoinWallet(PendingCompleted<TauriBitcoinFullScanProgress>),
BackgroundRefund(PendingCompleted<BackgroundRefundProgress>),
ListSellers(PendingCompleted<ListSellersProgress>),
}
#[typeshare]
#[derive(Clone, Serialize)]
pub struct TauriBackgroundProgressWrapper {
#[typeshare(serialized_as = "string")]
id: Uuid,
event: TauriBackgroundProgress,
}
#[typeshare]
#[derive(Display, Clone, Serialize)]
pub enum TauriContextStatusEvent {
NotInitialized,
Initializing,
Available,
Failed,
}
#[derive(Serialize, Clone)]
#[typeshare]
pub struct TauriSwapProgressEventWrapper {
#[typeshare(serialized_as = "string")]
swap_id: Uuid,
event: TauriSwapProgressEvent,
}
#[derive(Serialize, Clone)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum TauriSwapProgressEvent {
Resuming,
ReceivedQuote(BidQuote),
WaitingForBtcDeposit {
#[typeshare(serialized_as = "string")]
deposit_address: bitcoin::Address,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::amount::serde::as_sat")]
max_giveable: bitcoin::Amount,
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::amount::serde::as_sat")]
min_bitcoin_lock_tx_fee: bitcoin::Amount,
known_quotes: Vec<QuoteWithAddress>,
},
SwapSetupInflight {
#[typeshare(serialized_as = "number")]
#[serde(with = "::bitcoin::amount::serde::as_sat")]
btc_lock_amount: bitcoin::Amount,
},
RetrievingMoneroBlockheight,
BtcLockPublishInflight,
BtcLockTxInMempool {
#[typeshare(serialized_as = "string")]
btc_lock_txid: bitcoin::Txid,
#[typeshare(serialized_as = "Option<number>")]
btc_lock_confirmations: Option<u64>,
},
XmrLockTxInMempool {
#[typeshare(serialized_as = "string")]
xmr_lock_txid: monero::TxHash,
#[typeshare(serialized_as = "Option<number>")]
xmr_lock_tx_confirmations: Option<u64>,
#[typeshare(serialized_as = "number")]
xmr_lock_tx_target_confirmations: u64,
},
XmrLocked,
EncryptedSignatureSent,
RedeemingMonero,
WaitingForXmrConfirmationsBeforeRedeem {
#[typeshare(serialized_as = "string")]
xmr_lock_txid: monero::TxHash,
#[typeshare(serialized_as = "number")]
xmr_lock_tx_confirmations: u64,
#[typeshare(serialized_as = "number")]
xmr_lock_tx_target_confirmations: u64,
},
XmrRedeemInMempool {
#[typeshare(serialized_as = "Vec<string>")]
xmr_redeem_txids: Vec<monero::TxHash>,
xmr_receive_pool: MoneroAddressPool,
},
CancelTimelockExpired,
BtcCancelled {
#[typeshare(serialized_as = "string")]
btc_cancel_txid: Txid,
},
// tx_early_refund has been published but has not been confirmed yet
// we can still transition into BtcRefunded from here
BtcEarlyRefundPublished {
#[typeshare(serialized_as = "string")]
btc_early_refund_txid: Txid,
},
// tx_refund has been published but has not been confirmed yet
// we can still transition into BtcEarlyRefunded from here
BtcRefundPublished {
#[typeshare(serialized_as = "string")]
btc_refund_txid: Txid,
},
// tx_early_refund has been confirmed
BtcEarlyRefunded {
#[typeshare(serialized_as = "string")]
btc_early_refund_txid: Txid,
},
// tx_refund has been confirmed
BtcRefunded {
#[typeshare(serialized_as = "string")]
btc_refund_txid: Txid,
},
BtcPunished,
AttemptingCooperativeRedeem,
CooperativeRedeemAccepted,
CooperativeRedeemRejected {
reason: String,
},
Released,
}
/// This event is emitted whenever there is a log message issued in the CLI.
///
/// It contains a json serialized object containing the log message and metadata.
#[typeshare]
#[derive(Debug, Serialize, Clone)]
pub struct TauriLogEvent {
/// The serialized object containing the log message and metadata.
pub buffer: String,
}
#[derive(Serialize, Clone)]
#[typeshare]
pub struct TauriDatabaseStateEvent {
#[typeshare(serialized_as = "string")]
swap_id: Uuid,
}
#[derive(Serialize, Clone)]
#[typeshare]
pub struct TauriTimelockChangeEvent {
#[typeshare(serialized_as = "string")]
swap_id: Uuid,
timelock: Option<ExpiredTimelocks>,
}
#[derive(Serialize, Clone)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum BackgroundRefundState {
Started,
Failed { error: String },
Completed,
}
#[typeshare]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type", content = "content")]
pub enum MoneroNodeConfig {
Pool,
SingleNode { url: String },
}
/// This struct contains the settings for the Context
#[typeshare]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TauriSettings {
/// Configuration for Monero node connection
pub monero_node_config: MoneroNodeConfig,
/// The URLs of the Electrum RPC servers e.g `["ssl://bitcoin.com:50001", "ssl://backup.com:50001"]`
pub electrum_rpc_urls: Vec<String>,
/// Whether to initialize and use a tor client.
pub use_tor: bool,
/// Whether to route Monero wallet traffic through Tor
pub enable_monero_tor: bool,
}
#[typeshare]
#[derive(Debug, Serialize, Clone)]
pub struct ListSellersProgress {
pub rendezvous_points_connected: u32,
pub rendezvous_points_total: u32,
pub rendezvous_points_failed: u32,
pub peers_discovered: u32,
pub quotes_received: u32,
pub quotes_failed: u32,
}
// Add this struct before the TauriHandle implementation
struct ApprovalCleanupGuard {
request_id: Option<Uuid>,
approval_store: Arc<Mutex<HashMap<Uuid, PendingApproval>>>,
handle: TauriHandle,
}
impl ApprovalCleanupGuard {
fn new(
request_id: Uuid,
handle: TauriHandle,
approval_store: Arc<Mutex<HashMap<Uuid, PendingApproval>>>,
) -> Self {
Self {
request_id: Some(request_id),
handle,
approval_store,
}
}
/// Disarm the guard so it won't cleanup on drop (call when normally resolved)
fn disarm(&mut self) {
self.request_id = None;
}
}
impl Drop for ApprovalCleanupGuard {
fn drop(&mut self) {
if let Some(request_id) = self.request_id.take() {
let approval_store = self.approval_store.clone();
let handle = self.handle.clone();
tokio::task::spawn_blocking(move || {
tracing::debug!(%request_id, "Approval handle dropped, we should cleanup now");
// Lock the Mutex
if let Ok(mut approval_store) = approval_store.lock() {
// Check if the request id still present in the map
if let Some(mut pending_approval) = approval_store.remove(&request_id) {
// If there is still someone listening, send a rejection
if let Some(responder) = pending_approval.responder.take() {
let _ = responder.send(serde_json::Value::Bool(false));
}
handle.emit_approval(ApprovalRequest {
request: pending_approval.request.clone().request,
request_status: RequestStatus::Rejected,
request_id,
});
}
}
});
}
}
}