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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- ASB + CONTROLLER: Add a `monero_seed` command to the controller shell. You can use it to export the seed and restore height of the internal Monero wallet. You can use those to import the wallet into a wallet software of your own choosing.
- GUI: You can now change the Monero Node without having to restart.
- GUI: You can now export the seed phrase of the Monero wallet.

## [3.0.0-beta.8] - 2025-08-10

Expand Down
113 changes: 74 additions & 39 deletions src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Props = {
displayCopyIcon?: boolean;
enableQrCode?: boolean;
light?: boolean;
spoilerText?: string;
};

function QRCodeModal({ open, onClose, content }: ModalProps) {
Expand Down Expand Up @@ -63,10 +64,12 @@ export default function ActionableMonospaceTextBox({
displayCopyIcon = true,
enableQrCode = true,
light = false,
spoilerText,
}: Props) {
const [copied, setCopied] = useState(false);
const [qrCodeOpen, setQrCodeOpen] = useState(false);
const [isQrCodeButtonHovered, setIsQrCodeButtonHovered] = useState(false);
const [isRevealed, setIsRevealed] = useState(!spoilerText);

const handleCopy = async () => {
await writeText(content);
Expand All @@ -76,52 +79,84 @@ export default function ActionableMonospaceTextBox({

return (
<>
<Tooltip
title={
isQrCodeButtonHovered
? ""
: copied
? "Copied to clipboard"
: "Click to copy"
}
arrow
>
<Box
sx={{
display: "flex",
alignItems: "center",
cursor: "pointer",
}}
<Box sx={{ position: "relative" }}>
<Tooltip
title={
isQrCodeButtonHovered
? ""
: copied
? "Copied to clipboard"
: "Click to copy"
}
arrow
>
<Box sx={{ flexGrow: 1 }} onClick={handleCopy}>
<MonospaceTextBox light={light}>
{content}
{displayCopyIcon && (
<IconButton
onClick={handleCopy}
size="small"
sx={{ marginLeft: 1 }}
>
<FileCopyOutlined />
</IconButton>
)}
{enableQrCode && (
<Tooltip title="Show QR Code" arrow>
<Box
sx={{
display: "flex",
alignItems: "center",
cursor: "pointer",
filter: spoilerText && !isRevealed ? "blur(8px)" : "none",
transition: "filter 0.3s ease",
}}
>
<Box sx={{ flexGrow: 1 }} onClick={handleCopy}>
<MonospaceTextBox light={light}>
{content}
{displayCopyIcon && (
<IconButton
onClick={() => setQrCodeOpen(true)}
onMouseEnter={() => setIsQrCodeButtonHovered(true)}
onMouseLeave={() => setIsQrCodeButtonHovered(false)}
onClick={handleCopy}
size="small"
sx={{ marginLeft: 1 }}
>
<QrCodeIcon />
<FileCopyOutlined />
</IconButton>
</Tooltip>
)}
</MonospaceTextBox>
)}
{enableQrCode && (
<Tooltip title="Show QR Code" arrow>
<IconButton
onClick={() => setQrCodeOpen(true)}
onMouseEnter={() => setIsQrCodeButtonHovered(true)}
onMouseLeave={() => setIsQrCodeButtonHovered(false)}
size="small"
sx={{ marginLeft: 1 }}
>
<QrCodeIcon />
</IconButton>
</Tooltip>
)}
</MonospaceTextBox>
</Box>
</Box>
</Tooltip>

{spoilerText && !isRevealed && (
<Box
onClick={() => setIsRevealed(true)}
sx={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
bgcolor: "rgba(0, 0, 0, 0.1)",
borderRadius: 1,
}}
>
<Box
sx={{
bgcolor: "background.paper",
p: 2,
borderRadius: 1,
boxShadow: 2,
}}
>
{spoilerText}
</Box>
</Box>
</Box>
</Tooltip>
)}
</Box>

{enableQrCode && (
<QRCodeModal
open={qrCodeOpen}
Expand Down
51 changes: 51 additions & 0 deletions src-gui/src/renderer/components/pages/monero/SeedPhraseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ListItemIcon, MenuItem, Typography } from "@mui/material";
import { Key as KeyIcon } from "@mui/icons-material";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { getMoneroSeedAndRestoreHeight } from "renderer/rpc";
import {
GetMoneroSeedResponse,
GetRestoreHeightResponse,
} from "models/tauriModel";

interface SeedPhraseButtonProps {
onMenuClose: () => void;
onSeedPhraseSuccess: (
response: [GetMoneroSeedResponse, GetRestoreHeightResponse],
) => void;
}

export default function SeedPhraseButton({
onMenuClose,
onSeedPhraseSuccess,
}: SeedPhraseButtonProps) {
const handleSeedPhraseSuccess = (
response: [GetMoneroSeedResponse, GetRestoreHeightResponse],
) => {
onSeedPhraseSuccess(response);
onMenuClose();
};

return (
<MenuItem component="div">
<PromiseInvokeButton
onInvoke={getMoneroSeedAndRestoreHeight}
onSuccess={handleSeedPhraseSuccess}
displayErrorSnackbar={true}
variant="text"
sx={{
justifyContent: "flex-start",
textTransform: "none",
padding: 0,
minHeight: "auto",
width: "100%",
color: "text.primary",
}}
>
<ListItemIcon>
<KeyIcon />
</ListItemIcon>
<Typography>Seedphrase</Typography>
</PromiseInvokeButton>
</MenuItem>
);
}
64 changes: 64 additions & 0 deletions src-gui/src/renderer/components/pages/monero/SeedPhraseModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Typography,
} from "@mui/material";
import ActionableMonospaceTextBox from "../../other/ActionableMonospaceTextBox";
import {
GetMoneroSeedResponse,
GetRestoreHeightResponse,
} from "models/tauriModel";

interface SeedPhraseModalProps {
onClose: () => void;
seed: [GetMoneroSeedResponse, GetRestoreHeightResponse] | null;
}

export default function SeedPhraseModal({
onClose,
seed,
}: SeedPhraseModalProps) {
if (seed === null) {
return null;
}

return (
<Dialog open={true} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle>Wallet Seed Phrase</DialogTitle>
<DialogContent>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<ActionableMonospaceTextBox
content={seed[0].seed}
displayCopyIcon={true}
enableQrCode={false}
spoilerText="Press to reveal"
/>
<ActionableMonospaceTextBox
content={seed[1].height.toString()}
displayCopyIcon={true}
enableQrCode={false}
/>
</Box>

<Typography
variant="caption"
color="text.secondary"
sx={{ mt: 2, display: "block", fontStyle: "italic" }}
>
Keep this seed phrase safe and secure. Write it down on paper and
store it in a safe place. Keep the restore height in mind when you
restore your wallet on another device.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="contained">
Close
</Button>
</DialogActions>
</Dialog>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ import SendTransactionModal from "../SendTransactionModal";
import { useNavigate } from "react-router-dom";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import SetRestoreHeightModal from "../SetRestoreHeightModal";
import SeedPhraseButton from "../SeedPhraseButton";
import SeedPhraseModal from "../SeedPhraseModal";
import DfxButton from "./DFXWidget";
import {
GetMoneroSeedResponse,
GetRestoreHeightResponse,
} from "models/tauriModel";

interface WalletActionButtonsProps {
balance: {
Expand All @@ -37,11 +43,16 @@ export default function WalletActionButtons({
balance,
}: WalletActionButtonsProps) {
const navigate = useNavigate();

const [sendDialogOpen, setSendDialogOpen] = useState(false);
const [restoreHeightDialogOpen, setRestoreHeightDialogOpen] = useState(false);
const [seedPhrase, setSeedPhrase] = useState<
[GetMoneroSeedResponse, GetRestoreHeightResponse] | null
>(null);

const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
const menuOpen = Boolean(menuAnchorEl);

const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setMenuAnchorEl(event.currentTarget);
};
Expand All @@ -55,6 +66,7 @@ export default function WalletActionButtons({
open={restoreHeightDialogOpen}
onClose={() => setRestoreHeightDialogOpen(false)}
/>
<SeedPhraseModal onClose={() => setSeedPhrase(null)} seed={seedPhrase} />
<SendTransactionModal
balance={balance}
open={sendDialogOpen}
Expand Down Expand Up @@ -100,6 +112,10 @@ export default function WalletActionButtons({
</ListItemIcon>
<Typography>Restore Height</Typography>
</MenuItem>
<SeedPhraseButton
onMenuClose={handleMenuClose}
onSeedPhraseSuccess={setSeedPhrase}
/>
</Menu>
</Box>
</>
Expand Down

This file was deleted.

12 changes: 9 additions & 3 deletions src-gui/src/renderer/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
SetRestoreHeightResponse,
GetRestoreHeightResponse,
MoneroNodeConfig,
GetMoneroSeedResponse,
} from "models/tauriModel";
import {
rpcSetBalance,
Expand Down Expand Up @@ -495,9 +496,14 @@ export async function getMoneroSyncProgress(): Promise<GetMoneroSyncProgressResp
);
}

export async function getMoneroSeed(): Promise<string> {
// Returns the wallet's seed phrase as a single string. Backend must expose the `get_monero_seed` command.
return await invokeNoArgs<string>("get_monero_seed");
export async function getMoneroSeed(): Promise<GetMoneroSeedResponse> {
return await invokeNoArgs<GetMoneroSeedResponse>("get_monero_seed");
}

export async function getMoneroSeedAndRestoreHeight(): Promise<
[GetMoneroSeedResponse, GetRestoreHeightResponse]
> {
return Promise.all([getMoneroSeed(), getRestoreHeight()]);
}

// Wallet management functions that handle Redux dispatching
Expand Down
12 changes: 7 additions & 5 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ use swap::cli::{
CheckMoneroNodeResponse, CheckSeedArgs, CheckSeedResponse, DfxAuthenticateResponse,
ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs,
GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs,
GetMoneroMainAddressArgs, GetMoneroSyncProgressArgs, GetPendingApprovalsResponse,
GetRestoreHeightArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs,
MoneroRecoveryArgs, RedactArgs, RejectApprovalArgs, RejectApprovalResponse,
ResolveApprovalArgs, ResumeSwapArgs, SendMoneroArgs, SetRestoreHeightArgs,
SuspendCurrentSwapArgs, WithdrawBtcArgs,
GetMoneroMainAddressArgs, GetMoneroSeedArgs, GetMoneroSyncProgressArgs,
GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs,
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs,
SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
},
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
Context, ContextBuilder,
Expand Down Expand Up @@ -206,6 +206,7 @@ pub fn run() {
get_monero_balance,
send_monero,
get_monero_sync_progress,
get_monero_seed,
check_seed,
get_pending_approvals,
set_monero_restore_height,
Expand Down Expand Up @@ -270,6 +271,7 @@ 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);
tauri_command!(get_monero_seed, GetMoneroSeedArgs, no_args);

/// Here we define Tauri commands whose implementation is not delegated to the Request trait
#[tauri::command]
Expand Down
Loading