mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-05-12 03:35:06 -04:00
feat: cargo project at root
This commit is contained in:
parent
aa0c0623ca
commit
709a2820c4
313 changed files with 1 additions and 740 deletions
|
@ -0,0 +1,51 @@
|
|||
import { Box, Button, makeStyles, Typography } from '@material-ui/core';
|
||||
import InfoBox from '../../modal/swap/InfoBox';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
spacedBox: {
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const GITHUB_ISSUE_URL =
|
||||
'https://github.com/UnstoppableSwap/unstoppableswap-gui/issues/new/choose';
|
||||
const MATRIX_ROOM_URL = 'https://matrix.to/#/#unstoppableswap:matrix.org';
|
||||
export const DISCORD_URL = 'https://discord.gg/APJ6rJmq';
|
||||
|
||||
export default function ContactInfoBox() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title="Get in touch"
|
||||
mainContent={
|
||||
<Typography variant="subtitle2">
|
||||
If you need help or just want to reach out to the contributors of this
|
||||
project you can open a GitHub issue, join our Matrix room or Discord
|
||||
</Typography>
|
||||
}
|
||||
additionalContent={
|
||||
<Box className={classes.spacedBox}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => window.open(GITHUB_ISSUE_URL)}
|
||||
>
|
||||
Open GitHub issue
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => window.open(MATRIX_ROOM_URL)}
|
||||
>
|
||||
Join Matrix room
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={() => window.open(DISCORD_URL)}>
|
||||
Join Discord
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
icon={null}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
}
|
25
src-gui/src/renderer/components/pages/help/DonateInfoBox.tsx
Normal file
25
src-gui/src/renderer/components/pages/help/DonateInfoBox.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Typography } from '@material-ui/core';
|
||||
import DepositAddressInfoBox from '../../modal/swap/DepositAddressInfoBox';
|
||||
import MoneroIcon from '../../icons/MoneroIcon';
|
||||
|
||||
const XMR_DONATE_ADDRESS =
|
||||
'87jS4C7ngk9EHdqFFuxGFgg8AyH63dRUoULshWDybFJaP75UA89qsutG5B1L1QTc4w228nsqsv8EjhL7bz8fB3611Mh98mg';
|
||||
|
||||
export default function DonateInfoBox() {
|
||||
return (
|
||||
<DepositAddressInfoBox
|
||||
title="Donate"
|
||||
address={XMR_DONATE_ADDRESS}
|
||||
icon={<MoneroIcon />}
|
||||
additionalContent={
|
||||
<Typography variant="subtitle2">
|
||||
We rely on generous donors like you to keep development moving
|
||||
forward. To bring Atomic Swaps to life, we need resources. If you have
|
||||
the possibility, please consider making a donation to the project. All
|
||||
funds will be used to support contributors and critical
|
||||
infrastructure.
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { Button, Typography } from '@material-ui/core';
|
||||
import { useState } from 'react';
|
||||
import InfoBox from '../../modal/swap/InfoBox';
|
||||
import FeedbackDialog from '../../modal/feedback/FeedbackDialog';
|
||||
|
||||
export default function FeedbackInfoBox() {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title="Feedback"
|
||||
mainContent={
|
||||
<Typography variant="subtitle2">
|
||||
The main goal of this project is to make Atomic Swaps easier to use,
|
||||
and for that we need genuine users' input. Please leave some
|
||||
feedback, it takes just two minutes. I'll read each and every
|
||||
survey response and take your feedback into consideration.
|
||||
</Typography>
|
||||
}
|
||||
additionalContent={
|
||||
<>
|
||||
<Button variant="outlined" onClick={() => setShowDialog(true)}>
|
||||
Give feedback
|
||||
</Button>
|
||||
<FeedbackDialog
|
||||
open={showDialog}
|
||||
onClose={() => setShowDialog(false)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
icon={null}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
}
|
28
src-gui/src/renderer/components/pages/help/HelpPage.tsx
Normal file
28
src-gui/src/renderer/components/pages/help/HelpPage.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Box, makeStyles } from '@material-ui/core';
|
||||
import ContactInfoBox from './ContactInfoBox';
|
||||
import FeedbackInfoBox from './FeedbackInfoBox';
|
||||
import DonateInfoBox from './DonateInfoBox';
|
||||
import TorInfoBox from './TorInfoBox';
|
||||
import RpcControlBox from './RpcControlBox';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: 'flex',
|
||||
gap: theme.spacing(2),
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}));
|
||||
|
||||
export default function HelpPage() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<RpcControlBox />
|
||||
<TorInfoBox />
|
||||
<FeedbackInfoBox />
|
||||
<ContactInfoBox />
|
||||
<DonateInfoBox />
|
||||
</Box>
|
||||
);
|
||||
}
|
74
src-gui/src/renderer/components/pages/help/RpcControlBox.tsx
Normal file
74
src-gui/src/renderer/components/pages/help/RpcControlBox.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { Box, makeStyles } from '@material-ui/core';
|
||||
import IpcInvokeButton from 'renderer/components/IpcInvokeButton';
|
||||
import { useAppSelector } from 'store/hooks';
|
||||
import StopIcon from '@material-ui/icons/Stop';
|
||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
||||
import { RpcProcessStateType } from 'models/rpcModel';
|
||||
import InfoBox from '../../modal/swap/InfoBox';
|
||||
import CliLogsBox from '../../other/RenderedCliLog';
|
||||
import FolderOpenIcon from '@material-ui/icons/FolderOpen';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
actionsOuter: {
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
export default function RpcControlBox() {
|
||||
const rpcProcess = useAppSelector((state) => state.rpc.process);
|
||||
const isRunning =
|
||||
rpcProcess.type === RpcProcessStateType.STARTED ||
|
||||
rpcProcess.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS;
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title={`Swap Daemon (${rpcProcess.type})`}
|
||||
mainContent={
|
||||
isRunning || rpcProcess.type === RpcProcessStateType.EXITED ? (
|
||||
<CliLogsBox
|
||||
label="Swap Daemon Logs (current session only)"
|
||||
logs={rpcProcess.logs}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
additionalContent={
|
||||
<Box className={classes.actionsOuter}>
|
||||
<IpcInvokeButton
|
||||
variant="contained"
|
||||
ipcChannel="spawn-start-rpc"
|
||||
ipcArgs={[]}
|
||||
endIcon={<PlayArrowIcon />}
|
||||
disabled={isRunning}
|
||||
requiresRpc={false}
|
||||
>
|
||||
Start Daemon
|
||||
</IpcInvokeButton>
|
||||
<IpcInvokeButton
|
||||
variant="contained"
|
||||
ipcChannel="stop-cli"
|
||||
ipcArgs={[]}
|
||||
endIcon={<StopIcon />}
|
||||
disabled={!isRunning}
|
||||
requiresRpc={false}
|
||||
>
|
||||
Stop Daemon
|
||||
</IpcInvokeButton>
|
||||
<IpcInvokeButton
|
||||
ipcChannel="open-data-dir-in-file-explorer"
|
||||
ipcArgs={[]}
|
||||
endIcon={<FolderOpenIcon />}
|
||||
requiresRpc={false}
|
||||
isIconButton
|
||||
size="small"
|
||||
tooltipTitle="Open the data directory of the Swap Daemon in your file explorer"
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
icon={null}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
}
|
71
src-gui/src/renderer/components/pages/help/TorInfoBox.tsx
Normal file
71
src-gui/src/renderer/components/pages/help/TorInfoBox.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { Box, makeStyles, Typography } from '@material-ui/core';
|
||||
import IpcInvokeButton from 'renderer/components/IpcInvokeButton';
|
||||
import { useAppSelector } from 'store/hooks';
|
||||
import StopIcon from '@material-ui/icons/Stop';
|
||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
||||
import InfoBox from '../../modal/swap/InfoBox';
|
||||
import CliLogsBox from '../../other/RenderedCliLog';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
actionsOuter: {
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function TorInfoBox() {
|
||||
const isTorRunning = useAppSelector((state) => state.tor.processRunning);
|
||||
const torStdOut = useAppSelector((s) => s.tor.stdOut);
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title="Tor (The Onion Router)"
|
||||
mainContent={
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2">
|
||||
Tor is a network that allows you to anonymously connect to the
|
||||
internet. It is a free and open network that is operated by
|
||||
volunteers. You can start and stop Tor by clicking the buttons
|
||||
below. If Tor is running, all traffic will be routed through it and
|
||||
the swap provider will not be able to see your IP address.
|
||||
</Typography>
|
||||
<CliLogsBox label="Tor Daemon Logs" logs={torStdOut.split('\n')} />
|
||||
</Box>
|
||||
}
|
||||
additionalContent={
|
||||
<Box className={classes.actionsOuter}>
|
||||
<IpcInvokeButton
|
||||
variant="contained"
|
||||
disabled={isTorRunning}
|
||||
ipcChannel="spawn-tor"
|
||||
ipcArgs={[]}
|
||||
endIcon={<PlayArrowIcon />}
|
||||
requiresRpc={false}
|
||||
>
|
||||
Start Tor
|
||||
</IpcInvokeButton>
|
||||
<IpcInvokeButton
|
||||
variant="contained"
|
||||
disabled={!isTorRunning}
|
||||
ipcChannel="stop-tor"
|
||||
ipcArgs={[]}
|
||||
endIcon={<StopIcon />}
|
||||
requiresRpc={false}
|
||||
>
|
||||
Stop Tor
|
||||
</IpcInvokeButton>
|
||||
</Box>
|
||||
}
|
||||
icon={null}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { Typography } from '@material-ui/core';
|
||||
import { useIsSwapRunning } from 'store/hooks';
|
||||
import HistoryTable from './table/HistoryTable';
|
||||
import SwapDialog from '../../modal/swap/SwapDialog';
|
||||
import SwapTxLockAlertsBox from '../../alert/SwapTxLockAlertsBox';
|
||||
|
||||
export default function HistoryPage() {
|
||||
const showDialog = useIsSwapRunning();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h3">History</Typography>
|
||||
<SwapTxLockAlertsBox />
|
||||
<HistoryTable />
|
||||
<SwapDialog open={showDialog} onClose={() => {}} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from '@material-ui/core';
|
||||
import { useState } from 'react';
|
||||
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
|
||||
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
|
||||
import {
|
||||
getHumanReadableDbStateType,
|
||||
getSwapBtcAmount,
|
||||
getSwapXmrAmount,
|
||||
GetSwapInfoResponse,
|
||||
} from '../../../../../models/rpcModel';
|
||||
import HistoryRowActions from './HistoryRowActions';
|
||||
import HistoryRowExpanded from './HistoryRowExpanded';
|
||||
import { BitcoinAmount, MoneroAmount } from '../../../other/Units';
|
||||
|
||||
type HistoryRowProps = {
|
||||
swap: GetSwapInfoResponse;
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
amountTransferContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
function AmountTransfer({
|
||||
btcAmount,
|
||||
xmrAmount,
|
||||
}: {
|
||||
xmrAmount: number;
|
||||
btcAmount: number;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.amountTransferContainer}>
|
||||
<BitcoinAmount amount={btcAmount} />
|
||||
<ArrowForwardIcon />
|
||||
<MoneroAmount amount={xmrAmount} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HistoryRow({ swap }: HistoryRowProps) {
|
||||
const btcAmount = getSwapBtcAmount(swap);
|
||||
const xmrAmount = getSwapXmrAmount(swap);
|
||||
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<IconButton size="small" onClick={() => setExpanded(!expanded)}>
|
||||
{expanded ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
<TableCell>{swap.swapId.substring(0, 5)}...</TableCell>
|
||||
<TableCell>
|
||||
<AmountTransfer xmrAmount={xmrAmount} btcAmount={btcAmount} />
|
||||
</TableCell>
|
||||
<TableCell>{getHumanReadableDbStateType(swap.stateName)}</TableCell>
|
||||
<TableCell>
|
||||
<HistoryRowActions swap={swap} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<TableRow>
|
||||
<TableCell style={{ padding: 0 }} colSpan={6}>
|
||||
<Collapse in={expanded} timeout="auto">
|
||||
{expanded && <HistoryRowExpanded swap={swap} />}
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
import { Tooltip } from '@material-ui/core';
|
||||
import Button, { ButtonProps } from '@material-ui/core/Button/Button';
|
||||
import DoneIcon from '@material-ui/icons/Done';
|
||||
import ErrorIcon from '@material-ui/icons/Error';
|
||||
import { green, red } from '@material-ui/core/colors';
|
||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
||||
import IpcInvokeButton from '../../../IpcInvokeButton';
|
||||
import {
|
||||
GetSwapInfoResponse,
|
||||
SwapStateName,
|
||||
isSwapStateNamePossiblyCancellableSwap,
|
||||
isSwapStateNamePossiblyRefundableSwap,
|
||||
} from '../../../../../models/rpcModel';
|
||||
|
||||
export function SwapResumeButton({
|
||||
swap,
|
||||
...props
|
||||
}: { swap: GetSwapInfoResponse } & ButtonProps) {
|
||||
return (
|
||||
<IpcInvokeButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={swap.completed}
|
||||
ipcChannel="spawn-resume-swap"
|
||||
ipcArgs={[swap.swapId]}
|
||||
endIcon={<PlayArrowIcon />}
|
||||
requiresRpc
|
||||
{...props}
|
||||
>
|
||||
Resume
|
||||
</IpcInvokeButton>
|
||||
);
|
||||
}
|
||||
|
||||
export function SwapCancelRefundButton({
|
||||
swap,
|
||||
...props
|
||||
}: { swap: GetSwapInfoResponse } & ButtonProps) {
|
||||
const cancelOrRefundable =
|
||||
isSwapStateNamePossiblyCancellableSwap(swap.stateName) ||
|
||||
isSwapStateNamePossiblyRefundableSwap(swap.stateName);
|
||||
|
||||
if (!cancelOrRefundable) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<IpcInvokeButton
|
||||
ipcChannel="spawn-cancel-refund"
|
||||
ipcArgs={[swap.swapId]}
|
||||
requiresRpc
|
||||
displayErrorSnackbar={false}
|
||||
{...props}
|
||||
>
|
||||
Attempt manual Cancel & Refund
|
||||
</IpcInvokeButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HistoryRowActions({
|
||||
swap,
|
||||
}: {
|
||||
swap: GetSwapInfoResponse;
|
||||
}) {
|
||||
if (swap.stateName === SwapStateName.XmrRedeemed) {
|
||||
return (
|
||||
<Tooltip title="The swap is completed because you have redeemed the XMR">
|
||||
<DoneIcon style={{ color: green[500] }} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (swap.stateName === SwapStateName.BtcRefunded) {
|
||||
return (
|
||||
<Tooltip title="The swap is completed because your BTC have been refunded">
|
||||
<DoneIcon style={{ color: green[500] }} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (swap.stateName === SwapStateName.BtcPunished) {
|
||||
return (
|
||||
<Tooltip title="The swap is completed because you have been punished">
|
||||
<ErrorIcon style={{ color: red[500] }} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return <SwapResumeButton swap={swap} />;
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import {
|
||||
Box,
|
||||
Link,
|
||||
makeStyles,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
} from '@material-ui/core';
|
||||
import { getBitcoinTxExplorerUrl } from 'utils/conversionUtils';
|
||||
import { isTestnet } from 'store/config';
|
||||
import {
|
||||
getHumanReadableDbStateType,
|
||||
getSwapBtcAmount,
|
||||
getSwapExchangeRate,
|
||||
getSwapTxFees,
|
||||
getSwapXmrAmount,
|
||||
GetSwapInfoResponse,
|
||||
} from '../../../../../models/rpcModel';
|
||||
import SwapLogFileOpenButton from './SwapLogFileOpenButton';
|
||||
import { SwapCancelRefundButton } from './HistoryRowActions';
|
||||
import { SwapMoneroRecoveryButton } from './SwapMoneroRecoveryButton';
|
||||
import {
|
||||
BitcoinAmount,
|
||||
MoneroAmount,
|
||||
MoneroBitcoinExchangeRate,
|
||||
} from 'renderer/components/other/Units';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: 'grid',
|
||||
padding: theme.spacing(1),
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
actionsOuter: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function HistoryRowExpanded({
|
||||
swap,
|
||||
}: {
|
||||
swap: GetSwapInfoResponse;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
const { seller, startDate } = swap;
|
||||
const btcAmount = getSwapBtcAmount(swap);
|
||||
const xmrAmount = getSwapXmrAmount(swap);
|
||||
const txFees = getSwapTxFees(swap);
|
||||
const exchangeRate = getSwapExchangeRate(swap);
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>Started on</TableCell>
|
||||
<TableCell>{startDate}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Swap ID</TableCell>
|
||||
<TableCell>{swap.swapId}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>State Name</TableCell>
|
||||
<TableCell>
|
||||
{getHumanReadableDbStateType(swap.stateName)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Monero Amount</TableCell>
|
||||
<TableCell>
|
||||
<MoneroAmount amount={xmrAmount} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Bitcoin Amount</TableCell>
|
||||
<TableCell>
|
||||
<BitcoinAmount amount={btcAmount} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Exchange Rate</TableCell>
|
||||
<TableCell>
|
||||
<MoneroBitcoinExchangeRate rate={exchangeRate} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Bitcoin Network Fees</TableCell>
|
||||
<TableCell>
|
||||
<BitcoinAmount amount={txFees} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Provider Address</TableCell>
|
||||
<TableCell>
|
||||
<Box>{seller.addresses.join(', ')}</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>Bitcoin lock transaction</TableCell>
|
||||
<TableCell>
|
||||
<Link
|
||||
href={getBitcoinTxExplorerUrl(swap.txLockId, isTestnet())}
|
||||
target="_blank"
|
||||
>
|
||||
{swap.txLockId}
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box className={classes.actionsOuter}>
|
||||
<SwapLogFileOpenButton
|
||||
swapId={swap.swapId}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
<SwapCancelRefundButton swap={swap} variant="contained" size="small" />
|
||||
<SwapMoneroRecoveryButton
|
||||
swap={swap}
|
||||
variant="contained"
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
Box,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from '@material-ui/core';
|
||||
import { sortBy } from 'lodash';
|
||||
import { parseDateString } from 'utils/parseUtils';
|
||||
import {
|
||||
useAppSelector,
|
||||
useSwapInfosSortedByDate,
|
||||
} from '../../../../../store/hooks';
|
||||
import HistoryRow from './HistoryRow';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function HistoryTable() {
|
||||
const classes = useStyles();
|
||||
const swapSortedByDate = useSwapInfosSortedByDate();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell />
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>Amount</TableCell>
|
||||
<TableCell>State</TableCell>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{swapSortedByDate.map((swap) => (
|
||||
<HistoryRow swap={swap} key={swap.swapId} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { ButtonProps } from '@material-ui/core/Button/Button';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@material-ui/core';
|
||||
import { useState } from 'react';
|
||||
import { CliLog } from 'models/cliModel';
|
||||
import IpcInvokeButton from '../../../IpcInvokeButton';
|
||||
import CliLogsBox from '../../../other/RenderedCliLog';
|
||||
|
||||
export default function SwapLogFileOpenButton({
|
||||
swapId,
|
||||
...props
|
||||
}: { swapId: string } & ButtonProps) {
|
||||
const [logs, setLogs] = useState<CliLog[] | null>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IpcInvokeButton
|
||||
ipcArgs={[swapId]}
|
||||
ipcChannel="get-swap-logs"
|
||||
onSuccess={(data) => {
|
||||
setLogs(data as CliLog[]);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
view log
|
||||
</IpcInvokeButton>
|
||||
{logs && (
|
||||
<Dialog open onClose={() => setLogs(null)} fullWidth maxWidth="lg">
|
||||
<DialogTitle>Logs of swap {swapId}</DialogTitle>
|
||||
<DialogContent>
|
||||
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setLogs(null)}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import { ButtonProps } from '@material-ui/core/Button/Button';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
Link,
|
||||
} from '@material-ui/core';
|
||||
import { useAppDispatch, useAppSelector } from 'store/hooks';
|
||||
import { rpcResetMoneroRecoveryKeys } from 'store/features/rpcSlice';
|
||||
import {
|
||||
GetSwapInfoResponse,
|
||||
isSwapMoneroRecoverable,
|
||||
} from '../../../../../models/rpcModel';
|
||||
import IpcInvokeButton from '../../../IpcInvokeButton';
|
||||
import DialogHeader from '../../../modal/DialogHeader';
|
||||
import ScrollablePaperTextBox from '../../../other/ScrollablePaperTextBox';
|
||||
|
||||
function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
|
||||
const dispatch = useAppDispatch();
|
||||
const keys = useAppSelector((s) => s.rpc.state.moneroRecovery);
|
||||
|
||||
function onClose() {
|
||||
dispatch(rpcResetMoneroRecoveryKeys());
|
||||
}
|
||||
|
||||
if (keys === null || keys.swapId !== swap.swapId) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogHeader
|
||||
title={`Recovery Keys for swap ${swap.swapId.substring(0, 5)}...`}
|
||||
/>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
You can use the keys below to manually redeem the Monero funds from
|
||||
the multi-signature wallet.
|
||||
<ul>
|
||||
<li>
|
||||
This is useful if the swap daemon fails to redeem the funds itself
|
||||
</li>
|
||||
<li>
|
||||
If you have come this far, there is no risk of losing funds. You
|
||||
are the only one with access to these keys and can use them to
|
||||
access your funds
|
||||
</li>
|
||||
<li>
|
||||
View{' '}
|
||||
<Link
|
||||
href="https://www.getmonero.org/resources/user-guides/restore_from_keys.html"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
this guide
|
||||
</Link>{' '}
|
||||
for a detailed description on how to import the keys and spend the
|
||||
funds.
|
||||
</li>
|
||||
</ul>
|
||||
</DialogContentText>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{[
|
||||
['Primary Address', keys.keys.address],
|
||||
['View Key', keys.keys.view_key],
|
||||
['Spend Key', keys.keys.spend_key],
|
||||
['Restore Height', keys.keys.restore_height.toString()],
|
||||
].map(([title, value]) => (
|
||||
<ScrollablePaperTextBox
|
||||
minHeight="2rem"
|
||||
title={title}
|
||||
copyValue={value}
|
||||
rows={[value]}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary" variant="contained">
|
||||
Done
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function SwapMoneroRecoveryButton({
|
||||
swap,
|
||||
...props
|
||||
}: { swap: GetSwapInfoResponse } & ButtonProps) {
|
||||
const isRecoverable = isSwapMoneroRecoverable(swap.stateName);
|
||||
|
||||
if (!isRecoverable) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<IpcInvokeButton
|
||||
ipcChannel="spawn-monero-recovery"
|
||||
ipcArgs={[swap.swapId]}
|
||||
requiresRpc
|
||||
{...props}
|
||||
>
|
||||
Display Monero Recovery Keys
|
||||
</IpcInvokeButton>
|
||||
<MoneroRecoveryKeysDialog swap={swap} />
|
||||
</>
|
||||
);
|
||||
}
|
31
src-gui/src/renderer/components/pages/swap/ApiAlertsBox.tsx
Normal file
31
src-gui/src/renderer/components/pages/swap/ApiAlertsBox.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Box } from '@material-ui/core';
|
||||
import { Alert, AlertTitle } from '@material-ui/lab';
|
||||
import { removeAlert } from 'store/features/alertsSlice';
|
||||
import { useAppDispatch, useAppSelector } from 'store/hooks';
|
||||
|
||||
export default function ApiAlertsBox() {
|
||||
const alerts = useAppSelector((state) => state.alerts.alerts);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function onRemoveAlert(id: number) {
|
||||
dispatch(removeAlert(id));
|
||||
}
|
||||
|
||||
if (alerts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box style={{ display: 'flex', justifyContent: 'center', gap: '1rem' }}>
|
||||
{alerts.map((alert) => (
|
||||
<Alert
|
||||
variant="filled"
|
||||
severity={alert.severity}
|
||||
key={alert.id}
|
||||
onClose={() => onRemoveAlert(alert.id)}
|
||||
>
|
||||
<AlertTitle>{alert.title}</AlertTitle>
|
||||
{alert.body}
|
||||
</Alert>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
25
src-gui/src/renderer/components/pages/swap/SwapPage.tsx
Normal file
25
src-gui/src/renderer/components/pages/swap/SwapPage.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Box, makeStyles } from '@material-ui/core';
|
||||
import SwapWidget from './SwapWidget';
|
||||
import ApiAlertsBox from './ApiAlertsBox';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
paddingBottom: theme.spacing(1),
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SwapPage() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<ApiAlertsBox />
|
||||
<SwapWidget />
|
||||
</Box>
|
||||
);
|
||||
}
|
274
src-gui/src/renderer/components/pages/swap/SwapWidget.tsx
Normal file
274
src-gui/src/renderer/components/pages/swap/SwapWidget.tsx
Normal file
|
@ -0,0 +1,274 @@
|
|||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import {
|
||||
makeStyles,
|
||||
Box,
|
||||
Paper,
|
||||
Typography,
|
||||
TextField,
|
||||
LinearProgress,
|
||||
Fab,
|
||||
} from '@material-ui/core';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
|
||||
import SwapHorizIcon from '@material-ui/icons/SwapHoriz';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { satsToBtc } from 'utils/conversionUtils';
|
||||
import { useAppSelector } from 'store/hooks';
|
||||
import { ExtendedProviderStatus } from 'models/apiModel';
|
||||
import { isSwapState } from 'models/storeModel';
|
||||
import SwapDialog from '../../modal/swap/SwapDialog';
|
||||
import ProviderSelect from '../../modal/provider/ProviderSelect';
|
||||
import {
|
||||
ListSellersDialogOpenButton,
|
||||
ProviderSubmitDialogOpenButton,
|
||||
} from '../../modal/provider/ProviderListDialog';
|
||||
|
||||
// After RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN failed reconnection attempts we can assume the public registry is down
|
||||
const RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN = 1;
|
||||
|
||||
function isRegistryDown(reconnectionAttempts: number): boolean {
|
||||
return reconnectionAttempts > RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
inner: {
|
||||
width: 'min(480px, 100%)',
|
||||
minHeight: '150px',
|
||||
display: 'grid',
|
||||
padding: theme.spacing(1),
|
||||
gridGap: theme.spacing(1),
|
||||
},
|
||||
header: {
|
||||
padding: 0,
|
||||
},
|
||||
headerText: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
providerInfo: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
swapIconOuter: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
swapIcon: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
noProvidersAlertOuter: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
noProvidersAlertButtonsOuter: {
|
||||
display: 'flex',
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
function Title() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.header}>
|
||||
<Typography variant="h5" className={classes.headerText}>
|
||||
Swap
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function HasProviderSwapWidget({
|
||||
selectedProvider,
|
||||
}: {
|
||||
selectedProvider: ExtendedProviderStatus;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
const forceShowDialog = useAppSelector((state) =>
|
||||
isSwapState(state.swap.state),
|
||||
);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [btcFieldValue, setBtcFieldValue] = useState<number | string>(
|
||||
satsToBtc(selectedProvider.minSwapAmount),
|
||||
);
|
||||
const [xmrFieldValue, setXmrFieldValue] = useState(1);
|
||||
|
||||
function onBtcAmountChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
setBtcFieldValue(event.target.value);
|
||||
}
|
||||
|
||||
function updateXmrValue() {
|
||||
const parsedBtcAmount = Number(btcFieldValue);
|
||||
if (Number.isNaN(parsedBtcAmount)) {
|
||||
setXmrFieldValue(0);
|
||||
} else {
|
||||
const convertedXmrAmount =
|
||||
parsedBtcAmount / satsToBtc(selectedProvider.price);
|
||||
setXmrFieldValue(convertedXmrAmount);
|
||||
}
|
||||
}
|
||||
|
||||
function getBtcFieldError(): string | null {
|
||||
const parsedBtcAmount = Number(btcFieldValue);
|
||||
if (Number.isNaN(parsedBtcAmount)) {
|
||||
return 'This is not a valid number';
|
||||
}
|
||||
if (parsedBtcAmount < satsToBtc(selectedProvider.minSwapAmount)) {
|
||||
return `The minimum swap amount is ${satsToBtc(
|
||||
selectedProvider.minSwapAmount,
|
||||
)} BTC. Switch to a different provider if you want to swap less.`;
|
||||
}
|
||||
if (parsedBtcAmount > satsToBtc(selectedProvider.maxSwapAmount)) {
|
||||
return `The maximum swap amount is ${satsToBtc(
|
||||
selectedProvider.maxSwapAmount,
|
||||
)} BTC. Switch to a different provider if you want to swap more.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleGuideDialogOpen() {
|
||||
setShowDialog(true);
|
||||
}
|
||||
|
||||
useEffect(updateXmrValue, [btcFieldValue, selectedProvider]);
|
||||
|
||||
return (
|
||||
// 'elevation' prop can't be passed down (type def issue)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<Box className={classes.inner} component={Paper} elevation={5}>
|
||||
<Title />
|
||||
<TextField
|
||||
label="Send"
|
||||
size="medium"
|
||||
variant="outlined"
|
||||
value={btcFieldValue}
|
||||
onChange={onBtcAmountChange}
|
||||
error={!!getBtcFieldError()}
|
||||
helperText={getBtcFieldError()}
|
||||
autoFocus
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">BTC</InputAdornment>,
|
||||
}}
|
||||
/>
|
||||
<Box className={classes.swapIconOuter}>
|
||||
<ArrowDownwardIcon fontSize="small" />
|
||||
</Box>
|
||||
<TextField
|
||||
label="Receive"
|
||||
variant="outlined"
|
||||
size="medium"
|
||||
value={xmrFieldValue.toFixed(6)}
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">XMR</InputAdornment>,
|
||||
}}
|
||||
/>
|
||||
<ProviderSelect />
|
||||
<Fab variant="extended" color="primary" onClick={handleGuideDialogOpen}>
|
||||
<SwapHorizIcon className={classes.swapIcon} />
|
||||
Swap
|
||||
</Fab>
|
||||
<SwapDialog
|
||||
open={showDialog || forceShowDialog}
|
||||
onClose={() => setShowDialog(false)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function HasNoProvidersSwapWidget() {
|
||||
const forceShowDialog = useAppSelector((state) =>
|
||||
isSwapState(state.swap.state),
|
||||
);
|
||||
const isPublicRegistryDown = useAppSelector((state) =>
|
||||
isRegistryDown(
|
||||
state.providers.registry.failedReconnectAttemptsSinceLastSuccess,
|
||||
),
|
||||
);
|
||||
const classes = useStyles();
|
||||
|
||||
const alertBox = isPublicRegistryDown ? (
|
||||
<Alert severity="info">
|
||||
<Box className={classes.noProvidersAlertOuter}>
|
||||
<Typography>
|
||||
Currently, the public registry of providers seems to be unreachable.
|
||||
Here's what you can do:
|
||||
<ul>
|
||||
<li>
|
||||
Try discovering a provider by connecting to a rendezvous point
|
||||
</li>
|
||||
<li>
|
||||
Try again later when the public registry may be reachable again
|
||||
</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
<Box>
|
||||
<ListSellersDialogOpenButton />
|
||||
</Box>
|
||||
</Box>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert severity="info">
|
||||
<Box className={classes.noProvidersAlertOuter}>
|
||||
<Typography>
|
||||
Currently, there are no providers (trading partners) available in the
|
||||
official registry. Here's what you can do:
|
||||
<ul>
|
||||
<li>
|
||||
Try discovering a provider by connecting to a rendezvous point
|
||||
</li>
|
||||
<li>Add a new provider to the public registry</li>
|
||||
<li>Try again later when more providers may be available</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
<Box>
|
||||
<ProviderSubmitDialogOpenButton />
|
||||
<ListSellersDialogOpenButton />
|
||||
</Box>
|
||||
</Box>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{alertBox}
|
||||
<SwapDialog open={forceShowDialog} onClose={() => {}} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderLoadingSwapWidget() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
// 'elevation' prop can't be passed down (type def issue)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<Box className={classes.inner} component={Paper} elevation={15}>
|
||||
<Title />
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SwapWidget() {
|
||||
const selectedProvider = useAppSelector(
|
||||
(state) => state.providers.selectedProvider,
|
||||
);
|
||||
// If we fail more than RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN reconnect attempts, we'll show the "no providers" widget. We can assume the public registry is down.
|
||||
const providerLoading = useAppSelector(
|
||||
(state) =>
|
||||
state.providers.registry.providers === null &&
|
||||
!isRegistryDown(
|
||||
state.providers.registry.failedReconnectAttemptsSinceLastSuccess,
|
||||
),
|
||||
);
|
||||
|
||||
if (providerLoading) {
|
||||
return <ProviderLoadingSwapWidget />;
|
||||
}
|
||||
if (selectedProvider) {
|
||||
return <HasProviderSwapWidget selectedProvider={selectedProvider} />;
|
||||
}
|
||||
return <HasNoProvidersSwapWidget />;
|
||||
}
|
31
src-gui/src/renderer/components/pages/wallet/WalletPage.tsx
Normal file
31
src-gui/src/renderer/components/pages/wallet/WalletPage.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Box, makeStyles, Typography } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import WithdrawWidget from './WithdrawWidget';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gridGap: theme.spacing(0.5),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function WalletPage() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Typography variant="h3">Wallet</Typography>
|
||||
<Alert severity="info">
|
||||
You do not have to deposit money before starting a swap. Instead, you
|
||||
will be greeted with a deposit address after you initiate one.
|
||||
</Alert>
|
||||
<Typography variant="subtitle1">
|
||||
If funds are left in your wallet after a swap, you can withdraw them to
|
||||
your wallet. If you decide to leave them inside the internal wallet, the
|
||||
funds will automatically be used when starting a new swap.
|
||||
</Typography>
|
||||
<WithdrawWidget />
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { Button, CircularProgress, IconButton } from '@material-ui/core';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import IpcInvokeButton from '../../IpcInvokeButton';
|
||||
import { checkBitcoinBalance } from 'renderer/rpc';
|
||||
|
||||
export default function WalletRefreshButton() {
|
||||
return <IconButton onClick={() => checkBitcoinBalance(true)}>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { Box, Button, makeStyles, Typography } from '@material-ui/core';
|
||||
import { useState } from 'react';
|
||||
import SendIcon from '@material-ui/icons/Send';
|
||||
import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks';
|
||||
import { RpcMethod } from 'models/rpcModel';
|
||||
import BitcoinIcon from '../../icons/BitcoinIcon';
|
||||
import WithdrawDialog from '../../modal/wallet/WithdrawDialog';
|
||||
import WalletRefreshButton from './WalletRefreshButton';
|
||||
import InfoBox from '../../modal/swap/InfoBox';
|
||||
import { SatsAmount } from 'renderer/components/other/Units';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
title: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function WithdrawWidget() {
|
||||
const classes = useStyles();
|
||||
const walletBalance = useAppSelector((state) => state.rpc.state.balance);
|
||||
const checkingBalance = useIsRpcEndpointBusy(RpcMethod.GET_BTC_BALANCE);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
function onShowDialog() {
|
||||
setShowDialog(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<InfoBox
|
||||
title={
|
||||
<Box className={classes.title}>
|
||||
Wallet Balance
|
||||
<WalletRefreshButton />
|
||||
</Box>
|
||||
}
|
||||
mainContent={
|
||||
<Typography variant="h5">
|
||||
<SatsAmount amount={walletBalance} />
|
||||
</Typography>
|
||||
}
|
||||
icon={<BitcoinIcon />}
|
||||
additionalContent={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
endIcon={<SendIcon />}
|
||||
size="large"
|
||||
onClick={onShowDialog}
|
||||
disabled={
|
||||
walletBalance === null || checkingBalance || walletBalance <= 0
|
||||
}
|
||||
>
|
||||
Withdraw
|
||||
</Button>
|
||||
}
|
||||
loading={false}
|
||||
/>
|
||||
<WithdrawDialog open={showDialog} onClose={() => setShowDialog(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue