feat(gui): Review logs before sending feedback (#301)

* add review buttons that open the attached logs before submitting feedback
* add redact switches to redact transaction id's from attached logs
This commit is contained in:
Raphael 2025-04-23 15:09:19 +02:00 committed by GitHub
parent 3fa31ba139
commit e8084d65ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 255 additions and 85 deletions

View file

@ -53,7 +53,7 @@ export function parseCliLogString(log: string): CliLog | string {
try { try {
const parsed = JSON.parse(log); const parsed = JSON.parse(log);
if (isCliLog(parsed)) { if (isCliLog(parsed)) {
return parsed; return parsed as CliLog;
} else { } else {
return log; return log;
} }
@ -61,3 +61,4 @@ export function parseCliLogString(log: string): CliLog | string {
return log; return log;
} }
} }

View file

@ -7,50 +7,49 @@ import {
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
FormControl,
FormControlLabel, FormControlLabel,
IconButton,
MenuItem, MenuItem,
Paper, Paper,
Select, Select,
Switch,
TextField, TextField,
Tooltip,
Typography,
} from "@material-ui/core"; } from "@material-ui/core";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { useState } from "react"; import { useEffect, useState } from "react";
import TruncatedText from "renderer/components/other/TruncatedText"; import TruncatedText from "renderer/components/other/TruncatedText";
import { store } from "renderer/store/storeRenderer"; import { store } from "renderer/store/storeRenderer";
import { useActiveSwapInfo, useAppSelector } from "store/hooks"; import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { parseDateString } from "utils/parseUtils"; import { logsToRawString, parseDateString } from "utils/parseUtils";
import { submitFeedbackViaHttp } from "../../../api"; import { submitFeedbackViaHttp } from "../../../api";
import LoadingButton from "../../other/LoadingButton"; import LoadingButton from "../../other/LoadingButton";
import { PiconeroAmount } from "../../other/Units"; import { PiconeroAmount } from "../../other/Units";
import { getLogsOfSwap } from "renderer/rpc"; import { getLogsOfSwap, redactLogs } from "renderer/rpc";
import logger from "utils/logger"; import logger from "utils/logger";
import { Label, Visibility } from "@material-ui/icons";
import CliLogsBox from "renderer/components/other/RenderedCliLog";
import { CliLog, parseCliLogString } from "models/cliModel";
async function submitFeedback(body: string, swapId: string | number, submitDaemonLogs: boolean) { async function submitFeedback(body: string, swapId: string | null, swapLogs: string | null, daemonLogs: string | null) {
let attachedBody = ""; let attachedBody = "";
if (swapId !== 0 && typeof swapId === "string") { if (swapId !== null) {
const swapInfo = store.getState().rpc.state.swapInfos[swapId]; const swapInfo = store.getState().rpc.state.swapInfos[swapId];
if (swapInfo === undefined) { if (swapInfo === undefined) {
throw new Error(`Swap with id ${swapId} not found`); throw new Error(`Swap with id ${swapId} not found`);
} }
// Retrieve logs for the specific swap attachedBody = `${JSON.stringify(swapInfo, null, 4)}\n\nLogs: ${swapLogs ?? ""}`;
const logs = await getLogsOfSwap(swapId, false);
attachedBody = `${JSON.stringify(swapInfo, null, 4)} \n\nLogs: ${logs.logs
.map((l) => JSON.stringify(l))
.join("\n====\n")}`;
} }
if (submitDaemonLogs) { if (daemonLogs !== null) {
const logs = store.getState().rpc?.logs ?? []; attachedBody += `\n\nDaemon Logs: ${daemonLogs ?? ""}`;
attachedBody += `\n\nDaemon Logs: ${logs
.map((l) => JSON.stringify(l))
.join("\n====\n")}`;
} }
console.log(`Sending feedback with attachement: \`\n${attachedBody}\``)
await submitFeedbackViaHttp(body, attachedBody); await submitFeedbackViaHttp(body, attachedBody);
} }
@ -58,14 +57,14 @@ async function submitFeedback(body: string, swapId: string | number, submitDaemo
* This component is a dialog that allows the user to submit feedback to the * This component is a dialog that allows the user to submit feedback to the
* developers. The user can enter a message and optionally attach logs from a * developers. The user can enter a message and optionally attach logs from a
* specific swap. * specific swap.
* selectedSwap = 0 means no swap is attached * selectedSwap = null means no swap is attached
*/ */
function SwapSelectDropDown({ function SwapSelectDropDown({
selectedSwap, selectedSwap,
setSelectedSwap, setSelectedSwap,
}: { }: {
selectedSwap: string | number; selectedSwap: string | null;
setSelectedSwap: (swapId: string | number) => void; setSelectedSwap: (swapId: string | null) => void;
}) { }) {
const swaps = useAppSelector((state) => const swaps = useAppSelector((state) =>
Object.values(state.rpc.state.swapInfos), Object.values(state.rpc.state.swapInfos),
@ -73,15 +72,16 @@ function SwapSelectDropDown({
return ( return (
<Select <Select
value={selectedSwap} value={selectedSwap ?? ""}
label="Attach logs"
variant="outlined" variant="outlined"
onChange={(e) => setSelectedSwap(e.target.value as string)} onChange={(e) => setSelectedSwap(e.target.value as string || null)}
style={{ width: "100%" }}
displayEmpty
> >
<MenuItem value={0}>Do not attach a swap</MenuItem> <MenuItem value="">Do not attach a swap</MenuItem>
{swaps.map((swap) => ( {swaps.map((swap) => (
<MenuItem value={swap.swap_id} key={swap.swap_id}> <MenuItem value={swap.swap_id} key={swap.swap_id}>
Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{" "} Swap{" "}<TruncatedText>{swap.swap_id}</TruncatedText>{" "}from{" "}
{new Date(parseDateString(swap.start_date)).toDateString()} ( {new Date(parseDateString(swap.start_date)).toDateString()} (
<PiconeroAmount amount={swap.xmr_amount} />) <PiconeroAmount amount={swap.xmr_amount} />)
</MenuItem> </MenuItem>
@ -105,24 +105,92 @@ export default function FeedbackDialog({
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
const [selectedAttachedSwap, setSelectedAttachedSwap] = useState< const [selectedSwap, setSelectedSwap] = useState<
string | number string | null
>(currentSwapId?.swap_id || 0); >(currentSwapId?.swap_id || null);
const [swapLogs, setSwapLogs] = useState<(string | CliLog)[] | null>(null);
const [attachDaemonLogs, setAttachDaemonLogs] = useState(true); const [attachDaemonLogs, setAttachDaemonLogs] = useState(true);
const [daemonLogs, setDaemonLogs] = useState<(string | CliLog)[] | null>(null);
useEffect(() => {
// Reset logs if no swap is selected
if (selectedSwap === null) {
setSwapLogs(null);
return;
}
// Fetch the logs from the rust backend and update the state
getLogsOfSwap(selectedSwap, false).then((response) => setSwapLogs(response.logs.map(parseCliLogString)))
}, [selectedSwap]);
useEffect(() => {
if (attachDaemonLogs === false) {
setDaemonLogs(null);
return;
}
setDaemonLogs(store.getState().rpc?.logs)
}, [attachDaemonLogs]);
// Whether to display the log editor
const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false);
const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false);
const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH; const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH;
const clearState = () => {
setBodyText("");
setAttachDaemonLogs(false);
setSelectedSwap(null);
}
const sendFeedback = async () => {
if (pending) {
return;
}
try {
setPending(true);
await submitFeedback(bodyText, selectedSwap, logsToRawString(swapLogs), logsToRawString(daemonLogs));
enqueueSnackbar("Feedback submitted successfully!", {
variant: "success",
});
clearState()
} catch (e) {
logger.error(`Failed to submit feedback: ${e}`);
enqueueSnackbar(`Failed to submit feedback (${e})`, {
variant: "error",
});
} finally {
setPending(false);
}
onClose();
}
const setSwapLogsRedacted = async (redact: boolean) => {
setSwapLogs((await getLogsOfSwap(selectedSwap, redact)).logs.map(parseCliLogString))
}
const setDaemonLogsRedacted = async (redact: boolean) => {
if (!redact)
return setDaemonLogs(store.getState().rpc?.logs)
const redactedLogs = await redactLogs(daemonLogs);
setDaemonLogs(redactedLogs)
}
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose}>
<DialogTitle>Submit Feedback</DialogTitle> <DialogTitle>Submit Feedback</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <ul>
Got something to say? Drop us a message below. If you had an issue <li>Got something to say? Drop us a message below. </li>
with a specific swap, select it from the dropdown to attach the logs. <li>If you had an issue with a specific swap, select it from the dropdown to attach the logs.
It will help us figure out what went wrong. It will help us figure out what went wrong.
<br /> </li>
We appreciate you taking the time to share your thoughts! Every feedback is read by a core developer! <li>We appreciate you taking the time to share your thoughts! Every message is read by a core developer!</li>
</DialogContentText> </ul>
<Box <Box
style={{ style={{
display: "flex", display: "flex",
@ -145,50 +213,61 @@ export default function FeedbackDialog({
fullWidth fullWidth
error={bodyTooLong} error={bodyTooLong}
/> />
<SwapSelectDropDown <Box style={{
selectedSwap={selectedAttachedSwap} display: "flex",
setSelectedSwap={setSelectedAttachedSwap} flexDirection: "row",
/> justifyContent: "space-between",
<Paper variant="outlined" style={{ padding: "0.5rem" }}> gap: "1rem",
<FormControlLabel }}>
control={
<Checkbox <SwapSelectDropDown
color="primary" selectedSwap={selectedSwap}
checked={attachDaemonLogs} setSelectedSwap={setSelectedSwap}
onChange={(e) => setAttachDaemonLogs(e.target.checked)}
/>
}
label="Attach daemon logs"
/> />
</Paper> <Tooltip title="View the logs">
<Box style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<IconButton onClick={() => setSwapLogsEditorOpen(true)} disabled={selectedSwap === null}>
<Visibility />
</IconButton>
</Box>
</Tooltip>
</Box>
<LogViewer open={swapLogsEditorOpen} setOpen={setSwapLogsEditorOpen} logs={swapLogs} redact={setSwapLogsRedacted} />
<Box style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: "1rem",
}}>
<Paper variant="outlined" style={{ padding: "0.5rem", width: "100%" }} >
<FormControlLabel
control={
<Checkbox
color="primary"
checked={attachDaemonLogs}
onChange={(e) => setAttachDaemonLogs(e.target.checked)}
/>
}
label="Attach logs from the current session"
/>
</Paper>
<Tooltip title="View the logs">
<Box style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<IconButton onClick={() => setDaemonLogsEditorOpen(true)} disabled={attachDaemonLogs === false}>
<Visibility />
</IconButton>
</Box>
</Tooltip>
</Box>
<LogViewer open={daemonLogsEditorOpen} setOpen={setDaemonLogsEditorOpen} logs={daemonLogs} redact={setDaemonLogsRedacted} />
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose}>Cancel</Button> <Button onClick={() => { clearState(); onClose() }}>Cancel</Button>
<LoadingButton <LoadingButton
color="primary" color="primary"
variant="contained" variant="contained"
onClick={async () => { onClick={sendFeedback}
if (pending) {
return;
}
try {
setPending(true);
await submitFeedback(bodyText, selectedAttachedSwap, attachDaemonLogs);
enqueueSnackbar("Feedback submitted successfully!", {
variant: "success",
});
} catch (e) {
logger.error(`Failed to submit feedback: ${e}`);
enqueueSnackbar(`Failed to submit feedback (${e})`, {
variant: "error",
});
} finally {
setPending(false);
}
onClose();
}}
loading={pending} loading={pending}
> >
Submit Submit
@ -197,3 +276,44 @@ export default function FeedbackDialog({
</Dialog> </Dialog>
); );
} }
function LogViewer(
{ open,
setOpen,
logs,
redact
}: {
open: boolean,
setOpen: (_: boolean) => void,
logs: (string | CliLog)[] | null,
redact: (_: boolean) => void
}) {
return (
<Dialog open={open} onClose={() => setOpen(false)} fullWidth>
<DialogContent>
<Box>
<DialogContentText>
<Box style={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
<Typography>
These are the logs that would be attached to your feedback message and provided to us developers.
They help us narrow down the problem you encountered.
</Typography>
</Box>
</DialogContentText>
<CliLogsBox label="Logs" logs={logs} topRightButton={<Paper style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', paddingLeft: "0.5rem" }} variant="outlined">
Redact
<Switch color="primary" onChange={(_, checked: boolean) => redact(checked)} />
</Paper>} />
</Box>
</DialogContent>
<DialogActions>
<Button variant="contained" color="primary" onClick={() => setOpen(false)}>
Close
</Button>
</DialogActions>
</Dialog >
)
}

View file

@ -1,6 +1,6 @@
import { Box, Chip, Typography } from "@material-ui/core"; import { Box, Chip, Typography } from "@material-ui/core";
import { CliLog } from "models/cliModel"; import { CliLog } from "models/cliModel";
import { useMemo, useState } from "react"; import { ReactNode, useMemo, useState } from "react";
import { logsToRawString } from "utils/parseUtils"; import { logsToRawString } from "utils/parseUtils";
import ScrollablePaperTextBox from "./ScrollablePaperTextBox"; import ScrollablePaperTextBox from "./ScrollablePaperTextBox";
@ -61,9 +61,11 @@ function RenderedCliLog({ log }: { log: CliLog }) {
export default function CliLogsBox({ export default function CliLogsBox({
label, label,
logs, logs,
topRightButton = null,
}: { }: {
label: string; label: string;
logs: (CliLog | string)[]; logs: (CliLog | string)[];
topRightButton?: ReactNode
}) { }) {
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
@ -82,6 +84,7 @@ export default function CliLogsBox({
copyValue={logsToRawString(logs)} copyValue={logsToRawString(logs)}
searchQuery={searchQuery} searchQuery={searchQuery}
setSearchQuery={setSearchQuery} setSearchQuery={setSearchQuery}
topRightButton={topRightButton}
rows={memoizedLogs.map((log) => rows={memoizedLogs.map((log) =>
typeof log === "string" ? ( typeof log === "string" ? (
<Typography key={log} component="pre"> <Typography key={log} component="pre">

View file

@ -2,7 +2,7 @@ import { Box, Divider, IconButton, Paper, Typography } from "@material-ui/core";
import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined"; import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"; import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp";
import { ReactNode, useRef } from "react"; import { ReactNode, useEffect, useRef } from "react";
import { VList, VListHandle } from "virtua"; import { VList, VListHandle } from "virtua";
import { ExpandableSearchBox } from "./ExpandableSearchBox"; import { ExpandableSearchBox } from "./ExpandableSearchBox";
@ -14,6 +14,7 @@ export default function ScrollablePaperTextBox({
copyValue, copyValue,
searchQuery = null, searchQuery = null,
setSearchQuery = null, setSearchQuery = null,
topRightButton = null,
minHeight = MIN_HEIGHT, minHeight = MIN_HEIGHT,
}: { }: {
rows: ReactNode[]; rows: ReactNode[];
@ -22,6 +23,7 @@ export default function ScrollablePaperTextBox({
searchQuery: string | null; searchQuery: string | null;
setSearchQuery?: ((query: string) => void) | null; setSearchQuery?: ((query: string) => void) | null;
minHeight?: string; minHeight?: string;
topRightButton?: ReactNode | null
}) { }) {
const virtuaEl = useRef<VListHandle | null>(null); const virtuaEl = useRef<VListHandle | null>(null);
@ -48,7 +50,10 @@ export default function ScrollablePaperTextBox({
width: "100%", width: "100%",
}} }}
> >
<Typography>{title}</Typography> <Box style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
<Typography>{title}</Typography>
{topRightButton}
</Box>
<Divider /> <Divider />
<Box <Box
style={{ style={{
@ -69,12 +74,12 @@ export default function ScrollablePaperTextBox({
<IconButton onClick={onCopy} size="small"> <IconButton onClick={onCopy} size="small">
<FileCopyOutlinedIcon /> <FileCopyOutlinedIcon />
</IconButton> </IconButton>
<IconButton onClick={scrollToBottom} size="small">
<KeyboardArrowDownIcon />
</IconButton>
<IconButton onClick={scrollToTop} size="small"> <IconButton onClick={scrollToTop} size="small">
<KeyboardArrowUpIcon /> <KeyboardArrowUpIcon />
</IconButton> </IconButton>
<IconButton onClick={scrollToBottom} size="small">
<KeyboardArrowDownIcon />
</IconButton>
{searchQuery !== undefined && setSearchQuery !== undefined && ( {searchQuery !== undefined && setSearchQuery !== undefined && (
<ExpandableSearchBox query={searchQuery} setQuery={setSearchQuery} /> <ExpandableSearchBox query={searchQuery} setQuery={setSearchQuery} />
)} )}

View file

@ -25,6 +25,8 @@ import {
GetDataDirArgs, GetDataDirArgs,
ResolveApprovalArgs, ResolveApprovalArgs,
ResolveApprovalResponse, ResolveApprovalResponse,
RedactArgs,
RedactResponse,
} from "models/tauriModel"; } from "models/tauriModel";
import { import {
rpcSetBalance, rpcSetBalance,
@ -40,6 +42,8 @@ import { getNetwork, isTestnet } from "store/config";
import { Blockchain, Network } from "store/features/settingsSlice"; import { Blockchain, Network } from "store/features/settingsSlice";
import { setStatus } from "store/features/nodesSlice"; import { setStatus } from "store/features/nodesSlice";
import { discoveredMakersByRendezvous } from "store/features/makersSlice"; import { discoveredMakersByRendezvous } from "store/features/makersSlice";
import { CliLog } from "models/cliModel";
import { logsToRawString, parseLogsFromString } from "utils/parseUtils";
export const PRESET_RENDEZVOUS_POINTS = [ export const PRESET_RENDEZVOUS_POINTS = [
"/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
@ -164,6 +168,18 @@ export async function getLogsOfSwap(
}); });
} }
/// Call the rust backend to redact logs.
export async function redactLogs(
logs: (string | CliLog)[]
): Promise<(string | CliLog)[]> {
const response = await invoke<RedactArgs, RedactResponse>("redact", {
text: logsToRawString(logs)
})
console.log(response.text.split("\n").length)
return parseLogsFromString(response.text);
}
export async function listSellersAtRendezvousPoint( export async function listSellersAtRendezvousPoint(
rendezvousPointAddress: string, rendezvousPointAddress: string,
): Promise<ListSellersResponse> { ): Promise<ListSellersResponse> {

View file

@ -10,7 +10,7 @@ import {
} from "models/tauriModel"; } from "models/tauriModel";
import { MoneroRecoveryResponse } from "../../models/rpcModel"; import { MoneroRecoveryResponse } from "../../models/rpcModel";
import { GetSwapInfoResponseExt } from "models/tauriModelExt"; import { GetSwapInfoResponseExt } from "models/tauriModelExt";
import { getLogsAndStringsFromRawFileString } from "utils/parseUtils"; import { parseLogsFromString } from "utils/parseUtils";
import { CliLog } from "models/cliModel"; import { CliLog } from "models/cliModel";
import logger from "utils/logger"; import logger from "utils/logger";
@ -68,7 +68,7 @@ export const rpcSlice = createSlice({
reducers: { reducers: {
receivedCliLog(slice, action: PayloadAction<TauriLogEvent>) { receivedCliLog(slice, action: PayloadAction<TauriLogEvent>) {
const buffer = action.payload.buffer; const buffer = action.payload.buffer;
const logs = getLogsAndStringsFromRawFileString(buffer); const logs = parseLogsFromString(buffer);
const logsWithoutExisting = logs.filter(log => !slice.logs.includes(log)); const logsWithoutExisting = logs.filter(log => !slice.logs.includes(log));
slice.logs = slice.logs.concat(logsWithoutExisting); slice.logs = slice.logs.concat(logsWithoutExisting);
}, },

View file

@ -52,7 +52,7 @@ export function getLinesOfString(data: string): string[] {
.filter((l) => l.length > 0); .filter((l) => l.length > 0);
} }
export function getLogsAndStringsFromRawFileString( export function parseLogsFromString(
rawFileData: string, rawFileData: string,
): (CliLog | string)[] { ): (CliLog | string)[] {
return getLinesOfString(rawFileData).map(parseCliLogString); return getLinesOfString(rawFileData).map(parseCliLogString);

View file

@ -9,8 +9,8 @@ use swap::cli::{
CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, CheckElectrumNodeResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse,
ExportBitcoinWalletArgs, GetDataDirArgs, GetHistoryArgs, GetLogsArgs, ExportBitcoinWalletArgs, GetDataDirArgs, GetHistoryArgs, GetLogsArgs,
GetMoneroAddressesArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs, GetMoneroAddressesArgs, GetSwapInfoArgs, GetSwapInfosAllArgs, ListSellersArgs,
MoneroRecoveryArgs, ResolveApprovalArgs, ResumeSwapArgs, SuspendCurrentSwapArgs, MoneroRecoveryArgs, RedactArgs, ResolveApprovalArgs, ResumeSwapArgs,
WithdrawBtcArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
}, },
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings}, tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
Context, ContextBuilder, Context, ContextBuilder,
@ -186,6 +186,7 @@ pub fn run() {
get_wallet_descriptor, get_wallet_descriptor,
get_data_dir, get_data_dir,
resolve_approval_request, resolve_approval_request,
redact
]) ])
.setup(setup) .setup(setup)
.build(tauri::generate_context!()) .build(tauri::generate_context!())
@ -227,6 +228,7 @@ tauri_command!(get_logs, GetLogsArgs);
tauri_command!(list_sellers, ListSellersArgs); tauri_command!(list_sellers, ListSellersArgs);
tauri_command!(cancel_and_refund, CancelAndRefundArgs); tauri_command!(cancel_and_refund, CancelAndRefundArgs);
tauri_command!(resolve_approval_request, ResolveApprovalArgs); tauri_command!(resolve_approval_request, ResolveApprovalArgs);
tauri_command!(redact, RedactArgs);
// These commands require no arguments // These commands require no arguments
tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args); tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args);

View file

@ -3,7 +3,7 @@ use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock, T
use crate::cli::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent}; use crate::cli::api::tauri_bindings::{TauriEmitter, TauriSwapProgressEvent};
use crate::cli::api::Context; use crate::cli::api::Context;
use crate::cli::{list_sellers as list_sellers_impl, EventLoop, Seller, SellerStatus}; use crate::cli::{list_sellers as list_sellers_impl, EventLoop, Seller, SellerStatus};
use crate::common::get_logs; use crate::common::{get_logs, redact};
use crate::libp2p_ext::MultiAddrExt; use crate::libp2p_ext::MultiAddrExt;
use crate::monero::wallet_rpc::MoneroDaemon; use crate::monero::wallet_rpc::MoneroDaemon;
use crate::network::quote::{BidQuote, ZeroQuoteReceived}; use crate::network::quote::{BidQuote, ZeroQuoteReceived};
@ -420,6 +420,29 @@ impl Request for GetLogsArgs {
} }
} }
/// Best effort redaction of logs, e.g. wallet addresses, swap-ids
#[typeshare]
#[derive(Serialize, Deserialize, Debug)]
pub struct RedactArgs {
pub text: String,
}
#[typeshare]
#[derive(Serialize, Debug)]
pub struct RedactResponse {
pub text: String,
}
impl Request for RedactArgs {
type Response = RedactResponse;
async fn request(self, _: Arc<Context>) -> Result<Self::Response> {
Ok(RedactResponse {
text: redact(&self.text),
})
}
}
#[typeshare] #[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct GetMoneroAddressesArgs; pub struct GetMoneroAddressesArgs;