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

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