From 9a04bd5682d55f69b0399a9056ae8c31cc737f13 Mon Sep 17 00:00:00 2001 From: Mohan <86064887+binarybaron@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:42:53 +0200 Subject: [PATCH] feat(protocol, wallet): Reveal enc sig fast (#357) * feat(asb, cli): Add safety margin to chosen Bitcoin fee for pre-signed transactions * feat(gui): Add Context init overlay * feat(protocol): Reveal enc sig fast (before full 10 confirmations) * feat(wallet): Use mempool.space as a secondary fee estimation source * log libp2p crates * revert useless stuff * remove unused elements in state machine * remove redundant diff * minimize diff * dont make xmr_lock_tx_target_confirmations optional * pass target conf in listener callback for monero txs * refactor * refactor * nitpicks * feat: add migration file for xmr field in state3, state4, state5 and state6 * revert .gitignore * add monero_double_spend_safe_confirmations to env.rs * change durations in SwapStateStepper.tsx * remove unused helper functions * use env_config.monero_double_spend_safe_confirmations in state machine * refactor * Update src-gui/src/renderer/components/modal/swap/pages/in_progress/WaitingForXmrConfirmationsBeforeRedeemPage.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix label for retry op --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- monero-rpc/src/monerod.rs | 2 +- monero-sys/src/lib.rs | 4 +- .../alert/SwapStatusAlert/SwapStatusAlert.tsx | 2 +- .../modal/swap/SwapStateStepper.tsx | 7 +- .../modal/swap/TransactionInfoBox.tsx | 2 +- .../modal/swap/pages/SwapStatePage.tsx | 14 +- .../pages/in_progress/BitcoinRedeemedPage.tsx | 5 - .../pages/in_progress/RedeemingMoneroPage.tsx | 7 + .../in_progress/SwapSetupInflightPage.tsx | 3 +- ...ingForXmrConfirmationsBeforeRedeemPage.tsx | 29 +++ .../in_progress/XmrLockInMempoolPage.tsx | 3 +- .../navigation/UnfinishedSwapsCountBadge.tsx | 3 +- .../components/other/RenderedCliLog.tsx | 7 +- .../other/ScrollablePaperTextBox.tsx | 8 + ..._amount_in_state3_state4_state5_state6.sql | 206 ++++++++++++++++++ swap/src/cli/api/tauri_bindings.rs | 12 +- swap/src/env.rs | 5 + swap/src/monero/wallet.rs | 2 +- swap/src/protocol/alice/state.rs | 8 +- swap/src/protocol/alice/swap.rs | 8 +- swap/src/protocol/bob/state.rs | 54 +++-- swap/src/protocol/bob/swap.rs | 99 +++++++-- 22 files changed, 427 insertions(+), 63 deletions(-) delete mode 100644 src-gui/src/renderer/components/modal/swap/pages/in_progress/BitcoinRedeemedPage.tsx create mode 100644 src-gui/src/renderer/components/modal/swap/pages/in_progress/RedeemingMoneroPage.tsx create mode 100644 src-gui/src/renderer/components/modal/swap/pages/in_progress/WaitingForXmrConfirmationsBeforeRedeemPage.tsx create mode 100644 swap/migrations/20250630085829_xmr_amount_in_state3_state4_state5_state6.sql diff --git a/monero-rpc/src/monerod.rs b/monero-rpc/src/monerod.rs index debb999f..5b758b3b 100644 --- a/monero-rpc/src/monerod.rs +++ b/monero-rpc/src/monerod.rs @@ -229,7 +229,7 @@ mod byte_array { phantom: PhantomData<(T, [u8; N])>, } - impl<'de, T, const N: usize> serde::de::Visitor<'de> for Visitor + impl serde::de::Visitor<'_> for Visitor where T: TryFrom<[u8; N]>, { diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs index cf4cc35f..9d4df588 100644 --- a/monero-sys/src/lib.rs +++ b/monero-sys/src/lib.rs @@ -615,7 +615,7 @@ impl WalletHandle { destination_address: &monero::Address, expected_amount: monero::Amount, confirmations: u64, - listener: Option, + listener: Option, ) -> anyhow::Result<()> { tracing::info!(%txid, %destination_address, amount=%expected_amount, %confirmations, "Waiting until transaction is confirmed"); @@ -659,7 +659,7 @@ impl WalletHandle { // If the listener exists, notify it of the result if let Some(listener) = &listener { - listener(tx_status.confirmations); + listener((tx_status.confirmations, confirmations)); } // Stop when we have the required number of confirmations diff --git a/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx b/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx index 1f48960c..17cfdee4 100644 --- a/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx +++ b/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx @@ -239,7 +239,7 @@ export default function SwapStatusAlert({ swap: GetSwapInfoResponseExt; isRunning: boolean; onlyShowIfUnusualAmountOfTimeHasPassed?: boolean; -}): JSX.Element | null { +}) { // If the swap is completed, we do not need to display anything if (!isGetSwapInfoResponseRunningSwap(swap)) { return null; diff --git a/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx b/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx index 957798ba..4fd3d297 100644 --- a/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx +++ b/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx @@ -83,7 +83,8 @@ function getActiveStep(state: SwapState | null): PathStep | null { // Step 3: Waiting for XMR redemption // Bitcoin has been redeemed by Alice, now waiting for us to redeem Monero - case "BtcRedeemed": + case "WaitingForXmrConfirmationsBeforeRedeem": + case "RedeemingMonero": return [PathType.HAPPY_PATH, 3, isReleased]; // Step 4: Swap completed successfully @@ -162,9 +163,9 @@ function SwapStepper({ const HAPPY_PATH_STEP_LABELS = [ { label: "Locking your BTC", duration: "~12min" }, - { label: "They lock their XMR", duration: "~18min" }, + { label: "They lock their XMR", duration: "~10min" }, { label: "They redeem the BTC", duration: "~2min" }, - { label: "Redeeming your XMR", duration: "~2min" }, + { label: "Redeeming your XMR", duration: "~10min" }, ]; const UNHAPPY_PATH_STEP_LABELS = [ diff --git a/src-gui/src/renderer/components/modal/swap/TransactionInfoBox.tsx b/src-gui/src/renderer/components/modal/swap/TransactionInfoBox.tsx index cd396ee2..f23f478c 100644 --- a/src-gui/src/renderer/components/modal/swap/TransactionInfoBox.tsx +++ b/src-gui/src/renderer/components/modal/swap/TransactionInfoBox.tsx @@ -9,7 +9,7 @@ export type TransactionInfoBoxProps = { explorerUrlCreator: ((txId: string) => string) | null; additionalContent: ReactNode; loading: boolean; - icon: JSX.Element; + icon: ReactNode; }; export default function TransactionInfoBox({ diff --git a/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx b/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx index c73b32dd..62ea974f 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx @@ -12,11 +12,12 @@ import XmrRedeemInMempoolPage from "./done/XmrRedeemInMempoolPage"; import ProcessExitedPage from "./exited/ProcessExitedPage"; import BitcoinCancelledPage from "./in_progress/BitcoinCancelledPage"; import BitcoinLockTxInMempoolPage from "./in_progress/BitcoinLockTxInMempoolPage"; -import BitcoinRedeemedPage from "./in_progress/BitcoinRedeemedPage"; +import RedeemingMoneroPage from "./in_progress/RedeemingMoneroPage"; import CancelTimelockExpiredPage from "./in_progress/CancelTimelockExpiredPage"; import EncryptedSignatureSentPage from "./in_progress/EncryptedSignatureSentPage"; import ReceivedQuotePage from "./in_progress/ReceivedQuotePage"; import SwapSetupInflightPage from "./in_progress/SwapSetupInflightPage"; +import WaitingForXmrConfirmationsBeforeRedeemPage from "./in_progress/WaitingForXmrConfirmationsBeforeRedeemPage"; import XmrLockedPage from "./in_progress/XmrLockedPage"; import XmrLockTxInMempoolPage from "./in_progress/XmrLockInMempoolPage"; import InitPage from "./init/InitPage"; @@ -62,8 +63,15 @@ export default function SwapStatePage({ state }: { state: SwapState | null }) { return ; case "EncryptedSignatureSent": return ; - case "BtcRedeemed": - return ; + case "RedeemingMonero": + return ; + case "WaitingForXmrConfirmationsBeforeRedeem": + if (state.curr.type === "WaitingForXmrConfirmationsBeforeRedeem") { + return ( + + ); + } + break; case "XmrRedeemInMempool": if (state.curr.type === "XmrRedeemInMempool") { return ; diff --git a/src-gui/src/renderer/components/modal/swap/pages/in_progress/BitcoinRedeemedPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/in_progress/BitcoinRedeemedPage.tsx deleted file mode 100644 index 863f82b8..00000000 --- a/src-gui/src/renderer/components/modal/swap/pages/in_progress/BitcoinRedeemedPage.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle"; - -export default function BitcoinRedeemedPage() { - return ; -} diff --git a/src-gui/src/renderer/components/modal/swap/pages/in_progress/RedeemingMoneroPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/in_progress/RedeemingMoneroPage.tsx new file mode 100644 index 00000000..d9b9b5db --- /dev/null +++ b/src-gui/src/renderer/components/modal/swap/pages/in_progress/RedeemingMoneroPage.tsx @@ -0,0 +1,7 @@ +import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle"; + +export default function RedeemingMoneroPage() { + return ( + + ); +} 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 3a315908..d561cec2 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 @@ -481,7 +481,8 @@ const MoneroSecondaryContent = ({ // Arrow animation styling extracted for reuse const arrowSx = { fontSize: "3rem", - color: (theme: any) => theme.palette.primary.main, + color: (theme: { palette: { primary: { main: string } } }) => + theme.palette.primary.main, animation: "slideArrow 2s infinite", "@keyframes slideArrow": { "0%": { diff --git a/src-gui/src/renderer/components/modal/swap/pages/in_progress/WaitingForXmrConfirmationsBeforeRedeemPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/in_progress/WaitingForXmrConfirmationsBeforeRedeemPage.tsx new file mode 100644 index 00000000..baac46e5 --- /dev/null +++ b/src-gui/src/renderer/components/modal/swap/pages/in_progress/WaitingForXmrConfirmationsBeforeRedeemPage.tsx @@ -0,0 +1,29 @@ +import { Box, DialogContentText } from "@mui/material"; +import { TauriSwapProgressEventContent } from "models/tauriModelExt"; +import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox"; + +export default function WaitingForXmrConfirmationsBeforeRedeemPage({ + xmr_lock_txid, + xmr_lock_tx_confirmations, + xmr_lock_tx_target_confirmations, +}: TauriSwapProgressEventContent<"WaitingForXmrConfirmationsBeforeRedeem">) { + return ( + + + We are waiting for the Monero lock transaction to receive enough + confirmations before we can sweep them to your address. + + + + + ); +} diff --git a/src-gui/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx index de8f695e..1c04e2de 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/in_progress/XmrLockInMempoolPage.tsx @@ -6,8 +6,9 @@ import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox"; export default function XmrLockTxInMempoolPage({ xmr_lock_tx_confirmations, xmr_lock_txid, + xmr_lock_tx_target_confirmations, }: TauriSwapProgressEventContent<"XmrLockTxInMempool">) { - const additionalContent = `Confirmations: ${formatConfirmations(xmr_lock_tx_confirmations, 10)}`; + const additionalContent = `Confirmations: ${formatConfirmations(xmr_lock_tx_confirmations, xmr_lock_tx_target_confirmations)}`; return ( diff --git a/src-gui/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx b/src-gui/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx index 1c3a0636..1f0205ac 100644 --- a/src-gui/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx +++ b/src-gui/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx @@ -1,10 +1,11 @@ +import React from "react"; import { Badge } from "@mui/material"; import { useResumeableSwapsCountExcludingPunished } from "store/hooks"; export default function UnfinishedSwapsBadge({ children, }: { - children: JSX.Element; + children: React.ReactNode; }) { const resumableSwapsCount = useResumeableSwapsCountExcludingPunished(); diff --git a/src-gui/src/renderer/components/other/RenderedCliLog.tsx b/src-gui/src/renderer/components/other/RenderedCliLog.tsx index 0dc26dd0..897e5ce2 100644 --- a/src-gui/src/renderer/components/other/RenderedCliLog.tsx +++ b/src-gui/src/renderer/components/other/RenderedCliLog.tsx @@ -16,7 +16,7 @@ function RenderedCliLog({ log }: { log: CliLog }) { }; return ( - + )} - {fields.message} + {fields.message} {Object.entries(fields).map(([key, value]) => { if (key !== "message") { return ( @@ -62,10 +62,12 @@ export default function CliLogsBox({ label, logs, topRightButton = null, + autoScroll = false, }: { label: string; logs: (CliLog | string)[]; topRightButton?: ReactNode; + autoScroll?: boolean; }) { const [searchQuery, setSearchQuery] = useState(""); @@ -85,6 +87,7 @@ export default function CliLogsBox({ searchQuery={searchQuery} setSearchQuery={setSearchQuery} topRightButton={topRightButton} + autoScroll={autoScroll} rows={memoizedLogs.map((log) => typeof log === "string" ? ( diff --git a/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx b/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx index 98fb2356..76d05566 100644 --- a/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx +++ b/src-gui/src/renderer/components/other/ScrollablePaperTextBox.tsx @@ -16,6 +16,7 @@ export default function ScrollablePaperTextBox({ setSearchQuery = null, topRightButton = null, minHeight = MIN_HEIGHT, + autoScroll = false, }: { rows: ReactNode[]; title: string; @@ -24,6 +25,7 @@ export default function ScrollablePaperTextBox({ setSearchQuery?: ((query: string) => void) | null; minHeight?: string; topRightButton?: ReactNode | null; + autoScroll?: boolean; }) { const virtuaEl = useRef(null); @@ -39,6 +41,12 @@ export default function ScrollablePaperTextBox({ virtuaEl.current?.scrollToIndex(0); } + useEffect(() => { + if (autoScroll) { + scrollToBottom(); + } + }, [rows.length, autoScroll]); + return ( , + #[typeshare(serialized_as = "number")] + xmr_lock_tx_target_confirmations: u64, }, XmrLocked, EncryptedSignatureSent, - BtcRedeemed, + 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")] xmr_redeem_txids: Vec, diff --git a/swap/src/env.rs b/swap/src/env.rs index 0de5f275..a80d65a2 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -18,6 +18,8 @@ pub struct Config { pub monero_finality_confirmations: u64, // If Alice does manage to lock her Monero within this timeout, she will initiate an early refund of the Bitcoin. pub monero_lock_retry_timeout: Duration, + // After this many confirmations we assume that the Monero transaction is safe from double spending + pub monero_double_spend_safe_confirmations: u64, #[serde(with = "monero_network")] pub monero_network: monero::Network, } @@ -60,6 +62,7 @@ impl GetConfig for Mainnet { // she will initiate an early refund of Bobs Bitcoin monero_lock_retry_timeout: 10.std_minutes(), monero_finality_confirmations: 10, + monero_double_spend_safe_confirmations: 2, monero_network: monero::Network::Mainnet, } } @@ -78,6 +81,7 @@ impl GetConfig for Testnet { monero_avg_block_time: 2.std_minutes(), monero_lock_retry_timeout: 10.std_minutes(), monero_finality_confirmations: 10, + monero_double_spend_safe_confirmations: 2, monero_network: monero::Network::Stagenet, } } @@ -96,6 +100,7 @@ impl GetConfig for Regtest { monero_avg_block_time: 1.std_seconds(), monero_lock_retry_timeout: 1.std_minutes(), monero_finality_confirmations: 10, + monero_double_spend_safe_confirmations: 2, monero_network: monero::Network::Mainnet, // yes this is strange } } diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index 39fb4d62..f621d1ec 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -183,7 +183,7 @@ impl Wallets { pub async fn wait_until_confirmed( &self, watch_request: WatchRequest, - listener: Option, + listener: Option, ) -> Result<()> { let wallet = self.main_wallet().await; diff --git a/swap/src/protocol/alice/state.rs b/swap/src/protocol/alice/state.rs index bfd1844c..b53f5a8b 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap/src/protocol/alice/state.rs @@ -576,8 +576,12 @@ impl State3 { monero_wallet .wait_until_confirmed( self.lock_xmr_watch_request(transfer_proof_2, 10), - Some(move |confirmations| { - tracing::debug!(%confirmations, "Monero lock transaction confirmed"); + Some(move |(confirmations, target_confirmations)| { + tracing::debug!( + %confirmations, + %target_confirmations, + "Monero lock transaction got a confirmation" + ); }), ) .await diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index c4b0821f..cfc47b6f 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -304,8 +304,12 @@ where monero_wallet .wait_until_confirmed( state3.lock_xmr_watch_request(transfer_proof.clone(), 1), - Some(|confirmations| { - tracing::debug!(%confirmations, "Monero lock tx got new confirmation") + Some(|(confirmations, target_confirmations)| { + tracing::debug!( + %confirmations, + %target_confirmations, + "Monero lock tx got new confirmation" + ) }), ) .await diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 30438298..d0ff2211 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -446,7 +446,11 @@ pub struct State3 { } impl State3 { - pub fn lock_xmr_watch_request(&self, transfer_proof: TransferProof) -> WatchRequest { + pub fn lock_xmr_watch_request( + &self, + transfer_proof: TransferProof, + confirmation_target: u64, + ) -> WatchRequest { let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b)); let S = self.S_a_monero + S_b_monero; @@ -455,7 +459,7 @@ impl State3 { public_spend_key: S, public_view_key: self.v.public(), transfer_proof, - confirmation_target: self.min_monero_confirmations, + confirmation_target, expected_amount: self.xmr.into(), } } @@ -471,6 +475,7 @@ impl State3 { s_b: self.s_b, S_a_bitcoin: self.S_a_bitcoin, v: self.v, + xmr: self.xmr, cancel_timelock: self.cancel_timelock, punish_timelock: self.punish_timelock, refund_address: self.refund_address, @@ -501,6 +506,7 @@ impl State3 { tx_refund_encsig: self.tx_refund_encsig.clone(), tx_refund_fee: self.tx_refund_fee, tx_cancel_fee: self.tx_cancel_fee, + xmr: self.xmr, } } @@ -531,22 +537,6 @@ impl State3 { )) } - pub fn attempt_cooperative_redeem( - &self, - s_a: monero::PrivateKey, - monero_wallet_restore_blockheight: BlockHeight, - lock_transfer_proof: TransferProof, - ) -> State5 { - State5 { - s_a, - s_b: self.s_b, - v: self.v, - tx_lock: self.tx_lock.clone(), - monero_wallet_restore_blockheight, - lock_transfer_proof, - } - } - pub fn construct_tx_early_refund(&self) -> bitcoin::TxEarlyRefund { bitcoin::TxEarlyRefund::new(&self.tx_lock, &self.refund_address, self.tx_refund_fee) } @@ -572,6 +562,7 @@ pub struct State4 { s_b: monero::Scalar, S_a_bitcoin: bitcoin::PublicKey, v: monero::PrivateViewKey, + xmr: monero::Amount, pub cancel_timelock: CancelTimelock, punish_timelock: PunishTimelock, #[serde(with = "address_serde")] @@ -612,6 +603,7 @@ impl State4 { s_a, s_b: self.s_b, v: self.v, + xmr: self.xmr, tx_lock: self.tx_lock.clone(), monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight, lock_transfer_proof: self.lock_transfer_proof.clone(), @@ -682,6 +674,7 @@ impl State4 { tx_refund_encsig: self.tx_refund_encsig, tx_refund_fee: self.tx_refund_fee, tx_cancel_fee: self.tx_cancel_fee, + xmr: self.xmr, } } @@ -696,6 +689,7 @@ pub struct State5 { s_a: monero::PrivateKey, s_b: monero::Scalar, v: monero::PrivateViewKey, + xmr: monero::Amount, tx_lock: bitcoin::TxLock, pub monero_wallet_restore_blockheight: BlockHeight, pub lock_transfer_proof: TransferProof, @@ -713,6 +707,23 @@ impl State5 { self.tx_lock.txid() } + pub fn lock_xmr_watch_request_for_sweep(&self) -> monero::wallet::WatchRequest { + let S_b_monero = + monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b)); + let S_a_monero = monero::PublicKey::from_private_key(&self.s_a); + let S = S_a_monero + S_b_monero; + + monero::wallet::WatchRequest { + public_spend_key: S, + public_view_key: self.v.public(), + transfer_proof: self.lock_transfer_proof.clone(), + // To sweep the funds we need 10 full confirmations because + // Monero requires 10 on an UTXO before it can be spent. + confirmation_target: 10, + expected_amount: self.xmr.into(), + } + } + pub async fn redeem_xmr( &self, monero_wallet: &monero::Wallets, @@ -767,6 +778,7 @@ pub struct State6 { b: bitcoin::SecretKey, s_b: monero::Scalar, v: monero::PrivateViewKey, + pub xmr: monero::Amount, pub monero_wallet_restore_blockheight: BlockHeight, pub cancel_timelock: CancelTimelock, punish_timelock: PunishTimelock, @@ -885,15 +897,19 @@ impl State6 { pub fn tx_lock_id(&self) -> bitcoin::Txid { self.tx_lock.txid() } + pub fn attempt_cooperative_redeem( &self, - s_a: monero::PrivateKey, + s_a: monero::Scalar, lock_transfer_proof: TransferProof, ) -> State5 { + let s_a = monero::PrivateKey::from_scalar(s_a); + State5 { s_a, s_b: self.s_b, v: self.v, + xmr: self.xmr, tx_lock: self.tx_lock.clone(), monero_wallet_restore_blockheight: self.monero_wallet_restore_blockheight, lock_transfer_proof, diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index bd67813f..bb07f8be 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -11,7 +11,7 @@ use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, use crate::network::swap_setup::bob::NewSwap; use crate::protocol::bob::state::*; use crate::protocol::{bob, Database}; -use crate::{bitcoin, monero}; +use crate::{bitcoin, env, monero}; use anyhow::{bail, Context as AnyContext, Result}; use std::sync::Arc; use std::time::Duration; @@ -78,6 +78,7 @@ pub async fn run_until( swap.monero_wallet.clone(), swap.monero_receive_pool.clone(), swap.event_emitter.clone(), + swap.env_config, ) .await?; @@ -105,6 +106,7 @@ async fn next_state( monero_wallet: Arc, monero_receive_pool: MoneroAddressPool, event_emitter: Option, + env_config: env::Config, ) -> Result { tracing::debug!(%state, "Advancing state"); @@ -358,6 +360,7 @@ async fn next_state( TauriSwapProgressEvent::XmrLockTxInMempool { xmr_lock_txid: lock_transfer_proof.tx_hash(), xmr_lock_tx_confirmations: None, + xmr_lock_tx_target_confirmations: env_config.monero_double_spend_safe_confirmations, }, ); @@ -381,29 +384,34 @@ async fn next_state( ); // Clone these so that we can move them into the listener closure - let transfer_proof_clone = lock_transfer_proof.clone(); - let watch_request = state.lock_xmr_watch_request(lock_transfer_proof.clone()); + let lock_transfer_proof_clone = lock_transfer_proof.clone(); + let lock_transfer_proof_clone_for_state = lock_transfer_proof.clone(); + let watch_request = state.lock_xmr_watch_request( + lock_transfer_proof, + env_config.monero_double_spend_safe_confirmations, + ); let watch_future = monero_wallet.wait_until_confirmed( watch_request, - Some(move |confirmations| { + Some(move |(confirmations, target_confirmations)| { // Emit an event to notify about the new confirmation event_emitter.emit_swap_progress_event( swap_id, TauriSwapProgressEvent::XmrLockTxInMempool { - xmr_lock_txid: lock_transfer_proof.clone().tx_hash(), + xmr_lock_txid: lock_transfer_proof_clone.tx_hash(), xmr_lock_tx_confirmations: Some(confirmations), + xmr_lock_tx_target_confirmations: target_confirmations, }, ); }), ); select! { - // Wait for the Monero lock transaction to be fully confirmed + // Wait for the Monero lock transaction to be confirmed with only 2 confirmations (early reveal) received_xmr = watch_future => { match received_xmr { Ok(()) => - BobState::XmrLocked(state.xmr_locked(monero_wallet_restore_blockheight, transfer_proof_clone.clone())), + BobState::XmrLocked(state.xmr_locked(monero_wallet_restore_blockheight, lock_transfer_proof_clone_for_state)), Err(err) if err.to_string().contains("amount mismatch") => { // Alice locked insufficient Monero tracing::warn!(%err, "Insufficient Monero have been locked!"); @@ -534,10 +542,40 @@ async fn next_state( } } BobState::BtcRedeemed(state) => { - event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::BtcRedeemed); + // Now we wait for the full 10 confirmations on the Monero lock transaction + // because we simply cannot spend it if we don't have 10 confirmations + let watch_request = state.lock_xmr_watch_request_for_sweep(); + + // Clone these for the closure + let event_emitter_clone = event_emitter.clone(); + let transfer_proof_hash = state.lock_transfer_proof.tx_hash(); + + let watch_future = monero_wallet.wait_until_confirmed( + watch_request, + Some( + move |(xmr_lock_tx_confirmations, xmr_lock_tx_target_confirmations)| { + event_emitter_clone.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::WaitingForXmrConfirmationsBeforeRedeem { + xmr_lock_txid: transfer_proof_hash.clone(), + xmr_lock_tx_confirmations, + xmr_lock_tx_target_confirmations, + }, + ); + }, + ), + ); + + // Wait for the 10 confirmations to complete + watch_future + .await + .map_err(|e| anyhow::anyhow!("Failed to wait for XMR confirmations: {}", e))?; + + event_emitter + .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::RedeemingMonero); let xmr_redeem_txids = retry( - "Refund Monero", + "Redeeming Monero", || async { state .redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone()) @@ -719,7 +757,6 @@ async fn next_state( } BobState::BtcPunished { state, tx_lock_id } => { tracing::info!("You have been punished for not refunding in time"); - event_emitter.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::BtcPunished); event_emitter.emit_swap_progress_event( swap_id, @@ -739,15 +776,43 @@ 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, lock_transfer_proof); + let watch_request = state5.lock_xmr_watch_request_for_sweep(); + let event_emitter_clone = event_emitter.clone(); + let state5_clone = state5.clone(); + + // Wait for XMR confirmations before redeeming + monero_wallet + .wait_until_confirmed( + watch_request, + Some( + move |( + xmr_lock_tx_confirmations, + xmr_lock_tx_target_confirmations, + )| { + let event_emitter = event_emitter_clone.clone(); + let tx_hash = state5_clone.lock_transfer_proof.tx_hash(); + + event_emitter.emit_swap_progress_event( + swap_id, + TauriSwapProgressEvent::WaitingForXmrConfirmationsBeforeRedeem { + xmr_lock_txid: tx_hash, + xmr_lock_tx_confirmations, + xmr_lock_tx_target_confirmations, + }, + ); + }, + ), + ) + .await + .map_err(|e| { + anyhow::anyhow!( + "Failed to wait for XMR confirmations during cooperative redeem: {}", + e + ) + })?; + match retry( "Redeeming Monero", || async {