feat(gui): Partially available global state (#593)

* feat(gui): Partially availiable global state

* move tauri command into own module

* move commands list into src-tauri/src/commands.rs

* cleanup swap/src/cli/api.rs

* add contextRequirement attribute to PromiseInvokeButton

* amend

* allow wallet operation on partially availiable context

* improvements

* fix some linter errors

* limit amount of logs to 5k

* keep behaviour from before

* make sure if swapId is null useActiveSwapLogs, return no logs

* remove unused variable

* create ContextStatusType enum
This commit is contained in:
Mohan 2025-10-02 21:28:12 +02:00 committed by GitHub
parent 3b701fe1c5
commit 7d019bfb30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 2361 additions and 2080 deletions

View file

@ -31,9 +31,13 @@ function isCliLog(log: unknown): log is CliLog {
} }
export function isCliLogRelatedToSwap( export function isCliLogRelatedToSwap(
log: CliLog | string, log: CliLog | string | null | undefined,
swapId: string, swapId: string,
): boolean { ): boolean {
if (log === null || log === undefined) {
return false;
}
// If we only have a string, simply check if the string contains the swap id // If we only have a string, simply check if the string contains the swap id
// This provides reasonable backwards compatability // This provides reasonable backwards compatability
if (typeof log === "string") { if (typeof log === "string") {
@ -44,7 +48,7 @@ export function isCliLogRelatedToSwap(
// - the log has the swap id as an attribute // - the log has the swap id as an attribute
// - there exists a span which has the swap id as an attribute // - there exists a span which has the swap id as an attribute
return ( return (
log.fields["swap_id"] === swapId || ("fields" in log && log.fields["swap_id"] === swapId) ||
(log.spans?.some((span) => span["swap_id"] === swapId) ?? false) (log.spans?.some((span) => span["swap_id"] === swapId) ?? false)
); );
} }

View file

@ -3,13 +3,17 @@ import {
ApprovalRequest, ApprovalRequest,
ExpiredTimelocks, ExpiredTimelocks,
GetSwapInfoResponse, GetSwapInfoResponse,
PendingCompleted,
QuoteWithAddress,
SelectMakerDetails, SelectMakerDetails,
TauriBackgroundProgress, TauriBackgroundProgress,
TauriSwapProgressEvent, TauriSwapProgressEvent,
SendMoneroDetails, SendMoneroDetails,
ContextStatus,
} from "./tauriModel"; } from "./tauriModel";
import {
ContextStatusType,
ResultContextStatus,
RPCSlice,
} from "store/features/rpcSlice";
export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"]; export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"];
@ -382,3 +386,30 @@ export function haveFundsBeenLocked(
return true; return true;
} }
export function isContextFullyInitialized(
status: ResultContextStatus,
): boolean {
if (status == null || status.type === ContextStatusType.Error) {
return false;
}
return (
status.status.bitcoin_wallet_available &&
status.status.monero_wallet_available &&
status.status.database_available &&
status.status.tor_available
);
}
export function isContextWithBitcoinWallet(
status: ContextStatus | null,
): boolean {
return status?.bitcoin_wallet_available ?? false;
}
export function isContextWithMoneroWallet(
status: ContextStatus | null,
): boolean {
return status?.monero_wallet_available ?? false;
}

View file

@ -227,9 +227,8 @@ export async function fetchAllConversations(): Promise<void> {
store.dispatch(setConversation({ feedbackId, messages })); store.dispatch(setConversation({ feedbackId, messages }));
} catch (error) { } catch (error) {
logger.error( logger.error(
error, { error, feedbackId },
"Error fetching messages for feedback id", "Error fetching messages for feedback",
feedbackId,
); );
} }
} }

View file

@ -1,7 +1,8 @@
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { TauriContextStatusEvent, TauriEvent } from "models/tauriModel"; import { TauriEvent } from "models/tauriModel";
import { import {
contextStatusEventReceived, contextStatusEventReceived,
contextInitializationFailed,
rpcSetBalance, rpcSetBalance,
timelockChangeEventReceived, timelockChangeEventReceived,
approvalEventReceived, approvalEventReceived,
@ -18,7 +19,7 @@ import {
updateRates, updateRates,
} from "./api"; } from "./api";
import { import {
checkContextAvailability, checkContextStatus,
getSwapInfo, getSwapInfo,
initializeContext, initializeContext,
listSellersAtRendezvousPoint, listSellersAtRendezvousPoint,
@ -53,6 +54,9 @@ const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
// Fetch pending approvals every 2 seconds // Fetch pending approvals every 2 seconds
const FETCH_PENDING_APPROVALS_INTERVAL = 2 * 1_000; const FETCH_PENDING_APPROVALS_INTERVAL = 2 * 1_000;
// Check context status every 2 seconds
const CHECK_CONTEXT_STATUS_INTERVAL = 2 * 1_000;
function setIntervalImmediate(callback: () => void, interval: number): void { function setIntervalImmediate(callback: () => void, interval: number): void {
callback(); callback();
setInterval(callback, interval); setInterval(callback, interval);
@ -76,87 +80,86 @@ export async function setupBackgroundTasks(): Promise<void> {
// Setup Tauri event listeners // Setup Tauri event listeners
// Check if the context is already available. This is to prevent unnecessary re-initialization // Check if the context is already available. This is to prevent unnecessary re-initialization
if (await checkContextAvailability()) { setIntervalImmediate(async () => {
store.dispatch( const contextStatus = await checkContextStatus();
contextStatusEventReceived(TauriContextStatusEvent.Available), store.dispatch(contextStatusEventReceived(contextStatus));
); }, CHECK_CONTEXT_STATUS_INTERVAL);
} else {
const contextStatus = await checkContextStatus();
// If all components are unavailable, we need to initialize the context
if (
!contextStatus.bitcoin_wallet_available &&
!contextStatus.monero_wallet_available &&
!contextStatus.database_available &&
!contextStatus.tor_available
)
// Warning: If we reload the page while the Context is being initialized, this function will throw an error // Warning: If we reload the page while the Context is being initialized, this function will throw an error
initializeContext().catch((e) => { initializeContext().catch((e) => {
logger.error( logger.error(
e, e,
"Failed to initialize context on page load. This might be because we reloaded the page while the context was being initialized", "Failed to initialize context on page load. This might be because we reloaded the page while the context was being initialized",
); );
// Wait a short time before retrying store.dispatch(contextInitializationFailed(String(e)));
setTimeout(() => {
initializeContext().catch((e) => {
logger.error(e, "Failed to initialize context even after retry");
});
}, 2000);
}); });
}
// Listen for the unified event
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
const { channelName, event: eventData } = event.payload;
switch (channelName) {
case "SwapProgress":
store.dispatch(swapProgressEventReceived(eventData));
break;
case "ContextInitProgress":
store.dispatch(contextStatusEventReceived(eventData));
break;
case "CliLog":
store.dispatch(receivedCliLog(eventData));
break;
case "BalanceChange":
store.dispatch(rpcSetBalance(eventData.balance));
break;
case "SwapDatabaseStateUpdate":
getSwapInfo(eventData.swap_id);
// This is ugly but it's the best we can do for now
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
// in the database. So we wait a bit before fetching the new state
setTimeout(() => getSwapInfo(eventData.swap_id), 3000);
break;
case "TimelockChange":
store.dispatch(timelockChangeEventReceived(eventData));
break;
case "Approval":
store.dispatch(approvalEventReceived(eventData));
break;
case "BackgroundProgress":
store.dispatch(backgroundProgressEventReceived(eventData));
break;
case "PoolStatusUpdate":
store.dispatch(poolStatusReceived(eventData));
break;
case "MoneroWalletUpdate":
console.log("MoneroWalletUpdate", eventData);
if (eventData.type === "BalanceChange") {
store.dispatch(setBalance(eventData.content));
}
if (eventData.type === "HistoryUpdate") {
store.dispatch(setHistory(eventData.content));
}
if (eventData.type === "SyncProgress") {
store.dispatch(setSyncProgress(eventData.content));
}
break;
default:
exhaustiveGuard(channelName);
}
});
} }
// Listen for the unified event
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
const { channelName, event: eventData } = event.payload;
switch (channelName) {
case "SwapProgress":
store.dispatch(swapProgressEventReceived(eventData));
break;
case "CliLog":
store.dispatch(receivedCliLog(eventData));
break;
case "BalanceChange":
store.dispatch(rpcSetBalance(eventData.balance));
break;
case "SwapDatabaseStateUpdate":
getSwapInfo(eventData.swap_id);
// This is ugly but it's the best we can do for now
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
// in the database. So we wait a bit before fetching the new state
setTimeout(() => getSwapInfo(eventData.swap_id), 3000);
break;
case "TimelockChange":
store.dispatch(timelockChangeEventReceived(eventData));
break;
case "Approval":
store.dispatch(approvalEventReceived(eventData));
break;
case "BackgroundProgress":
store.dispatch(backgroundProgressEventReceived(eventData));
break;
case "PoolStatusUpdate":
store.dispatch(poolStatusReceived(eventData));
break;
case "MoneroWalletUpdate":
console.log("MoneroWalletUpdate", eventData);
if (eventData.type === "BalanceChange") {
store.dispatch(setBalance(eventData.content));
}
if (eventData.type === "HistoryUpdate") {
store.dispatch(setHistory(eventData.content));
}
if (eventData.type === "SyncProgress") {
store.dispatch(setSyncProgress(eventData.content));
}
break;
default:
exhaustiveGuard(channelName);
}
});

View file

@ -25,6 +25,7 @@ import SeedSelectionDialog from "./modal/seed-selection/SeedSelectionDialog";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog"; import PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog";
import ContextErrorDialog from "./modal/context-error/ContextErrorDialog";
declare module "@mui/material/styles" { declare module "@mui/material/styles" {
interface Theme { interface Theme {
@ -54,6 +55,7 @@ export default function App() {
<IntroductionModal /> <IntroductionModal />
<SeedSelectionDialog /> <SeedSelectionDialog />
<PasswordEntryDialog /> <PasswordEntryDialog />
<ContextErrorDialog />
<Router> <Router>
<Navigation /> <Navigation />
<InnerContent /> <InnerContent />

View file

@ -8,9 +8,12 @@ import {
Tooltip, Tooltip,
} from "@mui/material"; } from "@mui/material";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import { ContextStatus } from "models/tauriModel";
import { isContextFullyInitialized } from "models/tauriModelExt";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import { useIsContextAvailable } from "store/hooks"; import { ContextStatusType } from "store/features/rpcSlice";
import { useAppSelector, useIsContextAvailable } from "store/hooks";
interface PromiseInvokeButtonProps<T> { interface PromiseInvokeButtonProps<T> {
onSuccess?: (data: T) => void | null; onSuccess?: (data: T) => void | null;
@ -23,7 +26,10 @@ interface PromiseInvokeButtonProps<T> {
disabled?: boolean; disabled?: boolean;
displayErrorSnackbar?: boolean; displayErrorSnackbar?: boolean;
tooltipTitle?: string | null; tooltipTitle?: string | null;
requiresContext?: boolean; // true means that the entire context must be available
// false means that the context doesn't have to be available at all
// a custom function means that the context must satisfy the function
contextRequirement?: ((status: ContextStatus) => boolean) | false | true;
} }
export default function PromiseInvokeButton<T>({ export default function PromiseInvokeButton<T>({
@ -39,13 +45,11 @@ export default function PromiseInvokeButton<T>({
isChipButton = false, isChipButton = false,
displayErrorSnackbar = false, displayErrorSnackbar = false,
onPendingChange = null, onPendingChange = null,
requiresContext = true, contextRequirement = true,
tooltipTitle = null, tooltipTitle = null,
...rest ...rest
}: PromiseInvokeButtonProps<T> & ButtonProps) { }: PromiseInvokeButtonProps<T> & ButtonProps) {
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
const isContextAvailable = useIsContextAvailable();
const [isPending, setIsPending] = useState(false); const [isPending, setIsPending] = useState(false);
const isLoading = isPending || isLoadingOverride; const isLoading = isPending || isLoadingOverride;
@ -73,7 +77,23 @@ export default function PromiseInvokeButton<T>({
} }
} }
const requiresContextButNotAvailable = requiresContext && !isContextAvailable; const requiresContextButNotAvailable = useAppSelector((state) => {
const status = state.rpc.status;
if (contextRequirement === false) {
return false;
}
if (contextRequirement === true || contextRequirement == null) {
return !isContextFullyInitialized(status);
}
if (status == null || status.type === ContextStatusType.Error) {
return true;
}
return !contextRequirement(status.status);
});
const isDisabled = disabled || isLoading || requiresContextButNotAvailable; const isDisabled = disabled || isLoading || requiresContextButNotAvailable;
const actualTooltipTitle = const actualTooltipTitle =

View file

@ -5,10 +5,7 @@ import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks";
import { exhaustiveGuard } from "utils/typescriptUtils"; import { exhaustiveGuard } from "utils/typescriptUtils";
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert"; import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
import { bytesToMb } from "utils/conversionUtils"; import { bytesToMb } from "utils/conversionUtils";
import { import { TauriBackgroundProgress } from "models/tauriModel";
TauriBackgroundProgress,
TauriContextStatusEvent,
} from "models/tauriModel";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import TruncatedText from "../other/TruncatedText"; import TruncatedText from "../other/TruncatedText";
import BitcoinIcon from "../icons/BitcoinIcon"; import BitcoinIcon from "../icons/BitcoinIcon";
@ -182,41 +179,6 @@ function PartialInitStatus({
} }
} }
export default function DaemonStatusAlert() {
const contextStatus = useAppSelector((s) => s.rpc.status);
const navigate = useNavigate();
switch (contextStatus) {
case null:
return null;
case TauriContextStatusEvent.NotInitialized:
return null;
case TauriContextStatusEvent.Initializing:
return null;
case TauriContextStatusEvent.Available:
return <Alert severity="success">The daemon is running</Alert>;
case TauriContextStatusEvent.Failed:
return (
<Alert
severity="error"
action={
<Button
size="small"
variant="outlined"
onClick={() => navigate("/settings#daemon-control-box")}
>
View Logs
</Button>
}
>
The daemon has stopped unexpectedly
</Alert>
);
default:
return exhaustiveGuard(contextStatus);
}
}
export function BackgroundProgressAlerts() { export function BackgroundProgressAlerts() {
const backgroundProgress = usePendingBackgroundProcesses(); const backgroundProgress = usePendingBackgroundProcesses();

View file

@ -72,6 +72,7 @@ export default function SwapSuspendAlert({
color="primary" color="primary"
onSuccess={onClose} onSuccess={onClose}
onInvoke={suspendCurrentSwap} onInvoke={suspendCurrentSwap}
contextRequirement={false}
> >
Suspend Suspend
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -0,0 +1,63 @@
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Typography,
} from "@mui/material";
import { relaunch } from "@tauri-apps/plugin-process";
import { useAppSelector } from "store/hooks";
import CliLogsBox from "renderer/components/other/RenderedCliLog";
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
import ContactInfoBox from "renderer/components/other/ContactInfoBox";
import { ContextStatusType } from "store/features/rpcSlice";
export default function ContextErrorDialog() {
const logs = useAppSelector((state) => state.logs.state.logs);
const errorMessage = useAppSelector((state) =>
state.rpc.status?.type === ContextStatusType.Error
? state.rpc.status.error
: null,
);
if (errorMessage === null) {
return null;
}
return (
<Dialog open={true} maxWidth="md" fullWidth disableEscapeKeyDown>
<DialogTitle>Failed to start</DialogTitle>
<DialogContent>
<DialogContentText>
Check the logs below for details. Try restarting the GUI. Reach out to
the developers and the community if this continues.
</DialogContentText>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
}}
>
<Box sx={{ alignSelf: "center" }}>
<ContactInfoBox />
</Box>
<ActionableMonospaceTextBox
content={errorMessage}
displayCopyIcon={true}
enableQrCode={false}
/>
<CliLogsBox label="Logs" logs={logs} minHeight="30vh" />
</Box>
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={() => relaunch()}>
Restart GUI
</Button>
</DialogActions>
</Dialog>
);
}

View file

@ -227,7 +227,7 @@ export default function FeedbackDialog({
<DialogActions> <DialogActions>
<Button onClick={handleClose}>Cancel</Button> <Button onClick={handleClose}>Cancel</Button>
<PromiseInvokeButton <PromiseInvokeButton
requiresContext={false} contextRequirement={false}
color="primary" color="primary"
variant="contained" variant="contained"
onInvoke={submitFeedback} onInvoke={submitFeedback}

View file

@ -9,13 +9,13 @@ import {
Switch, Switch,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { CliLog } from "models/cliModel";
import CliLogsBox from "renderer/components/other/RenderedCliLog"; import CliLogsBox from "renderer/components/other/RenderedCliLog";
import { HashedLog } from "store/features/logsSlice";
interface LogViewerProps { interface LogViewerProps {
open: boolean; open: boolean;
setOpen: (_: boolean) => void; setOpen: (_: boolean) => void;
logs: (string | CliLog)[] | null; logs: HashedLog[];
setIsRedacted: (_: boolean) => void; setIsRedacted: (_: boolean) => void;
isRedacted: boolean; isRedacted: boolean;
} }

View file

@ -3,12 +3,13 @@ import { store } from "renderer/store/storeRenderer";
import { useActiveSwapInfo } from "store/hooks"; import { useActiveSwapInfo } from "store/hooks";
import { logsToRawString } from "utils/parseUtils"; import { logsToRawString } from "utils/parseUtils";
import { getLogsOfSwap, redactLogs } from "renderer/rpc"; import { getLogsOfSwap, redactLogs } from "renderer/rpc";
import { CliLog, parseCliLogString } from "models/cliModel"; import { parseCliLogString } from "models/cliModel";
import logger from "utils/logger"; import logger from "utils/logger";
import { submitFeedbackViaHttp } from "renderer/api"; import { submitFeedbackViaHttp } from "renderer/api";
import { addFeedbackId } from "store/features/conversationsSlice"; import { addFeedbackId } from "store/features/conversationsSlice";
import { AttachmentInput } from "models/apiModel"; import { AttachmentInput } from "models/apiModel";
import { useSnackbar } from "notistack"; import { useSnackbar } from "notistack";
import { HashedLog, hashLogs } from "store/features/logsSlice";
export const MAX_FEEDBACK_LENGTH = 4000; export const MAX_FEEDBACK_LENGTH = 4000;
@ -21,8 +22,8 @@ interface FeedbackInputState {
} }
interface FeedbackLogsState { interface FeedbackLogsState {
swapLogs: (string | CliLog)[] | null; swapLogs: HashedLog[];
daemonLogs: (string | CliLog)[] | null; daemonLogs: HashedLog[];
} }
const initialInputState: FeedbackInputState = { const initialInputState: FeedbackInputState = {
@ -34,8 +35,8 @@ const initialInputState: FeedbackInputState = {
}; };
const initialLogsState: FeedbackLogsState = { const initialLogsState: FeedbackLogsState = {
swapLogs: null, swapLogs: [],
daemonLogs: null, daemonLogs: [],
}; };
export function useFeedback() { export function useFeedback() {
@ -55,56 +56,60 @@ export function useFeedback() {
useEffect(() => { useEffect(() => {
if (inputState.selectedSwap === null) { if (inputState.selectedSwap === null) {
setLogsState((prev) => ({ ...prev, swapLogs: null })); setLogsState((prev) => ({ ...prev, swapLogs: [] }));
return; return;
} }
getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted) getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted)
.then((response) => { .then((response) => {
const parsedLogs = response.logs.map(parseCliLogString);
setLogsState((prev) => ({ setLogsState((prev) => ({
...prev, ...prev,
swapLogs: response.logs.map(parseCliLogString), swapLogs: hashLogs(parsedLogs),
})); }));
setError(null); setError(null);
}) })
.catch((e) => { .catch((e) => {
logger.error(`Failed to fetch swap logs: ${e}`); logger.error(`Failed to fetch swap logs: ${e}`);
setLogsState((prev) => ({ ...prev, swapLogs: null })); setLogsState((prev) => ({ ...prev, swapLogs: [] }));
setError(`Failed to fetch swap logs: ${e}`); setError(`Failed to fetch swap logs: ${e}`);
}); });
}, [inputState.selectedSwap, inputState.isSwapLogsRedacted]); }, [inputState.selectedSwap, inputState.isSwapLogsRedacted]);
useEffect(() => { useEffect(() => {
if (!inputState.attachDaemonLogs) { if (!inputState.attachDaemonLogs) {
setLogsState((prev) => ({ ...prev, daemonLogs: null })); setLogsState((prev) => ({ ...prev, daemonLogs: [] }));
return; return;
} }
try { try {
const hashedLogs = store.getState().logs?.state.logs ?? [];
if (inputState.isDaemonLogsRedacted) { if (inputState.isDaemonLogsRedacted) {
redactLogs(store.getState().logs?.state.logs) const logs = hashedLogs.map((h) => h.log);
redactLogs(logs)
.then((redactedLogs) => { .then((redactedLogs) => {
setLogsState((prev) => ({ setLogsState((prev) => ({
...prev, ...prev,
daemonLogs: redactedLogs, daemonLogs: hashLogs(redactedLogs),
})); }));
setError(null); setError(null);
}) })
.catch((e) => { .catch((e) => {
logger.error(`Failed to redact daemon logs: ${e}`); logger.error(`Failed to redact daemon logs: ${e}`);
setLogsState((prev) => ({ ...prev, daemonLogs: null })); setLogsState((prev) => ({ ...prev, daemonLogs: [] }));
setError(`Failed to redact daemon logs: ${e}`); setError(`Failed to redact daemon logs: ${e}`);
}); });
} else { } else {
setLogsState((prev) => ({ setLogsState((prev) => ({
...prev, ...prev,
daemonLogs: store.getState().logs?.state.logs, daemonLogs: hashedLogs,
})); }));
setError(null); setError(null);
} }
} catch (e) { } catch (e) {
logger.error(`Failed to fetch daemon logs: ${e}`); logger.error(`Failed to fetch daemon logs: ${e}`);
setLogsState((prev) => ({ ...prev, daemonLogs: null })); setLogsState((prev) => ({ ...prev, daemonLogs: [] }));
setError(`Failed to fetch daemon logs: ${e}`); setError(`Failed to fetch daemon logs: ${e}`);
} }
}, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted]); }, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted]);
@ -123,18 +128,18 @@ export function useFeedback() {
const attachments: AttachmentInput[] = []; const attachments: AttachmentInput[] = [];
// Add swap logs as an attachment // Add swap logs as an attachment
if (logsState.swapLogs) { if (logsState.swapLogs.length > 0) {
attachments.push({ attachments.push({
key: `swap_logs_${inputState.selectedSwap}.txt`, key: `swap_logs_${inputState.selectedSwap}.txt`,
content: logsToRawString(logsState.swapLogs), content: logsToRawString(logsState.swapLogs.map((h) => h.log)),
}); });
} }
// Handle daemon logs // Handle daemon logs
if (logsState.daemonLogs) { if (logsState.daemonLogs.length > 0) {
attachments.push({ attachments.push({
key: "daemon_logs.txt", key: "daemon_logs.txt",
content: logsToRawString(logsState.daemonLogs), content: logsToRawString(logsState.daemonLogs.map((h) => h.log)),
}); });
} }

View file

@ -116,7 +116,7 @@ export default function PasswordEntryDialog() {
<PromiseInvokeButton <PromiseInvokeButton
onInvoke={accept} onInvoke={accept}
variant="contained" variant="contained"
requiresContext={false} contextRequirement={false}
> >
Unlock Unlock
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -364,7 +364,7 @@ export default function SeedSelectionDialog() {
<PromiseInvokeButton <PromiseInvokeButton
variant="text" variant="text"
onInvoke={Legacy} onInvoke={Legacy}
requiresContext={false} contextRequirement={false}
color="inherit" color="inherit"
> >
No wallet (Legacy) No wallet (Legacy)
@ -373,7 +373,7 @@ export default function SeedSelectionDialog() {
onInvoke={accept} onInvoke={accept}
variant="contained" variant="contained"
disabled={isDisabled} disabled={isDisabled}
requiresContext={false} contextRequirement={false}
> >
Continue Continue
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -1,15 +1,9 @@
import { Box, DialogContentText } from "@mui/material"; import { Box, DialogContentText } from "@mui/material";
import { import { useActiveSwapLogs } from "store/hooks";
useActiveSwapInfo,
useActiveSwapLogs,
useAppSelector,
} from "store/hooks";
import JsonTreeView from "../../../other/JSONViewTree";
import CliLogsBox from "../../../other/RenderedCliLog"; import CliLogsBox from "../../../other/RenderedCliLog";
export default function DebugPage() { export default function DebugPage() {
const logs = useActiveSwapLogs(); const logs = useActiveSwapLogs();
const cliState = useActiveSwapInfo();
return ( return (
<Box sx={{ padding: 2, display: "flex", flexDirection: "column", gap: 2 }}> <Box sx={{ padding: 2, display: "flex", flexDirection: "column", gap: 2 }}>

View file

@ -6,6 +6,7 @@ import DialogHeader from "../DialogHeader";
import AddressInputPage from "./pages/AddressInputPage"; import AddressInputPage from "./pages/AddressInputPage";
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage"; import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
import WithdrawDialogContent from "./WithdrawDialogContent"; import WithdrawDialogContent from "./WithdrawDialogContent";
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
export default function WithdrawDialog({ export default function WithdrawDialog({
open, open,
@ -61,6 +62,7 @@ export default function WithdrawDialog({
onInvoke={() => withdrawBtc(withdrawAddress)} onInvoke={() => withdrawBtc(withdrawAddress)}
onPendingChange={setPending} onPendingChange={setPending}
onSuccess={setWithdrawTxId} onSuccess={setWithdrawTxId}
contextRequirement={isContextWithBitcoinWallet}
> >
Withdraw Withdraw
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -1,14 +1,9 @@
import { Box, Tooltip } from "@mui/material"; import { Box, Tooltip } from "@mui/material";
import GitHubIcon from "@mui/icons-material/GitHub"; import { BackgroundProgressAlerts } from "../alert/DaemonStatusAlert";
import DaemonStatusAlert, {
BackgroundProgressAlerts,
} from "../alert/DaemonStatusAlert";
import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert"; import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert"; import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
import LinkIconButton from "../icons/LinkIconButton";
import BackgroundRefundAlert from "../alert/BackgroundRefundAlert"; import BackgroundRefundAlert from "../alert/BackgroundRefundAlert";
import MatrixIcon from "../icons/MatrixIcon"; import ContactInfoBox from "../other/ContactInfoBox";
import { MenuBook } from "@mui/icons-material";
export default function NavigationFooter() { export default function NavigationFooter() {
return ( return (
@ -23,36 +18,8 @@ export default function NavigationFooter() {
<FundsLeftInWalletAlert /> <FundsLeftInWalletAlert />
<UnfinishedSwapsAlert /> <UnfinishedSwapsAlert />
<BackgroundRefundAlert /> <BackgroundRefundAlert />
<DaemonStatusAlert />
<BackgroundProgressAlerts /> <BackgroundProgressAlerts />
<Box <ContactInfoBox />
sx={{
display: "flex",
justifyContent: "space-evenly",
}}
>
<Tooltip title="Check out the GitHub repository">
<span>
<LinkIconButton url="https://github.com/eigenwallet/core">
<GitHubIcon />
</LinkIconButton>
</span>
</Tooltip>
<Tooltip title="Join the Matrix room">
<span>
<LinkIconButton url="https://matrix.to/#/#unstoppableswap-space:matrix.org">
<MatrixIcon />
</LinkIconButton>
</span>
</Tooltip>
<Tooltip title="Read our official documentation">
<span>
<LinkIconButton url="https://docs.unstoppableswap.net">
<MenuBook />
</LinkIconButton>
</span>
</Tooltip>
</Box>
</Box> </Box>
); );
} }

View file

@ -0,0 +1,46 @@
import MatrixIcon from "../icons/MatrixIcon";
import { MenuBook } from "@mui/icons-material";
import DiscordIcon from "../icons/DiscordIcon";
import GitHubIcon from "@mui/icons-material/GitHub";
import LinkIconButton from "../icons/LinkIconButton";
import { Box, Tooltip } from "@mui/material";
export default function ContactInfoBox() {
return (
<Box
sx={{
display: "flex",
justifyContent: "space-evenly",
}}
>
<Tooltip title="Check out the GitHub repository">
<span>
<LinkIconButton url="https://github.com/eigenwallet/core">
<GitHubIcon />
</LinkIconButton>
</span>
</Tooltip>
<Tooltip title="Join the Matrix room">
<span>
<LinkIconButton url="https://eigenwallet.org/matrix">
<MatrixIcon />
</LinkIconButton>
</span>
</Tooltip>
<Tooltip title="Join the Discord server">
<span>
<LinkIconButton url="https://eigenwallet.org/discord">
<DiscordIcon />
</LinkIconButton>
</span>
</Tooltip>
<Tooltip title="Read our official documentation">
<span>
<LinkIconButton url="https://docs.unstoppableswap.net">
<MenuBook />
</LinkIconButton>
</span>
</Tooltip>
</Box>
);
}

View file

@ -1,5 +1,6 @@
import { Box, Chip, Typography } from "@mui/material"; import { Box, Chip, Typography } from "@mui/material";
import { CliLog } from "models/cliModel"; import { CliLog } from "models/cliModel";
import { HashedLog } from "store/features/logsSlice";
import { ReactNode, 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";
@ -62,44 +63,54 @@ export default function CliLogsBox({
label, label,
logs, logs,
topRightButton = null, topRightButton = null,
autoScroll = false, autoScroll = true,
minHeight, minHeight,
}: { }: {
label: string; label: string;
logs: (CliLog | string)[]; logs: HashedLog[];
topRightButton?: ReactNode; topRightButton?: ReactNode;
autoScroll?: boolean; autoScroll?: boolean;
minHeight?: string; minHeight?: string;
}) { }) {
const [searchQuery, setSearchQuery] = useState<string>(""); const [searchQuery, setSearchQuery] = useState<string>("");
const memoizedLogs = useMemo(() => { const filteredLogs = useMemo(() => {
if (searchQuery.length === 0) { if (searchQuery.length === 0) {
return logs; return logs;
} }
return logs.filter((log) =>
return logs.filter(({ log }) =>
JSON.stringify(log).toLowerCase().includes(searchQuery.toLowerCase()), JSON.stringify(log).toLowerCase().includes(searchQuery.toLowerCase()),
); );
}, [logs, searchQuery]); }, [logs, searchQuery]);
const rows = useMemo(() => {
return filteredLogs.map(({ log, hash }) =>
typeof log === "string" ? (
<Typography key={hash} component="pre">
{log}
</Typography>
) : (
<RenderedCliLog log={log} key={hash} />
),
);
}, [filteredLogs]);
const rawStrings = useMemo(
() => filteredLogs.map(({ log }) => log),
[filteredLogs],
);
return ( return (
<ScrollablePaperTextBox <ScrollablePaperTextBox
minHeight={minHeight} minHeight={minHeight}
title={label} title={label}
copyValue={logsToRawString(logs)} copyValue={logsToRawString(rawStrings)}
searchQuery={searchQuery} searchQuery={searchQuery}
setSearchQuery={setSearchQuery} setSearchQuery={setSearchQuery}
topRightButton={topRightButton} topRightButton={topRightButton}
autoScroll={autoScroll} autoScroll={autoScroll}
rows={memoizedLogs.map((log) => rows={rows}
typeof log === "string" ? (
<Typography key={log} component="pre">
{log}
</Typography>
) : (
<RenderedCliLog log={log} key={JSON.stringify(log)} />
),
)}
/> />
); );
} }

View file

@ -293,7 +293,7 @@ function ConversationModal({
// Fetch updated conversations // Fetch updated conversations
fetchAllConversations(); fetchAllConversations();
} catch (error) { } catch (error) {
logger.error("Error sending message:", error); logger.error(`Error sending message: ${error}`);
enqueueSnackbar("Failed to send message. Please try again.", { enqueueSnackbar("Failed to send message. Please try again.", {
variant: "error", variant: "error",
}); });

View file

@ -1,31 +1,27 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import FolderOpenIcon from "@mui/icons-material/FolderOpen"; import FolderOpenIcon from "@mui/icons-material/FolderOpen";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox"; import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
import CliLogsBox from "renderer/components/other/RenderedCliLog"; import CliLogsBox from "renderer/components/other/RenderedCliLog";
import { getDataDir, initializeContext } from "renderer/rpc"; import { getDataDir } from "renderer/rpc";
import { relaunch } from "@tauri-apps/plugin-process"; import { relaunch } from "@tauri-apps/plugin-process";
import RotateLeftIcon from "@mui/icons-material/RotateLeft"; import RotateLeftIcon from "@mui/icons-material/RotateLeft";
import { revealItemInDir } from "@tauri-apps/plugin-opener"; import { revealItemInDir } from "@tauri-apps/plugin-opener";
import { TauriContextStatusEvent } from "models/tauriModel"; import { ContextStatusType } from "store/features/rpcSlice";
export default function DaemonControlBox() { export default function DaemonControlBox() {
const logs = useAppSelector((s) => s.logs.state.logs); const logs = useAppSelector((s) => s.logs.state.logs);
// The daemon can be manually started if it has failed or if it has not been started yet const stringifiedDaemonStatus = useAppSelector((s) => {
const canContextBeManuallyStarted = useAppSelector( if (s.rpc.status === null) {
(s) => return "not started";
s.rpc.status === TauriContextStatusEvent.Failed || s.rpc.status === null, }
); if (s.rpc.status.type === ContextStatusType.Error) {
const isContextInitializing = useAppSelector( return "failed";
(s) => s.rpc.status === TauriContextStatusEvent.Initializing, }
); return "running";
});
const stringifiedDaemonStatus = useAppSelector(
(s) => s.rpc.status ?? "not started",
);
return ( return (
<InfoBox <InfoBox
@ -36,22 +32,11 @@ export default function DaemonControlBox() {
} }
additionalContent={ additionalContent={
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}> <Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<PromiseInvokeButton
variant="contained"
endIcon={<PlayArrowIcon />}
onInvoke={initializeContext}
requiresContext={false}
disabled={!canContextBeManuallyStarted}
isLoadingOverride={isContextInitializing}
displayErrorSnackbar
>
Start Daemon
</PromiseInvokeButton>
<PromiseInvokeButton <PromiseInvokeButton
variant="contained" variant="contained"
endIcon={<RotateLeftIcon />} endIcon={<RotateLeftIcon />}
onInvoke={relaunch} onInvoke={relaunch}
requiresContext={false} contextRequirement={false}
displayErrorSnackbar displayErrorSnackbar
> >
Restart GUI Restart GUI
@ -59,7 +44,7 @@ export default function DaemonControlBox() {
<PromiseInvokeButton <PromiseInvokeButton
endIcon={<FolderOpenIcon />} endIcon={<FolderOpenIcon />}
isIconButton isIconButton
requiresContext={false} contextRequirement={false}
size="small" size="small"
tooltipTitle="Open the data directory in your file explorer" tooltipTitle="Open the data directory in your file explorer"
onInvoke={async () => { onInvoke={async () => {

View file

@ -15,6 +15,7 @@ import { getWalletDescriptor } from "renderer/rpc";
import { ExportBitcoinWalletResponse } from "models/tauriModel"; import { ExportBitcoinWalletResponse } from "models/tauriModel";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox"; import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
export default function ExportDataBox() { export default function ExportDataBox() {
const [walletDescriptor, setWalletDescriptor] = const [walletDescriptor, setWalletDescriptor] =
@ -52,6 +53,7 @@ export default function ExportDataBox() {
onInvoke={getWalletDescriptor} onInvoke={getWalletDescriptor}
onSuccess={setWalletDescriptor} onSuccess={setWalletDescriptor}
displayErrorSnackbar={true} displayErrorSnackbar={true}
contextRequirement={isContextWithBitcoinWallet}
> >
Reveal Bitcoin Wallet Private Key Reveal Bitcoin Wallet Private Key
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -14,7 +14,8 @@ export default function ExportLogsButton({
}: ExportLogsButtonProps) { }: ExportLogsButtonProps) {
async function handleExportLogs() { async function handleExportLogs() {
const swapLogs = await getLogsOfSwap(swap_id, false); const swapLogs = await getLogsOfSwap(swap_id, false);
const daemonLogs = store.getState().logs?.state.logs; const hashedDaemonLogs = store.getState().logs?.state.logs ?? [];
const daemonLogs = hashedDaemonLogs.map((h) => h.log);
const logContent = { const logContent = {
swap_logs: logsToRawString(swapLogs.logs), swap_logs: logsToRawString(swapLogs.logs),

View file

@ -6,21 +6,23 @@ import {
DialogTitle, DialogTitle,
} from "@mui/material"; } from "@mui/material";
import { ButtonProps } from "@mui/material/Button"; import { ButtonProps } from "@mui/material/Button";
import { CliLog, parseCliLogString } from "models/cliModel"; import { parseCliLogString } from "models/cliModel";
import { GetLogsResponse } from "models/tauriModel"; import { GetLogsResponse } from "models/tauriModel";
import { useState } from "react"; import { useState } from "react";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { getLogsOfSwap } from "renderer/rpc"; import { getLogsOfSwap } from "renderer/rpc";
import CliLogsBox from "../../../other/RenderedCliLog"; import CliLogsBox from "../../../other/RenderedCliLog";
import { HashedLog, hashLogs } from "store/features/logsSlice";
export default function SwapLogFileOpenButton({ export default function SwapLogFileOpenButton({
swapId, swapId,
...props ...props
}: { swapId: string } & ButtonProps) { }: { swapId: string } & ButtonProps) {
const [logs, setLogs] = useState<(CliLog | string)[] | null>(null); const [logs, setLogs] = useState<HashedLog[] | null>(null);
function onLogsReceived(response: GetLogsResponse) { function onLogsReceived(response: GetLogsResponse) {
setLogs(response.logs.map(parseCliLogString)); const parsedLogs = response.logs.map(parseCliLogString);
setLogs(hashLogs(parsedLogs));
} }
return ( return (

View file

@ -6,6 +6,7 @@ import {
GetMoneroSeedResponse, GetMoneroSeedResponse,
GetRestoreHeightResponse, GetRestoreHeightResponse,
} from "models/tauriModel"; } from "models/tauriModel";
import { isContextWithMoneroWallet } from "models/tauriModelExt";
interface SeedPhraseButtonProps { interface SeedPhraseButtonProps {
onMenuClose: () => void; onMenuClose: () => void;
@ -32,6 +33,7 @@ export default function SeedPhraseButton({
onSuccess={handleSeedPhraseSuccess} onSuccess={handleSeedPhraseSuccess}
displayErrorSnackbar={true} displayErrorSnackbar={true}
variant="text" variant="text"
contextRequirement={isContextWithMoneroWallet}
sx={{ sx={{
justifyContent: "flex-start", justifyContent: "flex-start",
textTransform: "none", textTransform: "none",

View file

@ -17,6 +17,7 @@ import { getRestoreHeight, setMoneroRestoreHeight } from "renderer/rpc";
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { Dayjs } from "dayjs"; import { Dayjs } from "dayjs";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { isContextWithMoneroWallet } from "models/tauriModelExt";
enum RestoreOption { enum RestoreOption {
BlockHeight = "blockHeight", BlockHeight = "blockHeight",
@ -133,6 +134,7 @@ export default function SetRestoreHeightModal({
onSuccess={onClose} onSuccess={onClose}
displayErrorSnackbar={true} displayErrorSnackbar={true}
onPendingChange={setIsPending} onPendingChange={setIsPending}
contextRequirement={isContextWithMoneroWallet}
> >
Confirm Confirm
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -13,6 +13,7 @@ import DFXSwissLogo from "assets/dfx-logo.svg";
import { useState } from "react"; import { useState } from "react";
import { dfxAuthenticate } from "renderer/rpc"; import { dfxAuthenticate } from "renderer/rpc";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { isContextWithMoneroWallet } from "models/tauriModelExt";
function DFXLogo({ height = 24 }: { height?: number }) { function DFXLogo({ height = 24 }: { height?: number }) {
return ( return (
@ -53,6 +54,7 @@ export default function DfxButton() {
tooltipTitle="Buy Monero with fiat using DFX" tooltipTitle="Buy Monero with fiat using DFX"
displayErrorSnackbar displayErrorSnackbar
isChipButton isChipButton
contextRequirement={isContextWithMoneroWallet}
> >
Buy Monero Buy Monero
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -130,7 +130,7 @@ export default function SendApprovalContent({
color="error" color="error"
startIcon={<CloseIcon />} startIcon={<CloseIcon />}
displayErrorSnackbar={true} displayErrorSnackbar={true}
requiresContext={false} contextRequirement={false}
> >
Reject Reject
</PromiseInvokeButton> </PromiseInvokeButton>
@ -141,7 +141,7 @@ export default function SendApprovalContent({
color="primary" color="primary"
startIcon={<CheckIcon />} startIcon={<CheckIcon />}
displayErrorSnackbar={true} displayErrorSnackbar={true}
requiresContext={false} contextRequirement={false}
> >
Send Send
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -14,6 +14,7 @@ import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { sendMoneroTransaction } from "renderer/rpc"; import { sendMoneroTransaction } from "renderer/rpc";
import { useAppSelector } from "store/hooks"; import { useAppSelector } from "store/hooks";
import { SendMoneroResponse } from "models/tauriModel"; import { SendMoneroResponse } from "models/tauriModel";
import { isContextWithMoneroWallet } from "models/tauriModelExt";
interface SendTransactionContentProps { interface SendTransactionContentProps {
balance: { balance: {
@ -168,6 +169,7 @@ export default function SendTransactionContent({
disabled={isSendDisabled} disabled={isSendDisabled}
onSuccess={handleSendSuccess} onSuccess={handleSendSuccess}
onPendingChange={setIsSending} onPendingChange={setIsSending}
contextRequirement={isContextWithMoneroWallet}
> >
Send Send
</PromiseInvokeButton> </PromiseInvokeButton>

View file

@ -145,7 +145,6 @@ export default function SwapSetupInflightPage({
resolveApproval(request.request_id, false as unknown as object) resolveApproval(request.request_id, false as unknown as object)
} }
displayErrorSnackbar displayErrorSnackbar
requiresContext
> >
Deny Deny
</PromiseInvokeButton> </PromiseInvokeButton>
@ -158,7 +157,6 @@ export default function SwapSetupInflightPage({
resolveApproval(request.request_id, true as unknown as object) resolveApproval(request.request_id, true as unknown as object)
} }
displayErrorSnackbar displayErrorSnackbar
requiresContext
endIcon={<CheckIcon />} endIcon={<CheckIcon />}
> >
{`Confirm`} {`Confirm`}

View file

@ -2,6 +2,7 @@ import RefreshIcon from "@mui/icons-material/Refresh";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton"; import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { checkBitcoinBalance } from "renderer/rpc"; import { checkBitcoinBalance } from "renderer/rpc";
import { isSyncingBitcoin } from "store/hooks"; import { isSyncingBitcoin } from "store/hooks";
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
export default function WalletRefreshButton() { export default function WalletRefreshButton() {
const isSyncing = isSyncingBitcoin(); const isSyncing = isSyncingBitcoin();
@ -14,6 +15,7 @@ export default function WalletRefreshButton() {
onInvoke={() => checkBitcoinBalance()} onInvoke={() => checkBitcoinBalance()}
displayErrorSnackbar displayErrorSnackbar
size="small" size="small"
contextRequirement={isContextWithBitcoinWallet}
/> />
); );
} }

View file

@ -46,11 +46,13 @@ import {
GetRestoreHeightResponse, GetRestoreHeightResponse,
MoneroNodeConfig, MoneroNodeConfig,
GetMoneroSeedResponse, GetMoneroSeedResponse,
ContextStatus,
} from "models/tauriModel"; } from "models/tauriModel";
import { import {
rpcSetBalance, rpcSetBalance,
rpcSetSwapInfo, rpcSetSwapInfo,
approvalRequestsReplaced, approvalRequestsReplaced,
contextInitializationFailed,
} from "store/features/rpcSlice"; } from "store/features/rpcSlice";
import { import {
setMainAddress, setMainAddress,
@ -282,9 +284,8 @@ export async function getMoneroRecoveryKeys(
); );
} }
export async function checkContextAvailability(): Promise<boolean> { export async function checkContextStatus(): Promise<ContextStatus> {
const available = await invokeNoArgs<boolean>("is_context_available"); return await invokeNoArgs<ContextStatus>("get_context_status");
return available;
} }
export async function getLogsOfSwap( export async function getLogsOfSwap(
@ -335,7 +336,6 @@ export async function initializeContext() {
store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null; store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null;
// Check the state of the Monero node // Check the state of the Monero node
const moneroNodeConfig = const moneroNodeConfig =
useMoneroRpcPool || useMoneroRpcPool ||
moneroNodeUrl == null || moneroNodeUrl == null ||
@ -356,18 +356,17 @@ export async function initializeContext() {
enable_monero_tor: useMoneroTor, enable_monero_tor: useMoneroTor,
}; };
logger.info("Initializing context with settings", tauriSettings); logger.info({ tauriSettings }, "Initializing context with settings");
try { try {
await invokeUnsafe<void>("initialize_context", { await invokeUnsafe<void>("initialize_context", {
settings: tauriSettings, settings: tauriSettings,
testnet, testnet,
}); });
logger.info("Initialized context");
} catch (error) { } catch (error) {
throw new Error("Couldn't initialize context: " + error); throw new Error(error);
} }
logger.info("Initialized context");
} }
export async function getWalletDescriptor() { export async function getWalletDescriptor() {

View file

@ -2,9 +2,13 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { TauriLogEvent } from "models/tauriModel"; import { TauriLogEvent } from "models/tauriModel";
import { parseLogsFromString } from "utils/parseUtils"; import { parseLogsFromString } from "utils/parseUtils";
import { CliLog } from "models/cliModel"; import { CliLog } from "models/cliModel";
import { fnv1a } from "utils/hash";
/// We only keep the last 5000 logs in the store
const MAX_LOG_ENTRIES = 5000;
interface LogsState { interface LogsState {
logs: (CliLog | string)[]; logs: HashedLog[];
} }
export interface LogsSlice { export interface LogsSlice {
@ -17,21 +21,67 @@ const initialState: LogsSlice = {
}, },
}; };
export type HashedLog = {
log: CliLog | string;
hash: string;
};
export const logsSlice = createSlice({ export const logsSlice = createSlice({
name: "logs", name: "logs",
initialState, initialState,
reducers: { reducers: {
receivedCliLog(slice, action: PayloadAction<TauriLogEvent>) { receivedCliLog(slice, action: PayloadAction<TauriLogEvent>) {
const buffer = action.payload.buffer; const parsedLogs = parseLogsFromString(action.payload.buffer);
const logs = parseLogsFromString(buffer); const hashedLogs = parsedLogs.map(createHashedLog);
const logsWithoutExisting = logs.filter( for (const entry of hashedLogs) {
(log) => !slice.state.logs.includes(log), slice.state.logs.push(entry);
); }
slice.state.logs = slice.state.logs.concat(logsWithoutExisting);
// If we have too many logs, discard 1/10 of them (oldest logs)
// We explictly discard more than we need to, such that we don't have to
// do this too often
if (slice.state.logs.length > MAX_LOG_ENTRIES) {
const removeCount = Math.floor(slice.state.logs.length / 10);
slice.state.logs = slice.state.logs.slice(removeCount);
}
},
clearLogs(slice) {
slice.state.logs = [];
}, },
}, },
}); });
export const { receivedCliLog } = logsSlice.actions; function serializeLog(log: CliLog | string): string {
if (typeof log === "string") {
return `str:${log}`;
}
const parts = [
"obj",
log.timestamp,
log.level,
log.target ?? "",
JSON.stringify(log.fields),
];
if (log.spans != null && log.spans.length > 0) {
parts.push(JSON.stringify(log.spans));
}
return parts.join("|");
}
function createHashedLog(log: CliLog | string): HashedLog {
return {
log,
hash: fnv1a(serializeLog(log)),
};
}
export function hashLogs(logs: (CliLog | string)[]): HashedLog[] {
return logs.map(createHashedLog);
}
export const { receivedCliLog, clearLogs } = logsSlice.actions;
export default logsSlice.reducer; export default logsSlice.reducer;

View file

@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedMakerStatus, MakerStatus } from "models/apiModel"; import { ExtendedMakerStatus, MakerStatus } from "models/apiModel";
import { import {
GetSwapInfoResponse, GetSwapInfoResponse,
TauriContextStatusEvent, ContextStatus,
TauriTimelockChangeEvent, TauriTimelockChangeEvent,
BackgroundRefundState, BackgroundRefundState,
ApprovalRequest, ApprovalRequest,
@ -37,8 +37,17 @@ interface State {
}; };
} }
export enum ContextStatusType {
Status = "status",
Error = "error",
}
export type ResultContextStatus =
| { type: ContextStatusType.Status; status: ContextStatus }
| { type: ContextStatusType.Error; error: string };
export interface RPCSlice { export interface RPCSlice {
status: TauriContextStatusEvent | null; status: ResultContextStatus | null;
state: State; state: State;
} }
@ -60,11 +69,18 @@ export const rpcSlice = createSlice({
name: "rpc", name: "rpc",
initialState, initialState,
reducers: { reducers: {
contextStatusEventReceived( contextStatusEventReceived(slice, action: PayloadAction<ContextStatus>) {
slice, // Don't overwrite error state
action: PayloadAction<TauriContextStatusEvent>, //
) { // Once we're in an error state, stay there
slice.status = action.payload; if (slice.status?.type === ContextStatusType.Error) {
return;
}
slice.status = { type: ContextStatusType.Status, status: action.payload };
},
contextInitializationFailed(slice, action: PayloadAction<string>) {
slice.status = { type: ContextStatusType.Error, error: action.payload };
}, },
timelockChangeEventReceived( timelockChangeEventReceived(
slice: RPCSlice, slice: RPCSlice,
@ -160,6 +176,7 @@ export const rpcSlice = createSlice({
export const { export const {
contextStatusEventReceived, contextStatusEventReceived,
contextInitializationFailed,
rpcSetBalance, rpcSetBalance,
rpcSetWithdrawTxId, rpcSetWithdrawTxId,
rpcResetWithdrawTxId, rpcResetWithdrawTxId,

View file

@ -16,6 +16,7 @@ import {
isPendingSendMoneroApprovalEvent, isPendingSendMoneroApprovalEvent,
PendingPasswordApprovalRequest, PendingPasswordApprovalRequest,
isPendingPasswordApprovalEvent, isPendingPasswordApprovalEvent,
isContextFullyInitialized,
} from "models/tauriModelExt"; } from "models/tauriModelExt";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from "renderer/store/storeRenderer"; import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
@ -28,7 +29,6 @@ import { RatesState } from "./features/ratesSlice";
import { import {
TauriBackgroundProgress, TauriBackgroundProgress,
TauriBitcoinSyncProgress, TauriBitcoinSyncProgress,
TauriContextStatusEvent,
} from "models/tauriModel"; } from "models/tauriModel";
export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppDispatch = () => useDispatch<AppDispatch>();
@ -111,9 +111,7 @@ export function useIsSpecificSwapRunning(swapId: string | null) {
} }
export function useIsContextAvailable() { export function useIsContextAvailable() {
return useAppSelector( return useAppSelector((state) => isContextFullyInitialized(state.rpc.status));
(state) => state.rpc.status === TauriContextStatusEvent.Available,
);
} }
/// We do not use a sanity check here, as opposed to the other useSwapInfo hooks, /// We do not use a sanity check here, as opposed to the other useSwapInfo hooks,
@ -139,10 +137,13 @@ export function useActiveSwapLogs() {
const swapId = useActiveSwapId(); const swapId = useActiveSwapId();
const logs = useAppSelector((s) => s.logs.state.logs); const logs = useAppSelector((s) => s.logs.state.logs);
return useMemo( return useMemo(() => {
() => logs.filter((log) => isCliLogRelatedToSwap(log, swapId)), if (swapId == null) {
[logs, swapId], return [];
); }
return logs.filter((log) => isCliLogRelatedToSwap(log.log, swapId));
}, [logs, swapId]);
} }
export function useAllMakers() { export function useAllMakers() {

View file

@ -11,7 +11,10 @@ import {
getCurrentMoneroNodeConfig, getCurrentMoneroNodeConfig,
} from "renderer/rpc"; } from "renderer/rpc";
import logger from "utils/logger"; import logger from "utils/logger";
import { contextStatusEventReceived } from "store/features/rpcSlice"; import {
contextStatusEventReceived,
ContextStatusType,
} from "store/features/rpcSlice";
import { import {
addNode, addNode,
setFetchFiatPrices, setFetchFiatPrices,
@ -21,14 +24,12 @@ import {
Network, Network,
} from "store/features/settingsSlice"; } from "store/features/settingsSlice";
import { fetchFeedbackMessagesViaHttp, updateRates } from "renderer/api"; import { fetchFeedbackMessagesViaHttp, updateRates } from "renderer/api";
import { store } from "renderer/store/storeRenderer"; import { RootState, store } from "renderer/store/storeRenderer";
import { swapProgressEventReceived } from "store/features/swapSlice"; import { swapProgressEventReceived } from "store/features/swapSlice";
import { import {
addFeedbackId, addFeedbackId,
setConversation, setConversation,
} from "store/features/conversationsSlice"; } from "store/features/conversationsSlice";
import { TauriContextStatusEvent, MoneroNodeConfig } from "models/tauriModel";
import { getNetwork } from "store/config";
// Create a Map to store throttled functions per swap_id // Create a Map to store throttled functions per swap_id
const throttledGetSwapInfoFunctions = new Map< const throttledGetSwapInfoFunctions = new Map<
@ -60,30 +61,80 @@ const getThrottledSwapInfoUpdater = (swapId: string) => {
export function createMainListeners() { export function createMainListeners() {
const listener = createListenerMiddleware(); const listener = createListenerMiddleware();
// Listener for when the Context becomes available // Listener for when the Context status state changes
// When the context becomes available, we check the bitcoin balance, fetch all swap infos and connect to the rendezvous point // When the context becomes available, we check the bitcoin balance, fetch all swap infos and connect to the rendezvous point
listener.startListening({ listener.startListening({
actionCreator: contextStatusEventReceived, predicate: (action, currentState, previousState) => {
effect: async (action) => { const currentStatus = (currentState as RootState).rpc.status;
const status = action.payload; const previousStatus = (previousState as RootState).rpc.status;
// If the context is available, check the Bitcoin balance and fetch all swap infos // Only trigger if the status actually changed
if (status === TauriContextStatusEvent.Available) { return currentStatus !== previousStatus;
logger.debug( },
"Context is available, checking Bitcoin balance and history", effect: async (action, api) => {
const currentStatus = (api.getState() as RootState).rpc.status;
const previousStatus = (api.getOriginalState() as RootState).rpc.status;
const status =
currentStatus?.type === ContextStatusType.Status
? currentStatus.status
: null;
const previousContextStatus =
previousStatus?.type === ContextStatusType.Status
? previousStatus.status
: null;
if (!status) return;
// If the Bitcoin wallet just came available, check the Bitcoin balance
if (
status.bitcoin_wallet_available &&
!previousContextStatus?.bitcoin_wallet_available
) {
logger.info(
"Bitcoin wallet just became available, checking balance...",
); );
await Promise.allSettled([ await checkBitcoinBalance();
checkBitcoinBalance(), }
getAllSwapInfos(),
fetchSellersAtPresetRendezvousPoints(), // If the Monero wallet just came available, initialize the Monero wallet
initializeMoneroWallet(), if (
]); status.monero_wallet_available &&
!previousContextStatus?.monero_wallet_available
) {
logger.info("Monero wallet just became available, initializing...");
await initializeMoneroWallet();
// Also set the Monero node to the current one // Also set the Monero node to the current one
// In case the user changed this WHILE the context was unavailable
const nodeConfig = await getCurrentMoneroNodeConfig(); const nodeConfig = await getCurrentMoneroNodeConfig();
await changeMoneroNode(nodeConfig); await changeMoneroNode(nodeConfig);
} }
// If the database and Bitcoin wallet just came available, fetch all swap infos
if (
status.database_available &&
status.bitcoin_wallet_available &&
!(
previousContextStatus?.database_available &&
previousContextStatus?.bitcoin_wallet_available
)
) {
logger.info(
"Database & Bitcoin wallet just became available, fetching swap infos...",
);
await getAllSwapInfos();
}
// If the database just became availiable, fetch sellers at preset rendezvous points
if (
status.database_available &&
!previousContextStatus?.database_available
) {
logger.info(
"Database just became available, fetching sellers at preset rendezvous points...",
);
await fetchSellersAtPresetRendezvousPoints();
}
}, },
}); });
@ -151,9 +202,9 @@ export function createMainListeners() {
try { try {
const nodeConfig = await getCurrentMoneroNodeConfig(); const nodeConfig = await getCurrentMoneroNodeConfig();
await changeMoneroNode(nodeConfig); await changeMoneroNode(nodeConfig);
logger.info("Changed Monero node configuration to: ", nodeConfig); logger.info({ nodeConfig }, "Changed Monero node configuration to: ");
} catch (error) { } catch (error) {
logger.error("Failed to change Monero node configuration:", error); logger.error({ error }, "Failed to change Monero node configuration:");
} }
}, },
}); });

13
src-gui/src/utils/hash.ts Normal file
View file

@ -0,0 +1,13 @@
const FNV_OFFSET_BASIS = 0x811c9dc5;
const FNV_PRIME = 0x01000193;
export function fnv1a(value: string): string {
let hash = FNV_OFFSET_BASIS;
for (let i = 0; i < value.length; i += 1) {
hash ^= value.charCodeAt(i);
hash = (hash * FNV_PRIME) >>> 0;
}
return hash.toString(16).padStart(8, "0");
}

View file

@ -2,16 +2,7 @@
"$schema": "../gen/schemas/desktop-schema.json", "$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability", "identifier": "desktop-capability",
"description": "Capabilities for desktop windows", "description": "Capabilities for desktop windows",
"platforms": [ "platforms": ["macOS", "windows", "linux"],
"macOS", "windows": ["main"],
"windows", "permissions": ["cli:default", "cli:allow-cli-matches"]
"linux" }
],
"windows": [
"main"
],
"permissions": [
"cli:default",
"cli:allow-cli-matches"
]
}

View file

@ -1,116 +1,116 @@
{ {
"images" : [ "images": [
{ {
"size" : "20x20", "size": "20x20",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-20x20@2x.png", "filename": "AppIcon-20x20@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "20x20", "size": "20x20",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-20x20@3x.png", "filename": "AppIcon-20x20@3x.png",
"scale" : "3x" "scale": "3x"
}, },
{ {
"size" : "29x29", "size": "29x29",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-29x29@2x-1.png", "filename": "AppIcon-29x29@2x-1.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "29x29", "size": "29x29",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-29x29@3x.png", "filename": "AppIcon-29x29@3x.png",
"scale" : "3x" "scale": "3x"
}, },
{ {
"size" : "40x40", "size": "40x40",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-40x40@2x.png", "filename": "AppIcon-40x40@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "40x40", "size": "40x40",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-40x40@3x.png", "filename": "AppIcon-40x40@3x.png",
"scale" : "3x" "scale": "3x"
}, },
{ {
"size" : "60x60", "size": "60x60",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-60x60@2x.png", "filename": "AppIcon-60x60@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "60x60", "size": "60x60",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "AppIcon-60x60@3x.png", "filename": "AppIcon-60x60@3x.png",
"scale" : "3x" "scale": "3x"
}, },
{ {
"size" : "20x20", "size": "20x20",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-20x20@1x.png", "filename": "AppIcon-20x20@1x.png",
"scale" : "1x" "scale": "1x"
}, },
{ {
"size" : "20x20", "size": "20x20",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-20x20@2x-1.png", "filename": "AppIcon-20x20@2x-1.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "29x29", "size": "29x29",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-29x29@1x.png", "filename": "AppIcon-29x29@1x.png",
"scale" : "1x" "scale": "1x"
}, },
{ {
"size" : "29x29", "size": "29x29",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-29x29@2x.png", "filename": "AppIcon-29x29@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "40x40", "size": "40x40",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-40x40@1x.png", "filename": "AppIcon-40x40@1x.png",
"scale" : "1x" "scale": "1x"
}, },
{ {
"size" : "40x40", "size": "40x40",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-40x40@2x-1.png", "filename": "AppIcon-40x40@2x-1.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "76x76", "size": "76x76",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-76x76@1x.png", "filename": "AppIcon-76x76@1x.png",
"scale" : "1x" "scale": "1x"
}, },
{ {
"size" : "76x76", "size": "76x76",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-76x76@2x.png", "filename": "AppIcon-76x76@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "83.5x83.5", "size": "83.5x83.5",
"idiom" : "ipad", "idiom": "ipad",
"filename" : "AppIcon-83.5x83.5@2x.png", "filename": "AppIcon-83.5x83.5@2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "1024x1024", "size": "1024x1024",
"idiom" : "ios-marketing", "idiom": "ios-marketing",
"filename" : "AppIcon-512@2x.png", "filename": "AppIcon-512@2x.png",
"scale" : "1x" "scale": "1x"
} }
], ],
"info" : { "info": {
"version" : 1, "version": 1,
"author" : "xcode" "author": "xcode"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"info" : { "info": {
"version" : 1, "version": 1,
"author" : "xcode" "author": "xcode"
} }
} }

452
src-tauri/src/commands.rs Normal file
View file

@ -0,0 +1,452 @@
use std::collections::HashMap;
use std::io::Write;
use std::result::Result;
use swap::cli::{
api::{
data,
request::{
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ChangeMoneroNodeArgs,
CheckElectrumNodeArgs, CheckElectrumNodeResponse, CheckMoneroNodeArgs,
CheckMoneroNodeResponse, CheckSeedArgs, CheckSeedResponse, DfxAuthenticateResponse,
ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs,
GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs,
GetMoneroMainAddressArgs, GetMoneroSeedArgs, GetMoneroSyncProgressArgs,
GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs,
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs,
SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
},
tauri_bindings::{ContextStatus, TauriSettings},
ContextBuilder,
},
command::Bitcoin,
};
use tauri_plugin_dialog::DialogExt;
use zip::{write::SimpleFileOptions, ZipWriter};
use crate::{commands::util::ToStringResult, State};
/// This macro returns the list of all command handlers
/// You can call this and insert the output into [`tauri::app::Builder::invoke_handler`]
///
/// Note: When you add a new command, add it here.
#[macro_export]
macro_rules! generate_command_handlers {
() => {
tauri::generate_handler![
get_balance,
get_monero_addresses,
get_swap_info,
get_swap_infos_all,
withdraw_btc,
buy_xmr,
resume_swap,
get_history,
monero_recovery,
get_logs,
list_sellers,
suspend_current_swap,
cancel_and_refund,
initialize_context,
check_monero_node,
check_electrum_node,
get_wallet_descriptor,
get_current_swap,
get_data_dir,
resolve_approval_request,
redact,
save_txt_files,
get_monero_history,
get_monero_main_address,
get_monero_balance,
send_monero,
get_monero_sync_progress,
get_monero_seed,
check_seed,
get_pending_approvals,
set_monero_restore_height,
reject_approval_request,
get_restore_height,
dfx_authenticate,
change_monero_node,
get_context_status
]
};
}
#[macro_use]
mod util {
use std::result::Result;
/// Trait to convert Result<T, E> to Result<T, String>
/// Tauri commands require the error type to be a string
pub(crate) trait ToStringResult<T> {
fn to_string_result(self) -> Result<T, String>;
}
impl<T, E: ToString> ToStringResult<T> for Result<T, E> {
fn to_string_result(self) -> Result<T, String> {
self.map_err(|e| e.to_string())
}
}
/// This macro is used to create boilerplate functions as tauri commands
/// that simply delegate handling to the respective request type.
///
/// # Example
/// ```ignored
/// tauri_command!(get_balance, BalanceArgs);
/// ```
/// will resolve to
/// ```ignored
/// #[tauri::command]
/// async fn get_balance(context: tauri::State<'...>, args: BalanceArgs) -> Result<BalanceArgs::Response, String> {
/// args.handle(context.inner().clone()).await.to_string_result()
/// }
/// ```
/// # Example 2
/// ```ignored
/// tauri_command!(get_balance, BalanceArgs, no_args);
/// ```
/// will resolve to
/// ```ignored
/// #[tauri::command]
/// async fn get_balance(context: tauri::State<'...>) -> Result<BalanceArgs::Response, String> {
/// BalanceArgs {}.handle(context.inner().clone()).await.to_string_result()
/// }
/// ```
macro_rules! tauri_command {
($fn_name:ident, $request_name:ident) => {
#[tauri::command]
pub async fn $fn_name(
state: tauri::State<'_, State>,
args: $request_name,
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
<$request_name as swap::cli::api::request::Request>::request(args, state.context())
.await
.to_string_result()
}
};
($fn_name:ident, $request_name:ident, no_args) => {
#[tauri::command]
pub async fn $fn_name(
state: tauri::State<'_, State>,
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
<$request_name as swap::cli::api::request::Request>::request(
$request_name {},
state.context(),
)
.await
.to_string_result()
}
};
}
}
/// Tauri command to initialize the Context
#[tauri::command]
pub async fn initialize_context(
settings: TauriSettings,
testnet: bool,
state: tauri::State<'_, State>,
) -> Result<(), String> {
// We want to prevent multiple initalizations at the same time
let _context_lock = state
.context_lock
.try_lock()
.map_err(|_| "Context is already being initialized".to_string())?;
// Fail if the context is already initialized
// TODO: Maybe skip the stuff below if one of the context fields is already initialized?
// if context_lock.is_some() {
// return Err("Context is already initialized".to_string());
// }
// Get tauri handle from the state
let tauri_handle = state.handle.clone();
// Now populate the context in the background
let context_result = ContextBuilder::new(testnet)
.with_bitcoin(Bitcoin {
bitcoin_electrum_rpc_urls: settings.electrum_rpc_urls.clone(),
bitcoin_target_block: None,
})
.with_monero(settings.monero_node_config)
.with_json(false)
.with_debug(true)
.with_tor(settings.use_tor)
.with_enable_monero_tor(settings.enable_monero_tor)
.with_tauri(tauri_handle.clone())
.build(state.context())
.await;
match context_result {
Ok(()) => {
tracing::info!("Context initialized");
Ok(())
}
Err(e) => {
tracing::error!(error = ?e, "Failed to initialize context");
Err(e.to_string())
}
}
}
#[tauri::command]
pub async fn get_context_status(state: tauri::State<'_, State>) -> Result<ContextStatus, String> {
Ok(state.context().status().await)
}
#[tauri::command]
pub async fn resolve_approval_request(
args: ResolveApprovalArgs,
state: tauri::State<'_, State>,
) -> Result<(), String> {
let request_id = args
.request_id
.parse()
.map_err(|e| format!("Invalid request ID '{}': {}", args.request_id, e))?;
state
.handle
.resolve_approval(request_id, args.accept)
.await
.to_string_result()?;
Ok(())
}
#[tauri::command]
pub async fn reject_approval_request(
args: RejectApprovalArgs,
state: tauri::State<'_, State>,
) -> Result<RejectApprovalResponse, String> {
let request_id = args
.request_id
.parse()
.map_err(|e| format!("Invalid request ID '{}': {}", args.request_id, e))?;
state
.handle
.reject_approval(request_id)
.await
.to_string_result()?;
Ok(RejectApprovalResponse { success: true })
}
#[tauri::command]
pub async fn get_pending_approvals(
state: tauri::State<'_, State>,
) -> Result<GetPendingApprovalsResponse, String> {
let approvals = state
.handle
.get_pending_approvals()
.await
.to_string_result()?;
Ok(GetPendingApprovalsResponse { approvals })
}
#[tauri::command]
pub async fn check_monero_node(
args: CheckMoneroNodeArgs,
_: tauri::State<'_, State>,
) -> Result<CheckMoneroNodeResponse, String> {
args.request().await.to_string_result()
}
#[tauri::command]
pub async fn check_electrum_node(
args: CheckElectrumNodeArgs,
_: tauri::State<'_, State>,
) -> Result<CheckElectrumNodeResponse, String> {
args.request().await.to_string_result()
}
#[tauri::command]
pub async fn check_seed(
args: CheckSeedArgs,
_: tauri::State<'_, State>,
) -> Result<CheckSeedResponse, String> {
args.request().await.to_string_result()
}
// Returns the data directory
// This is independent of the context to ensure the user can open the directory even if the context cannot
// be initialized (for troubleshooting purposes)
#[tauri::command]
pub async fn get_data_dir(
args: GetDataDirArgs,
_: tauri::State<'_, State>,
) -> Result<String, String> {
Ok(data::data_dir_from(None, args.is_testnet)
.to_string_result()?
.to_string_lossy()
.to_string())
}
#[tauri::command]
pub async fn save_txt_files(
app: tauri::AppHandle,
zip_file_name: String,
content: HashMap<String, String>,
) -> Result<(), String> {
// Step 1: Get the owned PathBuf from the dialog
let path_buf_from_dialog: tauri_plugin_dialog::FilePath = app
.dialog()
.file()
.set_file_name(format!("{}.zip", &zip_file_name).as_str())
.add_filter(&zip_file_name, &["zip"])
.blocking_save_file() // This returns Option<PathBuf>
.ok_or_else(|| "Dialog cancelled or file path not selected".to_string())?; // Converts to Result<PathBuf, String> and unwraps to PathBuf
// Step 2: Now get a &Path reference from the owned PathBuf.
// The user's code structure implied an .as_path().ok_or_else(...) chain which was incorrect for &Path.
// We'll directly use the PathBuf, or if &Path is strictly needed:
let selected_file_path: &std::path::Path = path_buf_from_dialog
.as_path()
.ok_or_else(|| "Could not convert file path".to_string())?;
let zip_file = std::fs::File::create(selected_file_path)
.map_err(|e| format!("Failed to create file: {}", e))?;
let mut zip = ZipWriter::new(zip_file);
for (filename, file_content_str) in content.iter() {
zip.start_file(
format!("{}.txt", filename).as_str(),
SimpleFileOptions::default(),
) // Pass &str to start_file
.map_err(|e| format!("Failed to start file {}: {}", &filename, e))?; // Use &filename
zip.write_all(file_content_str.as_bytes())
.map_err(|e| format!("Failed to write to file {}: {}", &filename, e))?;
// Use &filename
}
zip.finish()
.map_err(|e| format!("Failed to finish zip: {}", e))?;
Ok(())
}
#[tauri::command]
pub async fn dfx_authenticate(
state: tauri::State<'_, State>,
) -> Result<DfxAuthenticateResponse, String> {
use dfx_swiss_sdk::{DfxClient, SignRequest};
use tokio::sync::{mpsc, oneshot};
use tokio_util::task::AbortOnDropHandle;
let context = state.context();
// Get the monero wallet manager
let monero_manager = context
.try_get_monero_manager()
.await
.map_err(|_| "Monero wallet manager not available for DFX authentication".to_string())?;
let wallet = monero_manager.main_wallet().await;
let address = wallet.main_address().await.to_string();
// Create channel for authentication
let (auth_tx, mut auth_rx) = mpsc::channel::<(SignRequest, oneshot::Sender<String>)>(10);
// Create DFX client
let mut client = DfxClient::new(address, Some("https://api.dfx.swiss".to_string()), auth_tx);
// Start signing task with AbortOnDropHandle
let signing_task = tokio::spawn(async move {
tracing::info!("DFX signing service started and listening for requests");
while let Some((sign_request, response_tx)) = auth_rx.recv().await {
tracing::debug!(
message = %sign_request.message,
blockchains = ?sign_request.blockchains,
"Received DFX signing request"
);
// Sign the message using the main Monero wallet
let signature = match wallet
.sign_message(&sign_request.message, None, false)
.await
{
Ok(sig) => {
tracing::debug!(
signature_preview = %&sig[..std::cmp::min(50, sig.len())],
"Message signed successfully for DFX"
);
sig
}
Err(e) => {
tracing::error!(error = ?e, "Failed to sign message for DFX");
continue;
}
};
// Send signature back to DFX client
if let Err(_) = response_tx.send(signature) {
tracing::warn!("Failed to send signature response through channel to DFX client");
}
}
tracing::info!("DFX signing service stopped");
});
// Create AbortOnDropHandle so the task gets cleaned up
let _abort_handle = AbortOnDropHandle::new(signing_task);
// Authenticate with DFX
tracing::info!("Starting DFX authentication...");
client
.authenticate()
.await
.map_err(|e| format!("Failed to authenticate with DFX: {}", e))?;
let access_token = client
.access_token
.as_ref()
.ok_or("No access token available after authentication")?
.clone();
let kyc_url = format!("https://app.dfx.swiss/buy?session={}", access_token);
tracing::info!("DFX authentication completed successfully");
Ok(DfxAuthenticateResponse {
access_token,
kyc_url,
})
}
// Here we define the Tauri commands that will be available to the frontend
// The commands are defined using the `tauri_command!` macro.
// Implementations are handled by the Request trait
tauri_command!(get_balance, BalanceArgs);
tauri_command!(buy_xmr, BuyXmrArgs);
tauri_command!(resume_swap, ResumeSwapArgs);
tauri_command!(withdraw_btc, WithdrawBtcArgs);
tauri_command!(monero_recovery, MoneroRecoveryArgs);
tauri_command!(get_logs, GetLogsArgs);
tauri_command!(list_sellers, ListSellersArgs);
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
tauri_command!(redact, RedactArgs);
tauri_command!(send_monero, SendMoneroArgs);
tauri_command!(change_monero_node, ChangeMoneroNodeArgs);
// These commands require no arguments
tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args);
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);
tauri_command!(get_swap_info, GetSwapInfoArgs);
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
tauri_command!(get_history, GetHistoryArgs, no_args);
tauri_command!(get_monero_addresses, GetMoneroAddressesArgs, no_args);
tauri_command!(get_monero_history, GetMoneroHistoryArgs, no_args);
tauri_command!(get_current_swap, GetCurrentSwapArgs, no_args);
tauri_command!(set_monero_restore_height, SetRestoreHeightArgs);
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);

View file

@ -1,121 +1,43 @@
use std::collections::HashMap;
use std::io::Write;
use std::result::Result; use std::result::Result;
use std::sync::Arc; use std::sync::Arc;
use swap::cli::{ use swap::cli::api::{tauri_bindings::TauriHandle, Context};
api::{ use tauri::{Manager, RunEvent};
data, use tokio::sync::Mutex;
request::{
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ChangeMoneroNodeArgs,
CheckElectrumNodeArgs, CheckElectrumNodeResponse, CheckMoneroNodeArgs,
CheckMoneroNodeResponse, CheckSeedArgs, CheckSeedResponse, DfxAuthenticateResponse,
ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs,
GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs,
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,
},
command::Bitcoin,
};
use tauri::{async_runtime::RwLock, Manager, RunEvent};
use tauri_plugin_dialog::DialogExt;
use zip::{write::SimpleFileOptions, ZipWriter};
/// Trait to convert Result<T, E> to Result<T, String> mod commands;
/// Tauri commands require the error type to be a string
trait ToStringResult<T> {
fn to_string_result(self) -> Result<T, String>;
}
impl<T, E: ToString> ToStringResult<T> for Result<T, E> { use commands::*;
fn to_string_result(self) -> Result<T, String> {
self.map_err(|e| e.to_string())
}
}
/// This macro is used to create boilerplate functions as tauri commands
/// that simply delegate handling to the respective request type.
///
/// # Example
/// ```ignored
/// tauri_command!(get_balance, BalanceArgs);
/// ```
/// will resolve to
/// ```ignored
/// #[tauri::command]
/// async fn get_balance(context: tauri::State<'...>, args: BalanceArgs) -> Result<BalanceArgs::Response, String> {
/// args.handle(context.inner().clone()).await.to_string_result()
/// }
/// ```
/// # Example 2
/// ```ignored
/// tauri_command!(get_balance, BalanceArgs, no_args);
/// ```
/// will resolve to
/// ```ignored
/// #[tauri::command]
/// async fn get_balance(context: tauri::State<'...>) -> Result<BalanceArgs::Response, String> {
/// BalanceArgs {}.handle(context.inner().clone()).await.to_string_result()
/// }
/// ```
macro_rules! tauri_command {
($fn_name:ident, $request_name:ident) => {
#[tauri::command]
async fn $fn_name(
state: tauri::State<'_, State>,
args: $request_name,
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
// Throw error if context is not available
let context = state.try_get_context()?;
<$request_name as swap::cli::api::request::Request>::request(args, context)
.await
.to_string_result()
}
};
($fn_name:ident, $request_name:ident, no_args) => {
#[tauri::command]
async fn $fn_name(
state: tauri::State<'_, State>,
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
// Throw error if context is not available
let context = state.try_get_context()?;
<$request_name as swap::cli::api::request::Request>::request($request_name {}, context)
.await
.to_string_result()
}
};
}
/// Represents the shared Tauri state. It is accessed by Tauri commands /// Represents the shared Tauri state. It is accessed by Tauri commands
struct State { struct State {
pub context: RwLock<Option<Arc<Context>>>, pub context: Arc<Context>,
/// Whenever someone wants to modify the context, they should acquire this lock
///
/// [`Context`] uses RwLock internally which means we do not need write access to the context
/// to modify its internal state.
///
/// However, we want to avoid multiple processes intializing the context at the same time.
pub context_lock: Mutex<()>,
pub handle: TauriHandle, pub handle: TauriHandle,
} }
impl State { impl State {
/// Creates a new State instance with no Context /// Creates a new State instance with no Context
fn new(handle: TauriHandle) -> Self { fn new(handle: TauriHandle) -> Self {
let context = Arc::new(Context::new_with_tauri_handle(handle.clone()));
let context_lock = Mutex::new(());
Self { Self {
context: RwLock::new(None), context,
context_lock,
handle, handle,
} }
} }
/// Attempts to retrieve the context /// Attempts to retrieve the context
/// Returns an error if the context is not available /// Returns an error if the context is not available
fn try_get_context(&self) -> Result<Arc<Context>, String> { fn context(&self) -> Arc<Context> {
self.context self.context.clone()
.try_read()
.map_err(|_| "Context is being modified".to_string())?
.clone()
.ok_or("Context not available".to_string())
} }
} }
@ -177,44 +99,7 @@ pub fn run() {
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![ .invoke_handler(generate_command_handlers!())
get_balance,
get_monero_addresses,
get_swap_info,
get_swap_infos_all,
withdraw_btc,
buy_xmr,
resume_swap,
get_history,
monero_recovery,
get_logs,
list_sellers,
suspend_current_swap,
cancel_and_refund,
is_context_available,
initialize_context,
check_monero_node,
check_electrum_node,
get_wallet_descriptor,
get_current_swap,
get_data_dir,
resolve_approval_request,
redact,
save_txt_files,
get_monero_history,
get_monero_main_address,
get_monero_balance,
send_monero,
get_monero_sync_progress,
get_monero_seed,
check_seed,
get_pending_approvals,
set_monero_restore_height,
reject_approval_request,
get_restore_height,
dfx_authenticate,
change_monero_node,
])
.setup(setup) .setup(setup)
.build(tauri::generate_context!()) .build(tauri::generate_context!())
.expect("error while building tauri application") .expect("error while building tauri application")
@ -225,335 +110,13 @@ pub fn run() {
// If the application is forcibly closed, this may not be called. // If the application is forcibly closed, this may not be called.
// TODO: fix that // TODO: fix that
let state = app.state::<State>(); let state = app.state::<State>();
let context_to_cleanup = if let Ok(context_lock) = state.context.try_read() { let lock = state.context_lock.try_lock();
context_lock.clone() if let Ok(_) = lock {
} else { if let Err(e) = state.context().cleanup() {
println!("Failed to acquire lock on context"); println!("Failed to cleanup context: {}", e);
None
};
if let Some(context) = context_to_cleanup {
if let Err(err) = context.cleanup() {
println!("Cleanup failed {}", err);
} }
} }
} }
_ => {} _ => {}
}) })
} }
// Here we define the Tauri commands that will be available to the frontend
// The commands are defined using the `tauri_command!` macro.
// Implementations are handled by the Request trait
tauri_command!(get_balance, BalanceArgs);
tauri_command!(buy_xmr, BuyXmrArgs);
tauri_command!(resume_swap, ResumeSwapArgs);
tauri_command!(withdraw_btc, WithdrawBtcArgs);
tauri_command!(monero_recovery, MoneroRecoveryArgs);
tauri_command!(get_logs, GetLogsArgs);
tauri_command!(list_sellers, ListSellersArgs);
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
tauri_command!(redact, RedactArgs);
tauri_command!(send_monero, SendMoneroArgs);
tauri_command!(change_monero_node, ChangeMoneroNodeArgs);
// These commands require no arguments
tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args);
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);
tauri_command!(get_swap_info, GetSwapInfoArgs);
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
tauri_command!(get_history, GetHistoryArgs, no_args);
tauri_command!(get_monero_addresses, GetMoneroAddressesArgs, no_args);
tauri_command!(get_monero_history, GetMoneroHistoryArgs, no_args);
tauri_command!(get_current_swap, GetCurrentSwapArgs, no_args);
tauri_command!(set_monero_restore_height, SetRestoreHeightArgs);
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]
async fn is_context_available(state: tauri::State<'_, State>) -> Result<bool, String> {
// TODO: Here we should return more information about status of the context (e.g. initializing, failed)
Ok(state.try_get_context().is_ok())
}
#[tauri::command]
async fn check_monero_node(
args: CheckMoneroNodeArgs,
_: tauri::State<'_, State>,
) -> Result<CheckMoneroNodeResponse, String> {
args.request().await.to_string_result()
}
#[tauri::command]
async fn check_electrum_node(
args: CheckElectrumNodeArgs,
_: tauri::State<'_, State>,
) -> Result<CheckElectrumNodeResponse, String> {
args.request().await.to_string_result()
}
#[tauri::command]
async fn check_seed(
args: CheckSeedArgs,
_: tauri::State<'_, State>,
) -> Result<CheckSeedResponse, String> {
args.request().await.to_string_result()
}
// Returns the data directory
// This is independent of the context to ensure the user can open the directory even if the context cannot
// be initialized (for troubleshooting purposes)
#[tauri::command]
async fn get_data_dir(args: GetDataDirArgs, _: tauri::State<'_, State>) -> Result<String, String> {
Ok(data::data_dir_from(None, args.is_testnet)
.to_string_result()?
.to_string_lossy()
.to_string())
}
#[tauri::command]
async fn save_txt_files(
app: tauri::AppHandle,
zip_file_name: String,
content: HashMap<String, String>,
) -> Result<(), String> {
// Step 1: Get the owned PathBuf from the dialog
let path_buf_from_dialog: tauri_plugin_dialog::FilePath = app
.dialog()
.file()
.set_file_name(format!("{}.zip", &zip_file_name).as_str())
.add_filter(&zip_file_name, &["zip"])
.blocking_save_file() // This returns Option<PathBuf>
.ok_or_else(|| "Dialog cancelled or file path not selected".to_string())?; // Converts to Result<PathBuf, String> and unwraps to PathBuf
// Step 2: Now get a &Path reference from the owned PathBuf.
// The user's code structure implied an .as_path().ok_or_else(...) chain which was incorrect for &Path.
// We'll directly use the PathBuf, or if &Path is strictly needed:
let selected_file_path: &std::path::Path = path_buf_from_dialog
.as_path()
.ok_or_else(|| "Could not convert file path".to_string())?;
let zip_file = std::fs::File::create(selected_file_path)
.map_err(|e| format!("Failed to create file: {}", e))?;
let mut zip = ZipWriter::new(zip_file);
for (filename, file_content_str) in content.iter() {
zip.start_file(
format!("{}.txt", filename).as_str(),
SimpleFileOptions::default(),
) // Pass &str to start_file
.map_err(|e| format!("Failed to start file {}: {}", &filename, e))?; // Use &filename
zip.write_all(file_content_str.as_bytes())
.map_err(|e| format!("Failed to write to file {}: {}", &filename, e))?;
// Use &filename
}
zip.finish()
.map_err(|e| format!("Failed to finish zip: {}", e))?;
Ok(())
}
#[tauri::command]
async fn resolve_approval_request(
args: ResolveApprovalArgs,
state: tauri::State<'_, State>,
) -> Result<(), String> {
let request_id = args
.request_id
.parse()
.map_err(|e| format!("Invalid request ID '{}': {}", args.request_id, e))?;
state
.handle
.resolve_approval(request_id, args.accept)
.await
.to_string_result()?;
Ok(())
}
#[tauri::command]
async fn reject_approval_request(
args: RejectApprovalArgs,
state: tauri::State<'_, State>,
) -> Result<RejectApprovalResponse, String> {
let request_id = args
.request_id
.parse()
.map_err(|e| format!("Invalid request ID '{}': {}", args.request_id, e))?;
state
.handle
.reject_approval(request_id)
.await
.to_string_result()?;
Ok(RejectApprovalResponse { success: true })
}
#[tauri::command]
async fn get_pending_approvals(
state: tauri::State<'_, State>,
) -> Result<GetPendingApprovalsResponse, String> {
let approvals = state
.handle
.get_pending_approvals()
.await
.to_string_result()?;
Ok(GetPendingApprovalsResponse { approvals })
}
/// Tauri command to initialize the Context
#[tauri::command]
async fn initialize_context(
settings: TauriSettings,
testnet: bool,
state: tauri::State<'_, State>,
) -> Result<(), String> {
// Lock at the beginning - fail immediately if already locked
let mut context_lock = state
.context
.try_write()
.map_err(|_| "Context is already being initialized".to_string())?;
// Fail if the context is already initialized
if context_lock.is_some() {
return Err("Context is already initialized".to_string());
}
// Get tauri handle from the state
let tauri_handle = state.handle.clone();
// Notify frontend that the context is being initialized
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Initializing);
let context_result = ContextBuilder::new(testnet)
.with_bitcoin(Bitcoin {
bitcoin_electrum_rpc_urls: settings.electrum_rpc_urls.clone(),
bitcoin_target_block: None,
})
.with_monero(settings.monero_node_config)
.with_json(false)
.with_debug(true)
.with_tor(settings.use_tor)
.with_enable_monero_tor(settings.enable_monero_tor)
.with_tauri(tauri_handle.clone())
.build()
.await;
match context_result {
Ok(context_instance) => {
*context_lock = Some(Arc::new(context_instance));
tracing::info!("Context initialized");
// Emit event to frontend
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
Ok(())
}
Err(e) => {
tracing::error!(error = ?e, "Failed to initialize context");
// Emit event to frontend
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Failed);
Err(e.to_string())
}
}
}
#[tauri::command]
async fn dfx_authenticate(
state: tauri::State<'_, State>,
) -> Result<DfxAuthenticateResponse, String> {
use dfx_swiss_sdk::{DfxClient, SignRequest};
use tokio::sync::{mpsc, oneshot};
use tokio_util::task::AbortOnDropHandle;
let context = state.try_get_context()?;
// Get the monero wallet manager
let monero_manager = context
.monero_manager
.as_ref()
.ok_or("Monero wallet manager not available for DFX authentication")?;
let wallet = monero_manager.main_wallet().await;
let address = wallet.main_address().await.to_string();
// Create channel for authentication
let (auth_tx, mut auth_rx) = mpsc::channel::<(SignRequest, oneshot::Sender<String>)>(10);
// Create DFX client
let mut client = DfxClient::new(address, Some("https://api.dfx.swiss".to_string()), auth_tx);
// Start signing task with AbortOnDropHandle
let signing_task = tokio::spawn(async move {
tracing::info!("DFX signing service started and listening for requests");
while let Some((sign_request, response_tx)) = auth_rx.recv().await {
tracing::debug!(
message = %sign_request.message,
blockchains = ?sign_request.blockchains,
"Received DFX signing request"
);
// Sign the message using the main Monero wallet
let signature = match wallet
.sign_message(&sign_request.message, None, false)
.await
{
Ok(sig) => {
tracing::debug!(
signature_preview = %&sig[..std::cmp::min(50, sig.len())],
"Message signed successfully for DFX"
);
sig
}
Err(e) => {
tracing::error!(error = ?e, "Failed to sign message for DFX");
continue;
}
};
// Send signature back to DFX client
if let Err(_) = response_tx.send(signature) {
tracing::warn!("Failed to send signature response through channel to DFX client");
}
}
tracing::info!("DFX signing service stopped");
});
// Create AbortOnDropHandle so the task gets cleaned up
let _abort_handle = AbortOnDropHandle::new(signing_task);
// Authenticate with DFX
tracing::info!("Starting DFX authentication...");
client
.authenticate()
.await
.map_err(|e| format!("Failed to authenticate with DFX: {}", e))?;
let access_token = client
.access_token
.as_ref()
.ok_or("No access token available after authentication")?
.clone();
let kyc_url = format!("https://app.dfx.swiss/buy?session={}", access_token);
tracing::info!("DFX authentication completed successfully");
Ok(DfxAuthenticateResponse {
access_token,
kyc_url,
})
}

File diff suppressed because it is too large Load diff

View file

@ -421,7 +421,8 @@ impl Request for GetLogsArgs {
type Response = GetLogsResponse; type Response = GetLogsResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let dir = self.logs_dir.unwrap_or(ctx.config.log_dir.clone()); let config = ctx.try_get_config().await?;
let dir = self.logs_dir.unwrap_or(config.log_dir.clone());
let logs = get_logs(dir, self.swap_id, self.redact).await?; let logs = get_logs(dir, self.swap_id, self.redact).await?;
for msg in &logs { for msg in &logs {
@ -470,11 +471,8 @@ impl Request for GetRestoreHeightArgs {
type Response = GetRestoreHeightResponse; type Response = GetRestoreHeightResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let wallet = ctx let wallet_manager = ctx.try_get_monero_manager().await?;
.monero_manager let wallet = wallet_manager.main_wallet().await;
.as_ref()
.context("Monero wallet manager not available")?;
let wallet = wallet.main_wallet().await;
let height = wallet.get_restore_height().await?; let height = wallet.get_restore_height().await?;
Ok(GetRestoreHeightResponse { height }) Ok(GetRestoreHeightResponse { height })
@ -496,7 +494,8 @@ impl Request for GetMoneroAddressesArgs {
type Response = GetMoneroAddressesResponse; type Response = GetMoneroAddressesResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let addresses = ctx.db.get_monero_addresses().await?; let db = ctx.try_get_db().await?;
let addresses = db.get_monero_addresses().await?;
Ok(GetMoneroAddressesResponse { addresses }) Ok(GetMoneroAddressesResponse { addresses })
} }
} }
@ -515,11 +514,8 @@ impl Request for GetMoneroHistoryArgs {
type Response = GetMoneroHistoryResponse; type Response = GetMoneroHistoryResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let wallet = ctx let wallet_manager = ctx.try_get_monero_manager().await?;
.monero_manager let wallet = wallet_manager.main_wallet().await;
.as_ref()
.context("Monero wallet manager not available")?;
let wallet = wallet.main_wallet().await;
let transactions = wallet.history().await; let transactions = wallet.history().await;
Ok(GetMoneroHistoryResponse { transactions }) Ok(GetMoneroHistoryResponse { transactions })
@ -541,11 +537,8 @@ impl Request for GetMoneroMainAddressArgs {
type Response = GetMoneroMainAddressResponse; type Response = GetMoneroMainAddressResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let wallet = ctx let wallet_manager = ctx.try_get_monero_manager().await?;
.monero_manager let wallet = wallet_manager.main_wallet().await;
.as_ref()
.context("Monero wallet manager not available")?;
let wallet = wallet.main_wallet().await;
let address = wallet.main_address().await; let address = wallet.main_address().await;
Ok(GetMoneroMainAddressResponse { address }) Ok(GetMoneroMainAddressResponse { address })
} }
@ -582,11 +575,8 @@ impl Request for SetRestoreHeightArgs {
type Response = SetRestoreHeightResponse; type Response = SetRestoreHeightResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let wallet = ctx let wallet_manager = ctx.try_get_monero_manager().await?;
.monero_manager let wallet = wallet_manager.main_wallet().await;
.as_ref()
.context("Monero wallet manager not available")?;
let wallet = wallet.main_wallet().await;
let height = match self { let height = match self {
SetRestoreHeightArgs::Height(height) => height as u64, SetRestoreHeightArgs::Height(height) => height as u64,
@ -661,10 +651,7 @@ impl Request for GetMoneroBalanceArgs {
type Response = GetMoneroBalanceResponse; type Response = GetMoneroBalanceResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let wallet_manager = ctx let wallet_manager = ctx.try_get_monero_manager().await?;
.monero_manager
.as_ref()
.context("Monero wallet manager not available")?;
let wallet = wallet_manager.main_wallet().await; let wallet = wallet_manager.main_wallet().await;
let total_balance = wallet.total_balance().await; let total_balance = wallet.total_balance().await;
@ -706,10 +693,7 @@ impl Request for SendMoneroArgs {
type Response = SendMoneroResponse; type Response = SendMoneroResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let wallet_manager = ctx let wallet_manager = ctx.try_get_monero_manager().await?;
.monero_manager
.as_ref()
.context("Monero wallet manager not available")?;
let wallet = wallet_manager.main_wallet().await; let wallet = wallet_manager.main_wallet().await;
// Parse the address // Parse the address
@ -717,7 +701,8 @@ impl Request for SendMoneroArgs {
.map_err(|e| anyhow::anyhow!("Invalid Monero address: {}", e))?; .map_err(|e| anyhow::anyhow!("Invalid Monero address: {}", e))?;
let tauri_handle = ctx let tauri_handle = ctx
.tauri_handle() .tauri_handle
.clone()
.context("Tauri needs to be available to approve transactions")?; .context("Tauri needs to be available to approve transactions")?;
// This is a closure that will be called by the monero-sys library to get approval for the transaction // This is a closure that will be called by the monero-sys library to get approval for the transaction
@ -793,7 +778,8 @@ pub async fn suspend_current_swap(context: Arc<Context>) -> Result<SuspendCurren
#[tracing::instrument(fields(method = "get_swap_infos_all"), skip(context))] #[tracing::instrument(fields(method = "get_swap_infos_all"), skip(context))]
pub async fn get_swap_infos_all(context: Arc<Context>) -> Result<Vec<GetSwapInfoResponse>> { pub async fn get_swap_infos_all(context: Arc<Context>) -> Result<Vec<GetSwapInfoResponse>> {
let swap_ids = context.db.all().await?; let db = context.try_get_db().await?;
let swap_ids = db.all().await?;
let mut swap_infos = Vec::new(); let mut swap_infos = Vec::new();
for (swap_id, _) in swap_ids { for (swap_id, _) in swap_ids {
@ -813,27 +799,23 @@ pub async fn get_swap_info(
args: GetSwapInfoArgs, args: GetSwapInfoArgs,
context: Arc<Context>, context: Arc<Context>,
) -> Result<GetSwapInfoResponse> { ) -> Result<GetSwapInfoResponse> {
let bitcoin_wallet = context let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
.bitcoin_wallet let db = context.try_get_db().await?;
.as_ref()
.context("Could not get Bitcoin wallet")?;
let state = context.db.get_state(args.swap_id).await?; let state = db.get_state(args.swap_id).await?;
let is_completed = state.swap_finished(); let is_completed = state.swap_finished();
let peer_id = context let peer_id = db
.db
.get_peer_id(args.swap_id) .get_peer_id(args.swap_id)
.await .await
.with_context(|| "Could not get PeerID")?; .with_context(|| "Could not get PeerID")?;
let addresses = context let addresses = db
.db
.get_addresses(peer_id) .get_addresses(peer_id)
.await .await
.with_context(|| "Could not get addressess")?; .with_context(|| "Could not get addressess")?;
let start_date = context.db.get_swap_start_date(args.swap_id).await?; let start_date = db.get_swap_start_date(args.swap_id).await?;
let swap_state: BobState = state.try_into()?; let swap_state: BobState = state.try_into()?;
@ -847,8 +829,7 @@ pub async fn get_swap_info(
btc_refund_address, btc_refund_address,
cancel_timelock, cancel_timelock,
punish_timelock, punish_timelock,
) = context ) = db
.db
.get_states(args.swap_id) .get_states(args.swap_id)
.await? .await?
.iter() .iter()
@ -884,7 +865,7 @@ pub async fn get_swap_info(
let timelock = swap_state.expired_timelocks(bitcoin_wallet.clone()).await?; let timelock = swap_state.expired_timelocks(bitcoin_wallet.clone()).await?;
let monero_receive_pool = context.db.get_monero_address_pool(args.swap_id).await?; let monero_receive_pool = db.get_monero_address_pool(args.swap_id).await?;
Ok(GetSwapInfoResponse { Ok(GetSwapInfoResponse {
swap_id: args.swap_id, swap_id: args.swap_id,
@ -924,15 +905,13 @@ pub async fn buy_xmr(
monero_receive_pool, monero_receive_pool,
} = buy_xmr; } = buy_xmr;
monero_receive_pool.assert_network(context.config.env_config.monero_network)?; let config = context.try_get_config().await?;
let db = context.try_get_db().await?;
monero_receive_pool.assert_network(config.env_config.monero_network)?;
monero_receive_pool.assert_sum_to_one()?; monero_receive_pool.assert_sum_to_one()?;
let bitcoin_wallet = Arc::clone( let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
context
.bitcoin_wallet
.as_ref()
.expect("Could not find Bitcoin wallet"),
);
let bitcoin_change_address = match bitcoin_change_address { let bitcoin_change_address = match bitcoin_change_address {
Some(addr) => addr Some(addr) => addr
@ -950,21 +929,15 @@ pub async fn buy_xmr(
} }
}; };
let monero_wallet = Arc::clone( let monero_wallet = context.try_get_monero_manager().await?;
context
.monero_manager
.as_ref()
.context("Could not get Monero wallet")?,
);
let env_config = context.config.env_config; let env_config = config.env_config;
let seed = context.config.seed.clone().context("Could not get seed")?; let seed = config.seed.clone().context("Could not get seed")?;
// Prepare variables for the quote fetching process // Prepare variables for the quote fetching process
let identity = seed.derive_libp2p_identity(); let identity = seed.derive_libp2p_identity();
let namespace = context.config.namespace; let namespace = config.namespace;
let tor_client = context.tor_client.clone(); let tor_client = context.tor_client.read().await.clone();
let db = Some(context.db.clone());
let tauri_handle = context.tauri_handle.clone(); let tauri_handle = context.tauri_handle.clone();
// Wait for the user to approve a seller and to deposit coins // Wait for the user to approve a seller and to deposit coins
@ -973,10 +946,18 @@ pub async fn buy_xmr(
let bitcoin_wallet_for_closures = Arc::clone(&bitcoin_wallet); let bitcoin_wallet_for_closures = Arc::clone(&bitcoin_wallet);
// Clone bitcoin_change_address before moving it in the emit call // Clone variables before moving them into closures
let bitcoin_change_address_for_spawn = bitcoin_change_address.clone(); let bitcoin_change_address_for_spawn = bitcoin_change_address.clone();
let rendezvous_points_clone = rendezvous_points.clone(); let rendezvous_points_clone = rendezvous_points.clone();
let sellers_clone = sellers.clone(); let sellers_clone = sellers.clone();
let db_for_fetch = db.clone();
let tor_client_for_swarm = tor_client.clone();
// Clone tauri_handle for different closures
let tauri_handle_for_fetch = tauri_handle.clone();
let tauri_handle_for_determine = tauri_handle.clone();
let tauri_handle_for_selection = tauri_handle.clone();
let tauri_handle_for_suspension = tauri_handle.clone();
// Acquire the lock before the user has selected a maker and we already have funds in the wallet // Acquire the lock before the user has selected a maker and we already have funds in the wallet
// because we need to be able to cancel the determine_btc_to_swap(..) // because we need to be able to cancel the determine_btc_to_swap(..)
@ -989,9 +970,9 @@ pub async fn buy_xmr(
let sellers = sellers_clone.clone(); let sellers = sellers_clone.clone();
let namespace = namespace; let namespace = namespace;
let identity = identity.clone(); let identity = identity.clone();
let db = db.clone(); let db = db_for_fetch.clone();
let tor_client = tor_client.clone(); let tor_client = tor_client.clone();
let tauri_handle = tauri_handle.clone(); let tauri_handle = tauri_handle_for_fetch.clone();
Box::pin(async move { Box::pin(async move {
fetch_quotes_task( fetch_quotes_task(
@ -999,7 +980,7 @@ pub async fn buy_xmr(
namespace, namespace,
sellers, sellers,
identity, identity,
db, Some(db),
tor_client, tor_client,
tauri_handle, tauri_handle,
).await ).await
@ -1027,10 +1008,10 @@ pub async fn buy_xmr(
async move { w.sync().await } async move { w.sync().await }
} }
}, },
context.tauri_handle.clone(), tauri_handle_for_determine,
swap_id, swap_id,
|quote_with_address| { |quote_with_address| {
let tauri_handle = context.tauri_handle.clone(); let tauri_handle_clone = tauri_handle_for_selection.clone();
Box::new(async move { Box::new(async move {
let details = SelectMakerDetails { let details = SelectMakerDetails {
swap_id, swap_id,
@ -1038,7 +1019,7 @@ pub async fn buy_xmr(
maker: quote_with_address, maker: quote_with_address,
}; };
tauri_handle.request_maker_selection(details, 300).await tauri_handle_clone.request_maker_selection(details, 300).await
}) as Box<dyn Future<Output = Result<bool>> + Send> }) as Box<dyn Future<Output = Result<bool>> + Send>
}, },
) => { ) => {
@ -1046,55 +1027,51 @@ pub async fn buy_xmr(
} }
_ = context.swap_lock.listen_for_swap_force_suspension() => { _ = context.swap_lock.listen_for_swap_force_suspension() => {
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active."); context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); if let Some(handle) = tauri_handle_for_suspension {
handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
}
bail!("Shutdown signal received"); bail!("Shutdown signal received");
}, },
}; };
// Insert the peer_id into the database // Insert the peer_id into the database
context.db.insert_peer_id(swap_id, seller_peer_id).await?; db.insert_peer_id(swap_id, seller_peer_id).await?;
context db.insert_address(seller_peer_id, seller_multiaddr.clone())
.db
.insert_address(seller_peer_id, seller_multiaddr.clone())
.await?; .await?;
let behaviour = cli::Behaviour::new( let behaviour = cli::Behaviour::new(
seller_peer_id, seller_peer_id,
env_config, env_config,
bitcoin_wallet.clone(), bitcoin_wallet.clone(),
(seed.derive_libp2p_identity(), context.config.namespace), (seed.derive_libp2p_identity(), namespace),
); );
let mut swarm = swarm::cli( let mut swarm = swarm::cli(
seed.derive_libp2p_identity(), seed.derive_libp2p_identity(),
context.tor_client.clone(), tor_client_for_swarm,
behaviour, behaviour,
) )
.await?; .await?;
swarm.add_peer_address(seller_peer_id, seller_multiaddr.clone()); swarm.add_peer_address(seller_peer_id, seller_multiaddr.clone());
context db.insert_monero_address_pool(swap_id, monero_receive_pool.clone())
.db
.insert_monero_address_pool(swap_id, monero_receive_pool.clone())
.await?; .await?;
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized"); tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
context.tauri_handle.emit_swap_progress_event( tauri_handle.emit_swap_progress_event(
swap_id, swap_id,
TauriSwapProgressEvent::ReceivedQuote(quote.clone()), TauriSwapProgressEvent::ReceivedQuote(quote.clone()),
); );
// Now create the event loop we use for the swap // Now create the event loop we use for the swap
let (event_loop, event_loop_handle) = let (event_loop, event_loop_handle) =
EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?; EventLoop::new(swap_id, swarm, seller_peer_id, db.clone())?;
let event_loop = tokio::spawn(event_loop.run().in_current_span()); let event_loop = tokio::spawn(event_loop.run().in_current_span());
context tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(quote));
.tauri_handle
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(quote));
context.tasks.clone().spawn(async move { context.tasks.clone().spawn(async move {
tokio::select! { tokio::select! {
@ -1103,7 +1080,7 @@ pub async fn buy_xmr(
tracing::debug!("Shutdown signal received, exiting"); tracing::debug!("Shutdown signal received, exiting");
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active."); context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
bail!("Shutdown signal received"); bail!("Shutdown signal received");
}, },
@ -1120,9 +1097,9 @@ pub async fn buy_xmr(
}, },
swap_result = async { swap_result = async {
let swap = Swap::new( let swap = Swap::new(
Arc::clone(&context.db), db.clone(),
swap_id, swap_id,
Arc::clone(&bitcoin_wallet), bitcoin_wallet.clone(),
monero_wallet, monero_wallet,
env_config, env_config,
event_loop_handle, event_loop_handle,
@ -1130,7 +1107,7 @@ pub async fn buy_xmr(
bitcoin_change_address_for_spawn, bitcoin_change_address_for_spawn,
tx_lock_amount, tx_lock_amount,
tx_lock_fee tx_lock_fee
).with_event_emitter(context.tauri_handle.clone()); ).with_event_emitter(tauri_handle.clone());
bob::run(swap).await bob::run(swap).await
} => { } => {
@ -1152,7 +1129,7 @@ pub async fn buy_xmr(
.await .await
.expect("Could not release swap lock"); .expect("Could not release swap lock");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
Ok::<_, anyhow::Error>(()) Ok::<_, anyhow::Error>(())
}.in_current_span()).await; }.in_current_span()).await;
@ -1167,11 +1144,16 @@ pub async fn resume_swap(
) -> Result<ResumeSwapResponse> { ) -> Result<ResumeSwapResponse> {
let ResumeSwapArgs { swap_id } = resume; let ResumeSwapArgs { swap_id } = resume;
let seller_peer_id = context.db.get_peer_id(swap_id).await?; let db = context.try_get_db().await?;
let seller_addresses = context.db.get_addresses(seller_peer_id).await?; let config = context.try_get_config().await?;
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
let monero_manager = context.try_get_monero_manager().await?;
let tor_client = context.tor_client.read().await.clone();
let seed = context let seller_peer_id = db.get_peer_id(swap_id).await?;
.config let seller_addresses = db.get_addresses(seller_peer_id).await?;
let seed = config
.seed .seed
.as_ref() .as_ref()
.context("Could not get seed")? .context("Could not get seed")?
@ -1179,16 +1161,11 @@ pub async fn resume_swap(
let behaviour = cli::Behaviour::new( let behaviour = cli::Behaviour::new(
seller_peer_id, seller_peer_id,
context.config.env_config, config.env_config,
Arc::clone( bitcoin_wallet.clone(),
context (seed.clone(), config.namespace),
.bitcoin_wallet
.as_ref()
.context("Could not get Bitcoin wallet")?,
),
(seed.clone(), context.config.namespace),
); );
let mut swarm = swarm::cli(seed.clone(), context.tor_client.clone(), behaviour).await?; let mut swarm = swarm::cli(seed.clone(), tor_client, behaviour).await?;
let our_peer_id = swarm.local_peer_id(); let our_peer_id = swarm.local_peer_id();
tracing::debug!(peer_id = %our_peer_id, "Network layer initialized"); tracing::debug!(peer_id = %our_peer_id, "Network layer initialized");
@ -1199,36 +1176,27 @@ pub async fn resume_swap(
} }
let (event_loop, event_loop_handle) = let (event_loop, event_loop_handle) =
EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?; EventLoop::new(swap_id, swarm, seller_peer_id, db.clone())?;
let monero_receive_pool = context.db.get_monero_address_pool(swap_id).await?; let monero_receive_pool = db.get_monero_address_pool(swap_id).await?;
let tauri_handle = context.tauri_handle.clone();
let swap = Swap::from_db( let swap = Swap::from_db(
Arc::clone(&context.db), db.clone(),
swap_id, swap_id,
Arc::clone( bitcoin_wallet,
context monero_manager,
.bitcoin_wallet config.env_config,
.as_ref()
.context("Could not get Bitcoin wallet")?,
),
context
.monero_manager
.as_ref()
.context("Could not get Monero wallet manager")?
.clone(),
context.config.env_config,
event_loop_handle, event_loop_handle,
monero_receive_pool, monero_receive_pool,
) )
.await? .await?
.with_event_emitter(context.tauri_handle.clone()); .with_event_emitter(tauri_handle.clone());
context.swap_lock.acquire_swap_lock(swap_id).await?; context.swap_lock.acquire_swap_lock(swap_id).await?;
context tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Resuming);
.tauri_handle
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Resuming);
context.tasks.clone().spawn( context.tasks.clone().spawn(
async move { async move {
@ -1239,7 +1207,7 @@ pub async fn resume_swap(
tracing::debug!("Shutdown signal received, exiting"); tracing::debug!("Shutdown signal received, exiting");
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active."); context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
bail!("Shutdown signal received"); bail!("Shutdown signal received");
}, },
@ -1272,7 +1240,7 @@ pub async fn resume_swap(
.await .await
.expect("Could not release swap lock"); .expect("Could not release swap lock");
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released); tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
Ok::<(), anyhow::Error>(()) Ok::<(), anyhow::Error>(())
} }
@ -1290,15 +1258,12 @@ pub async fn cancel_and_refund(
context: Arc<Context>, context: Arc<Context>,
) -> Result<serde_json::Value> { ) -> Result<serde_json::Value> {
let CancelAndRefundArgs { swap_id } = cancel_and_refund; let CancelAndRefundArgs { swap_id } = cancel_and_refund;
let bitcoin_wallet = context let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
.bitcoin_wallet let db = context.try_get_db().await?;
.as_ref()
.context("Could not get Bitcoin wallet")?;
context.swap_lock.acquire_swap_lock(swap_id).await?; context.swap_lock.acquire_swap_lock(swap_id).await?;
let state = let state = cli::cancel_and_refund(swap_id, bitcoin_wallet, db).await;
cli::cancel_and_refund(swap_id, Arc::clone(bitcoin_wallet), Arc::clone(&context.db)).await;
context context
.swap_lock .swap_lock
@ -1319,7 +1284,8 @@ pub async fn cancel_and_refund(
#[tracing::instrument(fields(method = "get_history"), skip(context))] #[tracing::instrument(fields(method = "get_history"), skip(context))]
pub async fn get_history(context: Arc<Context>) -> Result<GetHistoryResponse> { pub async fn get_history(context: Arc<Context>) -> Result<GetHistoryResponse> {
let swaps = context.db.all().await?; let db = context.try_get_db().await?;
let swaps = db.all().await?;
let mut vec: Vec<GetHistoryEntry> = Vec::new(); let mut vec: Vec<GetHistoryEntry> = Vec::new();
for (swap_id, state) in swaps { for (swap_id, state) in swaps {
let state: BobState = state.try_into()?; let state: BobState = state.try_into()?;
@ -1334,7 +1300,8 @@ pub async fn get_history(context: Arc<Context>) -> Result<GetHistoryResponse> {
#[tracing::instrument(fields(method = "get_config"), skip(context))] #[tracing::instrument(fields(method = "get_config"), skip(context))]
pub async fn get_config(context: Arc<Context>) -> Result<serde_json::Value> { pub async fn get_config(context: Arc<Context>) -> Result<serde_json::Value> {
let data_dir_display = context.config.data_dir.display(); let config = context.try_get_config().await?;
let data_dir_display = config.data_dir.display();
tracing::info!(path=%data_dir_display, "Data directory"); tracing::info!(path=%data_dir_display, "Data directory");
tracing::info!(path=%format!("{}/logs", data_dir_display), "Log files directory"); tracing::info!(path=%format!("{}/logs", data_dir_display), "Log files directory");
tracing::info!(path=%format!("{}/sqlite", data_dir_display), "Sqlite file location"); tracing::info!(path=%format!("{}/sqlite", data_dir_display), "Sqlite file location");
@ -1357,10 +1324,7 @@ pub async fn withdraw_btc(
context: Arc<Context>, context: Arc<Context>,
) -> Result<WithdrawBtcResponse> { ) -> Result<WithdrawBtcResponse> {
let WithdrawBtcArgs { address, amount } = withdraw_btc; let WithdrawBtcArgs { address, amount } = withdraw_btc;
let bitcoin_wallet = context let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
.bitcoin_wallet
.as_ref()
.context("Could not get Bitcoin wallet")?;
let (withdraw_tx_unsigned, amount) = match amount { let (withdraw_tx_unsigned, amount) = match amount {
Some(amount) => { Some(amount) => {
@ -1402,10 +1366,7 @@ pub async fn withdraw_btc(
#[tracing::instrument(fields(method = "get_balance"), skip(context))] #[tracing::instrument(fields(method = "get_balance"), skip(context))]
pub async fn get_balance(balance: BalanceArgs, context: Arc<Context>) -> Result<BalanceResponse> { pub async fn get_balance(balance: BalanceArgs, context: Arc<Context>) -> Result<BalanceResponse> {
let BalanceArgs { force_refresh } = balance; let BalanceArgs { force_refresh } = balance;
let bitcoin_wallet = context let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
.bitcoin_wallet
.as_ref()
.context("Could not get Bitcoin wallet")?;
if force_refresh { if force_refresh {
bitcoin_wallet.sync().await?; bitcoin_wallet.sync().await?;
@ -1441,8 +1402,12 @@ pub async fn list_sellers(
.filter_map(|rendezvous_point| rendezvous_point.split_peer_id()) .filter_map(|rendezvous_point| rendezvous_point.split_peer_id())
.collect(); .collect();
let identity = context let config = context.try_get_config().await?;
.config let db = context.try_get_db().await?;
let tor_client = context.tor_client.read().await.clone();
let tauri_handle = context.tauri_handle.clone();
let identity = config
.seed .seed
.as_ref() .as_ref()
.context("Cannot extract seed")? .context("Cannot extract seed")?
@ -1450,11 +1415,11 @@ pub async fn list_sellers(
let sellers = list_sellers_impl( let sellers = list_sellers_impl(
rendezvous_nodes, rendezvous_nodes,
context.config.namespace, config.namespace,
context.tor_client.clone(), tor_client,
identity, identity,
Some(context.db.clone()), Some(db.clone()),
context.tauri_handle(), tauri_handle,
) )
.await?; .await?;
@ -1480,10 +1445,7 @@ pub async fn list_sellers(
// Add the peer as known to the database // Add the peer as known to the database
// This'll allow us to later request a quote again // This'll allow us to later request a quote again
// without having to re-discover the peer at the rendezvous point // without having to re-discover the peer at the rendezvous point
context db.insert_address(*peer_id, multiaddr.clone()).await?;
.db
.insert_address(*peer_id, multiaddr.clone())
.await?;
} }
SellerStatus::Unreachable(UnreachableSeller { peer_id }) => { SellerStatus::Unreachable(UnreachableSeller { peer_id }) => {
tracing::trace!( tracing::trace!(
@ -1500,10 +1462,7 @@ pub async fn list_sellers(
#[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))] #[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))]
pub async fn export_bitcoin_wallet(context: Arc<Context>) -> Result<serde_json::Value> { pub async fn export_bitcoin_wallet(context: Arc<Context>) -> Result<serde_json::Value> {
let bitcoin_wallet = context let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
.bitcoin_wallet
.as_ref()
.context("Could not get Bitcoin wallet")?;
let wallet_export = bitcoin_wallet.wallet_export("cli").await?; let wallet_export = bitcoin_wallet.wallet_export("cli").await?;
tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet"); tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet");
@ -1518,14 +1477,17 @@ pub async fn monero_recovery(
context: Arc<Context>, context: Arc<Context>,
) -> Result<serde_json::Value> { ) -> Result<serde_json::Value> {
let MoneroRecoveryArgs { swap_id } = monero_recovery; let MoneroRecoveryArgs { swap_id } = monero_recovery;
let swap_state: BobState = context.db.get_state(swap_id).await?.try_into()?; let db = context.try_get_db().await?;
let config = context.try_get_config().await?;
let swap_state: BobState = db.get_state(swap_id).await?.try_into()?;
if let BobState::BtcRedeemed(state5) = swap_state { if let BobState::BtcRedeemed(state5) = swap_state {
let (spend_key, view_key) = state5.xmr_keys(); let (spend_key, view_key) = state5.xmr_keys();
let restore_height = state5.monero_wallet_restore_blockheight.height; let restore_height = state5.monero_wallet_restore_blockheight.height;
let address = monero::Address::standard( let address = monero::Address::standard(
context.config.env_config.monero_network, config.env_config.monero_network,
monero::PublicKey::from_private_key(&spend_key), monero::PublicKey::from_private_key(&spend_key),
monero::PublicKey::from(view_key.public()), monero::PublicKey::from(view_key.public()),
); );
@ -1978,10 +1940,7 @@ impl Request for GetMoneroSyncProgressArgs {
type Response = GetMoneroSyncProgressResponse; type Response = GetMoneroSyncProgressResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let wallet_manager = ctx let wallet_manager = ctx.try_get_monero_manager().await?;
.monero_manager
.as_ref()
.context("Monero wallet manager not available")?;
let wallet = wallet_manager.main_wallet().await; let wallet = wallet_manager.main_wallet().await;
let sync_progress = wallet.call(|wallet| wallet.sync_progress()).await; let sync_progress = wallet.call(|wallet| wallet.sync_progress()).await;
@ -2008,10 +1967,7 @@ impl Request for GetMoneroSeedArgs {
type Response = GetMoneroSeedResponse; type Response = GetMoneroSeedResponse;
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> { async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
let wallet_manager = ctx let wallet_manager = ctx.try_get_monero_manager().await?;
.monero_manager
.as_ref()
.context("Monero wallet manager not available")?;
let wallet = wallet_manager.main_wallet().await; let wallet = wallet_manager.main_wallet().await;
let seed = wallet.seed().await?; let seed = wallet.seed().await?;

View file

@ -21,12 +21,13 @@ use tokio::sync::oneshot;
use typeshare::typeshare; use typeshare::typeshare;
use uuid::Uuid; use uuid::Uuid;
const TAURI_UNIFIED_EVENT_NAME: &str = "tauri-unified-event";
#[typeshare] #[typeshare]
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
#[serde(tag = "channelName", content = "event")] #[serde(tag = "channelName", content = "event")]
pub enum TauriEvent { pub enum TauriEvent {
SwapProgress(TauriSwapProgressEventWrapper), SwapProgress(TauriSwapProgressEventWrapper),
ContextInitProgress(TauriContextStatusEvent),
CliLog(TauriLogEvent), CliLog(TauriLogEvent),
BalanceChange(BalanceResponse), BalanceChange(BalanceResponse),
SwapDatabaseStateUpdate(TauriDatabaseStateEvent), SwapDatabaseStateUpdate(TauriDatabaseStateEvent),
@ -46,7 +47,14 @@ pub enum MoneroWalletUpdate {
HistoryUpdate(GetMoneroHistoryResponse), HistoryUpdate(GetMoneroHistoryResponse),
} }
const TAURI_UNIFIED_EVENT_NAME: &str = "tauri-unified-event"; #[typeshare]
#[derive(Clone, Debug, Serialize)]
pub struct ContextStatus {
pub bitcoin_wallet_available: bool,
pub monero_wallet_available: bool,
pub database_available: bool,
pub tor_available: bool,
}
#[typeshare] #[typeshare]
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -456,10 +464,6 @@ pub trait TauriEmitter {
})); }));
} }
fn emit_context_init_progress_event(&self, event: TauriContextStatusEvent) {
self.emit_unified_event(TauriEvent::ContextInitProgress(event));
}
fn emit_cli_log_event(&self, event: TauriLogEvent) { fn emit_cli_log_event(&self, event: TauriLogEvent) {
self.emit_unified_event(TauriEvent::CliLog(event)); self.emit_unified_event(TauriEvent::CliLog(event));
} }

View file

@ -79,17 +79,16 @@ where
.transpose()? .transpose()?
.map(|address| address.into_unchecked()); .map(|address| address.into_unchecked());
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_tor(tor.enable_tor) .with_tor(tor.enable_tor)
.with_bitcoin(bitcoin) .with_bitcoin(bitcoin)
.with_monero(monero) .with_monero(monero)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
BuyXmrArgs { BuyXmrArgs {
rendezvous_points: vec![], rendezvous_points: vec![],
@ -103,14 +102,13 @@ where
Ok(context) Ok(context)
} }
CliCommand::History => { CliCommand::History => {
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
GetHistoryArgs {}.request(context.clone()).await?; GetHistoryArgs {}.request(context.clone()).await?;
@ -121,14 +119,13 @@ where
redact, redact,
swap_id, swap_id,
} => { } => {
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
GetLogsArgs { GetLogsArgs {
logs_dir, logs_dir,
@ -141,29 +138,27 @@ where
Ok(context) Ok(context)
} }
CliCommand::Config => { CliCommand::Config => {
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
GetConfigArgs {}.request(context.clone()).await?; GetConfigArgs {}.request(context.clone()).await?;
Ok(context) Ok(context)
} }
CliCommand::Balance { bitcoin } => { CliCommand::Balance { bitcoin } => {
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_bitcoin(bitcoin) .with_bitcoin(bitcoin)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
BalanceArgs { BalanceArgs {
force_refresh: true, force_refresh: true,
@ -180,15 +175,14 @@ where
} => { } => {
let address = bitcoin_address::validate(address, is_testnet)?; let address = bitcoin_address::validate(address, is_testnet)?;
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_bitcoin(bitcoin) .with_bitcoin(bitcoin)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
WithdrawBtcArgs { amount, address } WithdrawBtcArgs { amount, address }
.request(context.clone()) .request(context.clone())
@ -202,17 +196,16 @@ where
monero, monero,
tor, tor,
} => { } => {
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_tor(tor.enable_tor) .with_tor(tor.enable_tor)
.with_bitcoin(bitcoin) .with_bitcoin(bitcoin)
.with_monero(monero) .with_monero(monero)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
ResumeSwapArgs { swap_id }.request(context.clone()).await?; ResumeSwapArgs { swap_id }.request(context.clone()).await?;
@ -222,15 +215,14 @@ where
swap_id: SwapId { swap_id }, swap_id: SwapId { swap_id },
bitcoin, bitcoin,
} => { } => {
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_bitcoin(bitcoin) .with_bitcoin(bitcoin)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
CancelAndRefundArgs { swap_id } CancelAndRefundArgs { swap_id }
.request(context.clone()) .request(context.clone())
@ -242,15 +234,14 @@ where
rendezvous_point, rendezvous_point,
tor, tor,
} => { } => {
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_tor(tor.enable_tor) .with_tor(tor.enable_tor)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
ListSellersArgs { ListSellersArgs {
rendezvous_points: vec![rendezvous_point], rendezvous_points: vec![rendezvous_point],
@ -261,15 +252,14 @@ where
Ok(context) Ok(context)
} }
CliCommand::ExportBitcoinWallet { bitcoin } => { CliCommand::ExportBitcoinWallet { bitcoin } => {
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_bitcoin(bitcoin) .with_bitcoin(bitcoin)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
ExportBitcoinWalletArgs {}.request(context.clone()).await?; ExportBitcoinWalletArgs {}.request(context.clone()).await?;
@ -278,14 +268,13 @@ where
CliCommand::MoneroRecovery { CliCommand::MoneroRecovery {
swap_id: SwapId { swap_id }, swap_id: SwapId { swap_id },
} => { } => {
let context = Arc::new( let context = Arc::new(Context::new_without_tauri_handle());
ContextBuilder::new(is_testnet) ContextBuilder::new(is_testnet)
.with_data_dir(data) .with_data_dir(data)
.with_debug(debug) .with_debug(debug)
.with_json(json) .with_json(json)
.build() .build(context.clone())
.await?, .await?;
);
MoneroRecoveryArgs { swap_id } MoneroRecoveryArgs { swap_id }
.request(context.clone()) .request(context.clone())

View file

@ -33,17 +33,55 @@ pub fn init(
tauri_handle: Option<TauriHandle>, tauri_handle: Option<TauriHandle>,
trace_stdout: bool, trace_stdout: bool,
) -> Result<()> { ) -> Result<()> {
let TOR_CRATES: Vec<&str> = vec!["arti"]; let TOR_CRATES: Vec<&str> = vec![
"arti",
"arti-client",
"arti-fork",
"tor-api2",
"tor-async-utils",
"tor-basic-utils",
"tor-bytes",
"tor-cell",
"tor-cert",
"tor-chanmgr",
"tor-checkable",
"tor-circmgr",
"tor-config",
"tor-config-path",
"tor-consdiff",
"tor-dirclient",
"tor-dirmgr",
"tor-error",
"tor-general-addr",
"tor-guardmgr",
"tor-hsclient",
"tor-hscrypto",
"tor-hsservice",
"tor-key-forge",
"tor-keymgr",
"tor-linkspec",
"tor-llcrypto",
"tor-log-ratelim",
"tor-memquota",
"tor-netdir",
"tor-netdoc",
"tor-persist",
"tor-proto",
"tor-protover",
"tor-relay-selection",
"tor-rtcompat",
"tor-rtmock",
"tor-socksproto",
"tor-units",
];
let LIBP2P_CRATES: Vec<&str> = vec![ let LIBP2P_CRATES: Vec<&str> = vec![
// Main libp2p crates
"libp2p", "libp2p",
"libp2p_swarm", "libp2p_swarm",
"libp2p_core", "libp2p_core",
"libp2p_tcp", "libp2p_tcp",
"libp2p_noise", "libp2p_noise",
"libp2p_tor", "libp2p_tor",
// Specific libp2p module targets that appear in logs
"libp2p_core::transport", "libp2p_core::transport",
"libp2p_core::transport::choice", "libp2p_core::transport::choice",
"libp2p_core::transport::dummy", "libp2p_core::transport::dummy",
@ -67,6 +105,7 @@ pub fn init(
"libp2p_dcutr", "libp2p_dcutr",
"monero_cpp", "monero_cpp",
]; ];
let OUR_CRATES: Vec<&str> = vec![ let OUR_CRATES: Vec<&str> = vec![
"swap", "swap",
"asb", "asb",
@ -79,8 +118,6 @@ pub fn init(
"monero_rpc_pool", "monero_rpc_pool",
]; ];
let INFO_LEVEL_CRATES: Vec<&str> = vec![];
// General log file for non-verbose logs // General log file for non-verbose logs
let file_appender: RollingFileAppender = tracing_appender::rolling::never(&dir, "swap-all.log"); let file_appender: RollingFileAppender = tracing_appender::rolling::never(&dir, "swap-all.log");
@ -104,11 +141,10 @@ pub fn init(
.with_file(true) .with_file(true)
.with_line_number(true) .with_line_number(true)
.json() .json()
.with_filter(env_filter_with_info_crates( .with_filter(env_filter_with_all_crates(vec![(
level_filter,
OUR_CRATES.clone(), OUR_CRATES.clone(),
INFO_LEVEL_CRATES.clone(), level_filter,
)?); )])?);
// Layer for writing to the verbose log file // Layer for writing to the verbose log file
// Crates: All crates with different levels (libp2p at INFO+, others at TRACE) // Crates: All crates with different levels (libp2p at INFO+, others at TRACE)
@ -121,13 +157,11 @@ pub fn init(
.with_file(true) .with_file(true)
.with_line_number(true) .with_line_number(true)
.json() .json()
.with_filter(env_filter_with_all_crates( .with_filter(env_filter_with_all_crates(vec![
LevelFilter::TRACE, (OUR_CRATES.clone(), LevelFilter::TRACE),
OUR_CRATES.clone(), (LIBP2P_CRATES.clone(), LevelFilter::TRACE),
LIBP2P_CRATES.clone(), (TOR_CRATES.clone(), LevelFilter::TRACE),
TOR_CRATES.clone(), ])?);
INFO_LEVEL_CRATES.clone(),
)?);
// Layer for writing to the terminal // Layer for writing to the terminal
// Crates: swap, asb // Crates: swap, asb
@ -152,29 +186,21 @@ pub fn init(
.with_file(true) .with_file(true)
.with_line_number(true) .with_line_number(true)
.json() .json()
.with_filter(env_filter_with_all_crates( .with_filter(env_filter_with_all_crates(vec![
level_filter, (OUR_CRATES.clone(), LevelFilter::DEBUG),
OUR_CRATES.clone(), (LIBP2P_CRATES.clone(), LevelFilter::INFO),
LIBP2P_CRATES.clone(), (TOR_CRATES.clone(), LevelFilter::INFO),
TOR_CRATES.clone(), ])?);
INFO_LEVEL_CRATES.clone(),
)?);
// If trace_stdout is true, we log all messages to the terminal // If trace_stdout is true, we log all messages to the terminal
// Otherwise, we only log the bare minimum // Otherwise, we only log the bare minimum
let terminal_layer_env_filter = match trace_stdout { let terminal_layer_env_filter = match trace_stdout {
true => env_filter_with_all_crates( true => env_filter_with_all_crates(vec![
LevelFilter::TRACE, (OUR_CRATES.clone(), level_filter),
OUR_CRATES.clone(), (TOR_CRATES.clone(), level_filter),
LIBP2P_CRATES.clone(), (LIBP2P_CRATES.clone(), LevelFilter::INFO),
TOR_CRATES.clone(), ])?,
INFO_LEVEL_CRATES.clone(), false => env_filter_with_all_crates(vec![(OUR_CRATES.clone(), level_filter)])?,
)?,
false => env_filter_with_info_crates(
level_filter,
OUR_CRATES.clone(),
INFO_LEVEL_CRATES.clone(),
)?,
}; };
let final_terminal_layer = match format { let final_terminal_layer = match format {
@ -201,60 +227,18 @@ pub fn init(
Ok(()) Ok(())
} }
/// This function controls which crate's logs actually get logged and from which level, with info-level crates at INFO level or higher.
fn env_filter_with_info_crates(
level_filter: LevelFilter,
our_crates: Vec<&str>,
info_level_crates: Vec<&str>,
) -> Result<EnvFilter> {
let mut filter = EnvFilter::from_default_env();
// Add directives for each crate in the provided list
for crate_name in our_crates {
filter = filter.add_directive(Directive::from_str(&format!(
"{}={}",
crate_name, &level_filter
))?);
}
for crate_name in info_level_crates {
filter = filter.add_directive(Directive::from_str(&format!("{}=INFO", crate_name))?);
}
Ok(filter)
}
/// This function controls which crate's logs actually get logged and from which level, including all crate categories. /// This function controls which crate's logs actually get logged and from which level, including all crate categories.
fn env_filter_with_all_crates( fn env_filter_with_all_crates(crates: Vec<(Vec<&str>, LevelFilter)>) -> Result<EnvFilter> {
level_filter: LevelFilter,
our_crates: Vec<&str>,
libp2p_crates: Vec<&str>,
tor_crates: Vec<&str>,
info_level_crates: Vec<&str>,
) -> Result<EnvFilter> {
let mut filter = EnvFilter::from_default_env(); let mut filter = EnvFilter::from_default_env();
// Add directives for each crate in the provided list // Add directives for each group of crates with their specified level filter
for crate_name in our_crates { for (crate_names, level_filter) in crates {
filter = filter.add_directive(Directive::from_str(&format!( for crate_name in crate_names {
"{}={}", filter = filter.add_directive(Directive::from_str(&format!(
crate_name, &level_filter "{}={}",
))?); crate_name, &level_filter
} ))?);
}
for crate_name in libp2p_crates {
filter = filter.add_directive(Directive::from_str(&format!("{}=INFO", crate_name))?);
}
for crate_name in tor_crates {
filter = filter.add_directive(Directive::from_str(&format!(
"{}={}",
crate_name, &level_filter
))?);
}
for crate_name in info_level_crates {
filter = filter.add_directive(Directive::from_str(&format!("{}=INFO", crate_name))?);
} }
Ok(filter) Ok(filter)