From a628e7df357bcaa7da1c1ba731340b0c5aa60a09 Mon Sep 17 00:00:00 2001 From: Rigidity Date: Fri, 28 Feb 2025 00:44:49 -0500 Subject: [PATCH] RPC in UI with toggle --- Cargo.lock | 3 + src-tauri/Cargo.toml | 3 + src-tauri/src/app_state.rs | 111 ++++++++++++------------------------- src-tauri/src/commands.rs | 77 +++++++++++++++++++++++-- src-tauri/src/lib.rs | 19 +++++-- src/bindings.ts | 15 +++++ src/pages/Settings.tsx | 91 +++++++++++++++++++++++++++++- 7 files changed, 233 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b01a313..a5c14c42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5036,13 +5036,16 @@ dependencies = [ name = "sage-tauri" version = "0.9.6" dependencies = [ + "anyhow", "aws-lc-rs", "chia-wallet-sdk", "glob", + "rustls 0.23.18", "sage", "sage-api", "sage-api-macro", "sage-config", + "sage-rpc", "sage-wallet", "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5a8400d8..b32bec67 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ sage-api = { workspace = true, features = ["tauri"] } sage-api-macro = { workspace = true } sage-config = { workspace = true } sage-wallet = { workspace = true } +sage-rpc = { workspace = true } serde = { workspace = true, features = ["derive"] } tauri = { workspace = true, features = [] } tauri-specta = { workspace = true, features = ["derive", "typescript"] } @@ -32,6 +33,8 @@ tauri-plugin-shell = { workspace = true } tauri-plugin-os = { workspace = true } serde_json = { workspace = true, features = ["arbitrary_precision"] } tracing = { workspace = true } +anyhow = { workspace = true } +rustls = { workspace = true } # This is to ensure that the bindgen feature is enabled for the aws-lc-rs crate. # https://aws.github.io/aws-lc-rs/platform_support.html#tested-platforms diff --git a/src-tauri/src/app_state.rs b/src-tauri/src/app_state.rs index 67339623..aae81854 100644 --- a/src-tauri/src/app_state.rs +++ b/src-tauri/src/app_state.rs @@ -1,83 +1,44 @@ -use std::{ - ops::{Deref, DerefMut}, - path::Path, - sync::Arc, -}; +use std::sync::Arc; -use sage::Sage; +use sage::{Result, Sage}; use sage_api::SyncEvent as ApiEvent; use sage_wallet::SyncEvent; use tauri::{AppHandle, Emitter}; -use tokio::sync::Mutex; - -use crate::error::Result; - -pub type AppState = Arc>; - -pub struct AppStateInner { - pub app_handle: AppHandle, - pub initialized: bool, - pub sage: Sage, -} - -impl Deref for AppStateInner { - type Target = Sage; - - fn deref(&self) -> &Self::Target { - &self.sage - } -} - -impl DerefMut for AppStateInner { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.sage - } -} - -impl AppStateInner { - pub fn new(app_handle: AppHandle, path: &Path) -> Self { - Self { - app_handle, - initialized: false, - sage: Sage::new(path), - } - } - - pub async fn initialize(&mut self) -> Result { - if self.initialized { - return Ok(true); - } - - self.initialized = true; - - let mut receiver = self.sage.initialize().await?; - let app_handle = self.app_handle.clone(); - - tokio::spawn(async move { - while let Some(event) = receiver.recv().await { - let event = match event { - SyncEvent::Start(ip) => ApiEvent::Start { ip: ip.to_string() }, - SyncEvent::Stop => ApiEvent::Stop, - SyncEvent::Subscribed => ApiEvent::Subscribed, - SyncEvent::DerivationIndex { .. } => ApiEvent::Derivation, - // TODO: New event? - SyncEvent::CoinsUpdated { .. } - | SyncEvent::TransactionUpdated { .. } - | SyncEvent::TransactionEnded { .. } - | SyncEvent::OfferUpdated { .. } => ApiEvent::CoinState, - SyncEvent::PuzzleBatchSynced => ApiEvent::PuzzleBatchSynced, - SyncEvent::CatInfo => ApiEvent::CatInfo, - SyncEvent::DidInfo => ApiEvent::DidInfo, - SyncEvent::NftData => ApiEvent::NftData, - }; - if app_handle.emit("sync-event", event).is_err() { - break; - } +use tokio::{sync::Mutex, task::JoinHandle}; + +pub struct Initialized(pub Mutex); + +pub struct RpcTask(pub Mutex>>>); + +pub type AppState = Arc>; + +pub async fn initialize(app_handle: AppHandle, sage: &mut Sage) -> Result<()> { + let mut receiver = sage.initialize().await?; + + tokio::spawn(async move { + while let Some(event) = receiver.recv().await { + let event = match event { + SyncEvent::Start(ip) => ApiEvent::Start { ip: ip.to_string() }, + SyncEvent::Stop => ApiEvent::Stop, + SyncEvent::Subscribed => ApiEvent::Subscribed, + SyncEvent::DerivationIndex { .. } => ApiEvent::Derivation, + // TODO: New event? + SyncEvent::CoinsUpdated { .. } + | SyncEvent::TransactionUpdated { .. } + | SyncEvent::TransactionEnded { .. } + | SyncEvent::OfferUpdated { .. } => ApiEvent::CoinState, + SyncEvent::PuzzleBatchSynced => ApiEvent::PuzzleBatchSynced, + SyncEvent::CatInfo => ApiEvent::CatInfo, + SyncEvent::DidInfo => ApiEvent::DidInfo, + SyncEvent::NftData => ApiEvent::NftData, + }; + if app_handle.emit("sync-event", event).is_err() { + break; } + } - Result::Ok(()) - }); + Result::Ok(()) + }); - Ok(false) - } + Ok(()) } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 9cb140f5..4ef096ed 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -4,20 +4,37 @@ use chia_wallet_sdk::decode_address; use sage_api::{wallet_connect::*, *}; use sage_api_macro::impl_endpoints_tauri; use sage_config::{NetworkConfig, WalletConfig}; +use sage_rpc::start_rpc; use specta::specta; -use tauri::{command, State}; +use tauri::{command, AppHandle, State}; use tokio::time::sleep; use tracing::error; -use crate::{app_state::AppState, error::Result}; +use crate::{ + app_state::{self, AppState, Initialized, RpcTask}, + error::Result, +}; #[command] #[specta] -pub async fn initialize(state: State<'_, AppState>) -> Result<()> { - if state.lock().await.initialize().await? { +pub async fn initialize( + app_handle: AppHandle, + state: State<'_, AppState>, + initialized: State<'_, Initialized>, + rpc_task: State<'_, RpcTask>, +) -> Result<()> { + let mut initialized = initialized.0.lock().await; + + if *initialized { return Ok(()); } + *initialized = true; + + let mut sage = state.lock().await; + app_state::initialize(app_handle, &mut sage).await?; + drop(sage); + let app_state = (*state).clone(); tokio::spawn(async move { @@ -26,7 +43,7 @@ pub async fn initialize(state: State<'_, AppState>) -> Result<()> { let app_state = app_state.lock().await; - if let Err(error) = app_state.sage.save_peers().await { + if let Err(error) = app_state.save_peers().await { error!("Error while saving peers: {error:?}"); } @@ -34,6 +51,12 @@ pub async fn initialize(state: State<'_, AppState>) -> Result<()> { } }); + let app_state = state.lock().await; + + if app_state.config.rpc.run_on_startup { + *rpc_task.0.lock().await = Some(tokio::spawn(start_rpc((*state).clone()))); + } + Ok(()) } @@ -68,3 +91,47 @@ pub async fn network_config(state: State<'_, AppState>) -> Result pub async fn wallet_config(state: State<'_, AppState>, fingerprint: u32) -> Result { Ok(state.lock().await.try_wallet_config(fingerprint).clone()) } + +#[command] +#[specta] +pub async fn is_rpc_running(rpc_task: State<'_, RpcTask>) -> Result { + Ok(rpc_task.0.lock().await.is_some()) +} + +#[command] +#[specta] +pub async fn start_rpc_server( + state: State<'_, AppState>, + rpc_task: State<'_, RpcTask>, +) -> Result<()> { + let mut rpc_task = rpc_task.0.lock().await; + *rpc_task = Some(tokio::spawn(start_rpc((*state).clone()))); + Ok(()) +} + +#[command] +#[specta] +pub async fn stop_rpc_server(rpc_task: State<'_, RpcTask>) -> Result<()> { + let mut rpc_task = rpc_task.0.lock().await; + if let Some(handle) = rpc_task.take() { + handle.abort(); + } + Ok(()) +} + +#[command] +#[specta] +pub async fn get_rpc_run_on_startup(state: State<'_, AppState>) -> Result { + Ok(state.lock().await.config.rpc.run_on_startup) +} + +#[command] +#[specta] +pub async fn set_rpc_run_on_startup( + state: State<'_, AppState>, + run_on_startup: bool, +) -> Result<()> { + state.lock().await.config.rpc.run_on_startup = run_on_startup; + state.lock().await.save_config()?; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b6a44b33..87c53641 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,6 @@ -use app_state::{AppState, AppStateInner}; +use app_state::{AppState, Initialized, RpcTask}; +use rustls::crypto::aws_lc_rs::default_provider; +use sage::Sage; use sage_api::SyncEvent; use tauri::Manager; use tauri_specta::{collect_commands, collect_events, Builder, ErrorHandlingMode}; @@ -13,6 +15,10 @@ use specta_typescript::{BigIntExportBehavior, Typescript}; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + default_provider() + .install_default() + .expect("could not install AWS LC provider"); + let builder = Builder::::new() .error_handling(ErrorHandlingMode::Throw) // Then register them (separated by a comma) @@ -99,6 +105,11 @@ pub fn run() { commands::sign_message_with_public_key, commands::sign_message_by_address, commands::send_transaction_immediately, + commands::is_rpc_running, + commands::start_rpc_server, + commands::stop_rpc_server, + commands::get_rpc_run_on_startup, + commands::set_rpc_run_on_startup, ]) .events(collect_events![SyncEvent]); @@ -134,10 +145,10 @@ pub fn run() { app.handle().plugin(tauri_plugin_safe_area_insets::init())?; } builder.mount_events(app); - let app_handle = app.handle().clone(); let path = app.path().app_data_dir()?; - let inner = AppStateInner::new(app_handle, &path); - let app_state = AppState::new(Mutex::new(inner)); + let app_state = AppState::new(Mutex::new(Sage::new(&path))); + app.manage(Initialized(Mutex::new(false))); + app.manage(RpcTask(Mutex::new(None))); app.manage(app_state); Ok(()) }) diff --git a/src/bindings.ts b/src/bindings.ts index 7a64e23b..c1d007bb 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -250,6 +250,21 @@ async signMessageByAddress(req: SignMessageByAddress) : Promise { return await TAURI_INVOKE("send_transaction_immediately", { req }); +}, +async isRpcRunning() : Promise { + return await TAURI_INVOKE("is_rpc_running"); +}, +async startRpcServer() : Promise { + return await TAURI_INVOKE("start_rpc_server"); +}, +async stopRpcServer() : Promise { + return await TAURI_INVOKE("stop_rpc_server"); +}, +async getRpcRunOnStartup() : Promise { + return await TAURI_INVOKE("get_rpc_run_on_startup"); +}, +async setRpcRunOnStartup(runOnStartup: boolean) : Promise { + return await TAURI_INVOKE("set_rpc_run_on_startup", { runOnStartup }); } } diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 835377f3..d87ec407 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -15,6 +15,7 @@ import { } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { useLanguage } from '@/contexts/LanguageContext'; +import { useDefaultOfferExpiry } from '@/hooks/useDefaultOfferExpiry'; import { useErrors } from '@/hooks/useErrors'; import useInitialization from '@/hooks/useInitialization'; import { useScannerOrClipboard } from '@/hooks/useScannerOrClipboard'; @@ -23,6 +24,7 @@ import { useWalletConnect } from '@/hooks/useWalletConnect'; import { clearState, fetchState } from '@/state'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; +import { getVersion } from '@tauri-apps/api/app'; import { useContext, useEffect, useState } from 'react'; import { DarkModeContext } from '../App'; import { @@ -33,8 +35,6 @@ import { WalletConfig, } from '../bindings'; import { isValidU32 } from '../validation'; -import { getVersion } from '@tauri-apps/api/app'; -import { useDefaultOfferExpiry } from '@/hooks/useDefaultOfferExpiry'; export interface DefaultOfferExpiry { enabled: boolean; @@ -62,6 +62,7 @@ export default function Settings() { + {wallet && } @@ -398,6 +399,92 @@ function NetworkSettings() { ); } +function RpcSettings() { + const { addError } = useErrors(); + + const [isRunning, setIsRunning] = useState(false); + const [runOnStartup, setRunOnStartup] = useState(false); + + useEffect(() => { + // Get initial state + Promise.all([commands.isRpcRunning(), commands.getRpcRunOnStartup()]) + .then(([running, startup]) => { + setIsRunning(running); + setRunOnStartup(startup); + }) + .catch(addError); + + // Poll RPC status + const interval = setInterval(() => { + commands.isRpcRunning().then(setIsRunning).catch(addError); + }, 1000); + + return () => clearInterval(interval); + }, [addError]); + + const start = () => { + commands + .startRpcServer() + .catch(addError) + .then(() => setIsRunning(true)); + }; + + const stop = () => { + commands + .stopRpcServer() + .catch(addError) + .then(() => setIsRunning(false)); + }; + + const toggleRunOnStartup = (checked: boolean) => { + commands + .setRpcRunOnStartup(checked) + .catch(addError) + .then(() => setRunOnStartup(checked)); + }; + + return ( + + + + RPC Server + + + +
+
+
+
+ + {isRunning ? Running : Stopped} + +
+ +
+ +
+ + +
+
+ + + ); +} + function WalletSettings(props: { wallet: KeyInfo }) { const { addError } = useErrors();