mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-01 13:04:48 -05:00
feat(gui): Monero wallet (#442)
* feat(gui): Monero wallet
* progress
* refactor
* progress, dont delete wallet, re-fetch approvals and background periodically
* show transaction history correctly
* Enable fetching tx hashes
* Try add the wallet listener event callbacks, not working
* fix: Redeem XMR to internal main wallet, not temp wallet
* type safety
* refactoring of callback system
* make free floating functions generic
* refactor: Format files
* refactor(gui): Split wallet components and redesign balanceOverview component
* refactor(gui): Add action buttons and transaction section
* wrapper event listener
* progress, compiles
* works!
* WORKS! Event received on balance change
* refactor: format and slight refactorings and comments
* refactor(gui): Start with implementation of send dialog
- new number input
- new button variant and size
* add @tauri-apps/plugin-dialog
* feat(gui): Add permissions for file dialog
* fix(monero-harness): Compile issue
* feat(gui): Extract seed from Monero wallet and use for derivation, allow opening existing wallet file
* feat(gui): Always refresh the approval list from frontend when resolving
* fix(monero-rpc-pool): Implement Into<String> for ServerInfo
* fix(monero-sys): Use oneshot channel for all wallets
* feat(gui, monero-sys): Display recently opened wallets
* small refactors
* fix(gui): Enable background_sync, display temp "Loading..." if values are null
* feat(gui): Remove headers from pages, show selected navigation item
* feat(gui): Explicitly tell user if no swaps have been made yet
* feat(gui): send sync and history updates
* feat(gui): Fetch monero wallet details when context becomes availiable
* feat(gui): Display Monero primary address without modal
* feat(gui): Make "swap" button on wallet page take you to "/swap"
* feat(gui): Rework send modal, adjust number input, added send to field
* feat(gui): set block restore height, not working
* refactor(gui): Optimize number input and add support for switching between currency
* feat(gui): Display real fiat currency prices in send modal
* feat(gui): Add error message for too high send amount
* feat(gui): Modern UI for SeedSelectionDialog
* feat(gui): Wrap MoneroWalletActions
* wip
* refactoring approval callback
* feat(gui): Send Direction of Transaction in History to Frontend
* feat(gui): Let user approve transaction before publishing
* feat: Display 8 digits for Monero amounts by default
* feat(monero-sys): Store pending (non published) transactions in Mutex map inside wallet thread
This allows seperating signing and publishing transactions cleanly
* dprint fmt
* fix(gui): Refresh Monero wallet history C++ struct before serializing
* feat(monero-rpc-pool): Fail after three JSON-RPC errors
* feat(monero-sys): Add wrapper around verify_wallet_password
* feat(gui): Allow opening password-protected Wallets
* refactor: fmt, remove receive button
* fix(gui): Convert to XMR before converting into Fiat
* feat(gui): Add dialog for setting restore height
* feat(gui): block height can be changed, blocks when too low
* refactor(monero-sys): Remove old WalletListener code
* feat(gui): Continually ask for user to select wallet and enter password, if user rejects, offer to select different wallet
* refactor(swap): Extract "select Monero wallet" into own function
* refactor(tauri): Dont kill monero-wallet-rpc
* refactor(tauri): Avoid multiple concurrent Contexts starting
* refactor: Change "Cancel" to "Change wallet" on PasswordEntryDialog
* feat(gui): show curent block height, fix blockage
* Cargo.lock update
* refactor(monero-sys): Use match instead of is_err() and expect(...)
* refactor: better context for WalletHandle constructor method errors handling
* refactor(monero-sys): Common open_with<F>(path: String, daemon: Daemon, wallet_op: F) function
* feat: check empty password before requeston password for wallet
* feat: Remove "Checking for available remote nodes" from frontend
* feat(gui): Allow sweeping entire Monero balance
* feat(monero-rpc-pool): Keep alive TCP connections, do not record JSON-RPC errors as failure if >=3 nodes failed
If >=3 nodes failed we assume it was an actual issue on our side, not an issue with the node
* refactor(swap): Remove dead code
* add comment to WalletHandleListener::on_refreshed{...}
* feat(gui): show current block height in the field
* refactor: remove unused UserCancelledError;
* refactor: No Arc<Mutex<_>> for Pending TXs map
* refactor: remove redundant } catch (error) {
* feat: add our new crates to `OUR_CRATES` in tracing util
* fix(gui): Add math.ceil to piconero conversion to ensure integer
* fix(gui): Close menu when option is clicked
* review and improve/reduce uses of unsafe, also remove unique_ptr wrapper around TransactionHistory to avoid double free
* fix(gui): Use monero amount from units.tsx
* fix(gui): Use PromiseInvokeButton for simplification for approving of send transaction
* update comment, rename function
* refactor(gui): Fix alignment of amounts
* refactor(gui): Remove sending and refreshing states from wallet
* fix(cli, gui): use old seed flow on no tauri, fix minor issues in gui
* fix: use the new named function
* refactor(gui): Add skeletons for monero wallet when still loading
* refactor(gui): Remove isLoading from wallet slice
* feat(gui): Add success dialog after send transaction was approved
* fix(gui): Floor piconero amount in sendMoneroTransaction
* feat(gui): Allow view on explorer button on send success modal
* feat(backend): save the wallet state on events
* fix(structure): move throttle into its own crate
* fix(log): remove spammy logs
* fix(logs): log folder in confid
* remove "sync progress: " log
* small refactors
* save wallet at most every 60s
* remove useless logs
* underscore unused variables
* feat(gui): Add timestamp of the tx
* feat(gui): Add the legacy wallet init option
* legac ybutton
* Fix(gui, asb): reverse the log config
remove log in bridge.h
cleanup
* use none for .store(..)
* display dot for running swap
---------
Co-authored-by: Maksim Kirillov <maksim.kirillov@staticlabs.de>
Co-authored-by: b-enedict <benedict.seuss@gmail.com>
Co-authored-by: einliterflasche <einliterflasche@pm.me>
This commit is contained in:
parent
eb0dc10489
commit
a7823d7489
118 changed files with 7857 additions and 3456 deletions
|
|
@ -21,7 +21,6 @@ rustls = { version = "0.23.26", default-features = false, features = ["ring"] }
|
|||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
swap = { path = "../swap", features = [ "tauri" ] }
|
||||
sysinfo = "=0.32.1"
|
||||
tauri = { version = "^2.0.0", features = [ "config-json5" ] }
|
||||
tauri-plugin-clipboard-manager = "^2.0.0"
|
||||
tauri-plugin-dialog = "2.2.2"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
"cli:allow-cli-matches",
|
||||
"updater:default",
|
||||
"process:allow-restart",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"dialog:allow-open",
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use anyhow::Context as AnyhowContext;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::result::Result;
|
||||
|
|
@ -10,9 +9,12 @@ use swap::cli::{
|
|||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
||||
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
|
||||
CheckSeedResponse, ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs,
|
||||
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetPendingApprovalsResponse,
|
||||
GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
|
||||
ResolveApprovalArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs,
|
||||
GetMoneroHistoryArgs, GetMoneroMainAddressArgs, GetMoneroSyncProgressArgs,
|
||||
GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs,
|
||||
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
|
||||
RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs,
|
||||
SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||
},
|
||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
||||
Context, ContextBuilder,
|
||||
|
|
@ -64,11 +66,11 @@ macro_rules! tauri_command {
|
|||
($fn_name:ident, $request_name:ident) => {
|
||||
#[tauri::command]
|
||||
async fn $fn_name(
|
||||
context: tauri::State<'_, RwLock<State>>,
|
||||
state: tauri::State<'_, State>,
|
||||
args: $request_name,
|
||||
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
||||
// Throw error if context is not available
|
||||
let context = context.read().await.try_get_context()?;
|
||||
let context = state.try_get_context()?;
|
||||
|
||||
<$request_name as swap::cli::api::request::Request>::request(args, context)
|
||||
.await
|
||||
|
|
@ -78,10 +80,10 @@ macro_rules! tauri_command {
|
|||
($fn_name:ident, $request_name:ident, no_args) => {
|
||||
#[tauri::command]
|
||||
async fn $fn_name(
|
||||
context: tauri::State<'_, RwLock<State>>,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
||||
// Throw error if context is not available
|
||||
let context = context.read().await.try_get_context()?;
|
||||
let context = state.try_get_context()?;
|
||||
|
||||
<$request_name as swap::cli::api::request::Request>::request($request_name {}, context)
|
||||
.await
|
||||
|
|
@ -92,7 +94,7 @@ macro_rules! tauri_command {
|
|||
|
||||
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
||||
struct State {
|
||||
pub context: Option<Arc<Context>>,
|
||||
pub context: RwLock<Option<Arc<Context>>>,
|
||||
pub handle: TauriHandle,
|
||||
}
|
||||
|
||||
|
|
@ -100,22 +102,17 @@ impl State {
|
|||
/// Creates a new State instance with no Context
|
||||
fn new(handle: TauriHandle) -> Self {
|
||||
Self {
|
||||
context: None,
|
||||
context: RwLock::new(None),
|
||||
handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
.try_read()
|
||||
.map_err(|_| "Context is being modified".to_string())?
|
||||
.clone()
|
||||
.ok_or("Context not available".to_string())
|
||||
}
|
||||
|
|
@ -149,8 +146,8 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
|
|||
// 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
|
||||
let handle = TauriHandle::new(app_handle.clone());
|
||||
let state = RwLock::new(State::new(handle));
|
||||
app_handle.manage::<RwLock<State>>(state);
|
||||
let state = State::new(handle);
|
||||
app_handle.manage::<State>(state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -203,8 +200,16 @@ pub fn run() {
|
|||
resolve_approval_request,
|
||||
redact,
|
||||
save_txt_files,
|
||||
get_monero_history,
|
||||
get_monero_main_address,
|
||||
get_monero_balance,
|
||||
send_monero,
|
||||
get_monero_sync_progress,
|
||||
check_seed,
|
||||
get_pending_approvals,
|
||||
set_monero_restore_height,
|
||||
reject_approval_request,
|
||||
get_restore_height
|
||||
])
|
||||
.setup(setup)
|
||||
.build(tauri::generate_context!())
|
||||
|
|
@ -215,18 +220,17 @@ pub fn run() {
|
|||
// This is necessary to among other things stop the monero-wallet-rpc process
|
||||
// If the application is forcibly closed, this may not be called.
|
||||
// TODO: fix that
|
||||
let context = app.state::<RwLock<State>>().inner().try_read();
|
||||
let state = app.state::<State>();
|
||||
let context_to_cleanup = if let Ok(context_lock) = state.context.try_read() {
|
||||
context_lock.clone()
|
||||
} else {
|
||||
println!("Failed to acquire lock on context");
|
||||
None
|
||||
};
|
||||
|
||||
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);
|
||||
if let Some(context) = context_to_cleanup {
|
||||
if let Err(err) = context.cleanup() {
|
||||
println!("Cleanup failed {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -246,6 +250,7 @@ tauri_command!(get_logs, GetLogsArgs);
|
|||
tauri_command!(list_sellers, ListSellersArgs);
|
||||
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
||||
tauri_command!(redact, RedactArgs);
|
||||
tauri_command!(send_monero, SendMoneroArgs);
|
||||
|
||||
// These commands require no arguments
|
||||
tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args);
|
||||
|
|
@ -254,19 +259,25 @@ tauri_command!(get_swap_info, GetSwapInfoArgs);
|
|||
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
|
||||
tauri_command!(get_history, GetHistoryArgs, no_args);
|
||||
tauri_command!(get_monero_addresses, GetMoneroAddressesArgs, no_args);
|
||||
tauri_command!(get_monero_history, GetMoneroHistoryArgs, no_args);
|
||||
tauri_command!(get_current_swap, GetCurrentSwapArgs, no_args);
|
||||
tauri_command!(set_monero_restore_height, SetRestoreHeightArgs);
|
||||
tauri_command!(get_restore_height, GetRestoreHeightArgs, no_args);
|
||||
tauri_command!(get_monero_main_address, GetMoneroMainAddressArgs, no_args);
|
||||
tauri_command!(get_monero_balance, GetMoneroBalanceArgs, no_args);
|
||||
tauri_command!(get_monero_sync_progress, GetMoneroSyncProgressArgs, no_args);
|
||||
|
||||
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
|
||||
#[tauri::command]
|
||||
async fn is_context_available(context: tauri::State<'_, RwLock<State>>) -> Result<bool, String> {
|
||||
async fn is_context_available(state: tauri::State<'_, State>) -> Result<bool, String> {
|
||||
// TODO: Here we should return more information about status of the context (e.g. initializing, failed)
|
||||
Ok(context.read().await.try_get_context().is_ok())
|
||||
Ok(state.try_get_context().is_ok())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn check_monero_node(
|
||||
args: CheckMoneroNodeArgs,
|
||||
_: tauri::State<'_, RwLock<State>>,
|
||||
_: tauri::State<'_, State>,
|
||||
) -> Result<CheckMoneroNodeResponse, String> {
|
||||
args.request().await.to_string_result()
|
||||
}
|
||||
|
|
@ -274,7 +285,7 @@ async fn check_monero_node(
|
|||
#[tauri::command]
|
||||
async fn check_electrum_node(
|
||||
args: CheckElectrumNodeArgs,
|
||||
_: tauri::State<'_, RwLock<State>>,
|
||||
_: tauri::State<'_, State>,
|
||||
) -> Result<CheckElectrumNodeResponse, String> {
|
||||
args.request().await.to_string_result()
|
||||
}
|
||||
|
|
@ -282,7 +293,7 @@ async fn check_electrum_node(
|
|||
#[tauri::command]
|
||||
async fn check_seed(
|
||||
args: CheckSeedArgs,
|
||||
_: tauri::State<'_, RwLock<State>>,
|
||||
_: tauri::State<'_, State>,
|
||||
) -> Result<CheckSeedResponse, String> {
|
||||
args.request().await.to_string_result()
|
||||
}
|
||||
|
|
@ -291,10 +302,7 @@ async fn check_seed(
|
|||
// This is independent of the context to ensure the user can open the directory even if the context cannot
|
||||
// be initialized (for troubleshooting purposes)
|
||||
#[tauri::command]
|
||||
async fn get_data_dir(
|
||||
args: GetDataDirArgs,
|
||||
_: tauri::State<'_, RwLock<State>>,
|
||||
) -> Result<String, String> {
|
||||
async fn get_data_dir(args: GetDataDirArgs, _: tauri::State<'_, State>) -> Result<String, String> {
|
||||
Ok(data::data_dir_from(None, args.is_testnet)
|
||||
.to_string_result()?
|
||||
.to_string_lossy()
|
||||
|
|
@ -349,26 +357,46 @@ async fn save_txt_files(
|
|||
#[tauri::command]
|
||||
async fn resolve_approval_request(
|
||||
args: ResolveApprovalArgs,
|
||||
state: tauri::State<'_, RwLock<State>>,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<(), String> {
|
||||
println!("Resolving approval request");
|
||||
let lock = state.read().await;
|
||||
let request_id = args
|
||||
.request_id
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid request ID '{}': {}", args.request_id, e))?;
|
||||
|
||||
lock.handle
|
||||
.resolve_approval(args.request_id.parse().unwrap(), args.accept)
|
||||
state
|
||||
.handle
|
||||
.resolve_approval(request_id, args.accept)
|
||||
.await
|
||||
.to_string_result()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn reject_approval_request(
|
||||
args: RejectApprovalArgs,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<RejectApprovalResponse, String> {
|
||||
let request_id = args
|
||||
.request_id
|
||||
.parse()
|
||||
.map_err(|e| format!("Invalid request ID '{}': {}", args.request_id, e))?;
|
||||
|
||||
state
|
||||
.handle
|
||||
.reject_approval(request_id)
|
||||
.await
|
||||
.to_string_result()?;
|
||||
|
||||
Ok(RejectApprovalResponse { success: true })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_pending_approvals(
|
||||
state: tauri::State<'_, RwLock<State>>,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<GetPendingApprovalsResponse, String> {
|
||||
let approvals = state
|
||||
.read()
|
||||
.await
|
||||
.handle
|
||||
.get_pending_approvals()
|
||||
.await
|
||||
|
|
@ -382,41 +410,21 @@ async fn get_pending_approvals(
|
|||
async fn initialize_context(
|
||||
settings: TauriSettings,
|
||||
testnet: bool,
|
||||
state: tauri::State<'_, RwLock<State>>,
|
||||
state: tauri::State<'_, State>,
|
||||
) -> Result<(), String> {
|
||||
// When the app crashes, the monero-wallet-rpc process may not be killed
|
||||
// This can lead to issues when the app is restarted
|
||||
// because the monero-wallet-rpc has a lock on the wallet
|
||||
// this will prevent the newly spawned instance from opening the wallet
|
||||
// To fix this, we kill any running monero-wallet-rpc processes
|
||||
let sys = sysinfo::System::new_with_specifics(
|
||||
sysinfo::RefreshKind::new().with_processes(sysinfo::ProcessRefreshKind::new()),
|
||||
);
|
||||
// Lock at the beginning - fail immediately if already locked
|
||||
let mut context_lock = state
|
||||
.context
|
||||
.try_write()
|
||||
.map_err(|_| "Context is already being initialized".to_string())?;
|
||||
|
||||
for (pid, process) in sys.processes() {
|
||||
if process
|
||||
.name()
|
||||
.to_string_lossy()
|
||||
.starts_with("monero-wallet-rpc")
|
||||
{
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
println!("Killing monero-wallet-rpc process with pid: {}", pid);
|
||||
process.kill();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
println!("Would kill monero-wallet-rpc process with pid: {}", pid);
|
||||
}
|
||||
// Fail if the context is already initialized
|
||||
if context_lock.is_some() {
|
||||
return Err("Context is already initialized".to_string());
|
||||
}
|
||||
|
||||
// Get app handle and create a Tauri handle
|
||||
let tauri_handle = state
|
||||
.try_read()
|
||||
.context("Context is already being initialized")
|
||||
.to_string_result()?
|
||||
.handle
|
||||
.clone();
|
||||
// Get tauri handle from the state
|
||||
let tauri_handle = state.handle.clone();
|
||||
|
||||
// Notify frontend that the context is being initialized
|
||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Initializing);
|
||||
|
|
@ -436,11 +444,7 @@ async fn initialize_context(
|
|||
|
||||
match context_result {
|
||||
Ok(context_instance) => {
|
||||
state
|
||||
.try_write()
|
||||
.context("Context is already being initialized")
|
||||
.to_string_result()?
|
||||
.set_context(Arc::new(context_instance));
|
||||
*context_lock = Some(Arc::new(context_instance));
|
||||
|
||||
tracing::info!("Context initialized");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue