mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-11-27 11:10:31 -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
115
Cargo.lock
generated
115
Cargo.lock
generated
|
|
@ -1978,16 +1978,6 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-deque"
|
|
||||||
version = "0.8.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-epoch",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-epoch"
|
name = "crossbeam-epoch"
|
||||||
version = "0.9.18"
|
version = "0.9.18"
|
||||||
|
|
@ -6078,6 +6068,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"backoff",
|
"backoff",
|
||||||
|
"chrono",
|
||||||
"cmake",
|
"cmake",
|
||||||
"cxx",
|
"cxx",
|
||||||
"cxx-build",
|
"cxx-build",
|
||||||
|
|
@ -6347,15 +6338,6 @@ dependencies = [
|
||||||
"instant",
|
"instant",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ntapi"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
|
|
@ -7839,26 +7821,6 @@ version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rayon"
|
|
||||||
version = "1.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
"rayon-core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rayon-core"
|
|
||||||
version = "1.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-deque",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rcgen"
|
name = "rcgen"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
|
|
@ -9829,6 +9791,7 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"testcontainers",
|
"testcontainers",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
|
"throttle",
|
||||||
"time 0.3.41",
|
"time 0.3.41",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tar",
|
"tokio-tar",
|
||||||
|
|
@ -9980,20 +9943,6 @@ dependencies = [
|
||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sysinfo"
|
|
||||||
version = "0.32.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
"memchr",
|
|
||||||
"ntapi",
|
|
||||||
"rayon",
|
|
||||||
"windows 0.57.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-configuration"
|
name = "system-configuration"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
|
|
@ -10630,6 +10579,13 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "throttle"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiff"
|
name = "tiff"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
|
@ -12412,7 +12368,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"swap",
|
"swap",
|
||||||
"sysinfo",
|
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-cli",
|
"tauri-plugin-cli",
|
||||||
|
|
@ -12956,8 +12911,8 @@ dependencies = [
|
||||||
"webview2-com-sys",
|
"webview2-com-sys",
|
||||||
"windows 0.61.3",
|
"windows 0.61.3",
|
||||||
"windows-core 0.61.2",
|
"windows-core 0.61.2",
|
||||||
"windows-implement 0.60.0",
|
"windows-implement",
|
||||||
"windows-interface 0.59.1",
|
"windows-interface",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -13060,16 +13015,6 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows"
|
|
||||||
version = "0.57.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
|
||||||
dependencies = [
|
|
||||||
"windows-core 0.57.0",
|
|
||||||
"windows-targets 0.52.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.61.3"
|
version = "0.61.3"
|
||||||
|
|
@ -13102,26 +13047,14 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-core"
|
|
||||||
version = "0.57.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
|
||||||
dependencies = [
|
|
||||||
"windows-implement 0.57.0",
|
|
||||||
"windows-interface 0.57.0",
|
|
||||||
"windows-result 0.1.2",
|
|
||||||
"windows-targets 0.52.6",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement 0.60.0",
|
"windows-implement",
|
||||||
"windows-interface 0.59.1",
|
"windows-interface",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
"windows-result 0.3.4",
|
"windows-result 0.3.4",
|
||||||
"windows-strings",
|
"windows-strings",
|
||||||
|
|
@ -13138,17 +13071,6 @@ dependencies = [
|
||||||
"windows-threading",
|
"windows-threading",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-implement"
|
|
||||||
version = "0.57.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.104",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.60.0"
|
version = "0.60.0"
|
||||||
|
|
@ -13160,17 +13082,6 @@ dependencies = [
|
||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-interface"
|
|
||||||
version = "0.57.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.104",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.59.1"
|
version = "0.59.1"
|
||||||
|
|
|
||||||
12
Cargo.toml
12
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [ "electrum-pool", "monero-rpc", "monero-rpc-pool", "monero-sys", "monero-seed", "src-tauri", "swap", "swap-env", "swap-fs", "swap-feed", "swap-serde"]
|
members = [ "electrum-pool", "monero-rpc", "monero-rpc-pool", "monero-sys", "monero-seed", "src-tauri", "swap", "swap-env", "swap-fs", "swap-feed", "swap-serde", "throttle"]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
|
@ -11,17 +11,17 @@ futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
tracing = { version = "0.1", features = ["attributes"] }
|
tracing = { version = "0.1", features = ["attributes"] }
|
||||||
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter", "time", "tracing-log", "json"] }
|
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "ansi", "env-filter", "time", "tracing-log", "json"] }
|
||||||
bitcoin = { version = "0.32", features = ["rand", "serde"] }
|
bitcoin = { version = "0.32", features = ["rand", "serde"] }
|
||||||
|
hex = "0.4"
|
||||||
|
libp2p = { version = "0.53.2" }
|
||||||
monero = { version = "0.12", features = ["serde_support"] }
|
monero = { version = "0.12", features = ["serde_support"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
|
||||||
typeshare = "1.0"
|
|
||||||
thiserror = "1"
|
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json"] }
|
||||||
rust_decimal = { version = "1", features = ["serde-float"] }
|
rust_decimal = { version = "1", features = ["serde-float"] }
|
||||||
rust_decimal_macros = "1"
|
rust_decimal_macros = "1"
|
||||||
libp2p = { version = "0.53.2" }
|
thiserror = "1"
|
||||||
|
typeshare = "1.0"
|
||||||
url = { version = "2", features = ["serde"] }
|
url = { version = "2", features = ["serde"] }
|
||||||
hex = "0.4"
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
|
# patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51
|
||||||
|
|
|
||||||
4
justfile
4
justfile
|
|
@ -26,7 +26,7 @@ test-ffi-address:
|
||||||
|
|
||||||
# Start the Tauri app
|
# Start the Tauri app
|
||||||
tauri:
|
tauri:
|
||||||
cd src-tauri && cargo tauri dev --no-watch -- -- --testnet
|
cd src-tauri && cargo tauri dev --no-watch --verbose -- -- --testnet
|
||||||
|
|
||||||
tauri-mainnet:
|
tauri-mainnet:
|
||||||
cd src-tauri && cargo tauri dev --no-watch
|
cd src-tauri && cargo tauri dev --no-watch
|
||||||
|
|
@ -105,4 +105,4 @@ prepare_mac_os_brew_dependencies:
|
||||||
# Takes a crate (e.g monero-rpc-pool) and uses code2prompt to copy to clipboard
|
# Takes a crate (e.g monero-rpc-pool) and uses code2prompt to copy to clipboard
|
||||||
# E.g code2prompt . --exclude "*.lock" --exclude ".sqlx/*" --exclude "target"
|
# E.g code2prompt . --exclude "*.lock" --exclude ".sqlx/*" --exclude "target"
|
||||||
code2prompt_single_crate crate:
|
code2prompt_single_crate crate:
|
||||||
cd {{crate}} && code2prompt . --exclude "*.lock" --exclude ".sqlx/*" --exclude "target"
|
cd {{crate}} && code2prompt . --exclude "*.lock" --exclude ".sqlx/*" --exclude "target"
|
||||||
|
|
|
||||||
|
|
@ -504,11 +504,7 @@ impl MoneroWallet {
|
||||||
/// Sweep multiple addresses with different ratios
|
/// Sweep multiple addresses with different ratios
|
||||||
/// If the address is `None`, the address will be set to the primary address of the
|
/// If the address is `None`, the address will be set to the primary address of the
|
||||||
/// main wallet.
|
/// main wallet.
|
||||||
pub async fn sweep_multi(
|
pub async fn sweep_multi(&self, addresses: &[Address], ratios: &[f64]) -> Result<TxReceipt> {
|
||||||
&self,
|
|
||||||
addresses: &[Address],
|
|
||||||
ratios: &[f64],
|
|
||||||
) -> Result<TxReceipt> {
|
|
||||||
tracing::info!("`{}` sweeping multi ({:?})", self.name, ratios);
|
tracing::info!("`{}` sweeping multi ({:?})", self.name, ratios);
|
||||||
self.balance().await?;
|
self.balance().await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ impl Database {
|
||||||
info!("Created application data directory: {}", data_dir.display());
|
info!("Created application data directory: {}", data_dir.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
let db_path = data_dir.join("nodes_v2.db");
|
let db_path = data_dir.join("nodes_v3.db");
|
||||||
|
|
||||||
info!("Using database at {}", db_path.display());
|
info!("Using database at {}", db_path.display());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ use proxy::{proxy_handler, stats_handler};
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub node_pool: Arc<NodePool>,
|
pub node_pool: Arc<NodePool>,
|
||||||
|
pub http_client: reqwest::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manages background tasks for the RPC pool
|
/// Manages background tasks for the RPC pool
|
||||||
|
|
@ -59,6 +60,12 @@ pub struct ServerInfo {
|
||||||
pub host: String,
|
pub host: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Into<String> for ServerInfo {
|
||||||
|
fn into(self) -> String {
|
||||||
|
format!("http://{}:{}", self.host, self.port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_app_with_receiver(
|
async fn create_app_with_receiver(
|
||||||
config: Config,
|
config: Config,
|
||||||
network: Network,
|
network: Network,
|
||||||
|
|
@ -97,7 +104,20 @@ async fn create_app_with_receiver(
|
||||||
status_update_handle,
|
status_update_handle,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app_state = AppState { node_pool };
|
// Create shared HTTP client with connection pooling and keep-alive
|
||||||
|
let http_client = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
|
.danger_accept_invalid_certs(true)
|
||||||
|
.pool_max_idle_per_host(10)
|
||||||
|
.pool_idle_timeout(std::time::Duration::from_secs(90))
|
||||||
|
.tcp_keepalive(std::time::Duration::from_secs(60))
|
||||||
|
.build()
|
||||||
|
.expect("Failed to create HTTP client");
|
||||||
|
|
||||||
|
let app_state = AppState {
|
||||||
|
node_pool,
|
||||||
|
http_client,
|
||||||
|
};
|
||||||
|
|
||||||
// Build the app
|
// Build the app
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,16 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
|
fn display_node(node: &(String, String, i64)) -> String {
|
||||||
|
format!("{}://{}:{}", node.0, node.1, node.2)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum HandlerError {
|
enum HandlerError {
|
||||||
NoNodes,
|
NoNodes,
|
||||||
PoolError(String),
|
PoolError(String),
|
||||||
RequestError(String),
|
RequestError(String),
|
||||||
|
JsonRpcError(String),
|
||||||
AllRequestsFailed(Vec<(String, String)>),
|
AllRequestsFailed(Vec<(String, String)>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,6 +30,7 @@ impl std::fmt::Display for HandlerError {
|
||||||
HandlerError::NoNodes => write!(f, "No nodes available"),
|
HandlerError::NoNodes => write!(f, "No nodes available"),
|
||||||
HandlerError::PoolError(msg) => write!(f, "Pool error: {}", msg),
|
HandlerError::PoolError(msg) => write!(f, "Pool error: {}", msg),
|
||||||
HandlerError::RequestError(msg) => write!(f, "Request error: {}", msg),
|
HandlerError::RequestError(msg) => write!(f, "Request error: {}", msg),
|
||||||
|
HandlerError::JsonRpcError(msg) => write!(f, "JSON-RPC error: {}", msg),
|
||||||
HandlerError::AllRequestsFailed(errors) => {
|
HandlerError::AllRequestsFailed(errors) => {
|
||||||
write!(f, "All requests failed: [")?;
|
write!(f, "All requests failed: [")?;
|
||||||
for (i, (node, error)) in errors.iter().enumerate() {
|
for (i, (node, error)) in errors.iter().enumerate() {
|
||||||
|
|
@ -60,17 +66,13 @@ fn extract_jsonrpc_method(body: &[u8]) -> Option<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn raw_http_request(
|
async fn raw_http_request(
|
||||||
|
client: &reqwest::Client,
|
||||||
node_url: (String, String, i64),
|
node_url: (String, String, i64),
|
||||||
path: &str,
|
path: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
body: Option<&[u8]>,
|
body: Option<&[u8]>,
|
||||||
) -> Result<Response, HandlerError> {
|
) -> Result<Response, HandlerError> {
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
|
||||||
.build()
|
|
||||||
.map_err(|e| HandlerError::RequestError(format!("{:#?}", e)))?;
|
|
||||||
|
|
||||||
let (scheme, host, port) = &node_url;
|
let (scheme, host, port) = &node_url;
|
||||||
let url = format!("{}://{}:{}{}", scheme, host, port, path);
|
let url = format!("{}://{}:{}{}", scheme, host, port, path);
|
||||||
|
|
||||||
|
|
@ -172,6 +174,7 @@ async fn record_failure(state: &AppState, scheme: &str, host: &str, port: i64) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn single_raw_request(
|
async fn single_raw_request(
|
||||||
|
client: &reqwest::Client,
|
||||||
node_url: (String, String, i64),
|
node_url: (String, String, i64),
|
||||||
path: &str,
|
path: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
|
|
@ -180,7 +183,7 @@ async fn single_raw_request(
|
||||||
) -> Result<(Response, (String, String, i64), f64), HandlerError> {
|
) -> Result<(Response, (String, String, i64), f64), HandlerError> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
match raw_http_request(node_url.clone(), path, method, headers, body).await {
|
match raw_http_request(client, node_url.clone(), path, method, headers, body).await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let elapsed = start_time.elapsed();
|
let elapsed = start_time.elapsed();
|
||||||
let latency_ms = elapsed.as_millis() as f64;
|
let latency_ms = elapsed.as_millis() as f64;
|
||||||
|
|
@ -195,7 +198,7 @@ async fn single_raw_request(
|
||||||
.map_err(|e| HandlerError::RequestError(format!("{:#?}", e)))?;
|
.map_err(|e| HandlerError::RequestError(format!("{:#?}", e)))?;
|
||||||
|
|
||||||
if is_jsonrpc_error(&body_bytes) {
|
if is_jsonrpc_error(&body_bytes) {
|
||||||
return Err(HandlerError::RequestError("JSON-RPC error".to_string()));
|
return Err(HandlerError::JsonRpcError("JSON-RPC error".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconstruct response with the body we consumed
|
// Reconstruct response with the body we consumed
|
||||||
|
|
@ -225,6 +228,7 @@ async fn sequential_requests(
|
||||||
body: Option<&[u8]>,
|
body: Option<&[u8]>,
|
||||||
) -> Result<Response, HandlerError> {
|
) -> Result<Response, HandlerError> {
|
||||||
const POOL_SIZE: usize = 20;
|
const POOL_SIZE: usize = 20;
|
||||||
|
const MAX_JSONRPC_ERRORS: usize = 3;
|
||||||
|
|
||||||
// Extract JSON-RPC method for better logging
|
// Extract JSON-RPC method for better logging
|
||||||
let jsonrpc_method = if path == "/json_rpc" {
|
let jsonrpc_method = if path == "/json_rpc" {
|
||||||
|
|
@ -238,7 +242,7 @@ async fn sequential_requests(
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut tried_nodes = 0;
|
let mut tried_nodes = 0;
|
||||||
let mut collected_errors: Vec<(String, String)> = Vec::new();
|
let mut collected_errors: Vec<((String, String, i64), HandlerError)> = Vec::new();
|
||||||
|
|
||||||
// Get the pool of nodes
|
// Get the pool of nodes
|
||||||
let available_pool = {
|
let available_pool = {
|
||||||
|
|
@ -283,7 +287,16 @@ async fn sequential_requests(
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
match single_raw_request(node.clone(), path, method, headers, body).await {
|
match single_raw_request(
|
||||||
|
&state.http_client,
|
||||||
|
node.clone(),
|
||||||
|
path,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok((response, winning_node, latency_ms)) => {
|
Ok((response, winning_node, latency_ms)) => {
|
||||||
let (scheme, host, port) = &winning_node;
|
let (scheme, host, port) = &winning_node;
|
||||||
let winning_node_display = format!("{}://{}:{}", scheme, host, port);
|
let winning_node_display = format!("{}://{}:{}", scheme, host, port);
|
||||||
|
|
@ -304,24 +317,61 @@ async fn sequential_requests(
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
collected_errors.push((node_display.clone(), e.to_string()));
|
collected_errors.push((node.clone(), e.clone()));
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
"Request failed with node {} with error {} - trying next node...",
|
"Request failed with node {}: {} - checking if we should fail fast...",
|
||||||
node_display, e
|
node_display, e
|
||||||
);
|
);
|
||||||
|
|
||||||
record_failure(state, &node.0, &node.1, node.2).await;
|
// Count JSON-RPC errors by checking through all collected errors (type-safe)
|
||||||
|
let jsonrpc_error_count = collected_errors
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, error)| matches!(error, HandlerError::JsonRpcError(_)))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// Fail fast after MAX_JSONRPC_ERRORS JSON-RPC errors
|
||||||
|
if jsonrpc_error_count >= MAX_JSONRPC_ERRORS {
|
||||||
|
match &jsonrpc_method {
|
||||||
|
Some(rpc_method) => error!(
|
||||||
|
"Failing fast after {} JSON-RPC errors for {} request (JSON-RPC: {}). These are likely request-specific issues that won't resolve on other servers.",
|
||||||
|
jsonrpc_error_count, method, rpc_method
|
||||||
|
),
|
||||||
|
None => error!(
|
||||||
|
"Failing fast after {} JSON-RPC errors for {} request. These are likely request-specific issues that won't resolve on other servers.",
|
||||||
|
jsonrpc_error_count, method
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record all non-JSON-RPC errors as failures
|
||||||
|
for (node, error) in collected_errors.iter() {
|
||||||
|
if !matches!(error, HandlerError::JsonRpcError(_)) {
|
||||||
|
record_failure(state, &node.0, &node.1, node.2).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(HandlerError::AllRequestsFailed(
|
||||||
|
collected_errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|(node, error)| (display_node(&node), error.to_string()))
|
||||||
|
.collect(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record failures for all nodes that were tried
|
||||||
|
for (node, _) in collected_errors.iter() {
|
||||||
|
record_failure(state, &node.0, &node.1, node.2).await;
|
||||||
|
}
|
||||||
|
|
||||||
// Log detailed error information
|
// Log detailed error information
|
||||||
let detailed_errors: Vec<String> = collected_errors
|
let detailed_errors: Vec<String> = collected_errors
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(node, error)| format!("{}: {}", node, error))
|
.map(|(node, error)| format!("{}: {}", display_node(node), error))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
match &jsonrpc_method {
|
match &jsonrpc_method {
|
||||||
|
|
@ -340,7 +390,12 @@ async fn sequential_requests(
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(HandlerError::AllRequestsFailed(collected_errors))
|
Err(HandlerError::AllRequestsFailed(
|
||||||
|
collected_errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|(node, error)| (display_node(&node), error.to_string()))
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Forward a request to the node pool, returning either a successful response or a simple
|
/// Forward a request to the node pool, returning either a successful response or a simple
|
||||||
|
|
@ -400,6 +455,15 @@ async fn proxy_request(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
HandlerError::JsonRpcError(msg) => {
|
||||||
|
json!({
|
||||||
|
"error": "JSON-RPC error",
|
||||||
|
"details": {
|
||||||
|
"type": "JsonRpcError",
|
||||||
|
"message": msg
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Response::builder()
|
Response::builder()
|
||||||
|
|
|
||||||
23
monero-sys/.sqlx/query-7e58428584d28a238ab37a83662b88afcef6fc5246f11c85a35869f79da61c34.json
generated
Normal file
23
monero-sys/.sqlx/query-7e58428584d28a238ab37a83662b88afcef6fc5246f11c85a35869f79da61c34.json
generated
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT wallet_path, last_opened_at\n FROM recent_wallets \n ORDER BY last_opened_at DESC\n LIMIT ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "wallet_path",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "last_opened_at",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [false, false]
|
||||||
|
},
|
||||||
|
"hash": "7e58428584d28a238ab37a83662b88afcef6fc5246f11c85a35869f79da61c34"
|
||||||
|
}
|
||||||
12
monero-sys/.sqlx/query-a679f789c90ede34cd840d23d90087520dcf1777fdf4cc3ed7aab0c9d70d060c.json
generated
Normal file
12
monero-sys/.sqlx/query-a679f789c90ede34cd840d23d90087520dcf1777fdf4cc3ed7aab0c9d70d060c.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO recent_wallets (wallet_path, last_opened_at)\n VALUES (?, ?)\n ON CONFLICT(wallet_path) DO UPDATE SET last_opened_at = excluded.last_opened_at\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "a679f789c90ede34cd840d23d90087520dcf1777fdf4cc3ed7aab0c9d70d060c"
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
backoff = { version = "0.4.0", features = ["futures", "tokio"] }
|
backoff = { version = "0.4.0", features = ["futures", "tokio"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
cxx = "1.0.137"
|
cxx = "1.0.137"
|
||||||
monero = { workspace = true }
|
monero = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
CREATE TABLE recent_wallets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
wallet_path TEXT UNIQUE NOT NULL,
|
||||||
|
last_opened_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_recent_wallets_last_opened ON recent_wallets(last_opened_at DESC);
|
||||||
56
monero-sys/regenerate_sqlx_cache.sh
Executable file
56
monero-sys/regenerate_sqlx_cache.sh
Executable file
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# regenerate_sqlx_cache.sh
|
||||||
|
#
|
||||||
|
# Script to regenerate SQLx query cache for monero-rpc-pool
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Creates a temporary SQLite database in a temp directory
|
||||||
|
# 2. Runs all database migrations to set up the schema
|
||||||
|
# 3. Regenerates the SQLx query cache (.sqlx directory)
|
||||||
|
# 4. Cleans up temporary files automatically
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./regenerate_sqlx_cache.sh
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - cargo and sqlx-cli must be installed
|
||||||
|
# - Must be run from the monero-rpc-pool directory
|
||||||
|
# - migrations/ directory must exist with valid migration files
|
||||||
|
#
|
||||||
|
# The generated .sqlx directory should be committed to version control
|
||||||
|
# to enable offline compilation without requiring DATABASE_URL.
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
echo "🔄 Regenerating SQLx query cache..."
|
||||||
|
|
||||||
|
# Create a temporary directory for the database
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
TEMP_DB="$TEMP_DIR/temp_sqlx_cache.sqlite"
|
||||||
|
DATABASE_URL="sqlite:$TEMP_DB"
|
||||||
|
|
||||||
|
echo "📁 Using temporary database: $TEMP_DB"
|
||||||
|
|
||||||
|
# Function to cleanup on exit
|
||||||
|
cleanup() {
|
||||||
|
echo "🧹 Cleaning up temporary files..."
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Export DATABASE_URL for sqlx commands
|
||||||
|
export DATABASE_URL
|
||||||
|
|
||||||
|
echo "🗄️ Creating database..."
|
||||||
|
cargo sqlx database create
|
||||||
|
|
||||||
|
echo "🔄 Running migrations..."
|
||||||
|
cargo sqlx migrate run
|
||||||
|
|
||||||
|
echo "⚡ Preparing SQLx query cache..."
|
||||||
|
cargo sqlx prepare
|
||||||
|
|
||||||
|
echo "✅ SQLx query cache regenerated successfully!"
|
||||||
|
echo "📝 The .sqlx directory has been updated with the latest query metadata."
|
||||||
|
echo "💡 Make sure to commit the .sqlx directory to version control."
|
||||||
|
|
@ -16,7 +16,9 @@
|
||||||
* - CXX doesn't support static methods as yet, so we define free functions here that
|
* - CXX doesn't support static methods as yet, so we define free functions here that
|
||||||
* simply call the appropriate static methods.
|
* simply call the appropriate static methods.
|
||||||
* - CXX also doesn't support returning strings by value from C++ to Rust, so we wrap
|
* - CXX also doesn't support returning strings by value from C++ to Rust, so we wrap
|
||||||
* those in a unique_ptr.
|
* those in a unique_ptr/shared_ptr.
|
||||||
|
* ATTENTION: unique_ptr will delete the object on drop,
|
||||||
|
* verify that you actually OWN the object before wrapping it in a unique_ptr. Use shared_ptr otherwise
|
||||||
* - CXX doesn't support optional arguments, so we make thin wrapper functions that either
|
* - CXX doesn't support optional arguments, so we make thin wrapper functions that either
|
||||||
* take the argument or not.
|
* take the argument or not.
|
||||||
*
|
*
|
||||||
|
|
@ -58,6 +60,26 @@ namespace Monero
|
||||||
auto addr = wallet.address(account_index, address_index);
|
auto addr = wallet.address(account_index, address_index);
|
||||||
return std::make_unique<std::string>(addr);
|
return std::make_unique<std::string>(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void rescanBlockchainAsync(Wallet &wallet)
|
||||||
|
{
|
||||||
|
wallet.rescanBlockchainAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void pauseRefresh(Wallet &wallet)
|
||||||
|
{
|
||||||
|
wallet.pauseRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void stop(Wallet &wallet)
|
||||||
|
{
|
||||||
|
wallet.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void startRefresh(Wallet &wallet)
|
||||||
|
{
|
||||||
|
wallet.startRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as for [`address`]
|
* Same as for [`address`]
|
||||||
|
|
@ -223,6 +245,16 @@ namespace Monero
|
||||||
return std::make_unique<std::vector<std::string>>(tx.txid());
|
return std::make_unique<std::vector<std::string>>(tx.txid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline uint64_t pendingTransactionFee(const PendingTransaction &tx)
|
||||||
|
{
|
||||||
|
return tx.fee();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline uint64_t pendingTransactionAmount(const PendingTransaction &tx)
|
||||||
|
{
|
||||||
|
return tx.amount();
|
||||||
|
}
|
||||||
|
|
||||||
inline std::unique_ptr<std::string> walletFilename(const Wallet &wallet)
|
inline std::unique_ptr<std::string> walletFilename(const Wallet &wallet)
|
||||||
{
|
{
|
||||||
return std::make_unique<std::string>(wallet.filename());
|
return std::make_unique<std::string>(wallet.filename());
|
||||||
|
|
@ -234,6 +266,156 @@ namespace Monero
|
||||||
{
|
{
|
||||||
v.push_back(s);
|
v.push_back(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hash of a transaction from TransactionInfo.
|
||||||
|
*/
|
||||||
|
inline std::unique_ptr<std::string> transactionInfoHash(const TransactionInfo &tx_info)
|
||||||
|
{
|
||||||
|
return std::make_unique<std::string>(tx_info.hash());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timestamp of a transaction from TransactionInfo.
|
||||||
|
*/
|
||||||
|
inline uint64_t transactionInfoTimestamp(const TransactionInfo &tx_info)
|
||||||
|
{
|
||||||
|
return static_cast<uint64_t>(tx_info.timestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
// bridge.h
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
#include "wallet/api/wallet2_api.h"
|
||||||
|
|
||||||
|
|
||||||
|
using CB_StringU64 = uintptr_t;
|
||||||
|
using CB_U64 = uintptr_t;
|
||||||
|
using CB_Void = uintptr_t;
|
||||||
|
using CB_Reorg = uintptr_t;
|
||||||
|
using CB_String = uintptr_t;
|
||||||
|
using CB_GetPassword = uintptr_t;
|
||||||
|
|
||||||
|
class FunctionBasedListener final : public Monero::WalletListener {
|
||||||
|
public:
|
||||||
|
FunctionBasedListener(
|
||||||
|
CB_StringU64 on_spent,
|
||||||
|
CB_StringU64 on_received,
|
||||||
|
CB_StringU64 on_unconfirmed_received,
|
||||||
|
CB_U64 on_new_block,
|
||||||
|
CB_Void on_updated,
|
||||||
|
CB_Void on_refreshed,
|
||||||
|
CB_Reorg on_reorg,
|
||||||
|
CB_String on_pool_tx_removed,
|
||||||
|
CB_GetPassword on_get_password)
|
||||||
|
:
|
||||||
|
on_spent_(on_spent),
|
||||||
|
on_received_(on_received),
|
||||||
|
on_unconfirmed_received_(on_unconfirmed_received),
|
||||||
|
on_new_block_(on_new_block),
|
||||||
|
on_updated_(on_updated),
|
||||||
|
on_refreshed_(on_refreshed),
|
||||||
|
on_reorg_(on_reorg),
|
||||||
|
on_pool_tx_removed_(on_pool_tx_removed),
|
||||||
|
on_get_password_(on_get_password) {}
|
||||||
|
|
||||||
|
void moneySpent(const std::string& txid, uint64_t amt) override {
|
||||||
|
if (on_spent_) {
|
||||||
|
auto* spent = reinterpret_cast<void(*)(const std::string&, uint64_t)>(on_spent_);
|
||||||
|
spent(txid, amt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void moneyReceived(const std::string& txid, uint64_t amt) override
|
||||||
|
{ if (on_received_) {
|
||||||
|
auto* received = reinterpret_cast<void(*)(const std::string&, uint64_t)>(on_received_);
|
||||||
|
received(txid, amt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void unconfirmedMoneyReceived(const std::string& txid, uint64_t amt) override
|
||||||
|
{ if (on_unconfirmed_received_) {
|
||||||
|
auto* unconfirmed_received = reinterpret_cast<void(*)(const std::string&, uint64_t)>(on_unconfirmed_received_);
|
||||||
|
unconfirmed_received(txid, amt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void newBlock(uint64_t h) override
|
||||||
|
{ if (on_new_block_) {
|
||||||
|
auto* new_block = reinterpret_cast<void(*)(uint64_t)>(on_new_block_);
|
||||||
|
new_block(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updated() override
|
||||||
|
{
|
||||||
|
if (on_updated_) {
|
||||||
|
auto* updated = reinterpret_cast<void(*)()>(on_updated_);
|
||||||
|
updated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshed() override
|
||||||
|
{ if (on_refreshed_) {
|
||||||
|
auto* refreshed = reinterpret_cast<void(*)()>(on_refreshed_);
|
||||||
|
refreshed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onReorg(uint64_t h, uint64_t d, size_t t) override
|
||||||
|
{ if (on_reorg_) {
|
||||||
|
auto* reorg = reinterpret_cast<void(*)(uint64_t, uint64_t, size_t)>(on_reorg_);
|
||||||
|
reorg(h, d, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPoolTxRemoved(const std::string& txid) override
|
||||||
|
{ if (on_pool_tx_removed_) {
|
||||||
|
auto* pool_tx_removed = reinterpret_cast<void(*)(const std::string&)>(on_pool_tx_removed_);
|
||||||
|
pool_tx_removed(txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
optional<std::string> onGetPassword(const char* reason) override {
|
||||||
|
if (on_get_password_) {
|
||||||
|
auto* get_password = reinterpret_cast<const char*(*)(const std::string&)>(on_get_password_);
|
||||||
|
return std::string(get_password(reason));
|
||||||
|
}
|
||||||
|
return optional<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
CB_StringU64 on_spent_;
|
||||||
|
CB_StringU64 on_received_;
|
||||||
|
CB_StringU64 on_unconfirmed_received_;
|
||||||
|
CB_U64 on_new_block_;
|
||||||
|
CB_Void on_updated_;
|
||||||
|
CB_Void on_refreshed_;
|
||||||
|
CB_Reorg on_reorg_;
|
||||||
|
CB_String on_pool_tx_removed_;
|
||||||
|
CB_GetPassword on_get_password_;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
WalletListener* create_listener(
|
||||||
|
CB_StringU64 on_spent,
|
||||||
|
CB_StringU64 on_received,
|
||||||
|
CB_StringU64 on_unconfirmed_received,
|
||||||
|
CB_U64 on_new_block,
|
||||||
|
CB_Void on_updated,
|
||||||
|
CB_Void on_refreshed,
|
||||||
|
CB_Reorg on_reorg,
|
||||||
|
CB_String on_pool_tx_removed,
|
||||||
|
CB_GetPassword on_get_password)
|
||||||
|
{
|
||||||
|
return new FunctionBasedListener(
|
||||||
|
on_spent,on_received,on_unconfirmed_received,on_new_block,
|
||||||
|
on_updated,on_refreshed,on_reorg,on_pool_tx_removed,on_get_password);
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy_listener(FunctionBasedListener* p) { delete p; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "easylogging++.h"
|
#include "easylogging++.h"
|
||||||
|
|
@ -359,3 +541,64 @@ using StringMap = std::map<String, String>;
|
||||||
using StringVec = std::vector<String>;
|
using StringVec = std::vector<String>;
|
||||||
|
|
||||||
static std::pair<StringMap, StringVec> _monero_sys_pair_instantiation;
|
static std::pair<StringMap, StringVec> _monero_sys_pair_instantiation;
|
||||||
|
|
||||||
|
namespace Monero {
|
||||||
|
|
||||||
|
// Adapter class that forwards Monero::WalletListener callbacks to Rust
|
||||||
|
class RustListenerAdapter final : public Monero::WalletListener {
|
||||||
|
public:
|
||||||
|
explicit RustListenerAdapter(rust::Box<wallet_listener::WalletListenerBox> listener)
|
||||||
|
: inner_(std::move(listener)) {}
|
||||||
|
|
||||||
|
// --- Required overrides ------------------------------------------------
|
||||||
|
void moneySpent(const std::string &txid, uint64_t amount) override {
|
||||||
|
wallet_listener::money_spent(*inner_, txid, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void moneyReceived(const std::string &txid, uint64_t amount) override {
|
||||||
|
wallet_listener::money_received(*inner_, txid, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void unconfirmedMoneyReceived(const std::string &txid, uint64_t amount) override {
|
||||||
|
wallet_listener::unconfirmed_money_received(*inner_, txid, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void newBlock(uint64_t height) override {
|
||||||
|
wallet_listener::new_block(*inner_, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updated() override {
|
||||||
|
wallet_listener::updated(*inner_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshed() override {
|
||||||
|
wallet_listener::refreshed(*inner_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onReorg(std::uint64_t height, std::uint64_t blocks_detached, std::size_t transfers_detached) override {
|
||||||
|
wallet_listener::on_reorg(*inner_, height, blocks_detached, transfers_detached);
|
||||||
|
}
|
||||||
|
|
||||||
|
optional<std::string> onGetPassword(const char * /*reason*/) override {
|
||||||
|
return optional<std::string>(); // Not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
void onPoolTxRemoved(const std::string &txid) override {
|
||||||
|
wallet_listener::pool_tx_removed(*inner_, txid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
rust::Box<wallet_listener::WalletListenerBox> inner_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Monero
|
||||||
|
|
||||||
|
namespace wallet_listener {
|
||||||
|
Monero::WalletListener* create_rust_listener_adapter(rust::Box<WalletListenerBox> listener) {
|
||||||
|
return new Monero::RustListenerAdapter(std::move(listener));
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy_rust_listener_adapter(Monero::WalletListener* ptr) {
|
||||||
|
delete ptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,11 @@ pub mod ffi {
|
||||||
/// A pending transaction.
|
/// A pending transaction.
|
||||||
type PendingTransaction;
|
type PendingTransaction;
|
||||||
|
|
||||||
/// A wallet listener.
|
/// A struct containing transaction history.
|
||||||
///
|
type TransactionHistory;
|
||||||
/// Can be attached to a wallet and will get notified upon specific events.
|
|
||||||
type WalletListener;
|
/// A struct containing a single transaction.
|
||||||
|
type TransactionInfo;
|
||||||
|
|
||||||
/// Get the wallet manager.
|
/// Get the wallet manager.
|
||||||
fn getWalletManager() -> Result<*mut WalletManager>;
|
fn getWalletManager() -> Result<*mut WalletManager>;
|
||||||
|
|
@ -100,6 +101,8 @@ pub mod ffi {
|
||||||
seed_offset: &CxxString,
|
seed_offset: &CxxString,
|
||||||
) -> Result<*mut Wallet>;
|
) -> Result<*mut Wallet>;
|
||||||
|
|
||||||
|
type WalletListener;
|
||||||
|
|
||||||
///virtual Wallet * openWallet(const std::string &path, const std::string &password, NetworkType nettype, uint64_t kdf_rounds = 1, WalletListener * listener = nullptr) = 0;
|
///virtual Wallet * openWallet(const std::string &path, const std::string &password, NetworkType nettype, uint64_t kdf_rounds = 1, WalletListener * listener = nullptr) = 0;
|
||||||
unsafe fn openWallet(
|
unsafe fn openWallet(
|
||||||
self: Pin<&mut WalletManager>,
|
self: Pin<&mut WalletManager>,
|
||||||
|
|
@ -117,9 +120,21 @@ pub mod ffi {
|
||||||
store: bool,
|
store: bool,
|
||||||
) -> Result<bool>;
|
) -> Result<bool>;
|
||||||
|
|
||||||
|
/// Store the wallet state.
|
||||||
|
fn store(self: Pin<&mut Wallet>, path: &CxxString) -> Result<bool>;
|
||||||
|
|
||||||
/// Check whether a wallet exists at the given path.
|
/// Check whether a wallet exists at the given path.
|
||||||
fn walletExists(self: Pin<&mut WalletManager>, path: &CxxString) -> Result<bool>;
|
fn walletExists(self: Pin<&mut WalletManager>, path: &CxxString) -> Result<bool>;
|
||||||
|
|
||||||
|
/// Verify the password for a wallet at the given path.
|
||||||
|
fn verifyWalletPassword(
|
||||||
|
self: &WalletManager,
|
||||||
|
keys_file_name: &CxxString,
|
||||||
|
password: &CxxString,
|
||||||
|
no_spend_key: bool,
|
||||||
|
kdf_rounds: u64,
|
||||||
|
) -> Result<bool>;
|
||||||
|
|
||||||
/// Set the address of the remote node ("daemon").
|
/// Set the address of the remote node ("daemon").
|
||||||
fn setDaemonAddress(self: Pin<&mut WalletManager>, address: &CxxString) -> Result<()>;
|
fn setDaemonAddress(self: Pin<&mut WalletManager>, address: &CxxString) -> Result<()>;
|
||||||
|
|
||||||
|
|
@ -160,6 +175,9 @@ pub mod ffi {
|
||||||
/// Get the seed of the wallet.
|
/// Get the seed of the wallet.
|
||||||
fn walletSeed(wallet: &Wallet, seed_offset: &CxxString) -> Result<UniquePtr<CxxString>>;
|
fn walletSeed(wallet: &Wallet, seed_offset: &CxxString) -> Result<UniquePtr<CxxString>>;
|
||||||
|
|
||||||
|
/// Set the seed language of the wallet.
|
||||||
|
fn setSeedLanguage(self: Pin<&mut Wallet>, language: &CxxString) -> Result<()>;
|
||||||
|
|
||||||
/// Get the wallet creation height.
|
/// Get the wallet creation height.
|
||||||
fn getRefreshFromBlockHeight(self: &Wallet) -> Result<u64>;
|
fn getRefreshFromBlockHeight(self: &Wallet) -> Result<u64>;
|
||||||
|
|
||||||
|
|
@ -181,6 +199,9 @@ pub mod ffi {
|
||||||
/// Get the current blockchain height.
|
/// Get the current blockchain height.
|
||||||
fn blockChainHeight(self: &Wallet) -> Result<u64>;
|
fn blockChainHeight(self: &Wallet) -> Result<u64>;
|
||||||
|
|
||||||
|
/// Set a listener to the wallet.
|
||||||
|
unsafe fn setListener(self: Pin<&mut Wallet>, listener: *mut WalletListener) -> Result<()>;
|
||||||
|
|
||||||
/// Get the daemon's blockchain height.
|
/// Get the daemon's blockchain height.
|
||||||
fn daemonBlockChainTargetHeight(self: &Wallet) -> Result<u64>;
|
fn daemonBlockChainTargetHeight(self: &Wallet) -> Result<u64>;
|
||||||
|
|
||||||
|
|
@ -199,6 +220,17 @@ pub mod ffi {
|
||||||
/// Force a specific restore height.
|
/// Force a specific restore height.
|
||||||
fn setRefreshFromBlockHeight(self: Pin<&mut Wallet>, height: u64) -> Result<()>;
|
fn setRefreshFromBlockHeight(self: Pin<&mut Wallet>, height: u64) -> Result<()>;
|
||||||
|
|
||||||
|
fn getBlockchainHeightByDate(self: &Wallet, year: u16, month: u8, day: u8) -> Result<u64>;
|
||||||
|
|
||||||
|
/// Rescan the blockchain asynchronously.
|
||||||
|
fn rescanBlockchainAsync(self: Pin<&mut Wallet>);
|
||||||
|
|
||||||
|
/// Pause the background refresh.
|
||||||
|
fn pauseRefresh(self: Pin<&mut Wallet>);
|
||||||
|
|
||||||
|
/// Stop the background refresh once (doesn't stop background refresh thread).
|
||||||
|
fn stop(self: Pin<&mut Wallet>);
|
||||||
|
|
||||||
/// Set whether to allow mismatched daemon versions.
|
/// Set whether to allow mismatched daemon versions.
|
||||||
fn setAllowMismatchedDaemonVersion(
|
fn setAllowMismatchedDaemonVersion(
|
||||||
self: Pin<&mut Wallet>,
|
self: Pin<&mut Wallet>,
|
||||||
|
|
@ -255,6 +287,12 @@ pub mod ffi {
|
||||||
tx: &PendingTransaction,
|
tx: &PendingTransaction,
|
||||||
) -> Result<UniquePtr<CxxVector<CxxString>>>;
|
) -> Result<UniquePtr<CxxVector<CxxString>>>;
|
||||||
|
|
||||||
|
/// Get the fee of a pending transaction.
|
||||||
|
fn pendingTransactionFee(tx: &PendingTransaction) -> Result<u64>;
|
||||||
|
|
||||||
|
/// Get the amount of a pending transaction.
|
||||||
|
fn pendingTransactionAmount(tx: &PendingTransaction) -> Result<u64>;
|
||||||
|
|
||||||
/// Get the transaction key (r) for a given txid.
|
/// Get the transaction key (r) for a given txid.
|
||||||
fn walletGetTxKey(wallet: &Wallet, txid: &CxxString) -> Result<UniquePtr<CxxString>>;
|
fn walletGetTxKey(wallet: &Wallet, txid: &CxxString) -> Result<UniquePtr<CxxString>>;
|
||||||
|
|
||||||
|
|
@ -271,6 +309,36 @@ pub mod ffi {
|
||||||
tx: *mut PendingTransaction,
|
tx: *mut PendingTransaction,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get the transaction history.
|
||||||
|
fn history(self: Pin<&mut Wallet>) -> Result<*mut TransactionHistory>;
|
||||||
|
|
||||||
|
/// Get the transaction history count.
|
||||||
|
fn count(self: &TransactionHistory) -> i32;
|
||||||
|
|
||||||
|
/// Get a transaction from the history by index.
|
||||||
|
fn transaction(self: &TransactionHistory, index: i32) -> *mut TransactionInfo;
|
||||||
|
|
||||||
|
/// Refresh the transaction history so it contains the latest transactions.
|
||||||
|
fn refresh(self: Pin<&mut TransactionHistory>) -> Result<()>;
|
||||||
|
|
||||||
|
/// Get the amount of the transaction.
|
||||||
|
fn amount(self: &TransactionInfo) -> u64;
|
||||||
|
|
||||||
|
/// Get the fee of the transaction.
|
||||||
|
fn fee(self: &TransactionInfo) -> u64;
|
||||||
|
|
||||||
|
/// Get the confirmations of the transaction.
|
||||||
|
fn confirmations(self: &TransactionInfo) -> u64;
|
||||||
|
|
||||||
|
/// Get the direction of the transaction.
|
||||||
|
fn direction(self: &TransactionInfo) -> i32;
|
||||||
|
|
||||||
|
/// Get the hash of the transaction.
|
||||||
|
fn transactionInfoHash(tx_info: &TransactionInfo) -> UniquePtr<CxxString>;
|
||||||
|
|
||||||
|
/// Get the timestamp of the transaction.
|
||||||
|
fn transactionInfoTimestamp(tx_info: &TransactionInfo) -> u64;
|
||||||
|
|
||||||
/// Sign a message with the wallet's private key.
|
/// Sign a message with the wallet's private key.
|
||||||
fn signMessage(
|
fn signMessage(
|
||||||
wallet: Pin<&mut Wallet>,
|
wallet: Pin<&mut Wallet>,
|
||||||
|
|
@ -334,6 +402,214 @@ pub mod log {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wallet listener bridge using cxx's virtual table approach
|
||||||
|
#[cxx::bridge(namespace = "wallet_listener")]
|
||||||
|
pub mod wallet_listener {
|
||||||
|
extern "Rust" {
|
||||||
|
// Opaque Rust type owned by C++
|
||||||
|
type WalletListenerBox;
|
||||||
|
|
||||||
|
// Callback methods invoked from C++
|
||||||
|
fn money_spent(listener: &mut WalletListenerBox, txid: &CxxString, amount: u64);
|
||||||
|
fn money_received(listener: &mut WalletListenerBox, txid: &CxxString, amount: u64);
|
||||||
|
fn unconfirmed_money_received(
|
||||||
|
listener: &mut WalletListenerBox,
|
||||||
|
txid: &CxxString,
|
||||||
|
amount: u64,
|
||||||
|
);
|
||||||
|
fn new_block(listener: &mut WalletListenerBox, height: u64);
|
||||||
|
fn updated(listener: &mut WalletListenerBox);
|
||||||
|
fn refreshed(listener: &mut WalletListenerBox);
|
||||||
|
fn on_reorg(
|
||||||
|
listener: &mut WalletListenerBox,
|
||||||
|
height: u64,
|
||||||
|
blocks_detached: u64,
|
||||||
|
transfers_detached: usize,
|
||||||
|
);
|
||||||
|
fn pool_tx_removed(listener: &mut WalletListenerBox, txid: &CxxString);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe extern "C++" {
|
||||||
|
include!("wallet/api/wallet2_api.h");
|
||||||
|
include!("bridge.h");
|
||||||
|
|
||||||
|
// The C++ WalletListener type lives in the Monero namespace.
|
||||||
|
#[namespace = "Monero"]
|
||||||
|
#[rust_name = "MoneroWalletListener"]
|
||||||
|
type WalletListener;
|
||||||
|
|
||||||
|
// Functions implemented in bridge.h that create / destroy the adapter.
|
||||||
|
#[namespace = "wallet_listener"]
|
||||||
|
fn create_rust_listener_adapter(
|
||||||
|
listener: Box<WalletListenerBox>,
|
||||||
|
) -> *mut MoneroWalletListener;
|
||||||
|
#[namespace = "wallet_listener"]
|
||||||
|
unsafe fn destroy_rust_listener_adapter(ptr: *mut MoneroWalletListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback functions called from C++ - these bridge the C++ callbacks to Rust trait methods
|
||||||
|
pub fn money_spent(listener: &mut WalletListenerBox, txid: &CxxString, amount: u64) {
|
||||||
|
listener.on_money_spent(&txid.to_string(), amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn money_received(listener: &mut WalletListenerBox, txid: &CxxString, amount: u64) {
|
||||||
|
listener.on_money_received(&txid.to_string(), amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unconfirmed_money_received(listener: &mut WalletListenerBox, txid: &CxxString, amount: u64) {
|
||||||
|
listener.on_unconfirmed_money_received(&txid.to_string(), amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_block(listener: &mut WalletListenerBox, height: u64) {
|
||||||
|
listener.on_new_block(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn updated(listener: &mut WalletListenerBox) {
|
||||||
|
listener.on_updated();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refreshed(listener: &mut WalletListenerBox) {
|
||||||
|
listener.on_refreshed();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_reorg(
|
||||||
|
listener: &mut WalletListenerBox,
|
||||||
|
height: u64,
|
||||||
|
blocks_detached: u64,
|
||||||
|
transfers_detached: usize,
|
||||||
|
) {
|
||||||
|
listener.on_reorg(height, blocks_detached, transfers_detached);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pool_tx_removed(listener: &mut WalletListenerBox, txid: &CxxString) {
|
||||||
|
listener.on_pool_tx_removed(&txid.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for wallet event listeners - allows custom callback implementations
|
||||||
|
pub trait WalletEventListener: Send + Sync {
|
||||||
|
fn on_money_spent(&self, txid: &str, amount: u64);
|
||||||
|
fn on_money_received(&self, txid: &str, amount: u64);
|
||||||
|
fn on_unconfirmed_money_received(&self, txid: &str, amount: u64);
|
||||||
|
fn on_new_block(&self, height: u64);
|
||||||
|
fn on_updated(&self);
|
||||||
|
fn on_refreshed(&self);
|
||||||
|
fn on_reorg(&self, height: u64, blocks_detached: u64, transfers_detached: usize);
|
||||||
|
fn on_pool_tx_removed(&self, txid: &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around Box<dyn WalletEventListener> because CXX doesn't support trait objects (yet).
|
||||||
|
pub struct WalletListenerBox {
|
||||||
|
inner: Box<dyn WalletEventListener>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletListenerBox {
|
||||||
|
/// Create a new wrapper around any WalletEventListener implementation
|
||||||
|
pub fn new(listener: Box<dyn WalletEventListener>) -> Self {
|
||||||
|
WalletListenerBox { inner: listener }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new boxed wrapper around any WalletEventListener implementation
|
||||||
|
pub fn new_boxed(listener: Box<dyn WalletEventListener>) -> Box<Self> {
|
||||||
|
Box::new(Self::new(listener))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletEventListener for WalletListenerBox {
|
||||||
|
fn on_money_spent(&self, txid: &str, amount: u64) {
|
||||||
|
self.inner.on_money_spent(txid, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_money_received(&self, txid: &str, amount: u64) {
|
||||||
|
self.inner.on_money_received(txid, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_unconfirmed_money_received(&self, txid: &str, amount: u64) {
|
||||||
|
self.inner.on_unconfirmed_money_received(txid, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_new_block(&self, height: u64) {
|
||||||
|
self.inner.on_new_block(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_updated(&self) {
|
||||||
|
self.inner.on_updated();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_refreshed(&self) {
|
||||||
|
self.inner.on_refreshed();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_reorg(&self, height: u64, blocks_detached: u64, transfers_detached: usize) {
|
||||||
|
self.inner
|
||||||
|
.on_reorg(height, blocks_detached, transfers_detached);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_pool_tx_removed(&self, txid: &str) {
|
||||||
|
self.inner.on_pool_tx_removed(txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listener implementation that logs all wallet events using tracing with filename context.
|
||||||
|
pub struct TraceListener {
|
||||||
|
/// The wallet filename for logging context
|
||||||
|
pub filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TraceListener {
|
||||||
|
/// Creates a new TraceListener with a filename for logging context.
|
||||||
|
pub fn new(filename: String) -> Self {
|
||||||
|
TraceListener { filename }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WalletEventListener for TraceListener {
|
||||||
|
fn on_money_spent(&self, txid: &str, amount: u64) {
|
||||||
|
tracing::info!(
|
||||||
|
wallet = self.filename,
|
||||||
|
"Money spent: {} XMR in transaction {}",
|
||||||
|
amount as f64 / 1e12,
|
||||||
|
txid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_money_received(&self, txid: &str, amount: u64) {
|
||||||
|
tracing::info!(
|
||||||
|
wallet = self.filename,
|
||||||
|
"Money received: {} XMR in transaction {}",
|
||||||
|
amount as f64 / 1e12,
|
||||||
|
txid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_unconfirmed_money_received(&self, txid: &str, amount: u64) {
|
||||||
|
tracing::info!(
|
||||||
|
wallet = self.filename,
|
||||||
|
"Unconfirmed money received: {} XMR in transaction {}",
|
||||||
|
amount as f64 / 1e12,
|
||||||
|
txid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_new_block(&self, _height: u64) {}
|
||||||
|
|
||||||
|
fn on_updated(&self) {}
|
||||||
|
|
||||||
|
fn on_refreshed(&self) {}
|
||||||
|
|
||||||
|
fn on_reorg(&self, height: u64, blocks_detached: u64, transfers_detached: usize) {
|
||||||
|
tracing::warn!(
|
||||||
|
wallet = self.filename,
|
||||||
|
"Blockchain reorganization at height {}: {} blocks detached, {} transfers detached",
|
||||||
|
height,
|
||||||
|
blocks_detached,
|
||||||
|
transfers_detached
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_pool_tx_removed(&self, _txid: &str) {}
|
||||||
|
}
|
||||||
|
|
||||||
/// This is the actual rust function that forwards the c++ log messages to tracing.
|
/// This is the actual rust function that forwards the c++ log messages to tracing.
|
||||||
/// It is called every time C++ issues a log message.
|
/// It is called every time C++ issues a log message.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
86
monero-sys/src/database.rs
Normal file
86
monero-sys/src/database.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Database {
|
||||||
|
pub pool: SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RecentWallet {
|
||||||
|
pub wallet_path: String,
|
||||||
|
pub last_opened_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn new(data_dir: PathBuf) -> Result<Self> {
|
||||||
|
if !data_dir.exists() {
|
||||||
|
std::fs::create_dir_all(&data_dir)?;
|
||||||
|
info!("Created wallet database directory: {}", data_dir.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let db_path = data_dir.join("recent_wallets.db");
|
||||||
|
let database_url = format!("sqlite:{}?mode=rwc", db_path.display());
|
||||||
|
let pool = SqlitePool::connect(&database_url).await?;
|
||||||
|
|
||||||
|
let db = Self { pool };
|
||||||
|
db.migrate().await?;
|
||||||
|
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn migrate(&self) -> Result<()> {
|
||||||
|
sqlx::migrate!("./migrations").run(&self.pool).await?;
|
||||||
|
info!("Recent wallets database migration completed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record that a wallet was accessed
|
||||||
|
pub async fn record_wallet_access(&self, wallet_path: &str) -> Result<()> {
|
||||||
|
let now = Utc::now().to_rfc3339();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO recent_wallets (wallet_path, last_opened_at)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT(wallet_path) DO UPDATE SET last_opened_at = excluded.last_opened_at
|
||||||
|
"#,
|
||||||
|
wallet_path,
|
||||||
|
now
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get recently opened wallets, most recent first
|
||||||
|
pub async fn get_recent_wallets(&self, limit: i64) -> Result<Vec<RecentWallet>> {
|
||||||
|
let rows = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT wallet_path, last_opened_at
|
||||||
|
FROM recent_wallets
|
||||||
|
ORDER BY last_opened_at DESC
|
||||||
|
LIMIT ?
|
||||||
|
"#,
|
||||||
|
limit
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let wallets: Vec<RecentWallet> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| RecentWallet {
|
||||||
|
wallet_path: row.wallet_path,
|
||||||
|
last_opened_at: row.last_opened_at.parse().unwrap_or_else(|_| Utc::now()),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(wallets)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -33,50 +33,68 @@ async fn test_sign_message() {
|
||||||
|
|
||||||
// Test message to sign
|
// Test message to sign
|
||||||
let test_message = "Hello, World! This is a test message for signing.";
|
let test_message = "Hello, World! This is a test message for signing.";
|
||||||
|
|
||||||
tracing::info!("Testing message signing with spend key (default address)");
|
tracing::info!("Testing message signing with spend key (default address)");
|
||||||
let signature_spend = wallet
|
let signature_spend = wallet
|
||||||
.sign_message(test_message, None, false)
|
.sign_message(test_message, None, false)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to sign message with spend key");
|
.expect("Failed to sign message with spend key");
|
||||||
|
|
||||||
tracing::info!("Signature with spend key: {}", signature_spend);
|
tracing::info!("Signature with spend key: {}", signature_spend);
|
||||||
assert!(!signature_spend.is_empty(), "Signature should not be empty");
|
assert!(!signature_spend.is_empty(), "Signature should not be empty");
|
||||||
assert!(signature_spend.len() > 10, "Signature should be reasonably long");
|
assert!(
|
||||||
|
signature_spend.len() > 10,
|
||||||
|
"Signature should be reasonably long"
|
||||||
|
);
|
||||||
|
|
||||||
tracing::info!("Testing message signing with view key (default address)");
|
tracing::info!("Testing message signing with view key (default address)");
|
||||||
let signature_view = wallet
|
let signature_view = wallet
|
||||||
.sign_message(test_message, None, true)
|
.sign_message(test_message, None, true)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to sign message with view key");
|
.expect("Failed to sign message with view key");
|
||||||
|
|
||||||
tracing::info!("Signature with view key: {}", signature_view);
|
tracing::info!("Signature with view key: {}", signature_view);
|
||||||
assert!(!signature_view.is_empty(), "Signature should not be empty");
|
assert!(!signature_view.is_empty(), "Signature should not be empty");
|
||||||
assert!(signature_view.len() > 10, "Signature should be reasonably long");
|
assert!(
|
||||||
|
signature_view.len() > 10,
|
||||||
|
"Signature should be reasonably long"
|
||||||
|
);
|
||||||
|
|
||||||
// Signatures should be different when using different keys
|
// Signatures should be different when using different keys
|
||||||
assert_ne!(signature_spend, signature_view, "Spend key and view key signatures should be different");
|
assert_ne!(
|
||||||
|
signature_spend, signature_view,
|
||||||
|
"Spend key and view key signatures should be different"
|
||||||
|
);
|
||||||
|
|
||||||
tracing::info!("Testing message signing with spend key (explicit address)");
|
tracing::info!("Testing message signing with spend key (explicit address)");
|
||||||
let signature_explicit = wallet
|
let signature_explicit = wallet
|
||||||
.sign_message(test_message, Some(&main_address.to_string()), false)
|
.sign_message(test_message, Some(&main_address.to_string()), false)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to sign message with explicit address");
|
.expect("Failed to sign message with explicit address");
|
||||||
|
|
||||||
tracing::info!("Signature with explicit address: {}", signature_explicit);
|
tracing::info!("Signature with explicit address: {}", signature_explicit);
|
||||||
assert!(!signature_explicit.is_empty(), "Signature should not be empty");
|
assert!(
|
||||||
|
!signature_explicit.is_empty(),
|
||||||
|
"Signature should not be empty"
|
||||||
|
);
|
||||||
|
|
||||||
// When using the same key and same address (main address), signatures should be the same
|
// When using the same key and same address (main address), signatures should be the same
|
||||||
assert_eq!(signature_spend, signature_explicit, "Signatures should be the same when using same key and address");
|
assert_eq!(
|
||||||
|
signature_spend, signature_explicit,
|
||||||
|
"Signatures should be the same when using same key and address"
|
||||||
|
);
|
||||||
|
|
||||||
tracing::info!("Testing empty message signing");
|
tracing::info!("Testing empty message signing");
|
||||||
let signature_empty = wallet
|
let signature_empty = wallet
|
||||||
.sign_message("", None, false)
|
.sign_message("", None, false)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to sign empty message");
|
.expect("Failed to sign empty message");
|
||||||
|
|
||||||
tracing::info!("Signature for empty message: {}", signature_empty);
|
tracing::info!("Signature for empty message: {}", signature_empty);
|
||||||
assert!(!signature_empty.is_empty(), "Signature should not be empty even for empty message");
|
assert!(
|
||||||
|
!signature_empty.is_empty(),
|
||||||
|
"Signature should not be empty even for empty message"
|
||||||
|
);
|
||||||
|
|
||||||
tracing::info!("All message signing tests passed!");
|
tracing::info!("All message signing tests passed!");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
948
src-gui/package-lock.json
generated
948
src-gui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,9 +4,9 @@
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check-bindings": "typeshare --lang=typescript --output-file __temp_bindings.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src && dprint fmt __temp_bindings.ts && diff -wbB __temp_bindings.ts ./src/models/tauriModel.ts && rm __temp_bindings.ts",
|
"check-bindings": "typeshare --lang=typescript --output-file __temp_bindings.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src ../monero-sys/src && dprint fmt __temp_bindings.ts && diff -wbB __temp_bindings.ts ./src/models/tauriModel.ts && rm __temp_bindings.ts",
|
||||||
"gen-bindings-verbose": "RUST_LOG=debug RUST_BACKTRACE=1 typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src && dprint fmt ./src/models/tauriModel.ts",
|
"gen-bindings-verbose": "RUST_LOG=debug RUST_BACKTRACE=1 typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src ../monero-sys/src && dprint fmt ./src/models/tauriModel.ts",
|
||||||
"gen-bindings": "typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src && dprint fmt ./src/models/tauriModel.ts",
|
"gen-bindings": "typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src ../electrum-pool/src ../monero-sys/src && dprint fmt ./src/models/tauriModel.ts",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
@ -22,10 +22,12 @@
|
||||||
"@mui/icons-material": "^7.1.1",
|
"@mui/icons-material": "^7.1.1",
|
||||||
"@mui/lab": "^7.0.0-beta.13",
|
"@mui/lab": "^7.0.0-beta.13",
|
||||||
"@mui/material": "^7.1.1",
|
"@mui/material": "^7.1.1",
|
||||||
|
"@mui/x-date-pickers": "^8.8.0",
|
||||||
"@reduxjs/toolkit": "^2.3.0",
|
"@reduxjs/toolkit": "^2.3.0",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
"@tauri-apps/plugin-cli": "^2.0.0",
|
"@tauri-apps/plugin-cli": "^2.0.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||||
"@tauri-apps/plugin-opener": "^2.0.0",
|
"@tauri-apps/plugin-opener": "^2.0.0",
|
||||||
"@tauri-apps/plugin-process": "^2.0.0",
|
"@tauri-apps/plugin-process": "^2.0.0",
|
||||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||||
|
|
@ -33,6 +35,7 @@
|
||||||
"@tauri-apps/plugin-updater": "2.7.1",
|
"@tauri-apps/plugin-updater": "2.7.1",
|
||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
"boring-avatars": "^1.11.2",
|
"boring-avatars": "^1.11.2",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"humanize-duration": "^3.32.1",
|
"humanize-duration": "^3.32.1",
|
||||||
"jdenticon": "^3.3.0",
|
"jdenticon": "^3.3.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
SelectMakerDetails,
|
SelectMakerDetails,
|
||||||
TauriBackgroundProgress,
|
TauriBackgroundProgress,
|
||||||
TauriSwapProgressEvent,
|
TauriSwapProgressEvent,
|
||||||
|
SendMoneroDetails,
|
||||||
} from "./tauriModel";
|
} from "./tauriModel";
|
||||||
|
|
||||||
export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"];
|
export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"];
|
||||||
|
|
@ -310,10 +311,13 @@ export type PendingSelectMakerApprovalRequest = PendingApprovalRequest & {
|
||||||
request: { type: "SelectMaker"; content: SelectMakerDetails };
|
request: { type: "SelectMaker"; content: SelectMakerDetails };
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface SortableQuoteWithAddress extends QuoteWithAddress {
|
export type PendingSendMoneroApprovalRequest = PendingApprovalRequest & {
|
||||||
expiration_ts?: number;
|
request: { type: "SendMonero"; content: SendMoneroDetails };
|
||||||
request_id?: string;
|
};
|
||||||
}
|
|
||||||
|
export type PendingPasswordApprovalRequest = PendingApprovalRequest & {
|
||||||
|
request: { type: "PasswordRequest"; content: { wallet_path: string } };
|
||||||
|
};
|
||||||
|
|
||||||
export function isPendingSelectMakerApprovalEvent(
|
export function isPendingSelectMakerApprovalEvent(
|
||||||
event: ApprovalRequest,
|
event: ApprovalRequest,
|
||||||
|
|
@ -327,6 +331,30 @@ export function isPendingSelectMakerApprovalEvent(
|
||||||
return event.request.type === "SelectMaker";
|
return event.request.type === "SelectMaker";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPendingSendMoneroApprovalEvent(
|
||||||
|
event: ApprovalRequest,
|
||||||
|
): event is PendingSendMoneroApprovalRequest {
|
||||||
|
// Check if the request is pending
|
||||||
|
if (event.request_status.state !== "Pending") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the request is a SendMonero request
|
||||||
|
return event.request.type === "SendMonero";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPendingPasswordApprovalEvent(
|
||||||
|
event: ApprovalRequest,
|
||||||
|
): event is PendingPasswordApprovalRequest {
|
||||||
|
// Check if the request is pending
|
||||||
|
if (event.request_status.state !== "Pending") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the request is a PasswordRequest request
|
||||||
|
return event.request.type === "PasswordRequest";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if any funds have been locked yet based on the swap progress event
|
* Checks if any funds have been locked yet based on the swap progress event
|
||||||
* Returns true for events where funds have been locked
|
* Returns true for events where funds have been locked
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,16 @@ import {
|
||||||
listSellersAtRendezvousPoint,
|
listSellersAtRendezvousPoint,
|
||||||
refreshApprovals,
|
refreshApprovals,
|
||||||
updateAllNodeStatuses,
|
updateAllNodeStatuses,
|
||||||
|
fetchAndUpdateBackgroundItems,
|
||||||
|
fetchAndUpdateApprovalItems,
|
||||||
} from "./rpc";
|
} from "./rpc";
|
||||||
import { store } from "./store/storeRenderer";
|
import { store } from "./store/storeRenderer";
|
||||||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||||
|
import {
|
||||||
|
setBalance,
|
||||||
|
setHistory,
|
||||||
|
setSyncProgress,
|
||||||
|
} from "store/features/walletSlice";
|
||||||
|
|
||||||
const TAURI_UNIFIED_EVENT_CHANNEL_NAME = "tauri-unified-event";
|
const TAURI_UNIFIED_EVENT_CHANNEL_NAME = "tauri-unified-event";
|
||||||
|
|
||||||
|
|
@ -45,7 +52,7 @@ const UPDATE_RATE_INTERVAL = 5 * 60 * 1_000;
|
||||||
// Fetch all conversations every 10 minutes
|
// Fetch all conversations every 10 minutes
|
||||||
const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
|
const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
|
||||||
|
|
||||||
// Fetch pending approvals every 10 seconds
|
// Fetch pending approvals every 2 seconds
|
||||||
const FETCH_PENDING_APPROVALS_INTERVAL = 2 * 1_000;
|
const FETCH_PENDING_APPROVALS_INTERVAL = 2 * 1_000;
|
||||||
|
|
||||||
function setIntervalImmediate(callback: () => void, interval: number): void {
|
function setIntervalImmediate(callback: () => void, interval: number): void {
|
||||||
|
|
@ -137,6 +144,19 @@ export async function setupBackgroundTasks(): Promise<void> {
|
||||||
store.dispatch(poolStatusReceived(eventData));
|
store.dispatch(poolStatusReceived(eventData));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "MoneroWalletUpdate":
|
||||||
|
console.log("MoneroWalletUpdate", eventData);
|
||||||
|
if (eventData.type === "BalanceChange") {
|
||||||
|
store.dispatch(setBalance(eventData.content));
|
||||||
|
}
|
||||||
|
if (eventData.type === "HistoryUpdate") {
|
||||||
|
store.dispatch(setHistory(eventData.content));
|
||||||
|
}
|
||||||
|
if (eventData.type === "SyncProgress") {
|
||||||
|
store.dispatch(setSyncProgress(eventData.content));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
exhaustiveGuard(channelName);
|
exhaustiveGuard(channelName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@ import { setupBackgroundTasks } from "renderer/background";
|
||||||
import "@fontsource/roboto";
|
import "@fontsource/roboto";
|
||||||
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
import FeedbackPage from "./pages/feedback/FeedbackPage";
|
||||||
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
import IntroductionModal from "./modal/introduction/IntroductionModal";
|
||||||
|
import MoneroWalletPage from "./pages/monero/MoneroWalletPage";
|
||||||
import SeedSelectionDialog from "./modal/seed-selection/SeedSelectionDialog";
|
import SeedSelectionDialog from "./modal/seed-selection/SeedSelectionDialog";
|
||||||
|
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||||
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
|
import PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog";
|
||||||
|
|
||||||
declare module "@mui/material/styles" {
|
declare module "@mui/material/styles" {
|
||||||
interface Theme {
|
interface Theme {
|
||||||
|
|
@ -44,16 +48,19 @@ export default function App() {
|
||||||
return (
|
return (
|
||||||
<StyledEngineProvider injectFirst>
|
<StyledEngineProvider injectFirst>
|
||||||
<ThemeProvider theme={currentTheme}>
|
<ThemeProvider theme={currentTheme}>
|
||||||
<CssBaseline />
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
<GlobalSnackbarProvider>
|
<CssBaseline />
|
||||||
<IntroductionModal />
|
<GlobalSnackbarProvider>
|
||||||
<SeedSelectionDialog />
|
<IntroductionModal />
|
||||||
<Router>
|
<SeedSelectionDialog />
|
||||||
<Navigation />
|
<PasswordEntryDialog />
|
||||||
<InnerContent />
|
<Router>
|
||||||
<UpdaterDialog />
|
<Navigation />
|
||||||
</Router>
|
<InnerContent />
|
||||||
</GlobalSnackbarProvider>
|
<UpdaterDialog />
|
||||||
|
</Router>
|
||||||
|
</GlobalSnackbarProvider>
|
||||||
|
</LocalizationProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StyledEngineProvider>
|
</StyledEngineProvider>
|
||||||
);
|
);
|
||||||
|
|
@ -70,12 +77,13 @@ function InnerContent() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/" element={<MoneroWalletPage />} />
|
||||||
|
<Route path="/monero-wallet" element={<MoneroWalletPage />} />
|
||||||
<Route path="/swap" element={<SwapPage />} />
|
<Route path="/swap" element={<SwapPage />} />
|
||||||
<Route path="/history" element={<HistoryPage />} />
|
<Route path="/history" element={<HistoryPage />} />
|
||||||
<Route path="/wallet" element={<WalletPage />} />
|
<Route path="/bitcoin-wallet" element={<WalletPage />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
<Route path="/feedback" element={<FeedbackPage />} />
|
<Route path="/feedback" element={<FeedbackPage />} />
|
||||||
<Route path="/" element={<SwapPage />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -186,18 +186,11 @@ export default function DaemonStatusAlert() {
|
||||||
const contextStatus = useAppSelector((s) => s.rpc.status);
|
const contextStatus = useAppSelector((s) => s.rpc.status);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (
|
|
||||||
contextStatus === null ||
|
|
||||||
contextStatus === TauriContextStatusEvent.NotInitialized
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<LoadingSpinnerAlert severity="warning">
|
|
||||||
Checking for available remote nodes
|
|
||||||
</LoadingSpinnerAlert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (contextStatus) {
|
switch (contextStatus) {
|
||||||
|
case null:
|
||||||
|
return null;
|
||||||
|
case TauriContextStatusEvent.NotInitialized:
|
||||||
|
return null;
|
||||||
case TauriContextStatusEvent.Initializing:
|
case TauriContextStatusEvent.Initializing:
|
||||||
return null;
|
return null;
|
||||||
case TauriContextStatusEvent.Available:
|
case TauriContextStatusEvent.Available:
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export default function FundsLeftInWalletAlert() {
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => navigate("/wallet")}
|
onClick={() => navigate("/bitcoin-wallet")}
|
||||||
>
|
>
|
||||||
View
|
View
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from "react";
|
||||||
import { Box, Alert, AlertTitle } from "@mui/material";
|
import { Box, Alert, AlertTitle } from "@mui/material";
|
||||||
import {
|
import {
|
||||||
BobStateName,
|
BobStateName,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ type MoneroAddressTextFieldProps = TextFieldProps & {
|
||||||
address: string;
|
address: string;
|
||||||
onAddressChange: (address: string) => void;
|
onAddressChange: (address: string) => void;
|
||||||
onAddressValidityChange: (valid: boolean) => void;
|
onAddressValidityChange: (valid: boolean) => void;
|
||||||
helperText: string;
|
helperText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MoneroAddressTextField({
|
export default function MoneroAddressTextField({
|
||||||
|
|
|
||||||
178
src-gui/src/renderer/components/inputs/NumberInput.tsx
Normal file
178
src-gui/src/renderer/components/inputs/NumberInput.tsx
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { darken, useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
interface NumberInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
fontWeight?: number;
|
||||||
|
textAlign?: "left" | "center" | "right";
|
||||||
|
minWidth?: number;
|
||||||
|
step?: number;
|
||||||
|
largeStep?: number;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NumberInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "0.00",
|
||||||
|
fontSize = "2em",
|
||||||
|
fontWeight = 600,
|
||||||
|
textAlign = "right",
|
||||||
|
minWidth = 60,
|
||||||
|
step = 0.001,
|
||||||
|
largeStep = 0.1,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
}: NumberInputProps) {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [inputWidth, setInputWidth] = useState(minWidth);
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
const measureRef = useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
|
// Calculate precision from step value
|
||||||
|
const getDecimalPrecision = (num: number): number => {
|
||||||
|
const str = num.toString();
|
||||||
|
if (str.includes(".")) {
|
||||||
|
return str.split(".")[1].length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [userPrecision, setUserPrecision] = useState(() =>
|
||||||
|
getDecimalPrecision(step),
|
||||||
|
); // Track user's decimal precision
|
||||||
|
const [minPrecision, setMinPrecision] = useState(3);
|
||||||
|
const appliedPrecision =
|
||||||
|
userPrecision > minPrecision ? userPrecision : minPrecision;
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
// Initialize with placeholder if no value provided
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
(!value || value.trim() === "" || parseFloat(value) === 0) &&
|
||||||
|
!isFocused
|
||||||
|
) {
|
||||||
|
onChange(placeholder);
|
||||||
|
}
|
||||||
|
}, [placeholder, isFocused, value, onChange]);
|
||||||
|
|
||||||
|
// Update precision when step changes
|
||||||
|
useEffect(() => {
|
||||||
|
setUserPrecision(getDecimalPrecision(step));
|
||||||
|
setMinPrecision(getDecimalPrecision(step));
|
||||||
|
}, [step]);
|
||||||
|
|
||||||
|
// Measure text width to size input dynamically
|
||||||
|
useEffect(() => {
|
||||||
|
if (measureRef.current) {
|
||||||
|
const text = value;
|
||||||
|
measureRef.current.textContent = text;
|
||||||
|
const textWidth = measureRef.current.offsetWidth;
|
||||||
|
setInputWidth(Math.max(textWidth + 5, minWidth)); // Add padding and minimum width
|
||||||
|
}
|
||||||
|
}, [value, placeholder, minWidth]);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
const currentValue = parseFloat(value) || 0;
|
||||||
|
const increment = e.shiftKey ? largeStep : step;
|
||||||
|
const newValue =
|
||||||
|
e.key === "ArrowUp"
|
||||||
|
? currentValue + increment
|
||||||
|
: Math.max(0, currentValue - increment);
|
||||||
|
// Use the user's precision for formatting
|
||||||
|
onChange(newValue.toFixed(appliedPrecision));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const inputValue = e.target.value;
|
||||||
|
// Allow empty string, numbers, and decimal points
|
||||||
|
if (inputValue === "" || /^\d*\.?\d*$/.test(inputValue)) {
|
||||||
|
onChange(inputValue);
|
||||||
|
|
||||||
|
// Track the user's decimal precision
|
||||||
|
if (inputValue.includes(".")) {
|
||||||
|
const decimalPart = inputValue.split(".")[1];
|
||||||
|
setUserPrecision(decimalPart ? decimalPart.length : 0);
|
||||||
|
} else if (inputValue && !isNaN(parseFloat(inputValue))) {
|
||||||
|
// No decimal point, so precision is 0
|
||||||
|
setUserPrecision(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
setIsFocused(false);
|
||||||
|
// Reset to placeholder if value is zero or empty
|
||||||
|
if (!value || value.trim() === "" || parseFloat(value) === 0) {
|
||||||
|
onChange(placeholder);
|
||||||
|
} else if (!isNaN(parseFloat(value))) {
|
||||||
|
// Format valid numbers on blur using user's precision
|
||||||
|
const formatted = parseFloat(value).toFixed(appliedPrecision);
|
||||||
|
// Remove trailing zeros if precision allows
|
||||||
|
onChange(parseFloat(formatted).toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
setIsFocused(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine if we should show placeholder styling
|
||||||
|
const isShowingPlaceholder = value === placeholder && !isFocused;
|
||||||
|
|
||||||
|
const defaultStyle: React.CSSProperties = {
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
textAlign,
|
||||||
|
border: "none",
|
||||||
|
outline: "none",
|
||||||
|
background: "transparent",
|
||||||
|
width: `${inputWidth}px`,
|
||||||
|
minWidth: `${minWidth}px`,
|
||||||
|
fontFamily: "inherit",
|
||||||
|
color: isShowingPlaceholder
|
||||||
|
? darken(theme.palette.text.primary, 0.5)
|
||||||
|
: theme.palette.text.primary,
|
||||||
|
padding: "4px 0",
|
||||||
|
transition: "color 0.2s ease",
|
||||||
|
...style,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", display: "inline-block" }}>
|
||||||
|
{/* Hidden span for measuring text width */}
|
||||||
|
<span
|
||||||
|
ref={measureRef}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
visibility: "hidden",
|
||||||
|
fontSize,
|
||||||
|
fontWeight,
|
||||||
|
fontFamily: "inherit",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
inputMode="decimal"
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
className={className}
|
||||||
|
style={defaultStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Visibility, VisibilityOff } from "@mui/icons-material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { usePendingPasswordApproval } from "store/hooks";
|
||||||
|
import { rejectApproval, resolveApproval } from "renderer/rpc";
|
||||||
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
|
||||||
|
export default function PasswordEntryDialog() {
|
||||||
|
const pendingApprovals = usePendingPasswordApproval();
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const approval = pendingApprovals[0];
|
||||||
|
|
||||||
|
const accept = async () => {
|
||||||
|
if (!approval) {
|
||||||
|
throw new Error("No approval request found for password entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await resolveApproval<string>(approval.request_id, password);
|
||||||
|
setPassword("");
|
||||||
|
setError("");
|
||||||
|
} catch (err) {
|
||||||
|
setError("Invalid password. Please try again.");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reject = async () => {
|
||||||
|
if (!approval) {
|
||||||
|
throw new Error("No approval request found for password entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rejectApproval<string>(approval.request_id, "");
|
||||||
|
setPassword("");
|
||||||
|
setError("");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error rejecting password request:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTogglePasswordVisibility = () => {
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!approval) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={true}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
BackdropProps={{
|
||||||
|
sx: {
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Enter Wallet Password</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
label="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPassword(e.target.value);
|
||||||
|
if (error) setError("");
|
||||||
|
}}
|
||||||
|
error={!!error}
|
||||||
|
helperText={error}
|
||||||
|
autoFocus
|
||||||
|
margin="normal"
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
accept();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
onClick={handleTogglePasswordVisibility}
|
||||||
|
edge="end"
|
||||||
|
aria-label="toggle password visibility"
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={reject} variant="outlined">
|
||||||
|
Change wallet
|
||||||
|
</Button>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
onInvoke={accept}
|
||||||
|
variant="contained"
|
||||||
|
requiresContext={false}
|
||||||
|
>
|
||||||
|
Unlock
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -10,17 +9,43 @@ import {
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemText,
|
||||||
|
Divider,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { usePendingSeedSelectionApproval } from "store/hooks";
|
import { usePendingSeedSelectionApproval } from "store/hooks";
|
||||||
import { resolveApproval, checkSeed } from "renderer/rpc";
|
import { resolveApproval, checkSeed } from "renderer/rpc";
|
||||||
|
import { SeedChoice } from "models/tauriModel";
|
||||||
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
|
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
|
||||||
export default function SeedSelectionDialog() {
|
export default function SeedSelectionDialog() {
|
||||||
const pendingApprovals = usePendingSeedSelectionApproval();
|
const pendingApprovals = usePendingSeedSelectionApproval();
|
||||||
const [selectedOption, setSelectedOption] = useState<string>("RandomSeed");
|
const [selectedOption, setSelectedOption] = useState<
|
||||||
|
SeedChoice["type"] | undefined
|
||||||
|
>("RandomSeed");
|
||||||
const [customSeed, setCustomSeed] = useState<string>("");
|
const [customSeed, setCustomSeed] = useState<string>("");
|
||||||
const [isSeedValid, setIsSeedValid] = useState<boolean>(false);
|
const [isSeedValid, setIsSeedValid] = useState<boolean>(false);
|
||||||
const approval = pendingApprovals[0]; // Handle the first pending approval
|
const [walletPath, setWalletPath] = useState<string>("");
|
||||||
|
|
||||||
|
const approval = pendingApprovals[0];
|
||||||
|
|
||||||
|
// Extract recent wallets from the approval request content
|
||||||
|
const recentWallets =
|
||||||
|
approval?.request?.type === "SeedSelection"
|
||||||
|
? approval.request.content.recent_wallets
|
||||||
|
: [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedOption === "FromSeed" && customSeed.trim()) {
|
if (selectedOption === "FromSeed" && customSeed.trim()) {
|
||||||
|
|
@ -36,51 +61,193 @@ export default function SeedSelectionDialog() {
|
||||||
}
|
}
|
||||||
}, [customSeed, selectedOption]);
|
}, [customSeed, selectedOption]);
|
||||||
|
|
||||||
const handleClose = async (accept: boolean) => {
|
// Auto-select the first recent wallet if available
|
||||||
if (!approval) return;
|
useEffect(() => {
|
||||||
|
if (recentWallets.length > 0) {
|
||||||
if (accept) {
|
setSelectedOption("FromWalletPath");
|
||||||
const seedChoice =
|
setWalletPath(recentWallets[0]);
|
||||||
selectedOption === "RandomSeed"
|
|
||||||
? { type: "RandomSeed" }
|
|
||||||
: { type: "FromSeed", content: { seed: customSeed } };
|
|
||||||
|
|
||||||
await resolveApproval(approval.request_id, seedChoice);
|
|
||||||
} else {
|
|
||||||
// On reject, just close without approval
|
|
||||||
await resolveApproval(approval.request_id, { type: "RandomSeed" });
|
|
||||||
}
|
}
|
||||||
|
}, [recentWallets.length]);
|
||||||
|
|
||||||
|
const selectWalletFile = async () => {
|
||||||
|
const selected = await open({
|
||||||
|
multiple: false,
|
||||||
|
directory: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
setWalletPath(selected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Legacy = async () => {
|
||||||
|
if (!approval)
|
||||||
|
throw new Error("No approval request found for seed selection");
|
||||||
|
|
||||||
|
await resolveApproval<SeedChoice>(approval.request_id, {
|
||||||
|
type: "Legacy",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const accept = async () => {
|
||||||
|
if (!approval)
|
||||||
|
throw new Error("No approval request found for seed selection");
|
||||||
|
|
||||||
|
const seedChoice: SeedChoice =
|
||||||
|
selectedOption === "RandomSeed"
|
||||||
|
? { type: "RandomSeed" }
|
||||||
|
: selectedOption === "FromSeed"
|
||||||
|
? { type: "FromSeed", content: { seed: customSeed } }
|
||||||
|
: { type: "FromWalletPath", content: { wallet_path: walletPath } };
|
||||||
|
|
||||||
|
await resolveApproval<SeedChoice>(approval.request_id, seedChoice);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// Disable the button if the user is restoring from a seed and the seed is invalid
|
||||||
<Dialog open={true} maxWidth="sm" fullWidth>
|
// or if selecting wallet path and no path is selected
|
||||||
<DialogTitle>Monero Wallet</DialogTitle>
|
const isDisabled =
|
||||||
<DialogContent>
|
selectedOption === "FromSeed"
|
||||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
? customSeed.trim().length === 0 || !isSeedValid
|
||||||
Choose what seed to use for the wallet.
|
: selectedOption === "FromWalletPath"
|
||||||
</Typography>
|
? !walletPath
|
||||||
|
: false;
|
||||||
|
|
||||||
<FormControl component="fieldset">
|
return (
|
||||||
<RadioGroup
|
<Dialog
|
||||||
value={selectedOption}
|
open={true}
|
||||||
onChange={(e) => setSelectedOption(e.target.value)}
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
sx={{ "& .MuiDialog-paper": { minHeight: "min(32rem, 80vh)" } }}
|
||||||
|
BackdropProps={{
|
||||||
|
sx: {
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}>
|
||||||
|
{/* Open existing wallet option */}
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
border: selectedOption === "FromWalletPath" ? 2 : 1,
|
||||||
|
borderColor:
|
||||||
|
selectedOption === "FromWalletPath"
|
||||||
|
? "primary.main"
|
||||||
|
: "divider",
|
||||||
|
"&:hover": { borderColor: "primary.main" },
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => setSelectedOption("FromWalletPath")}
|
||||||
>
|
>
|
||||||
<FormControlLabel
|
<CardContent
|
||||||
value="RandomSeed"
|
sx={{
|
||||||
control={<Radio />}
|
display: "flex",
|
||||||
label="Create a new wallet"
|
flexDirection: "column",
|
||||||
/>
|
alignItems: "center",
|
||||||
<FormControlLabel
|
justifyContent: "center",
|
||||||
value="FromSeed"
|
gap: 1,
|
||||||
control={<Radio />}
|
}}
|
||||||
label="Restore wallet from seed"
|
>
|
||||||
/>
|
<FolderOpenIcon sx={{ fontSize: 32, color: "text.secondary" }} />
|
||||||
</RadioGroup>
|
<Typography
|
||||||
</FormControl>
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ textAlign: "center" }}
|
||||||
|
>
|
||||||
|
Open wallet file
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Create new wallet option */}
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
border: selectedOption === "RandomSeed" ? 2 : 1,
|
||||||
|
borderColor:
|
||||||
|
selectedOption === "RandomSeed" ? "primary.main" : "divider",
|
||||||
|
"&:hover": { borderColor: "primary.main" },
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => setSelectedOption("RandomSeed")}
|
||||||
|
>
|
||||||
|
<CardContent
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddIcon sx={{ fontSize: 32, color: "text.secondary" }} />
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ textAlign: "center" }}
|
||||||
|
>
|
||||||
|
Create new wallet
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Restore from seed option */}
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
border: selectedOption === "FromSeed" ? 2 : 1,
|
||||||
|
borderColor:
|
||||||
|
selectedOption === "FromSeed" ? "primary.main" : "divider",
|
||||||
|
"&:hover": { borderColor: "primary.main" },
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
onClick={() => setSelectedOption("FromSeed")}
|
||||||
|
>
|
||||||
|
<CardContent
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshIcon sx={{ fontSize: 32, color: "text.secondary" }} />
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ textAlign: "center" }}
|
||||||
|
>
|
||||||
|
Restore from seed
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{selectedOption === "RandomSeed" && (
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ textAlign: "center" }}
|
||||||
|
>
|
||||||
|
A new wallet with a random seed phrase will be generated.
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ textAlign: "center" }}
|
||||||
|
>
|
||||||
|
You will have the option to back it up later.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedOption === "FromSeed" && (
|
{selectedOption === "FromSeed" && (
|
||||||
<TextField
|
<TextField
|
||||||
|
|
@ -90,7 +257,6 @@ export default function SeedSelectionDialog() {
|
||||||
label="Enter your seed phrase"
|
label="Enter your seed phrase"
|
||||||
value={customSeed}
|
value={customSeed}
|
||||||
onChange={(e) => setCustomSeed(e.target.value)}
|
onChange={(e) => setCustomSeed(e.target.value)}
|
||||||
sx={{ mt: 2 }}
|
|
||||||
placeholder="Enter your Monero 25 words seed phrase..."
|
placeholder="Enter your Monero 25 words seed phrase..."
|
||||||
error={!isSeedValid && customSeed.length > 0}
|
error={!isSeedValid && customSeed.length > 0}
|
||||||
helperText={
|
helperText={
|
||||||
|
|
@ -102,19 +268,115 @@ export default function SeedSelectionDialog() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{selectedOption === "FromWalletPath" && (
|
||||||
|
<Box sx={{ gap: 2, display: "flex", flexDirection: "column" }}>
|
||||||
|
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Wallet file path"
|
||||||
|
value={walletPath || ""}
|
||||||
|
placeholder="Select a wallet file..."
|
||||||
|
InputProps={{
|
||||||
|
readOnly: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={selectWalletFile}
|
||||||
|
sx={{ minWidth: "120px", height: "56px" }}
|
||||||
|
startIcon={<SearchIcon />}
|
||||||
|
>
|
||||||
|
Browse
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
{recentWallets.length > 0 && (
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
border: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
borderRadius: 1,
|
||||||
|
maxHeight: 200,
|
||||||
|
overflowY: "scroll",
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
display: "block !important",
|
||||||
|
width: "8px !important",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-track": {
|
||||||
|
display: "block !important",
|
||||||
|
background: "rgba(255,255,255,.1) !important",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb": {
|
||||||
|
display: "block !important",
|
||||||
|
background: "rgba(255,255,255,.6) !important",
|
||||||
|
borderRadius: "4px",
|
||||||
|
minHeight: "20px !important",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb:hover": {
|
||||||
|
background: "rgba(255,255,255,.8) !important",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-corner": {
|
||||||
|
background: "transparent !important",
|
||||||
|
},
|
||||||
|
scrollbarWidth: "thin",
|
||||||
|
scrollbarColor: "rgba(255,255,255,.6) rgba(255,255,255,.1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List disablePadding>
|
||||||
|
{recentWallets.map((path, index) => (
|
||||||
|
<Box key={index}>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemButton
|
||||||
|
selected={walletPath === path}
|
||||||
|
onClick={() => setWalletPath(path)}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={path.split("/").pop() || path}
|
||||||
|
secondary={path}
|
||||||
|
primaryTypographyProps={{
|
||||||
|
fontWeight: walletPath === path ? 600 : 400,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
}}
|
||||||
|
secondaryTypographyProps={{
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
sx: {
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
{index < recentWallets.length - 1 && <Divider />}
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions sx={{ justifyContent: "space-between" }}>
|
||||||
<Button
|
<PromiseInvokeButton
|
||||||
onClick={() => handleClose(true)}
|
variant="text"
|
||||||
variant="contained"
|
onInvoke={Legacy}
|
||||||
disabled={
|
requiresContext={false}
|
||||||
selectedOption === "FromSeed"
|
color="inherit"
|
||||||
? !customSeed.trim() || !isSeedValid
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Confirm
|
No wallet (Legacy)
|
||||||
</Button>
|
</PromiseInvokeButton>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
onInvoke={accept}
|
||||||
|
variant="contained"
|
||||||
|
disabled={isDisabled}
|
||||||
|
requiresContext={false}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</PromiseInvokeButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { Box, DialogTitle, Typography } from "@mui/material";
|
|
||||||
import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge";
|
|
||||||
import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
|
|
||||||
|
|
||||||
export default function SwapDialogTitle({
|
|
||||||
title,
|
|
||||||
debug,
|
|
||||||
setDebug,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
debug: boolean;
|
|
||||||
setDebug: (d: boolean) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DialogTitle
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6">{title}</Typography>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gridGap: 1 }}>
|
|
||||||
<FeedbackSubmitBadge />
|
|
||||||
<DebugPageSwitchBadge enabled={debug} setEnabled={setDebug} />
|
|
||||||
</Box>
|
|
||||||
</DialogTitle>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -22,6 +22,7 @@ export default function DebugPage() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CliLogsBox
|
<CliLogsBox
|
||||||
|
minHeight="min(20rem, 70vh)"
|
||||||
logs={logs}
|
logs={logs}
|
||||||
label="Logs relevant to the swap (only current session)"
|
label="Logs relevant to the swap (only current session)"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -5,26 +5,35 @@ import SwapHorizOutlinedIcon from "@mui/icons-material/SwapHorizOutlined";
|
||||||
import FeedbackOutlinedIcon from "@mui/icons-material/FeedbackOutlined";
|
import FeedbackOutlinedIcon from "@mui/icons-material/FeedbackOutlined";
|
||||||
import RouteListItemIconButton from "./RouteListItemIconButton";
|
import RouteListItemIconButton from "./RouteListItemIconButton";
|
||||||
import UnfinishedSwapsBadge from "./UnfinishedSwapsCountBadge";
|
import UnfinishedSwapsBadge from "./UnfinishedSwapsCountBadge";
|
||||||
import { useTotalUnreadMessagesCount } from "store/hooks";
|
import { useIsSwapRunning, useTotalUnreadMessagesCount } from "store/hooks";
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
|
import AttachMoneyIcon from "@mui/icons-material/AttachMoney";
|
||||||
|
import BitcoinIcon from "../icons/BitcoinIcon";
|
||||||
|
import MoneroIcon from "../icons/MoneroIcon";
|
||||||
|
|
||||||
export default function NavigationHeader() {
|
export default function NavigationHeader() {
|
||||||
const totalUnreadCount = useTotalUnreadMessagesCount();
|
const totalUnreadCount = useTotalUnreadMessagesCount();
|
||||||
|
const isSwapRunning = useIsSwapRunning();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<List>
|
<List>
|
||||||
<RouteListItemIconButton name="Swap" route="/swap">
|
<RouteListItemIconButton name="Wallet" route={["/monero-wallet", "/"]}>
|
||||||
<SwapHorizOutlinedIcon />
|
<MoneroIcon />
|
||||||
|
</RouteListItemIconButton>
|
||||||
|
<RouteListItemIconButton name="Wallet" route="/bitcoin-wallet">
|
||||||
|
<BitcoinIcon />
|
||||||
|
</RouteListItemIconButton>
|
||||||
|
<RouteListItemIconButton name="Swap" route={["/swap"]}>
|
||||||
|
<Badge invisible={!isSwapRunning} variant="dot" color="primary">
|
||||||
|
<SwapHorizOutlinedIcon />
|
||||||
|
</Badge>
|
||||||
</RouteListItemIconButton>
|
</RouteListItemIconButton>
|
||||||
<RouteListItemIconButton name="History" route="/history">
|
<RouteListItemIconButton name="History" route="/history">
|
||||||
<UnfinishedSwapsBadge>
|
<UnfinishedSwapsBadge>
|
||||||
<HistoryOutlinedIcon />
|
<HistoryOutlinedIcon />
|
||||||
</UnfinishedSwapsBadge>
|
</UnfinishedSwapsBadge>
|
||||||
</RouteListItemIconButton>
|
</RouteListItemIconButton>
|
||||||
<RouteListItemIconButton name="Wallet" route="/wallet">
|
|
||||||
<AccountBalanceWalletIcon />
|
|
||||||
</RouteListItemIconButton>
|
|
||||||
<RouteListItemIconButton name="Feedback" route="/feedback">
|
<RouteListItemIconButton name="Feedback" route="/feedback">
|
||||||
<Badge
|
<Badge
|
||||||
badgeContent={totalUnreadCount}
|
badgeContent={totalUnreadCount}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ListItemIcon, ListItemText } from "@mui/material";
|
import { ListItemIcon, ListItemText } from "@mui/material";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import ListItemButton from "@mui/material/ListItemButton";
|
import ListItemButton from "@mui/material/ListItemButton";
|
||||||
|
|
||||||
|
|
@ -10,13 +10,31 @@ export default function RouteListItemIconButton({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
route: string;
|
route: string[] | string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const routeArray = Array.isArray(route) ? route : [route];
|
||||||
|
const firstRoute = routeArray[0];
|
||||||
|
const isSelected = routeArray.some((r) => location.pathname === r);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItemButton onClick={() => navigate(route)} key={name}>
|
<ListItemButton
|
||||||
|
onClick={() => navigate(firstRoute)}
|
||||||
|
key={name}
|
||||||
|
sx={
|
||||||
|
isSelected
|
||||||
|
? {
|
||||||
|
backgroundColor: "action.hover",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "action.selected",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<ListItemIcon>{children}</ListItemIcon>
|
<ListItemIcon>{children}</ListItemIcon>
|
||||||
<ListItemText primary={name} />
|
<ListItemText primary={name} />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,18 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Badge } from "@mui/material";
|
import { Badge } from "@mui/material";
|
||||||
import { useResumeableSwapsCountExcludingPunished } from "store/hooks";
|
import { useIsSwapRunning, useResumeableSwapsCountExcludingPunished } from "store/hooks";
|
||||||
|
|
||||||
export default function UnfinishedSwapsBadge({
|
export default function UnfinishedSwapsBadge({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const isSwapRunning = useIsSwapRunning();
|
||||||
const resumableSwapsCount = useResumeableSwapsCountExcludingPunished();
|
const resumableSwapsCount = useResumeableSwapsCountExcludingPunished();
|
||||||
|
|
||||||
if (resumableSwapsCount > 0) {
|
const displayedResumableSwapsCount = isSwapRunning ? resumableSwapsCount - 1 : resumableSwapsCount;
|
||||||
|
|
||||||
|
if (displayedResumableSwapsCount > 0) {
|
||||||
return (
|
return (
|
||||||
<Badge badgeContent={resumableSwapsCount} color="primary">
|
<Badge badgeContent={resumableSwapsCount} color="primary">
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ type Props = {
|
||||||
content: string;
|
content: string;
|
||||||
displayCopyIcon?: boolean;
|
displayCopyIcon?: boolean;
|
||||||
enableQrCode?: boolean;
|
enableQrCode?: boolean;
|
||||||
|
light?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function QRCodeModal({ open, onClose, content }: ModalProps) {
|
function QRCodeModal({ open, onClose, content }: ModalProps) {
|
||||||
|
|
@ -57,6 +58,7 @@ export default function ActionableMonospaceTextBox({
|
||||||
content,
|
content,
|
||||||
displayCopyIcon = true,
|
displayCopyIcon = true,
|
||||||
enableQrCode = true,
|
enableQrCode = true,
|
||||||
|
light = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [qrCodeOpen, setQrCodeOpen] = useState(false);
|
const [qrCodeOpen, setQrCodeOpen] = useState(false);
|
||||||
|
|
@ -88,7 +90,7 @@ export default function ActionableMonospaceTextBox({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ flexGrow: 1 }} onClick={handleCopy}>
|
<Box sx={{ flexGrow: 1 }} onClick={handleCopy}>
|
||||||
<MonospaceTextBox>
|
<MonospaceTextBox light={light}>
|
||||||
{content}
|
{content}
|
||||||
{displayCopyIcon && (
|
{displayCopyIcon && (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,18 @@ import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
light?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MonospaceTextBox({ children }: Props) {
|
export default function MonospaceTextBox({ children, light = false }: Props) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
backgroundColor: theme.palette.grey[900],
|
backgroundColor: light ? "transparent" : theme.palette.grey[900],
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
|
border: light ? `1px solid ${theme.palette.grey[800]}` : "none",
|
||||||
padding: theme.spacing(1),
|
padding: theme.spacing(1),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -63,11 +63,13 @@ export default function CliLogsBox({
|
||||||
logs,
|
logs,
|
||||||
topRightButton = null,
|
topRightButton = null,
|
||||||
autoScroll = false,
|
autoScroll = false,
|
||||||
|
minHeight,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
logs: (CliLog | string)[];
|
logs: (CliLog | string)[];
|
||||||
topRightButton?: ReactNode;
|
topRightButton?: ReactNode;
|
||||||
autoScroll?: boolean;
|
autoScroll?: boolean;
|
||||||
|
minHeight?: string;
|
||||||
}) {
|
}) {
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
|
|
||||||
|
|
@ -82,6 +84,7 @@ export default function CliLogsBox({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollablePaperTextBox
|
<ScrollablePaperTextBox
|
||||||
|
minHeight={minHeight}
|
||||||
title={label}
|
title={label}
|
||||||
copyValue={logsToRawString(logs)}
|
copyValue={logsToRawString(logs)}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export default function ScrollablePaperTextBox({
|
||||||
gap: "0.5rem",
|
gap: "0.5rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VList ref={virtuaEl} style={{ height: MIN_HEIGHT, width: "100%" }}>
|
<VList ref={virtuaEl} style={{ height: "100vh", width: "100%" }}>
|
||||||
{rows}
|
{rows}
|
||||||
</VList>
|
</VList>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export default function TruncatedText({
|
||||||
let finalChildren = children ?? "";
|
let finalChildren = children ?? "";
|
||||||
|
|
||||||
const truncatedText =
|
const truncatedText =
|
||||||
finalChildren.length > limit
|
finalChildren.length > limit
|
||||||
? truncateMiddle
|
? truncateMiddle
|
||||||
? finalChildren.slice(0, Math.floor(limit / 2)) +
|
? finalChildren.slice(0, Math.floor(limit / 2)) +
|
||||||
ellipsis +
|
ellipsis +
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,43 @@ export function AmountWithUnit({
|
||||||
return (
|
return (
|
||||||
<Tooltip arrow title={title}>
|
<Tooltip arrow title={title}>
|
||||||
<span>
|
<span>
|
||||||
{amount != null
|
{amount != null ? amount.toFixed(fixedPrecision) : "?"} {unit}
|
||||||
? Number.parseFloat(amount.toFixed(fixedPrecision))
|
|
||||||
: "?"}{" "}
|
|
||||||
{unit}
|
|
||||||
{parenthesisText != null ? ` (${parenthesisText})` : null}
|
{parenthesisText != null ? ` (${parenthesisText})` : null}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function FiatPiconeroAmount({
|
||||||
|
amount,
|
||||||
|
fixedPrecision = 2,
|
||||||
|
}: {
|
||||||
|
amount: Amount;
|
||||||
|
fixedPrecision?: number;
|
||||||
|
}) {
|
||||||
|
const xmrPrice = useAppSelector((state) => state.rates.xmrPrice);
|
||||||
|
const [fetchFiatPrices, fiatCurrency] = useSettings((settings) => [
|
||||||
|
settings.fetchFiatPrices,
|
||||||
|
settings.fiatCurrency,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!fetchFiatPrices ||
|
||||||
|
fiatCurrency == null ||
|
||||||
|
amount == null ||
|
||||||
|
xmrPrice == null
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{(piconerosToXmr(amount) * xmrPrice).toFixed(fixedPrecision)}{" "}
|
||||||
|
{fiatCurrency}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
AmountWithUnit.defaultProps = {
|
AmountWithUnit.defaultProps = {
|
||||||
exchangeRate: null,
|
exchangeRate: null,
|
||||||
};
|
};
|
||||||
|
|
@ -59,14 +86,20 @@ export function BitcoinAmount({ amount }: { amount: Amount }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MoneroAmount({ amount }: { amount: Amount }) {
|
export function MoneroAmount({
|
||||||
|
amount,
|
||||||
|
fixedPrecision = 4,
|
||||||
|
}: {
|
||||||
|
amount: Amount;
|
||||||
|
fixedPrecision?: number;
|
||||||
|
}) {
|
||||||
const xmrRate = useAppSelector((state) => state.rates.xmrPrice);
|
const xmrRate = useAppSelector((state) => state.rates.xmrPrice);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AmountWithUnit
|
<AmountWithUnit
|
||||||
amount={amount}
|
amount={amount}
|
||||||
unit="XMR"
|
unit="XMR"
|
||||||
fixedPrecision={4}
|
fixedPrecision={fixedPrecision}
|
||||||
exchangeRate={xmrRate}
|
exchangeRate={xmrRate}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -128,8 +161,17 @@ export function SatsAmount({ amount }: { amount: Amount }) {
|
||||||
return <BitcoinAmount amount={btcAmount} />;
|
return <BitcoinAmount amount={btcAmount} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PiconeroAmount({ amount }: { amount: Amount }) {
|
export function PiconeroAmount({
|
||||||
|
amount,
|
||||||
|
fixedPrecision = 8,
|
||||||
|
}: {
|
||||||
|
amount: Amount;
|
||||||
|
fixedPrecision?: number;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<MoneroAmount amount={amount == null ? null : piconerosToXmr(amount)} />
|
<MoneroAmount
|
||||||
|
amount={amount == null ? null : piconerosToXmr(amount)}
|
||||||
|
fixedPrecision={fixedPrecision}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import HistoryTable from "./table/HistoryTable";
|
||||||
export default function HistoryPage() {
|
export default function HistoryPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography variant="h3">History</Typography>
|
|
||||||
<SwapTxLockAlertsBox />
|
<SwapTxLockAlertsBox />
|
||||||
<HistoryTable />
|
<HistoryTable />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import {
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
Typography,
|
||||||
|
Skeleton,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useSwapInfosSortedByDate } from "../../../../../store/hooks";
|
import { useSwapInfosSortedByDate } from "../../../../../store/hooks";
|
||||||
import HistoryRow from "./HistoryRow";
|
import HistoryRow from "./HistoryRow";
|
||||||
|
|
@ -23,19 +25,75 @@ export default function HistoryTable() {
|
||||||
>
|
>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
{swapSortedByDate.length > 0 && (
|
||||||
<TableRow>
|
<TableHead>
|
||||||
<TableCell />
|
<TableRow>
|
||||||
<TableCell>ID</TableCell>
|
<TableCell />
|
||||||
<TableCell>Amount</TableCell>
|
<TableCell>ID</TableCell>
|
||||||
<TableCell>State</TableCell>
|
<TableCell>Amount</TableCell>
|
||||||
<TableCell />
|
<TableCell>State</TableCell>
|
||||||
</TableRow>
|
<TableCell />
|
||||||
</TableHead>
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
)}
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{swapSortedByDate.map((swap) => (
|
{swapSortedByDate.length === 0 ? (
|
||||||
<HistoryRow {...swap} key={swap.swap_id} />
|
<>
|
||||||
))}
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} sx={{ textAlign: "center", py: 4 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
color="text.secondary"
|
||||||
|
gutterBottom
|
||||||
|
>
|
||||||
|
Nothing to see here
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
You haven't made any swaps yet
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/* Skeleton rows for visual loading effect */}
|
||||||
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton
|
||||||
|
animation={false}
|
||||||
|
variant="circular"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton animation={false} variant="text" width="80%" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton animation={false} variant="text" width="60%" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton
|
||||||
|
animation={false}
|
||||||
|
variant="rectangular"
|
||||||
|
width={80}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton
|
||||||
|
animation={false}
|
||||||
|
variant="circular"
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
swapSortedByDate.map((swap) => (
|
||||||
|
<HistoryRow {...swap} key={swap.swap_id} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,11 @@ export default function SwapLogFileOpenButton({
|
||||||
<Dialog open onClose={() => setLogs(null)} fullWidth maxWidth="lg">
|
<Dialog open onClose={() => setLogs(null)} fullWidth maxWidth="lg">
|
||||||
<DialogTitle>Logs of swap {swapId}</DialogTitle>
|
<DialogTitle>Logs of swap {swapId}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
|
<CliLogsBox
|
||||||
|
minHeight="min(20rem, 70vh)"
|
||||||
|
logs={logs}
|
||||||
|
label="Logs relevant to the swap"
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { useAppSelector } from "store/hooks";
|
||||||
|
import { initializeMoneroWallet } from "renderer/rpc";
|
||||||
|
import {
|
||||||
|
WalletOverview,
|
||||||
|
TransactionHistory,
|
||||||
|
WalletActionButtons,
|
||||||
|
} from "./components";
|
||||||
|
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||||
|
import WalletPageLoadingState from "./components/WalletPageLoadingState";
|
||||||
|
|
||||||
|
// Main MoneroWalletPage component
|
||||||
|
export default function MoneroWalletPage() {
|
||||||
|
const { mainAddress, balance, syncProgress, history } = useAppSelector(
|
||||||
|
(state) => state.wallet.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeMoneroWallet();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isLoading = balance === null;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <WalletPageLoadingState />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 800,
|
||||||
|
mx: "auto",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 2,
|
||||||
|
pb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WalletOverview balance={balance} syncProgress={syncProgress} />
|
||||||
|
<ActionableMonospaceTextBox
|
||||||
|
content={mainAddress}
|
||||||
|
displayCopyIcon={true}
|
||||||
|
/>
|
||||||
|
<WalletActionButtons balance={balance} />
|
||||||
|
<TransactionHistory history={history} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Dialog } from "@mui/material";
|
||||||
|
import SendTransactionContent from "./components/SendTransactionContent";
|
||||||
|
import SendApprovalContent from "./components/SendApprovalContent";
|
||||||
|
import { useState } from "react";
|
||||||
|
import SendSuccessContent from "./components/SendSuccessContent";
|
||||||
|
import { usePendingSendMoneroApproval } from "store/hooks";
|
||||||
|
import { SendMoneroResponse } from "models/tauriModel";
|
||||||
|
|
||||||
|
interface SendTransactionModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
balance: {
|
||||||
|
unlocked_balance: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SendTransactionModal({
|
||||||
|
balance,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: SendTransactionModalProps) {
|
||||||
|
const pendingApprovals = usePendingSendMoneroApproval();
|
||||||
|
const hasPendingApproval = pendingApprovals.length > 0;
|
||||||
|
|
||||||
|
const [successResponse, setSuccessResponse] = useState<SendMoneroResponse | null>(null);
|
||||||
|
|
||||||
|
const showSuccess = successResponse !== null;
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
onClose();
|
||||||
|
setSuccessResponse(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth={!showSuccess}
|
||||||
|
PaperProps={{
|
||||||
|
sx: { borderRadius: 2 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!showSuccess && !hasPendingApproval && (
|
||||||
|
<SendTransactionContent balance={balance} onClose={onClose} onSuccess={setSuccessResponse} />
|
||||||
|
)}
|
||||||
|
{!showSuccess && hasPendingApproval && (
|
||||||
|
<SendApprovalContent onClose={onClose} />
|
||||||
|
)}
|
||||||
|
{showSuccess && (
|
||||||
|
<SendSuccessContent onClose={onClose} successDetails={successResponse} />
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
Radio,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import { DialogTitle } from "@mui/material";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { getRestoreHeight, setMoneroRestoreHeight } from "renderer/rpc";
|
||||||
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||||
|
import { Dayjs } from "dayjs";
|
||||||
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
|
||||||
|
enum RestoreOption {
|
||||||
|
BlockHeight = "blockHeight",
|
||||||
|
RestoreDate = "restoreDate",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SetRestoreHeightModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const [restoreOption, setRestoreOption] = useState(RestoreOption.BlockHeight);
|
||||||
|
const [restoreHeight, setRestoreHeight] = useState<number | "">("");
|
||||||
|
const [restoreDate, setRestoreDate] = useState<Dayjs | null>(null);
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
const [currentRestoreHeight, setCurrentRestoreHeight] =
|
||||||
|
useState<string>("Loading...");
|
||||||
|
|
||||||
|
const handleRestoreHeight = async () => {
|
||||||
|
if (restoreOption === RestoreOption.BlockHeight) {
|
||||||
|
if (typeof restoreHeight === "number") {
|
||||||
|
await setMoneroRestoreHeight(restoreHeight);
|
||||||
|
}
|
||||||
|
} else if (restoreOption === RestoreOption.RestoreDate) {
|
||||||
|
if (restoreDate) {
|
||||||
|
await setMoneroRestoreHeight(restoreDate.toDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCurrentRestoreHeight = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getRestoreHeight();
|
||||||
|
setCurrentRestoreHeight(response.height.toString());
|
||||||
|
setRestoreHeight(response.height); // Set the input field to current height
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch restore height:", error);
|
||||||
|
setCurrentRestoreHeight("Error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
fetchCurrentRestoreHeight();
|
||||||
|
}
|
||||||
|
}, [open, isPending]);
|
||||||
|
|
||||||
|
const accordionStyle = {
|
||||||
|
"& .MuiAccordionSummary-content": {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: 1,
|
||||||
|
},
|
||||||
|
"&::before": {
|
||||||
|
opacity: "1 !important",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose}>
|
||||||
|
<DialogTitle>Set Restore Height</DialogTitle>
|
||||||
|
<DialogContent sx={{ minWidth: "500px", minHeight: "300px" }}>
|
||||||
|
<Accordion
|
||||||
|
elevation={0}
|
||||||
|
expanded={restoreOption === RestoreOption.BlockHeight}
|
||||||
|
onChange={() => setRestoreOption(RestoreOption.BlockHeight)}
|
||||||
|
disableGutters
|
||||||
|
sx={accordionStyle}
|
||||||
|
>
|
||||||
|
<AccordionSummary>
|
||||||
|
<Typography>Restore by block height</Typography>
|
||||||
|
<Radio checked={restoreOption === RestoreOption.BlockHeight} />
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<TextField
|
||||||
|
label="Restore Height"
|
||||||
|
type="number"
|
||||||
|
value={restoreHeight}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setRestoreHeight(value === "" ? "" : Number(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion
|
||||||
|
elevation={0}
|
||||||
|
expanded={restoreOption === RestoreOption.RestoreDate}
|
||||||
|
onChange={() => setRestoreOption(RestoreOption.RestoreDate)}
|
||||||
|
disableGutters
|
||||||
|
sx={accordionStyle}
|
||||||
|
>
|
||||||
|
<AccordionSummary>
|
||||||
|
<Typography>Restore by date</Typography>
|
||||||
|
<Radio checked={restoreOption === RestoreOption.RestoreDate} />
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<DatePicker
|
||||||
|
label="Restore Date"
|
||||||
|
value={restoreDate}
|
||||||
|
disableFuture
|
||||||
|
onChange={(date) => setRestoreDate(date)}
|
||||||
|
/>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
onInvoke={handleRestoreHeight}
|
||||||
|
onSuccess={onClose}
|
||||||
|
displayErrorSnackbar={true}
|
||||||
|
onPendingChange={setIsPending}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
import { Box, Button, Card, Grow, Typography } from "@mui/material";
|
||||||
|
import NumberInput from "renderer/components/inputs/NumberInput";
|
||||||
|
import SwapVertIcon from "@mui/icons-material/SwapVert";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { piconerosToXmr } from "../../../../../utils/conversionUtils";
|
||||||
|
import { MoneroAmount } from "renderer/components/other/Units";
|
||||||
|
|
||||||
|
interface SendAmountInputProps {
|
||||||
|
balance: {
|
||||||
|
unlocked_balance: string;
|
||||||
|
};
|
||||||
|
amount: string;
|
||||||
|
onAmountChange: (amount: string) => void;
|
||||||
|
onMaxClicked?: () => void;
|
||||||
|
onMaxToggled?: () => void;
|
||||||
|
currency: string;
|
||||||
|
onCurrencyChange: (currency: string) => void;
|
||||||
|
fiatCurrency: string;
|
||||||
|
xmrPrice: number;
|
||||||
|
showFiatRate: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SendAmountInput({
|
||||||
|
balance,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
onCurrencyChange,
|
||||||
|
onAmountChange,
|
||||||
|
onMaxClicked,
|
||||||
|
onMaxToggled,
|
||||||
|
fiatCurrency,
|
||||||
|
xmrPrice,
|
||||||
|
showFiatRate,
|
||||||
|
disabled = false,
|
||||||
|
}: SendAmountInputProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const isMaxSelected = amount === "<MAX>";
|
||||||
|
|
||||||
|
// Calculate secondary amount for display
|
||||||
|
const secondaryAmount = (() => {
|
||||||
|
if (isMaxSelected) {
|
||||||
|
return "All available funds";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!amount || amount === "" || isNaN(parseFloat(amount))) {
|
||||||
|
return "0.00";
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryValue = parseFloat(amount);
|
||||||
|
if (currency === "XMR") {
|
||||||
|
// Primary is XMR, secondary is USD
|
||||||
|
return (primaryValue * xmrPrice).toFixed(2);
|
||||||
|
} else {
|
||||||
|
// Primary is USD, secondary is XMR
|
||||||
|
return (primaryValue / xmrPrice).toFixed(3);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const handleMaxAmount = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
if (onMaxToggled) {
|
||||||
|
onMaxToggled();
|
||||||
|
} else if (onMaxClicked) {
|
||||||
|
onMaxClicked();
|
||||||
|
} else {
|
||||||
|
// Fallback to old behavior if no callback provided
|
||||||
|
if (
|
||||||
|
balance?.unlocked_balance !== undefined &&
|
||||||
|
balance?.unlocked_balance !== null
|
||||||
|
) {
|
||||||
|
// TODO: We need to use a real fee here and call sweep(...) instead of just subtracting a fixed amount
|
||||||
|
const unlocked = parseFloat(balance.unlocked_balance);
|
||||||
|
const maxAmountXmr = piconerosToXmr(unlocked - 10000000000); // Subtract ~0.01 XMR for fees
|
||||||
|
|
||||||
|
if (currency === "XMR") {
|
||||||
|
onAmountChange(Math.max(0, maxAmountXmr).toString());
|
||||||
|
} else {
|
||||||
|
// Convert to USD for display
|
||||||
|
const maxAmountUsd = maxAmountXmr * xmrPrice;
|
||||||
|
onAmountChange(Math.max(0, maxAmountUsd).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMaxTextClick = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
if (isMaxSelected && onMaxToggled) {
|
||||||
|
onMaxToggled();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCurrencySwap = () => {
|
||||||
|
if (!isMaxSelected && !disabled) {
|
||||||
|
onCurrencyChange(currency === "XMR" ? fiatCurrency : "XMR");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAmountTooHigh =
|
||||||
|
!isMaxSelected &&
|
||||||
|
(currency === "XMR"
|
||||||
|
? parseFloat(amount) >
|
||||||
|
piconerosToXmr(parseFloat(balance.unlocked_balance))
|
||||||
|
: parseFloat(amount) / xmrPrice >
|
||||||
|
piconerosToXmr(parseFloat(balance.unlocked_balance)));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
tabIndex={0}
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
border: `1px solid ${theme.palette.grey[800]}`,
|
||||||
|
width: "100%",
|
||||||
|
height: 250,
|
||||||
|
opacity: disabled ? 0.6 : 1,
|
||||||
|
pointerEvents: disabled ? "none" : "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{ display: "flex", flexDirection: "column", alignItems: "center" }}
|
||||||
|
>
|
||||||
|
{isAmountTooHigh && (
|
||||||
|
<Grow
|
||||||
|
in
|
||||||
|
style={{ transitionDelay: isAmountTooHigh ? "100ms" : "0ms" }}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" align="center" color="error">
|
||||||
|
You don't have enough
|
||||||
|
<br /> unlocked balance to send this amount.
|
||||||
|
</Typography>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
<Box sx={{ display: "flex", alignItems: "baseline", gap: 1 }}>
|
||||||
|
{isMaxSelected ? (
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
onClick={handleMaxTextClick}
|
||||||
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "primary.main",
|
||||||
|
cursor: disabled ? "default" : "pointer",
|
||||||
|
userSelect: "none",
|
||||||
|
"&:hover": {
|
||||||
|
opacity: disabled ? 1 : 0.8,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
title={disabled ? "" : "Click to edit amount"}
|
||||||
|
>
|
||||||
|
<MAX>
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<NumberInput
|
||||||
|
value={amount}
|
||||||
|
onChange={disabled ? () => {} : onAmountChange}
|
||||||
|
placeholder={currency === "XMR" ? "0.000" : "0.00"}
|
||||||
|
fontSize="3em"
|
||||||
|
fontWeight={600}
|
||||||
|
minWidth={60}
|
||||||
|
step={currency === "XMR" ? 0.001 : 0.01}
|
||||||
|
largeStep={currency === "XMR" ? 0.1 : 10}
|
||||||
|
/>
|
||||||
|
<Typography variant="h4" color="text.secondary">
|
||||||
|
{currency}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{showFiatRate && (
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5 }}>
|
||||||
|
<SwapVertIcon
|
||||||
|
onClick={handleCurrencySwap}
|
||||||
|
sx={{
|
||||||
|
cursor: isMaxSelected || disabled ? "default" : "pointer",
|
||||||
|
opacity: isMaxSelected || disabled ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
{secondaryAmount}{" "}
|
||||||
|
{isMaxSelected ? "" : currency === "XMR" ? fiatCurrency : "XMR"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: 1.5,
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 12,
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography color="text.secondary">Available</Typography>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "baseline", gap: 0.5 }}>
|
||||||
|
<Typography color="text.primary">
|
||||||
|
<MoneroAmount
|
||||||
|
amount={piconerosToXmr(parseFloat(balance.unlocked_balance))}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
<Typography color="text.secondary">XMR</Typography>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant={isMaxSelected ? "contained" : "secondary"}
|
||||||
|
size="tiny"
|
||||||
|
onClick={handleMaxAmount}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
Max
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
} from "@mui/material";
|
||||||
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import { resolveApproval } from "renderer/rpc";
|
||||||
|
import { usePendingSendMoneroApproval } from "store/hooks";
|
||||||
|
import { PiconeroAmount } from "renderer/components/other/Units";
|
||||||
|
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||||
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
|
||||||
|
interface SendApprovalContentProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SendApprovalContent({
|
||||||
|
onClose,
|
||||||
|
}: SendApprovalContentProps) {
|
||||||
|
const pendingApprovals = usePendingSendMoneroApproval();
|
||||||
|
const [timeLeft, setTimeLeft] = useState<number>(0);
|
||||||
|
|
||||||
|
const approval = pendingApprovals[0]; // Handle the first approval request
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!approval?.request_status ||
|
||||||
|
approval.request_status.state !== "Pending"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expirationTs = approval.request_status.content.expiration_ts;
|
||||||
|
const expiresAtMs = expirationTs * 1000;
|
||||||
|
|
||||||
|
const tick = () => {
|
||||||
|
const remainingMs = Math.max(expiresAtMs - Date.now(), 0);
|
||||||
|
setTimeLeft(Math.ceil(remainingMs / 1000));
|
||||||
|
};
|
||||||
|
|
||||||
|
tick();
|
||||||
|
const id = setInterval(tick, 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [approval]);
|
||||||
|
|
||||||
|
const handleApprove = async () => {
|
||||||
|
if (!approval) throw new Error("No approval request available");
|
||||||
|
await resolveApproval(approval.request_id, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = async () => {
|
||||||
|
if (!approval) throw new Error("No approval request available");
|
||||||
|
await resolveApproval(approval.request_id, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!approval) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { address, amount, fee } = approval.request.content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogTitle>
|
||||||
|
<Typography variant="h6" component="div">
|
||||||
|
Confirm Monero Transfer
|
||||||
|
</Typography>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
|
{/* Amount */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Amount to Send
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
<PiconeroAmount amount={amount} fixedPrecision={12} />
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Fee */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Network Fee
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
<PiconeroAmount amount={fee} fixedPrecision={12} />
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Destination Address */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
|
Destination Address
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" color="text.secondary">
|
||||||
|
<ActionableMonospaceTextBox
|
||||||
|
content={address}
|
||||||
|
displayCopyIcon={true}
|
||||||
|
enableQrCode={false}
|
||||||
|
light={true}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Time remaining */}
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ textAlign: "center" }}
|
||||||
|
>
|
||||||
|
{timeLeft > 0
|
||||||
|
? `Request expires in ${timeLeft}s`
|
||||||
|
: "Request expired"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ p: 3, gap: 1 }}>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
onInvoke={handleReject}
|
||||||
|
onSuccess={onClose}
|
||||||
|
disabled={timeLeft === 0}
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
startIcon={<CloseIcon />}
|
||||||
|
displayErrorSnackbar={true}
|
||||||
|
requiresContext={false}
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
onInvoke={handleApprove}
|
||||||
|
disabled={timeLeft === 0}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<CheckIcon />}
|
||||||
|
displayErrorSnackbar={true}
|
||||||
|
requiresContext={false}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { Box, Button, Typography } from "@mui/material";
|
||||||
|
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||||
|
import { FiatPiconeroAmount, PiconeroAmount } from "renderer/components/other/Units";
|
||||||
|
import MonospaceTextBox from "renderer/components/other/MonospaceTextBox";
|
||||||
|
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
|
||||||
|
import { SendMoneroResponse } from "models/tauriModel";
|
||||||
|
import { getMoneroTxExplorerUrl } from "../../../../../utils/conversionUtils";
|
||||||
|
import { isTestnet } from "store/config";
|
||||||
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
|
|
||||||
|
export default function SendSuccessContent({
|
||||||
|
onClose,
|
||||||
|
successDetails,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
successDetails: SendMoneroResponse | null;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const address = successDetails?.address;
|
||||||
|
const amount = successDetails?.amount_sent;
|
||||||
|
const explorerUrl = getMoneroTxExplorerUrl(successDetails?.tx_hash, isTestnet());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
minHeight: "400px",
|
||||||
|
minWidth: "500px",
|
||||||
|
gap: 7,
|
||||||
|
p: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckCircleIcon sx={{ fontSize: 64, mt: 3 }} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h4">Transaction Published</Typography>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "center", gap: 1 }}>
|
||||||
|
<Typography variant="body1" color="text.secondary">Sent</Typography>
|
||||||
|
<Typography variant="body1" color="text.primary">
|
||||||
|
<PiconeroAmount amount={amount} fixedPrecision={4}/>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">(<FiatPiconeroAmount amount={amount} />)</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "center", gap: 1 }}>
|
||||||
|
<Typography variant="body1" color="text.secondary">to</Typography>
|
||||||
|
<Typography variant="body1" color="text.primary">
|
||||||
|
<MonospaceTextBox>{address.slice(0, 8)} ... {address.slice(-8)}</MonospaceTextBox>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 1 }}>
|
||||||
|
<Button onClick={onClose} variant="contained" color="primary">Done</Button>
|
||||||
|
<Button color="primary" size="small" endIcon={<ArrowOutwardIcon />} onClick={() => open(explorerUrl)}>View on Explorer</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { xmrToPiconeros } from "../../../../../utils/conversionUtils";
|
||||||
|
import SendAmountInput from "./SendAmountInput";
|
||||||
|
import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField";
|
||||||
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
import { sendMoneroTransaction } from "renderer/rpc";
|
||||||
|
import { useAppSelector } from "store/hooks";
|
||||||
|
import { SendMoneroResponse } from "models/tauriModel";
|
||||||
|
|
||||||
|
interface SendTransactionContentProps {
|
||||||
|
balance: {
|
||||||
|
unlocked_balance: string;
|
||||||
|
};
|
||||||
|
onClose: () => void;
|
||||||
|
onSuccess: (response: SendMoneroResponse) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SendTransactionContent({
|
||||||
|
balance,
|
||||||
|
onSuccess,
|
||||||
|
onClose,
|
||||||
|
}: SendTransactionContentProps) {
|
||||||
|
const [sendAddress, setSendAddress] = useState("");
|
||||||
|
const [sendAmount, setSendAmount] = useState("");
|
||||||
|
const [previousAmount, setPreviousAmount] = useState("");
|
||||||
|
const [enableSend, setEnableSend] = useState(false);
|
||||||
|
const [currency, setCurrency] = useState("XMR");
|
||||||
|
const [isMaxSelected, setIsMaxSelected] = useState(false);
|
||||||
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
|
||||||
|
const showFiatRate = useAppSelector(
|
||||||
|
(state) => state.settings.fetchFiatPrices,
|
||||||
|
);
|
||||||
|
const fiatCurrency = useAppSelector((state) => state.settings.fiatCurrency);
|
||||||
|
const xmrPrice = useAppSelector((state) => state.rates.xmrPrice);
|
||||||
|
|
||||||
|
const handleCurrencyChange = (newCurrency: string) => {
|
||||||
|
if (!showFiatRate || !xmrPrice || isMaxSelected || isSending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sendAmount === "" || parseFloat(sendAmount) === 0) {
|
||||||
|
setSendAmount(newCurrency === "XMR" ? "0.000" : "0.00");
|
||||||
|
} else {
|
||||||
|
setSendAmount(
|
||||||
|
newCurrency === "XMR"
|
||||||
|
? (parseFloat(sendAmount) / xmrPrice).toFixed(3)
|
||||||
|
: (parseFloat(sendAmount) * xmrPrice).toFixed(2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setCurrency(newCurrency);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMaxToggled = () => {
|
||||||
|
if (isSending) return;
|
||||||
|
|
||||||
|
if (isMaxSelected) {
|
||||||
|
// Disable MAX mode - restore previous amount
|
||||||
|
setIsMaxSelected(false);
|
||||||
|
setSendAmount(previousAmount);
|
||||||
|
} else {
|
||||||
|
// Enable MAX mode - save current amount first
|
||||||
|
setPreviousAmount(sendAmount);
|
||||||
|
setIsMaxSelected(true);
|
||||||
|
setSendAmount("<MAX>");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAmountChange = (newAmount: string) => {
|
||||||
|
if (isSending) return;
|
||||||
|
|
||||||
|
if (newAmount !== "<MAX>") {
|
||||||
|
setIsMaxSelected(false);
|
||||||
|
}
|
||||||
|
setSendAmount(newAmount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddressChange = (newAddress: string) => {
|
||||||
|
if (isSending) return;
|
||||||
|
setSendAddress(newAddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
const moneroAmount =
|
||||||
|
currency === "XMR"
|
||||||
|
? parseFloat(sendAmount)
|
||||||
|
: parseFloat(sendAmount) / xmrPrice;
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!sendAddress) {
|
||||||
|
throw new Error("Address is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMaxSelected) {
|
||||||
|
return sendMoneroTransaction({
|
||||||
|
address: sendAddress,
|
||||||
|
amount: { type: "Sweep" },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!sendAmount || sendAmount === "<MAX>") {
|
||||||
|
throw new Error("Amount is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendMoneroTransaction({
|
||||||
|
address: sendAddress,
|
||||||
|
amount: {
|
||||||
|
type: "Specific",
|
||||||
|
// Floor the amount to avoid rounding decimal amounts
|
||||||
|
// The amount is in piconeros, so it NEEDS to be a whole number
|
||||||
|
amount: Math.floor(xmrToPiconeros(moneroAmount)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendSuccess = (response: SendMoneroResponse) => {
|
||||||
|
// Clear form after successful send
|
||||||
|
handleClear();
|
||||||
|
onSuccess(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
setSendAddress("");
|
||||||
|
setSendAmount("");
|
||||||
|
setPreviousAmount("");
|
||||||
|
setIsMaxSelected(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSendDisabled =
|
||||||
|
!enableSend || (!isMaxSelected && (!sendAmount || sendAmount === "<MAX>"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogTitle>Send</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
|
<SendAmountInput
|
||||||
|
balance={balance}
|
||||||
|
amount={sendAmount}
|
||||||
|
onAmountChange={handleAmountChange}
|
||||||
|
onMaxToggled={handleMaxToggled}
|
||||||
|
currency={currency}
|
||||||
|
fiatCurrency={fiatCurrency}
|
||||||
|
xmrPrice={xmrPrice}
|
||||||
|
showFiatRate={showFiatRate}
|
||||||
|
onCurrencyChange={handleCurrencyChange}
|
||||||
|
disabled={isSending}
|
||||||
|
/>
|
||||||
|
<MoneroAddressTextField
|
||||||
|
address={sendAddress}
|
||||||
|
onAddressChange={handleAddressChange}
|
||||||
|
onAddressValidityChange={setEnableSend}
|
||||||
|
label="Send to"
|
||||||
|
fullWidth
|
||||||
|
disabled={isSending}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
onInvoke={handleSend}
|
||||||
|
disabled={isSendDisabled}
|
||||||
|
onSuccess={handleSendSuccess}
|
||||||
|
onPendingChange={setIsSending}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Box, darken, lighten, useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
function getColor(colorName: string) {
|
||||||
|
const theme = useTheme();
|
||||||
|
switch (colorName) {
|
||||||
|
case "primary":
|
||||||
|
return theme.palette.primary.main;
|
||||||
|
case "secondary":
|
||||||
|
return theme.palette.secondary.main;
|
||||||
|
case "success":
|
||||||
|
return theme.palette.success.main;
|
||||||
|
case "warning":
|
||||||
|
return theme.palette.warning.main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StateIndicator({
|
||||||
|
color,
|
||||||
|
pulsating,
|
||||||
|
}: {
|
||||||
|
color: string;
|
||||||
|
pulsating: boolean;
|
||||||
|
}) {
|
||||||
|
const mainShade = getColor(color);
|
||||||
|
const darkShade = darken(mainShade, 0.4);
|
||||||
|
const glowShade = lighten(mainShade, 0.4);
|
||||||
|
|
||||||
|
const intensePulsatingStyles = {
|
||||||
|
animation: "pulse 2s infinite",
|
||||||
|
"@keyframes pulse": {
|
||||||
|
"0%": { opacity: 0.5 },
|
||||||
|
"50%": { opacity: 1 },
|
||||||
|
"100%": { opacity: 0.5 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const softPulsatingStyles = {
|
||||||
|
animation: "pulse 3.5s infinite",
|
||||||
|
"@keyframes pulse": {
|
||||||
|
"0%": { opacity: 0.7 },
|
||||||
|
"50%": { opacity: 1 },
|
||||||
|
"100%": { opacity: 0.7 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundImage: `radial-gradient(circle, ${mainShade}, ${darkShade})`,
|
||||||
|
boxShadow: `0 0 10px 0 ${glowShade}`,
|
||||||
|
...(pulsating ? intensePulsatingStyles : softPulsatingStyles),
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
Chip,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
Stack,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { OpenInNew as OpenInNewIcon } from "@mui/icons-material";
|
||||||
|
import { open } from "@tauri-apps/plugin-shell";
|
||||||
|
import { PiconeroAmount } from "../../../other/Units";
|
||||||
|
import { getMoneroTxExplorerUrl } from "../../../../../utils/conversionUtils";
|
||||||
|
import { isTestnet } from "store/config";
|
||||||
|
import { TransactionInfo } from "models/tauriModel";
|
||||||
|
|
||||||
|
interface TransactionHistoryProps {
|
||||||
|
history?: {
|
||||||
|
transactions: TransactionInfo[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component for displaying transaction history
|
||||||
|
export default function TransactionHistory({
|
||||||
|
history,
|
||||||
|
}: TransactionHistoryProps) {
|
||||||
|
if (!history || !history.transactions || history.transactions.length === 0) {
|
||||||
|
return <Typography variant="h5">Transaction History</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography variant="h5">Transaction History</Typography>
|
||||||
|
|
||||||
|
<TableContainer component={Paper} variant="outlined">
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Amount</TableCell>
|
||||||
|
<TableCell>Fee</TableCell>
|
||||||
|
<TableCell align="right">Confirmations</TableCell>
|
||||||
|
<TableCell align="center">Explorer</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{[...history.transactions]
|
||||||
|
.sort((a, b) => a.confirmations - b.confirmations)
|
||||||
|
.map((tx, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell>
|
||||||
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
|
<PiconeroAmount amount={tx.amount} />
|
||||||
|
<Chip
|
||||||
|
label={tx.direction === "In" ? "Received" : "Sent"}
|
||||||
|
color={tx.direction === "In" ? "success" : "default"}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<PiconeroAmount amount={tx.fee} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Chip
|
||||||
|
label={tx.confirmations}
|
||||||
|
color={tx.confirmations >= 10 ? "success" : "warning"}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{tx.tx_hash && (
|
||||||
|
<Tooltip title="View on block explorer">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
const url = getMoneroTxExplorerUrl(
|
||||||
|
tx.tx_hash,
|
||||||
|
isTestnet(),
|
||||||
|
);
|
||||||
|
open(url);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OpenInNewIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
IconButton,
|
||||||
|
ListItemIcon,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
Send as SendIcon,
|
||||||
|
SwapHoriz as SwapIcon,
|
||||||
|
Restore as RestoreIcon,
|
||||||
|
MoreHoriz as MoreHorizIcon,
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { setMoneroRestoreHeight } from "renderer/rpc";
|
||||||
|
import SendTransactionModal from "../SendTransactionModal";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
import SetRestoreHeightModal from "../SetRestoreHeightModal";
|
||||||
|
|
||||||
|
interface WalletActionButtonsProps {
|
||||||
|
balance: {
|
||||||
|
unlocked_balance: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function RestoreHeightDialog({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const [restoreHeight, setRestoreHeight] = useState(0);
|
||||||
|
|
||||||
|
const handleRestoreHeight = async () => {
|
||||||
|
await setMoneroRestoreHeight(restoreHeight);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose}>
|
||||||
|
<DialogTitle>Restore Height</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
label="Restore Height"
|
||||||
|
type="number"
|
||||||
|
value={restoreHeight}
|
||||||
|
onChange={(e) => setRestoreHeight(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<PromiseInvokeButton
|
||||||
|
onInvoke={handleRestoreHeight}
|
||||||
|
displayErrorSnackbar={true}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</PromiseInvokeButton>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WalletActionButtons({
|
||||||
|
balance,
|
||||||
|
}: WalletActionButtonsProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [sendDialogOpen, setSendDialogOpen] = useState(false);
|
||||||
|
const [restoreHeightDialogOpen, setRestoreHeightDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
|
const menuOpen = Boolean(menuAnchorEl);
|
||||||
|
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setMenuAnchorEl(event.currentTarget);
|
||||||
|
};
|
||||||
|
const handleMenuClose = () => {
|
||||||
|
setMenuAnchorEl(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SetRestoreHeightModal
|
||||||
|
open={restoreHeightDialogOpen}
|
||||||
|
onClose={() => setRestoreHeightDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
<SendTransactionModal
|
||||||
|
balance={balance}
|
||||||
|
open={sendDialogOpen}
|
||||||
|
onClose={() => setSendDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 1,
|
||||||
|
mb: 2,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
icon={<SendIcon />}
|
||||||
|
label="Send"
|
||||||
|
variant="button"
|
||||||
|
clickable
|
||||||
|
onClick={() => setSendDialogOpen(true)}
|
||||||
|
/>
|
||||||
|
<Chip
|
||||||
|
onClick={() => navigate("/swap")}
|
||||||
|
icon={<SwapIcon />}
|
||||||
|
label="Swap"
|
||||||
|
variant="button"
|
||||||
|
clickable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton onClick={handleMenuClick}>
|
||||||
|
<MoreHorizIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Menu anchorEl={menuAnchorEl} open={menuOpen} onClose={handleMenuClose}>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setRestoreHeightDialogOpen(true);
|
||||||
|
handleMenuClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
<RestoreIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography>Restore Height</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
CircularProgress,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Divider,
|
||||||
|
CardHeader,
|
||||||
|
LinearProgress,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { PiconeroAmount } from "../../../other/Units";
|
||||||
|
import { FiatPiconeroAmount } from "../../../other/Units";
|
||||||
|
import StateIndicator from "./StateIndicator";
|
||||||
|
|
||||||
|
interface WalletOverviewProps {
|
||||||
|
balance?: {
|
||||||
|
unlocked_balance: string;
|
||||||
|
total_balance: string;
|
||||||
|
};
|
||||||
|
syncProgress?: {
|
||||||
|
current_block: number;
|
||||||
|
target_block: number;
|
||||||
|
progress_percentage: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component for displaying wallet address and balance
|
||||||
|
export default function WalletOverview({
|
||||||
|
balance,
|
||||||
|
syncProgress,
|
||||||
|
}: WalletOverviewProps) {
|
||||||
|
const pendingBalance =
|
||||||
|
parseFloat(balance.total_balance) - parseFloat(balance.unlocked_balance);
|
||||||
|
|
||||||
|
const isSyncing = syncProgress && syncProgress.progress_percentage < 100;
|
||||||
|
const blocksLeft = syncProgress?.target_block - syncProgress?.current_block;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card sx={{ p: 2, position: "relative", borderRadius: 2 }} elevation={4}>
|
||||||
|
{syncProgress && syncProgress.progress_percentage < 100 && (
|
||||||
|
<LinearProgress
|
||||||
|
value={syncProgress.progress_percentage}
|
||||||
|
variant="determinate"
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Balance */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1.5fr 1fr max-content",
|
||||||
|
rowGap: 0.5,
|
||||||
|
columnGap: 2,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ mb: 1, gridColumn: "1", gridRow: "1" }}
|
||||||
|
>
|
||||||
|
Available Funds
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4" sx={{ gridColumn: "1", gridRow: "2" }}>
|
||||||
|
<PiconeroAmount
|
||||||
|
amount={parseFloat(balance.unlocked_balance)}
|
||||||
|
fixedPrecision={4}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ gridColumn: "1", gridRow: "3" }}
|
||||||
|
>
|
||||||
|
<FiatPiconeroAmount amount={parseFloat(balance.unlocked_balance)} />
|
||||||
|
</Typography>
|
||||||
|
{pendingBalance > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="warning"
|
||||||
|
sx={{
|
||||||
|
mb: 1,
|
||||||
|
animation: "pulse 2s infinite",
|
||||||
|
gridColumn: "2",
|
||||||
|
gridRow: "1",
|
||||||
|
alignSelf: "end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pending
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="h5"
|
||||||
|
sx={{ gridColumn: "2", gridRow: "2", alignSelf: "center" }}
|
||||||
|
>
|
||||||
|
<PiconeroAmount amount={pendingBalance} fixedPrecision={4} />
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ gridColumn: "2", gridRow: "3" }}
|
||||||
|
>
|
||||||
|
<FiatPiconeroAmount amount={pendingBalance} />
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{isSyncing ? "syncing" : "synced"}
|
||||||
|
</Typography>
|
||||||
|
<StateIndicator
|
||||||
|
color={isSyncing ? "primary" : "success"}
|
||||||
|
pulsating={isSyncing}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{isSyncing && (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{blocksLeft.toLocaleString()} blocks left
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { Box, Card, Chip, Skeleton, Typography } from "@mui/material";
|
||||||
|
import StateIndicator from "./StateIndicator";
|
||||||
|
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||||
|
|
||||||
|
const DUMMY_ADDRESS =
|
||||||
|
"888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H";
|
||||||
|
|
||||||
|
export default function WalletPageLoadingState() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxWidth: 800,
|
||||||
|
mx: "auto",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 2,
|
||||||
|
pb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card sx={{ p: 2, position: "relative", borderRadius: 2 }} elevation={4}>
|
||||||
|
{/* Balance */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1.5fr 1fr max-content",
|
||||||
|
rowGap: 0.5,
|
||||||
|
columnGap: 2,
|
||||||
|
mb: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ mb: 1, gridColumn: "1", gridRow: "1" }}
|
||||||
|
>
|
||||||
|
Available Funds
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h4" sx={{ gridColumn: "1", gridRow: "2" }}>
|
||||||
|
<Skeleton variant="text" width="80%" />
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ gridColumn: "1", gridRow: "3" }}
|
||||||
|
>
|
||||||
|
<Skeleton variant="text" width="40%" />
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2">loading</Typography>
|
||||||
|
<StateIndicator color="primary" pulsating={true} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Skeleton variant="rounded" width="100%">
|
||||||
|
<ActionableMonospaceTextBox content={DUMMY_ADDRESS} />
|
||||||
|
</Skeleton>
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mb: 2 }}>
|
||||||
|
{Array.from({ length: 2 }).map((_) => (
|
||||||
|
<Skeleton variant="rounded" sx={{ borderRadius: "100px" }}>
|
||||||
|
<Chip label="Loading..." variant="button" />
|
||||||
|
</Skeleton>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="h5">Transaction History</Typography>
|
||||||
|
<Skeleton variant="rounded" width="100%" height={40} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export { default as WalletOverview } from "./WalletOverview";
|
||||||
|
export { default as TransactionHistory } from "./TransactionHistory";
|
||||||
|
export { default as WalletActionButtons } from "./WalletActionButtons";
|
||||||
|
export { default as SendTransactionContent } from "./SendTransactionContent";
|
||||||
|
export { default as SendApprovalContent } from "./SendApprovalContent";
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
import { Box, Link, Typography } from "@mui/material";
|
import { Box, Link, Typography } from "@mui/material";
|
||||||
import { ReactNode } from "react";
|
|
||||||
import InfoBox from "./InfoBox";
|
import InfoBox from "./InfoBox";
|
||||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ export default function InitPage() {
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
// We force this to true for now because the internal wallet is not really accessible from the GUI yet
|
// We force this to true for now because the internal wallet is not really accessible from the GUI yet
|
||||||
const [useExternalRedeemAddress, _setUseExternalRedeemAddress] =
|
const [useExternalRedeemAddress, setUseExternalRedeemAddress] =
|
||||||
useState(true);
|
useState(true);
|
||||||
|
|
||||||
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
|
const [redeemAddressValid, setRedeemAddressValid] = useState(false);
|
||||||
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
const [refundAddressValid, setRefundAddressValid] = useState(false);
|
||||||
|
|
@ -40,14 +40,35 @@ export default function InitPage() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Paper variant="outlined" style={{}}>
|
<Paper variant="outlined" style={{}}>
|
||||||
<MoneroAddressTextField
|
<Tabs
|
||||||
label="Monero redeem address"
|
value={useExternalRedeemAddress ? 1 : 0}
|
||||||
address={redeemAddress}
|
indicatorColor="primary"
|
||||||
onAddressChange={setRedeemAddress}
|
variant="fullWidth"
|
||||||
onAddressValidityChange={setRedeemAddressValid}
|
onChange={(_, newValue) =>
|
||||||
fullWidth
|
setUseExternalRedeemAddress(newValue === 1)
|
||||||
helperText="The monero will be sent to this address"
|
}
|
||||||
/>
|
>
|
||||||
|
<Tab label="Redeem to internal Monero wallet" value={0} />
|
||||||
|
<Tab label="Redeem to external Monero address" value={1} />
|
||||||
|
</Tabs>
|
||||||
|
<Box style={{ padding: "16px" }}>
|
||||||
|
{useExternalRedeemAddress ? (
|
||||||
|
<MoneroAddressTextField
|
||||||
|
label="External Monero redeem address"
|
||||||
|
address={redeemAddress}
|
||||||
|
onAddressChange={setRedeemAddress}
|
||||||
|
onAddressValidityChange={setRedeemAddressValid}
|
||||||
|
helperText="The monero will be sent to this address if the swap is successful."
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Typography variant="caption">
|
||||||
|
The Monero will be sent to the internal Monero wallet of the
|
||||||
|
GUI. You can then withdraw them from there or use them for
|
||||||
|
another swap directly.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Paper variant="outlined" style={{}}>
|
<Paper variant="outlined" style={{}}>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ export default function WalletPage() {
|
||||||
gap: "1rem",
|
gap: "1rem",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h3">Wallet</Typography>
|
|
||||||
<Alert severity="info">
|
<Alert severity="info">
|
||||||
You do not have to deposit money before starting a swap. Instead, you
|
You do not have to deposit money before starting a swap. Instead, you
|
||||||
will be greeted with a deposit address after you initiate one.
|
will be greeted with a deposit address after you initiate one.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,23 @@
|
||||||
import { createTheme, ThemeOptions } from "@mui/material";
|
import { createTheme, ThemeOptions } from "@mui/material";
|
||||||
import { indigo } from "@mui/material/colors";
|
import { indigo } from "@mui/material/colors";
|
||||||
|
|
||||||
|
// Extend the theme to include custom chip variants
|
||||||
|
declare module "@mui/material/Chip" {
|
||||||
|
interface ChipPropsVariantOverrides {
|
||||||
|
button: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend the theme to include custom button variants and sizes
|
||||||
|
declare module "@mui/material/Button" {
|
||||||
|
interface ButtonPropsVariantOverrides {
|
||||||
|
secondary: true;
|
||||||
|
}
|
||||||
|
interface ButtonPropsSizeOverrides {
|
||||||
|
tiny: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export enum Theme {
|
export enum Theme {
|
||||||
Light = "light",
|
Light = "light",
|
||||||
Dark = "dark",
|
Dark = "dark",
|
||||||
|
|
@ -33,7 +50,61 @@ const baseTheme: ThemeOptions = {
|
||||||
backgroundColor: "color-mix(in srgb, #bdbdbd 10%, transparent)",
|
backgroundColor: "color-mix(in srgb, #bdbdbd 10%, transparent)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
sizeTiny: {
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: "4px 8px",
|
||||||
|
minHeight: "24px",
|
||||||
|
minWidth: "auto",
|
||||||
|
lineHeight: 1.2,
|
||||||
|
textTransform: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
props: { variant: "secondary" },
|
||||||
|
style: ({ theme }) => ({
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "rgba(255, 255, 255, 0.08)"
|
||||||
|
: "rgba(0, 0, 0, 0.04)",
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "rgba(255, 255, 255, 0.12)"
|
||||||
|
: "rgba(0, 0, 0, 0.08)",
|
||||||
|
borderColor:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "rgba(255, 255, 255, 0.23)"
|
||||||
|
: "rgba(0, 0, 0, 0.23)",
|
||||||
|
},
|
||||||
|
"&:disabled": {
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "rgba(255, 255, 255, 0.04)"
|
||||||
|
: "rgba(0, 0, 0, 0.02)",
|
||||||
|
color: theme.palette.text.disabled,
|
||||||
|
borderColor:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "rgba(255, 255, 255, 0.08)"
|
||||||
|
: "rgba(0, 0, 0, 0.08)",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
MuiChip: {
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
props: { variant: "button" },
|
||||||
|
style: ({ theme }) => ({
|
||||||
|
padding: "12px 16px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
MuiDialog: {
|
MuiDialog: {
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,31 @@ import {
|
||||||
RedactResponse,
|
RedactResponse,
|
||||||
GetCurrentSwapResponse,
|
GetCurrentSwapResponse,
|
||||||
LabeledMoneroAddress,
|
LabeledMoneroAddress,
|
||||||
GetPendingApprovalsArgs,
|
GetMoneroHistoryResponse,
|
||||||
|
GetMoneroMainAddressResponse,
|
||||||
|
GetMoneroBalanceResponse,
|
||||||
|
SendMoneroArgs,
|
||||||
|
SendMoneroResponse,
|
||||||
|
GetMoneroSyncProgressResponse,
|
||||||
GetPendingApprovalsResponse,
|
GetPendingApprovalsResponse,
|
||||||
|
RejectApprovalArgs,
|
||||||
|
RejectApprovalResponse,
|
||||||
|
SetRestoreHeightArgs,
|
||||||
|
SetRestoreHeightResponse,
|
||||||
|
GetRestoreHeightResponse,
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
import {
|
import {
|
||||||
rpcSetBalance,
|
rpcSetBalance,
|
||||||
rpcSetSwapInfo,
|
rpcSetSwapInfo,
|
||||||
approvalRequestsReplaced,
|
approvalRequestsReplaced,
|
||||||
} from "store/features/rpcSlice";
|
} from "store/features/rpcSlice";
|
||||||
|
import {
|
||||||
|
setMainAddress,
|
||||||
|
setBalance,
|
||||||
|
setSyncProgress,
|
||||||
|
setHistory,
|
||||||
|
} from "store/features/walletSlice";
|
||||||
import { store } from "./store/storeRenderer";
|
import { store } from "./store/storeRenderer";
|
||||||
import { Maker } from "models/apiModel";
|
|
||||||
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
|
import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils";
|
||||||
import { MoneroRecoveryResponse } from "models/rpcModel";
|
import { MoneroRecoveryResponse } from "models/rpcModel";
|
||||||
import { ListSellersResponse } from "../models/tauriModel";
|
import { ListSellersResponse } from "../models/tauriModel";
|
||||||
|
|
@ -417,6 +432,129 @@ export async function getMoneroAddresses(): Promise<GetMoneroAddressesResponse>
|
||||||
return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses");
|
return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getRestoreHeight(): Promise<GetRestoreHeightResponse> {
|
||||||
|
return await invokeNoArgs<GetRestoreHeightResponse>("get_restore_height");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setMoneroRestoreHeight(
|
||||||
|
height: number | Date,
|
||||||
|
): Promise<SetRestoreHeightResponse> {
|
||||||
|
const args: SetRestoreHeightArgs =
|
||||||
|
typeof height === "number"
|
||||||
|
? { type: "Height", height: height }
|
||||||
|
: {
|
||||||
|
type: "Date",
|
||||||
|
height: {
|
||||||
|
year: height.getFullYear(),
|
||||||
|
month: height.getMonth() + 1, // JavaScript months are 0-indexed, but we want 1-indexed
|
||||||
|
day: height.getDate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return await invoke<SetRestoreHeightArgs, SetRestoreHeightResponse>(
|
||||||
|
"set_monero_restore_height",
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMoneroHistory(): Promise<GetMoneroHistoryResponse> {
|
||||||
|
return await invokeNoArgs<GetMoneroHistoryResponse>("get_monero_history");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMoneroMainAddress(): Promise<GetMoneroMainAddressResponse> {
|
||||||
|
return await invokeNoArgs<GetMoneroMainAddressResponse>(
|
||||||
|
"get_monero_main_address",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMoneroBalance(): Promise<GetMoneroBalanceResponse> {
|
||||||
|
return await invokeNoArgs<GetMoneroBalanceResponse>("get_monero_balance");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMonero(
|
||||||
|
args: SendMoneroArgs,
|
||||||
|
): Promise<SendMoneroResponse> {
|
||||||
|
return await invoke<SendMoneroArgs, SendMoneroResponse>("send_monero", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMoneroSyncProgress(): Promise<GetMoneroSyncProgressResponse> {
|
||||||
|
return await invokeNoArgs<GetMoneroSyncProgressResponse>(
|
||||||
|
"get_monero_sync_progress",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallet management functions that handle Redux dispatching
|
||||||
|
export async function initializeMoneroWallet() {
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
addressResponse,
|
||||||
|
balanceResponse,
|
||||||
|
syncProgressResponse,
|
||||||
|
historyResponse,
|
||||||
|
] = await Promise.all([
|
||||||
|
getMoneroMainAddress(),
|
||||||
|
getMoneroBalance(),
|
||||||
|
getMoneroSyncProgress(),
|
||||||
|
getMoneroHistory(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
store.dispatch(setMainAddress(addressResponse.address));
|
||||||
|
store.dispatch(setBalance(balanceResponse));
|
||||||
|
store.dispatch(setSyncProgress(syncProgressResponse));
|
||||||
|
store.dispatch(setHistory(historyResponse));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch Monero wallet data:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendMoneroTransaction(
|
||||||
|
args: SendMoneroArgs,
|
||||||
|
): Promise<SendMoneroResponse> {
|
||||||
|
try {
|
||||||
|
const response = await sendMonero(args);
|
||||||
|
|
||||||
|
// Refresh balance and history after sending - but don't let this block the response
|
||||||
|
Promise.all([
|
||||||
|
getMoneroBalance(),
|
||||||
|
getMoneroHistory(),
|
||||||
|
]).then(([newBalance, newHistory]) => {
|
||||||
|
store.dispatch(setBalance(newBalance));
|
||||||
|
store.dispatch(setHistory(newHistory));
|
||||||
|
}).catch(refreshErr => {
|
||||||
|
console.error("Failed to refresh wallet data after send:", refreshErr);
|
||||||
|
// Could emit a toast notification here
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to send Monero:", err);
|
||||||
|
throw err; // ✅ Re-throw so caller can handle appropriately
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshWalletDataAfterTransaction() {
|
||||||
|
try {
|
||||||
|
const [newBalance, newHistory] = await Promise.all([
|
||||||
|
getMoneroBalance(),
|
||||||
|
getMoneroHistory(),
|
||||||
|
]);
|
||||||
|
store.dispatch(setBalance(newBalance));
|
||||||
|
store.dispatch(setHistory(newHistory));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to refresh wallet data after transaction:", err);
|
||||||
|
// Maybe show a non-blocking notification to user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateMoneroSyncProgress() {
|
||||||
|
try {
|
||||||
|
const response = await getMoneroSyncProgress();
|
||||||
|
store.dispatch(setSyncProgress(response));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch sync progress:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getDataDir(): Promise<string> {
|
export async function getDataDir(): Promise<string> {
|
||||||
const testnet = isTestnet();
|
const testnet = isTestnet();
|
||||||
return await invoke<GetDataDirArgs, string>("get_data_dir", {
|
return await invoke<GetDataDirArgs, string>("get_data_dir", {
|
||||||
|
|
@ -424,22 +562,37 @@ export async function getDataDir(): Promise<string> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveApproval(
|
export async function resolveApproval<T>(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
accept: object,
|
accept: T,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
|
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
|
||||||
"resolve_approval_request",
|
"resolve_approval_request",
|
||||||
{ request_id: requestId, accept },
|
{ request_id: requestId, accept: accept as object },
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} finally {
|
||||||
// Refresh approval list when resolve fails to keep UI in sync
|
// Always refresh the approval list
|
||||||
await refreshApprovals();
|
await refreshApprovals();
|
||||||
throw error;
|
|
||||||
|
// Refresh the approval list a few miliseconds later to again
|
||||||
|
// Just to make sure :)
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshApprovals();
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function rejectApproval<T>(
|
||||||
|
requestId: string,
|
||||||
|
reject: T,
|
||||||
|
): Promise<void> {
|
||||||
|
await invoke<RejectApprovalArgs, RejectApprovalResponse>(
|
||||||
|
"reject_approval_request",
|
||||||
|
{ request_id: requestId },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function refreshApprovals(): Promise<void> {
|
export async function refreshApprovals(): Promise<void> {
|
||||||
const response = await invokeNoArgs<GetPendingApprovalsResponse>(
|
const response = await invokeNoArgs<GetPendingApprovalsResponse>(
|
||||||
"get_pending_approvals",
|
"get_pending_approvals",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import settingsSlice from "./features/settingsSlice";
|
||||||
import nodesSlice from "./features/nodesSlice";
|
import nodesSlice from "./features/nodesSlice";
|
||||||
import conversationsSlice from "./features/conversationsSlice";
|
import conversationsSlice from "./features/conversationsSlice";
|
||||||
import poolSlice from "./features/poolSlice";
|
import poolSlice from "./features/poolSlice";
|
||||||
|
import walletSlice from "./features/walletSlice";
|
||||||
|
|
||||||
export const reducers = {
|
export const reducers = {
|
||||||
swap: swapReducer,
|
swap: swapReducer,
|
||||||
|
|
@ -18,4 +19,5 @@ export const reducers = {
|
||||||
nodes: nodesSlice,
|
nodes: nodesSlice,
|
||||||
conversations: conversationsSlice,
|
conversations: conversationsSlice,
|
||||||
pool: poolSlice,
|
pool: poolSlice,
|
||||||
|
wallet: walletSlice,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,18 @@ export const rpcSlice = createSlice({
|
||||||
backgroundProgressEventRemoved(slice, action: PayloadAction<string>) {
|
backgroundProgressEventRemoved(slice, action: PayloadAction<string>) {
|
||||||
delete slice.state.background[action.payload];
|
delete slice.state.background[action.payload];
|
||||||
},
|
},
|
||||||
|
rpcSetBackgroundItems(
|
||||||
|
slice,
|
||||||
|
action: PayloadAction<{ [key: string]: TauriBackgroundProgress }>,
|
||||||
|
) {
|
||||||
|
slice.state.background = action.payload;
|
||||||
|
},
|
||||||
|
rpcSetApprovalItems(
|
||||||
|
slice,
|
||||||
|
action: PayloadAction<{ [requestId: string]: ApprovalRequest }>,
|
||||||
|
) {
|
||||||
|
slice.state.approvalRequests = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -175,6 +187,8 @@ export const {
|
||||||
approvalRequestsReplaced,
|
approvalRequestsReplaced,
|
||||||
backgroundProgressEventReceived,
|
backgroundProgressEventReceived,
|
||||||
backgroundProgressEventRemoved,
|
backgroundProgressEventRemoved,
|
||||||
|
rpcSetBackgroundItems,
|
||||||
|
rpcSetApprovalItems,
|
||||||
} = rpcSlice.actions;
|
} = rpcSlice.actions;
|
||||||
|
|
||||||
export default rpcSlice.reducer;
|
export default rpcSlice.reducer;
|
||||||
|
|
|
||||||
65
src-gui/src/store/features/walletSlice.ts
Normal file
65
src-gui/src/store/features/walletSlice.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import {
|
||||||
|
GetMoneroBalanceResponse,
|
||||||
|
GetMoneroHistoryResponse,
|
||||||
|
GetMoneroSyncProgressResponse,
|
||||||
|
} from "models/tauriModel";
|
||||||
|
|
||||||
|
interface WalletState {
|
||||||
|
// Wallet data
|
||||||
|
mainAddress: string | null;
|
||||||
|
balance: GetMoneroBalanceResponse | null;
|
||||||
|
syncProgress: GetMoneroSyncProgressResponse | null;
|
||||||
|
history: GetMoneroHistoryResponse | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletSlice {
|
||||||
|
state: WalletState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: WalletSlice = {
|
||||||
|
state: {
|
||||||
|
// Wallet data
|
||||||
|
mainAddress: null,
|
||||||
|
balance: null,
|
||||||
|
syncProgress: null,
|
||||||
|
history: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const walletSlice = createSlice({
|
||||||
|
name: "wallet",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
// Wallet data actions
|
||||||
|
setMainAddress(slice, action: PayloadAction<string>) {
|
||||||
|
slice.state.mainAddress = action.payload;
|
||||||
|
},
|
||||||
|
setBalance(slice, action: PayloadAction<GetMoneroBalanceResponse>) {
|
||||||
|
slice.state.balance = action.payload;
|
||||||
|
},
|
||||||
|
setSyncProgress(
|
||||||
|
slice,
|
||||||
|
action: PayloadAction<GetMoneroSyncProgressResponse>,
|
||||||
|
) {
|
||||||
|
slice.state.syncProgress = action.payload;
|
||||||
|
},
|
||||||
|
setHistory(slice, action: PayloadAction<GetMoneroHistoryResponse>) {
|
||||||
|
slice.state.history = action.payload;
|
||||||
|
},
|
||||||
|
// Reset actions
|
||||||
|
resetWalletState(slice) {
|
||||||
|
slice.state = initialState.state;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setMainAddress,
|
||||||
|
setBalance,
|
||||||
|
setSyncProgress,
|
||||||
|
setHistory,
|
||||||
|
resetWalletState,
|
||||||
|
} = walletSlice.actions;
|
||||||
|
|
||||||
|
export default walletSlice.reducer;
|
||||||
|
|
@ -12,6 +12,10 @@ import {
|
||||||
isPendingSelectMakerApprovalEvent,
|
isPendingSelectMakerApprovalEvent,
|
||||||
haveFundsBeenLocked,
|
haveFundsBeenLocked,
|
||||||
PendingSeedSelectionApprovalRequest,
|
PendingSeedSelectionApprovalRequest,
|
||||||
|
PendingSendMoneroApprovalRequest,
|
||||||
|
isPendingSendMoneroApprovalEvent,
|
||||||
|
PendingPasswordApprovalRequest,
|
||||||
|
isPendingPasswordApprovalEvent,
|
||||||
} from "models/tauriModelExt";
|
} from "models/tauriModelExt";
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
||||||
|
|
@ -207,6 +211,11 @@ export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalReque
|
||||||
return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c));
|
return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePendingSendMoneroApproval(): PendingSendMoneroApprovalRequest[] {
|
||||||
|
const approvals = usePendingApprovals();
|
||||||
|
return approvals.filter((c) => isPendingSendMoneroApprovalEvent(c));
|
||||||
|
}
|
||||||
|
|
||||||
export function usePendingSelectMakerApproval(): PendingSelectMakerApprovalRequest[] {
|
export function usePendingSelectMakerApproval(): PendingSelectMakerApprovalRequest[] {
|
||||||
const approvals = usePendingApprovals();
|
const approvals = usePendingApprovals();
|
||||||
return approvals.filter((c) => isPendingSelectMakerApprovalEvent(c));
|
return approvals.filter((c) => isPendingSelectMakerApprovalEvent(c));
|
||||||
|
|
@ -217,6 +226,11 @@ export function usePendingSeedSelectionApproval(): PendingSeedSelectionApprovalR
|
||||||
return approvals.filter((c) => isPendingSeedSelectionApprovalEvent(c));
|
return approvals.filter((c) => isPendingSeedSelectionApprovalEvent(c));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePendingPasswordApproval(): PendingPasswordApprovalRequest[] {
|
||||||
|
const approvals = usePendingApprovals();
|
||||||
|
return approvals.filter((c) => isPendingPasswordApprovalEvent(c));
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns all the pending background processes
|
/// Returns all the pending background processes
|
||||||
/// In the format [id, {componentName, {type: "Pending", content: {consumed, total}}}]
|
/// In the format [id, {componentName, {type: "Pending", content: {consumed, total}}}]
|
||||||
export function usePendingBackgroundProcesses(): [
|
export function usePendingBackgroundProcesses(): [
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
updateAllNodeStatuses,
|
updateAllNodeStatuses,
|
||||||
fetchSellersAtPresetRendezvousPoints,
|
fetchSellersAtPresetRendezvousPoints,
|
||||||
getSwapInfo,
|
getSwapInfo,
|
||||||
|
initializeMoneroWallet,
|
||||||
} from "renderer/rpc";
|
} from "renderer/rpc";
|
||||||
import logger from "utils/logger";
|
import logger from "utils/logger";
|
||||||
import { contextStatusEventReceived } from "store/features/rpcSlice";
|
import { contextStatusEventReceived } from "store/features/rpcSlice";
|
||||||
|
|
@ -69,6 +70,7 @@ export function createMainListeners() {
|
||||||
checkBitcoinBalance(),
|
checkBitcoinBalance(),
|
||||||
getAllSwapInfos(),
|
getAllSwapInfos(),
|
||||||
fetchSellersAtPresetRendezvousPoints(),
|
fetchSellersAtPresetRendezvousPoints(),
|
||||||
|
initializeMoneroWallet(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ export function piconerosToXmr(piconeros: number): number {
|
||||||
return piconeros / 1000000000000;
|
return piconeros / 1000000000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function xmrToPiconeros(xmr: number): number {
|
||||||
|
return Math.ceil(xmr * 1000000000000);
|
||||||
|
}
|
||||||
|
|
||||||
export function isXmrAddressValid(address: string, stagenet: boolean) {
|
export function isXmrAddressValid(address: string, stagenet: boolean) {
|
||||||
const re = stagenet
|
const re = stagenet
|
||||||
? "^(?:[57][0-9A-Za-z]{94}|[57][0-9A-Za-z]{105})$"
|
? "^(?:[57][0-9A-Za-z]{94}|[57][0-9A-Za-z]{105})$"
|
||||||
|
|
|
||||||
3345
src-gui/yarn.lock
3345
src-gui/yarn.lock
File diff suppressed because it is too large
Load diff
|
|
@ -21,7 +21,6 @@ rustls = { version = "0.23.26", default-features = false, features = ["ring"] }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
swap = { path = "../swap", features = [ "tauri" ] }
|
swap = { path = "../swap", features = [ "tauri" ] }
|
||||||
sysinfo = "=0.32.1"
|
|
||||||
tauri = { version = "^2.0.0", features = [ "config-json5" ] }
|
tauri = { version = "^2.0.0", features = [ "config-json5" ] }
|
||||||
tauri-plugin-clipboard-manager = "^2.0.0"
|
tauri-plugin-clipboard-manager = "^2.0.0"
|
||||||
tauri-plugin-dialog = "2.2.2"
|
tauri-plugin-dialog = "2.2.2"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
"cli:allow-cli-matches",
|
"cli:allow-cli-matches",
|
||||||
"updater:default",
|
"updater:default",
|
||||||
"process:allow-restart",
|
"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::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::result::Result;
|
use std::result::Result;
|
||||||
|
|
@ -10,9 +9,12 @@ use swap::cli::{
|
||||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, CheckElectrumNodeArgs,
|
||||||
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
|
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckSeedArgs,
|
||||||
CheckSeedResponse, ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs,
|
CheckSeedResponse, ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs,
|
||||||
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetPendingApprovalsResponse,
|
GetHistoryArgs, GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs,
|
||||||
GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
|
GetMoneroHistoryArgs, GetMoneroMainAddressArgs, GetMoneroSyncProgressArgs,
|
||||||
ResolveApprovalArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs,
|
||||||
|
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
|
||||||
|
RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs,
|
||||||
|
SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||||
},
|
},
|
||||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
||||||
Context, ContextBuilder,
|
Context, ContextBuilder,
|
||||||
|
|
@ -64,11 +66,11 @@ macro_rules! tauri_command {
|
||||||
($fn_name:ident, $request_name:ident) => {
|
($fn_name:ident, $request_name:ident) => {
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn $fn_name(
|
async fn $fn_name(
|
||||||
context: tauri::State<'_, RwLock<State>>,
|
state: tauri::State<'_, State>,
|
||||||
args: $request_name,
|
args: $request_name,
|
||||||
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
||||||
// Throw error if context is not available
|
// 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)
|
<$request_name as swap::cli::api::request::Request>::request(args, context)
|
||||||
.await
|
.await
|
||||||
|
|
@ -78,10 +80,10 @@ macro_rules! tauri_command {
|
||||||
($fn_name:ident, $request_name:ident, no_args) => {
|
($fn_name:ident, $request_name:ident, no_args) => {
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn $fn_name(
|
async fn $fn_name(
|
||||||
context: tauri::State<'_, RwLock<State>>,
|
state: tauri::State<'_, State>,
|
||||||
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
||||||
// Throw error if context is not available
|
// 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)
|
<$request_name as swap::cli::api::request::Request>::request($request_name {}, context)
|
||||||
.await
|
.await
|
||||||
|
|
@ -92,7 +94,7 @@ macro_rules! tauri_command {
|
||||||
|
|
||||||
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
||||||
struct State {
|
struct State {
|
||||||
pub context: Option<Arc<Context>>,
|
pub context: RwLock<Option<Arc<Context>>>,
|
||||||
pub handle: TauriHandle,
|
pub handle: TauriHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,22 +102,17 @@ impl State {
|
||||||
/// Creates a new State instance with no Context
|
/// Creates a new State instance with no Context
|
||||||
fn new(handle: TauriHandle) -> Self {
|
fn new(handle: TauriHandle) -> Self {
|
||||||
Self {
|
Self {
|
||||||
context: None,
|
context: RwLock::new(None),
|
||||||
handle,
|
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
|
/// Attempts to retrieve the context
|
||||||
/// Returns an error if the context is not available
|
/// Returns an error if the context is not available
|
||||||
fn try_get_context(&self) -> Result<Arc<Context>, String> {
|
fn try_get_context(&self) -> Result<Arc<Context>, String> {
|
||||||
self.context
|
self.context
|
||||||
|
.try_read()
|
||||||
|
.map_err(|_| "Context is being modified".to_string())?
|
||||||
.clone()
|
.clone()
|
||||||
.ok_or("Context not available".to_string())
|
.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
|
// 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
|
// 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 handle = TauriHandle::new(app_handle.clone());
|
||||||
let state = RwLock::new(State::new(handle));
|
let state = State::new(handle);
|
||||||
app_handle.manage::<RwLock<State>>(state);
|
app_handle.manage::<State>(state);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -203,8 +200,16 @@ pub fn run() {
|
||||||
resolve_approval_request,
|
resolve_approval_request,
|
||||||
redact,
|
redact,
|
||||||
save_txt_files,
|
save_txt_files,
|
||||||
|
get_monero_history,
|
||||||
|
get_monero_main_address,
|
||||||
|
get_monero_balance,
|
||||||
|
send_monero,
|
||||||
|
get_monero_sync_progress,
|
||||||
check_seed,
|
check_seed,
|
||||||
get_pending_approvals,
|
get_pending_approvals,
|
||||||
|
set_monero_restore_height,
|
||||||
|
reject_approval_request,
|
||||||
|
get_restore_height
|
||||||
])
|
])
|
||||||
.setup(setup)
|
.setup(setup)
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
|
|
@ -215,18 +220,17 @@ pub fn run() {
|
||||||
// This is necessary to among other things stop the monero-wallet-rpc process
|
// This is necessary to among other things stop the monero-wallet-rpc process
|
||||||
// If the application is forcibly closed, this may not be called.
|
// If the application is forcibly closed, this may not be called.
|
||||||
// TODO: fix that
|
// 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 {
|
if let Some(context) = context_to_cleanup {
|
||||||
Ok(context) => {
|
if let Err(err) = context.cleanup() {
|
||||||
if let Some(context) = context.context.as_ref() {
|
println!("Cleanup failed {}", err);
|
||||||
if let Err(err) = context.cleanup() {
|
|
||||||
println!("Cleanup failed {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("Failed to acquire lock on context: {}", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -246,6 +250,7 @@ tauri_command!(get_logs, GetLogsArgs);
|
||||||
tauri_command!(list_sellers, ListSellersArgs);
|
tauri_command!(list_sellers, ListSellersArgs);
|
||||||
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
||||||
tauri_command!(redact, RedactArgs);
|
tauri_command!(redact, RedactArgs);
|
||||||
|
tauri_command!(send_monero, SendMoneroArgs);
|
||||||
|
|
||||||
// These commands require no arguments
|
// These commands require no arguments
|
||||||
tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args);
|
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_swap_infos_all, GetSwapInfosAllArgs, no_args);
|
||||||
tauri_command!(get_history, GetHistoryArgs, no_args);
|
tauri_command!(get_history, GetHistoryArgs, no_args);
|
||||||
tauri_command!(get_monero_addresses, GetMoneroAddressesArgs, 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!(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
|
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
|
||||||
#[tauri::command]
|
#[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)
|
// 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]
|
#[tauri::command]
|
||||||
async fn check_monero_node(
|
async fn check_monero_node(
|
||||||
args: CheckMoneroNodeArgs,
|
args: CheckMoneroNodeArgs,
|
||||||
_: tauri::State<'_, RwLock<State>>,
|
_: tauri::State<'_, State>,
|
||||||
) -> Result<CheckMoneroNodeResponse, String> {
|
) -> Result<CheckMoneroNodeResponse, String> {
|
||||||
args.request().await.to_string_result()
|
args.request().await.to_string_result()
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +285,7 @@ async fn check_monero_node(
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn check_electrum_node(
|
async fn check_electrum_node(
|
||||||
args: CheckElectrumNodeArgs,
|
args: CheckElectrumNodeArgs,
|
||||||
_: tauri::State<'_, RwLock<State>>,
|
_: tauri::State<'_, State>,
|
||||||
) -> Result<CheckElectrumNodeResponse, String> {
|
) -> Result<CheckElectrumNodeResponse, String> {
|
||||||
args.request().await.to_string_result()
|
args.request().await.to_string_result()
|
||||||
}
|
}
|
||||||
|
|
@ -282,7 +293,7 @@ async fn check_electrum_node(
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn check_seed(
|
async fn check_seed(
|
||||||
args: CheckSeedArgs,
|
args: CheckSeedArgs,
|
||||||
_: tauri::State<'_, RwLock<State>>,
|
_: tauri::State<'_, State>,
|
||||||
) -> Result<CheckSeedResponse, String> {
|
) -> Result<CheckSeedResponse, String> {
|
||||||
args.request().await.to_string_result()
|
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
|
// This is independent of the context to ensure the user can open the directory even if the context cannot
|
||||||
// be initialized (for troubleshooting purposes)
|
// be initialized (for troubleshooting purposes)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_data_dir(
|
async fn get_data_dir(args: GetDataDirArgs, _: tauri::State<'_, State>) -> Result<String, String> {
|
||||||
args: GetDataDirArgs,
|
|
||||||
_: tauri::State<'_, RwLock<State>>,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
Ok(data::data_dir_from(None, args.is_testnet)
|
Ok(data::data_dir_from(None, args.is_testnet)
|
||||||
.to_string_result()?
|
.to_string_result()?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
|
|
@ -349,26 +357,46 @@ async fn save_txt_files(
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn resolve_approval_request(
|
async fn resolve_approval_request(
|
||||||
args: ResolveApprovalArgs,
|
args: ResolveApprovalArgs,
|
||||||
state: tauri::State<'_, RwLock<State>>,
|
state: tauri::State<'_, State>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
println!("Resolving approval request");
|
let request_id = args
|
||||||
let lock = state.read().await;
|
.request_id
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("Invalid request ID '{}': {}", args.request_id, e))?;
|
||||||
|
|
||||||
lock.handle
|
state
|
||||||
.resolve_approval(args.request_id.parse().unwrap(), args.accept)
|
.handle
|
||||||
|
.resolve_approval(request_id, args.accept)
|
||||||
.await
|
.await
|
||||||
.to_string_result()?;
|
.to_string_result()?;
|
||||||
|
|
||||||
Ok(())
|
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]
|
#[tauri::command]
|
||||||
async fn get_pending_approvals(
|
async fn get_pending_approvals(
|
||||||
state: tauri::State<'_, RwLock<State>>,
|
state: tauri::State<'_, State>,
|
||||||
) -> Result<GetPendingApprovalsResponse, String> {
|
) -> Result<GetPendingApprovalsResponse, String> {
|
||||||
let approvals = state
|
let approvals = state
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.handle
|
.handle
|
||||||
.get_pending_approvals()
|
.get_pending_approvals()
|
||||||
.await
|
.await
|
||||||
|
|
@ -382,41 +410,21 @@ async fn get_pending_approvals(
|
||||||
async fn initialize_context(
|
async fn initialize_context(
|
||||||
settings: TauriSettings,
|
settings: TauriSettings,
|
||||||
testnet: bool,
|
testnet: bool,
|
||||||
state: tauri::State<'_, RwLock<State>>,
|
state: tauri::State<'_, State>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// When the app crashes, the monero-wallet-rpc process may not be killed
|
// Lock at the beginning - fail immediately if already locked
|
||||||
// This can lead to issues when the app is restarted
|
let mut context_lock = state
|
||||||
// because the monero-wallet-rpc has a lock on the wallet
|
.context
|
||||||
// this will prevent the newly spawned instance from opening the wallet
|
.try_write()
|
||||||
// To fix this, we kill any running monero-wallet-rpc processes
|
.map_err(|_| "Context is already being initialized".to_string())?;
|
||||||
let sys = sysinfo::System::new_with_specifics(
|
|
||||||
sysinfo::RefreshKind::new().with_processes(sysinfo::ProcessRefreshKind::new()),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (pid, process) in sys.processes() {
|
// Fail if the context is already initialized
|
||||||
if process
|
if context_lock.is_some() {
|
||||||
.name()
|
return Err("Context is already initialized".to_string());
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get app handle and create a Tauri handle
|
// Get tauri handle from the state
|
||||||
let tauri_handle = state
|
let tauri_handle = state.handle.clone();
|
||||||
.try_read()
|
|
||||||
.context("Context is already being initialized")
|
|
||||||
.to_string_result()?
|
|
||||||
.handle
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
// Notify frontend that the context is being initialized
|
// Notify frontend that the context is being initialized
|
||||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Initializing);
|
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Initializing);
|
||||||
|
|
@ -436,11 +444,7 @@ async fn initialize_context(
|
||||||
|
|
||||||
match context_result {
|
match context_result {
|
||||||
Ok(context_instance) => {
|
Ok(context_instance) => {
|
||||||
state
|
*context_lock = Some(Arc::new(context_instance));
|
||||||
.try_write()
|
|
||||||
.context("Context is already being initialized")
|
|
||||||
.to_string_result()?
|
|
||||||
.set_context(Arc::new(context_instance));
|
|
||||||
|
|
||||||
tracing::info!("Context initialized");
|
tracing::info!("Context initialized");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,21 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true }
|
|
||||||
time = "0.3"
|
|
||||||
swap-serde = { path = "../swap-serde" }
|
|
||||||
bitcoin = { workspace = true }
|
|
||||||
monero = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tracing = { workspace = true }
|
bitcoin = { workspace = true }
|
||||||
swap-fs = { path = "../swap-fs" }
|
|
||||||
dialoguer = "0.11"
|
|
||||||
config = { version = "0.14", default-features = false, features = ["toml"] }
|
config = { version = "0.14", default-features = false, features = ["toml"] }
|
||||||
|
dialoguer = "0.11"
|
||||||
libp2p = { workspace = true, features = ["serde"] }
|
libp2p = { workspace = true, features = ["serde"] }
|
||||||
thiserror = { workspace = true }
|
monero = { workspace = true }
|
||||||
rust_decimal = { workspace = true }
|
rust_decimal = { workspace = true }
|
||||||
url = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
swap-fs = { path = "../swap-fs" }
|
||||||
|
swap-serde = { path = "../swap-serde" }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
time = "0.3"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
tracing = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::env::{Mainnet, Testnet};
|
use crate::env::{Mainnet, Testnet};
|
||||||
use swap_fs::{ensure_directory_exists, system_config_dir, system_data_dir};
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use config::ConfigError;
|
use config::ConfigError;
|
||||||
use dialoguer::theme::ColorfulTheme;
|
use dialoguer::theme::ColorfulTheme;
|
||||||
|
|
@ -12,6 +11,7 @@ use std::ffi::OsStr;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use swap_fs::{ensure_directory_exists, system_config_dir, system_data_dir};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub trait GetDefaults {
|
pub trait GetDefaults {
|
||||||
|
|
@ -130,9 +130,15 @@ pub struct Data {
|
||||||
pub struct Network {
|
pub struct Network {
|
||||||
#[serde(deserialize_with = "swap_serde::libp2p::multiaddresses::deserialize")]
|
#[serde(deserialize_with = "swap_serde::libp2p::multiaddresses::deserialize")]
|
||||||
pub listen: Vec<Multiaddr>,
|
pub listen: Vec<Multiaddr>,
|
||||||
#[serde(default, deserialize_with = "swap_serde::libp2p::multiaddresses::deserialize")]
|
#[serde(
|
||||||
|
default,
|
||||||
|
deserialize_with = "swap_serde::libp2p::multiaddresses::deserialize"
|
||||||
|
)]
|
||||||
pub rendezvous_point: Vec<Multiaddr>,
|
pub rendezvous_point: Vec<Multiaddr>,
|
||||||
#[serde(default, deserialize_with = "swap_serde::libp2p::multiaddresses::deserialize")]
|
#[serde(
|
||||||
|
default,
|
||||||
|
deserialize_with = "swap_serde::libp2p::multiaddresses::deserialize"
|
||||||
|
)]
|
||||||
pub external_addresses: Vec<Multiaddr>,
|
pub external_addresses: Vec<Multiaddr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,7 +265,7 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
)
|
)
|
||||||
.interact_text()?;
|
.interact_text()?;
|
||||||
let data_dir = data_dir.as_str().parse()?;
|
let data_dir: PathBuf = data_dir.as_str().parse()?;
|
||||||
|
|
||||||
let target_block = Input::with_theme(&ColorfulTheme::default())
|
let target_block = Input::with_theme(&ColorfulTheme::default())
|
||||||
.with_prompt("How fast should your Bitcoin transactions be confirmed? Your transaction fee will be calculated based on this target. Hit return to use default")
|
.with_prompt("How fast should your Bitcoin transactions be confirmed? Your transaction fee will be calculated based on this target. Hit return to use default")
|
||||||
|
|
@ -373,7 +379,7 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
|
||||||
println!();
|
println!();
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
data: Data { dir: data_dir },
|
data: Data { dir: data_dir},
|
||||||
network: Network {
|
network: Network {
|
||||||
listen: listen_addresses,
|
listen: listen_addresses,
|
||||||
rendezvous_point: rendezvous_points, // keeping the singular key name for backcompat
|
rendezvous_point: rendezvous_points, // keeping the singular key name for backcompat
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
use crate::config::Config as AsbConfig;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::cmp::max;
|
use std::cmp::max;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use time::ext::NumericalStdDuration;
|
use time::ext::NumericalStdDuration;
|
||||||
use crate::config::Config as AsbConfig;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
|
@ -136,8 +136,6 @@ pub fn new(is_testnet: bool, asb_config: &AsbConfig) -> Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
|
pub mod config;
|
||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod config;
|
|
||||||
|
|
@ -20,6 +20,17 @@ pub fn system_data_dir() -> Result<PathBuf> {
|
||||||
.context("Could not generate default system data-dir dir path")
|
.context("Could not generate default system data-dir dir path")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn system_data_dir_eigenwallet(testnet: bool) -> Result<PathBuf> {
|
||||||
|
let application_directory = match testnet {
|
||||||
|
true => "eigenwallet-testnet",
|
||||||
|
false => "eigenwallet",
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectDirs::from("", "", application_directory)
|
||||||
|
.map(|proj_dirs| proj_dirs.data_dir().to_path_buf())
|
||||||
|
.context("Could not generate default system data-dir dir path")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ensure_directory_exists(file: &Path) -> Result<(), std::io::Error> {
|
pub fn ensure_directory_exists(file: &Path) -> Result<(), std::io::Error> {
|
||||||
if let Some(path) = file.parent() {
|
if let Some(path) = file.parent() {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true }
|
|
||||||
monero = { workspace = true }
|
|
||||||
bitcoin = { workspace = true }
|
|
||||||
libp2p = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
url = { workspace = true }
|
|
||||||
hex = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
bitcoin = { workspace = true }
|
||||||
|
hex = { workspace = true }
|
||||||
|
libp2p = { workspace = true }
|
||||||
|
monero = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
use bitcoin::Network;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use bitcoin::{Network};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
#[serde(remote = "Network")]
|
#[serde(remote = "Network")]
|
||||||
|
|
@ -73,4 +73,4 @@ pub mod address_serde {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,4 +37,4 @@ pub mod urls {
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
pub mod monero;
|
|
||||||
pub mod bitcoin;
|
pub mod bitcoin;
|
||||||
|
pub mod electrum;
|
||||||
pub mod libp2p;
|
pub mod libp2p;
|
||||||
pub mod electrum;
|
pub mod monero;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ pub mod multiaddresses {
|
||||||
use serde::de::Unexpected;
|
use serde::de::Unexpected;
|
||||||
use serde::{de, Deserialize, Deserializer};
|
use serde::{de, Deserialize, Deserializer};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Multiaddr>, D::Error>
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Multiaddr>, D::Error>
|
||||||
where
|
where
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
|
|
@ -24,17 +24,17 @@ pub mod multiaddresses {
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
if let Value::String(s) = v {
|
if let Value::String(s) = v {
|
||||||
s.trim().parse().map_err(de::Error::custom)
|
s.trim().parse().map_err(de::Error::custom)
|
||||||
} else {
|
} else {
|
||||||
Err(de::Error::custom("expected a string"))
|
Err(de::Error::custom("expected a string"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(list?)
|
Ok(list?)
|
||||||
}
|
|
||||||
value => Err(de::Error::invalid_type(
|
|
||||||
Unexpected::Other(&value.to_string()),
|
|
||||||
&"a string or array",
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
|
value => Err(de::Error::invalid_type(
|
||||||
|
Unexpected::Other(&value.to_string()),
|
||||||
|
&"a string or array",
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use monero::{Network, Amount};
|
use monero::{Amount, Network};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|
@ -11,6 +11,7 @@ pub enum network {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod private_key {
|
pub mod private_key {
|
||||||
|
use hex;
|
||||||
use monero::consensus::{Decodable, Encodable};
|
use monero::consensus::{Decodable, Encodable};
|
||||||
use monero::PrivateKey;
|
use monero::PrivateKey;
|
||||||
use serde::de::Visitor;
|
use serde::de::Visitor;
|
||||||
|
|
@ -18,7 +19,6 @@ pub mod private_key {
|
||||||
use serde::{de, Deserializer, Serializer};
|
use serde::{de, Deserializer, Serializer};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use hex;
|
|
||||||
|
|
||||||
struct BytesVisitor;
|
struct BytesVisitor;
|
||||||
|
|
||||||
|
|
@ -143,4 +143,4 @@ pub mod address {
|
||||||
};
|
};
|
||||||
validate(address, expected_network)
|
validate(address, expected_network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "081c729a0f1ad6e4ff3e13d6702c946bc4d37d50f40670b4f51d2efcce595aa6"
|
"hash": "081c729a0f1ad6e4ff3e13d6702c946bc4d37d50f40670b4f51d2efcce595aa6"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [true]
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "0d465a17ebbb5761421def759c73cad023c30705d5b41a1399ef79d8d2571d7c"
|
"hash": "0d465a17ebbb5761421def759c73cad023c30705d5b41a1399ef79d8d2571d7c"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [true]
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "1f332be08a5426f3fbcadea4e755d82ff1cdc2690eb464ccc607d3a613fa76a1"
|
"hash": "1f332be08a5426f3fbcadea4e755d82ff1cdc2690eb464ccc607d3a613fa76a1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [true, true]
|
||||||
true,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "5cc61dd0315571bc198401a354cd9431ee68360941f341386cbacf44ea598de8"
|
"hash": "5cc61dd0315571bc198401a354cd9431ee68360941f341386cbacf44ea598de8"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 0
|
"Right": 0
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false, false]
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6"
|
"hash": "6130b6cdd184181f890964eb460741f5cf23b5237fb676faed009106627a4ca6"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "88f761a4f7a0429cad1df0b1bebb1c0a27b2a45656549b23076d7542cfa21ecf"
|
"hash": "88f761a4f7a0429cad1df0b1bebb1c0a27b2a45656549b23076d7542cfa21ecf"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "d78acba5eb8563826dd190e0886aa665aae3c6f1e312ee444e65df1c95afe8b2"
|
"hash": "d78acba5eb8563826dd190e0886aa665aae3c6f1e312ee444e65df1c95afe8b2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [true, false, false]
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "dff8b986c3dde27b8121775e48a58564fa346b038866699210a63f8a33b03f0b"
|
"hash": "dff8b986c3dde27b8121775e48a58564fa346b038866699210a63f8a33b03f0b"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "e05620f420f8c1022971eeb66a803323a8cf258cbebb2834e3f7cf8f812fa646"
|
"hash": "e05620f420f8c1022971eeb66a803323a8cf258cbebb2834e3f7cf8f812fa646"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"Right": 1
|
"Right": 1
|
||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [false]
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"hash": "e9d422daf774d099fcbde6c4cda35821da948bd86cc57798b4d8375baf0b51ae"
|
"hash": "e9d422daf774d099fcbde6c4cda35821da948bd86cc57798b4d8375baf0b51ae"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ swap-env = { path = "../swap-env" }
|
||||||
swap-feed = { path = "../swap-feed" }
|
swap-feed = { path = "../swap-feed" }
|
||||||
swap-fs = { path = "../swap-fs" }
|
swap-fs = { path = "../swap-fs" }
|
||||||
swap-serde = { path = "../swap-serde" }
|
swap-serde = { path = "../swap-serde" }
|
||||||
|
throttle = { path = "../throttle" }
|
||||||
tauri = { version = "2.0", features = ["config-json5"], optional = true, default-features = false }
|
tauri = { version = "2.0", features = ["config-json5"], optional = true, default-features = false }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
use swap_env::config::GetDefaults;
|
|
||||||
use crate::bitcoin::{bitcoin_address, Amount};
|
use crate::bitcoin::{bitcoin_address, Amount};
|
||||||
use swap_env::env;
|
|
||||||
use swap_env::env::GetConfig;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bitcoin::address::NetworkUnchecked;
|
use bitcoin::address::NetworkUnchecked;
|
||||||
use bitcoin::Address;
|
use bitcoin::Address;
|
||||||
|
|
@ -9,6 +6,9 @@ use serde::Serialize;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
use swap_env::config::GetDefaults;
|
||||||
|
use swap_env::env;
|
||||||
|
use swap_env::env::GetConfig;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub fn parse_args<I, T>(raw_args: I) -> Result<Arguments>
|
pub fn parse_args<I, T>(raw_args: I) -> Result<Arguments>
|
||||||
|
|
@ -402,7 +402,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_start_command_mapping_mainnet() {
|
fn ensure_start_command_mapping_mainnet() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![BINARY_NAME, "start"];
|
let raw_ars = vec![BINARY_NAME, "start"];
|
||||||
|
|
@ -420,7 +422,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_history_command_mapping_mainnet() {
|
fn ensure_history_command_mapping_mainnet() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![BINARY_NAME, "history"];
|
let raw_ars = vec![BINARY_NAME, "history"];
|
||||||
|
|
@ -440,7 +444,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_balance_command_mapping_mainnet() {
|
fn ensure_balance_command_mapping_mainnet() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![BINARY_NAME, "balance"];
|
let raw_ars = vec![BINARY_NAME, "balance"];
|
||||||
|
|
@ -458,7 +464,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_withdraw_command_mapping_mainnet() {
|
fn ensure_withdraw_command_mapping_mainnet() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
BINARY_NAME,
|
BINARY_NAME,
|
||||||
|
|
@ -484,7 +492,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_cancel_command_mapping_mainnet() {
|
fn ensure_cancel_command_mapping_mainnet() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
|
|
@ -510,7 +520,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_refund_command_mappin_mainnet() {
|
fn ensure_refund_command_mappin_mainnet() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
|
|
@ -536,7 +548,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_punish_command_mapping_mainnet() {
|
fn ensure_punish_command_mapping_mainnet() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
|
|
@ -562,7 +576,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_safely_abort_command_mapping_mainnet() {
|
fn ensure_safely_abort_command_mapping_mainnet() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
|
|
@ -588,7 +604,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_start_command_mapping_for_testnet() {
|
fn ensure_start_command_mapping_for_testnet() {
|
||||||
let default_testnet_conf_path = env::Testnet::get_config_file_defaults().unwrap().config_path;
|
let default_testnet_conf_path = env::Testnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let testnet_env_config = env::Testnet::get_config();
|
let testnet_env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![BINARY_NAME, "--testnet", "start"];
|
let raw_ars = vec![BINARY_NAME, "--testnet", "start"];
|
||||||
|
|
@ -606,7 +624,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_history_command_mapping_testnet() {
|
fn ensure_history_command_mapping_testnet() {
|
||||||
let default_testnet_conf_path = env::Testnet::get_config_file_defaults().unwrap().config_path;
|
let default_testnet_conf_path = env::Testnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let testnet_env_config = env::Testnet::get_config();
|
let testnet_env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![BINARY_NAME, "--testnet", "history"];
|
let raw_ars = vec![BINARY_NAME, "--testnet", "history"];
|
||||||
|
|
@ -626,7 +646,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_balance_command_mapping_testnet() {
|
fn ensure_balance_command_mapping_testnet() {
|
||||||
let default_testnet_conf_path = env::Testnet::get_config_file_defaults().unwrap().config_path;
|
let default_testnet_conf_path = env::Testnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let testnet_env_config = env::Testnet::get_config();
|
let testnet_env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![BINARY_NAME, "--testnet", "balance"];
|
let raw_ars = vec![BINARY_NAME, "--testnet", "balance"];
|
||||||
|
|
@ -644,7 +666,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_export_monero_command_mapping_testnet() {
|
fn ensure_export_monero_command_mapping_testnet() {
|
||||||
let default_testnet_conf_path = env::Testnet::get_config_file_defaults().unwrap().config_path;
|
let default_testnet_conf_path = env::Testnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let testnet_env_config = env::Testnet::get_config();
|
let testnet_env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![BINARY_NAME, "--testnet", "export-monero-wallet"];
|
let raw_ars = vec![BINARY_NAME, "--testnet", "export-monero-wallet"];
|
||||||
|
|
@ -663,7 +687,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_withdraw_command_mapping_testnet() {
|
fn ensure_withdraw_command_mapping_testnet() {
|
||||||
let default_testnet_conf_path = env::Testnet::get_config_file_defaults().unwrap().config_path;
|
let default_testnet_conf_path = env::Testnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let testnet_env_config = env::Testnet::get_config();
|
let testnet_env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
|
|
@ -690,7 +716,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_cancel_command_mapping_testnet() {
|
fn ensure_cancel_command_mapping_testnet() {
|
||||||
let default_testnet_conf_path = env::Testnet::get_config_file_defaults().unwrap().config_path;
|
let default_testnet_conf_path = env::Testnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let testnet_env_config = env::Testnet::get_config();
|
let testnet_env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
|
|
@ -717,7 +745,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_refund_command_mapping_testnet() {
|
fn ensure_refund_command_mapping_testnet() {
|
||||||
let default_testnet_conf_path = env::Testnet::get_config_file_defaults().unwrap().config_path;
|
let default_testnet_conf_path = env::Testnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let testnet_env_config = env::Testnet::get_config();
|
let testnet_env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
|
|
@ -744,7 +774,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_punish_command_mapping_testnet() {
|
fn ensure_punish_command_mapping_testnet() {
|
||||||
let default_testnet_conf_path = env::Testnet::get_config_file_defaults().unwrap().config_path;
|
let default_testnet_conf_path = env::Testnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let testnet_env_config = env::Testnet::get_config();
|
let testnet_env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
|
|
@ -771,7 +803,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_safely_abort_command_mapping_testnet() {
|
fn ensure_safely_abort_command_mapping_testnet() {
|
||||||
let default_testnet_conf_path = env::Testnet::get_config_file_defaults().unwrap().config_path;
|
let default_testnet_conf_path = env::Testnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let testnet_env_config = env::Testnet::get_config();
|
let testnet_env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![
|
let raw_ars = vec![
|
||||||
|
|
@ -798,7 +832,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_disable_timestamp_mapping() {
|
fn ensure_disable_timestamp_mapping() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![BINARY_NAME, "--disable-timestamp", "start"];
|
let raw_ars = vec![BINARY_NAME, "--disable-timestamp", "start"];
|
||||||
|
|
@ -816,7 +852,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ensure_trace_mapping() {
|
fn ensure_trace_mapping() {
|
||||||
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults().unwrap().config_path;
|
let default_mainnet_conf_path = env::Mainnet::get_config_file_defaults()
|
||||||
|
.unwrap()
|
||||||
|
.config_path;
|
||||||
let mainnet_env_config = env::Mainnet::get_config();
|
let mainnet_env_config = env::Mainnet::get_config();
|
||||||
|
|
||||||
let raw_ars = vec![BINARY_NAME, "--trace", "start"];
|
let raw_ars = vec![BINARY_NAME, "--trace", "start"];
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,6 @@ use std::sync::Arc;
|
||||||
use structopt::clap;
|
use structopt::clap;
|
||||||
use structopt::clap::ErrorKind;
|
use structopt::clap::ErrorKind;
|
||||||
use swap::asb::command::{parse_args, Arguments, Command};
|
use swap::asb::command::{parse_args, Arguments, Command};
|
||||||
use swap_env::config::{
|
|
||||||
initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized,
|
|
||||||
};
|
|
||||||
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
|
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
|
||||||
use swap::common::tor::init_tor_client;
|
use swap::common::tor::init_tor_client;
|
||||||
use swap::common::tracing_util::Format;
|
use swap::common::tracing_util::Format;
|
||||||
|
|
@ -40,6 +37,9 @@ use swap::protocol::{Database, State};
|
||||||
use swap::seed::Seed;
|
use swap::seed::Seed;
|
||||||
use swap::{bitcoin, monero};
|
use swap::{bitcoin, monero};
|
||||||
use swap_feed;
|
use swap_feed;
|
||||||
|
use swap_env::config::{
|
||||||
|
initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized,
|
||||||
|
};
|
||||||
use tracing_subscriber::filter::LevelFilter;
|
use tracing_subscriber::filter::LevelFilter;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -133,7 +133,7 @@ pub async fn main() -> Result<()> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let seed = Seed::from_file_or_generate(&config.data.dir, None)
|
let seed = Seed::from_file_or_generate(&config.data.dir)
|
||||||
.await
|
.await
|
||||||
.expect("Could not retrieve/initialize seed");
|
.expect("Could not retrieve/initialize seed");
|
||||||
|
|
||||||
|
|
@ -431,7 +431,7 @@ pub async fn main() -> Result<()> {
|
||||||
let monero_wallet = init_monero_wallet(&config, env_config).await?;
|
let monero_wallet = init_monero_wallet(&config, env_config).await?;
|
||||||
let main_wallet = monero_wallet.main_wallet().await;
|
let main_wallet = monero_wallet.main_wallet().await;
|
||||||
|
|
||||||
let seed = main_wallet.seed().await;
|
let seed = main_wallet.seed().await?;
|
||||||
let creation_height = main_wallet.creation_height().await;
|
let creation_height = main_wallet.creation_height().await;
|
||||||
|
|
||||||
println!("Seed : {seed}");
|
println!("Seed : {seed}");
|
||||||
|
|
@ -474,7 +474,9 @@ async fn init_bitcoin_wallet(
|
||||||
if sync {
|
if sync {
|
||||||
wallet.sync().await?;
|
wallet.sync().await?;
|
||||||
} else {
|
} else {
|
||||||
tracing::info!("Skipping Bitcoin wallet sync because we are only using it for receiving funds");
|
tracing::info!(
|
||||||
|
"Skipping Bitcoin wallet sync because we are only using it for receiving funds"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(wallet)
|
Ok(wallet)
|
||||||
|
|
@ -528,6 +530,7 @@ async fn init_monero_wallet(
|
||||||
env_config.monero_network,
|
env_config.monero_network,
|
||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context("Failed to initialize Monero wallets")?;
|
.context("Failed to initialize Monero wallets")?;
|
||||||
|
|
|
||||||
|
|
@ -481,7 +481,6 @@ pub struct NotThreeWitnesses(usize);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use swap_env::env::{GetConfig, Regtest};
|
|
||||||
use crate::monero::TransferProof;
|
use crate::monero::TransferProof;
|
||||||
use crate::protocol::{alice, bob};
|
use crate::protocol::{alice, bob};
|
||||||
use bitcoin::secp256k1;
|
use bitcoin::secp256k1;
|
||||||
|
|
@ -490,6 +489,7 @@ mod tests {
|
||||||
use monero::PrivateKey;
|
use monero::PrivateKey;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use std::matches;
|
use std::matches;
|
||||||
|
use swap_env::env::{GetConfig, Regtest};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod tauri_bindings;
|
pub mod tauri_bindings;
|
||||||
|
|
||||||
|
use crate::cli::api::tauri_bindings::SeedChoice;
|
||||||
use crate::cli::command::{Bitcoin, Monero};
|
use crate::cli::command::{Bitcoin, Monero};
|
||||||
use crate::common::tor::init_tor_client;
|
use crate::common::tor::init_tor_client;
|
||||||
use crate::common::tracing_util::Format;
|
use crate::common::tracing_util::Format;
|
||||||
use crate::database::{open_db, AccessMode};
|
use crate::database::{open_db, AccessMode};
|
||||||
use crate::monero::Wallets;
|
|
||||||
use swap_env::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet};
|
|
||||||
use swap_fs::system_data_dir;
|
|
||||||
use crate::network::rendezvous::XmrBtcNamespace;
|
use crate::network::rendezvous::XmrBtcNamespace;
|
||||||
use crate::protocol::Database;
|
use crate::protocol::Database;
|
||||||
use crate::seed::Seed;
|
use crate::seed::Seed;
|
||||||
|
|
@ -19,6 +17,8 @@ use std::fmt;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Once};
|
use std::sync::{Arc, Once};
|
||||||
|
use swap_env::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet};
|
||||||
|
use swap_fs::system_data_dir;
|
||||||
use tauri_bindings::{
|
use tauri_bindings::{
|
||||||
MoneroNodeConfig, TauriBackgroundProgress, TauriContextStatusEvent, TauriEmitter, TauriHandle,
|
MoneroNodeConfig, TauriBackgroundProgress, TauriContextStatusEvent, TauriEmitter, TauriHandle,
|
||||||
};
|
};
|
||||||
|
|
@ -40,6 +40,7 @@ pub struct Config {
|
||||||
seed: Option<Seed>,
|
seed: Option<Seed>,
|
||||||
debug: bool,
|
debug: bool,
|
||||||
json: bool,
|
json: bool,
|
||||||
|
log_dir: PathBuf,
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
is_testnet: bool,
|
is_testnet: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +282,12 @@ impl ContextBuilder {
|
||||||
|
|
||||||
/// Takes the builder, initializes the context by initializing the wallets and other components and returns the Context.
|
/// Takes the builder, initializes the context by initializing the wallets and other components and returns the Context.
|
||||||
pub async fn build(self) -> Result<Context> {
|
pub async fn build(self) -> Result<Context> {
|
||||||
let data_dir = &data::data_dir_from(self.data, self.is_testnet)?;
|
// This is the data directory for the eigenwallet (wallet files)
|
||||||
|
let eigenwallet_data_dir = &eigenwallet_data::new(self.is_testnet)?;
|
||||||
|
|
||||||
|
let base_data_dir = &data::data_dir_from(self.data, self.is_testnet)?;
|
||||||
|
let log_dir = base_data_dir.join("logs");
|
||||||
|
let env_config = env_config_from(self.is_testnet);
|
||||||
|
|
||||||
// Initialize logging
|
// Initialize logging
|
||||||
let format = if self.json { Format::Json } else { Format::Raw };
|
let format = if self.json { Format::Json } else { Format::Raw };
|
||||||
|
|
@ -295,7 +301,7 @@ impl ContextBuilder {
|
||||||
let _ = common::tracing_util::init(
|
let _ = common::tracing_util::init(
|
||||||
level_filter,
|
level_filter,
|
||||||
format,
|
format,
|
||||||
data_dir.join("logs"),
|
log_dir.clone(),
|
||||||
self.tauri_handle.clone(),
|
self.tauri_handle.clone(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
@ -308,11 +314,97 @@ impl ContextBuilder {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// These are needed for everything else, and are blocking calls
|
// Start the rpc pool for the monero wallet
|
||||||
let env_config = env_config_from(self.is_testnet);
|
let (server_info, mut status_receiver, pool_handle) =
|
||||||
let seed = &Seed::from_file_or_generate(data_dir.as_path(), self.tauri_handle.clone())
|
monero_rpc_pool::start_server_with_random_port(
|
||||||
|
monero_rpc_pool::config::Config::new_random_port(
|
||||||
|
"127.0.0.1".to_string(),
|
||||||
|
base_data_dir.join("monero-rpc-pool"),
|
||||||
|
),
|
||||||
|
match self.is_testnet {
|
||||||
|
true => crate::monero::Network::Stagenet,
|
||||||
|
false => crate::monero::Network::Mainnet,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Listen for pool status updates and forward them to frontend
|
||||||
|
let pool_tauri_handle = self.tauri_handle.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(status) = status_receiver.recv().await {
|
||||||
|
pool_tauri_handle.emit_pool_status_update(status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determine the monero node address to use
|
||||||
|
let (monero_node_address, monero_rpc_pool_handle) = match &self.monero_config {
|
||||||
|
Some(MoneroNodeConfig::Pool) => {
|
||||||
|
let rpc_url = server_info.into();
|
||||||
|
(rpc_url, Some(Arc::new(pool_handle)))
|
||||||
|
}
|
||||||
|
Some(MoneroNodeConfig::SingleNode { url }) => (url.clone(), None),
|
||||||
|
None => {
|
||||||
|
// Default to pool if no monero config is provided
|
||||||
|
let rpc_url = server_info.into();
|
||||||
|
(rpc_url, Some(Arc::new(pool_handle)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a daemon struct for the monero wallet based on the node address
|
||||||
|
let daemon = monero_sys::Daemon {
|
||||||
|
address: monero_node_address,
|
||||||
|
ssl: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize wallet database for tracking recent wallets
|
||||||
|
let wallet_database = monero_sys::Database::new(eigenwallet_data_dir.clone())
|
||||||
.await
|
.await
|
||||||
.context("Failed to read seed in file")?;
|
.context("Failed to initialize wallet database")?;
|
||||||
|
|
||||||
|
// Prompt the user to open/create a Monero wallet
|
||||||
|
let (wallet, seed) = request_and_open_monero_wallet(
|
||||||
|
self.tauri_handle.clone(),
|
||||||
|
eigenwallet_data_dir,
|
||||||
|
base_data_dir,
|
||||||
|
env_config,
|
||||||
|
&daemon,
|
||||||
|
&wallet_database,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let primary_address = wallet.main_address().await;
|
||||||
|
|
||||||
|
// Derive data directory from primary address
|
||||||
|
let data_dir = base_data_dir
|
||||||
|
.join("identities")
|
||||||
|
.join(primary_address.to_string());
|
||||||
|
|
||||||
|
// Ensure the identity directory exists
|
||||||
|
swap_fs::ensure_directory_exists(&data_dir)
|
||||||
|
.context("Failed to create identity directory")?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
primary_address = %primary_address,
|
||||||
|
data_dir = %data_dir.display(),
|
||||||
|
"Using wallet-specific data directory"
|
||||||
|
);
|
||||||
|
|
||||||
|
let wallet_database = Some(Arc::new(wallet_database));
|
||||||
|
|
||||||
|
// Create the monero wallet manager
|
||||||
|
let monero_manager = Some(Arc::new(
|
||||||
|
monero::Wallets::new_with_existing_wallet(
|
||||||
|
eigenwallet_data_dir.to_path_buf(),
|
||||||
|
daemon.clone(),
|
||||||
|
env_config.monero_network,
|
||||||
|
false,
|
||||||
|
self.tauri_handle.clone(),
|
||||||
|
wallet,
|
||||||
|
wallet_database,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to initialize Monero wallets with existing wallet")?,
|
||||||
|
));
|
||||||
|
|
||||||
// Create the data structure we use to manage the swap lock
|
// Create the data structure we use to manage the swap lock
|
||||||
let swap_lock = Arc::new(SwapLock::new());
|
let swap_lock = Arc::new(SwapLock::new());
|
||||||
|
|
@ -350,8 +442,8 @@ impl ContextBuilder {
|
||||||
|
|
||||||
let wallet = init_bitcoin_wallet(
|
let wallet = init_bitcoin_wallet(
|
||||||
urls,
|
urls,
|
||||||
seed,
|
&seed,
|
||||||
data_dir,
|
&data_dir,
|
||||||
env_config,
|
env_config,
|
||||||
target_block,
|
target_block,
|
||||||
self.tauri_handle.clone(),
|
self.tauri_handle.clone(),
|
||||||
|
|
@ -368,68 +460,6 @@ impl ContextBuilder {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let initialize_monero_wallet = async {
|
|
||||||
match self.monero_config {
|
|
||||||
Some(monero_config) => {
|
|
||||||
let monero_progress_handle = tauri_handle
|
|
||||||
.new_background_process_with_initial_progress(
|
|
||||||
TauriBackgroundProgress::OpeningMoneroWallet,
|
|
||||||
(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we are instructed to use a pool, we start it and use it
|
|
||||||
// Otherwise we use the single node address provided by the user
|
|
||||||
let (monero_node_address, rpc_pool_handle) = match monero_config {
|
|
||||||
MoneroNodeConfig::Pool => {
|
|
||||||
// Start RPC pool and use it
|
|
||||||
let (server_info, mut status_receiver, pool_handle) =
|
|
||||||
monero_rpc_pool::start_server_with_random_port(
|
|
||||||
monero_rpc_pool::config::Config::new_random_port(
|
|
||||||
"127.0.0.1".to_string(),
|
|
||||||
data_dir.join("monero-rpc-pool"),
|
|
||||||
),
|
|
||||||
match self.is_testnet {
|
|
||||||
true => crate::monero::Network::Stagenet,
|
|
||||||
false => crate::monero::Network::Mainnet,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let rpc_url =
|
|
||||||
format!("http://{}:{}", server_info.host, server_info.port);
|
|
||||||
tracing::info!("Monero RPC Pool started on {}", rpc_url);
|
|
||||||
|
|
||||||
// Start listening for pool status updates and forward them to frontend
|
|
||||||
if let Some(ref handle) = self.tauri_handle {
|
|
||||||
let pool_tauri_handle = handle.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Ok(status) = status_receiver.recv().await {
|
|
||||||
pool_tauri_handle.emit_pool_status_update(status);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
(rpc_url, Some(Arc::new(pool_handle)))
|
|
||||||
}
|
|
||||||
MoneroNodeConfig::SingleNode { url } => (url, None),
|
|
||||||
};
|
|
||||||
|
|
||||||
let wallets = init_monero_wallet(
|
|
||||||
data_dir.as_path(),
|
|
||||||
monero_node_address,
|
|
||||||
env_config,
|
|
||||||
tauri_handle.clone(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
monero_progress_handle.finish();
|
|
||||||
|
|
||||||
Ok((Some(wallets), rpc_pool_handle))
|
|
||||||
}
|
|
||||||
None => Ok((None, None)),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let initialize_tor_client = async {
|
let initialize_tor_client = async {
|
||||||
// Don't init a tor client unless we should use it.
|
// Don't init a tor client unless we should use it.
|
||||||
if !self.tor {
|
if !self.tor {
|
||||||
|
|
@ -437,7 +467,7 @@ impl ContextBuilder {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let maybe_tor_client = init_tor_client(data_dir, tauri_handle.clone())
|
let maybe_tor_client = init_tor_client(&data_dir, tauri_handle.clone())
|
||||||
.await
|
.await
|
||||||
.inspect_err(|err| {
|
.inspect_err(|err| {
|
||||||
tracing::warn!(%err, "Failed to create Tor client. We will continue without Tor");
|
tracing::warn!(%err, "Failed to create Tor client. We will continue without Tor");
|
||||||
|
|
@ -447,11 +477,8 @@ impl ContextBuilder {
|
||||||
Ok(maybe_tor_client)
|
Ok(maybe_tor_client)
|
||||||
};
|
};
|
||||||
|
|
||||||
let (bitcoin_wallet, (monero_manager, monero_rpc_pool_handle), tor) = tokio::try_join!(
|
let (bitcoin_wallet, tor) =
|
||||||
initialize_bitcoin_wallet,
|
tokio::try_join!(initialize_bitcoin_wallet, initialize_tor_client,)?;
|
||||||
initialize_monero_wallet,
|
|
||||||
initialize_tor_client,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// If we have a bitcoin wallet and a tauri handle, we start a background task
|
// If we have a bitcoin wallet and a tauri handle, we start a background task
|
||||||
if let Some(wallet) = bitcoin_wallet.clone() {
|
if let Some(wallet) = bitcoin_wallet.clone() {
|
||||||
|
|
@ -466,8 +493,6 @@ impl ContextBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
|
|
||||||
|
|
||||||
let context = Context {
|
let context = Context {
|
||||||
db,
|
db,
|
||||||
bitcoin_wallet,
|
bitcoin_wallet,
|
||||||
|
|
@ -480,14 +505,17 @@ impl ContextBuilder {
|
||||||
json: self.json,
|
json: self.json,
|
||||||
is_testnet: self.is_testnet,
|
is_testnet: self.is_testnet,
|
||||||
data_dir: data_dir.clone(),
|
data_dir: data_dir.clone(),
|
||||||
|
log_dir: log_dir.clone(),
|
||||||
},
|
},
|
||||||
swap_lock,
|
swap_lock,
|
||||||
tasks,
|
tasks,
|
||||||
tauri_handle: self.tauri_handle,
|
tauri_handle: self.tauri_handle,
|
||||||
tor_client: tor,
|
tor_client: tor,
|
||||||
monero_rpc_pool_handle,
|
monero_rpc_pool_handle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
|
||||||
|
|
||||||
Ok(context)
|
Ok(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -525,6 +553,15 @@ impl Context {
|
||||||
|
|
||||||
pub fn cleanup(&self) -> Result<()> {
|
pub fn cleanup(&self) -> Result<()> {
|
||||||
// TODO: close all monero wallets
|
// TODO: close all monero wallets
|
||||||
|
// call store(..) on all wallets
|
||||||
|
|
||||||
|
let monero_manager = self.monero_manager.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Some(monero_manager) = monero_manager {
|
||||||
|
let wallet = monero_manager.main_wallet().await;
|
||||||
|
wallet.store(None).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -575,56 +612,227 @@ async fn init_bitcoin_wallet(
|
||||||
Ok(wallet)
|
Ok(wallet)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_monero_wallet(
|
async fn request_and_open_monero_wallet_legacy(
|
||||||
data_dir: &Path,
|
data_dir: &PathBuf,
|
||||||
monero_daemon_address: String,
|
|
||||||
env_config: EnvConfig,
|
env_config: EnvConfig,
|
||||||
tauri_handle: Option<TauriHandle>,
|
daemon: &monero_sys::Daemon,
|
||||||
) -> Result<Arc<Wallets>> {
|
) -> Result<monero_sys::WalletHandle, Error> {
|
||||||
let network = env_config.monero_network;
|
let wallet_path = data_dir.join("swap-tool-blockchain-monitoring-wallet");
|
||||||
let wallet_dir = data_dir.join("monero").join("monero-data");
|
|
||||||
|
|
||||||
let daemon = monero_sys::Daemon {
|
let wallet = monero::Wallet::open_or_create(
|
||||||
address: monero_daemon_address,
|
wallet_path.display().to_string(),
|
||||||
ssl: false,
|
daemon.clone(),
|
||||||
};
|
env_config.monero_network,
|
||||||
|
true,
|
||||||
// This is the name of a wallet we only use for blockchain monitoring
|
|
||||||
const DEFAULT_WALLET: &str = "swap-tool-blockchain-monitoring-wallet";
|
|
||||||
|
|
||||||
// Remove the monitoring wallet if it exists
|
|
||||||
// It doesn't contain any coins
|
|
||||||
// Deleting it ensures we never have issues at startup
|
|
||||||
// And we reset the restore height
|
|
||||||
let wallet_path = wallet_dir.join(DEFAULT_WALLET);
|
|
||||||
if wallet_path.exists() {
|
|
||||||
tracing::debug!(
|
|
||||||
wallet_path = %wallet_path.display(),
|
|
||||||
"Removing monitoring wallet"
|
|
||||||
);
|
|
||||||
let _ = tokio::fs::remove_file(&wallet_path).await;
|
|
||||||
}
|
|
||||||
let keys_path = wallet_path.with_extension("keys");
|
|
||||||
if keys_path.exists() {
|
|
||||||
tracing::debug!(
|
|
||||||
keys_path = %keys_path.display(),
|
|
||||||
"Removing monitoring wallet keys"
|
|
||||||
);
|
|
||||||
let _ = tokio::fs::remove_file(keys_path).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let wallets = monero::Wallets::new(
|
|
||||||
wallet_dir,
|
|
||||||
DEFAULT_WALLET.to_string(),
|
|
||||||
daemon,
|
|
||||||
network,
|
|
||||||
false,
|
|
||||||
tauri_handle,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.context("Failed to initialize Monero wallets")?;
|
.context("Failed to create wallet")?;
|
||||||
|
|
||||||
Ok(Arc::new(wallets))
|
Ok(wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens or creates a Monero wallet after asking the user via the Tauri UI.
|
||||||
|
///
|
||||||
|
/// The user can:
|
||||||
|
/// - Create a new wallet with a random seed.
|
||||||
|
/// - Recover a wallet from a given seed phrase.
|
||||||
|
/// - Open an existing wallet file (with password verification).
|
||||||
|
///
|
||||||
|
/// Errors if the user aborts, provides an incorrect password, or the wallet
|
||||||
|
/// fails to open/create.
|
||||||
|
async fn request_and_open_monero_wallet(
|
||||||
|
tauri_handle: Option<TauriHandle>,
|
||||||
|
eigenwallet_data_dir: &PathBuf,
|
||||||
|
legacy_data_dir: &PathBuf,
|
||||||
|
env_config: EnvConfig,
|
||||||
|
daemon: &monero_sys::Daemon,
|
||||||
|
wallet_database: &monero_sys::Database,
|
||||||
|
) -> Result<(monero_sys::WalletHandle, Seed), Error> {
|
||||||
|
let eigenwallet_wallets_dir = eigenwallet_data_dir.join("wallets");
|
||||||
|
|
||||||
|
let wallet = match tauri_handle {
|
||||||
|
Some(tauri_handle) => {
|
||||||
|
// Get recent wallets from database
|
||||||
|
let recent_wallets: Vec<String> = wallet_database
|
||||||
|
.get_recent_wallets(5)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|w| w.wallet_path)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// This loop continually requests the user to select a wallet file
|
||||||
|
// It then requests the user to provide a password.
|
||||||
|
// It repeats until the user provides a valid password or rejects the password request
|
||||||
|
// When the user rejects the password request, we prompt him to select a wallet again
|
||||||
|
loop {
|
||||||
|
let seed_choice = tauri_handle
|
||||||
|
.request_seed_selection_with_recent_wallets(recent_wallets.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let _monero_progress_handle = tauri_handle
|
||||||
|
.new_background_process_with_initial_progress(
|
||||||
|
TauriBackgroundProgress::OpeningMoneroWallet,
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
|
||||||
|
fn new_wallet_path(eigenwallet_wallets_dir: &PathBuf) -> Result<PathBuf> {
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
let wallet_path = eigenwallet_wallets_dir.join(format!("wallet_{}", timestamp));
|
||||||
|
|
||||||
|
if let Some(parent) = wallet_path.parent() {
|
||||||
|
swap_fs::ensure_directory_exists(parent)
|
||||||
|
.context("Failed to create wallet directory")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(wallet_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
let wallet = match seed_choice {
|
||||||
|
SeedChoice::RandomSeed => {
|
||||||
|
// Create wallet with Unix timestamp as name
|
||||||
|
let wallet_path = new_wallet_path(&eigenwallet_wallets_dir)
|
||||||
|
.context("Failed to determine path for new wallet")?;
|
||||||
|
|
||||||
|
monero::Wallet::open_or_create(
|
||||||
|
wallet_path.display().to_string(),
|
||||||
|
daemon.clone(),
|
||||||
|
env_config.monero_network,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to create wallet from random seed")?
|
||||||
|
}
|
||||||
|
SeedChoice::FromSeed { seed: mnemonic } => {
|
||||||
|
// Create wallet from provided seed
|
||||||
|
let wallet_path = new_wallet_path(&eigenwallet_wallets_dir)
|
||||||
|
.context("Failed to determine path for new wallet")?;
|
||||||
|
|
||||||
|
monero::Wallet::open_or_create_from_seed(
|
||||||
|
wallet_path.display().to_string(),
|
||||||
|
mnemonic,
|
||||||
|
env_config.monero_network,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
daemon.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to create wallet from provided seed")?
|
||||||
|
}
|
||||||
|
SeedChoice::FromWalletPath { wallet_path } => {
|
||||||
|
// Helper function to verify password
|
||||||
|
let verify_password = |password: String| -> Result<bool> {
|
||||||
|
monero_sys::WalletHandle::verify_wallet_password(
|
||||||
|
wallet_path.clone(),
|
||||||
|
password,
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to verify wallet password: {}", e))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Request and verify password before opening wallet
|
||||||
|
let wallet_password: Option<String> = {
|
||||||
|
const WALLET_EMPTY_PASSWORD: &str = "";
|
||||||
|
|
||||||
|
// First try empty password
|
||||||
|
if verify_password(WALLET_EMPTY_PASSWORD.to_string())? {
|
||||||
|
Some(WALLET_EMPTY_PASSWORD.to_string())
|
||||||
|
} else {
|
||||||
|
// If empty password fails, ask user for password
|
||||||
|
loop {
|
||||||
|
// Request password from user
|
||||||
|
let password = tauri_handle
|
||||||
|
.request_password(wallet_path.clone())
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to get password from user: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// If the user rejects the password request (presses cancel)
|
||||||
|
// We prompt him to select a wallet again
|
||||||
|
let password = match password {
|
||||||
|
Some(password) => password,
|
||||||
|
None => break None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Verify the password using the helper function
|
||||||
|
match verify_password(password.clone()) {
|
||||||
|
Ok(true) => {
|
||||||
|
break Some(password);
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
// Continue loop to request password again
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let password = match wallet_password {
|
||||||
|
Some(password) => password,
|
||||||
|
// None means the user rejected the password request
|
||||||
|
// We prompt him to select a wallet again
|
||||||
|
None => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open existing wallet with verified password
|
||||||
|
monero::Wallet::open_or_create_with_password(
|
||||||
|
wallet_path.clone(),
|
||||||
|
password,
|
||||||
|
daemon.clone(),
|
||||||
|
env_config.monero_network,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("Failed to open wallet from provided path")?
|
||||||
|
}
|
||||||
|
|
||||||
|
SeedChoice::Legacy => {
|
||||||
|
let wallet = request_and_open_monero_wallet_legacy(legacy_data_dir, env_config, daemon).await?;
|
||||||
|
let seed = Seed::from_file_or_generate(legacy_data_dir)
|
||||||
|
.await
|
||||||
|
.context("Failed to extract seed from wallet")?;
|
||||||
|
|
||||||
|
break (wallet, seed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract seed from the wallet
|
||||||
|
tracing::info!("Extracting seed from wallet directory: {}", legacy_data_dir.display());
|
||||||
|
let seed = Seed::from_monero_wallet(&wallet)
|
||||||
|
.await
|
||||||
|
.context("Failed to extract seed from wallet")?;
|
||||||
|
|
||||||
|
break (wallet, seed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have a tauri handle, we use the seed.pem file
|
||||||
|
// This is used for the CLI to monitor the blockchain
|
||||||
|
None => {
|
||||||
|
let wallet = request_and_open_monero_wallet_legacy(legacy_data_dir, env_config, daemon).await?;
|
||||||
|
let seed = Seed::from_file_or_generate(legacy_data_dir)
|
||||||
|
.await
|
||||||
|
.context("Failed to extract seed from wallet")?;
|
||||||
|
|
||||||
|
(wallet, seed)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(wallet)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod data {
|
pub mod data {
|
||||||
|
|
@ -646,6 +854,16 @@ pub mod data {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod eigenwallet_data {
|
||||||
|
use swap_fs::system_data_dir_eigenwallet;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn new(testnet: bool) -> Result<PathBuf> {
|
||||||
|
Ok(system_data_dir_eigenwallet(testnet)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn env_config_from(testnet: bool) -> EnvConfig {
|
fn env_config_from(testnet: bool) -> EnvConfig {
|
||||||
if testnet {
|
if testnet {
|
||||||
Testnet::get_config()
|
Testnet::get_config()
|
||||||
|
|
@ -657,6 +875,7 @@ fn env_config_from(testnet: bool) -> EnvConfig {
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn for_harness(seed: Seed, env_config: EnvConfig) -> Self {
|
pub fn for_harness(seed: Seed, env_config: EnvConfig) -> Self {
|
||||||
let data_dir = data::data_dir_from(None, false).expect("Could not find data directory");
|
let data_dir = data::data_dir_from(None, false).expect("Could not find data directory");
|
||||||
|
let log_dir = data_dir.join("logs"); // not used in production
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
namespace: XmrBtcNamespace::from_is_testnet(false),
|
namespace: XmrBtcNamespace::from_is_testnet(false),
|
||||||
|
|
@ -666,6 +885,7 @@ impl Config {
|
||||||
json: false,
|
json: false,
|
||||||
is_testnet: false,
|
is_testnet: false,
|
||||||
data_dir,
|
data_dir,
|
||||||
|
log_dir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -707,7 +927,8 @@ pub mod api_test {
|
||||||
json: bool,
|
json: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let data_dir = data::data_dir_from(data_dir, is_testnet).unwrap();
|
let data_dir = data::data_dir_from(data_dir, is_testnet).unwrap();
|
||||||
let seed = Seed::from_file_or_generate(data_dir.as_path(), None)
|
let log_dir = data_dir.clone().join("logs");
|
||||||
|
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let env_config = env_config_from(is_testnet);
|
let env_config = env_config_from(is_testnet);
|
||||||
|
|
@ -720,6 +941,7 @@ pub mod api_test {
|
||||||
json,
|
json,
|
||||||
is_testnet,
|
is_testnet,
|
||||||
data_dir,
|
data_dir,
|
||||||
|
log_dir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
use super::tauri_bindings::TauriHandle;
|
use super::tauri_bindings::TauriHandle;
|
||||||
use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock};
|
use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock};
|
||||||
use crate::cli::api::tauri_bindings::{SelectMakerDetails, TauriEmitter, TauriSwapProgressEvent};
|
use crate::cli::api::tauri_bindings::{
|
||||||
|
ApprovalRequestType, SelectMakerDetails, SendMoneroDetails, TauriEmitter,
|
||||||
|
TauriSwapProgressEvent,
|
||||||
|
};
|
||||||
use crate::cli::api::Context;
|
use crate::cli::api::Context;
|
||||||
use crate::cli::list_sellers::{list_sellers_init, QuoteWithAddress, UnreachableSeller};
|
use crate::cli::list_sellers::{list_sellers_init, QuoteWithAddress, UnreachableSeller};
|
||||||
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus};
|
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, SellerStatus};
|
||||||
|
|
@ -30,6 +33,7 @@ use serde_json::json;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
@ -417,7 +421,7 @@ impl Request for GetLogsArgs {
|
||||||
type Response = GetLogsResponse;
|
type Response = GetLogsResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let dir = self.logs_dir.unwrap_or(ctx.config.data_dir.join("logs"));
|
let dir = self.logs_dir.unwrap_or(ctx.config.log_dir.clone());
|
||||||
let logs = get_logs(dir, self.swap_id, self.redact).await?;
|
let logs = get_logs(dir, self.swap_id, self.redact).await?;
|
||||||
|
|
||||||
for msg in &logs {
|
for msg in &logs {
|
||||||
|
|
@ -451,6 +455,32 @@ impl Request for RedactArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct GetRestoreHeightArgs;
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct GetRestoreHeightResponse {
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
pub height: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request for GetRestoreHeightArgs {
|
||||||
|
type Response = GetRestoreHeightResponse;
|
||||||
|
|
||||||
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
|
let wallet = ctx
|
||||||
|
.monero_manager
|
||||||
|
.as_ref()
|
||||||
|
.context("Monero wallet manager not available")?;
|
||||||
|
let wallet = wallet.main_wallet().await;
|
||||||
|
let height = wallet.get_restore_height().await?;
|
||||||
|
|
||||||
|
Ok(GetRestoreHeightResponse { height })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct GetMoneroAddressesArgs;
|
pub struct GetMoneroAddressesArgs;
|
||||||
|
|
@ -471,6 +501,282 @@ impl Request for GetMoneroAddressesArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct GetMoneroHistoryArgs;
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Clone, Deserialize, Debug)]
|
||||||
|
pub struct GetMoneroHistoryResponse {
|
||||||
|
pub transactions: Vec<monero_sys::TransactionInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request for GetMoneroHistoryArgs {
|
||||||
|
type Response = GetMoneroHistoryResponse;
|
||||||
|
|
||||||
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
|
let wallet = ctx
|
||||||
|
.monero_manager
|
||||||
|
.as_ref()
|
||||||
|
.context("Monero wallet manager not available")?;
|
||||||
|
let wallet = wallet.main_wallet().await;
|
||||||
|
|
||||||
|
let transactions = wallet.history().await;
|
||||||
|
Ok(GetMoneroHistoryResponse { transactions })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct GetMoneroMainAddressArgs;
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct GetMoneroMainAddressResponse {
|
||||||
|
#[typeshare(serialized_as = "String")]
|
||||||
|
pub address: monero::Address,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request for GetMoneroMainAddressArgs {
|
||||||
|
type Response = GetMoneroMainAddressResponse;
|
||||||
|
|
||||||
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
|
let wallet = ctx
|
||||||
|
.monero_manager
|
||||||
|
.as_ref()
|
||||||
|
.context("Monero wallet manager not available")?;
|
||||||
|
let wallet = wallet.main_wallet().await;
|
||||||
|
let address = wallet.main_address().await;
|
||||||
|
Ok(GetMoneroMainAddressResponse { address })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct Date {
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
pub year: u16,
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
pub month: u8,
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
pub day: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(tag = "type", content = "height")]
|
||||||
|
pub enum SetRestoreHeightArgs {
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
Height(u32),
|
||||||
|
#[typeshare(serialized_as = "object")]
|
||||||
|
Date(Date),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SetRestoreHeightResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request for SetRestoreHeightArgs {
|
||||||
|
type Response = SetRestoreHeightResponse;
|
||||||
|
|
||||||
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
|
let wallet = ctx
|
||||||
|
.monero_manager
|
||||||
|
.as_ref()
|
||||||
|
.context("Monero wallet manager not available")?;
|
||||||
|
let wallet = wallet.main_wallet().await;
|
||||||
|
|
||||||
|
let height = match self {
|
||||||
|
SetRestoreHeightArgs::Height(height) => height as u64,
|
||||||
|
SetRestoreHeightArgs::Date(date) => {
|
||||||
|
let year: u16 = date.year;
|
||||||
|
let month: u8 = date.month;
|
||||||
|
let day: u8 = date.day;
|
||||||
|
|
||||||
|
// Validate ranges
|
||||||
|
if month < 1 || month > 12 {
|
||||||
|
bail!("Month must be between 1 and 12");
|
||||||
|
}
|
||||||
|
if day < 1 || day > 31 {
|
||||||
|
bail!("Day must be between 1 and 31");
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"Getting blockchain height for date: {}-{}-{}",
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day
|
||||||
|
);
|
||||||
|
|
||||||
|
let height = wallet
|
||||||
|
.get_blockchain_height_by_date(year, month, day)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Failed to get blockchain height for date {}-{}-{}",
|
||||||
|
year, month, day
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
tracing::info!(
|
||||||
|
"Blockchain height for date {}-{}-{}: {}",
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
height
|
||||||
|
);
|
||||||
|
|
||||||
|
height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
wallet.set_restore_height(height).await?;
|
||||||
|
|
||||||
|
wallet.pause_refresh().await;
|
||||||
|
wallet.stop().await;
|
||||||
|
tracing::debug!("Background refresh stopped");
|
||||||
|
|
||||||
|
wallet.rescan_blockchain_async().await;
|
||||||
|
wallet.start_refresh().await;
|
||||||
|
tracing::info!("Rescanning blockchain from height {} completed", height);
|
||||||
|
|
||||||
|
Ok(SetRestoreHeightResponse { success: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New request type for Monero balance
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct GetMoneroBalanceArgs;
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct GetMoneroBalanceResponse {
|
||||||
|
#[typeshare(serialized_as = "string")]
|
||||||
|
pub total_balance: crate::monero::Amount,
|
||||||
|
#[typeshare(serialized_as = "string")]
|
||||||
|
pub unlocked_balance: crate::monero::Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request for GetMoneroBalanceArgs {
|
||||||
|
type Response = GetMoneroBalanceResponse;
|
||||||
|
|
||||||
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
|
let wallet_manager = ctx
|
||||||
|
.monero_manager
|
||||||
|
.as_ref()
|
||||||
|
.context("Monero wallet manager not available")?;
|
||||||
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
|
|
||||||
|
let total_balance = wallet.total_balance().await;
|
||||||
|
let unlocked_balance = wallet.unlocked_balance().await;
|
||||||
|
|
||||||
|
Ok(GetMoneroBalanceResponse {
|
||||||
|
total_balance: crate::monero::Amount::from_piconero(total_balance.as_pico()),
|
||||||
|
unlocked_balance: crate::monero::Amount::from_piconero(unlocked_balance.as_pico()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SendMoneroArgs {
|
||||||
|
#[typeshare(serialized_as = "String")]
|
||||||
|
pub address: String,
|
||||||
|
pub amount: SendMoneroAmount,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", content = "amount")]
|
||||||
|
pub enum SendMoneroAmount {
|
||||||
|
Sweep,
|
||||||
|
Specific(crate::monero::Amount),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SendMoneroResponse {
|
||||||
|
pub tx_hash: String,
|
||||||
|
pub address: String,
|
||||||
|
pub amount_sent: crate::monero::Amount,
|
||||||
|
pub fee: crate::monero::Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request for SendMoneroArgs {
|
||||||
|
type Response = SendMoneroResponse;
|
||||||
|
|
||||||
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
|
let wallet_manager = ctx
|
||||||
|
.monero_manager
|
||||||
|
.as_ref()
|
||||||
|
.context("Monero wallet manager not available")?;
|
||||||
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
|
|
||||||
|
// Parse the address
|
||||||
|
let address = monero::Address::from_str(&self.address)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid Monero address: {}", e))?;
|
||||||
|
|
||||||
|
let tauri_handle = ctx
|
||||||
|
.tauri_handle()
|
||||||
|
.context("Tauri needs to be available to approve transactions")?;
|
||||||
|
|
||||||
|
// This is a closure that will be called by the monero-sys library to get approval for the transaction
|
||||||
|
// It sends an approval request to the frontend and returns true if the user approves the transaction
|
||||||
|
let approval_callback: Arc<
|
||||||
|
dyn Fn(
|
||||||
|
String,
|
||||||
|
::monero::Amount,
|
||||||
|
::monero::Amount,
|
||||||
|
)
|
||||||
|
-> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send>>
|
||||||
|
+ Send
|
||||||
|
+ Sync,
|
||||||
|
> = std::sync::Arc::new(
|
||||||
|
move |_txid: String, amount: ::monero::Amount, fee: ::monero::Amount| {
|
||||||
|
let tauri_handle = tauri_handle.clone();
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
let details = SendMoneroDetails {
|
||||||
|
address: address.to_string(),
|
||||||
|
amount: amount.into(),
|
||||||
|
fee: fee.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
tauri_handle
|
||||||
|
.request_approval::<bool>(
|
||||||
|
ApprovalRequestType::SendMonero(details),
|
||||||
|
Some(60 * 5),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let amount = match self.amount {
|
||||||
|
SendMoneroAmount::Sweep => None,
|
||||||
|
SendMoneroAmount::Specific(amount) => Some(amount.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// This is the actual call to the monero-sys library to send the transaction
|
||||||
|
// monero-sys will call the approval callback after it has constructed and signed the transaction
|
||||||
|
// once the user approves, the transaction is published
|
||||||
|
let (receipt, amount_sent, fee) = wallet
|
||||||
|
.transfer_with_approval(&address, amount, approval_callback)
|
||||||
|
.await?
|
||||||
|
.context("Transaction was not approved by user")?;
|
||||||
|
|
||||||
|
Ok(SendMoneroResponse {
|
||||||
|
tx_hash: receipt.txid,
|
||||||
|
address: address.to_string(),
|
||||||
|
amount_sent: amount_sent.into(),
|
||||||
|
fee: fee.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(fields(method = "suspend_current_swap"), skip(context))]
|
#[tracing::instrument(fields(method = "suspend_current_swap"), skip(context))]
|
||||||
pub async fn suspend_current_swap(context: Arc<Context>) -> Result<SuspendCurrentSwapResponse> {
|
pub async fn suspend_current_swap(context: Arc<Context>) -> Result<SuspendCurrentSwapResponse> {
|
||||||
let swap_id = context.swap_lock.get_current_swap_id().await;
|
let swap_id = context.swap_lock.get_current_swap_id().await;
|
||||||
|
|
@ -1248,23 +1554,6 @@ pub async fn get_current_swap(context: Arc<Context>) -> Result<GetCurrentSwapRes
|
||||||
Ok(GetCurrentSwapResponse { swap_id })
|
Ok(GetCurrentSwapResponse { swap_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_approval_request(
|
|
||||||
resolve_approval: ResolveApprovalArgs,
|
|
||||||
ctx: Arc<Context>,
|
|
||||||
) -> Result<ResolveApprovalResponse> {
|
|
||||||
let request_id = Uuid::parse_str(&resolve_approval.request_id).context("Invalid request ID")?;
|
|
||||||
|
|
||||||
if let Some(handle) = ctx.tauri_handle.clone() {
|
|
||||||
handle
|
|
||||||
.resolve_approval(request_id, resolve_approval.accept)
|
|
||||||
.await?;
|
|
||||||
} else {
|
|
||||||
bail!("Cannot resolve approval without a Tauri handle");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ResolveApprovalResponse { success: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_quotes_task(
|
pub async fn fetch_quotes_task(
|
||||||
rendezvous_points: Vec<Multiaddr>,
|
rendezvous_points: Vec<Multiaddr>,
|
||||||
namespace: XmrBtcNamespace,
|
namespace: XmrBtcNamespace,
|
||||||
|
|
@ -1638,6 +1927,18 @@ pub struct ResolveApprovalResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RejectApprovalArgs {
|
||||||
|
pub request_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct RejectApprovalResponse {
|
||||||
|
pub success: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct CheckSeedArgs {
|
pub struct CheckSeedArgs {
|
||||||
|
|
@ -1659,6 +1960,42 @@ impl CheckSeedArgs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New request type for Monero sync progress
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct GetMoneroSyncProgressArgs;
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Serialize, Clone, Deserialize, Debug)]
|
||||||
|
pub struct GetMoneroSyncProgressResponse {
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
pub current_block: u64,
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
pub target_block: u64,
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
pub progress_percentage: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request for GetMoneroSyncProgressArgs {
|
||||||
|
type Response = GetMoneroSyncProgressResponse;
|
||||||
|
|
||||||
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
|
let wallet_manager = ctx
|
||||||
|
.monero_manager
|
||||||
|
.as_ref()
|
||||||
|
.context("Monero wallet manager not available")?;
|
||||||
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
|
|
||||||
|
let sync_progress = wallet.call(|wallet| wallet.sync_progress()).await;
|
||||||
|
|
||||||
|
Ok(GetMoneroSyncProgressResponse {
|
||||||
|
current_block: sync_progress.current_block,
|
||||||
|
target_block: sync_progress.target_block,
|
||||||
|
progress_percentage: sync_progress.percentage(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct GetPendingApprovalsResponse {
|
pub struct GetPendingApprovalsResponse {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
use super::request::BalanceResponse;
|
use super::request::BalanceResponse;
|
||||||
use crate::bitcoin;
|
use crate::bitcoin;
|
||||||
|
use crate::cli::api::request::{
|
||||||
|
GetMoneroBalanceResponse, GetMoneroHistoryResponse, GetMoneroSyncProgressResponse,
|
||||||
|
};
|
||||||
use crate::cli::list_sellers::QuoteWithAddress;
|
use crate::cli::list_sellers::QuoteWithAddress;
|
||||||
use crate::monero::MoneroAddressPool;
|
use crate::monero::MoneroAddressPool;
|
||||||
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote};
|
||||||
|
|
@ -31,6 +34,16 @@ pub enum TauriEvent {
|
||||||
Approval(ApprovalRequest),
|
Approval(ApprovalRequest),
|
||||||
BackgroundProgress(TauriBackgroundProgressWrapper),
|
BackgroundProgress(TauriBackgroundProgressWrapper),
|
||||||
PoolStatusUpdate(PoolStatus),
|
PoolStatusUpdate(PoolStatus),
|
||||||
|
MoneroWalletUpdate(MoneroWalletUpdate),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", content = "content")]
|
||||||
|
pub enum MoneroWalletUpdate {
|
||||||
|
BalanceChange(GetMoneroBalanceResponse),
|
||||||
|
SyncProgress(GetMoneroSyncProgressResponse),
|
||||||
|
HistoryUpdate(GetMoneroHistoryResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAURI_UNIFIED_EVENT_NAME: &str = "tauri-unified-event";
|
const TAURI_UNIFIED_EVENT_NAME: &str = "tauri-unified-event";
|
||||||
|
|
@ -62,12 +75,42 @@ pub struct SelectMakerDetails {
|
||||||
pub maker: QuoteWithAddress,
|
pub maker: QuoteWithAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SendMoneroDetails {
|
||||||
|
/// Destination address for the Monero transfer
|
||||||
|
#[typeshare(serialized_as = "string")]
|
||||||
|
pub address: String,
|
||||||
|
/// Amount to send
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
pub amount: monero::Amount,
|
||||||
|
/// Transaction fee
|
||||||
|
#[typeshare(serialized_as = "number")]
|
||||||
|
pub fee: monero::Amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct PasswordRequestDetails {
|
||||||
|
/// The wallet file path that requires a password
|
||||||
|
pub wallet_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type", content = "content")]
|
#[serde(tag = "type", content = "content")]
|
||||||
pub enum SeedChoice {
|
pub enum SeedChoice {
|
||||||
RandomSeed,
|
RandomSeed,
|
||||||
FromSeed { seed: String },
|
FromSeed { seed: String },
|
||||||
|
FromWalletPath { wallet_path: String },
|
||||||
|
Legacy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[typeshare]
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SeedSelectionDetails {
|
||||||
|
/// List of recently used wallet paths
|
||||||
|
pub recent_wallets: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
|
|
@ -90,8 +133,13 @@ pub enum ApprovalRequestType {
|
||||||
/// Contains available makers and swap details.
|
/// Contains available makers and swap details.
|
||||||
SelectMaker(SelectMakerDetails),
|
SelectMaker(SelectMakerDetails),
|
||||||
/// Request seed selection from user.
|
/// Request seed selection from user.
|
||||||
/// User can choose between random seed or provide their own.
|
/// User can choose between random seed, provide their own, or select wallet file.
|
||||||
SeedSelection,
|
SeedSelection(SeedSelectionDetails),
|
||||||
|
/// Request approval for publishing a Monero transaction.
|
||||||
|
SendMonero(SendMoneroDetails),
|
||||||
|
/// Request password for wallet file.
|
||||||
|
/// User must provide password to unlock the selected wallet.
|
||||||
|
PasswordRequest(PasswordRequestDetails),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
|
|
@ -297,9 +345,9 @@ impl TauriHandle {
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
#[cfg(not(feature = "tauri"))]
|
#[cfg(not(feature = "tauri"))]
|
||||||
{
|
{
|
||||||
return Err(anyhow!(
|
Err(anyhow!(
|
||||||
"Cannot resolve approval: Tauri feature not enabled."
|
"Cannot resolve approval: Tauri feature not enabled."
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
|
|
@ -323,26 +371,38 @@ impl TauriHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_pending_approvals(&self) -> Result<Vec<ApprovalRequest>> {
|
pub async fn reject_approval(&self, request_id: Uuid) -> Result<()> {
|
||||||
#[cfg(not(feature = "tauri"))]
|
#[cfg(not(feature = "tauri"))]
|
||||||
{
|
{
|
||||||
return Ok(Vec::new());
|
Err(anyhow!(
|
||||||
|
"Cannot reject approval: Tauri feature not enabled."
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tauri")]
|
#[cfg(feature = "tauri")]
|
||||||
{
|
{
|
||||||
let pending_map = self
|
let mut pending_map = self
|
||||||
.0
|
.0
|
||||||
.pending_approvals
|
.pending_approvals
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
|
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
|
||||||
|
if let Some(mut pending) = pending_map.remove(&request_id) {
|
||||||
|
// Send rejection through oneshot channel
|
||||||
|
if let Some(responder) = pending.responder.take() {
|
||||||
|
let _ = responder.send(serde_json::Value::Null);
|
||||||
|
|
||||||
let approvals: Vec<ApprovalRequest> = pending_map
|
// Emit the rejection event
|
||||||
.values()
|
let mut approval = pending.request.clone();
|
||||||
.map(|pending| pending.request.clone())
|
approval.request_status = RequestStatus::Rejected;
|
||||||
.collect();
|
self.emit_approval(approval);
|
||||||
|
|
||||||
Ok(approvals)
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Approval responder was already consumed"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Approval not found or already handled"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -352,7 +412,9 @@ impl Display for ApprovalRequest {
|
||||||
match self.request {
|
match self.request {
|
||||||
ApprovalRequestType::LockBitcoin(..) => write!(f, "LockBitcoin()"),
|
ApprovalRequestType::LockBitcoin(..) => write!(f, "LockBitcoin()"),
|
||||||
ApprovalRequestType::SelectMaker(..) => write!(f, "SelectMaker()"),
|
ApprovalRequestType::SelectMaker(..) => write!(f, "SelectMaker()"),
|
||||||
ApprovalRequestType::SeedSelection => write!(f, "SeedSelection()"),
|
ApprovalRequestType::SeedSelection(_) => write!(f, "SeedSelection()"),
|
||||||
|
ApprovalRequestType::SendMonero(_) => write!(f, "SendMonero()"),
|
||||||
|
ApprovalRequestType::PasswordRequest(_) => write!(f, "PasswordRequest()"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -373,6 +435,13 @@ pub trait TauriEmitter {
|
||||||
|
|
||||||
async fn request_seed_selection(&self) -> Result<SeedChoice>;
|
async fn request_seed_selection(&self) -> Result<SeedChoice>;
|
||||||
|
|
||||||
|
async fn request_seed_selection_with_recent_wallets(
|
||||||
|
&self,
|
||||||
|
recent_wallets: Vec<String>,
|
||||||
|
) -> Result<SeedChoice>;
|
||||||
|
|
||||||
|
async fn request_password(&self, wallet_path: String) -> Result<String>;
|
||||||
|
|
||||||
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()>;
|
fn emit_tauri_event<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()>;
|
||||||
|
|
||||||
fn emit_unified_event(&self, event: TauriEvent) {
|
fn emit_unified_event(&self, event: TauriEvent) {
|
||||||
|
|
@ -468,7 +537,22 @@ impl TauriEmitter for TauriHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn request_seed_selection(&self) -> Result<SeedChoice> {
|
async fn request_seed_selection(&self) -> Result<SeedChoice> {
|
||||||
self.request_approval(ApprovalRequestType::SeedSelection, None)
|
self.request_seed_selection_with_recent_wallets(vec![])
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn request_seed_selection_with_recent_wallets(
|
||||||
|
&self,
|
||||||
|
recent_wallets: Vec<String>,
|
||||||
|
) -> Result<SeedChoice> {
|
||||||
|
let details = SeedSelectionDetails { recent_wallets };
|
||||||
|
self.request_approval(ApprovalRequestType::SeedSelection(details), None)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn request_password(&self, wallet_path: String) -> Result<String> {
|
||||||
|
let details = PasswordRequestDetails { wallet_path };
|
||||||
|
self.request_approval(ApprovalRequestType::PasswordRequest(details), None)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -541,6 +625,27 @@ impl TauriEmitter for Option<TauriHandle> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn request_seed_selection_with_recent_wallets(
|
||||||
|
&self,
|
||||||
|
recent_wallets: Vec<String>,
|
||||||
|
) -> Result<SeedChoice> {
|
||||||
|
match self {
|
||||||
|
Some(tauri) => {
|
||||||
|
tauri
|
||||||
|
.request_seed_selection_with_recent_wallets(recent_wallets)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
None => bail!("No Tauri handle available"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn request_password(&self, wallet_path: String) -> Result<String> {
|
||||||
|
match self {
|
||||||
|
Some(tauri) => tauri.request_password(wallet_path).await,
|
||||||
|
None => bail!("No Tauri handle available"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn new_background_process<T: Clone>(
|
fn new_background_process<T: Clone>(
|
||||||
&self,
|
&self,
|
||||||
component: fn(PendingCompleted<T>) -> TauriBackgroundProgress,
|
component: fn(PendingCompleted<T>) -> TauriBackgroundProgress,
|
||||||
|
|
@ -566,7 +671,30 @@ impl TauriEmitter for Option<TauriHandle> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A handle for updating a specific background process's progress
|
impl TauriHandle {
|
||||||
|
#[cfg(feature = "tauri")]
|
||||||
|
pub async fn get_pending_approvals(&self) -> Result<Vec<ApprovalRequest>> {
|
||||||
|
let pending_map = self
|
||||||
|
.0
|
||||||
|
.pending_approvals
|
||||||
|
.lock()
|
||||||
|
.map_err(|e| anyhow!("Failed to acquire approval lock: {}", e))?;
|
||||||
|
|
||||||
|
let approvals: Vec<ApprovalRequest> = pending_map
|
||||||
|
.values()
|
||||||
|
.map(|pending| pending.request.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(approvals)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "tauri"))]
|
||||||
|
pub async fn get_pending_approvals(&self) -> Result<Vec<ApprovalRequest>> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A handle for updating a specific background progress's progress
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::bitcoin;
|
||||||
use crate::monero::{Scalar, TransferProof};
|
use crate::monero::{Scalar, TransferProof};
|
||||||
use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason;
|
use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason;
|
||||||
use crate::network::quote::BidQuote;
|
use crate::network::quote::BidQuote;
|
||||||
|
|
@ -7,8 +8,6 @@ use crate::network::{
|
||||||
cooperative_xmr_redeem_after_punish, encrypted_signature, quote, redial, transfer_proof,
|
cooperative_xmr_redeem_after_punish, encrypted_signature, quote, redial, transfer_proof,
|
||||||
};
|
};
|
||||||
use crate::protocol::bob::State2;
|
use crate::protocol::bob::State2;
|
||||||
use crate::bitcoin;
|
|
||||||
use swap_env::env;
|
|
||||||
use anyhow::{anyhow, Error, Result};
|
use anyhow::{anyhow, Error, Result};
|
||||||
use libp2p::request_response::{
|
use libp2p::request_response::{
|
||||||
InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel,
|
InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel,
|
||||||
|
|
@ -17,6 +16,7 @@ use libp2p::swarm::NetworkBehaviour;
|
||||||
use libp2p::{identify, identity, ping, PeerId};
|
use libp2p::{identify, identity, ping, PeerId};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use swap_env::env;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum OutEvent {
|
pub enum OutEvent {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue