feat: swap history tauri connector

This commit is contained in:
binarybaron 2024-08-08 12:02:59 +02:00
parent cdd6635c8f
commit 2e1b6f6b43
No known key found for this signature in database
GPG key ID: 99B75D3E1476A26E
22 changed files with 1315 additions and 1297 deletions

View file

@ -1,86 +1,98 @@
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';
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';
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;
swap: GetSwapInfoResponse;
};
const useStyles = makeStyles((theme) => ({
amountTransferContainer: {
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
},
amountTransferContainer: {
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
},
}));
function AmountTransfer({
btcAmount,
xmrAmount,
btcAmount,
xmrAmount,
}: {
xmrAmount: number;
btcAmount: number;
xmrAmount: number;
btcAmount: number;
}) {
const classes = useStyles();
const classes = useStyles();
return (
<Box className={classes.amountTransferContainer}>
<BitcoinAmount amount={btcAmount} />
<ArrowForwardIcon />
<MoneroAmount amount={xmrAmount} />
</Box>
);
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 btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
const [expanded, setExpanded] = useState(false);
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>
return (
<>
<TableRow>
<TableCell>
<IconButton
size="small"
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<KeyboardArrowUpIcon />
) : (
<KeyboardArrowDownIcon />
)}
</IconButton>
</TableCell>
<TableCell>{swap.swap_id.substring(0, 5)}...</TableCell>
<TableCell>
<AmountTransfer
xmrAmount={xmrAmount}
btcAmount={btcAmount}
/>
</TableCell>
<TableCell>
{getHumanReadableDbStateType(swap.state_name)}
</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>
</>
);
<TableRow>
<TableCell style={{ padding: 0 }} colSpan={6}>
<Collapse in={expanded} timeout="auto">
{expanded && <HistoryRowExpanded swap={swap} />}
</Collapse>
</TableCell>
</TableRow>
</>
);
}

View file

@ -1,90 +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 { 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';
GetSwapInfoResponse,
SwapStateName,
isSwapStateNamePossiblyCancellableSwap,
isSwapStateNamePossiblyRefundableSwap,
} from "../../../../../models/rpcModel";
export function SwapResumeButton({
swap,
...props
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>
);
return (
<IpcInvokeButton
variant="contained"
color="primary"
disabled={swap.completed}
ipcChannel="spawn-resume-swap"
ipcArgs={[swap.swap_id]}
endIcon={<PlayArrowIcon />}
requiresRpc
{...props}
>
Resume
</IpcInvokeButton>
);
}
export function SwapCancelRefundButton({
swap,
...props
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
const cancelOrRefundable =
isSwapStateNamePossiblyCancellableSwap(swap.stateName) ||
isSwapStateNamePossiblyRefundableSwap(swap.stateName);
const cancelOrRefundable =
isSwapStateNamePossiblyCancellableSwap(swap.state_name) ||
isSwapStateNamePossiblyRefundableSwap(swap.state_name);
if (!cancelOrRefundable) {
return <></>;
}
if (!cancelOrRefundable) {
return <></>;
}
return (
<IpcInvokeButton
ipcChannel="spawn-cancel-refund"
ipcArgs={[swap.swapId]}
requiresRpc
displayErrorSnackbar={false}
{...props}
>
Attempt manual Cancel & Refund
</IpcInvokeButton>
);
return (
<IpcInvokeButton
ipcChannel="spawn-cancel-refund"
ipcArgs={[swap.swap_id]}
requiresRpc
displayErrorSnackbar={false}
{...props}
>
Attempt manual Cancel & Refund
</IpcInvokeButton>
);
}
export default function HistoryRowActions({
swap,
swap,
}: {
swap: GetSwapInfoResponse;
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.state_name === 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.state_name === 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>
);
}
if (swap.state_name === SwapStateName.BtcPunished) {
return (
<Tooltip title="The swap is completed because you have been punished">
<ErrorIcon style={{ color: red[500] }} />
</Tooltip>
);
}
return <SwapResumeButton swap={swap} />;
return <SwapResumeButton swap={swap} />;
}

View file

@ -1,134 +1,143 @@
import {
Box,
Link,
makeStyles,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
} from '@material-ui/core';
import { getBitcoinTxExplorerUrl } from 'utils/conversionUtils';
import { isTestnet } from 'store/config';
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';
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';
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),
},
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,
}: {
swap: GetSwapInfoResponse;
swap: GetSwapInfoResponse;
}) {
const classes = useStyles();
const classes = useStyles();
const { seller, startDate } = swap;
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
const txFees = getSwapTxFees(swap);
const exchangeRate = getSwapExchangeRate(swap);
const { seller, start_date: 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>
);
return (
<Box className={classes.outer}>
<TableContainer>
<Table>
<TableBody>
<TableRow>
<TableCell>Started on</TableCell>
<TableCell>{startDate}</TableCell>
</TableRow>
<TableRow>
<TableCell>Swap ID</TableCell>
<TableCell>{swap.swap_id}</TableCell>
</TableRow>
<TableRow>
<TableCell>State Name</TableCell>
<TableCell>
{getHumanReadableDbStateType(swap.state_name)}
</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.tx_lock_id,
isTestnet(),
)}
target="_blank"
>
{swap.tx_lock_id}
</Link>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Box className={classes.actionsOuter}>
<SwapLogFileOpenButton
swapId={swap.swap_id}
variant="outlined"
size="small"
/>
<SwapCancelRefundButton
swap={swap}
variant="contained"
size="small"
/>
<SwapMoneroRecoveryButton
swap={swap}
variant="contained"
size="small"
/>
</Box>
</Box>
);
}

View file

@ -1,53 +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';
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';
useAppSelector,
useSwapInfosSortedByDate,
} from "../../../../../store/hooks";
import HistoryRow from "./HistoryRow";
const useStyles = makeStyles((theme) => ({
outer: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
},
outer: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
},
}));
export default function HistoryTable() {
const classes = useStyles();
const swapSortedByDate = useSwapInfosSortedByDate();
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>
);
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.swap_id} />
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
}

View file

@ -1,119 +1,120 @@
import { ButtonProps } from '@material-ui/core/Button/Button';
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';
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';
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);
const dispatch = useAppDispatch();
const keys = useAppSelector((s) => s.rpc.state.moneroRecovery);
function onClose() {
dispatch(rpcResetMoneroRecoveryKeys());
}
function onClose() {
dispatch(rpcResetMoneroRecoveryKeys());
}
if (keys === null || keys.swapId !== swap.swapId) {
return <></>;
}
if (keys === null || keys.swapId !== swap.swap_id) {
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]}
return (
<Dialog open onClose={onClose} maxWidth="sm" fullWidth>
<DialogHeader
title={`Recovery Keys for swap ${swap.swap_id.substring(0, 5)}...`}
/>
))}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary" variant="contained">
Done
</Button>
</DialogActions>
</Dialog>
);
<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,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
const isRecoverable = isSwapMoneroRecoverable(swap.stateName);
const isRecoverable = isSwapMoneroRecoverable(swap.state_name);
if (!isRecoverable) {
return <></>;
}
if (!isRecoverable) {
return <></>;
}
return (
<>
<IpcInvokeButton
ipcChannel="spawn-monero-recovery"
ipcArgs={[swap.swapId]}
requiresRpc
{...props}
>
Display Monero Recovery Keys
</IpcInvokeButton>
<MoneroRecoveryKeysDialog swap={swap} />
</>
);
return (
<>
<IpcInvokeButton
ipcChannel="spawn-monero-recovery"
ipcArgs={[swap.swap_id]}
requiresRpc
{...props}
>
Display Monero Recovery Keys
</IpcInvokeButton>
<MoneroRecoveryKeysDialog swap={swap} />
</>
);
}