feat(gui): Migrate to Tauri events

- Replace Electron IPC with Tauri invoke() for API calls
- Implement TauriSwapProgressEvent for state management
- Remove IpcInvokeButton, replace with PromiseInvokeButton
- Update models: new tauriModel.ts, refactor rpcModel.ts
- Simplify SwapSlice state, remove processRunning flag
- Refactor SwapStatePage to use TauriSwapProgressEvent
- Update HistoryRow and HistoryRowActions for new data structures
- Remove unused Electron-specific components (e.g., RpcStatusAlert)
- Update dependencies: React 18, Material-UI v4 to v5
- Implement typeshare for Rust/TypeScript type synchronization
- Add BobStateName enum for more precise swap state tracking
- Refactor utility functions for Tauri compatibility
- Remove JSONStream and other Electron-specific dependencies
This commit is contained in:
binarybaron 2024-08-26 15:32:28 +02:00
parent d54f5c6c77
commit cf641bc8bb
No known key found for this signature in database
GPG key ID: 99B75D3E1476A26E
77 changed files with 2484 additions and 2167 deletions

View file

@ -3,7 +3,7 @@ import FolderOpenIcon from "@material-ui/icons/FolderOpen";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import StopIcon from "@material-ui/icons/Stop";
import { RpcProcessStateType } from "models/rpcModel";
import IpcInvokeButton from "renderer/components/IpcInvokeButton";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { useAppSelector } from "store/hooks";
import InfoBox from "../../modal/swap/InfoBox";
import CliLogsBox from "../../other/RenderedCliLog";
@ -36,34 +36,34 @@ export default function RpcControlBox() {
}
additionalContent={
<Box className={classes.actionsOuter}>
<IpcInvokeButton
<PromiseInvokeButton
variant="contained"
ipcChannel="spawn-start-rpc"
ipcArgs={[]}
endIcon={<PlayArrowIcon />}
disabled={isRunning}
requiresRpc={false}
onClick={() => {
throw new Error("Not implemented");
}}
>
Start Daemon
</IpcInvokeButton>
<IpcInvokeButton
</PromiseInvokeButton>
<PromiseInvokeButton
variant="contained"
ipcChannel="stop-cli"
ipcArgs={[]}
endIcon={<StopIcon />}
disabled={!isRunning}
requiresRpc={false}
onClick={() => {
throw new Error("Not implemented");
}}
>
Stop Daemon
</IpcInvokeButton>
<IpcInvokeButton
ipcChannel="open-data-dir-in-file-explorer"
ipcArgs={[]}
</PromiseInvokeButton>
<PromiseInvokeButton
endIcon={<FolderOpenIcon />}
requiresRpc={false}
isIconButton
size="small"
tooltipTitle="Open the data directory of the Swap Daemon in your file explorer"
onClick={() => {
throw new Error("Not implemented");
}}
/>
</Box>
}

View file

@ -1,7 +1,7 @@
import { Box, makeStyles, Typography } from "@material-ui/core";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import StopIcon from "@material-ui/icons/Stop";
import IpcInvokeButton from "renderer/components/IpcInvokeButton";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { useAppSelector } from "store/hooks";
import InfoBox from "../../modal/swap/InfoBox";
import CliLogsBox from "../../other/RenderedCliLog";
@ -42,26 +42,26 @@ export default function TorInfoBox() {
}
additionalContent={
<Box className={classes.actionsOuter}>
<IpcInvokeButton
<PromiseInvokeButton
variant="contained"
disabled={isTorRunning}
ipcChannel="spawn-tor"
ipcArgs={[]}
endIcon={<PlayArrowIcon />}
requiresRpc={false}
onClick={() => {
throw new Error("Not implemented");
}}
>
Start Tor
</IpcInvokeButton>
<IpcInvokeButton
</PromiseInvokeButton>
<PromiseInvokeButton
variant="contained"
disabled={!isTorRunning}
ipcChannel="stop-tor"
ipcArgs={[]}
endIcon={<StopIcon />}
requiresRpc={false}
onClick={() => {
throw new Error("Not implemented");
}}
>
Stop Tor
</IpcInvokeButton>
</PromiseInvokeButton>
</Box>
}
icon={null}

View file

@ -1,11 +1,11 @@
import { Typography } from "@material-ui/core";
import { useIsSwapRunning } from "store/hooks";
import { useAppSelector } from "store/hooks";
import SwapTxLockAlertsBox from "../../alert/SwapTxLockAlertsBox";
import SwapDialog from "../../modal/swap/SwapDialog";
import HistoryTable from "./table/HistoryTable";
export default function HistoryPage() {
const showDialog = useIsSwapRunning();
const showDialog = useAppSelector((state) => state.swap.state !== null);
return (
<>

View file

@ -6,23 +6,14 @@ import {
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 { GetSwapInfoResponse } from "models/tauriModel";
import { useState } from "react";
import { PiconeroAmount, SatsAmount } from "../../../other/Units";
import HistoryRowActions from "./HistoryRowActions";
import HistoryRowExpanded from "./HistoryRowExpanded";
import { BitcoinAmount, MoneroAmount } from "../../../other/Units";
type HistoryRowProps = {
swap: GetSwapInfoResponse;
};
const useStyles = makeStyles((theme) => ({
amountTransferContainer: {
@ -43,17 +34,14 @@ function AmountTransfer({
return (
<Box className={classes.amountTransferContainer}>
<BitcoinAmount amount={btcAmount} />
<SatsAmount amount={btcAmount} />
<ArrowForwardIcon />
<MoneroAmount amount={xmrAmount} />
<PiconeroAmount amount={xmrAmount} />
</Box>
);
}
export default function HistoryRow({ swap }: HistoryRowProps) {
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
export default function HistoryRow(swap: GetSwapInfoResponse) {
const [expanded, setExpanded] = useState(false);
return (
@ -64,13 +52,16 @@ export default function HistoryRow({ swap }: HistoryRowProps) {
{expanded ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{swap.swap_id.substring(0, 5)}...</TableCell>
<TableCell>{swap.swap_id}</TableCell>
<TableCell>
<AmountTransfer xmrAmount={xmrAmount} btcAmount={btcAmount} />
<AmountTransfer
xmrAmount={swap.xmr_amount}
btcAmount={swap.btc_amount}
/>
</TableCell>
<TableCell>{getHumanReadableDbStateType(swap.state_name)}</TableCell>
<TableCell>{swap.state_name.toString()}</TableCell>
<TableCell>
<HistoryRowActions swap={swap} />
<HistoryRowActions {...swap} />
</TableCell>
</TableRow>

View file

@ -1,68 +1,65 @@
import { Tooltip } from "@material-ui/core";
import Button, { ButtonProps } from "@material-ui/core/Button/Button";
import { ButtonProps } from "@material-ui/core/Button/Button";
import { green, red } from "@material-ui/core/colors";
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 } from "models/tauriModel";
import {
GetSwapInfoResponse,
SwapStateName,
isSwapStateNamePossiblyCancellableSwap,
isSwapStateNamePossiblyRefundableSwap,
} from "../../../../../models/rpcModel";
BobStateName,
GetSwapInfoResponseExt,
isBobStateNamePossiblyCancellableSwap,
isBobStateNamePossiblyRefundableSwap,
} from "models/tauriModelExt";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { resumeSwap } from "renderer/rpc";
export function SwapResumeButton({
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
}: ButtonProps & { swap: GetSwapInfoResponse }) {
return (
<IpcInvokeButton
<PromiseInvokeButton
variant="contained"
color="primary"
disabled={swap.completed}
ipcChannel="spawn-resume-swap"
ipcArgs={[swap.swap_id]}
endIcon={<PlayArrowIcon />}
requiresRpc
onClick={() => resumeSwap(swap.swap_id)}
{...props}
>
Resume
</IpcInvokeButton>
</PromiseInvokeButton>
);
}
export function SwapCancelRefundButton({
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
}: { swap: GetSwapInfoResponseExt } & ButtonProps) {
const cancelOrRefundable =
isSwapStateNamePossiblyCancellableSwap(swap.state_name) ||
isSwapStateNamePossiblyRefundableSwap(swap.state_name);
isBobStateNamePossiblyCancellableSwap(swap.state_name) ||
isBobStateNamePossiblyRefundableSwap(swap.state_name);
if (!cancelOrRefundable) {
return <></>;
}
return (
<IpcInvokeButton
ipcChannel="spawn-cancel-refund"
ipcArgs={[swap.swap_id]}
requiresRpc
<PromiseInvokeButton
displayErrorSnackbar={false}
{...props}
onClick={async () => {
// TODO: Implement this using the Tauri RPC
throw new Error("Not implemented");
}}
>
Attempt manual Cancel & Refund
</IpcInvokeButton>
</PromiseInvokeButton>
);
}
export default function HistoryRowActions({
swap,
}: {
swap: GetSwapInfoResponse;
}) {
if (swap.state_name === SwapStateName.XmrRedeemed) {
export default function HistoryRowActions(swap: GetSwapInfoResponse) {
if (swap.state_name === BobStateName.XmrRedeemed) {
return (
<Tooltip title="The swap is completed because you have redeemed the XMR">
<DoneIcon style={{ color: green[500] }} />
@ -70,7 +67,7 @@ export default function HistoryRowActions({
);
}
if (swap.state_name === SwapStateName.BtcRefunded) {
if (swap.state_name === BobStateName.BtcRefunded) {
return (
<Tooltip title="The swap is completed because your BTC have been refunded">
<DoneIcon style={{ color: green[500] }} />
@ -78,7 +75,9 @@ export default function HistoryRowActions({
);
}
if (swap.state_name === SwapStateName.BtcPunished) {
// TODO: Display a button here to attempt a cooperative redeem
// See this PR: https://github.com/UnstoppableSwap/unstoppableswap-gui/pull/212
if (swap.state_name === BobStateName.BtcPunished) {
return (
<Tooltip title="The swap is completed because you have been punished">
<ErrorIcon style={{ color: red[500] }} />

View file

@ -8,24 +8,15 @@ import {
TableContainer,
TableRow,
} from "@material-ui/core";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import { isTestnet } from "store/config";
import { GetSwapInfoResponse } from "models/tauriModel";
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,
PiconeroAmount,
SatsAmount,
} from "renderer/components/other/Units";
import { isTestnet } from "store/config";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import SwapLogFileOpenButton from "./SwapLogFileOpenButton";
const useStyles = makeStyles((theme) => ({
outer: {
@ -47,12 +38,6 @@ export default function HistoryRowExpanded({
}) {
const classes = useStyles();
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>
@ -60,7 +45,7 @@ export default function HistoryRowExpanded({
<TableBody>
<TableRow>
<TableCell>Started on</TableCell>
<TableCell>{startDate}</TableCell>
<TableCell>{swap.start_date}</TableCell>
</TableRow>
<TableRow>
<TableCell>Swap ID</TableCell>
@ -68,38 +53,39 @@ export default function HistoryRowExpanded({
</TableRow>
<TableRow>
<TableCell>State Name</TableCell>
<TableCell>
{getHumanReadableDbStateType(swap.state_name)}
</TableCell>
<TableCell>{swap.state_name}</TableCell>
</TableRow>
<TableRow>
<TableCell>Monero Amount</TableCell>
<TableCell>
<MoneroAmount amount={xmrAmount} />
<PiconeroAmount amount={swap.xmr_amount} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin Amount</TableCell>
<TableCell>
<BitcoinAmount amount={btcAmount} />
<SatsAmount amount={swap.btc_amount} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Exchange Rate</TableCell>
<TableCell>
<MoneroBitcoinExchangeRate rate={exchangeRate} />
<MoneroBitcoinExchangeRate
satsAmount={swap.btc_amount}
piconeroAmount={swap.xmr_amount}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin Network Fees</TableCell>
<TableCell>
<BitcoinAmount amount={txFees} />
<SatsAmount amount={swap.tx_lock_fee} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Provider Address</TableCell>
<TableCell>
<Box>{seller.addresses.join(", ")}</Box>
<Box>{swap.seller.addresses.join(", ")}</Box>
</TableCell>
</TableRow>
<TableRow>
@ -122,12 +108,16 @@ export default function HistoryRowExpanded({
variant="outlined"
size="small"
/>
<SwapCancelRefundButton swap={swap} variant="contained" size="small" />
<SwapMoneroRecoveryButton
swap={swap}
variant="contained"
size="small"
/>
{/*
// TOOD: reimplement these buttons using Tauri
<SwapCancelRefundButton swap={swap} variant="contained" size="small" />
<SwapMoneroRecoveryButton
swap={swap}
variant="contained"
size="small"
/>
*/}
</Box>
</Box>
);

View file

@ -9,12 +9,7 @@ import {
TableHead,
TableRow,
} from "@material-ui/core";
import { sortBy } from "lodash";
import { parseDateString } from "utils/parseUtils";
import {
useAppSelector,
useSwapInfosSortedByDate,
} from "../../../../../store/hooks";
import { useSwapInfosSortedByDate } from "../../../../../store/hooks";
import HistoryRow from "./HistoryRow";
const useStyles = makeStyles((theme) => ({
@ -43,7 +38,7 @@ export default function HistoryTable() {
</TableHead>
<TableBody>
{swapSortedByDate.map((swap) => (
<HistoryRow swap={swap} key={swap.swap_id} />
<HistoryRow {...swap} key={swap.swap_id} />
))}
</TableBody>
</Table>

View file

@ -1,4 +1,3 @@
import { ButtonProps } from "@material-ui/core/Button/Button";
import {
Button,
Dialog,
@ -6,9 +5,10 @@ import {
DialogContent,
DialogTitle,
} from "@material-ui/core";
import { useState } from "react";
import { ButtonProps } from "@material-ui/core/Button/Button";
import { CliLog } from "models/cliModel";
import IpcInvokeButton from "../../../IpcInvokeButton";
import { useState } from "react";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import CliLogsBox from "../../../other/RenderedCliLog";
export default function SwapLogFileOpenButton({
@ -19,16 +19,17 @@ export default function SwapLogFileOpenButton({
return (
<>
<IpcInvokeButton
ipcArgs={[swapId]}
ipcChannel="get-swap-logs"
<PromiseInvokeButton
onSuccess={(data) => {
setLogs(data as CliLog[]);
}}
onClick={async () => {
throw new Error("Not implemented");
}}
{...props}
>
view log
</IpcInvokeButton>
View log
</PromiseInvokeButton>
{logs && (
<Dialog open onClose={() => setLogs(null)} fullWidth maxWidth="lg">
<DialogTitle>Logs of swap {swapId}</DialogTitle>

View file

@ -1,4 +1,3 @@
import { ButtonProps } from "@material-ui/core/Button/Button";
import {
Box,
Button,
@ -8,17 +7,17 @@ import {
DialogContentText,
Link,
} from "@material-ui/core";
import { useAppDispatch, useAppSelector } from "store/hooks";
import { ButtonProps } from "@material-ui/core/Button/Button";
import { GetSwapInfoArgs } from "models/tauriModel";
import { rpcResetMoneroRecoveryKeys } from "store/features/rpcSlice";
import {
GetSwapInfoResponse,
isSwapMoneroRecoverable,
} from "../../../../../models/rpcModel";
import IpcInvokeButton from "../../../IpcInvokeButton";
import { useAppDispatch, useAppSelector } from "store/hooks";
import DialogHeader from "../../../modal/DialogHeader";
import ScrollablePaperTextBox from "../../../other/ScrollablePaperTextBox";
function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
function MoneroRecoveryKeysDialog() {
// TODO: Reimplement this using the new Tauri API
return null;
const dispatch = useAppDispatch();
const keys = useAppSelector((s) => s.rpc.state.moneroRecovery);
@ -96,24 +95,28 @@ function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
export function SwapMoneroRecoveryButton({
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
}: { swap: GetSwapInfoArgs } & ButtonProps) {
return <> </>;
/* TODO: Reimplement this using the new Tauri API
const isRecoverable = isSwapMoneroRecoverable(swap.state_name);
if (!isRecoverable) {
return <></>;
}
return (
<>
<IpcInvokeButton
ipcChannel="spawn-monero-recovery"
ipcArgs={[swap.swap_id]}
requiresRpc
<PromiseInvokeButton
onClick={async () => {
throw new Error("Not implemented");
}}
{...props}
>
Display Monero Recovery Keys
</IpcInvokeButton>
</PromiseInvokeButton>
<MoneroRecoveryKeysDialog swap={swap} />
</>
);
*/
}

View file

@ -1,27 +1,26 @@
import { ChangeEvent, useEffect, useState } from "react";
import {
makeStyles,
Box,
Paper,
Typography,
TextField,
LinearProgress,
Fab,
LinearProgress,
makeStyles,
Paper,
TextField,
Typography,
} 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 { ChangeEvent, useEffect, useState } from "react";
import { useAppSelector } from "store/hooks";
import { satsToBtc } from "utils/conversionUtils";
import {
ListSellersDialogOpenButton,
ProviderSubmitDialogOpenButton,
} from "../../modal/provider/ProviderListDialog";
import ProviderSelect from "../../modal/provider/ProviderSelect";
import SwapDialog from "../../modal/swap/SwapDialog";
// After RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN failed reconnection attempts we can assume the public registry is down
const RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN = 1;
@ -84,9 +83,7 @@ function HasProviderSwapWidget({
}) {
const classes = useStyles();
const forceShowDialog = useAppSelector((state) =>
isSwapState(state.swap.state),
);
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
const [showDialog, setShowDialog] = useState(false);
const [btcFieldValue, setBtcFieldValue] = useState<number | string>(
satsToBtc(selectedProvider.minSwapAmount),
@ -177,9 +174,7 @@ function HasProviderSwapWidget({
}
function HasNoProvidersSwapWidget() {
const forceShowDialog = useAppSelector((state) =>
isSwapState(state.swap.state),
);
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
const isPublicRegistryDown = useAppSelector((state) =>
isRegistryDown(
state.providers.registry.failedReconnectAttemptsSinceLastSuccess,

View file

@ -1,8 +1,6 @@
import { Button, CircularProgress, IconButton } from "@material-ui/core";
import RefreshIcon from "@material-ui/icons/Refresh";
import IpcInvokeButton from "../../IpcInvokeButton";
import { checkBitcoinBalance } from "renderer/rpc";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { checkBitcoinBalance } from "renderer/rpc";
export default function WalletRefreshButton() {
return (