mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-17 17:44:02 -05:00
feat(gui): Approve dialog before publishing Bitcoin lock transaction (#291)
This diff introduces a new "approvals" mechanism that alters the swap flow by requiring explicit user intervention before the Bitcoin lock transaction is broadcast. Previously, the Bitcoin lock was executed automatically without any user prompt. Now, the backend defines `ApprovalRequestType` (e.g. a `PreBtcLock` variant with details like `btc_lock_amount`, `btc_network_fee`, and `xmr_receive_amount`) and `ApprovalEvent` (with statuses such as `Pending`, `Resolved`, and `Rejected`). The method `request_approval()` in the `TauriHandle` struct uses a oneshot channel and concurrent timeout handling via `tokio::select!` to wait for the user's decision. Based on the outcome—explicit approval or timeout/rejection—the approval event is emitted through the `emit_approval()` helper, thereby gating the subsequent broadcast of the Bitcoin lock transaction. On the UI side, changes have been made to reflect the new flow; the modal (for example, in `SwapSetupInflightPage.tsx`) now displays the swap details along with explicit action buttons that call `resolveApproval()` via RPC when clicked. The Redux store, selectors, and hooks like `usePendingPreBtcLockApproval()` have been updated to track and display these approval events. As a result, the overall functionality now requires the user to explicitly approve the swap offer before proceeding, ensuring they are aware of the swap's key parameters and that the locking of funds occurs only after their confirmation.
This commit is contained in:
parent
ab5f93ff44
commit
9ddf2daafe
20 changed files with 613 additions and 65 deletions
|
|
@ -26,9 +26,7 @@ export default function RemainingFundsWillBeUsedAlert() {
|
|||
variant="filled"
|
||||
>
|
||||
The remaining funds of <SatsAmount amount={balance} /> in the wallet
|
||||
will be used for the next swap. If the remaining funds exceed the
|
||||
minimum swap amount of the maker, a swap will be initiated
|
||||
instantaneously.
|
||||
will be used for the next swap
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@ import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
|||
|
||||
export default function ReceivedQuotePage() {
|
||||
return (
|
||||
<CircularProgressWithSubtitle description="Processing received quote" />
|
||||
<CircularProgressWithSubtitle description="Syncing local wallet" />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,152 @@
|
|||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||
import { SatsAmount } from "renderer/components/other/Units";
|
||||
import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { resolveApproval } from 'renderer/rpc';
|
||||
import { PendingLockBitcoinApprovalRequest, TauriSwapProgressEventContent } from 'models/tauriModelExt';
|
||||
import {
|
||||
SatsAmount,
|
||||
PiconeroAmount,
|
||||
MoneroBitcoinExchangeRateFromAmounts
|
||||
} from 'renderer/components/other/Units';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Divider,
|
||||
} from '@material-ui/core';
|
||||
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
|
||||
import { useActiveSwapId, usePendingLockBitcoinApproval } from 'store/hooks';
|
||||
import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton';
|
||||
import InfoBox from 'renderer/components/modal/swap/InfoBox';
|
||||
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
|
||||
import CheckIcon from '@material-ui/icons/Check';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
detailGrid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'auto 1fr',
|
||||
rowGap: theme.spacing(1),
|
||||
columnGap: theme.spacing(2),
|
||||
alignItems: 'center',
|
||||
marginBlock: theme.spacing(2),
|
||||
},
|
||||
label: {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
receiveValue: {
|
||||
fontWeight: 'bold',
|
||||
color: theme.palette.success.main,
|
||||
},
|
||||
actions: {
|
||||
marginTop: theme.spacing(2),
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: theme.spacing(2),
|
||||
},
|
||||
cancelButton: {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
/// A hook that returns the LockBitcoin confirmation request for the active swap
|
||||
/// Returns null if no confirmation request is found
|
||||
function useActiveLockBitcoinApprovalRequest(): PendingLockBitcoinApprovalRequest | null {
|
||||
const approvals = usePendingLockBitcoinApproval();
|
||||
const activeSwapId = useActiveSwapId();
|
||||
|
||||
return approvals
|
||||
?.find(r => r.content.details.content.swap_id === activeSwapId) || null;
|
||||
}
|
||||
|
||||
export default function SwapSetupInflightPage({
|
||||
btc_lock_amount,
|
||||
btc_tx_lock_fee,
|
||||
}: TauriSwapProgressEventContent<"SwapSetupInflight">) {
|
||||
}: TauriSwapProgressEventContent<'SwapSetupInflight'>) {
|
||||
const classes = useStyles();
|
||||
const request = useActiveLockBitcoinApprovalRequest();
|
||||
|
||||
const [timeLeft, setTimeLeft] = useState<number>(0);
|
||||
|
||||
const expiresAtMs = request?.content.expiration_ts * 1000 || 0;
|
||||
|
||||
useEffect(() => {
|
||||
const tick = () => {
|
||||
const remainingMs = Math.max(expiresAtMs - Date.now(), 0);
|
||||
setTimeLeft(Math.ceil(remainingMs / 1000));
|
||||
};
|
||||
|
||||
tick();
|
||||
const id = setInterval(tick, 250);
|
||||
return () => clearInterval(id);
|
||||
}, [expiresAtMs]);
|
||||
|
||||
// If we do not have an approval request yet for the Bitcoin lock transaction, we haven't received the offer from Alice yet
|
||||
// Display a loading spinner to the user for as long as the swap_setup request is in flight
|
||||
if (!request) {
|
||||
return <CircularProgressWithSubtitle description={<>Negotiating offer for <SatsAmount amount={btc_lock_amount} /></>} />;
|
||||
}
|
||||
|
||||
const { btc_network_fee, xmr_receive_amount } = request.content.details.content;
|
||||
|
||||
return (
|
||||
<CircularProgressWithSubtitle
|
||||
description={
|
||||
<InfoBox
|
||||
title="Approve Swap"
|
||||
icon={<></>}
|
||||
loading={false}
|
||||
mainContent={
|
||||
<>
|
||||
Starting swap with maker to lock <SatsAmount amount={btc_lock_amount} />
|
||||
<Divider />
|
||||
<Box className={classes.detailGrid}>
|
||||
<Typography className={classes.label}>You send</Typography>
|
||||
<Typography>
|
||||
<SatsAmount amount={btc_lock_amount} />
|
||||
</Typography>
|
||||
|
||||
<Typography className={classes.label}>Bitcoin network fees</Typography>
|
||||
<Typography>
|
||||
<SatsAmount amount={btc_network_fee} />
|
||||
</Typography>
|
||||
|
||||
<Typography className={classes.label}>You receive</Typography>
|
||||
<Typography className={classes.receiveValue}>
|
||||
<PiconeroAmount amount={xmr_receive_amount} />
|
||||
</Typography>
|
||||
|
||||
<Typography className={classes.label}>Exchange rate</Typography>
|
||||
<Typography>
|
||||
<MoneroBitcoinExchangeRateFromAmounts
|
||||
satsAmount={btc_lock_amount}
|
||||
piconeroAmount={xmr_receive_amount}
|
||||
displayMarkup
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
}
|
||||
additionalContent={
|
||||
<Box className={classes.actions}>
|
||||
<PromiseInvokeButton
|
||||
variant="text"
|
||||
size="large"
|
||||
className={classes.cancelButton}
|
||||
onInvoke={() => resolveApproval(request.content.request_id, false)}
|
||||
displayErrorSnackbar
|
||||
requiresContext
|
||||
>
|
||||
Deny
|
||||
</PromiseInvokeButton>
|
||||
|
||||
<PromiseInvokeButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
onInvoke={() => resolveApproval(request.content.request_id, true)}
|
||||
displayErrorSnackbar
|
||||
requiresContext
|
||||
endIcon={<CheckIcon />}
|
||||
>
|
||||
{`Confirm & lock BTC (${timeLeft}s)`}
|
||||
</PromiseInvokeButton>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ export default function InitPage() {
|
|||
onInvoke={init}
|
||||
displayErrorSnackbar
|
||||
>
|
||||
Request quote and start swap
|
||||
Begin swap
|
||||
</PromiseInvokeButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue