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 (
+
+ );
+}
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 (
+
+ );
+}
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 {