This commit is contained in:
Binarybaron 2025-04-16 19:16:03 +02:00
parent 94484c390f
commit d3b2b5b2e8
10 changed files with 247 additions and 65 deletions

1
Cargo.lock generated
View File

@ -11777,6 +11777,7 @@ dependencies = [
"tauri-plugin-store",
"tauri-plugin-updater",
"tracing",
"uuid",
]
[[package]]

View File

@ -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) => ({

View File

@ -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<number | null>(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 (
<Box className={classes.confirmationBox}>
<Typography variant="h6" gutterBottom>Confirm Swap Details</Typography>
<Divider />
<Box mt={2} mb={2} textAlign="left">
<Typography gutterBottom>
Please review and confirm the swap amounts below before locking your Bitcoin.
<br />
You lock <SatsAmount amount={btc_lock_amount} />
<br />
You pay <SatsAmount amount={btc_network_fee} /> in network fees
<br />
You receive <PiconeroAmount amount={xmr_receive_amount} />
</Typography>
<Typography>
Exchange rate: <MoneroBitcoinExchangeRateFromAmounts displayMarkup satsAmount={btc_lock_amount} piconeroAmount={xmr_receive_amount} />
</Typography>
{timeLeft !== null && (
<Box className={classes.timerContainer}>
<CircularProgress variant="determinate" value={progress} size={24} />
<Typography variant="body2">Time remaining: {timeLeft}s</Typography>
</Box>
)}
</Box>
<Divider />
<Box className={classes.actions}>
<PromiseInvokeButton
variant="outlined"
disabled={timeLeft === 0 || !request}
onInvoke={() =>
invoke('deny_confirmation', { requestId: request.request_id })
}
displayErrorSnackbar={true}
requiresContext={true} // Assuming context is needed for the command
>
Deny
</PromiseInvokeButton>
<PromiseInvokeButton
variant="contained"
color="primary"
disabled={timeLeft === 0 || !request}
onInvoke={ () =>
invoke('accept_confirmation', { requestId: request.request_id })
}
displayErrorSnackbar={true}
requiresContext={true} // Assuming context is needed for the command
>
Accept
</PromiseInvokeButton>
</Box>
</Box>
);
}
return (
<CircularProgressWithSubtitle
description={
<>
Starting swap with maker to lock <SatsAmount amount={btc_lock_amount} />
Negotiating with maker to swap <SatsAmount amount={btc_lock_amount} />
</>
}
/>

View File

@ -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({
)}
</li>
<li>
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{" ≈ "}
<MoneroSatsExchangeRate rate={quote.price} displayMarkup={true} />
</li>
<li>
The network fee of{" "}
The Network fee of{" "}
<SatsAmount amount={min_bitcoin_lock_tx_fee} /> will
automatically be deducted from the deposited coins
</li>
<li>
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
</li>
<li>
<DepositAmountHelper

View File

@ -9,7 +9,6 @@ import {
confirmationResolved,
ConfirmationRequestPayload,
} from "../../../../store/features/rpcSlice";
import ConfirmationModal from "../../modal/ConfirmationModal";
const useStyles = makeStyles((theme) => ({
outer: {
@ -66,7 +65,6 @@ export default function SwapPage() {
<Box className={classes.outer}>
<ApiAlertsBox />
<SwapWidget />
<ConfirmationModal />
</Box>
);
}

View File

@ -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<ConfirmationRequestPayload>) {
console.log("received confirmation request", action.payload);
slice.state.pendingConfirmations[action.payload.request_id] = action.payload;
},
confirmationResolved(slice, action: PayloadAction<{ requestId: string }>) {

View File

@ -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"

View File

@ -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<Monero>,

View File

@ -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<bool> {
#[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<bool>;
fn emit_tauri_event<S: Serialize + Clone>(&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<bool> {
self.request_confirmation(request_type, timeout_secs).await
}
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
self.emit_tauri_event(event, payload)
}
@ -261,9 +290,24 @@ 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_confirmation(
&self,
request_type: ConfirmationRequestType,
timeout_secs: u64,
) -> Result<bool> {
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]

View File

@ -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<TauriHandle>
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