feat: cargo project at root

This commit is contained in:
binarybaron 2024-08-08 00:49:04 +02:00
parent aa0c0623ca
commit 709a2820c4
No known key found for this signature in database
GPG key ID: 99B75D3E1476A26E
313 changed files with 1 additions and 740 deletions

View file

@ -0,0 +1,22 @@
import QRCode from 'react-qr-code';
import { Box } from '@material-ui/core';
export default function BitcoinQrCode({ address }: { address: string }) {
return (
<Box
style={{
height: '100%',
margin: '0 auto',
}}
>
<QRCode
value={`bitcoin:${address}`}
size={256}
style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
/* @ts-ignore */
viewBox="0 0 256 256"
/>
</Box>
);
}

View file

@ -0,0 +1,25 @@
import { isTestnet } from 'store/config';
import { getBitcoinTxExplorerUrl } from 'utils/conversionUtils';
import BitcoinIcon from 'renderer/components/icons/BitcoinIcon';
import { ReactNode } from 'react';
import TransactionInfoBox from './TransactionInfoBox';
type Props = {
title: string;
txId: string;
additionalContent: ReactNode;
loading: boolean;
};
export default function BitcoinTransactionInfoBox({ txId, ...props }: Props) {
const explorerUrl = getBitcoinTxExplorerUrl(txId, isTestnet());
return (
<TransactionInfoBox
txId={txId}
explorerUrl={explorerUrl}
icon={<BitcoinIcon />}
{...props}
/>
);
}

View file

@ -0,0 +1,35 @@
import {
Box,
CircularProgress,
makeStyles,
Typography,
} from '@material-ui/core';
import { ReactNode } from 'react';
const useStyles = makeStyles((theme) => ({
subtitle: {
paddingTop: theme.spacing(1),
},
}));
export default function CircularProgressWithSubtitle({
description,
}: {
description: string | ReactNode;
}) {
const classes = useStyles();
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
flexDirection="column"
>
<CircularProgress size={50} />
<Typography variant="subtitle2" className={classes.subtitle}>
{description}
</Typography>
</Box>
);
}

View file

@ -0,0 +1,17 @@
import { Button } from '@material-ui/core';
import { ButtonProps } from '@material-ui/core/Button/Button';
export default function ClipboardIconButton({
text,
...props
}: { text: string } & ButtonProps) {
function writeToClipboard() {
throw new Error('Not implemented');
}
return (
<Button onClick={writeToClipboard} {...props}>
Copy
</Button>
);
}

View file

@ -0,0 +1,53 @@
import { ReactNode } from 'react';
import { Box, Typography } from '@material-ui/core';
import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined';
import InfoBox from './InfoBox';
import ClipboardIconButton from './ClipbiardIconButton';
import BitcoinQrCode from './BitcoinQrCode';
type Props = {
title: string;
address: string;
additionalContent: ReactNode;
icon: ReactNode;
};
export default function DepositAddressInfoBox({
title,
address,
additionalContent,
icon,
}: Props) {
return (
<InfoBox
title={title}
mainContent={<Typography variant="h5">{address}</Typography>}
additionalContent={
<Box>
<Box>
<ClipboardIconButton
text={address}
endIcon={<FileCopyOutlinedIcon />}
color="primary"
variant="contained"
size="medium"
/>
<Box
style={{
display: 'flex',
flexDirection: 'row',
gap: '0.5rem',
alignItems: 'center',
}}
>
<Box>{additionalContent}</Box>
<BitcoinQrCode address={address} />
</Box>
</Box>
</Box>
}
icon={icon}
loading={false}
/>
);
}

View file

@ -0,0 +1,53 @@
import {
Box,
LinearProgress,
makeStyles,
Paper,
Typography,
} from '@material-ui/core';
import { ReactNode } from 'react';
type Props = {
title: ReactNode;
mainContent: ReactNode;
additionalContent: ReactNode;
loading: boolean;
icon: ReactNode;
};
const useStyles = makeStyles((theme) => ({
outer: {
padding: theme.spacing(1.5),
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
},
upperContent: {
display: 'flex',
alignItems: 'center',
gap: theme.spacing(0.5),
},
}));
export default function InfoBox({
title,
mainContent,
additionalContent,
icon,
loading,
}: Props) {
const classes = useStyles();
return (
<Paper variant="outlined" className={classes.outer}>
<Typography variant="subtitle1">{title}</Typography>
<Box className={classes.upperContent}>
{icon}
{mainContent}
</Box>
{loading ? <LinearProgress variant="indeterminate" /> : null}
<Box>{additionalContent}</Box>
</Paper>
);
}

View file

@ -0,0 +1,25 @@
import { isTestnet } from 'store/config';
import { getMoneroTxExplorerUrl } from 'utils/conversionUtils';
import MoneroIcon from 'renderer/components/icons/MoneroIcon';
import { ReactNode } from 'react';
import TransactionInfoBox from './TransactionInfoBox';
type Props = {
title: string;
txId: string;
additionalContent: ReactNode;
loading: boolean;
};
export default function MoneroTransactionInfoBox({ txId, ...props }: Props) {
const explorerUrl = getMoneroTxExplorerUrl(txId, isTestnet());
return (
<TransactionInfoBox
txId={txId}
explorerUrl={explorerUrl}
icon={<MoneroIcon />}
{...props}
/>
);
}

View file

@ -0,0 +1,90 @@
import { useState } from 'react';
import {
Button,
Dialog,
DialogActions,
DialogContent,
makeStyles,
} from '@material-ui/core';
import { useAppDispatch, useAppSelector } from 'store/hooks';
import { swapReset } from 'store/features/swapSlice';
import SwapStatePage from './pages/SwapStatePage';
import SwapStateStepper from './SwapStateStepper';
import SwapSuspendAlert from '../SwapSuspendAlert';
import SwapDialogTitle from './SwapDialogTitle';
import DebugPage from './pages/DebugPage';
const useStyles = makeStyles({
content: {
minHeight: '25rem',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
},
});
export default function SwapDialog({
open,
onClose,
}: {
open: boolean;
onClose: () => void;
}) {
const classes = useStyles();
const swap = useAppSelector((state) => state.swap);
const [debug, setDebug] = useState(false);
const [openSuspendAlert, setOpenSuspendAlert] = useState(false);
const dispatch = useAppDispatch();
function onCancel() {
if (swap.processRunning) {
setOpenSuspendAlert(true);
} else {
onClose();
setTimeout(() => dispatch(swapReset()), 0);
}
}
// This prevents an issue where the Dialog is shown for a split second without a present swap state
if (!open) return null;
return (
<Dialog open={open} onClose={onCancel} maxWidth="md" fullWidth>
<SwapDialogTitle
debug={debug}
setDebug={setDebug}
title="Swap Bitcoin for Monero"
/>
<DialogContent dividers className={classes.content}>
{debug ? (
<DebugPage />
) : (
<>
<SwapStatePage swapState={swap.state} />
<SwapStateStepper />
</>
)}
</DialogContent>
<DialogActions>
<Button onClick={onCancel} variant="text">
Cancel
</Button>
<Button
color="primary"
variant="contained"
onClick={onCancel}
disabled={!(swap.state !== null && !swap.processRunning)}
>
Done
</Button>
</DialogActions>
<SwapSuspendAlert
open={openSuspendAlert}
onClose={() => setOpenSuspendAlert(false)}
/>
</Dialog>
);
}

View file

@ -0,0 +1,45 @@
import {
Box,
DialogTitle,
makeStyles,
Typography,
} from '@material-ui/core';
import TorStatusBadge from './pages/TorStatusBadge';
import FeedbackSubmitBadge from './pages/FeedbackSubmitBadge';
import DebugPageSwitchBadge from './pages/DebugPageSwitchBadge';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
},
rightSide: {
display: 'flex',
alignItems: 'center',
gridGap: theme.spacing(1),
},
}));
export default function SwapDialogTitle({
title,
debug,
setDebug,
}: {
title: string;
debug: boolean;
setDebug: (d: boolean) => void;
}) {
const classes = useStyles();
return (
<DialogTitle disableTypography className={classes.root}>
<Typography variant="h6">{title}</Typography>
<Box className={classes.rightSide}>
<FeedbackSubmitBadge />
<DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} />
<TorStatusBadge />
</Box>
</DialogTitle>
);
}

View file

@ -0,0 +1,166 @@
import { Step, StepLabel, Stepper, Typography } from '@material-ui/core';
import { SwapSpawnType } from 'models/cliModel';
import { SwapStateName } from 'models/rpcModel';
import { useActiveSwapInfo, useAppSelector } from 'store/hooks';
import { exhaustiveGuard } from 'utils/typescriptUtils';
export enum PathType {
HAPPY_PATH = 'happy path',
UNHAPPY_PATH = 'unhappy path',
}
function getActiveStep(
stateName: SwapStateName | null,
processExited: boolean,
): [PathType, number, boolean] {
switch (stateName) {
/// // Happy Path
// Step: 0 (Waiting for Bitcoin lock tx to be published)
case null:
return [PathType.HAPPY_PATH, 0, false];
case SwapStateName.Started:
case SwapStateName.SwapSetupCompleted:
return [PathType.HAPPY_PATH, 0, processExited];
// Step: 1 (Waiting for Bitcoin Lock confirmation and XMR Lock Publication)
// We have locked the Bitcoin and are waiting for the other party to lock their XMR
case SwapStateName.BtcLocked:
return [PathType.HAPPY_PATH, 1, processExited];
// Step: 2 (Waiting for XMR Lock confirmation)
// We have locked the Bitcoin and the other party has locked their XMR
case SwapStateName.XmrLockProofReceived:
return [PathType.HAPPY_PATH, 1, processExited];
// Step: 3 (Sending Encrypted Signature and waiting for Bitcoin Redemption)
// The XMR lock transaction has been confirmed
// We now need to send the encrypted signature to the other party and wait for them to redeem the Bitcoin
case SwapStateName.XmrLocked:
case SwapStateName.EncSigSent:
return [PathType.HAPPY_PATH, 2, processExited];
// Step: 4 (Waiting for XMR Redemption)
case SwapStateName.BtcRedeemed:
return [PathType.HAPPY_PATH, 3, processExited];
// Step: 4 (Completed) (Swap completed, XMR redeemed)
case SwapStateName.XmrRedeemed:
return [PathType.HAPPY_PATH, 4, false];
// Edge Case of Happy Path where the swap is safely aborted. We "fail" at the first step.
case SwapStateName.SafelyAborted:
return [PathType.HAPPY_PATH, 0, true];
// // Unhappy Path
// Step: 1 (Cancelling swap, checking if cancel transaction has been published already by the other party)
case SwapStateName.CancelTimelockExpired:
return [PathType.UNHAPPY_PATH, 0, processExited];
// Step: 2 (Attempt to publish the Bitcoin refund transaction)
case SwapStateName.BtcCancelled:
return [PathType.UNHAPPY_PATH, 1, processExited];
// Step: 2 (Completed) (Bitcoin refunded)
case SwapStateName.BtcRefunded:
return [PathType.UNHAPPY_PATH, 2, false];
// Step: 2 (We failed to publish the Bitcoin refund transaction)
// We failed to publish the Bitcoin refund transaction because the timelock has expired.
// We will be punished. Nothing we can do about it now.
case SwapStateName.BtcPunished:
return [PathType.UNHAPPY_PATH, 1, true];
default:
return exhaustiveGuard(stateName);
}
}
function HappyPathStepper({
activeStep,
error,
}: {
activeStep: number;
error: boolean;
}) {
return (
<Stepper activeStep={activeStep}>
<Step key={0}>
<StepLabel
optional={<Typography variant="caption">~12min</Typography>}
error={error && activeStep === 0}
>
Locking your BTC
</StepLabel>
</Step>
<Step key={1}>
<StepLabel
optional={<Typography variant="caption">~18min</Typography>}
error={error && activeStep === 1}
>
They lock their XMR
</StepLabel>
</Step>
<Step key={2}>
<StepLabel
optional={<Typography variant="caption">~2min</Typography>}
error={error && activeStep === 2}
>
They redeem the BTC
</StepLabel>
</Step>
<Step key={3}>
<StepLabel
optional={<Typography variant="caption">~2min</Typography>}
error={error && activeStep === 3}
>
Redeeming your XMR
</StepLabel>
</Step>
</Stepper>
);
}
function UnhappyPathStepper({
activeStep,
error,
}: {
activeStep: number;
error: boolean;
}) {
return (
<Stepper activeStep={activeStep}>
<Step key={0}>
<StepLabel
optional={<Typography variant="caption">~20min</Typography>}
error={error && activeStep === 0}
>
Cancelling swap
</StepLabel>
</Step>
<Step key={1}>
<StepLabel
optional={<Typography variant="caption">~20min</Typography>}
error={error && activeStep === 1}
>
Refunding your BTC
</StepLabel>
</Step>
</Stepper>
);
}
export default function SwapStateStepper() {
const currentSwapSpawnType = useAppSelector((s) => s.swap.spawnType);
const stateName = useActiveSwapInfo()?.stateName ?? null;
const processExited = useAppSelector((s) => !s.swap.processRunning);
const [pathType, activeStep, error] = getActiveStep(stateName, processExited);
// If the current swap is being manually cancelled and refund, we want to show the unhappy path even though the current state is not a "unhappy" state
if (currentSwapSpawnType === SwapSpawnType.CANCEL_REFUND) {
return <UnhappyPathStepper activeStep={0} error={error} />;
}
if (pathType === PathType.HAPPY_PATH) {
return <HappyPathStepper activeStep={activeStep} error={error} />;
}
return <UnhappyPathStepper activeStep={activeStep} error={error} />;
}

View file

@ -0,0 +1,40 @@
import { Link, Typography } from '@material-ui/core';
import { ReactNode } from 'react';
import InfoBox from './InfoBox';
type TransactionInfoBoxProps = {
title: string;
txId: string;
explorerUrl: string;
additionalContent: ReactNode;
loading: boolean;
icon: JSX.Element;
};
export default function TransactionInfoBox({
title,
txId,
explorerUrl,
additionalContent,
icon,
loading,
}: TransactionInfoBoxProps) {
return (
<InfoBox
title={title}
mainContent={<Typography variant="h5">{txId}</Typography>}
loading={loading}
additionalContent={
<>
<Typography variant="subtitle2">{additionalContent}</Typography>
<Typography variant="body1">
<Link href={explorerUrl} target="_blank">
View on explorer
</Link>
</Typography>
</>
}
icon={icon}
/>
);
}

View file

@ -0,0 +1,36 @@
import { Box, DialogContentText } from "@material-ui/core";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import CliLogsBox from "../../../other/RenderedCliLog";
import JsonTreeView from "../../../other/JSONViewTree";
export default function DebugPage() {
const torStdOut = useAppSelector((s) => s.tor.stdOut);
const logs = useAppSelector((s) => s.swap.logs);
const guiState = useAppSelector((s) => s);
const cliState = useActiveSwapInfo();
return (
<Box>
<DialogContentText>
<Box
style={{
display: "flex",
flexDirection: "column",
gap: "8px",
}}
>
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
<JsonTreeView
data={guiState}
label="Internal GUI State (inferred from Logs)"
/>
<JsonTreeView
data={cliState}
label="Swap Daemon State (exposed via API)"
/>
<CliLogsBox label="Tor Daemon Logs" logs={torStdOut.split("\n")} />
</Box>
</DialogContentText>
</Box>
);
}

View file

@ -0,0 +1,26 @@
import { Tooltip } from '@material-ui/core';
import IconButton from '@material-ui/core/IconButton';
import DeveloperBoardIcon from '@material-ui/icons/DeveloperBoard';
export default function DebugPageSwitchBadge({
enabled,
setEnabled,
}: {
enabled: boolean;
setEnabled: (enabled: boolean) => void;
}) {
const handleToggle = () => {
setEnabled(!enabled);
};
return (
<Tooltip title={enabled ? 'Hide debug view' : 'Show debug view'}>
<IconButton
onClick={handleToggle}
color={enabled ? 'primary' : 'default'}
>
<DeveloperBoardIcon />
</IconButton>
</Tooltip>
);
}

View file

@ -0,0 +1,22 @@
import { IconButton } from '@material-ui/core';
import FeedbackIcon from '@material-ui/icons/Feedback';
import FeedbackDialog from '../../feedback/FeedbackDialog';
import { useState } from 'react';
export default function FeedbackSubmitBadge() {
const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
return (
<>
{showFeedbackDialog && (
<FeedbackDialog
open={showFeedbackDialog}
onClose={() => setShowFeedbackDialog(false)}
/>
)}
<IconButton onClick={() => setShowFeedbackDialog(true)}>
<FeedbackIcon />
</IconButton>
</>
);
}

View file

@ -0,0 +1,106 @@
import { Box } from '@material-ui/core';
import { useAppSelector } from 'store/hooks';
import {
isSwapStateBtcCancelled,
isSwapStateBtcLockInMempool,
isSwapStateBtcPunished,
isSwapStateBtcRedemeed,
isSwapStateBtcRefunded,
isSwapStateInitiated,
isSwapStateProcessExited,
isSwapStateReceivedQuote,
isSwapStateStarted,
isSwapStateWaitingForBtcDeposit,
isSwapStateXmrLocked,
isSwapStateXmrLockInMempool,
isSwapStateXmrRedeemInMempool,
SwapState,
} from '../../../../../models/storeModel';
import InitiatedPage from './init/InitiatedPage';
import WaitingForBitcoinDepositPage from './init/WaitingForBitcoinDepositPage';
import StartedPage from './in_progress/StartedPage';
import BitcoinLockTxInMempoolPage from './in_progress/BitcoinLockTxInMempoolPage';
import XmrLockTxInMempoolPage from './in_progress/XmrLockInMempoolPage';
// eslint-disable-next-line import/no-cycle
import ProcessExitedPage from './exited/ProcessExitedPage';
import XmrRedeemInMempoolPage from './done/XmrRedeemInMempoolPage';
import ReceivedQuotePage from './in_progress/ReceivedQuotePage';
import BitcoinRedeemedPage from './in_progress/BitcoinRedeemedPage';
import InitPage from './init/InitPage';
import XmrLockedPage from './in_progress/XmrLockedPage';
import BitcoinCancelledPage from './in_progress/BitcoinCancelledPage';
import BitcoinRefundedPage from './done/BitcoinRefundedPage';
import BitcoinPunishedPage from './done/BitcoinPunishedPage';
import { SyncingMoneroWalletPage } from './in_progress/SyncingMoneroWalletPage';
export default function SwapStatePage({
swapState,
}: {
swapState: SwapState | null;
}) {
const isSyncingMoneroWallet = useAppSelector(
(state) => state.rpc.state.moneroWallet.isSyncing,
);
if (isSyncingMoneroWallet) {
return <SyncingMoneroWalletPage />;
}
if (swapState === null) {
return <InitPage />;
}
if (isSwapStateInitiated(swapState)) {
return <InitiatedPage />;
}
if (isSwapStateReceivedQuote(swapState)) {
return <ReceivedQuotePage />;
}
if (isSwapStateWaitingForBtcDeposit(swapState)) {
return <WaitingForBitcoinDepositPage state={swapState} />;
}
if (isSwapStateStarted(swapState)) {
return <StartedPage state={swapState} />;
}
if (isSwapStateBtcLockInMempool(swapState)) {
return <BitcoinLockTxInMempoolPage state={swapState} />;
}
if (isSwapStateXmrLockInMempool(swapState)) {
return <XmrLockTxInMempoolPage state={swapState} />;
}
if (isSwapStateXmrLocked(swapState)) {
return <XmrLockedPage />;
}
if (isSwapStateBtcRedemeed(swapState)) {
return <BitcoinRedeemedPage />;
}
if (isSwapStateXmrRedeemInMempool(swapState)) {
return <XmrRedeemInMempoolPage state={swapState} />;
}
if (isSwapStateBtcCancelled(swapState)) {
return <BitcoinCancelledPage />;
}
if (isSwapStateBtcRefunded(swapState)) {
return <BitcoinRefundedPage state={swapState} />;
}
if (isSwapStateBtcPunished(swapState)) {
return <BitcoinPunishedPage />;
}
if (isSwapStateProcessExited(swapState)) {
return <ProcessExitedPage state={swapState} />;
}
console.error(
`No swap state page found for swap state State: ${JSON.stringify(
swapState,
null,
4,
)}`,
);
return (
<Box>
No information to display
<br />
State: ${JSON.stringify(swapState, null, 4)}
</Box>
);
}

View file

@ -0,0 +1,19 @@
import { IconButton, Tooltip } from '@material-ui/core';
import { useAppSelector } from 'store/hooks';
import TorIcon from '../../../icons/TorIcon';
export default function TorStatusBadge() {
const tor = useAppSelector((s) => s.tor);
if (tor.processRunning) {
return (
<Tooltip title="Tor is running in the background">
<IconButton>
<TorIcon htmlColor="#7D4698" />
</IconButton>
</Tooltip>
);
}
return <></>;
}

View file

@ -0,0 +1,15 @@
import { Box, DialogContentText } from '@material-ui/core';
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
export default function BitcoinPunishedPage() {
return (
<Box>
<DialogContentText>
Unfortunately, the swap was not successful, and you&apos;ve incurred a
penalty because the swap was not refunded in time. Both the Bitcoin and
Monero are irretrievable.
</DialogContentText>
<FeedbackInfoBox />
</Box>
);
}

View file

@ -0,0 +1,43 @@
import { Box, DialogContentText } from '@material-ui/core';
import { SwapStateBtcRefunded } from 'models/storeModel';
import { useActiveSwapInfo } from 'store/hooks';
import BitcoinTransactionInfoBox from '../../BitcoinTransactionInfoBox';
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
export default function BitcoinRefundedPage({
state,
}: {
state: SwapStateBtcRefunded | null;
}) {
const swap = useActiveSwapInfo();
const additionalContent = swap
? `Refund address: ${swap.btcRefundAddress}`
: null;
return (
<Box>
<DialogContentText>
Unfortunately, the swap was not successful. However, rest assured that
all your Bitcoin has been refunded to the specified address. The swap
process is now complete, and you are free to exit the application.
</DialogContentText>
<Box
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
}}
>
{state && (
<BitcoinTransactionInfoBox
title="Bitcoin Refund Transaction"
txId={state.bobBtcRefundTxId}
loading={false}
additionalContent={additionalContent}
/>
)}
<FeedbackInfoBox />
</Box>
</Box>
);
}

View file

@ -0,0 +1,49 @@
import { Box, DialogContentText } from '@material-ui/core';
import { SwapStateXmrRedeemInMempool } from 'models/storeModel';
import { useActiveSwapInfo } from 'store/hooks';
import { getSwapXmrAmount } from 'models/rpcModel';
import MoneroTransactionInfoBox from '../../MoneroTransactionInfoBox';
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
type XmrRedeemInMempoolPageProps = {
state: SwapStateXmrRedeemInMempool | null;
};
export default function XmrRedeemInMempoolPage({
state,
}: XmrRedeemInMempoolPageProps) {
const swap = useActiveSwapInfo();
const additionalContent = swap
? `This transaction transfers ${getSwapXmrAmount(swap).toFixed(6)} XMR to ${
state?.bobXmrRedeemAddress
}`
: null;
return (
<Box>
<DialogContentText>
The swap was successful and the Monero has been sent to the address you
specified. The swap is completed and you may exit the application now.
</DialogContentText>
<Box
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
}}
>
{state && (
<>
<MoneroTransactionInfoBox
title="Monero Redeem Transaction"
txId={state.bobXmrRedeemTxId}
additionalContent={additionalContent}
loading={false}
/>
</>
)}
<FeedbackInfoBox />
</Box>
</Box>
);
}

View file

@ -0,0 +1,71 @@
import { Box, DialogContentText } from '@material-ui/core';
import { useActiveSwapInfo, useAppSelector } from 'store/hooks';
import { SwapStateProcessExited } from 'models/storeModel';
import CliLogsBox from '../../../../other/RenderedCliLog';
import { SwapSpawnType } from 'models/cliModel';
export default function ProcessExitedAndNotDonePage({
state,
}: {
state: SwapStateProcessExited;
}) {
const swap = useActiveSwapInfo();
const logs = useAppSelector((s) => s.swap.logs);
const spawnType = useAppSelector((s) => s.swap.spawnType);
function getText() {
const isCancelRefund = spawnType === SwapSpawnType.CANCEL_REFUND;
const hasRpcError = state.rpcError != null;
const hasSwap = swap != null;
let messages = [];
messages.push(
isCancelRefund
? 'The manual cancel and refund was unsuccessful.'
: 'The swap exited unexpectedly without completing.',
);
if (!hasSwap && !isCancelRefund) {
messages.push('No funds were locked.');
}
messages.push(
hasRpcError
? 'Check the error and the logs below for more information.'
: 'Check the logs below for more information.',
);
if (hasSwap) {
messages.push(`The swap is in the "${swap.stateName}" state.`);
if (!isCancelRefund) {
messages.push(
'Try resuming the swap or attempt to initiate a manual cancel and refund.',
);
}
}
return messages.join(' ');
}
return (
<Box>
<DialogContentText>{getText()}</DialogContentText>
<Box
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
}}
>
{state.rpcError && (
<CliLogsBox
logs={[state.rpcError]}
label="Error returned by the Swap Daemon"
/>
)}
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
</Box>
</Box>
);
}

View file

@ -0,0 +1,47 @@
import { useActiveSwapInfo } from 'store/hooks';
import { SwapStateName } from 'models/rpcModel';
import {
isSwapStateBtcPunished,
isSwapStateBtcRefunded,
isSwapStateXmrRedeemInMempool,
SwapStateProcessExited,
} from '../../../../../../models/storeModel';
import XmrRedeemInMempoolPage from '../done/XmrRedeemInMempoolPage';
import BitcoinPunishedPage from '../done/BitcoinPunishedPage';
// eslint-disable-next-line import/no-cycle
import SwapStatePage from '../SwapStatePage';
import BitcoinRefundedPage from '../done/BitcoinRefundedPage';
import ProcessExitedAndNotDonePage from './ProcessExitedAndNotDonePage';
type ProcessExitedPageProps = {
state: SwapStateProcessExited;
};
export default function ProcessExitedPage({ state }: ProcessExitedPageProps) {
const swap = useActiveSwapInfo();
// If we have a swap state, for a "done" state we should use it to display additional information that can't be extracted from the database
if (
isSwapStateXmrRedeemInMempool(state.prevState) ||
isSwapStateBtcRefunded(state.prevState) ||
isSwapStateBtcPunished(state.prevState)
) {
return <SwapStatePage swapState={state.prevState} />;
}
// If we don't have a swap state for a "done" state, we should fall back to using the database to display as much information as we can
if (swap) {
if (swap.stateName === SwapStateName.XmrRedeemed) {
return <XmrRedeemInMempoolPage state={null} />;
}
if (swap.stateName === SwapStateName.BtcRefunded) {
return <BitcoinRefundedPage state={null} />;
}
if (swap.stateName === SwapStateName.BtcPunished) {
return <BitcoinPunishedPage />;
}
}
// If the swap is not a "done" state (or we don't have a db state because the swap did complete the SwapSetup yet) we should tell the user and show logs
return <ProcessExitedAndNotDonePage state={state} />;
}

View file

@ -0,0 +1,5 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
export default function BitcoinCancelledPage() {
return <CircularProgressWithSubtitle description="Refunding your Bitcoin" />;
}

View file

@ -0,0 +1,38 @@
import { Box, DialogContentText } from '@material-ui/core';
import { SwapStateBtcLockInMempool } from 'models/storeModel';
import BitcoinTransactionInfoBox from '../../BitcoinTransactionInfoBox';
import SwapMightBeCancelledAlert from '../../../../alert/SwapMightBeCancelledAlert';
type BitcoinLockTxInMempoolPageProps = {
state: SwapStateBtcLockInMempool;
};
export default function BitcoinLockTxInMempoolPage({
state,
}: BitcoinLockTxInMempoolPageProps) {
return (
<Box>
<SwapMightBeCancelledAlert
bobBtcLockTxConfirmations={state.bobBtcLockTxConfirmations}
/>
<DialogContentText>
The Bitcoin lock transaction has been published. The swap will proceed
once the transaction is confirmed and the swap provider locks their
Monero.
</DialogContentText>
<BitcoinTransactionInfoBox
title="Bitcoin Lock Transaction"
txId={state.bobBtcLockTxId}
loading
additionalContent={
<>
Most swap providers require one confirmation before locking their
Monero
<br />
Confirmations: {state.bobBtcLockTxConfirmations}
</>
}
/>
</Box>
);
}

View file

@ -0,0 +1,5 @@
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 ReceivedQuotePage() {
return (
<CircularProgressWithSubtitle description="Exchanging keys, zero-knowledge proofs and generating multi-signature addresses" />
);
}

View file

@ -0,0 +1,16 @@
import { SwapStateStarted } from 'models/storeModel';
import { BitcoinAmount } from 'renderer/components/other/Units';
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
export default function StartedPage({ state }: { state: SwapStateStarted }) {
const description = state.txLockDetails ? (
<>
Locking <BitcoinAmount amount={state.txLockDetails.amount} /> with a
network fee of <BitcoinAmount amount={state.txLockDetails.fees} />
</>
) : (
'Locking Bitcoin'
);
return <CircularProgressWithSubtitle description={description} />;
}

View file

@ -0,0 +1,7 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
export function SyncingMoneroWalletPage() {
return (
<CircularProgressWithSubtitle description="Syncing Monero wallet with blockchain, this might take a while..." />
);
}

View file

@ -0,0 +1,29 @@
import { Box, DialogContentText } from '@material-ui/core';
import { SwapStateXmrLockInMempool } from 'models/storeModel';
import MoneroTransactionInfoBox from '../../MoneroTransactionInfoBox';
type XmrLockTxInMempoolPageProps = {
state: SwapStateXmrLockInMempool;
};
export default function XmrLockTxInMempoolPage({
state,
}: XmrLockTxInMempoolPageProps) {
const additionalContent = `Confirmations: ${state.aliceXmrLockTxConfirmations}/10`;
return (
<Box>
<DialogContentText>
They have published their Monero lock transaction. The swap will proceed
once the transaction has been confirmed.
</DialogContentText>
<MoneroTransactionInfoBox
title="Monero Lock Transaction"
txId={state.aliceXmrLockTxId}
additionalContent={additionalContent}
loading
/>
</Box>
);
}

View file

@ -0,0 +1,7 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
export default function XmrLockedPage() {
return (
<CircularProgressWithSubtitle description="Waiting for them to redeem the Bitcoin" />
);
}

View file

@ -0,0 +1,91 @@
import { useState } from 'react';
import { Box, makeStyles, TextField, Typography } from '@material-ui/core';
import { SwapStateWaitingForBtcDeposit } from 'models/storeModel';
import { useAppSelector } from 'store/hooks';
import { satsToBtc } from 'utils/conversionUtils';
import { MoneroAmount } from '../../../../other/Units';
const MONERO_FEE = 0.000016;
const useStyles = makeStyles((theme) => ({
outer: {
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
},
textField: {
'& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button': {
display: 'none',
},
'& input[type=number]': {
MozAppearance: 'textfield',
},
maxWidth: theme.spacing(16),
},
}));
function calcBtcAmountWithoutFees(amount: number, fees: number) {
return amount - fees;
}
export default function DepositAmountHelper({
state,
}: {
state: SwapStateWaitingForBtcDeposit;
}) {
const classes = useStyles();
const [amount, setAmount] = useState(state.minDeposit);
const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
function getTotalAmountAfterDeposit() {
return amount + satsToBtc(bitcoinBalance);
}
function hasError() {
return (
amount < state.minDeposit ||
getTotalAmountAfterDeposit() > state.maximumAmount
);
}
function calcXMRAmount(): number | null {
if (Number.isNaN(amount)) return null;
if (hasError()) return null;
if (state.price == null) return null;
console.log(
`Calculating calcBtcAmountWithoutFees(${getTotalAmountAfterDeposit()}, ${
state.minBitcoinLockTxFee
}) / ${state.price} - ${MONERO_FEE}`,
);
return (
calcBtcAmountWithoutFees(
getTotalAmountAfterDeposit(),
state.minBitcoinLockTxFee,
) /
state.price -
MONERO_FEE
);
}
return (
<Box className={classes.outer}>
<Typography variant="subtitle2">
Depositing {bitcoinBalance > 0 && <>another</>}
</Typography>
<TextField
error={hasError()}
value={amount}
onChange={(e) => setAmount(parseFloat(e.target.value))}
size="small"
type="number"
className={classes.textField}
/>
<Typography variant="subtitle2">
BTC will give you approximately{' '}
<MoneroAmount amount={calcXMRAmount()} />.
</Typography>
</Box>
);
}

View file

@ -0,0 +1,14 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
import { MoneroWalletRpcUpdateState } from '../../../../../../models/storeModel';
export default function DownloadingMoneroWalletRpcPage({
updateState,
}: {
updateState: MoneroWalletRpcUpdateState;
}) {
return (
<CircularProgressWithSubtitle
description={`Updating monero-wallet-rpc (${updateState.progress}) `}
/>
);
}

View file

@ -0,0 +1,82 @@
import { Box, DialogContentText, makeStyles } from '@material-ui/core';
import { useState } from 'react';
import BitcoinAddressTextField from 'renderer/components/inputs/BitcoinAddressTextField';
import MoneroAddressTextField from 'renderer/components/inputs/MoneroAddressTextField';
import { useAppSelector } from 'store/hooks';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import { isTestnet } from 'store/config';
import RemainingFundsWillBeUsedAlert from '../../../../alert/RemainingFundsWillBeUsedAlert';
import IpcInvokeButton from '../../../../IpcInvokeButton';
const useStyles = makeStyles((theme) => ({
initButton: {
marginTop: theme.spacing(1),
},
fieldsOuter: {
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
},
}));
export default function InitPage() {
const classes = useStyles();
const [redeemAddress, setRedeemAddress] = useState(
''
);
const [refundAddress, setRefundAddress] = useState(
''
);
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
const [refundAddressValid, setRefundAddressValid] = useState(false);
const selectedProvider = useAppSelector(
(state) => state.providers.selectedProvider,
);
return (
<Box>
<RemainingFundsWillBeUsedAlert />
<DialogContentText>
Please specify the address to which the Monero should be sent upon
completion of the swap and the address for receiving a Bitcoin refund if
the swap fails.
</DialogContentText>
<Box className={classes.fieldsOuter}>
<MoneroAddressTextField
label="Monero redeem address"
address={redeemAddress}
onAddressChange={setRedeemAddress}
onAddressValidityChange={setRedeemAddressValid}
helperText="The monero will be sent to this address"
fullWidth
/>
<BitcoinAddressTextField
label="Bitcoin refund address"
address={refundAddress}
onAddressChange={setRefundAddress}
onAddressValidityChange={setRefundAddressValid}
helperText="In case something goes terribly wrong, all Bitcoin will be refunded to this address"
fullWidth
/>
</Box>
<IpcInvokeButton
disabled={
!refundAddressValid || !redeemAddressValid || !selectedProvider
}
variant="contained"
color="primary"
size="large"
className={classes.initButton}
endIcon={<PlayArrowIcon />}
ipcChannel="spawn-buy-xmr"
ipcArgs={[selectedProvider, redeemAddress, refundAddress]}
displayErrorSnackbar={false}
>
Start swap
</IpcInvokeButton>
</Box>
);
}

View file

@ -0,0 +1,21 @@
import { useAppSelector } from 'store/hooks';
import { SwapSpawnType } from 'models/cliModel';
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle';
export default function InitiatedPage() {
const description = useAppSelector((s) => {
switch (s.swap.spawnType) {
case SwapSpawnType.INIT:
return 'Requesting quote from provider...';
case SwapSpawnType.RESUME:
return 'Resuming swap...';
case SwapSpawnType.CANCEL_REFUND:
return 'Attempting to cancel & refund swap...';
default:
// Should never be hit
return 'Initiating swap...';
}
});
return <CircularProgressWithSubtitle description={description} />;
}

View file

@ -0,0 +1,86 @@
import { Box, makeStyles, Typography } from '@material-ui/core';
import { SwapStateWaitingForBtcDeposit } from 'models/storeModel';
import { useAppSelector } from 'store/hooks';
import DepositAddressInfoBox from '../../DepositAddressInfoBox';
import BitcoinIcon from '../../../../icons/BitcoinIcon';
import DepositAmountHelper from './DepositAmountHelper';
import {
BitcoinAmount,
MoneroBitcoinExchangeRate,
SatsAmount,
} from '../../../../other/Units';
const useStyles = makeStyles((theme) => ({
amountHelper: {
display: 'flex',
alignItems: 'center',
},
additionalContent: {
paddingTop: theme.spacing(1),
gap: theme.spacing(0.5),
display: 'flex',
flexDirection: 'column',
},
}));
type WaitingForBtcDepositPageProps = {
state: SwapStateWaitingForBtcDeposit;
};
export default function WaitingForBtcDepositPage({
state,
}: WaitingForBtcDepositPageProps) {
const classes = useStyles();
const bitcoinBalance = useAppSelector((s) => s.rpc.state.balance) || 0;
// TODO: Account for BTC lock tx fees
return (
<Box>
<DepositAddressInfoBox
title="Bitcoin Deposit Address"
address={state.depositAddress}
additionalContent={
<Box className={classes.additionalContent}>
<Typography variant="subtitle2">
<ul>
{bitcoinBalance > 0 ? (
<li>
You have already deposited{' '}
<SatsAmount amount={bitcoinBalance} />
</li>
) : null}
<li>
Send any amount between{' '}
<BitcoinAmount amount={state.minDeposit} /> and{' '}
<BitcoinAmount amount={state.maxDeposit} /> to the address
above
{bitcoinBalance > 0 && (
<> (on top of the already deposited funds)</>
)}
</li>
<li>
All Bitcoin sent to this this address will converted into
Monero at an exchance rate of{' '}
<MoneroBitcoinExchangeRate rate={state.price} />
</li>
<li>
The network fee of{' '}
<BitcoinAmount amount={state.minBitcoinLockTxFee} /> will
automatically be deducted from the deposited coins
</li>
<li>
The swap will start automatically as soon as the minimum
amount is deposited
</li>
</ul>
</Typography>
<DepositAmountHelper
state={state}
/>
</Box>
}
icon={<BitcoinIcon />}
/>
</Box>
);
}