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:
binarybaron 2024-09-03 12:28:30 +02:00 committed by GitHub
parent 792fbbf746
commit e4141c763b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 369 additions and 191 deletions

View file

@ -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() {

View file

@ -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,10 +68,23 @@ 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)}>
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>
) : (
@ -70,5 +94,8 @@ export default function PromiseInvokeButton<T>({
endIcon={actualEndIcon}
{...rest}
/>
)}
</span>
</Tooltip>
);
}

View 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);
}
}

View file

@ -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 <></>;
}

View file

@ -72,6 +72,7 @@ export default function InitPage() {
className={classes.initButton}
endIcon={<PlayArrowIcon />}
onClick={init}
displayErrorSnackbar
>
Start swap
</PromiseInvokeButton>

View file

@ -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">

View file

@ -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
}

View file

@ -10,8 +10,8 @@ import {
} 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,

View file

@ -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");

View file

@ -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");

View file

@ -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);

View file

@ -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,

View file

@ -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,

View 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;
}

View file

@ -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,13 +54,13 @@ 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(),
)
// 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()
}
@ -67,28 +68,58 @@ macro_rules! tauri_command {
($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(),
)
// 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();
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);

View file

@ -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,

View file

@ -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 {