mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-05-02 14:56:10 -04:00
feat(tauri): Initialize Context in background (#59)
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
This commit is contained in:
parent
792fbbf746
commit
e4141c763b
17 changed files with 369 additions and 191 deletions
|
@ -26,19 +26,11 @@ const theme = createTheme({
|
||||||
},
|
},
|
||||||
secondary: indigo,
|
secondary: indigo,
|
||||||
},
|
},
|
||||||
transitions: {
|
|
||||||
create: () => "none",
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
MuiButtonBase: {
|
|
||||||
disableRipple: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
typography: {
|
typography: {
|
||||||
overline: {
|
overline: {
|
||||||
textTransform: 'none', // This prevents the text from being all caps
|
textTransform: "none", // This prevents the text from being all caps
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function InnerContent() {
|
function InnerContent() {
|
||||||
|
|
|
@ -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 CircularProgress from "@material-ui/core/CircularProgress";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
|
import { useIsContextAvailable } from "store/hooks";
|
||||||
|
|
||||||
interface PromiseInvokeButtonProps<T> {
|
interface PromiseInvokeButtonProps<T> {
|
||||||
onSuccess?: (data: T) => void;
|
onSuccess: (data: T) => void | null;
|
||||||
onClick: () => Promise<T>;
|
onClick: () => Promise<T>;
|
||||||
onPendingChange?: (isPending: boolean) => void;
|
onPendingChange: (isPending: boolean) => void | null;
|
||||||
isLoadingOverride?: boolean;
|
isLoadingOverride: boolean;
|
||||||
isIconButton?: boolean;
|
isIconButton: boolean;
|
||||||
loadIcon?: ReactNode;
|
loadIcon: ReactNode;
|
||||||
disabled?: boolean;
|
disabled: boolean;
|
||||||
displayErrorSnackbar?: boolean;
|
displayErrorSnackbar: boolean;
|
||||||
tooltipTitle?: string;
|
tooltipTitle: string | null;
|
||||||
|
requiresContext: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PromiseInvokeButton<T>({
|
export default function PromiseInvokeButton<T>({
|
||||||
disabled,
|
disabled = false,
|
||||||
onSuccess,
|
onSuccess = null,
|
||||||
onClick,
|
onClick,
|
||||||
endIcon,
|
endIcon,
|
||||||
loadIcon,
|
loadIcon = null,
|
||||||
isLoadingOverride,
|
isLoadingOverride = false,
|
||||||
isIconButton,
|
isIconButton = false,
|
||||||
displayErrorSnackbar,
|
displayErrorSnackbar = false,
|
||||||
onPendingChange,
|
onPendingChange = null,
|
||||||
|
requiresContext = true,
|
||||||
|
tooltipTitle = null,
|
||||||
...rest
|
...rest
|
||||||
}: ButtonProps & PromiseInvokeButtonProps<T>) {
|
}: ButtonProps & PromiseInvokeButtonProps<T>) {
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const isContextAvailable = useIsContextAvailable();
|
||||||
|
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
|
@ -36,7 +47,7 @@ export default function PromiseInvokeButton<T>({
|
||||||
? loadIcon || <CircularProgress size={24} />
|
? loadIcon || <CircularProgress size={24} />
|
||||||
: endIcon;
|
: endIcon;
|
||||||
|
|
||||||
async function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
|
async function handleClick() {
|
||||||
if (!isPending) {
|
if (!isPending) {
|
||||||
try {
|
try {
|
||||||
onPendingChange?.(true);
|
onPendingChange?.(true);
|
||||||
|
@ -57,18 +68,34 @@ export default function PromiseInvokeButton<T>({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDisabled = disabled || isLoading;
|
const requiresContextButNotAvailable = requiresContext && !isContextAvailable;
|
||||||
|
const isDisabled = disabled || isLoading || requiresContextButNotAvailable;
|
||||||
|
|
||||||
return isIconButton ? (
|
const actualTooltipTitle =
|
||||||
<IconButton onClick={handleClick} disabled={isDisabled} {...(rest as any)}>
|
(requiresContextButNotAvailable
|
||||||
{actualEndIcon}
|
? "Wait for the application to load all required components"
|
||||||
</IconButton>
|
: tooltipTitle) ?? "";
|
||||||
) : (
|
|
||||||
<Button
|
return (
|
||||||
onClick={handleClick}
|
<Tooltip title={actualTooltipTitle}>
|
||||||
disabled={isDisabled}
|
<span>
|
||||||
endIcon={actualEndIcon}
|
{isIconButton ? (
|
||||||
{...rest}
|
<IconButton
|
||||||
/>
|
onClick={handleClick}
|
||||||
|
disabled={isDisabled}
|
||||||
|
{...(rest as IconButtonProps)}
|
||||||
|
>
|
||||||
|
{actualEndIcon}
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleClick}
|
||||||
|
disabled={isDisabled}
|
||||||
|
endIcon={actualEndIcon}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
76
src-gui/src/renderer/components/alert/DaemonStatusAlert.tsx
Normal file
76
src-gui/src/renderer/components/alert/DaemonStatusAlert.tsx
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,30 +0,0 @@
|
||||||
import { CircularProgress } from "@material-ui/core";
|
|
||||||
import { Alert } from "@material-ui/lab";
|
|
||||||
import { RpcProcessStateType } from "models/rpcModel";
|
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
|
|
||||||
// TODO: Reimplement this using Tauri
|
|
||||||
// Currently the RPC process is always available, so this component is not needed
|
|
||||||
// since the UI is only displayed when the RPC process is available
|
|
||||||
export default function RpcStatusAlert() {
|
|
||||||
const rpcProcess = useAppSelector((s) => s.rpc.process);
|
|
||||||
if (rpcProcess.type === RpcProcessStateType.STARTED) {
|
|
||||||
return (
|
|
||||||
<Alert severity="warning" icon={<CircularProgress size={22} />}>
|
|
||||||
The swap daemon is starting
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (rpcProcess.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS) {
|
|
||||||
return <Alert severity="success">The swap daemon is running</Alert>;
|
|
||||||
}
|
|
||||||
if (rpcProcess.type === RpcProcessStateType.NOT_STARTED) {
|
|
||||||
return <Alert severity="warning">The swap daemon is being started</Alert>;
|
|
||||||
}
|
|
||||||
if (rpcProcess.type === RpcProcessStateType.EXITED) {
|
|
||||||
return (
|
|
||||||
<Alert severity="error">The swap daemon has stopped unexpectedly</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <></>;
|
|
||||||
}
|
|
|
@ -72,6 +72,7 @@ export default function InitPage() {
|
||||||
className={classes.initButton}
|
className={classes.initButton}
|
||||||
endIcon={<PlayArrowIcon />}
|
endIcon={<PlayArrowIcon />}
|
||||||
onClick={init}
|
onClick={init}
|
||||||
|
displayErrorSnackbar
|
||||||
>
|
>
|
||||||
Start swap
|
Start swap
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Box, makeStyles } from "@material-ui/core";
|
import { Box, makeStyles } from "@material-ui/core";
|
||||||
import GitHubIcon from "@material-ui/icons/GitHub";
|
import GitHubIcon from "@material-ui/icons/GitHub";
|
||||||
import RedditIcon from "@material-ui/icons/Reddit";
|
import RedditIcon from "@material-ui/icons/Reddit";
|
||||||
|
import DaemonStatusAlert from "../alert/DaemonStatusAlert";
|
||||||
import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
|
import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
|
||||||
import MoneroWalletRpcUpdatingAlert from "../alert/MoneroWalletRpcUpdatingAlert";
|
import MoneroWalletRpcUpdatingAlert from "../alert/MoneroWalletRpcUpdatingAlert";
|
||||||
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
|
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
|
||||||
|
@ -28,11 +29,7 @@ export default function NavigationFooter() {
|
||||||
<Box className={classes.outer}>
|
<Box className={classes.outer}>
|
||||||
<FundsLeftInWalletAlert />
|
<FundsLeftInWalletAlert />
|
||||||
<UnfinishedSwapsAlert />
|
<UnfinishedSwapsAlert />
|
||||||
|
<DaemonStatusAlert />
|
||||||
{
|
|
||||||
// TODO: Uncomment when we have implemented a way for the UI to be displayed before the context has been initialized
|
|
||||||
// <RpcStatusAlert />
|
|
||||||
}
|
|
||||||
<MoneroWalletRpcUpdatingAlert />
|
<MoneroWalletRpcUpdatingAlert />
|
||||||
<Box className={classes.linksOuter}>
|
<Box className={classes.linksOuter}>
|
||||||
<LinkIconButton url="https://reddit.com/r/unstoppableswap">
|
<LinkIconButton url="https://reddit.com/r/unstoppableswap">
|
||||||
|
|
|
@ -2,9 +2,8 @@ import { Box, makeStyles } from "@material-ui/core";
|
||||||
import FolderOpenIcon from "@material-ui/icons/FolderOpen";
|
import FolderOpenIcon from "@material-ui/icons/FolderOpen";
|
||||||
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||||
import StopIcon from "@material-ui/icons/Stop";
|
import StopIcon from "@material-ui/icons/Stop";
|
||||||
import { RpcProcessStateType } from "models/rpcModel";
|
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useIsContextAvailable } from "store/hooks";
|
||||||
import InfoBox from "../../modal/swap/InfoBox";
|
import InfoBox from "../../modal/swap/InfoBox";
|
||||||
import CliLogsBox from "../../other/RenderedCliLog";
|
import CliLogsBox from "../../other/RenderedCliLog";
|
||||||
|
|
||||||
|
@ -17,20 +16,17 @@ const useStyles = makeStyles((theme) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default function RpcControlBox() {
|
export default function RpcControlBox() {
|
||||||
const rpcProcess = useAppSelector((state) => state.rpc.process);
|
const isRunning = useIsContextAvailable();
|
||||||
const isRunning =
|
|
||||||
rpcProcess.type === RpcProcessStateType.STARTED ||
|
|
||||||
rpcProcess.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS;
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfoBox
|
<InfoBox
|
||||||
title={`Swap Daemon (${rpcProcess.type})`}
|
title={`Daemon Controller`}
|
||||||
mainContent={
|
mainContent={
|
||||||
isRunning || rpcProcess.type === RpcProcessStateType.EXITED ? (
|
isRunning ? (
|
||||||
<CliLogsBox
|
<CliLogsBox
|
||||||
label="Swap Daemon Logs (current session only)"
|
label="Swap Daemon Logs (current session only)"
|
||||||
logs={rpcProcess.logs}
|
logs={[]}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Link,
|
Link,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { OpenInNew } from "@material-ui/icons";
|
import { OpenInNew } from "@material-ui/icons";
|
||||||
import { GetSwapInfoResponse } from "models/tauriModel";
|
import { GetSwapInfoResponse } from "models/tauriModel";
|
||||||
import CopyableMonospaceTextBox from "renderer/components/other/CopyableAddress";
|
import CopyableMonospaceTextBox from "renderer/components/other/CopyableMonospaceTextBox";
|
||||||
import MonospaceTextBox from "renderer/components/other/InlineCode";
|
import MonospaceTextBox from "renderer/components/other/MonospaceTextBox";
|
||||||
import {
|
import {
|
||||||
MoneroBitcoinExchangeRate,
|
MoneroBitcoinExchangeRate,
|
||||||
PiconeroAmount,
|
PiconeroAmount,
|
||||||
SatsAmount,
|
SatsAmount,
|
||||||
} from "renderer/components/other/Units";
|
} from "renderer/components/other/Units";
|
||||||
import { isTestnet } from "store/config";
|
import { isTestnet } from "store/config";
|
||||||
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
|
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
|
||||||
|
|
|
@ -12,12 +12,16 @@ import {
|
||||||
fetchXmrPrice,
|
fetchXmrPrice,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import App from "./components/App";
|
import App from "./components/App";
|
||||||
import { checkBitcoinBalance, getRawSwapInfos } from "./rpc";
|
import {
|
||||||
|
checkBitcoinBalance,
|
||||||
|
getAllSwapInfos,
|
||||||
|
initEventListeners,
|
||||||
|
} from "./rpc";
|
||||||
import { persistor, store } from "./store/storeRenderer";
|
import { persistor, store } from "./store/storeRenderer";
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
checkBitcoinBalance();
|
checkBitcoinBalance();
|
||||||
getRawSwapInfos();
|
getAllSwapInfos();
|
||||||
}, 30 * 1000);
|
}, 30 * 1000);
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById("root");
|
||||||
|
|
|
@ -9,11 +9,16 @@ import {
|
||||||
ResumeSwapArgs,
|
ResumeSwapArgs,
|
||||||
ResumeSwapResponse,
|
ResumeSwapResponse,
|
||||||
SuspendCurrentSwapResponse,
|
SuspendCurrentSwapResponse,
|
||||||
|
TauriContextStatusEvent,
|
||||||
TauriSwapProgressEventWrapper,
|
TauriSwapProgressEventWrapper,
|
||||||
WithdrawBtcArgs,
|
WithdrawBtcArgs,
|
||||||
WithdrawBtcResponse,
|
WithdrawBtcResponse,
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
|
import {
|
||||||
|
contextStatusEventReceived,
|
||||||
|
rpcSetBalance,
|
||||||
|
rpcSetSwapInfo,
|
||||||
|
} from "store/features/rpcSlice";
|
||||||
import { swapTauriEventReceived } from "store/features/swapSlice";
|
import { swapTauriEventReceived } from "store/features/swapSlice";
|
||||||
import { store } from "./store/storeRenderer";
|
import { store } from "./store/storeRenderer";
|
||||||
import { Provider } from "models/apiModel";
|
import { Provider } from "models/apiModel";
|
||||||
|
@ -24,6 +29,11 @@ export async function initEventListeners() {
|
||||||
console.log("Received swap progress event", event.payload);
|
console.log("Received swap progress event", event.payload);
|
||||||
store.dispatch(swapTauriEventReceived(event.payload));
|
store.dispatch(swapTauriEventReceived(event.payload));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
listen<TauriContextStatusEvent>("context-init-progress-update", (event) => {
|
||||||
|
console.log("Received context init progress event", event.payload);
|
||||||
|
store.dispatch(contextStatusEventReceived(event.payload));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invoke<ARGS, RESPONSE>(
|
async function invoke<ARGS, RESPONSE>(
|
||||||
|
@ -47,7 +57,7 @@ export async function checkBitcoinBalance() {
|
||||||
store.dispatch(rpcSetBalance(response.balance));
|
store.dispatch(rpcSetBalance(response.balance));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRawSwapInfos() {
|
export async function getAllSwapInfos() {
|
||||||
const response =
|
const response =
|
||||||
await invokeNoArgs<GetSwapInfoResponse[]>("get_swap_infos_all");
|
await invokeNoArgs<GetSwapInfoResponse[]>("get_swap_infos_all");
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||||
import { persistReducer, persistStore } from "redux-persist";
|
import { persistReducer, persistStore } from "redux-persist";
|
||||||
import sessionStorage from "redux-persist/lib/storage/session";
|
import sessionStorage from "redux-persist/lib/storage/session";
|
||||||
import { reducers } from "store/combinedReducer";
|
import { reducers } from "store/combinedReducer";
|
||||||
|
import { createMainListeners } from "store/middleware/storeListener";
|
||||||
|
|
||||||
// We persist the redux store in sessionStorage
|
// We persist the redux store in sessionStorage
|
||||||
// The point of this is to preserve the store across reloads while not persisting it across GUI restarts
|
// The point of this is to preserve the store across reloads while not persisting it across GUI restarts
|
||||||
|
@ -20,6 +21,8 @@ const persistedReducer = persistReducer(
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: persistedReducer,
|
reducer: persistedReducer,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware().prepend(createMainListeners().middleware),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const persistor = persistStore(store);
|
export const persistor = persistStore(store);
|
||||||
|
|
|
@ -1,32 +1,12 @@
|
||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
|
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
|
||||||
import { GetSwapInfoResponse } from "models/tauriModel";
|
|
||||||
import { CliLog } from "../../models/cliModel";
|
|
||||||
import {
|
import {
|
||||||
MoneroRecoveryResponse,
|
GetSwapInfoResponse,
|
||||||
RpcProcessStateType,
|
TauriContextStatusEvent,
|
||||||
} from "../../models/rpcModel";
|
} from "models/tauriModel";
|
||||||
|
import { MoneroRecoveryResponse } from "../../models/rpcModel";
|
||||||
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
|
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||||
|
|
||||||
type Process =
|
|
||||||
| {
|
|
||||||
type: RpcProcessStateType.STARTED;
|
|
||||||
logs: (CliLog | string)[];
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: RpcProcessStateType.LISTENING_FOR_CONNECTIONS;
|
|
||||||
logs: (CliLog | string)[];
|
|
||||||
address: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: RpcProcessStateType.EXITED;
|
|
||||||
logs: (CliLog | string)[];
|
|
||||||
exitCode: number | null;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: RpcProcessStateType.NOT_STARTED;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
balance: number | null;
|
balance: number | null;
|
||||||
withdrawTxId: string | null;
|
withdrawTxId: string | null;
|
||||||
|
@ -48,15 +28,13 @@ interface State {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RPCSlice {
|
export interface RPCSlice {
|
||||||
process: Process;
|
status: TauriContextStatusEvent | null;
|
||||||
state: State;
|
state: State;
|
||||||
busyEndpoints: string[];
|
busyEndpoints: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: RPCSlice = {
|
const initialState: RPCSlice = {
|
||||||
process: {
|
status: null,
|
||||||
type: RpcProcessStateType.NOT_STARTED,
|
|
||||||
},
|
|
||||||
state: {
|
state: {
|
||||||
balance: null,
|
balance: null,
|
||||||
withdrawTxId: null,
|
withdrawTxId: null,
|
||||||
|
@ -77,35 +55,11 @@ export const rpcSlice = createSlice({
|
||||||
name: "rpc",
|
name: "rpc",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
rpcInitiate(slice) {
|
contextStatusEventReceived(
|
||||||
slice.process = {
|
|
||||||
type: RpcProcessStateType.STARTED,
|
|
||||||
logs: [],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
rpcProcessExited(
|
|
||||||
slice,
|
slice,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<TauriContextStatusEvent>,
|
||||||
exitCode: number | null;
|
|
||||||
exitSignal: NodeJS.Signals | null;
|
|
||||||
}>,
|
|
||||||
) {
|
) {
|
||||||
if (
|
slice.status = action.payload;
|
||||||
slice.process.type === RpcProcessStateType.STARTED ||
|
|
||||||
slice.process.type === RpcProcessStateType.LISTENING_FOR_CONNECTIONS
|
|
||||||
) {
|
|
||||||
slice.process = {
|
|
||||||
type: RpcProcessStateType.EXITED,
|
|
||||||
logs: slice.process.logs,
|
|
||||||
exitCode: action.payload.exitCode,
|
|
||||||
};
|
|
||||||
slice.state.moneroWalletRpc = {
|
|
||||||
updateState: false,
|
|
||||||
};
|
|
||||||
slice.state.moneroWallet = {
|
|
||||||
isSyncing: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
rpcSetBalance(slice, action: PayloadAction<number>) {
|
rpcSetBalance(slice, action: PayloadAction<number>) {
|
||||||
slice.state.balance = action.payload;
|
slice.state.balance = action.payload;
|
||||||
|
@ -156,8 +110,7 @@ export const rpcSlice = createSlice({
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
rpcProcessExited,
|
contextStatusEventReceived,
|
||||||
rpcInitiate,
|
|
||||||
rpcSetBalance,
|
rpcSetBalance,
|
||||||
rpcSetWithdrawTxId,
|
rpcSetWithdrawTxId,
|
||||||
rpcResetWithdrawTxId,
|
rpcResetWithdrawTxId,
|
||||||
|
|
|
@ -23,6 +23,10 @@ export function useIsSwapRunning() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useIsContextAvailable() {
|
||||||
|
return useAppSelector((state) => state.rpc.status?.type === "Available");
|
||||||
|
}
|
||||||
|
|
||||||
export function useSwapInfo(swapId: string | null) {
|
export function useSwapInfo(swapId: string | null) {
|
||||||
return useAppSelector((state) =>
|
return useAppSelector((state) =>
|
||||||
swapId ? (state.rpc.state.swapInfos[swapId] ?? null) : null,
|
swapId ? (state.rpc.state.swapInfos[swapId] ?? null) : null,
|
||||||
|
|
28
src-gui/src/store/middleware/storeListener.ts
Normal file
28
src-gui/src/store/middleware/storeListener.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { createListenerMiddleware } from "@reduxjs/toolkit";
|
||||||
|
import { getAllSwapInfos, checkBitcoinBalance } from "renderer/rpc";
|
||||||
|
import logger from "utils/logger";
|
||||||
|
import { contextStatusEventReceived } from "store/features/rpcSlice";
|
||||||
|
|
||||||
|
export function createMainListeners() {
|
||||||
|
const listener = createListenerMiddleware();
|
||||||
|
|
||||||
|
// Listener for when the Context becomes available
|
||||||
|
// When the context becomes available, we check the bitcoin balance and fetch all swap infos
|
||||||
|
listener.startListening({
|
||||||
|
actionCreator: contextStatusEventReceived,
|
||||||
|
effect: async (action) => {
|
||||||
|
const status = action.payload;
|
||||||
|
|
||||||
|
// If the context is available, check the bitcoin balance and fetch all swap infos
|
||||||
|
if (status.type === "Available") {
|
||||||
|
logger.debug(
|
||||||
|
"Context is available, checking bitcoin balance and history",
|
||||||
|
);
|
||||||
|
await checkBitcoinBalance();
|
||||||
|
await getAllSwapInfos();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return listener;
|
||||||
|
}
|
|
@ -6,18 +6,19 @@ use swap::cli::{
|
||||||
BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetSwapInfosAllArgs, ResumeSwapArgs,
|
BalanceArgs, BuyXmrArgs, GetHistoryArgs, GetSwapInfosAllArgs, ResumeSwapArgs,
|
||||||
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||||
},
|
},
|
||||||
tauri_bindings::TauriHandle,
|
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle},
|
||||||
Context, ContextBuilder,
|
Context, ContextBuilder,
|
||||||
},
|
},
|
||||||
command::{Bitcoin, Monero},
|
command::{Bitcoin, Monero},
|
||||||
};
|
};
|
||||||
use tauri::{Manager, RunEvent};
|
use tauri::{async_runtime::RwLock, Manager, RunEvent};
|
||||||
|
|
||||||
|
/// Trait to convert Result<T, E> to Result<T, String>
|
||||||
|
/// Tauri commands require the error type to be a string
|
||||||
trait ToStringResult<T> {
|
trait ToStringResult<T> {
|
||||||
fn to_string_result(self) -> Result<T, String>;
|
fn to_string_result(self) -> Result<T, String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement the trait for Result<T, E>
|
|
||||||
impl<T, E: ToString> ToStringResult<T> for Result<T, E> {
|
impl<T, E: ToString> ToStringResult<T> for Result<T, E> {
|
||||||
fn to_string_result(self) -> Result<T, String> {
|
fn to_string_result(self) -> Result<T, String> {
|
||||||
self.map_err(|e| e.to_string())
|
self.map_err(|e| e.to_string())
|
||||||
|
@ -53,42 +54,72 @@ macro_rules! tauri_command {
|
||||||
($fn_name:ident, $request_name:ident) => {
|
($fn_name:ident, $request_name:ident) => {
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn $fn_name(
|
async fn $fn_name(
|
||||||
context: tauri::State<'_, Arc<Context>>,
|
context: tauri::State<'_, RwLock<State>>,
|
||||||
args: $request_name,
|
args: $request_name,
|
||||||
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
||||||
<$request_name as swap::cli::api::request::Request>::request(
|
// Throw error if context is not available
|
||||||
args,
|
let context = context.read().await.try_get_context()?;
|
||||||
context.inner().clone(),
|
|
||||||
)
|
<$request_name as swap::cli::api::request::Request>::request(args, context)
|
||||||
.await
|
.await
|
||||||
.to_string_result()
|
.to_string_result()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
($fn_name:ident, $request_name:ident, no_args) => {
|
($fn_name:ident, $request_name:ident, no_args) => {
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn $fn_name(
|
async fn $fn_name(
|
||||||
context: tauri::State<'_, Arc<Context>>,
|
context: tauri::State<'_, RwLock<State>>,
|
||||||
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
||||||
<$request_name as swap::cli::api::request::Request>::request(
|
// Throw error if context is not available
|
||||||
$request_name {},
|
let context = context.read().await.try_get_context()?;
|
||||||
context.inner().clone(),
|
|
||||||
)
|
<$request_name as swap::cli::api::request::Request>::request($request_name {}, context)
|
||||||
.await
|
.await
|
||||||
.to_string_result()
|
.to_string_result()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri_command!(get_balance, BalanceArgs);
|
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
||||||
tauri_command!(buy_xmr, BuyXmrArgs);
|
struct State {
|
||||||
tauri_command!(resume_swap, ResumeSwapArgs);
|
pub context: Option<Arc<Context>>,
|
||||||
tauri_command!(withdraw_btc, WithdrawBtcArgs);
|
}
|
||||||
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);
|
|
||||||
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
|
|
||||||
tauri_command!(get_history, GetHistoryArgs, no_args);
|
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
/// Creates a new State instance with no Context
|
||||||
|
fn new() -> Self {
|
||||||
|
Self { context: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the context for the application state
|
||||||
|
/// This is typically called after the Context has been initialized
|
||||||
|
/// in the setup function
|
||||||
|
fn set_context(&mut self, context: impl Into<Option<Arc<Context>>>) {
|
||||||
|
self.context = context.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to retrieve the context
|
||||||
|
/// Returns an error if the context is not available
|
||||||
|
fn try_get_context(&self) -> Result<Arc<Context>, String> {
|
||||||
|
self.context
|
||||||
|
.clone()
|
||||||
|
.ok_or("Context not available")
|
||||||
|
.to_string_result()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets up the Tauri application
|
||||||
|
/// Initializes the Tauri state and spawns an async task to set up the Context
|
||||||
fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tauri::async_runtime::block_on(async {
|
let app_handle = app.app_handle().to_owned();
|
||||||
|
|
||||||
|
// We need to set a value for the Tauri state right at the start
|
||||||
|
// If we don't do this, Tauri commands will panic at runtime if no value is present
|
||||||
|
app_handle.manage::<RwLock<State>>(RwLock::new(State::new()));
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let tauri_handle = TauriHandle::new(app_handle.clone());
|
||||||
|
|
||||||
let context = ContextBuilder::new(true)
|
let context = ContextBuilder::new(true)
|
||||||
.with_bitcoin(Bitcoin {
|
.with_bitcoin(Bitcoin {
|
||||||
bitcoin_electrum_rpc_url: None,
|
bitcoin_electrum_rpc_url: None,
|
||||||
|
@ -99,11 +130,26 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
})
|
})
|
||||||
.with_json(false)
|
.with_json(false)
|
||||||
.with_debug(true)
|
.with_debug(true)
|
||||||
.with_tauri(TauriHandle::new(app.app_handle().to_owned()))
|
.with_tauri(tauri_handle.clone())
|
||||||
.build()
|
.build()
|
||||||
.await
|
.await;
|
||||||
.expect("failed to create context");
|
|
||||||
app.manage(Arc::new(context));
|
match context {
|
||||||
|
Ok(context) => {
|
||||||
|
let state = app_handle.state::<RwLock<State>>();
|
||||||
|
|
||||||
|
state.write().await.set_context(Arc::new(context));
|
||||||
|
|
||||||
|
// To display to the user that the setup is done, we emit an event to the Tauri frontend
|
||||||
|
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error while initializing context: {:?}", e);
|
||||||
|
|
||||||
|
// To display to the user that the setup failed, we emit an event to the Tauri frontend
|
||||||
|
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Failed);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -127,12 +173,35 @@ pub fn run() {
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|app, event| match event {
|
.run(|app, event| match event {
|
||||||
RunEvent::Exit | RunEvent::ExitRequested { .. } => {
|
RunEvent::Exit | RunEvent::ExitRequested { .. } => {
|
||||||
let context = app.state::<Arc<Context>>().inner();
|
// Here we cleanup the Context when the application is closed
|
||||||
|
// This is necessary to among other things stop the monero-wallet-rpc process
|
||||||
|
// If the application is forcibly closed, this may not be called
|
||||||
|
let context = app.state::<RwLock<State>>().inner().try_read();
|
||||||
|
|
||||||
if let Err(err) = context.cleanup() {
|
match context {
|
||||||
println!("Cleanup failed {}", err);
|
Ok(context) => {
|
||||||
|
if let Some(context) = context.context.as_ref() {
|
||||||
|
if let Err(err) = context.cleanup() {
|
||||||
|
println!("Cleanup failed {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Failed to acquire lock on context: {}", err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Here we define the Tauri commands that will be available to the frontend
|
||||||
|
// The commands are defined using the `tauri_command!` macro.
|
||||||
|
// Implementations are handled by the Request trait
|
||||||
|
tauri_command!(get_balance, BalanceArgs);
|
||||||
|
tauri_command!(buy_xmr, BuyXmrArgs);
|
||||||
|
tauri_command!(resume_swap, ResumeSwapArgs);
|
||||||
|
tauri_command!(withdraw_btc, WithdrawBtcArgs);
|
||||||
|
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);
|
||||||
|
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
|
||||||
|
tauri_command!(get_history, GetHistoryArgs, no_args);
|
||||||
|
|
|
@ -17,7 +17,9 @@ use std::fmt;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex as SyncMutex, Once};
|
use std::sync::{Arc, Mutex as SyncMutex, Once};
|
||||||
use tauri_bindings::TauriHandle;
|
use tauri_bindings::{
|
||||||
|
TauriContextInitializationProgress, TauriContextStatusEvent, TauriEmitter, TauriHandle,
|
||||||
|
};
|
||||||
use tokio::sync::{broadcast, broadcast::Sender, Mutex as TokioMutex, RwLock};
|
use tokio::sync::{broadcast, broadcast::Sender, Mutex as TokioMutex, RwLock};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::level_filters::LevelFilter;
|
use tracing::level_filters::LevelFilter;
|
||||||
|
@ -292,6 +294,13 @@ impl ContextBuilder {
|
||||||
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||||
.context("Failed to read seed in file")?;
|
.context("Failed to read seed in file")?;
|
||||||
|
|
||||||
|
// We initialize the Bitcoin wallet below
|
||||||
|
// To display the progress to the user, we emit events to the Tauri frontend
|
||||||
|
self.tauri_handle
|
||||||
|
.emit_context_init_progress_event(TauriContextStatusEvent::Initializing(
|
||||||
|
TauriContextInitializationProgress::OpeningBitcoinWallet,
|
||||||
|
));
|
||||||
|
|
||||||
let bitcoin_wallet = {
|
let bitcoin_wallet = {
|
||||||
if let Some(bitcoin) = self.bitcoin {
|
if let Some(bitcoin) = self.bitcoin {
|
||||||
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
|
let (bitcoin_electrum_rpc_url, bitcoin_target_block) =
|
||||||
|
@ -311,6 +320,13 @@ impl ContextBuilder {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We initialize the Monero wallet below
|
||||||
|
// To display the progress to the user, we emit events to the Tauri frontend
|
||||||
|
self.tauri_handle
|
||||||
|
.emit_context_init_progress_event(TauriContextStatusEvent::Initializing(
|
||||||
|
TauriContextInitializationProgress::OpeningMoneroWallet,
|
||||||
|
));
|
||||||
|
|
||||||
let (monero_wallet, monero_rpc_process) = {
|
let (monero_wallet, monero_rpc_process) = {
|
||||||
if let Some(monero) = self.monero {
|
if let Some(monero) = self.monero {
|
||||||
let monero_daemon_address = monero.apply_defaults(self.is_testnet);
|
let monero_daemon_address = monero.apply_defaults(self.is_testnet);
|
||||||
|
@ -322,10 +338,19 @@ impl ContextBuilder {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We initialize the Database below
|
||||||
|
// To display the progress to the user, we emit events to the Tauri frontend
|
||||||
|
self.tauri_handle
|
||||||
|
.emit_context_init_progress_event(TauriContextStatusEvent::Initializing(
|
||||||
|
TauriContextInitializationProgress::OpeningDatabase,
|
||||||
|
));
|
||||||
|
|
||||||
|
let db = open_db(data_dir.join("sqlite")).await?;
|
||||||
|
|
||||||
let tor_socks5_port = self.tor.map_or(9050, |tor| tor.tor_socks5_port);
|
let tor_socks5_port = self.tor.map_or(9050, |tor| tor.tor_socks5_port);
|
||||||
|
|
||||||
let context = Context {
|
let context = Context {
|
||||||
db: open_db(data_dir.join("sqlite")).await?,
|
db,
|
||||||
bitcoin_wallet,
|
bitcoin_wallet,
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
monero_rpc_process,
|
monero_rpc_process,
|
||||||
|
|
|
@ -2,10 +2,12 @@ use crate::{monero, network::quote::BidQuote};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bitcoin::Txid;
|
use bitcoin::Txid;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use strum::Display;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
static SWAP_PROGRESS_EVENT_NAME: &str = "swap-progress-update";
|
static SWAP_PROGRESS_EVENT_NAME: &str = "swap-progress-update";
|
||||||
|
static CONTEXT_INIT_PROGRESS_EVENT_NAME: &str = "context-init-progress-update";
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TauriHandle(
|
pub struct TauriHandle(
|
||||||
|
@ -41,6 +43,10 @@ pub trait TauriEmitter {
|
||||||
TauriSwapProgressEventWrapper { swap_id, event },
|
TauriSwapProgressEventWrapper { swap_id, event },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn emit_context_init_progress_event(&self, event: TauriContextStatusEvent) {
|
||||||
|
let _ = self.emit_tauri_event(CONTEXT_INIT_PROGRESS_EVENT_NAME, event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TauriEmitter for TauriHandle {
|
impl TauriEmitter for TauriHandle {
|
||||||
|
@ -58,6 +64,23 @@ impl TauriEmitter for Option<TauriHandle> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Display, Clone, Serialize)]
|
||||||
|
pub enum TauriContextInitializationProgress {
|
||||||
|
OpeningBitcoinWallet,
|
||||||
|
OpeningMoneroWallet,
|
||||||
|
OpeningDatabase,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Display, Clone, Serialize)]
|
||||||
|
#[serde(tag = "type", content = "content")]
|
||||||
|
pub enum TauriContextStatusEvent {
|
||||||
|
Initializing(TauriContextInitializationProgress),
|
||||||
|
Available,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct TauriSwapProgressEventWrapper {
|
pub struct TauriSwapProgressEventWrapper {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue