mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-21 11:25:50 -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.
|
- 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 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
|
## [3.0.0-beta.8] - 2025-08-10
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ type Props = {
|
||||||
displayCopyIcon?: boolean;
|
displayCopyIcon?: boolean;
|
||||||
enableQrCode?: boolean;
|
enableQrCode?: boolean;
|
||||||
light?: boolean;
|
light?: boolean;
|
||||||
|
spoilerText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function QRCodeModal({ open, onClose, content }: ModalProps) {
|
function QRCodeModal({ open, onClose, content }: ModalProps) {
|
||||||
|
|
@ -63,10 +64,12 @@ export default function ActionableMonospaceTextBox({
|
||||||
displayCopyIcon = true,
|
displayCopyIcon = true,
|
||||||
enableQrCode = true,
|
enableQrCode = true,
|
||||||
light = false,
|
light = false,
|
||||||
|
spoilerText,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [qrCodeOpen, setQrCodeOpen] = useState(false);
|
const [qrCodeOpen, setQrCodeOpen] = useState(false);
|
||||||
const [isQrCodeButtonHovered, setIsQrCodeButtonHovered] = useState(false);
|
const [isQrCodeButtonHovered, setIsQrCodeButtonHovered] = useState(false);
|
||||||
|
const [isRevealed, setIsRevealed] = useState(!spoilerText);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
await writeText(content);
|
await writeText(content);
|
||||||
|
|
@ -76,52 +79,84 @@ export default function ActionableMonospaceTextBox({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip
|
<Box sx={{ position: "relative" }}>
|
||||||
title={
|
<Tooltip
|
||||||
isQrCodeButtonHovered
|
title={
|
||||||
? ""
|
isQrCodeButtonHovered
|
||||||
: copied
|
? ""
|
||||||
? "Copied to clipboard"
|
: copied
|
||||||
: "Click to copy"
|
? "Copied to clipboard"
|
||||||
}
|
: "Click to copy"
|
||||||
arrow
|
}
|
||||||
>
|
arrow
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box sx={{ flexGrow: 1 }} onClick={handleCopy}>
|
<Box
|
||||||
<MonospaceTextBox light={light}>
|
sx={{
|
||||||
{content}
|
display: "flex",
|
||||||
{displayCopyIcon && (
|
alignItems: "center",
|
||||||
<IconButton
|
cursor: "pointer",
|
||||||
onClick={handleCopy}
|
filter: spoilerText && !isRevealed ? "blur(8px)" : "none",
|
||||||
size="small"
|
transition: "filter 0.3s ease",
|
||||||
sx={{ marginLeft: 1 }}
|
}}
|
||||||
>
|
>
|
||||||
<FileCopyOutlined />
|
<Box sx={{ flexGrow: 1 }} onClick={handleCopy}>
|
||||||
</IconButton>
|
<MonospaceTextBox light={light}>
|
||||||
)}
|
{content}
|
||||||
{enableQrCode && (
|
{displayCopyIcon && (
|
||||||
<Tooltip title="Show QR Code" arrow>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => setQrCodeOpen(true)}
|
onClick={handleCopy}
|
||||||
onMouseEnter={() => setIsQrCodeButtonHovered(true)}
|
|
||||||
onMouseLeave={() => setIsQrCodeButtonHovered(false)}
|
|
||||||
size="small"
|
size="small"
|
||||||
sx={{ marginLeft: 1 }}
|
sx={{ marginLeft: 1 }}
|
||||||
>
|
>
|
||||||
<QrCodeIcon />
|
<FileCopyOutlined />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
{enableQrCode && (
|
||||||
</MonospaceTextBox>
|
<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>
|
||||||
</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 && (
|
{enableQrCode && (
|
||||||
<QRCodeModal
|
<QRCodeModal
|
||||||
open={qrCodeOpen}
|
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 { useNavigate } from "react-router-dom";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import SetRestoreHeightModal from "../SetRestoreHeightModal";
|
import SetRestoreHeightModal from "../SetRestoreHeightModal";
|
||||||
|
import SeedPhraseButton from "../SeedPhraseButton";
|
||||||
|
import SeedPhraseModal from "../SeedPhraseModal";
|
||||||
import DfxButton from "./DFXWidget";
|
import DfxButton from "./DFXWidget";
|
||||||
|
import {
|
||||||
|
GetMoneroSeedResponse,
|
||||||
|
GetRestoreHeightResponse,
|
||||||
|
} from "models/tauriModel";
|
||||||
|
|
||||||
interface WalletActionButtonsProps {
|
interface WalletActionButtonsProps {
|
||||||
balance: {
|
balance: {
|
||||||
|
|
@ -37,11 +43,16 @@ export default function WalletActionButtons({
|
||||||
balance,
|
balance,
|
||||||
}: WalletActionButtonsProps) {
|
}: WalletActionButtonsProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [sendDialogOpen, setSendDialogOpen] = useState(false);
|
const [sendDialogOpen, setSendDialogOpen] = useState(false);
|
||||||
const [restoreHeightDialogOpen, setRestoreHeightDialogOpen] = useState(false);
|
const [restoreHeightDialogOpen, setRestoreHeightDialogOpen] = useState(false);
|
||||||
|
const [seedPhrase, setSeedPhrase] = useState<
|
||||||
|
[GetMoneroSeedResponse, GetRestoreHeightResponse] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||||
const menuOpen = Boolean(menuAnchorEl);
|
const menuOpen = Boolean(menuAnchorEl);
|
||||||
|
|
||||||
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setMenuAnchorEl(event.currentTarget);
|
setMenuAnchorEl(event.currentTarget);
|
||||||
};
|
};
|
||||||
|
|
@ -55,6 +66,7 @@ export default function WalletActionButtons({
|
||||||
open={restoreHeightDialogOpen}
|
open={restoreHeightDialogOpen}
|
||||||
onClose={() => setRestoreHeightDialogOpen(false)}
|
onClose={() => setRestoreHeightDialogOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<SeedPhraseModal onClose={() => setSeedPhrase(null)} seed={seedPhrase} />
|
||||||
<SendTransactionModal
|
<SendTransactionModal
|
||||||
balance={balance}
|
balance={balance}
|
||||||
open={sendDialogOpen}
|
open={sendDialogOpen}
|
||||||
|
|
@ -100,6 +112,10 @@ export default function WalletActionButtons({
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<Typography>Restore Height</Typography>
|
<Typography>Restore Height</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<SeedPhraseButton
|
||||||
|
onMenuClose={handleMenuClose}
|
||||||
|
onSeedPhraseSuccess={setSeedPhrase}
|
||||||
|
/>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</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,
|
SetRestoreHeightResponse,
|
||||||
GetRestoreHeightResponse,
|
GetRestoreHeightResponse,
|
||||||
MoneroNodeConfig,
|
MoneroNodeConfig,
|
||||||
|
GetMoneroSeedResponse,
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
import {
|
import {
|
||||||
rpcSetBalance,
|
rpcSetBalance,
|
||||||
|
|
@ -495,9 +496,14 @@ export async function getMoneroSyncProgress(): Promise<GetMoneroSyncProgressResp
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMoneroSeed(): Promise<string> {
|
export async function getMoneroSeed(): Promise<GetMoneroSeedResponse> {
|
||||||
// Returns the wallet's seed phrase as a single string. Backend must expose the `get_monero_seed` command.
|
return await invokeNoArgs<GetMoneroSeedResponse>("get_monero_seed");
|
||||||
return await invokeNoArgs<string>("get_monero_seed");
|
}
|
||||||
|
|
||||||
|
export async function getMoneroSeedAndRestoreHeight(): Promise<
|
||||||
|
[GetMoneroSeedResponse, GetRestoreHeightResponse]
|
||||||
|
> {
|
||||||
|
return Promise.all([getMoneroSeed(), getRestoreHeight()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wallet management functions that handle Redux dispatching
|
// Wallet management functions that handle Redux dispatching
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ use swap::cli::{
|
||||||
CheckMoneroNodeResponse, CheckSeedArgs, CheckSeedResponse, DfxAuthenticateResponse,
|
CheckMoneroNodeResponse, CheckSeedArgs, CheckSeedResponse, DfxAuthenticateResponse,
|
||||||
ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs,
|
ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs,
|
||||||
GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs,
|
GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs,
|
||||||
GetMoneroMainAddressArgs, GetMoneroSyncProgressArgs, GetPendingApprovalsResponse,
|
GetMoneroMainAddressArgs, GetMoneroSeedArgs, GetMoneroSyncProgressArgs,
|
||||||
GetRestoreHeightArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs,
|
GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs,
|
||||||
MoneroRecoveryArgs, RedactArgs, RejectApprovalArgs, RejectApprovalResponse,
|
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
|
||||||
ResolveApprovalArgs, ResumeSwapArgs, SendMoneroArgs, SetRestoreHeightArgs,
|
RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs,
|
||||||
SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
||||||
},
|
},
|
||||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
||||||
Context, ContextBuilder,
|
Context, ContextBuilder,
|
||||||
|
|
@ -206,6 +206,7 @@ pub fn run() {
|
||||||
get_monero_balance,
|
get_monero_balance,
|
||||||
send_monero,
|
send_monero,
|
||||||
get_monero_sync_progress,
|
get_monero_sync_progress,
|
||||||
|
get_monero_seed,
|
||||||
check_seed,
|
check_seed,
|
||||||
get_pending_approvals,
|
get_pending_approvals,
|
||||||
set_monero_restore_height,
|
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_main_address, GetMoneroMainAddressArgs, no_args);
|
||||||
tauri_command!(get_monero_balance, GetMoneroBalanceArgs, no_args);
|
tauri_command!(get_monero_balance, GetMoneroBalanceArgs, no_args);
|
||||||
tauri_command!(get_monero_sync_progress, GetMoneroSyncProgressArgs, 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
|
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
|
||||||
#[tauri::command]
|
#[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]
|
#[typeshare]
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct GetPendingApprovalsResponse {
|
pub struct GetPendingApprovalsResponse {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue