Skip to content

Commit

Permalink
feat(tauri): Initialize Context in background (#59)
Browse files Browse the repository at this point in the history
This PR does the following:
- The Context (including Bitcoin wallet, Monero wallet, ...) is initialized in the background. This allows the window to be displayed instantly upon startup.
- Host sends events to Guest about progress of Context initialization. Those events are used to display an alert in the navigation bar.
- If a Tauri command is invoked which requires the Context to be available, an error will be returned
- As soon as the Context becomes available the `Guest` requests the history and Bitcoin balance
- Re-enables Material UI animations
  • Loading branch information
binarybaron authored Sep 3, 2024
1 parent 792fbbf commit e4141c7
Show file tree
Hide file tree
Showing 17 changed files with 369 additions and 191 deletions.
12 changes: 2 additions & 10 deletions src-gui/src/renderer/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,11 @@ const theme = createTheme({
},
secondary: indigo,
},
transitions: {
create: () => "none",
},
props: {
MuiButtonBase: {
disableRipple: true,
},
},
typography: {
overline: {
textTransform: 'none', // This prevents the text from being all caps
textTransform: "none", // This prevents the text from being all caps
},
}
},
});

function InnerContent() {
Expand Down
85 changes: 56 additions & 29 deletions src-gui/src/renderer/components/PromiseInvokeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,44 @@
import { Button, ButtonProps, IconButton } from "@material-ui/core";
import {
Button,
ButtonProps,
IconButton,
IconButtonProps,
Tooltip,
} from "@material-ui/core";
import CircularProgress from "@material-ui/core/CircularProgress";
import { useSnackbar } from "notistack";
import { ReactNode, useState } from "react";
import { useIsContextAvailable } from "store/hooks";

interface PromiseInvokeButtonProps<T> {
onSuccess?: (data: T) => void;
onSuccess: (data: T) => void | null;
onClick: () => Promise<T>;
onPendingChange?: (isPending: boolean) => void;
isLoadingOverride?: boolean;
isIconButton?: boolean;
loadIcon?: ReactNode;
disabled?: boolean;
displayErrorSnackbar?: boolean;
tooltipTitle?: string;
onPendingChange: (isPending: boolean) => void | null;
isLoadingOverride: boolean;
isIconButton: boolean;
loadIcon: ReactNode;
disabled: boolean;
displayErrorSnackbar: boolean;
tooltipTitle: string | null;
requiresContext: boolean;
}

export default function PromiseInvokeButton<T>({
disabled,
onSuccess,
disabled = false,
onSuccess = null,
onClick,
endIcon,
loadIcon,
isLoadingOverride,
isIconButton,
displayErrorSnackbar,
onPendingChange,
loadIcon = null,
isLoadingOverride = false,
isIconButton = false,
displayErrorSnackbar = false,
onPendingChange = null,
requiresContext = true,
tooltipTitle = null,
...rest
}: ButtonProps & PromiseInvokeButtonProps<T>) {
const { enqueueSnackbar } = useSnackbar();
const isContextAvailable = useIsContextAvailable();

const [isPending, setIsPending] = useState(false);

Expand All @@ -36,7 +47,7 @@ export default function PromiseInvokeButton<T>({
? loadIcon || <CircularProgress size={24} />
: endIcon;

async function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
async function handleClick() {
if (!isPending) {
try {
onPendingChange?.(true);
Expand All @@ -57,18 +68,34 @@ export default function PromiseInvokeButton<T>({
}
}

const isDisabled = disabled || isLoading;
const requiresContextButNotAvailable = requiresContext && !isContextAvailable;
const isDisabled = disabled || isLoading || requiresContextButNotAvailable;

return isIconButton ? (
<IconButton onClick={handleClick} disabled={isDisabled} {...(rest as any)}>
{actualEndIcon}
</IconButton>
) : (
<Button
onClick={handleClick}
disabled={isDisabled}
endIcon={actualEndIcon}
{...rest}
/>
const actualTooltipTitle =
(requiresContextButNotAvailable
? "Wait for the application to load all required components"
: tooltipTitle) ?? "";

return (
<Tooltip title={actualTooltipTitle}>
<span>
{isIconButton ? (
<IconButton
onClick={handleClick}
disabled={isDisabled}
{...(rest as IconButtonProps)}
>
{actualEndIcon}
</IconButton>
) : (
<Button
onClick={handleClick}
disabled={isDisabled}
endIcon={actualEndIcon}
{...rest}
/>
)}
</span>
</Tooltip>
);
}
76 changes: 76 additions & 0 deletions src-gui/src/renderer/components/alert/DaemonStatusAlert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { CircularProgress } from "@material-ui/core";
import { Alert, AlertProps } from "@material-ui/lab";
import { TauriContextInitializationProgress } from "models/tauriModel";
import { useState } from "react";
import { useAppSelector } from "store/hooks";
import { exhaustiveGuard } from "utils/typescriptUtils";

const FUNNY_INIT_MESSAGES = [
"Initializing quantum entanglement...",
"Generating one-time pads from cosmic background radiation...",
"Negotiating key exchange with aliens...",
"Optimizing elliptic curves for maximum sneakiness...",
"Transforming plaintext into ciphertext via arcane XOR rituals...",
"Salting your hash with exotic mathematical seasonings...",
"Performing advanced modular arithmetic gymnastics...",
"Consulting the Oracle of Randomness...",
"Executing top-secret permutation protocols...",
"Summoning prime factors from the mathematical aether...",
"Deploying steganographic squirrels to hide your nuts of data...",
"Initializing the quantum superposition of your keys...",
"Applying post-quantum cryptographic voodoo...",
"Encrypting your data with the tears of frustrated regulators...",
];

function LoadingSpinnerAlert({ ...rest }: AlertProps) {
return <Alert icon={<CircularProgress size={22} />} {...rest} />;
}

export default function DaemonStatusAlert() {
const contextStatus = useAppSelector((s) => s.rpc.status);

const [initMessage] = useState(
FUNNY_INIT_MESSAGES[Math.floor(Math.random() * FUNNY_INIT_MESSAGES.length)],
);

if (contextStatus == null) {
return (
<LoadingSpinnerAlert severity="warning">
{initMessage}
</LoadingSpinnerAlert>
);
}

switch (contextStatus.type) {
case "Initializing":
switch (contextStatus.content) {
case TauriContextInitializationProgress.OpeningBitcoinWallet:
return (
<LoadingSpinnerAlert severity="warning">
Connecting to the Bitcoin network
</LoadingSpinnerAlert>
);
case TauriContextInitializationProgress.OpeningMoneroWallet:
return (
<LoadingSpinnerAlert severity="warning">
Connecting to the Monero network
</LoadingSpinnerAlert>
);
case TauriContextInitializationProgress.OpeningDatabase:
return (
<LoadingSpinnerAlert severity="warning">
Opening the local database
</LoadingSpinnerAlert>
);
}
break;
case "Available":
return <Alert severity="success">The daemon is running</Alert>;
case "Failed":
return (
<Alert severity="error">The daemon has stopped unexpectedly</Alert>
);
default:
return exhaustiveGuard(contextStatus);
}
}
30 changes: 0 additions & 30 deletions src-gui/src/renderer/components/alert/RpcStatusAlert.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default function InitPage() {
className={classes.initButton}
endIcon={<PlayArrowIcon />}
onClick={init}
displayErrorSnackbar
>
Start swap
</PromiseInvokeButton>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Box, makeStyles } from "@material-ui/core";
import GitHubIcon from "@material-ui/icons/GitHub";
import RedditIcon from "@material-ui/icons/Reddit";
import DaemonStatusAlert from "../alert/DaemonStatusAlert";
import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
import MoneroWalletRpcUpdatingAlert from "../alert/MoneroWalletRpcUpdatingAlert";
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
Expand Down Expand Up @@ -28,11 +29,7 @@ export default function NavigationFooter() {
<Box className={classes.outer}>
<FundsLeftInWalletAlert />
<UnfinishedSwapsAlert />

{
// TODO: Uncomment when we have implemented a way for the UI to be displayed before the context has been initialized
// <RpcStatusAlert />
}
<DaemonStatusAlert />
<MoneroWalletRpcUpdatingAlert />
<Box className={classes.linksOuter}>
<LinkIconButton url="https://reddit.com/r/unstoppableswap">
Expand Down
14 changes: 5 additions & 9 deletions src-gui/src/renderer/components/pages/help/RpcControlBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { Box, makeStyles } from "@material-ui/core";
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 PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { useAppSelector } from "store/hooks";
import { useIsContextAvailable } from "store/hooks";
import InfoBox from "../../modal/swap/InfoBox";
import CliLogsBox from "../../other/RenderedCliLog";

Expand All @@ -17,20 +16,17 @@ const useStyles = makeStyles((theme) => ({
}));

export default function RpcControlBox() {
const rpcProcess = useAppSelector((state) => state.rpc.process);
const isRunning =
rpcProcess.type === RpcProcessStateType.STARTED ||
rpcProcess.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS;
const isRunning = useIsContextAvailable();
const classes = useStyles();

return (
<InfoBox
title={`Swap Daemon (${rpcProcess.type})`}
title={`Daemon Controller`}
mainContent={
isRunning || rpcProcess.type === RpcProcessStateType.EXITED ? (
isRunning ? (
<CliLogsBox
label="Swap Daemon Logs (current session only)"
logs={rpcProcess.logs}
logs={[]}
/>
) : null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import {
Box,
Link,
makeStyles,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
Box,
Link,
makeStyles,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
} from "@material-ui/core";
import { OpenInNew } from "@material-ui/icons";
import { GetSwapInfoResponse } from "models/tauriModel";
import CopyableMonospaceTextBox from "renderer/components/other/CopyableAddress";
import MonospaceTextBox from "renderer/components/other/InlineCode";
import CopyableMonospaceTextBox from "renderer/components/other/CopyableMonospaceTextBox";
import MonospaceTextBox from "renderer/components/other/MonospaceTextBox";
import {
MoneroBitcoinExchangeRate,
PiconeroAmount,
SatsAmount,
MoneroBitcoinExchangeRate,
PiconeroAmount,
SatsAmount,
} from "renderer/components/other/Units";
import { isTestnet } from "store/config";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
Expand Down
8 changes: 6 additions & 2 deletions src-gui/src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ import {
fetchXmrPrice,
} from "./api";
import App from "./components/App";
import { checkBitcoinBalance, getRawSwapInfos } from "./rpc";
import {
checkBitcoinBalance,
getAllSwapInfos,
initEventListeners,
} from "./rpc";
import { persistor, store } from "./store/storeRenderer";

setInterval(() => {
checkBitcoinBalance();
getRawSwapInfos();
getAllSwapInfos();
}, 30 * 1000);

const container = document.getElementById("root");
Expand Down
Loading

0 comments on commit e4141c7

Please sign in to comment.