Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions monero-sys/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ pub mod ffi {

fn getBlockchainHeightByDate(self: &Wallet, year: u16, month: u8, day: u8) -> Result<u64>;

fn setPassword(self: Pin<&mut Wallet>, password: &CxxString) -> Result<bool>;

/// Rescan the blockchain asynchronously.
fn rescanBlockchainAsync(self: Pin<&mut Wallet>);

Expand Down
45 changes: 43 additions & 2 deletions monero-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,13 +340,42 @@ impl WalletHandle {
background_sync: bool,
daemon: Daemon,
) -> anyhow::Result<Self> {
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<Option<String>>,
network: monero::Network,
restore_height: u64,
background_sync: bool,
daemon: Daemon,
) -> anyhow::Result<Self> {
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,
Expand Down Expand Up @@ -601,6 +630,13 @@ impl WalletHandle {
Ok(())
}

/// Set the restore height of the wallet.
pub async fn set_password(&self, password: String) -> anyhow::Result<bool> {
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<u64> {
self.call(move |wallet| wallet.get_restore_height())
Expand Down Expand Up @@ -1742,6 +1778,11 @@ impl FfiWallet {
.getBlockchainHeightByDate(year, month, day)
}

pub fn set_password(&mut self, password: &str) -> Result<bool, Exception> {
let_cxx_string!(password = password);
self.inner.pinned().setPassword(&password)
}

/// Rescan the blockchain asynchronously.
fn rescan_blockchain_async(&mut self) {
self.inner.pinned().rescanBlockchainAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -37,6 +38,8 @@ export default function SeedSelectionDialog() {
>("RandomSeed");
const [customSeed, setCustomSeed] = useState<string>("");
const [isSeedValid, setIsSeedValid] = useState<boolean>(false);
const [password, setPassword] = useState<string>("");
const [isPasswordValid, setIsPasswordValid] = useState<boolean>(true);
const [walletPath, setWalletPath] = useState<string>("");

const approval = pendingApprovals[0];
Expand Down Expand Up @@ -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<SeedChoice>(approval.request_id, seedChoice);
Expand All @@ -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 (
<Dialog
Expand Down Expand Up @@ -232,6 +238,13 @@ export default function SeedSelectionDialog() {

{selectedOption === "RandomSeed" && (
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<NewPasswordInput
password={password}
setPassword={setPassword}
isPasswordValid={isPasswordValid}
setIsPasswordValid={setIsPasswordValid}
/>

<Typography
variant="body2"
color="text.secondary"
Expand All @@ -250,23 +263,34 @@ export default function SeedSelectionDialog() {
)}

{selectedOption === "FromSeed" && (
<TextField
fullWidth
multiline
rows={3}
label="Enter your seed phrase"
value={customSeed}
onChange={(e) => 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"
: ""
}
/>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<TextField
fullWidth
multiline
autoFocus
rows={3}
label="Enter your seed phrase"
value={customSeed}
onChange={(e) => 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"
: ""
}
/>

<NewPasswordInput
password={password}
setPassword={setPassword}
isPasswordValid={isPasswordValid}
setIsPasswordValid={setIsPasswordValid}
autoFocus={false}
/>
</Box>
)}

{selectedOption === "FromWalletPath" && (
Expand Down
62 changes: 62 additions & 0 deletions src-gui/src/renderer/components/other/NewPasswordInput.tsx
Original file line number Diff line number Diff line change
@@ -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<SetStateAction<string>>;
isPasswordValid: boolean;
setIsPasswordValid: Dispatch<SetStateAction<boolean>>;
autoFocus?: boolean;
}) {
const [passwordRepeat, setPasswordRepeat] = useState<string>("");
const [showPassword, setShowPassword] = useState<boolean>(false);

useEffect(() => {
setIsPasswordValid(password === passwordRepeat);
}, [password, passwordRepeat]);

return (
<>
<TextField
fullWidth
margin="dense"
type={showPassword ? "text" : "password"}
label="Set password (leave blank to go passwordless)"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={!isPasswordValid}
autoFocus={autoFocus}
/>

<TextField
fullWidth
margin="none"
type={showPassword ? "text" : "password"}
label="Repeat password"
value={passwordRepeat}
onChange={(e) => setPasswordRepeat(e.target.value)}
error={!isPasswordValid}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => setShowPassword(!showPassword)}
edge="end"
aria-label="toggle password visibility"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
</>
);
}
45 changes: 45 additions & 0 deletions src-gui/src/renderer/components/pages/monero/SetPasswordModal.tsx
Original file line number Diff line number Diff line change
@@ -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<string>("");
const [isPasswordValid, setIsPasswordValid] = useState<boolean>(true);

return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Change Password</DialogTitle>
<DialogContent sx={{ borderTop: "1em" }}>
<NewPasswordInput
password={password}
setPassword={setPassword}
isPasswordValid={isPasswordValid}
setIsPasswordValid={setIsPasswordValid}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<PromiseInvokeButton
disabled={!isPasswordValid}
onInvoke={async () => await setMoneroWalletPassword(password)}
onSuccess={onClose}
displayErrorSnackbar={true}
contextRequirement={isContextWithMoneroWallet}
>
Confirm
</PromiseInvokeButton>
</DialogActions>
</Dialog>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ 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";
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";
Expand All @@ -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);
Expand All @@ -66,6 +69,10 @@ export default function WalletActionButtons({
open={restoreHeightDialogOpen}
onClose={() => setRestoreHeightDialogOpen(false)}
/>
<SetPasswordModal
open={setPasswordDialogOpen}
onClose={() => setSetPasswordDialogOpen(false)}
/>
<SeedPhraseModal onClose={() => setSeedPhrase(null)} seed={seedPhrase} />
<SendTransactionModal
balance={balance}
Expand Down Expand Up @@ -121,6 +128,17 @@ export default function WalletActionButtons({
onMenuClose={handleMenuClose}
onSeedPhraseSuccess={setSeedPhrase}
/>
<MenuItem
onClick={() => {
setSetPasswordDialogOpen(true);
handleMenuClose();
}}
>
<ListItemIcon>
<LockOutlineIcon />
</ListItemIcon>
<Typography>Change Password</Typography>
</MenuItem>
</Menu>
</Box>
</Box>
Expand Down
11 changes: 11 additions & 0 deletions src-gui/src/renderer/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import {
ContextStatus,
GetSwapTimelockArgs,
GetSwapTimelockResponse,
SetMoneroWalletPasswordResponse,
SetMoneroWalletPasswordArgs,
} from "models/tauriModel";
import {
rpcSetSwapInfo,
Expand Down Expand Up @@ -533,6 +535,15 @@ export async function setMoneroRestoreHeight(
);
}

export async function setMoneroWalletPassword(
password: string,
): Promise<SetMoneroWalletPasswordResponse> {
return await invoke<SetMoneroWalletPasswordArgs, SetMoneroWalletPasswordResponse>(
"set_monero_wallet_password",
{ password },
);
}

export async function getMoneroHistory(): Promise<GetMoneroHistoryResponse> {
return await invokeNoArgs<GetMoneroHistoryResponse>("get_monero_history");
}
Expand Down
Loading
Loading