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,
|
||||
},
|
||||
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() {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
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}
|
||||
endIcon={<PlayArrowIcon />}
|
||||
onClick={init}
|
||||
displayErrorSnackbar
|
||||
>
|
||||
Start swap
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
@ -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";
|
||||
|
@ -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">
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -9,11 +9,16 @@ import {
|
|||
ResumeSwapArgs,
|
||||
ResumeSwapResponse,
|
||||
SuspendCurrentSwapResponse,
|
||||
TauriContextStatusEvent,
|
||||
TauriSwapProgressEventWrapper,
|
||||
WithdrawBtcArgs,
|
||||
WithdrawBtcResponse,
|
||||
} 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 { store } from "./store/storeRenderer";
|
||||
import { Provider } from "models/apiModel";
|
||||
|
@ -24,6 +29,11 @@ export async function initEventListeners() {
|
|||
console.log("Received swap progress event", 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>(
|
||||
|
@ -47,7 +57,7 @@ export async function checkBitcoinBalance() {
|
|||
store.dispatch(rpcSetBalance(response.balance));
|
||||
}
|
||||
|
||||
export async function getRawSwapInfos() {
|
||||
export async function getAllSwapInfos() {
|
||||
const response =
|
||||
await invokeNoArgs<GetSwapInfoResponse[]>("get_swap_infos_all");
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
|||
import { persistReducer, persistStore } from "redux-persist";
|
||||
import sessionStorage from "redux-persist/lib/storage/session";
|
||||
import { reducers } from "store/combinedReducer";
|
||||
import { createMainListeners } from "store/middleware/storeListener";
|
||||
|
||||
// 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
|
||||
|
@ -20,6 +21,8 @@ const persistedReducer = persistReducer(
|
|||
|
||||
export const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().prepend(createMainListeners().middleware),
|
||||
});
|
||||
|
||||
export const persistor = persistStore(store);
|
||||
|
|
|
@ -1,32 +1,12 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
|
||||
import { GetSwapInfoResponse } from "models/tauriModel";
|
||||
import { CliLog } from "../../models/cliModel";
|
||||
import {
|
||||
MoneroRecoveryResponse,
|
||||
RpcProcessStateType,
|
||||
} from "../../models/rpcModel";
|
||||
GetSwapInfoResponse,
|
||||
TauriContextStatusEvent,
|
||||
} from "models/tauriModel";
|
||||
import { MoneroRecoveryResponse } from "../../models/rpcModel";
|
||||
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 {
|
||||
balance: number | null;
|
||||
withdrawTxId: string | null;
|
||||
|
@ -48,15 +28,13 @@ interface State {
|
|||
}
|
||||
|
||||
export interface RPCSlice {
|
||||
process: Process;
|
||||
status: TauriContextStatusEvent | null;
|
||||
state: State;
|
||||
busyEndpoints: string[];
|
||||
}
|
||||
|
||||
const initialState: RPCSlice = {
|
||||
process: {
|
||||
type: RpcProcessStateType.NOT_STARTED,
|
||||
},
|
||||
status: null,
|
||||
state: {
|
||||
balance: null,
|
||||
withdrawTxId: null,
|
||||
|
@ -77,35 +55,11 @@ export const rpcSlice = createSlice({
|
|||
name: "rpc",
|
||||
initialState,
|
||||
reducers: {
|
||||
rpcInitiate(slice) {
|
||||
slice.process = {
|
||||
type: RpcProcessStateType.STARTED,
|
||||
logs: [],
|
||||
};
|
||||
},
|
||||
rpcProcessExited(
|
||||
contextStatusEventReceived(
|
||||
slice,
|
||||
action: PayloadAction<{
|
||||
exitCode: number | null;
|
||||
exitSignal: NodeJS.Signals | null;
|
||||
}>,
|
||||
action: PayloadAction<TauriContextStatusEvent>,
|
||||
) {
|
||||
if (
|
||||
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,
|
||||
};
|
||||
}
|
||||
slice.status = action.payload;
|
||||
},
|
||||
rpcSetBalance(slice, action: PayloadAction<number>) {
|
||||
slice.state.balance = action.payload;
|
||||
|
@ -156,8 +110,7 @@ export const rpcSlice = createSlice({
|
|||
});
|
||||
|
||||
export const {
|
||||
rpcProcessExited,
|
||||
rpcInitiate,
|
||||
contextStatusEventReceived,
|
||||
rpcSetBalance,
|
||||
rpcSetWithdrawTxId,
|
||||
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) {
|
||||
return useAppSelector((state) =>
|
||||
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,
|
||||
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||
},
|
||||
tauri_bindings::TauriHandle,
|
||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle},
|
||||
Context, ContextBuilder,
|
||||
},
|
||||
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> {
|
||||
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> {
|
||||
fn to_string_result(self) -> Result<T, String> {
|
||||
self.map_err(|e| e.to_string())
|
||||
|
@ -53,42 +54,72 @@ macro_rules! tauri_command {
|
|||
($fn_name:ident, $request_name:ident) => {
|
||||
#[tauri::command]
|
||||
async fn $fn_name(
|
||||
context: tauri::State<'_, Arc<Context>>,
|
||||
context: tauri::State<'_, RwLock<State>>,
|
||||
args: $request_name,
|
||||
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
||||
<$request_name as swap::cli::api::request::Request>::request(
|
||||
args,
|
||||
context.inner().clone(),
|
||||
)
|
||||
.await
|
||||
.to_string_result()
|
||||
// Throw error if context is not available
|
||||
let context = context.read().await.try_get_context()?;
|
||||
|
||||
<$request_name as swap::cli::api::request::Request>::request(args, context)
|
||||
.await
|
||||
.to_string_result()
|
||||
}
|
||||
};
|
||||
($fn_name:ident, $request_name:ident, no_args) => {
|
||||
#[tauri::command]
|
||||
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> {
|
||||
<$request_name as swap::cli::api::request::Request>::request(
|
||||
$request_name {},
|
||||
context.inner().clone(),
|
||||
)
|
||||
.await
|
||||
.to_string_result()
|
||||
// Throw error if context is not available
|
||||
let context = context.read().await.try_get_context()?;
|
||||
|
||||
<$request_name as swap::cli::api::request::Request>::request($request_name {}, context)
|
||||
.await
|
||||
.to_string_result()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
||||
struct State {
|
||||
pub context: Option<Arc<Context>>,
|
||||
}
|
||||
|
||||
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>> {
|
||||
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)
|
||||
.with_bitcoin(Bitcoin {
|
||||
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_debug(true)
|
||||
.with_tauri(TauriHandle::new(app.app_handle().to_owned()))
|
||||
.with_tauri(tauri_handle.clone())
|
||||
.build()
|
||||
.await
|
||||
.expect("failed to create context");
|
||||
app.manage(Arc::new(context));
|
||||
.await;
|
||||
|
||||
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(())
|
||||
|
@ -127,12 +173,35 @@ pub fn run() {
|
|||
.expect("error while building tauri application")
|
||||
.run(|app, event| match event {
|
||||
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() {
|
||||
println!("Cleanup failed {}", err);
|
||||
match context {
|
||||
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::path::PathBuf;
|
||||
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::task::JoinHandle;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
@ -292,6 +294,13 @@ impl ContextBuilder {
|
|||
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||
.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 = {
|
||||
if let Some(bitcoin) = self.bitcoin {
|
||||
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) = {
|
||||
if let Some(monero) = self.monero {
|
||||
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 context = Context {
|
||||
db: open_db(data_dir.join("sqlite")).await?,
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_wallet,
|
||||
monero_rpc_process,
|
||||
|
|
|
@ -2,10 +2,12 @@ use crate::{monero, network::quote::BidQuote};
|
|||
use anyhow::Result;
|
||||
use bitcoin::Txid;
|
||||
use serde::Serialize;
|
||||
use strum::Display;
|
||||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
static SWAP_PROGRESS_EVENT_NAME: &str = "swap-progress-update";
|
||||
static CONTEXT_INIT_PROGRESS_EVENT_NAME: &str = "context-init-progress-update";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TauriHandle(
|
||||
|
@ -41,6 +43,10 @@ pub trait TauriEmitter {
|
|||
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 {
|
||||
|
@ -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)]
|
||||
#[typeshare]
|
||||
pub struct TauriSwapProgressEventWrapper {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue