mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-19 07:32:31 -05:00
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:
parent
cc4069ebad
commit
9a04bd5682
22 changed files with 427 additions and 63 deletions
|
|
@ -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]>,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
||||
|
||||
export default function BitcoinRedeemedPage() {
|
||||
return <CircularProgressWithSubtitle description="Redeeming your Monero" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
||||
|
||||
export default function RedeemingMoneroPage() {
|
||||
return (
|
||||
<CircularProgressWithSubtitle description="Preparing to redeem your Monero" />
|
||||
);
|
||||
}
|
||||
|
|
@ -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%": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,14 +776,42 @@ async fn next_state(
|
|||
"Alice has accepted our request to cooperatively redeem the XMR"
|
||||
);
|
||||
|
||||
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::CooperativeRedeemAccepted,
|
||||
TauriSwapProgressEvent::WaitingForXmrConfirmationsBeforeRedeem {
|
||||
xmr_lock_txid: tx_hash,
|
||||
xmr_lock_tx_confirmations,
|
||||
xmr_lock_tx_target_confirmations,
|
||||
},
|
||||
);
|
||||
|
||||
let s_a = monero::PrivateKey { scalar: s_a };
|
||||
|
||||
let state5 = state.attempt_cooperative_redeem(s_a, lock_transfer_proof);
|
||||
},
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"Failed to wait for XMR confirmations during cooperative redeem: {}",
|
||||
e
|
||||
)
|
||||
})?;
|
||||
|
||||
match retry(
|
||||
"Redeeming Monero",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue