From 0a75ea6a197c4478120f3fd4e2b83e4c54f57468 Mon Sep 17 00:00:00 2001 From: Mohan <86064887+binarybaron@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:51:19 +0200 Subject: [PATCH] feat(wallet): Export Monero seedphrase (#515) * feat(wallet): Allow exporting Monero seed * refactors, display restore height too --- CHANGELOG.md | 1 + .../other/ActionableMonospaceTextBox.tsx | 113 ++++++++++++------ .../pages/monero/SeedPhraseButton.tsx | 51 ++++++++ .../pages/monero/SeedPhraseModal.tsx | 64 ++++++++++ .../monero/components/WalletActionButtons.tsx | 16 +++ .../swap/components/ClipbiardIconButton.tsx | 16 --- src-gui/src/renderer/rpc.ts | 12 +- src-tauri/src/lib.rs | 12 +- swap/src/cli/api/request.rs | 26 ++++ 9 files changed, 248 insertions(+), 63 deletions(-) create mode 100644 src-gui/src/renderer/components/pages/monero/SeedPhraseButton.tsx create mode 100644 src-gui/src/renderer/components/pages/monero/SeedPhraseModal.tsx delete mode 100644 src-gui/src/renderer/components/pages/swap/swap/components/ClipbiardIconButton.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index a62211a0..3e644d92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx b/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx index 0f25396b..c321ba9b 100644 --- a/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx +++ b/src-gui/src/renderer/components/other/ActionableMonospaceTextBox.tsx @@ -17,6 +17,7 @@ type Props = { displayCopyIcon?: boolean; enableQrCode?: boolean; light?: boolean; + spoilerText?: string; }; function QRCodeModal({ open, onClose, content }: ModalProps) { @@ -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); @@ -76,52 +79,84 @@ export default function ActionableMonospaceTextBox({ return ( <> - - + - - - {content} - {displayCopyIcon && ( - - - - )} - {enableQrCode && ( - + + + + {content} + {displayCopyIcon && ( setQrCodeOpen(true)} - onMouseEnter={() => setIsQrCodeButtonHovered(true)} - onMouseLeave={() => setIsQrCodeButtonHovered(false)} + onClick={handleCopy} size="small" sx={{ marginLeft: 1 }} > - + - - )} - + )} + {enableQrCode && ( + + setQrCodeOpen(true)} + onMouseEnter={() => setIsQrCodeButtonHovered(true)} + onMouseLeave={() => setIsQrCodeButtonHovered(false)} + size="small" + sx={{ marginLeft: 1 }} + > + + + + )} + + - - + + + {spoilerText && !isRevealed && ( + setIsRevealed(true)} + sx={{ + position: "absolute", + inset: 0, + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + bgcolor: "rgba(0, 0, 0, 0.1)", + borderRadius: 1, + }} + > + + {spoilerText} + + + )} + + {enableQrCode && ( void; + onSeedPhraseSuccess: ( + response: [GetMoneroSeedResponse, GetRestoreHeightResponse], + ) => void; +} + +export default function SeedPhraseButton({ + onMenuClose, + onSeedPhraseSuccess, +}: SeedPhraseButtonProps) { + const handleSeedPhraseSuccess = ( + response: [GetMoneroSeedResponse, GetRestoreHeightResponse], + ) => { + onSeedPhraseSuccess(response); + onMenuClose(); + }; + + return ( + + + + + + Seedphrase + + + ); +} diff --git a/src-gui/src/renderer/components/pages/monero/SeedPhraseModal.tsx b/src-gui/src/renderer/components/pages/monero/SeedPhraseModal.tsx new file mode 100644 index 00000000..b8740971 --- /dev/null +++ b/src-gui/src/renderer/components/pages/monero/SeedPhraseModal.tsx @@ -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 ( + + Wallet Seed Phrase + + + + + + + + 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. + + + + + + + ); +} 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 80ffd900..8c130fce 100644 --- a/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx +++ b/src-gui/src/renderer/components/pages/monero/components/WalletActionButtons.tsx @@ -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: { @@ -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); const menuOpen = Boolean(menuAnchorEl); + const handleMenuClick = (event: React.MouseEvent) => { setMenuAnchorEl(event.currentTarget); }; @@ -55,6 +66,7 @@ export default function WalletActionButtons({ open={restoreHeightDialogOpen} onClose={() => setRestoreHeightDialogOpen(false)} /> + setSeedPhrase(null)} seed={seedPhrase} /> Restore Height + diff --git a/src-gui/src/renderer/components/pages/swap/swap/components/ClipbiardIconButton.tsx b/src-gui/src/renderer/components/pages/swap/swap/components/ClipbiardIconButton.tsx deleted file mode 100644 index 47d94f79..00000000 --- a/src-gui/src/renderer/components/pages/swap/swap/components/ClipbiardIconButton.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import Button, { ButtonProps } from "@mui/material/Button"; - -export default function ClipboardIconButton({ - text, - ...props -}: { text: string } & ButtonProps) { - function writeToClipboard() { - throw new Error("Not implemented"); - } - - return ( - - ); -} diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 3ffbf873..7e943fdf 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -45,6 +45,7 @@ import { SetRestoreHeightResponse, GetRestoreHeightResponse, MoneroNodeConfig, + GetMoneroSeedResponse, } from "models/tauriModel"; import { rpcSetBalance, @@ -495,9 +496,14 @@ export async function getMoneroSyncProgress(): Promise { - // Returns the wallet's seed phrase as a single string. Backend must expose the `get_monero_seed` command. - return await invokeNoArgs("get_monero_seed"); +export async function getMoneroSeed(): Promise { + return await invokeNoArgs("get_monero_seed"); +} + +export async function getMoneroSeedAndRestoreHeight(): Promise< + [GetMoneroSeedResponse, GetRestoreHeightResponse] +> { + return Promise.all([getMoneroSeed(), getRestoreHeight()]); } // Wallet management functions that handle Redux dispatching diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c24d94fa..2e6227da 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, @@ -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, @@ -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] diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs index 9046c618..8cb5fb01 100644 --- a/swap/src/cli/api/request.rs +++ b/swap/src/cli/api/request.rs @@ -1994,6 +1994,32 @@ impl Request for GetMoneroSyncProgressArgs { } } +#[typeshare] +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct GetMoneroSeedArgs; + +#[typeshare] +#[derive(Serialize, Deserialize, Debug)] +pub struct GetMoneroSeedResponse { + pub seed: String, +} + +impl Request for GetMoneroSeedArgs { + type Response = GetMoneroSeedResponse; + + async fn request(self, ctx: Arc) -> Result { + let wallet_manager = ctx + .monero_manager + .as_ref() + .context("Monero wallet manager not available")?; + let wallet = wallet_manager.main_wallet().await; + + let seed = wallet.seed().await?; + + Ok(GetMoneroSeedResponse { seed }) + } +} + #[typeshare] #[derive(Serialize, Deserialize, Debug)] pub struct GetPendingApprovalsResponse {