From cf11a7fa274569c9a0edd71cfb720b289062b670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 12 Nov 2025 00:12:16 +0100 Subject: [PATCH 1/3] refactor(GUI): put SeedSelectionDialog FromSeed in its own box too --- .../seed-selection/SeedSelectionDialog.tsx | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src-gui/src/renderer/components/modal/seed-selection/SeedSelectionDialog.tsx b/src-gui/src/renderer/components/modal/seed-selection/SeedSelectionDialog.tsx index 9016c2140..eb139ab24 100644 --- a/src-gui/src/renderer/components/modal/seed-selection/SeedSelectionDialog.tsx +++ b/src-gui/src/renderer/components/modal/seed-selection/SeedSelectionDialog.tsx @@ -250,23 +250,25 @@ export default function SeedSelectionDialog() { )} {selectedOption === "FromSeed" && ( - setCustomSeed(e.target.value)} - placeholder="Enter your Monero 25 words seed phrase..." - error={!isSeedValid && customSeed.length > 0} - helperText={ - isSeedValid - ? "Seed is valid" - : customSeed.length > 0 - ? "Seed is invalid" - : "" - } - /> + + setCustomSeed(e.target.value)} + placeholder="Enter your Monero 25 words seed phrase..." + error={!isSeedValid && customSeed.length > 0} + helperText={ + isSeedValid + ? "Seed is valid" + : customSeed.length > 0 + ? "Seed is invalid" + : "" + } + /> + )} {selectedOption === "FromWalletPath" && ( From 77d14b25ecb5e39c903297fd72b38cecbdb71d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 12 Nov 2025 00:44:01 +0100 Subject: [PATCH 2/3] feat(GUI): allow setting passphrase when creating wallets Closes: #689 --- monero-sys/src/lib.rs | 33 +++++++++- .../seed-selection/SeedSelectionDialog.tsx | 34 ++++++++-- .../components/other/NewPasswordInput.tsx | 62 +++++++++++++++++++ swap/src/cli/api.rs | 21 +++++-- swap/src/cli/api/tauri_bindings.rs | 4 +- 5 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 src-gui/src/renderer/components/other/NewPasswordInput.tsx diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs index 4ad1486ca..b1d5bc745 100644 --- a/monero-sys/src/lib.rs +++ b/monero-sys/src/lib.rs @@ -340,13 +340,42 @@ impl WalletHandle { background_sync: bool, daemon: Daemon, ) -> anyhow::Result { + Self::open_or_create_from_seed_with_password( + path, + mnemonic, + None, + network, + restore_height, + background_sync, + daemon, + ) + .await + } + + pub async fn open_or_create_from_seed_with_password( + path: String, + mnemonic: String, + password: impl Into>, + network: monero::Network, + restore_height: u64, + background_sync: bool, + daemon: Daemon, + ) -> anyhow::Result { + let password = password.into(); + Self::open_with(path.clone(), daemon.clone(), move |manager| { if manager.wallet_exists(&path) { - manager.open_or_create_wallet(&path, None, network, background_sync, daemon.clone()) + manager.open_or_create_wallet( + &path, + password.as_ref(), + network, + background_sync, + daemon.clone(), + ) } else { manager.recover_wallet( &path, - None, + password.as_deref(), &mnemonic, network, restore_height, diff --git a/src-gui/src/renderer/components/modal/seed-selection/SeedSelectionDialog.tsx b/src-gui/src/renderer/components/modal/seed-selection/SeedSelectionDialog.tsx index eb139ab24..c9f3203f6 100644 --- a/src-gui/src/renderer/components/modal/seed-selection/SeedSelectionDialog.tsx +++ b/src-gui/src/renderer/components/modal/seed-selection/SeedSelectionDialog.tsx @@ -19,6 +19,7 @@ import { Card, CardContent, } from "@mui/material"; +import NewPasswordInput from "renderer/components/other/NewPasswordInput"; import { useState, useEffect } from "react"; import { usePendingSeedSelectionApproval } from "store/hooks"; import { resolveApproval, checkSeed } from "renderer/rpc"; @@ -37,6 +38,8 @@ export default function SeedSelectionDialog() { >("RandomSeed"); const [customSeed, setCustomSeed] = useState(""); const [isSeedValid, setIsSeedValid] = useState(false); + const [password, setPassword] = useState(""); + const [isPasswordValid, setIsPasswordValid] = useState(true); const [walletPath, setWalletPath] = useState(""); const approval = pendingApprovals[0]; @@ -95,9 +98,9 @@ export default function SeedSelectionDialog() { const seedChoice: SeedChoice = selectedOption === "RandomSeed" - ? { type: "RandomSeed" } + ? { type: "RandomSeed", content: { password } } : selectedOption === "FromSeed" - ? { type: "FromSeed", content: { seed: customSeed } } + ? { type: "FromSeed", content: { seed: customSeed, password } } : { type: "FromWalletPath", content: { wallet_path: walletPath } }; await resolveApproval(approval.request_id, seedChoice); @@ -107,14 +110,17 @@ export default function SeedSelectionDialog() { return null; } - // Disable the button if the user is restoring from a seed and the seed is invalid - // or if selecting wallet path and no path is selected + // Disable the button if the user is restoring from a seed and the seed is invalid, + // if selecting wallet path and no path is selected, + // or if setting a password and they don't match const isDisabled = selectedOption === "FromSeed" - ? customSeed.trim().length === 0 || !isSeedValid + ? customSeed.trim().length === 0 || !isSeedValid || !isPasswordValid : selectedOption === "FromWalletPath" ? !walletPath - : false; + : selectedOption === "RandomSeed" + ? !isPasswordValid + : false; return ( + + + + )} diff --git a/src-gui/src/renderer/components/other/NewPasswordInput.tsx b/src-gui/src/renderer/components/other/NewPasswordInput.tsx new file mode 100644 index 000000000..37ed0b7fe --- /dev/null +++ b/src-gui/src/renderer/components/other/NewPasswordInput.tsx @@ -0,0 +1,62 @@ +import { TextField, IconButton, InputAdornment } from "@mui/material"; +import { useState, useEffect, Dispatch, SetStateAction } from "react"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; + +export default function NewPasswordInput({ + password, + setPassword, + isPasswordValid, + setIsPasswordValid, + autoFocus = true, +}: { + password: string; + setPassword: Dispatch>; + isPasswordValid: boolean; + setIsPasswordValid: Dispatch>; + autoFocus?: boolean; +}) { + const [passwordRepeat, setPasswordRepeat] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + useEffect(() => { + setIsPasswordValid(password === passwordRepeat); + }, [password, passwordRepeat]); + + return ( + <> + setPassword(e.target.value)} + error={!isPasswordValid} + autoFocus={autoFocus} + /> + + setPasswordRepeat(e.target.value)} + error={!isPasswordValid} + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} + edge="end" + aria-label="toggle password visibility" + > + {showPassword ? : } + + + ), + }} + /> + + ); +} diff --git a/swap/src/cli/api.rs b/swap/src/cli/api.rs index 2bc5ec02e..92b4a51ad 100644 --- a/swap/src/cli/api.rs +++ b/swap/src/cli/api.rs @@ -908,13 +908,18 @@ mod wallet { } let wallet = match seed_choice { - SeedChoice::RandomSeed => { + SeedChoice::RandomSeed { password } => { // 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( + monero::Wallet::open_or_create_with_password( wallet_path.display().to_string(), + if password.is_empty() { + None + } else { + Some(password) + }, daemon.clone(), env_config.monero_network, true, @@ -922,14 +927,22 @@ mod wallet { .await .context("Failed to create wallet from random seed")? } - SeedChoice::FromSeed { seed: mnemonic } => { + SeedChoice::FromSeed { + seed: mnemonic, + password, + } => { // 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( + monero::Wallet::open_or_create_from_seed_with_password( wallet_path.display().to_string(), mnemonic, + if password.is_empty() { + None + } else { + Some(password) + }, env_config.monero_network, 0, true, diff --git a/swap/src/cli/api/tauri_bindings.rs b/swap/src/cli/api/tauri_bindings.rs index 2eaa36296..0e5f4462c 100644 --- a/swap/src/cli/api/tauri_bindings.rs +++ b/swap/src/cli/api/tauri_bindings.rs @@ -109,8 +109,8 @@ pub struct PasswordRequestDetails { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "type", content = "content")] pub enum SeedChoice { - RandomSeed, - FromSeed { seed: String }, + RandomSeed { password: String }, + FromSeed { seed: String, password: String }, FromWalletPath { wallet_path: String }, Legacy, } From 5b4ed604ee23152ff616b0edb74b70a8104242e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 12 Nov 2025 20:31:41 +0100 Subject: [PATCH 3/3] feat(GUI): allow setting a password on existing wallet Without saving, we can get the wallet into a state that causes libmonero to throw std::bad_alloc (in the GUI and in feather; opening it a second time in feather fixes it) --- monero-sys/src/bridge.rs | 2 + monero-sys/src/lib.rs | 12 +++++ .../pages/monero/SetPasswordModal.tsx | 45 +++++++++++++++++++ .../monero/components/WalletActionButtons.tsx | 18 ++++++++ src-gui/src/renderer/rpc.ts | 11 +++++ src-tauri/src/commands.rs | 5 ++- swap/src/cli/api/request.rs | 28 ++++++++++++ 7 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src-gui/src/renderer/components/pages/monero/SetPasswordModal.tsx diff --git a/monero-sys/src/bridge.rs b/monero-sys/src/bridge.rs index 29d38ba82..a6ff979e1 100644 --- a/monero-sys/src/bridge.rs +++ b/monero-sys/src/bridge.rs @@ -236,6 +236,8 @@ pub mod ffi { fn getBlockchainHeightByDate(self: &Wallet, year: u16, month: u8, day: u8) -> Result; + fn setPassword(self: Pin<&mut Wallet>, password: &CxxString) -> Result; + /// Rescan the blockchain asynchronously. fn rescanBlockchainAsync(self: Pin<&mut Wallet>); diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs index b1d5bc745..e8d3aa149 100644 --- a/monero-sys/src/lib.rs +++ b/monero-sys/src/lib.rs @@ -630,6 +630,13 @@ impl WalletHandle { Ok(()) } + /// Set the restore height of the wallet. + pub async fn set_password(&self, password: String) -> anyhow::Result { + self.call(move |wallet| wallet.set_password(&password)) + .await + .context("Failed to set password: FFI call failed with exception") + } + /// Get the restore height of the wallet. pub async fn get_restore_height(&self) -> anyhow::Result { self.call(move |wallet| wallet.get_restore_height()) @@ -1771,6 +1778,11 @@ impl FfiWallet { .getBlockchainHeightByDate(year, month, day) } + pub fn set_password(&mut self, password: &str) -> Result { + let_cxx_string!(password = password); + self.inner.pinned().setPassword(&password) + } + /// Rescan the blockchain asynchronously. fn rescan_blockchain_async(&mut self) { self.inner.pinned().rescanBlockchainAsync(); diff --git a/src-gui/src/renderer/components/pages/monero/SetPasswordModal.tsx b/src-gui/src/renderer/components/pages/monero/SetPasswordModal.tsx new file mode 100644 index 000000000..e99956d2d --- /dev/null +++ b/src-gui/src/renderer/components/pages/monero/SetPasswordModal.tsx @@ -0,0 +1,45 @@ +import { Button, Dialog, DialogActions, DialogContent } from "@mui/material"; + +import { DialogTitle } from "@mui/material"; +import { useState } from "react"; +import { setMoneroWalletPassword } from "renderer/rpc"; +import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; +import { isContextWithMoneroWallet } from "models/tauriModelExt"; +import NewPasswordInput from "renderer/components/other/NewPasswordInput"; + +export default function ChangePasswordModal({ + open, + onClose, +}: { + open: boolean; + onClose: () => void; +}) { + const [password, setPassword] = useState(""); + const [isPasswordValid, setIsPasswordValid] = useState(true); + + return ( + + Change Password + + + + + + await setMoneroWalletPassword(password)} + onSuccess={onClose} + displayErrorSnackbar={true} + contextRequirement={isContextWithMoneroWallet} + > + Confirm + + + + ); +} diff --git a/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx b/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx index 31825c6bd..610522328 100644 --- a/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx +++ b/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx @@ -18,6 +18,7 @@ import { SwapHoriz as SwapIcon, Restore as RestoreIcon, MoreHoriz as MoreHorizIcon, + LockOutline as LockOutlineIcon, } from "@mui/icons-material"; import { useState } from "react"; import { setMoneroRestoreHeight } from "renderer/rpc"; @@ -25,6 +26,7 @@ import SendTransactionModal from "../SendTransactionModal"; import { useNavigate } from "react-router-dom"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import SetRestoreHeightModal from "../SetRestoreHeightModal"; +import SetPasswordModal from "../SetPasswordModal"; import SeedPhraseButton from "../SeedPhraseButton"; import SeedPhraseModal from "../SeedPhraseModal"; import DfxButton from "./DFXWidget"; @@ -46,6 +48,7 @@ export default function WalletActionButtons({ const [sendDialogOpen, setSendDialogOpen] = useState(false); const [restoreHeightDialogOpen, setRestoreHeightDialogOpen] = useState(false); + const [setPasswordDialogOpen, setSetPasswordDialogOpen] = useState(false); const [seedPhrase, setSeedPhrase] = useState< [GetMoneroSeedResponse, GetRestoreHeightResponse] | null >(null); @@ -66,6 +69,10 @@ export default function WalletActionButtons({ open={restoreHeightDialogOpen} onClose={() => setRestoreHeightDialogOpen(false)} /> + setSetPasswordDialogOpen(false)} + /> setSeedPhrase(null)} seed={seedPhrase} /> + { + setSetPasswordDialogOpen(true); + handleMenuClose(); + }} + > + + + + Change Password + diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 597f781e3..293ddb0b4 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -50,6 +50,8 @@ import { ContextStatus, GetSwapTimelockArgs, GetSwapTimelockResponse, + SetMoneroWalletPasswordResponse, + SetMoneroWalletPasswordArgs, } from "models/tauriModel"; import { rpcSetSwapInfo, @@ -533,6 +535,15 @@ export async function setMoneroRestoreHeight( ); } +export async function setMoneroWalletPassword( + password: string, +): Promise { + return await invoke( + "set_monero_wallet_password", + { password }, + ); +} + export async function getMoneroHistory(): Promise { return await invokeNoArgs("get_monero_history"); } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 311373e22..44f0285f9 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -14,7 +14,8 @@ use swap::cli::{ GetMoneroSyncProgressArgs, GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs, RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs, - SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs, + SendMoneroArgs, SetMoneroWalletPasswordArgs, SetRestoreHeightArgs, + SuspendCurrentSwapArgs, WithdrawBtcArgs, }, tauri_bindings::{ContextStatus, TauriSettings}, ContextBuilder, @@ -68,6 +69,7 @@ macro_rules! generate_command_handlers { set_monero_restore_height, reject_approval_request, get_restore_height, + set_monero_wallet_password, dfx_authenticate, change_monero_node, get_context_status @@ -447,6 +449,7 @@ tauri_command!(get_monero_history, GetMoneroHistoryArgs, no_args); tauri_command!(get_current_swap, GetCurrentSwapArgs, no_args); tauri_command!(set_monero_restore_height, SetRestoreHeightArgs); tauri_command!(get_restore_height, GetRestoreHeightArgs, no_args); +tauri_command!(set_monero_wallet_password, SetMoneroWalletPasswordArgs); 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); diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs index f3240f2a4..876c170e3 100644 --- a/swap/src/cli/api/request.rs +++ b/swap/src/cli/api/request.rs @@ -683,6 +683,34 @@ impl Request for SetRestoreHeightArgs { } } +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct SetMoneroWalletPasswordArgs { + pub password: String, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct SetMoneroWalletPasswordResponse { + pub success: bool, +} + +impl Request for SetMoneroWalletPasswordArgs { + type Response = SetMoneroWalletPasswordResponse; + + async fn request(self, ctx: Arc) -> Result { + let wallet_manager = ctx.try_get_monero_manager().await?; + let wallet = wallet_manager.main_wallet().await; + + let success = wallet.set_password(self.password).await?; + if success { + wallet.store_in_current_file().await?; + } + + Ok(SetMoneroWalletPasswordResponse { success }) + } +} + // New request type for Monero balance #[typeshare] #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]