mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-21 03:15:28 -05:00
feat(wallet): Export Monero seedphrase (#515)
* feat(wallet): Allow exporting Monero seed * refactors, display restore height too
This commit is contained in:
parent
660423f873
commit
0a75ea6a19
9 changed files with 248 additions and 63 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<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>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</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>
|
||||
|
||||
{enableQrCode && (
|
||||
<QRCodeModal
|
||||
open={qrCodeOpen}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 | HTMLElement>(null);
|
||||
const menuOpen = Boolean(menuAnchorEl);
|
||||
|
||||
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
|
@ -55,6 +66,7 @@ export default function WalletActionButtons({
|
|||
open={restoreHeightDialogOpen}
|
||||
onClose={() => setRestoreHeightDialogOpen(false)}
|
||||
/>
|
||||
<SeedPhraseModal onClose={() => setSeedPhrase(null)} seed={seedPhrase} />
|
||||
<SendTransactionModal
|
||||
balance={balance}
|
||||
open={sendDialogOpen}
|
||||
|
|
@ -100,6 +112,10 @@ export default function WalletActionButtons({
|
|||
</ListItemIcon>
|
||||
<Typography>Restore Height</Typography>
|
||||
</MenuItem>
|
||||
<SeedPhraseButton
|
||||
onMenuClose={handleMenuClose}
|
||||
onSeedPhraseSuccess={setSeedPhrase}
|
||||
/>
|
||||
</Menu>
|
||||
</Box>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Button onClick={writeToClipboard} {...props}>
|
||||
Copy
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
@ -45,6 +45,7 @@ import {
|
|||
SetRestoreHeightResponse,
|
||||
GetRestoreHeightResponse,
|
||||
MoneroNodeConfig,
|
||||
GetMoneroSeedResponse,
|
||||
} from "models/tauriModel";
|
||||
import {
|
||||
rpcSetBalance,
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<Context>) -> Result<Self::Response> {
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue