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>
This commit is contained in:
Mohan 2025-06-30 16:42:53 +02:00 committed by GitHub
parent cc4069ebad
commit 9a04bd5682
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 427 additions and 63 deletions

View file

@ -229,7 +229,7 @@ mod byte_array {
phantom: PhantomData<(T, [u8; N])>,
}
impl<'de, T, const N: usize> serde::de::Visitor<'de> for Visitor<T, N>
impl<T, const N: usize> serde::de::Visitor<'_> for Visitor<T, N>
where
T: TryFrom<[u8; N]>,
{

View file

@ -615,7 +615,7 @@ impl WalletHandle {
destination_address: &monero::Address,
expected_amount: monero::Amount,
confirmations: u64,
listener: Option<impl Fn(u64) + Send + 'static>,
listener: Option<impl Fn((u64, u64)) + Send + 'static>,
) -> 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

View file

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

View file

@ -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 = [

View file

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

View file

@ -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 <XmrLockedPage />;
case "EncryptedSignatureSent":
return <EncryptedSignatureSentPage />;
case "BtcRedeemed":
return <BitcoinRedeemedPage />;
case "RedeemingMonero":
return <RedeemingMoneroPage />;
case "WaitingForXmrConfirmationsBeforeRedeem":
if (state.curr.type === "WaitingForXmrConfirmationsBeforeRedeem") {
return (
<WaitingForXmrConfirmationsBeforeRedeemPage {...state.curr.content} />
);
}
break;
case "XmrRedeemInMempool":
if (state.curr.type === "XmrRedeemInMempool") {
return <XmrRedeemInMempoolPage {...state.curr.content} />;

View file

@ -1,5 +0,0 @@
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function BitcoinRedeemedPage() {
return <CircularProgressWithSubtitle description="Redeeming your Monero" />;
}

View file

@ -0,0 +1,7 @@
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function RedeemingMoneroPage() {
return (
<CircularProgressWithSubtitle description="Preparing to redeem your Monero" />
);
}

View file

@ -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%": {

View file

@ -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 (
<Box>
<DialogContentText>
We are waiting for the Monero lock transaction to receive enough
confirmations before we can sweep them to your address.
</DialogContentText>
<MoneroTransactionInfoBox
title="Monero Lock Transaction"
txId={xmr_lock_txid}
additionalContent={
additionalContent={
`Confirmations: ${xmr_lock_tx_confirmations}/${xmr_lock_tx_target_confirmations}`
}
}
loading
/>
</Box>
);
}

View file

@ -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 (
<Box>

View file

@ -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();

View file

@ -16,7 +16,7 @@ function RenderedCliLog({ log }: { log: CliLog }) {
};
return (
<Box>
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Box
style={{
display: "flex",
@ -33,7 +33,6 @@ function RenderedCliLog({ log }: { log: CliLog }) {
<Chip label={target.split("::")[0]} size="small" variant="outlined" />
)}
<Chip label={timestamp} size="small" variant="outlined" />
<Typography variant="subtitle2">{fields.message}</Typography>
</Box>
<Box
sx={{
@ -43,6 +42,7 @@ function RenderedCliLog({ log }: { log: CliLog }) {
flexDirection: "column",
}}
>
<Typography variant="subtitle2">{fields.message}</Typography>
{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<string>("");
@ -85,6 +87,7 @@ export default function CliLogsBox({
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
topRightButton={topRightButton}
autoScroll={autoScroll}
rows={memoizedLogs.map((log) =>
typeof log === "string" ? (
<Typography key={log} component="pre">

View file

@ -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<VListHandle | null>(null);
@ -39,6 +41,12 @@ export default function ScrollablePaperTextBox({
virtuaEl.current?.scrollToIndex(0);
}
useEffect(() => {
if (autoScroll) {
scrollToBottom();
}
}, [rows.length, autoScroll]);
return (
<Paper
variant="outlined"

View file

@ -0,0 +1,206 @@
-- This migration adds the xmr field to Bob's State3, State4, State5, and State6 across all relevant swap states.
-- The xmr value is copied from the earliest SwapSetupCompleted state (State2) within the same swap when available.
-- Bob: Add xmr to State3 inside BtcLocked
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.BtcLocked.state3.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.BtcLocked') IS NOT NULL
AND json_extract(state, '$.Bob.BtcLocked.state3.xmr') IS NULL;
-- Bob: Add xmr to State3 inside XmrLockProofReceived
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.XmrLockProofReceived.state.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.XmrLockProofReceived') IS NOT NULL
AND json_extract(state, '$.Bob.XmrLockProofReceived.state.xmr') IS NULL;
-- Bob: Add xmr to State4 inside XmrLocked
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.XmrLocked.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.XmrLocked') IS NOT NULL
AND json_extract(state, '$.Bob.XmrLocked.xmr') IS NULL;
-- Bob: Add xmr to State4 inside EncSigSent
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.EncSigSent.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.EncSigSent') IS NOT NULL
AND json_extract(state, '$.Bob.EncSigSent.xmr') IS NULL;
-- Bob: Add xmr to State6 inside CancelTimelockExpired
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.CancelTimelockExpired.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.CancelTimelockExpired') IS NOT NULL
AND json_extract(state, '$.Bob.CancelTimelockExpired.xmr') IS NULL;
-- Bob: Add xmr to State6 inside BtcCancelled
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.BtcCancelled.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.BtcCancelled') IS NOT NULL
AND json_extract(state, '$.Bob.BtcCancelled.xmr') IS NULL;
-- Bob: Add xmr to State6 inside BtcRefundPublished
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.BtcRefundPublished.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.BtcRefundPublished') IS NOT NULL
AND json_extract(state, '$.Bob.BtcRefundPublished.xmr') IS NULL;
-- Bob: Add xmr to State6 inside BtcEarlyRefundPublished
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.BtcEarlyRefundPublished.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.BtcEarlyRefundPublished') IS NOT NULL
AND json_extract(state, '$.Bob.BtcEarlyRefundPublished.xmr') IS NULL;
-- Bob: Add xmr to State6 inside BtcRefunded
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.BtcRefunded.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.BtcRefunded') IS NOT NULL
AND json_extract(state, '$.Bob.BtcRefunded.xmr') IS NULL;
-- Bob: Add xmr to State6 inside BtcEarlyRefunded
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.BtcEarlyRefunded.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.BtcEarlyRefunded') IS NOT NULL
AND json_extract(state, '$.Bob.BtcEarlyRefunded.xmr') IS NULL;
-- Bob: Add xmr to State6 inside BtcPunished.state
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.BtcPunished.state.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.BtcPunished') IS NOT NULL
AND json_extract(state, '$.Bob.BtcPunished.state.xmr') IS NULL;
-- Bob: Add xmr to State5 inside BtcRedeemed
UPDATE swap_states SET
state = json_insert(
state,
'$.Bob.BtcRedeemed.xmr',
(
SELECT json_extract(states.state, '$.Bob.SwapSetupCompleted.xmr')
FROM swap_states AS states
WHERE
states.swap_id = swap_states.swap_id
AND json_extract(states.state, '$.Bob.SwapSetupCompleted') IS NOT NULL
LIMIT 1
)
)
WHERE json_extract(state, '$.Bob.BtcRedeemed') IS NOT NULL
AND json_extract(state, '$.Bob.BtcRedeemed.xmr') IS NULL;

View file

@ -624,10 +624,20 @@ pub enum TauriSwapProgressEvent {
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,
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<string>")]
xmr_redeem_txids: Vec<monero::TxHash>,

View file

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

View file

@ -183,7 +183,7 @@ impl Wallets {
pub async fn wait_until_confirmed(
&self,
watch_request: WatchRequest,
listener: Option<impl Fn(u64) + Send + 'static>,
listener: Option<impl Fn((u64, u64)) + Send + 'static>,
) -> Result<()> {
let wallet = self.main_wallet().await;

View file

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

View file

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

View file

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

View file

@ -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::Wallets>,
monero_receive_pool: MoneroAddressPool,
event_emitter: Option<TauriHandle>,
env_config: env::Config,
) -> Result<BobState> {
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 {