From d3b2b5b2e8f7a30938abaced5e9c3db5cf0bf261 Mon Sep 17 00:00:00 2001 From: Binarybaron Date: Wed, 16 Apr 2025 19:16:03 +0200 Subject: [PATCH] refactor --- Cargo.lock | 1 + .../components/modal/ConfirmationModal.tsx | 2 +- .../in_progress/SwapSetupInflightPage.tsx | 140 +++++++++++++++++- .../init/WaitingForBitcoinDepositPage.tsx | 10 +- .../components/pages/swap/SwapPage.tsx | 2 - src-gui/src/store/features/rpcSlice.ts | 6 +- src-tauri/Cargo.toml | 1 + swap/src/cli/api.rs | 1 - swap/src/cli/api/tauri_bindings.rs | 56 ++++++- swap/src/protocol/bob/swap.rs | 93 ++++++------ 10 files changed, 247 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d9ef8821..5bf725c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11777,6 +11777,7 @@ dependencies = [ "tauri-plugin-store", "tauri-plugin-updater", "tracing", + "uuid", ] [[package]] diff --git a/src-gui/src/renderer/components/modal/ConfirmationModal.tsx b/src-gui/src/renderer/components/modal/ConfirmationModal.tsx index 41477ebc..c00c8ed4 100644 --- a/src-gui/src/renderer/components/modal/ConfirmationModal.tsx +++ b/src-gui/src/renderer/components/modal/ConfirmationModal.tsx @@ -4,7 +4,7 @@ import { useAppSelector } from '../../../store/hooks'; // Adjust path import { ConfirmationRequestPayload } from '../../../store/features/rpcSlice'; // Adjust path import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Box, LinearProgress -} from '@material-ui/core'; // Assuming Material-UI is used +} from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; const useStyles = makeStyles((theme) => ({ diff --git a/src-gui/src/renderer/components/modal/swap/pages/in_progress/SwapSetupInflightPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/in_progress/SwapSetupInflightPage.tsx index 2c0fa5bf..b63f4f81 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/in_progress/SwapSetupInflightPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/in_progress/SwapSetupInflightPage.tsx @@ -1,16 +1,152 @@ +import React, { useState, useEffect } from 'react'; +import { invoke } from '@tauri-apps/api/core'; import { TauriSwapProgressEventContent } from "models/tauriModelExt"; -import { SatsAmount } from "renderer/components/other/Units"; +import { SatsAmount, MoneroAmount, PiconeroAmount, MoneroSatsExchangeRate, MoneroBitcoinExchangeRateFromAmounts } from "renderer/components/other/Units"; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle"; +import { + Box, Button, Typography, LinearProgress, Divider, + CircularProgress +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import { ConfirmationRequestPayload } from 'store/features/rpcSlice'; +import { useAppSelector } from 'store/hooks'; +import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton'; + +const useStyles = makeStyles((theme) => ({ + confirmationBox: { + width: '100%', + }, + timerProgress: { + marginTop: theme.spacing(1), + marginBottom: theme.spacing(2), + }, + actions: { + marginTop: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + gap: theme.spacing(2), + }, + valueHighlight: { + fontWeight: 'bold', + marginLeft: theme.spacing(1), + }, + timerContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: theme.spacing(2), + marginTop: theme.spacing(2), + }, +})); + +// Helper to find the relevant confirmation request +const findPreBtcLockRequest = (confirmations: { [key: string]: ConfirmationRequestPayload }): ConfirmationRequestPayload | undefined => { + return Object.values(confirmations).find(req => req.details.type === 'PreBtcLock'); +}; export default function SwapSetupInflightPage({ btc_lock_amount, btc_tx_lock_fee, }: TauriSwapProgressEventContent<"SwapSetupInflight">) { + const classes = useStyles(); + const pendingConfirmations = useAppSelector((state) => state.rpc.state.pendingConfirmations); + const request = findPreBtcLockRequest(pendingConfirmations); + + const [timeLeft, setTimeLeft] = useState(null); + const [progress, setProgress] = useState(100); + + // Timer effect + useEffect(() => { + if (request) { + setTimeLeft(request.timeout_secs); + setProgress(100); + const interval = setInterval(() => { + setTimeLeft((prevTime) => { + if (prevTime === null || prevTime <= 1) { + clearInterval(interval); + return 0; + } + const newTime = prevTime - 1; + if(request) { + setProgress((newTime / request.timeout_secs) * 100); + } else { + setProgress(0); // Or handle error/reset state + clearInterval(interval); + } + return newTime; + }); + }, 1000); + return () => clearInterval(interval); + } else { + setTimeLeft(null); + setProgress(100); + } + }, [request]); + + if (request) { + const {btc_lock_amount, btc_network_fee, xmr_receive_amount} = request.details.content; + + return ( + + Confirm Swap Details + + + + Please review and confirm the swap amounts below before locking your Bitcoin. +
+ You lock +
+ You pay in network fees +
+ You receive +
+ + Exchange rate: + + + {timeLeft !== null && ( + + + Time remaining: {timeLeft}s + + )} +
+ + + + invoke('deny_confirmation', { requestId: request.request_id }) + } + displayErrorSnackbar={true} + requiresContext={true} // Assuming context is needed for the command + > + Deny + + + + invoke('accept_confirmation', { requestId: request.request_id }) + } + displayErrorSnackbar={true} + requiresContext={true} // Assuming context is needed for the command + > + Accept + + +
+ ); + } + return ( - Starting swap with maker to lock + Negotiating with maker to swap } /> diff --git a/src-gui/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx index f6bc169e..08785f4a 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/init/WaitingForBitcoinDepositPage.tsx @@ -1,6 +1,5 @@ import { Box, makeStyles, Typography } from "@material-ui/core"; import { TauriSwapProgressEventContent } from "models/tauriModelExt"; -import { useAppSelector } from "store/hooks"; import BitcoinIcon from "../../../../icons/BitcoinIcon"; import { MoneroSatsExchangeRate, SatsAmount } from "../../../../other/Units"; import DepositAddressInfoBox from "../../DepositAddressInfoBox"; @@ -57,18 +56,17 @@ export default function WaitingForBtcDepositPage({ )}
  • - All Bitcoin sent to this this address will converted into - Monero at an exchance rate of{" "} + Bitcoin sent to this this address will be converted into + Monero at an exchange rate of{" ≈ "}
  • - The network fee of{" "} + The Network fee of{" ≈ "} will automatically be deducted from the deposited coins
  • - The swap will start automatically as soon as the minimum - amount is deposited. + After the deposit is detected, you'll get to confirm the exact details before your funds are locked
  • ({ outer: { @@ -66,7 +65,6 @@ export default function SwapPage() { - ); } diff --git a/src-gui/src/store/features/rpcSlice.ts b/src-gui/src/store/features/rpcSlice.ts index e0a3c894..ac8dd9b2 100644 --- a/src-gui/src/store/features/rpcSlice.ts +++ b/src-gui/src/store/features/rpcSlice.ts @@ -20,11 +20,12 @@ import logger from "utils/logger"; // interface PreBtcLockConfirmationData { ... } // type ConfirmationRequestData = ... +// Define the payload structure correctly, matching tauriModel.ts // This interface represents the actual payload received from the Tauri event `confirmation_request` -// It includes the request_id, timeout, and the flattened generated type -export interface ConfirmationRequestPayload extends ConfirmationRequestType { +export interface ConfirmationRequestPayload { request_id: string; timeout_secs: number; + details: ConfirmationRequestType; // The enum type is nested under details } // --- End Refactored Confirmation Types --- @@ -160,6 +161,7 @@ export const rpcSlice = createSlice({ }; }, confirmationRequested(slice, action: PayloadAction) { + console.log("received confirmation request", action.payload); slice.state.pendingConfirmations[action.payload.request_id] = action.payload; }, confirmationResolved(slice, action: PayloadAction<{ requestId: string }>) { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e8e89b9a..bce19b85 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -28,6 +28,7 @@ tauri-plugin-shell = "^2.0.0" tauri-plugin-store = "^2.0.0" tauri-plugin-updater = "^2.1.0" tracing = "0.1" +uuid = "1.16.0" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-cli = "^2.0.0" diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index fb5b407d..b9226a67 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -194,7 +194,6 @@ pub struct Context { } /// A conveniant builder struct for [`Context`]. -#[derive(Debug)] #[must_use = "ContextBuilder must be built to be useful"] pub struct ContextBuilder { monero: Option, diff --git a/swap/src/cli/api/tauri_bindings.rs b/swap/src/cli/api/tauri_bindings.rs index de02b0e4..397539a3 100644 --- a/swap/src/cli/api/tauri_bindings.rs +++ b/swap/src/cli/api/tauri_bindings.rs @@ -1,3 +1,4 @@ +use crate::bitcoin; use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote}; use anyhow::{anyhow, Result}; use bitcoin::Txid; @@ -22,11 +23,28 @@ const CONTEXT_INIT_PROGRESS_EVENT_NAME: &str = "context-init-progress-update"; const BALANCE_CHANGE_EVENT_NAME: &str = "balance-change"; const BACKGROUND_REFUND_EVENT_NAME: &str = "background-refund"; +#[typeshare] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PreBtcLockDetails { + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub btc_lock_amount: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub btc_network_fee: bitcoin::Amount, + #[typeshare(serialized_as = "number")] + pub xmr_receive_amount: monero::Amount, + #[typeshare(serialized_as = "string")] + pub swap_id: Uuid, +} + #[typeshare] #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "type", content = "content")] pub enum ConfirmationRequestType { - PreBtcLock { state2_json: String }, + /// Request confirmation before locking Bitcoin. + /// Contains specific details for review. + PreBtcLock(PreBtcLockDetails), } struct PendingConfirmation { @@ -87,7 +105,6 @@ impl TauriHandle { Ok(()) } - // --- Confirmation Methods --- pub async fn request_confirmation( &self, request_type: ConfirmationRequestType, @@ -95,10 +112,8 @@ impl TauriHandle { ) -> Result { #[cfg(not(feature = "tauri"))] { - // If Tauri feature is not enabled, we cannot show UI. - // Decide behavior: maybe auto-deny? - tracing::warn!("Confirmation requested but Tauri feature not enabled. Auto-denying."); - return Ok(false); + // We want the CLI to be non-interactive. Therefore, we accept by default if no TauriHandle is available + return Ok(true); } #[cfg(feature = "tauri")] @@ -201,6 +216,12 @@ impl TauriHandle { } pub trait TauriEmitter { + async fn request_confirmation( + &self, + request_type: ConfirmationRequestType, + timeout_secs: u64, + ) -> Result; + fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()>; fn emit_swap_progress_event(&self, swap_id: Uuid, event: TauriSwapProgressEvent) { @@ -252,6 +273,14 @@ pub trait TauriEmitter { } impl TauriEmitter for TauriHandle { + async fn request_confirmation( + &self, + request_type: ConfirmationRequestType, + timeout_secs: u64, + ) -> Result { + self.request_confirmation(request_type, timeout_secs).await + } + fn emit_tauri_event(&self, event: &str, payload: S) -> Result<()> { self.emit_tauri_event(event, payload) } @@ -261,9 +290,24 @@ impl TauriEmitter for Option { fn emit_tauri_event(&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_confirmation( + &self, + request_type: ConfirmationRequestType, + timeout_secs: u64, + ) -> Result { + match self { + Some(tauri) => tauri.request_confirmation(request_type, timeout_secs).await, + + // We want the CLI to be non-interactive. Therefore, we accept by default if no TauriHandle is available + None => Ok(true), + } + } } #[typeshare] diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 7a9fb0bc..b2abbce1 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,7 +1,9 @@ use crate::bitcoin::wallet::ScriptStatus; use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund}; use crate::cli::api::tauri_bindings::ConfirmationRequestType; -use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent}; +use crate::cli::api::tauri_bindings::{ + PreBtcLockDetails, TauriEmitter, TauriHandle, TauriSwapProgressEvent, +}; use crate::cli::api::Context; use crate::cli::EventLoopHandle; use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected}; @@ -135,8 +137,6 @@ async fn next_state( tracing::info!(%swap_id, "Starting new swap"); - // Ensure confirmation logic is NOT here - BobState::SwapSetupCompleted(state2) } BobState::SwapSetupCompleted(state2) => { @@ -151,43 +151,7 @@ async fn next_state( // which can lead to the wallet not detect the transaction. let monero_wallet_restore_blockheight = monero_wallet.block_height().await?; - // --- Start Confirmation Logic --- - const CONFIRMATION_TIMEOUT_SECS: u64 = 120; - let state2_json = serde_json::to_string(&state2) - .context("Failed to serialize State2 for confirmation")?; - let request_type = ConfirmationRequestType::PreBtcLock { state2_json }; - - tracing::info!("Requesting user confirmation before locking BTC..."); - // Use the event_emitter Option - if let Some(handle) = &event_emitter { - let confirmation_result = handle - .request_confirmation(request_type, CONFIRMATION_TIMEOUT_SECS) - .await; - - match confirmation_result { - Ok(true) => { - tracing::info!("User accepted BTC lock confirmation."); - // Proceed - } - Ok(false) => { - tracing::warn!("User denied or timed out on BTC lock confirmation."); - return Err(anyhow!( - "Swap aborted by user/timeout before locking Bitcoin." - )); - } - Err(e) => { - tracing::error!("Error during confirmation request: {}", e); - return Err(e.context("Failed to get user confirmation for BTC lock")); - } - } - } else { - // Handle case where no UI is available - tracing::warn!("Confirmation required, but no UI handle available. Aborting swap."); - return Err(anyhow!( - "Confirmation required, but no UI handle available." - )); - } - // --- End Confirmation Logic --- + let xmr_receive_amount = state2.xmr; // Alice and Bob have exchanged info // Sign the Bitcoin lock transaction @@ -197,12 +161,51 @@ async fn next_state( .await .context("Failed to sign Bitcoin lock transaction")?; - // Publish the signed Bitcoin lock transaction - let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; + let btc_network_fee = tx_lock.fee().context("Failed to get fee")?; + let btc_lock_amount = bitcoin::Amount::from_sat( + signed_tx + .output + .get(0) + .context("Failed to get lock amount")? + .value, + ); - BobState::BtcLocked { - state3, - monero_wallet_restore_blockheight, + const CONFIRMATION_TIMEOUT_SECS: u64 = 120; + + let request = ConfirmationRequestType::PreBtcLock(PreBtcLockDetails { + btc_lock_amount, + btc_network_fee, + xmr_receive_amount, + swap_id, + }); + + // We request confirmation before locking the Bitcoin, as the exchange rate determined at this step might be different from the + // we previously received from Alice. + let confirmation_result = event_emitter + .request_confirmation(request, CONFIRMATION_TIMEOUT_SECS) + .await; + + match confirmation_result { + Ok(true) => { + tracing::info!("User accepted swap details"); + + // Publish the signed Bitcoin lock transaction + let (..) = bitcoin_wallet.broadcast(signed_tx, "lock").await?; + + BobState::BtcLocked { + state3, + monero_wallet_restore_blockheight, + } + } + Ok(false) => { + tracing::warn!("User denied or timed out on swap details confirmation"); + + BobState::SafelyAborted + } + Err(e) => { + tracing::error!("Error during confirmation request: {}", e); + return Err(e.context("Failed to get user confirmation for swap details")); + } } } // Bob has locked Bitcoin