mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-18 01:54:29 -05:00
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:
parent
3b701fe1c5
commit
7d019bfb30
47 changed files with 2361 additions and 2080 deletions
|
|
@ -31,9 +31,13 @@ function isCliLog(log: unknown): log is CliLog {
|
|||
}
|
||||
|
||||
export function isCliLogRelatedToSwap(
|
||||
log: CliLog | string,
|
||||
log: CliLog | string | null | undefined,
|
||||
swapId: string,
|
||||
): boolean {
|
||||
if (log === null || log === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we only have a string, simply check if the string contains the swap id
|
||||
// This provides reasonable backwards compatability
|
||||
if (typeof log === "string") {
|
||||
|
|
@ -44,7 +48,7 @@ export function isCliLogRelatedToSwap(
|
|||
// - the log has the swap id as an attribute
|
||||
// - there exists a span which has the swap id as an attribute
|
||||
return (
|
||||
log.fields["swap_id"] === swapId ||
|
||||
("fields" in log && log.fields["swap_id"] === swapId) ||
|
||||
(log.spans?.some((span) => span["swap_id"] === swapId) ?? false)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,17 @@ import {
|
|||
ApprovalRequest,
|
||||
ExpiredTimelocks,
|
||||
GetSwapInfoResponse,
|
||||
PendingCompleted,
|
||||
QuoteWithAddress,
|
||||
SelectMakerDetails,
|
||||
TauriBackgroundProgress,
|
||||
TauriSwapProgressEvent,
|
||||
SendMoneroDetails,
|
||||
ContextStatus,
|
||||
} from "./tauriModel";
|
||||
import {
|
||||
ContextStatusType,
|
||||
ResultContextStatus,
|
||||
RPCSlice,
|
||||
} from "store/features/rpcSlice";
|
||||
|
||||
export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"];
|
||||
|
||||
|
|
@ -382,3 +386,30 @@ export function haveFundsBeenLocked(
|
|||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,9 +227,8 @@ export async function fetchAllConversations(): Promise<void> {
|
|||
store.dispatch(setConversation({ feedbackId, messages }));
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
"Error fetching messages for feedback id",
|
||||
feedbackId,
|
||||
{ error, feedbackId },
|
||||
"Error fetching messages for feedback",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { listen } from "@tauri-apps/api/event";
|
||||
import { TauriContextStatusEvent, TauriEvent } from "models/tauriModel";
|
||||
import { TauriEvent } from "models/tauriModel";
|
||||
import {
|
||||
contextStatusEventReceived,
|
||||
contextInitializationFailed,
|
||||
rpcSetBalance,
|
||||
timelockChangeEventReceived,
|
||||
approvalEventReceived,
|
||||
|
|
@ -18,7 +19,7 @@ import {
|
|||
updateRates,
|
||||
} from "./api";
|
||||
import {
|
||||
checkContextAvailability,
|
||||
checkContextStatus,
|
||||
getSwapInfo,
|
||||
initializeContext,
|
||||
listSellersAtRendezvousPoint,
|
||||
|
|
@ -53,6 +54,9 @@ const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
|
|||
// Fetch pending approvals every 2 seconds
|
||||
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 {
|
||||
callback();
|
||||
setInterval(callback, interval);
|
||||
|
|
@ -76,23 +80,27 @@ export async function setupBackgroundTasks(): Promise<void> {
|
|||
|
||||
// Setup Tauri event listeners
|
||||
// Check if the context is already available. This is to prevent unnecessary re-initialization
|
||||
if (await checkContextAvailability()) {
|
||||
store.dispatch(
|
||||
contextStatusEventReceived(TauriContextStatusEvent.Available),
|
||||
);
|
||||
} else {
|
||||
setIntervalImmediate(async () => {
|
||||
const contextStatus = await checkContextStatus();
|
||||
store.dispatch(contextStatusEventReceived(contextStatus));
|
||||
}, CHECK_CONTEXT_STATUS_INTERVAL);
|
||||
|
||||
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
|
||||
initializeContext().catch((e) => {
|
||||
logger.error(
|
||||
e,
|
||||
"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
|
||||
setTimeout(() => {
|
||||
initializeContext().catch((e) => {
|
||||
logger.error(e, "Failed to initialize context even after retry");
|
||||
});
|
||||
}, 2000);
|
||||
store.dispatch(contextInitializationFailed(String(e)));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -105,10 +113,6 @@ export async function setupBackgroundTasks(): Promise<void> {
|
|||
store.dispatch(swapProgressEventReceived(eventData));
|
||||
break;
|
||||
|
||||
case "ContextInitProgress":
|
||||
store.dispatch(contextStatusEventReceived(eventData));
|
||||
break;
|
||||
|
||||
case "CliLog":
|
||||
store.dispatch(receivedCliLog(eventData));
|
||||
break;
|
||||
|
|
@ -159,4 +163,3 @@ export async function setupBackgroundTasks(): Promise<void> {
|
|||
exhaustiveGuard(channelName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import SeedSelectionDialog from "./modal/seed-selection/SeedSelectionDialog";
|
|||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog";
|
||||
import ContextErrorDialog from "./modal/context-error/ContextErrorDialog";
|
||||
|
||||
declare module "@mui/material/styles" {
|
||||
interface Theme {
|
||||
|
|
@ -54,6 +55,7 @@ export default function App() {
|
|||
<IntroductionModal />
|
||||
<SeedSelectionDialog />
|
||||
<PasswordEntryDialog />
|
||||
<ContextErrorDialog />
|
||||
<Router>
|
||||
<Navigation />
|
||||
<InnerContent />
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@ import {
|
|||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { ContextStatus } from "models/tauriModel";
|
||||
import { isContextFullyInitialized } from "models/tauriModelExt";
|
||||
import { useSnackbar } from "notistack";
|
||||
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> {
|
||||
onSuccess?: (data: T) => void | null;
|
||||
|
|
@ -23,7 +26,10 @@ interface PromiseInvokeButtonProps<T> {
|
|||
disabled?: boolean;
|
||||
displayErrorSnackbar?: boolean;
|
||||
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>({
|
||||
|
|
@ -39,13 +45,11 @@ export default function PromiseInvokeButton<T>({
|
|||
isChipButton = false,
|
||||
displayErrorSnackbar = false,
|
||||
onPendingChange = null,
|
||||
requiresContext = true,
|
||||
contextRequirement = true,
|
||||
tooltipTitle = null,
|
||||
...rest
|
||||
}: PromiseInvokeButtonProps<T> & ButtonProps) {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const isContextAvailable = useIsContextAvailable();
|
||||
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
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 actualTooltipTitle =
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks";
|
|||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
|
||||
import { bytesToMb } from "utils/conversionUtils";
|
||||
import {
|
||||
TauriBackgroundProgress,
|
||||
TauriContextStatusEvent,
|
||||
} from "models/tauriModel";
|
||||
import { TauriBackgroundProgress } from "models/tauriModel";
|
||||
import { useEffect, useState } from "react";
|
||||
import TruncatedText from "../other/TruncatedText";
|
||||
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() {
|
||||
const backgroundProgress = usePendingBackgroundProcesses();
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ export default function SwapSuspendAlert({
|
|||
color="primary"
|
||||
onSuccess={onClose}
|
||||
onInvoke={suspendCurrentSwap}
|
||||
contextRequirement={false}
|
||||
>
|
||||
Suspend
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -227,7 +227,7 @@ export default function FeedbackDialog({
|
|||
<DialogActions>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<PromiseInvokeButton
|
||||
requiresContext={false}
|
||||
contextRequirement={false}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onInvoke={submitFeedback}
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ import {
|
|||
Switch,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { CliLog } from "models/cliModel";
|
||||
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||
import { HashedLog } from "store/features/logsSlice";
|
||||
|
||||
interface LogViewerProps {
|
||||
open: boolean;
|
||||
setOpen: (_: boolean) => void;
|
||||
logs: (string | CliLog)[] | null;
|
||||
logs: HashedLog[];
|
||||
setIsRedacted: (_: boolean) => void;
|
||||
isRedacted: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ import { store } from "renderer/store/storeRenderer";
|
|||
import { useActiveSwapInfo } from "store/hooks";
|
||||
import { logsToRawString } from "utils/parseUtils";
|
||||
import { getLogsOfSwap, redactLogs } from "renderer/rpc";
|
||||
import { CliLog, parseCliLogString } from "models/cliModel";
|
||||
import { parseCliLogString } from "models/cliModel";
|
||||
import logger from "utils/logger";
|
||||
import { submitFeedbackViaHttp } from "renderer/api";
|
||||
import { addFeedbackId } from "store/features/conversationsSlice";
|
||||
import { AttachmentInput } from "models/apiModel";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { HashedLog, hashLogs } from "store/features/logsSlice";
|
||||
|
||||
export const MAX_FEEDBACK_LENGTH = 4000;
|
||||
|
||||
|
|
@ -21,8 +22,8 @@ interface FeedbackInputState {
|
|||
}
|
||||
|
||||
interface FeedbackLogsState {
|
||||
swapLogs: (string | CliLog)[] | null;
|
||||
daemonLogs: (string | CliLog)[] | null;
|
||||
swapLogs: HashedLog[];
|
||||
daemonLogs: HashedLog[];
|
||||
}
|
||||
|
||||
const initialInputState: FeedbackInputState = {
|
||||
|
|
@ -34,8 +35,8 @@ const initialInputState: FeedbackInputState = {
|
|||
};
|
||||
|
||||
const initialLogsState: FeedbackLogsState = {
|
||||
swapLogs: null,
|
||||
daemonLogs: null,
|
||||
swapLogs: [],
|
||||
daemonLogs: [],
|
||||
};
|
||||
|
||||
export function useFeedback() {
|
||||
|
|
@ -55,56 +56,60 @@ export function useFeedback() {
|
|||
|
||||
useEffect(() => {
|
||||
if (inputState.selectedSwap === null) {
|
||||
setLogsState((prev) => ({ ...prev, swapLogs: null }));
|
||||
setLogsState((prev) => ({ ...prev, swapLogs: [] }));
|
||||
return;
|
||||
}
|
||||
|
||||
getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted)
|
||||
.then((response) => {
|
||||
const parsedLogs = response.logs.map(parseCliLogString);
|
||||
setLogsState((prev) => ({
|
||||
...prev,
|
||||
swapLogs: response.logs.map(parseCliLogString),
|
||||
swapLogs: hashLogs(parsedLogs),
|
||||
}));
|
||||
setError(null);
|
||||
})
|
||||
.catch((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}`);
|
||||
});
|
||||
}, [inputState.selectedSwap, inputState.isSwapLogsRedacted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputState.attachDaemonLogs) {
|
||||
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
|
||||
setLogsState((prev) => ({ ...prev, daemonLogs: [] }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const hashedLogs = store.getState().logs?.state.logs ?? [];
|
||||
|
||||
if (inputState.isDaemonLogsRedacted) {
|
||||
redactLogs(store.getState().logs?.state.logs)
|
||||
const logs = hashedLogs.map((h) => h.log);
|
||||
redactLogs(logs)
|
||||
.then((redactedLogs) => {
|
||||
setLogsState((prev) => ({
|
||||
...prev,
|
||||
daemonLogs: redactedLogs,
|
||||
daemonLogs: hashLogs(redactedLogs),
|
||||
}));
|
||||
setError(null);
|
||||
})
|
||||
.catch((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}`);
|
||||
});
|
||||
} else {
|
||||
setLogsState((prev) => ({
|
||||
...prev,
|
||||
daemonLogs: store.getState().logs?.state.logs,
|
||||
daemonLogs: hashedLogs,
|
||||
}));
|
||||
setError(null);
|
||||
}
|
||||
} catch (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}`);
|
||||
}
|
||||
}, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted]);
|
||||
|
|
@ -123,18 +128,18 @@ export function useFeedback() {
|
|||
|
||||
const attachments: AttachmentInput[] = [];
|
||||
// Add swap logs as an attachment
|
||||
if (logsState.swapLogs) {
|
||||
if (logsState.swapLogs.length > 0) {
|
||||
attachments.push({
|
||||
key: `swap_logs_${inputState.selectedSwap}.txt`,
|
||||
content: logsToRawString(logsState.swapLogs),
|
||||
content: logsToRawString(logsState.swapLogs.map((h) => h.log)),
|
||||
});
|
||||
}
|
||||
|
||||
// Handle daemon logs
|
||||
if (logsState.daemonLogs) {
|
||||
if (logsState.daemonLogs.length > 0) {
|
||||
attachments.push({
|
||||
key: "daemon_logs.txt",
|
||||
content: logsToRawString(logsState.daemonLogs),
|
||||
content: logsToRawString(logsState.daemonLogs.map((h) => h.log)),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export default function PasswordEntryDialog() {
|
|||
<PromiseInvokeButton
|
||||
onInvoke={accept}
|
||||
variant="contained"
|
||||
requiresContext={false}
|
||||
contextRequirement={false}
|
||||
>
|
||||
Unlock
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ export default function SeedSelectionDialog() {
|
|||
<PromiseInvokeButton
|
||||
variant="text"
|
||||
onInvoke={Legacy}
|
||||
requiresContext={false}
|
||||
contextRequirement={false}
|
||||
color="inherit"
|
||||
>
|
||||
No wallet (Legacy)
|
||||
|
|
@ -373,7 +373,7 @@ export default function SeedSelectionDialog() {
|
|||
onInvoke={accept}
|
||||
variant="contained"
|
||||
disabled={isDisabled}
|
||||
requiresContext={false}
|
||||
contextRequirement={false}
|
||||
>
|
||||
Continue
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
import { Box, DialogContentText } from "@mui/material";
|
||||
import {
|
||||
useActiveSwapInfo,
|
||||
useActiveSwapLogs,
|
||||
useAppSelector,
|
||||
} from "store/hooks";
|
||||
import JsonTreeView from "../../../other/JSONViewTree";
|
||||
import { useActiveSwapLogs } from "store/hooks";
|
||||
import CliLogsBox from "../../../other/RenderedCliLog";
|
||||
|
||||
export default function DebugPage() {
|
||||
const logs = useActiveSwapLogs();
|
||||
const cliState = useActiveSwapInfo();
|
||||
|
||||
return (
|
||||
<Box sx={{ padding: 2, display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import DialogHeader from "../DialogHeader";
|
|||
import AddressInputPage from "./pages/AddressInputPage";
|
||||
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
|
||||
import WithdrawDialogContent from "./WithdrawDialogContent";
|
||||
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
|
||||
|
||||
export default function WithdrawDialog({
|
||||
open,
|
||||
|
|
@ -61,6 +62,7 @@ export default function WithdrawDialog({
|
|||
onInvoke={() => withdrawBtc(withdrawAddress)}
|
||||
onPendingChange={setPending}
|
||||
onSuccess={setWithdrawTxId}
|
||||
contextRequirement={isContextWithBitcoinWallet}
|
||||
>
|
||||
Withdraw
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
import { Box, Tooltip } from "@mui/material";
|
||||
import GitHubIcon from "@mui/icons-material/GitHub";
|
||||
import DaemonStatusAlert, {
|
||||
BackgroundProgressAlerts,
|
||||
} from "../alert/DaemonStatusAlert";
|
||||
import { BackgroundProgressAlerts } from "../alert/DaemonStatusAlert";
|
||||
import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
|
||||
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
|
||||
import LinkIconButton from "../icons/LinkIconButton";
|
||||
import BackgroundRefundAlert from "../alert/BackgroundRefundAlert";
|
||||
import MatrixIcon from "../icons/MatrixIcon";
|
||||
import { MenuBook } from "@mui/icons-material";
|
||||
import ContactInfoBox from "../other/ContactInfoBox";
|
||||
|
||||
export default function NavigationFooter() {
|
||||
return (
|
||||
|
|
@ -23,36 +18,8 @@ export default function NavigationFooter() {
|
|||
<FundsLeftInWalletAlert />
|
||||
<UnfinishedSwapsAlert />
|
||||
<BackgroundRefundAlert />
|
||||
<DaemonStatusAlert />
|
||||
<BackgroundProgressAlerts />
|
||||
<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://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>
|
||||
<ContactInfoBox />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
46
src-gui/src/renderer/components/other/ContactInfoBox.tsx
Normal file
46
src-gui/src/renderer/components/other/ContactInfoBox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { Box, Chip, Typography } from "@mui/material";
|
||||
import { CliLog } from "models/cliModel";
|
||||
import { HashedLog } from "store/features/logsSlice";
|
||||
import { ReactNode, useMemo, useState } from "react";
|
||||
import { logsToRawString } from "utils/parseUtils";
|
||||
import ScrollablePaperTextBox from "./ScrollablePaperTextBox";
|
||||
|
|
@ -62,44 +63,54 @@ export default function CliLogsBox({
|
|||
label,
|
||||
logs,
|
||||
topRightButton = null,
|
||||
autoScroll = false,
|
||||
autoScroll = true,
|
||||
minHeight,
|
||||
}: {
|
||||
label: string;
|
||||
logs: (CliLog | string)[];
|
||||
logs: HashedLog[];
|
||||
topRightButton?: ReactNode;
|
||||
autoScroll?: boolean;
|
||||
minHeight?: string;
|
||||
}) {
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
|
||||
const memoizedLogs = useMemo(() => {
|
||||
const filteredLogs = useMemo(() => {
|
||||
if (searchQuery.length === 0) {
|
||||
return logs;
|
||||
}
|
||||
return logs.filter((log) =>
|
||||
|
||||
return logs.filter(({ log }) =>
|
||||
JSON.stringify(log).toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
);
|
||||
}, [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 (
|
||||
<ScrollablePaperTextBox
|
||||
minHeight={minHeight}
|
||||
title={label}
|
||||
copyValue={logsToRawString(logs)}
|
||||
copyValue={logsToRawString(rawStrings)}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
topRightButton={topRightButton}
|
||||
autoScroll={autoScroll}
|
||||
rows={memoizedLogs.map((log) =>
|
||||
typeof log === "string" ? (
|
||||
<Typography key={log} component="pre">
|
||||
{log}
|
||||
</Typography>
|
||||
) : (
|
||||
<RenderedCliLog log={log} key={JSON.stringify(log)} />
|
||||
),
|
||||
)}
|
||||
rows={rows}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ function ConversationModal({
|
|||
// Fetch updated conversations
|
||||
fetchAllConversations();
|
||||
} catch (error) {
|
||||
logger.error("Error sending message:", error);
|
||||
logger.error(`Error sending message: ${error}`);
|
||||
enqueueSnackbar("Failed to send message. Please try again.", {
|
||||
variant: "error",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,31 +1,27 @@
|
|||
import { Box } from "@mui/material";
|
||||
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
|
||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||
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 RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||
import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
||||
import { TauriContextStatusEvent } from "models/tauriModel";
|
||||
import { ContextStatusType } from "store/features/rpcSlice";
|
||||
|
||||
export default function DaemonControlBox() {
|
||||
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 canContextBeManuallyStarted = useAppSelector(
|
||||
(s) =>
|
||||
s.rpc.status === TauriContextStatusEvent.Failed || s.rpc.status === null,
|
||||
);
|
||||
const isContextInitializing = useAppSelector(
|
||||
(s) => s.rpc.status === TauriContextStatusEvent.Initializing,
|
||||
);
|
||||
|
||||
const stringifiedDaemonStatus = useAppSelector(
|
||||
(s) => s.rpc.status ?? "not started",
|
||||
);
|
||||
const stringifiedDaemonStatus = useAppSelector((s) => {
|
||||
if (s.rpc.status === null) {
|
||||
return "not started";
|
||||
}
|
||||
if (s.rpc.status.type === ContextStatusType.Error) {
|
||||
return "failed";
|
||||
}
|
||||
return "running";
|
||||
});
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
|
|
@ -36,22 +32,11 @@ export default function DaemonControlBox() {
|
|||
}
|
||||
additionalContent={
|
||||
<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
|
||||
variant="contained"
|
||||
endIcon={<RotateLeftIcon />}
|
||||
onInvoke={relaunch}
|
||||
requiresContext={false}
|
||||
contextRequirement={false}
|
||||
displayErrorSnackbar
|
||||
>
|
||||
Restart GUI
|
||||
|
|
@ -59,7 +44,7 @@ export default function DaemonControlBox() {
|
|||
<PromiseInvokeButton
|
||||
endIcon={<FolderOpenIcon />}
|
||||
isIconButton
|
||||
requiresContext={false}
|
||||
contextRequirement={false}
|
||||
size="small"
|
||||
tooltipTitle="Open the data directory in your file explorer"
|
||||
onInvoke={async () => {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { getWalletDescriptor } from "renderer/rpc";
|
|||
import { ExportBitcoinWalletResponse } from "models/tauriModel";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
|
||||
|
||||
export default function ExportDataBox() {
|
||||
const [walletDescriptor, setWalletDescriptor] =
|
||||
|
|
@ -52,6 +53,7 @@ export default function ExportDataBox() {
|
|||
onInvoke={getWalletDescriptor}
|
||||
onSuccess={setWalletDescriptor}
|
||||
displayErrorSnackbar={true}
|
||||
contextRequirement={isContextWithBitcoinWallet}
|
||||
>
|
||||
Reveal Bitcoin Wallet Private Key
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export default function ExportLogsButton({
|
|||
}: ExportLogsButtonProps) {
|
||||
async function handleExportLogs() {
|
||||
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 = {
|
||||
swap_logs: logsToRawString(swapLogs.logs),
|
||||
|
|
|
|||
|
|
@ -6,21 +6,23 @@ import {
|
|||
DialogTitle,
|
||||
} from "@mui/material";
|
||||
import { ButtonProps } from "@mui/material/Button";
|
||||
import { CliLog, parseCliLogString } from "models/cliModel";
|
||||
import { parseCliLogString } from "models/cliModel";
|
||||
import { GetLogsResponse } from "models/tauriModel";
|
||||
import { useState } from "react";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { getLogsOfSwap } from "renderer/rpc";
|
||||
import CliLogsBox from "../../../other/RenderedCliLog";
|
||||
import { HashedLog, hashLogs } from "store/features/logsSlice";
|
||||
|
||||
export default function SwapLogFileOpenButton({
|
||||
swapId,
|
||||
...props
|
||||
}: { swapId: string } & ButtonProps) {
|
||||
const [logs, setLogs] = useState<(CliLog | string)[] | null>(null);
|
||||
const [logs, setLogs] = useState<HashedLog[] | null>(null);
|
||||
|
||||
function onLogsReceived(response: GetLogsResponse) {
|
||||
setLogs(response.logs.map(parseCliLogString));
|
||||
const parsedLogs = response.logs.map(parseCliLogString);
|
||||
setLogs(hashLogs(parsedLogs));
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
GetMoneroSeedResponse,
|
||||
GetRestoreHeightResponse,
|
||||
} from "models/tauriModel";
|
||||
import { isContextWithMoneroWallet } from "models/tauriModelExt";
|
||||
|
||||
interface SeedPhraseButtonProps {
|
||||
onMenuClose: () => void;
|
||||
|
|
@ -32,6 +33,7 @@ export default function SeedPhraseButton({
|
|||
onSuccess={handleSeedPhraseSuccess}
|
||||
displayErrorSnackbar={true}
|
||||
variant="text"
|
||||
contextRequirement={isContextWithMoneroWallet}
|
||||
sx={{
|
||||
justifyContent: "flex-start",
|
||||
textTransform: "none",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { getRestoreHeight, setMoneroRestoreHeight } from "renderer/rpc";
|
|||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||
import { Dayjs } from "dayjs";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { isContextWithMoneroWallet } from "models/tauriModelExt";
|
||||
|
||||
enum RestoreOption {
|
||||
BlockHeight = "blockHeight",
|
||||
|
|
@ -133,6 +134,7 @@ export default function SetRestoreHeightModal({
|
|||
onSuccess={onClose}
|
||||
displayErrorSnackbar={true}
|
||||
onPendingChange={setIsPending}
|
||||
contextRequirement={isContextWithMoneroWallet}
|
||||
>
|
||||
Confirm
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import DFXSwissLogo from "assets/dfx-logo.svg";
|
|||
import { useState } from "react";
|
||||
import { dfxAuthenticate } from "renderer/rpc";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { isContextWithMoneroWallet } from "models/tauriModelExt";
|
||||
|
||||
function DFXLogo({ height = 24 }: { height?: number }) {
|
||||
return (
|
||||
|
|
@ -53,6 +54,7 @@ export default function DfxButton() {
|
|||
tooltipTitle="Buy Monero with fiat using DFX"
|
||||
displayErrorSnackbar
|
||||
isChipButton
|
||||
contextRequirement={isContextWithMoneroWallet}
|
||||
>
|
||||
Buy Monero
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export default function SendApprovalContent({
|
|||
color="error"
|
||||
startIcon={<CloseIcon />}
|
||||
displayErrorSnackbar={true}
|
||||
requiresContext={false}
|
||||
contextRequirement={false}
|
||||
>
|
||||
Reject
|
||||
</PromiseInvokeButton>
|
||||
|
|
@ -141,7 +141,7 @@ export default function SendApprovalContent({
|
|||
color="primary"
|
||||
startIcon={<CheckIcon />}
|
||||
displayErrorSnackbar={true}
|
||||
requiresContext={false}
|
||||
contextRequirement={false}
|
||||
>
|
||||
Send
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
|||
import { sendMoneroTransaction } from "renderer/rpc";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import { SendMoneroResponse } from "models/tauriModel";
|
||||
import { isContextWithMoneroWallet } from "models/tauriModelExt";
|
||||
|
||||
interface SendTransactionContentProps {
|
||||
balance: {
|
||||
|
|
@ -168,6 +169,7 @@ export default function SendTransactionContent({
|
|||
disabled={isSendDisabled}
|
||||
onSuccess={handleSendSuccess}
|
||||
onPendingChange={setIsSending}
|
||||
contextRequirement={isContextWithMoneroWallet}
|
||||
>
|
||||
Send
|
||||
</PromiseInvokeButton>
|
||||
|
|
|
|||
|
|
@ -145,7 +145,6 @@ export default function SwapSetupInflightPage({
|
|||
resolveApproval(request.request_id, false as unknown as object)
|
||||
}
|
||||
displayErrorSnackbar
|
||||
requiresContext
|
||||
>
|
||||
Deny
|
||||
</PromiseInvokeButton>
|
||||
|
|
@ -158,7 +157,6 @@ export default function SwapSetupInflightPage({
|
|||
resolveApproval(request.request_id, true as unknown as object)
|
||||
}
|
||||
displayErrorSnackbar
|
||||
requiresContext
|
||||
endIcon={<CheckIcon />}
|
||||
>
|
||||
{`Confirm`}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import RefreshIcon from "@mui/icons-material/Refresh";
|
|||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { checkBitcoinBalance } from "renderer/rpc";
|
||||
import { isSyncingBitcoin } from "store/hooks";
|
||||
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
|
||||
|
||||
export default function WalletRefreshButton() {
|
||||
const isSyncing = isSyncingBitcoin();
|
||||
|
|
@ -14,6 +15,7 @@ export default function WalletRefreshButton() {
|
|||
onInvoke={() => checkBitcoinBalance()}
|
||||
displayErrorSnackbar
|
||||
size="small"
|
||||
contextRequirement={isContextWithBitcoinWallet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,11 +46,13 @@ import {
|
|||
GetRestoreHeightResponse,
|
||||
MoneroNodeConfig,
|
||||
GetMoneroSeedResponse,
|
||||
ContextStatus,
|
||||
} from "models/tauriModel";
|
||||
import {
|
||||
rpcSetBalance,
|
||||
rpcSetSwapInfo,
|
||||
approvalRequestsReplaced,
|
||||
contextInitializationFailed,
|
||||
} from "store/features/rpcSlice";
|
||||
import {
|
||||
setMainAddress,
|
||||
|
|
@ -282,9 +284,8 @@ export async function getMoneroRecoveryKeys(
|
|||
);
|
||||
}
|
||||
|
||||
export async function checkContextAvailability(): Promise<boolean> {
|
||||
const available = await invokeNoArgs<boolean>("is_context_available");
|
||||
return available;
|
||||
export async function checkContextStatus(): Promise<ContextStatus> {
|
||||
return await invokeNoArgs<ContextStatus>("get_context_status");
|
||||
}
|
||||
|
||||
export async function getLogsOfSwap(
|
||||
|
|
@ -335,7 +336,6 @@ export async function initializeContext() {
|
|||
store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null;
|
||||
|
||||
// Check the state of the Monero node
|
||||
|
||||
const moneroNodeConfig =
|
||||
useMoneroRpcPool ||
|
||||
moneroNodeUrl == null ||
|
||||
|
|
@ -356,18 +356,17 @@ export async function initializeContext() {
|
|||
enable_monero_tor: useMoneroTor,
|
||||
};
|
||||
|
||||
logger.info("Initializing context with settings", tauriSettings);
|
||||
logger.info({ tauriSettings }, "Initializing context with settings");
|
||||
|
||||
try {
|
||||
await invokeUnsafe<void>("initialize_context", {
|
||||
settings: tauriSettings,
|
||||
testnet,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error("Couldn't initialize context: " + error);
|
||||
}
|
||||
|
||||
logger.info("Initialized context");
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWalletDescriptor() {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|||
import { TauriLogEvent } from "models/tauriModel";
|
||||
import { parseLogsFromString } from "utils/parseUtils";
|
||||
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 {
|
||||
logs: (CliLog | string)[];
|
||||
logs: HashedLog[];
|
||||
}
|
||||
|
||||
export interface LogsSlice {
|
||||
|
|
@ -17,21 +21,67 @@ const initialState: LogsSlice = {
|
|||
},
|
||||
};
|
||||
|
||||
export type HashedLog = {
|
||||
log: CliLog | string;
|
||||
hash: string;
|
||||
};
|
||||
|
||||
export const logsSlice = createSlice({
|
||||
name: "logs",
|
||||
initialState,
|
||||
reducers: {
|
||||
receivedCliLog(slice, action: PayloadAction<TauriLogEvent>) {
|
||||
const buffer = action.payload.buffer;
|
||||
const logs = parseLogsFromString(buffer);
|
||||
const logsWithoutExisting = logs.filter(
|
||||
(log) => !slice.state.logs.includes(log),
|
||||
);
|
||||
slice.state.logs = slice.state.logs.concat(logsWithoutExisting);
|
||||
const parsedLogs = parseLogsFromString(action.payload.buffer);
|
||||
const hashedLogs = parsedLogs.map(createHashedLog);
|
||||
for (const entry of hashedLogs) {
|
||||
slice.state.logs.push(entry);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|||
import { ExtendedMakerStatus, MakerStatus } from "models/apiModel";
|
||||
import {
|
||||
GetSwapInfoResponse,
|
||||
TauriContextStatusEvent,
|
||||
ContextStatus,
|
||||
TauriTimelockChangeEvent,
|
||||
BackgroundRefundState,
|
||||
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 {
|
||||
status: TauriContextStatusEvent | null;
|
||||
status: ResultContextStatus | null;
|
||||
state: State;
|
||||
}
|
||||
|
||||
|
|
@ -60,11 +69,18 @@ export const rpcSlice = createSlice({
|
|||
name: "rpc",
|
||||
initialState,
|
||||
reducers: {
|
||||
contextStatusEventReceived(
|
||||
slice,
|
||||
action: PayloadAction<TauriContextStatusEvent>,
|
||||
) {
|
||||
slice.status = action.payload;
|
||||
contextStatusEventReceived(slice, action: PayloadAction<ContextStatus>) {
|
||||
// Don't overwrite error state
|
||||
//
|
||||
// Once we're in an error state, stay there
|
||||
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(
|
||||
slice: RPCSlice,
|
||||
|
|
@ -160,6 +176,7 @@ export const rpcSlice = createSlice({
|
|||
|
||||
export const {
|
||||
contextStatusEventReceived,
|
||||
contextInitializationFailed,
|
||||
rpcSetBalance,
|
||||
rpcSetWithdrawTxId,
|
||||
rpcResetWithdrawTxId,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
isPendingSendMoneroApprovalEvent,
|
||||
PendingPasswordApprovalRequest,
|
||||
isPendingPasswordApprovalEvent,
|
||||
isContextFullyInitialized,
|
||||
} from "models/tauriModelExt";
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
||||
|
|
@ -28,7 +29,6 @@ import { RatesState } from "./features/ratesSlice";
|
|||
import {
|
||||
TauriBackgroundProgress,
|
||||
TauriBitcoinSyncProgress,
|
||||
TauriContextStatusEvent,
|
||||
} from "models/tauriModel";
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
|
|
@ -111,9 +111,7 @@ export function useIsSpecificSwapRunning(swapId: string | null) {
|
|||
}
|
||||
|
||||
export function useIsContextAvailable() {
|
||||
return useAppSelector(
|
||||
(state) => state.rpc.status === TauriContextStatusEvent.Available,
|
||||
);
|
||||
return useAppSelector((state) => isContextFullyInitialized(state.rpc.status));
|
||||
}
|
||||
|
||||
/// 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 logs = useAppSelector((s) => s.logs.state.logs);
|
||||
|
||||
return useMemo(
|
||||
() => logs.filter((log) => isCliLogRelatedToSwap(log, swapId)),
|
||||
[logs, swapId],
|
||||
);
|
||||
return useMemo(() => {
|
||||
if (swapId == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return logs.filter((log) => isCliLogRelatedToSwap(log.log, swapId));
|
||||
}, [logs, swapId]);
|
||||
}
|
||||
|
||||
export function useAllMakers() {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ import {
|
|||
getCurrentMoneroNodeConfig,
|
||||
} from "renderer/rpc";
|
||||
import logger from "utils/logger";
|
||||
import { contextStatusEventReceived } from "store/features/rpcSlice";
|
||||
import {
|
||||
contextStatusEventReceived,
|
||||
ContextStatusType,
|
||||
} from "store/features/rpcSlice";
|
||||
import {
|
||||
addNode,
|
||||
setFetchFiatPrices,
|
||||
|
|
@ -21,14 +24,12 @@ import {
|
|||
Network,
|
||||
} from "store/features/settingsSlice";
|
||||
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 {
|
||||
addFeedbackId,
|
||||
setConversation,
|
||||
} 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
|
||||
const throttledGetSwapInfoFunctions = new Map<
|
||||
|
|
@ -60,30 +61,80 @@ const getThrottledSwapInfoUpdater = (swapId: string) => {
|
|||
export function createMainListeners() {
|
||||
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
|
||||
listener.startListening({
|
||||
actionCreator: contextStatusEventReceived,
|
||||
effect: async (action) => {
|
||||
const status = action.payload;
|
||||
predicate: (action, currentState, previousState) => {
|
||||
const currentStatus = (currentState as RootState).rpc.status;
|
||||
const previousStatus = (previousState as RootState).rpc.status;
|
||||
|
||||
// If the context is available, check the Bitcoin balance and fetch all swap infos
|
||||
if (status === TauriContextStatusEvent.Available) {
|
||||
logger.debug(
|
||||
"Context is available, checking Bitcoin balance and history",
|
||||
// Only trigger if the status actually changed
|
||||
return currentStatus !== previousStatus;
|
||||
},
|
||||
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([
|
||||
checkBitcoinBalance(),
|
||||
getAllSwapInfos(),
|
||||
fetchSellersAtPresetRendezvousPoints(),
|
||||
initializeMoneroWallet(),
|
||||
]);
|
||||
await checkBitcoinBalance();
|
||||
}
|
||||
|
||||
// If the Monero wallet just came available, initialize the Monero wallet
|
||||
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
|
||||
// In case the user changed this WHILE the context was unavailable
|
||||
const nodeConfig = await getCurrentMoneroNodeConfig();
|
||||
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 {
|
||||
const nodeConfig = await getCurrentMoneroNodeConfig();
|
||||
await changeMoneroNode(nodeConfig);
|
||||
logger.info("Changed Monero node configuration to: ", nodeConfig);
|
||||
logger.info({ nodeConfig }, "Changed Monero node configuration to: ");
|
||||
} 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
13
src-gui/src/utils/hash.ts
Normal 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");
|
||||
}
|
||||
|
|
@ -2,16 +2,7 @@
|
|||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "desktop-capability",
|
||||
"description": "Capabilities for desktop windows",
|
||||
"platforms": [
|
||||
"macOS",
|
||||
"windows",
|
||||
"linux"
|
||||
],
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"cli:default",
|
||||
"cli:allow-cli-matches"
|
||||
]
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"windows": ["main"],
|
||||
"permissions": ["cli:default", "cli:allow-cli-matches"]
|
||||
}
|
||||
452
src-tauri/src/commands.rs
Normal file
452
src-tauri/src/commands.rs
Normal 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);
|
||||
|
|
@ -1,121 +1,43 @@
|
|||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::result::Result;
|
||||
use std::sync::Arc;
|
||||
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::{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};
|
||||
use swap::cli::api::{tauri_bindings::TauriHandle, Context};
|
||||
use tauri::{Manager, RunEvent};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// Trait to convert Result<T, E> to Result<T, String>
|
||||
/// Tauri commands require the error type to be a string
|
||||
trait ToStringResult<T> {
|
||||
fn to_string_result(self) -> Result<T, String>;
|
||||
}
|
||||
mod commands;
|
||||
|
||||
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]
|
||||
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()
|
||||
}
|
||||
};
|
||||
}
|
||||
use commands::*;
|
||||
|
||||
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
||||
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,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new State instance with no Context
|
||||
fn new(handle: TauriHandle) -> Self {
|
||||
let context = Arc::new(Context::new_with_tauri_handle(handle.clone()));
|
||||
let context_lock = Mutex::new(());
|
||||
|
||||
Self {
|
||||
context: RwLock::new(None),
|
||||
context,
|
||||
context_lock,
|
||||
handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to retrieve the context
|
||||
/// Returns an error if the context is not available
|
||||
fn try_get_context(&self) -> Result<Arc<Context>, String> {
|
||||
self.context
|
||||
.try_read()
|
||||
.map_err(|_| "Context is being modified".to_string())?
|
||||
.clone()
|
||||
.ok_or("Context not available".to_string())
|
||||
fn context(&self) -> Arc<Context> {
|
||||
self.context.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,44 +99,7 @@ pub fn run() {
|
|||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.invoke_handler(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,
|
||||
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,
|
||||
])
|
||||
.invoke_handler(generate_command_handlers!())
|
||||
.setup(setup)
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
|
|
@ -225,335 +110,13 @@ pub fn run() {
|
|||
// If the application is forcibly closed, this may not be called.
|
||||
// TODO: fix that
|
||||
let state = app.state::<State>();
|
||||
let context_to_cleanup = if let Ok(context_lock) = state.context.try_read() {
|
||||
context_lock.clone()
|
||||
} else {
|
||||
println!("Failed to acquire lock on context");
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(context) = context_to_cleanup {
|
||||
if let Err(err) = context.cleanup() {
|
||||
println!("Cleanup failed {}", err);
|
||||
let lock = state.context_lock.try_lock();
|
||||
if let Ok(_) = lock {
|
||||
if let Err(e) = state.context().cleanup() {
|
||||
println!("Failed to cleanup context: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
pub mod request;
|
||||
pub mod tauri_bindings;
|
||||
|
||||
use crate::cli::api::tauri_bindings::SeedChoice;
|
||||
use crate::cli::api::tauri_bindings::{ContextStatus, SeedChoice};
|
||||
use crate::cli::command::{Bitcoin, Monero};
|
||||
use crate::common::tor::{bootstrap_tor_client, create_tor_client};
|
||||
use crate::common::tracing_util::Format;
|
||||
|
|
@ -19,9 +19,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::{Arc, Once};
|
||||
use swap_env::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet};
|
||||
use swap_fs::system_data_dir;
|
||||
use tauri_bindings::{
|
||||
MoneroNodeConfig, TauriBackgroundProgress, TauriContextStatusEvent, TauriEmitter, TauriHandle,
|
||||
};
|
||||
use tauri_bindings::{MoneroNodeConfig, TauriBackgroundProgress, TauriEmitter, TauriHandle};
|
||||
use tokio::sync::{broadcast, broadcast::Sender, Mutex as TokioMutex, RwLock};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
|
@ -34,18 +32,54 @@ use super::watcher::Watcher;
|
|||
|
||||
static START: Once = Once::new();
|
||||
|
||||
mod config {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct Config {
|
||||
namespace: XmrBtcNamespace,
|
||||
pub(super) namespace: XmrBtcNamespace,
|
||||
pub env_config: EnvConfig,
|
||||
seed: Option<Seed>,
|
||||
debug: bool,
|
||||
json: bool,
|
||||
log_dir: PathBuf,
|
||||
data_dir: PathBuf,
|
||||
is_testnet: bool,
|
||||
pub(super) seed: Option<Seed>,
|
||||
pub(super) debug: bool,
|
||||
pub(super) json: bool,
|
||||
pub(super) log_dir: PathBuf,
|
||||
pub(super) data_dir: PathBuf,
|
||||
pub(super) is_testnet: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn for_harness(seed: Seed, env_config: EnvConfig) -> Self {
|
||||
let data_dir =
|
||||
super::data::data_dir_from(None, false).expect("Could not find data directory");
|
||||
let log_dir = data_dir.join("logs"); // not used in production
|
||||
|
||||
Self {
|
||||
namespace: XmrBtcNamespace::from_is_testnet(false),
|
||||
env_config,
|
||||
seed: seed.into(),
|
||||
debug: false,
|
||||
json: false,
|
||||
is_testnet: false,
|
||||
data_dir,
|
||||
log_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn env_config_from(testnet: bool) -> EnvConfig {
|
||||
if testnet {
|
||||
Testnet::get_config()
|
||||
} else {
|
||||
Mainnet::get_config()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use config::Config;
|
||||
|
||||
mod swap_lock {
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PendingTaskList(TokioMutex<Vec<JoinHandle<()>>>);
|
||||
|
||||
|
|
@ -174,27 +208,208 @@ impl Default for SwapLock {
|
|||
Self::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use swap_lock::{PendingTaskList, SwapLock};
|
||||
|
||||
mod context {
|
||||
use super::*;
|
||||
|
||||
/// Holds shared data for different parts of the CLI.
|
||||
///
|
||||
/// Some components are optional, allowing initialization of only necessary parts.
|
||||
/// For example, the `history` command doesn't require wallet initialization.
|
||||
///
|
||||
/// Many fields are wrapped in `Arc` for thread-safe sharing.
|
||||
/// Components are wrapped in Arc<RwLock> to allow independent initialization and cloning.
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pub db: Arc<dyn Database + Send + Sync>,
|
||||
pub db: Arc<RwLock<Option<Arc<dyn Database + Send + Sync>>>>,
|
||||
pub swap_lock: Arc<SwapLock>,
|
||||
pub config: Config,
|
||||
pub config: Arc<RwLock<Option<Config>>>,
|
||||
pub tasks: Arc<PendingTaskList>,
|
||||
tauri_handle: Option<TauriHandle>,
|
||||
bitcoin_wallet: Option<Arc<bitcoin::Wallet>>,
|
||||
pub monero_manager: Option<Arc<monero::Wallets>>,
|
||||
tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
|
||||
pub tauri_handle: Option<TauriHandle>,
|
||||
pub(super) bitcoin_wallet: Arc<RwLock<Option<Arc<bitcoin::Wallet>>>>,
|
||||
pub monero_manager: Arc<RwLock<Option<Arc<monero::Wallets>>>>,
|
||||
pub(super) tor_client: Arc<RwLock<Option<Arc<TorClient<TokioRustlsRuntime>>>>>,
|
||||
#[allow(dead_code)]
|
||||
monero_rpc_pool_handle: Option<Arc<monero_rpc_pool::PoolHandle>>,
|
||||
pub(super) monero_rpc_pool_handle: Arc<RwLock<Option<Arc<monero_rpc_pool::PoolHandle>>>>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new_with_tauri_handle(tauri_handle: TauriHandle) -> Self {
|
||||
Self::new(Some(tauri_handle))
|
||||
}
|
||||
|
||||
pub fn new_without_tauri_handle() -> Self {
|
||||
Self::new(None)
|
||||
}
|
||||
|
||||
/// Creates an empty Context with only the swap_lock and tasks initialized
|
||||
fn new(tauri_handle: Option<TauriHandle>) -> Self {
|
||||
Self {
|
||||
db: Arc::new(RwLock::new(None)),
|
||||
swap_lock: Arc::new(SwapLock::new()),
|
||||
config: Arc::new(RwLock::new(None)),
|
||||
tasks: Arc::new(PendingTaskList::default()),
|
||||
tauri_handle,
|
||||
bitcoin_wallet: Arc::new(RwLock::new(None)),
|
||||
monero_manager: Arc::new(RwLock::new(None)),
|
||||
tor_client: Arc::new(RwLock::new(None)),
|
||||
monero_rpc_pool_handle: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn status(&self) -> ContextStatus {
|
||||
ContextStatus {
|
||||
bitcoin_wallet_available: self.try_get_bitcoin_wallet().await.is_ok(),
|
||||
monero_wallet_available: self.try_get_monero_manager().await.is_ok(),
|
||||
database_available: self.try_get_db().await.is_ok(),
|
||||
tor_available: self.try_get_tor_client().await.is_ok(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the Bitcoin wallet, returning an error if not initialized
|
||||
pub async fn try_get_bitcoin_wallet(&self) -> Result<Arc<bitcoin::Wallet>> {
|
||||
self.bitcoin_wallet
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Bitcoin wallet not initialized")
|
||||
}
|
||||
|
||||
/// Get the Monero manager, returning an error if not initialized
|
||||
pub async fn try_get_monero_manager(&self) -> Result<Arc<monero::Wallets>> {
|
||||
self.monero_manager
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Monero wallet manager not initialized")
|
||||
}
|
||||
|
||||
/// Get the database, returning an error if not initialized
|
||||
pub async fn try_get_db(&self) -> Result<Arc<dyn Database + Send + Sync>> {
|
||||
self.db
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Database not initialized")
|
||||
}
|
||||
|
||||
/// Get the config, returning an error if not initialized
|
||||
pub async fn try_get_config(&self) -> Result<Config> {
|
||||
self.config
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Config not initialized")
|
||||
}
|
||||
|
||||
/// Get the Tor client, returning an error if not initialized
|
||||
pub async fn try_get_tor_client(&self) -> Result<Arc<TorClient<TokioRustlsRuntime>>> {
|
||||
self.tor_client
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Tor client not initialized")
|
||||
}
|
||||
|
||||
pub async fn for_harness(
|
||||
seed: Seed,
|
||||
env_config: EnvConfig,
|
||||
db_path: PathBuf,
|
||||
bob_bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bob_monero_wallet: Arc<monero::Wallets>,
|
||||
) -> Self {
|
||||
let config = Config::for_harness(seed, env_config);
|
||||
let db = open_db(db_path, AccessMode::ReadWrite, None)
|
||||
.await
|
||||
.expect("Could not open sqlite database");
|
||||
|
||||
Self {
|
||||
bitcoin_wallet: Arc::new(RwLock::new(Some(bob_bitcoin_wallet))),
|
||||
monero_manager: Arc::new(RwLock::new(Some(bob_monero_wallet))),
|
||||
config: Arc::new(RwLock::new(Some(config))),
|
||||
db: Arc::new(RwLock::new(Some(db))),
|
||||
swap_lock: SwapLock::new().into(),
|
||||
tasks: PendingTaskList::default().into(),
|
||||
tauri_handle: None,
|
||||
tor_client: Arc::new(RwLock::new(None)),
|
||||
monero_rpc_pool_handle: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup(&self) -> Result<()> {
|
||||
// TODO: close all monero wallets
|
||||
// call store(..) on all wallets
|
||||
|
||||
// TODO: This doesn't work because "there is no reactor running, must be called from the context of a Tokio 1.x runtime"
|
||||
// let monero_manager = self.monero_manager.clone();
|
||||
// tokio::spawn(async move {
|
||||
// if let Some(monero_manager) = monero_manager {
|
||||
// let wallet = monero_manager.main_wallet().await;
|
||||
// wallet.store(None).await;
|
||||
// }
|
||||
// });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn bitcoin_wallet(&self) -> Option<Arc<bitcoin::Wallet>> {
|
||||
self.bitcoin_wallet.read().await.clone()
|
||||
}
|
||||
|
||||
/// Change the Monero node configuration for all wallets
|
||||
pub async fn change_monero_node(&self, node_config: MoneroNodeConfig) -> Result<()> {
|
||||
let monero_manager = self.try_get_monero_manager().await?;
|
||||
|
||||
// Determine the daemon configuration based on the node config
|
||||
let daemon = match node_config {
|
||||
MoneroNodeConfig::Pool => {
|
||||
// Use the pool handle to get server info
|
||||
let pool_handle = self
|
||||
.monero_rpc_pool_handle
|
||||
.read()
|
||||
.await
|
||||
.clone()
|
||||
.context("Pool handle not available")?;
|
||||
|
||||
let server_info = pool_handle.server_info();
|
||||
let pool_url: String = server_info.clone().into();
|
||||
tracing::info!("Switching to Monero RPC pool: {}", pool_url);
|
||||
|
||||
monero_sys::Daemon::try_from(pool_url)?
|
||||
}
|
||||
MoneroNodeConfig::SingleNode { url } => {
|
||||
tracing::info!("Switching to single Monero node: {}", url);
|
||||
|
||||
monero_sys::Daemon::try_from(url.clone())?
|
||||
}
|
||||
};
|
||||
|
||||
// Update the wallet manager's daemon configuration
|
||||
monero_manager
|
||||
.change_monero_node(daemon.clone())
|
||||
.await
|
||||
.context("Failed to change Monero node in wallet manager")?;
|
||||
|
||||
tracing::info!(?daemon, "Switched Monero node");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Context {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use context::Context;
|
||||
|
||||
mod builder {
|
||||
use super::*;
|
||||
|
||||
/// A conveniant builder struct for [`Context`].
|
||||
#[must_use = "ContextBuilder must be built to be useful"]
|
||||
pub struct ContextBuilder {
|
||||
|
|
@ -289,14 +504,14 @@ impl ContextBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Takes the builder, initializes the context by initializing the wallets and other components and returns the Context.
|
||||
pub async fn build(self) -> Result<Context> {
|
||||
// This is the data directory for the eigenwallet (wallet files)
|
||||
/// Initializes the context by populating it with all configured components.
|
||||
///
|
||||
/// Context fields are set as early as possible for availability to other parts of the system.
|
||||
pub async fn build(self, context: Arc<Context>) -> Result<()> {
|
||||
let eigenwallet_data_dir = &eigenwallet_data::new(self.is_testnet)?;
|
||||
|
||||
let base_data_dir = &data::data_dir_from(self.data, self.is_testnet)?;
|
||||
let log_dir = base_data_dir.join("logs");
|
||||
let env_config = env_config_from(self.is_testnet);
|
||||
let env_config = config::env_config_from(self.is_testnet);
|
||||
|
||||
// Initialize logging
|
||||
let format = if self.json { Format::Json } else { Format::Raw };
|
||||
|
|
@ -323,19 +538,18 @@ impl ContextBuilder {
|
|||
);
|
||||
});
|
||||
|
||||
// Prepare parallel initialization tasks
|
||||
let future_seed_choice_and_database = {
|
||||
let tauri_handle = self.tauri_handle.clone();
|
||||
|
||||
async move {
|
||||
// Initialize wallet database for tracking recent wallets
|
||||
let wallet_database = monero_sys::Database::new(eigenwallet_data_dir.clone())
|
||||
.await
|
||||
.context("Failed to initialize wallet database")?;
|
||||
|
||||
// Request the user to select a wallet to use
|
||||
let seed_choice = match tauri_handle {
|
||||
Some(tauri_handle) => {
|
||||
Some(request_seed_choice(tauri_handle, &wallet_database).await?)
|
||||
Some(wallet::request_seed_choice(tauri_handle, &wallet_database).await?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
|
@ -344,7 +558,6 @@ impl ContextBuilder {
|
|||
}
|
||||
};
|
||||
|
||||
// Create unbootstrapped Tor client early if enabled
|
||||
let future_unbootstrapped_tor_client_rpc_pool = {
|
||||
let tauri_handle = self.tauri_handle.clone();
|
||||
async move {
|
||||
|
|
@ -360,7 +573,7 @@ impl ContextBuilder {
|
|||
None
|
||||
};
|
||||
|
||||
// Start the rpc pool for the monero wallet with optional Tor client based on enable_monero_tor setting
|
||||
// Start Monero RPC pool server
|
||||
let (server_info, status_receiver, pool_handle) =
|
||||
monero_rpc_pool::start_server_with_random_port(
|
||||
monero_rpc_pool::config::Config::new_random_port_with_tor_client(
|
||||
|
|
@ -378,6 +591,7 @@ impl ContextBuilder {
|
|||
)
|
||||
.await?;
|
||||
|
||||
// Bootstrap Tor client in background
|
||||
let bootstrap_tor_client_task = AbortOnDropHandle::new(tokio::spawn({
|
||||
let unbootstrapped_tor_client = unbootstrapped_tor_client.clone();
|
||||
let tauri_handle = tauri_handle.clone();
|
||||
|
|
@ -418,7 +632,9 @@ impl ContextBuilder {
|
|||
future_unbootstrapped_tor_client_rpc_pool
|
||||
)?;
|
||||
|
||||
// Listen for pool status updates and forward them to frontend
|
||||
*context.tor_client.write().await = unbootstrapped_tor_client.clone();
|
||||
|
||||
// Forward pool status updates to frontend
|
||||
let pool_tauri_handle = self.tauri_handle.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Ok(status) = status_receiver.recv().await {
|
||||
|
|
@ -426,7 +642,7 @@ impl ContextBuilder {
|
|||
}
|
||||
});
|
||||
|
||||
// Determine the monero node address to use
|
||||
// Determine Monero daemon to use
|
||||
let (monero_node_address, monero_rpc_pool_handle) = match &self.monero_config {
|
||||
Some(MoneroNodeConfig::Pool) => {
|
||||
let rpc_url = server_info.into();
|
||||
|
|
@ -434,17 +650,17 @@ impl ContextBuilder {
|
|||
}
|
||||
Some(MoneroNodeConfig::SingleNode { url }) => (url.clone(), None),
|
||||
None => {
|
||||
// Default to pool if no monero config is provided
|
||||
let rpc_url = server_info.into();
|
||||
(rpc_url, Some(Arc::new(pool_handle)))
|
||||
}
|
||||
};
|
||||
|
||||
// Create a daemon struct for the monero wallet based on the node address
|
||||
*context.monero_rpc_pool_handle.write().await = monero_rpc_pool_handle.clone();
|
||||
|
||||
let daemon = monero_sys::Daemon::try_from(monero_node_address)?;
|
||||
|
||||
// Prompt the user to open/create a Monero wallet
|
||||
let (wallet, seed) = open_monero_wallet(
|
||||
// Open or create Monero wallet
|
||||
let (wallet, seed) = wallet::open_monero_wallet(
|
||||
self.tauri_handle.clone(),
|
||||
eigenwallet_data_dir,
|
||||
base_data_dir,
|
||||
|
|
@ -457,12 +673,11 @@ impl ContextBuilder {
|
|||
|
||||
let primary_address = wallet.main_address().await;
|
||||
|
||||
// Derive data directory from primary address
|
||||
// Derive wallet-specific data directory
|
||||
let data_dir = base_data_dir
|
||||
.join("identities")
|
||||
.join(primary_address.to_string());
|
||||
|
||||
// Ensure the identity directory exists
|
||||
swap_fs::ensure_directory_exists(&data_dir)
|
||||
.context("Failed to create identity directory")?;
|
||||
|
||||
|
|
@ -474,8 +689,9 @@ impl ContextBuilder {
|
|||
|
||||
let wallet_database = Some(Arc::new(wallet_database));
|
||||
|
||||
// Create the monero wallet manager
|
||||
let monero_manager = Some(Arc::new(
|
||||
// Initialize Monero wallet manager
|
||||
async {
|
||||
let manager = Arc::new(
|
||||
monero::Wallets::new_with_existing_wallet(
|
||||
eigenwallet_data_dir.to_path_buf(),
|
||||
daemon.clone(),
|
||||
|
|
@ -487,13 +703,28 @@ impl ContextBuilder {
|
|||
)
|
||||
.await
|
||||
.context("Failed to initialize Monero wallets with existing wallet")?,
|
||||
));
|
||||
);
|
||||
|
||||
// Create the data structure we use to manage the swap lock
|
||||
let swap_lock = Arc::new(SwapLock::new());
|
||||
let tasks = PendingTaskList::default().into();
|
||||
*context.monero_manager.write().await = Some(manager);
|
||||
|
||||
// Initialize the database
|
||||
Ok::<_, Error>(())
|
||||
}
|
||||
.await?;
|
||||
|
||||
// Initialize config
|
||||
*context.config.write().await = Some(Config {
|
||||
namespace: XmrBtcNamespace::from_is_testnet(self.is_testnet),
|
||||
env_config,
|
||||
seed: seed.clone().into(),
|
||||
debug: self.debug,
|
||||
json: self.json,
|
||||
is_testnet: self.is_testnet,
|
||||
data_dir: data_dir.clone(),
|
||||
log_dir: log_dir.clone(),
|
||||
});
|
||||
|
||||
// Initialize swap database
|
||||
let db = async {
|
||||
let database_progress_handle = self
|
||||
.tauri_handle
|
||||
.new_background_process_with_initial_progress(
|
||||
|
|
@ -510,10 +741,17 @@ impl ContextBuilder {
|
|||
|
||||
database_progress_handle.finish();
|
||||
|
||||
*context.db.write().await = Some(db.clone());
|
||||
|
||||
Ok::<_, Error>(db)
|
||||
}
|
||||
.await?;
|
||||
|
||||
let tauri_handle = &self.tauri_handle.clone();
|
||||
|
||||
let initialize_bitcoin_wallet = async {
|
||||
match self.bitcoin {
|
||||
// Initialize Bitcoin wallet
|
||||
let bitcoin_wallet = async {
|
||||
let wallet = match self.bitcoin {
|
||||
Some(bitcoin) => {
|
||||
let (urls, target_block) = bitcoin.apply_defaults(self.is_testnet)?;
|
||||
|
||||
|
|
@ -523,7 +761,7 @@ impl ContextBuilder {
|
|||
(),
|
||||
);
|
||||
|
||||
let wallet = init_bitcoin_wallet(
|
||||
let wallet = wallet::init_bitcoin_wallet(
|
||||
urls,
|
||||
&seed,
|
||||
&data_dir,
|
||||
|
|
@ -535,15 +773,16 @@ impl ContextBuilder {
|
|||
|
||||
bitcoin_progress_handle.finish();
|
||||
|
||||
Ok::<std::option::Option<Arc<bitcoin::wallet::Wallet>>, Error>(Some(Arc::new(
|
||||
wallet,
|
||||
)))
|
||||
}
|
||||
None => Ok(None),
|
||||
Some(Arc::new(wallet))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let bitcoin_wallet = initialize_bitcoin_wallet.await?;
|
||||
*context.bitcoin_wallet.write().await = wallet.clone();
|
||||
|
||||
Ok::<_, Error>(wallet)
|
||||
}
|
||||
.await?;
|
||||
|
||||
// If we have a bitcoin wallet and a tauri handle, we start a background task
|
||||
if let Some(wallet) = bitcoin_wallet.clone() {
|
||||
|
|
@ -552,144 +791,26 @@ impl ContextBuilder {
|
|||
wallet,
|
||||
db.clone(),
|
||||
self.tauri_handle.clone(),
|
||||
swap_lock.clone(),
|
||||
context.swap_lock.clone(),
|
||||
);
|
||||
tokio::spawn(watcher.run());
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for Tor client to fully bootstrap
|
||||
bootstrap_tor_client_task.await?;
|
||||
|
||||
let context = Context {
|
||||
db,
|
||||
bitcoin_wallet,
|
||||
monero_manager,
|
||||
config: Config {
|
||||
namespace: XmrBtcNamespace::from_is_testnet(self.is_testnet),
|
||||
env_config,
|
||||
seed: seed.clone().into(),
|
||||
debug: self.debug,
|
||||
json: self.json,
|
||||
is_testnet: self.is_testnet,
|
||||
data_dir: data_dir.clone(),
|
||||
log_dir: log_dir.clone(),
|
||||
},
|
||||
swap_lock,
|
||||
tasks,
|
||||
tauri_handle: self.tauri_handle,
|
||||
tor_client: unbootstrapped_tor_client,
|
||||
monero_rpc_pool_handle,
|
||||
};
|
||||
|
||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn with_tauri_handle(mut self, tauri_handle: impl Into<Option<TauriHandle>>) -> Self {
|
||||
self.tauri_handle = tauri_handle.into();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn for_harness(
|
||||
seed: Seed,
|
||||
env_config: EnvConfig,
|
||||
db_path: PathBuf,
|
||||
bob_bitcoin_wallet: Arc<bitcoin::Wallet>,
|
||||
bob_monero_wallet: Arc<monero::Wallets>,
|
||||
) -> Self {
|
||||
let config = Config::for_harness(seed, env_config);
|
||||
|
||||
Self {
|
||||
bitcoin_wallet: Some(bob_bitcoin_wallet),
|
||||
monero_manager: Some(bob_monero_wallet),
|
||||
config,
|
||||
db: open_db(db_path, AccessMode::ReadWrite, None)
|
||||
.await
|
||||
.expect("Could not open sqlite database"),
|
||||
swap_lock: SwapLock::new().into(),
|
||||
tasks: PendingTaskList::default().into(),
|
||||
tauri_handle: None,
|
||||
tor_client: None,
|
||||
monero_rpc_pool_handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup(&self) -> Result<()> {
|
||||
// TODO: close all monero wallets
|
||||
// call store(..) on all wallets
|
||||
|
||||
// TODO: This doesn't work because "there is no reactor running, must be called from the context of a Tokio 1.x runtime"
|
||||
// let monero_manager = self.monero_manager.clone();
|
||||
// tokio::spawn(async move {
|
||||
// if let Some(monero_manager) = monero_manager {
|
||||
// let wallet = monero_manager.main_wallet().await;
|
||||
// wallet.store(None).await;
|
||||
// }
|
||||
// });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn bitcoin_wallet(&self) -> Option<Arc<bitcoin::Wallet>> {
|
||||
self.bitcoin_wallet.clone()
|
||||
}
|
||||
|
||||
pub fn tauri_handle(&self) -> Option<TauriHandle> {
|
||||
self.tauri_handle.clone()
|
||||
}
|
||||
|
||||
/// Change the Monero node configuration for all wallets
|
||||
pub async fn change_monero_node(&self, node_config: MoneroNodeConfig) -> Result<()> {
|
||||
let monero_manager = self
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
|
||||
// Determine the daemon configuration based on the node config
|
||||
let daemon = match node_config {
|
||||
MoneroNodeConfig::Pool => {
|
||||
// Use the pool handle to get server info
|
||||
let pool_handle = self
|
||||
.monero_rpc_pool_handle
|
||||
.as_ref()
|
||||
.context("Pool handle not available")?;
|
||||
|
||||
let server_info = pool_handle.server_info();
|
||||
let pool_url: String = server_info.clone().into();
|
||||
tracing::info!("Switching to Monero RPC pool: {}", pool_url);
|
||||
|
||||
monero_sys::Daemon::try_from(pool_url)?
|
||||
}
|
||||
MoneroNodeConfig::SingleNode { url } => {
|
||||
tracing::info!("Switching to single Monero node: {}", url);
|
||||
|
||||
monero_sys::Daemon::try_from(url.clone())?
|
||||
}
|
||||
};
|
||||
|
||||
// Update the wallet manager's daemon configuration
|
||||
monero_manager
|
||||
.change_monero_node(daemon.clone())
|
||||
.await
|
||||
.context("Failed to change Monero node in wallet manager")?;
|
||||
|
||||
tracing::info!(?daemon, "Switched Monero node");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Context {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "")
|
||||
}
|
||||
}
|
||||
|
||||
async fn init_bitcoin_wallet(
|
||||
pub use builder::ContextBuilder;
|
||||
|
||||
mod wallet {
|
||||
use super::*;
|
||||
|
||||
pub(super) async fn init_bitcoin_wallet(
|
||||
electrum_rpc_urls: Vec<String>,
|
||||
seed: &Seed,
|
||||
data_dir: &Path,
|
||||
|
|
@ -720,7 +841,7 @@ async fn init_bitcoin_wallet(
|
|||
Ok(wallet)
|
||||
}
|
||||
|
||||
async fn request_and_open_monero_wallet_legacy(
|
||||
pub(super) async fn request_and_open_monero_wallet_legacy(
|
||||
data_dir: &PathBuf,
|
||||
env_config: EnvConfig,
|
||||
daemon: &monero_sys::Daemon,
|
||||
|
|
@ -740,12 +861,13 @@ async fn request_and_open_monero_wallet_legacy(
|
|||
}
|
||||
|
||||
/// Requests the user to select a seed choice from a list of recent wallets
|
||||
async fn request_seed_choice(
|
||||
pub(super) async fn request_seed_choice(
|
||||
tauri_handle: TauriHandle,
|
||||
database: &monero_sys::Database,
|
||||
) -> Result<SeedChoice> {
|
||||
let recent_wallets = database.get_recent_wallets(5).await?;
|
||||
let recent_wallets: Vec<String> = recent_wallets.into_iter().map(|w| w.wallet_path).collect();
|
||||
let recent_wallets: Vec<String> =
|
||||
recent_wallets.into_iter().map(|w| w.wallet_path).collect();
|
||||
|
||||
let seed_choice = tauri_handle
|
||||
.request_seed_selection_with_recent_wallets(recent_wallets)
|
||||
|
|
@ -763,7 +885,7 @@ async fn request_seed_choice(
|
|||
///
|
||||
/// Errors if the user aborts, provides an incorrect password, or the wallet
|
||||
/// fails to open/create.
|
||||
async fn open_monero_wallet(
|
||||
pub(super) async fn open_monero_wallet(
|
||||
tauri_handle: Option<TauriHandle>,
|
||||
eigenwallet_data_dir: &PathBuf,
|
||||
legacy_data_dir: &PathBuf,
|
||||
|
|
@ -793,7 +915,8 @@ async fn open_monero_wallet(
|
|||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let wallet_path = eigenwallet_wallets_dir.join(format!("wallet_{}", timestamp));
|
||||
let wallet_path =
|
||||
eigenwallet_wallets_dir.join(format!("wallet_{}", timestamp));
|
||||
|
||||
if let Some(parent) = wallet_path.parent() {
|
||||
swap_fs::ensure_directory_exists(parent)
|
||||
|
|
@ -843,7 +966,9 @@ async fn open_monero_wallet(
|
|||
wallet_path.clone(),
|
||||
password,
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to verify wallet password: {}", e))
|
||||
.map_err(|e| {
|
||||
anyhow::anyhow!("Failed to verify wallet password: {}", e)
|
||||
})
|
||||
};
|
||||
|
||||
// Request and verify password before opening wallet
|
||||
|
|
@ -897,8 +1022,10 @@ async fn open_monero_wallet(
|
|||
// None means the user rejected the password request
|
||||
// We prompt him to select a wallet again
|
||||
None => {
|
||||
seed_choice =
|
||||
request_seed_choice(tauri_handle.clone().unwrap(), database)
|
||||
seed_choice = request_seed_choice(
|
||||
tauri_handle.clone().unwrap(),
|
||||
database,
|
||||
)
|
||||
.await?;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -948,7 +1075,8 @@ async fn open_monero_wallet(
|
|||
// This is used for the CLI to monitor the blockchain
|
||||
None => {
|
||||
let wallet =
|
||||
request_and_open_monero_wallet_legacy(legacy_data_dir, env_config, daemon).await?;
|
||||
request_and_open_monero_wallet_legacy(legacy_data_dir, env_config, daemon)
|
||||
.await?;
|
||||
let seed = Seed::from_file_or_generate(legacy_data_dir)
|
||||
.await
|
||||
.context("Failed to extract seed from wallet")?;
|
||||
|
|
@ -959,6 +1087,7 @@ async fn open_monero_wallet(
|
|||
|
||||
Ok(wallet)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod data {
|
||||
use super::*;
|
||||
|
|
@ -989,32 +1118,6 @@ pub mod eigenwallet_data {
|
|||
}
|
||||
}
|
||||
|
||||
fn env_config_from(testnet: bool) -> EnvConfig {
|
||||
if testnet {
|
||||
Testnet::get_config()
|
||||
} else {
|
||||
Mainnet::get_config()
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn for_harness(seed: Seed, env_config: EnvConfig) -> Self {
|
||||
let data_dir = data::data_dir_from(None, false).expect("Could not find data directory");
|
||||
let log_dir = data_dir.join("logs"); // not used in production
|
||||
|
||||
Self {
|
||||
namespace: XmrBtcNamespace::from_is_testnet(false),
|
||||
env_config,
|
||||
seed: seed.into(),
|
||||
debug: false,
|
||||
json: false,
|
||||
is_testnet: false,
|
||||
data_dir,
|
||||
log_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Monero> for MoneroNodeConfig {
|
||||
fn from(monero: Monero) -> Self {
|
||||
match monero.monero_node_address {
|
||||
|
|
@ -1056,7 +1159,7 @@ pub mod api_test {
|
|||
let seed = Seed::from_file_or_generate(data_dir.as_path())
|
||||
.await
|
||||
.unwrap();
|
||||
let env_config = env_config_from(is_testnet);
|
||||
let env_config = config::env_config_from(is_testnet);
|
||||
|
||||
Self {
|
||||
namespace: XmrBtcNamespace::from_is_testnet(is_testnet),
|
||||
|
|
|
|||
|
|
@ -421,7 +421,8 @@ impl Request for GetLogsArgs {
|
|||
type Response = GetLogsResponse;
|
||||
|
||||
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?;
|
||||
|
||||
for msg in &logs {
|
||||
|
|
@ -470,11 +471,8 @@ impl Request for GetRestoreHeightArgs {
|
|||
type Response = GetRestoreHeightResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
let wallet = ctx
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
let wallet = wallet.main_wallet().await;
|
||||
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||
let wallet = wallet_manager.main_wallet().await;
|
||||
let height = wallet.get_restore_height().await?;
|
||||
|
||||
Ok(GetRestoreHeightResponse { height })
|
||||
|
|
@ -496,7 +494,8 @@ impl Request for GetMoneroAddressesArgs {
|
|||
type Response = GetMoneroAddressesResponse;
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -515,11 +514,8 @@ impl Request for GetMoneroHistoryArgs {
|
|||
type Response = GetMoneroHistoryResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
let wallet = ctx
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
let wallet = wallet.main_wallet().await;
|
||||
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||
let wallet = wallet_manager.main_wallet().await;
|
||||
|
||||
let transactions = wallet.history().await;
|
||||
Ok(GetMoneroHistoryResponse { transactions })
|
||||
|
|
@ -541,11 +537,8 @@ impl Request for GetMoneroMainAddressArgs {
|
|||
type Response = GetMoneroMainAddressResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
let wallet = ctx
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
let wallet = wallet.main_wallet().await;
|
||||
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||
let wallet = wallet_manager.main_wallet().await;
|
||||
let address = wallet.main_address().await;
|
||||
Ok(GetMoneroMainAddressResponse { address })
|
||||
}
|
||||
|
|
@ -582,11 +575,8 @@ impl Request for SetRestoreHeightArgs {
|
|||
type Response = SetRestoreHeightResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
let wallet = ctx
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
let wallet = wallet.main_wallet().await;
|
||||
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||
let wallet = wallet_manager.main_wallet().await;
|
||||
|
||||
let height = match self {
|
||||
SetRestoreHeightArgs::Height(height) => height as u64,
|
||||
|
|
@ -661,10 +651,7 @@ impl Request for GetMoneroBalanceArgs {
|
|||
type Response = GetMoneroBalanceResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
let wallet_manager = ctx
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||
let wallet = wallet_manager.main_wallet().await;
|
||||
|
||||
let total_balance = wallet.total_balance().await;
|
||||
|
|
@ -706,10 +693,7 @@ impl Request for SendMoneroArgs {
|
|||
type Response = SendMoneroResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
let wallet_manager = ctx
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||
let wallet = wallet_manager.main_wallet().await;
|
||||
|
||||
// Parse the address
|
||||
|
|
@ -717,7 +701,8 @@ impl Request for SendMoneroArgs {
|
|||
.map_err(|e| anyhow::anyhow!("Invalid Monero address: {}", e))?;
|
||||
|
||||
let tauri_handle = ctx
|
||||
.tauri_handle()
|
||||
.tauri_handle
|
||||
.clone()
|
||||
.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
|
||||
|
|
@ -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))]
|
||||
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();
|
||||
|
||||
for (swap_id, _) in swap_ids {
|
||||
|
|
@ -813,27 +799,23 @@ pub async fn get_swap_info(
|
|||
args: GetSwapInfoArgs,
|
||||
context: Arc<Context>,
|
||||
) -> Result<GetSwapInfoResponse> {
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||
let db = context.try_get_db().await?;
|
||||
|
||||
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 peer_id = context
|
||||
.db
|
||||
let peer_id = db
|
||||
.get_peer_id(args.swap_id)
|
||||
.await
|
||||
.with_context(|| "Could not get PeerID")?;
|
||||
|
||||
let addresses = context
|
||||
.db
|
||||
let addresses = db
|
||||
.get_addresses(peer_id)
|
||||
.await
|
||||
.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()?;
|
||||
|
||||
|
|
@ -847,8 +829,7 @@ pub async fn get_swap_info(
|
|||
btc_refund_address,
|
||||
cancel_timelock,
|
||||
punish_timelock,
|
||||
) = context
|
||||
.db
|
||||
) = db
|
||||
.get_states(args.swap_id)
|
||||
.await?
|
||||
.iter()
|
||||
|
|
@ -884,7 +865,7 @@ pub async fn get_swap_info(
|
|||
|
||||
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 {
|
||||
swap_id: args.swap_id,
|
||||
|
|
@ -924,15 +905,13 @@ pub async fn buy_xmr(
|
|||
monero_receive_pool,
|
||||
} = 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()?;
|
||||
|
||||
let bitcoin_wallet = Arc::clone(
|
||||
context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.expect("Could not find Bitcoin wallet"),
|
||||
);
|
||||
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||
|
||||
let bitcoin_change_address = match bitcoin_change_address {
|
||||
Some(addr) => addr
|
||||
|
|
@ -950,21 +929,15 @@ pub async fn buy_xmr(
|
|||
}
|
||||
};
|
||||
|
||||
let monero_wallet = Arc::clone(
|
||||
context
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Could not get Monero wallet")?,
|
||||
);
|
||||
let monero_wallet = context.try_get_monero_manager().await?;
|
||||
|
||||
let env_config = context.config.env_config;
|
||||
let seed = context.config.seed.clone().context("Could not get seed")?;
|
||||
let env_config = config.env_config;
|
||||
let seed = config.seed.clone().context("Could not get seed")?;
|
||||
|
||||
// Prepare variables for the quote fetching process
|
||||
let identity = seed.derive_libp2p_identity();
|
||||
let namespace = context.config.namespace;
|
||||
let tor_client = context.tor_client.clone();
|
||||
let db = Some(context.db.clone());
|
||||
let namespace = config.namespace;
|
||||
let tor_client = context.tor_client.read().await.clone();
|
||||
let tauri_handle = context.tauri_handle.clone();
|
||||
|
||||
// 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);
|
||||
|
||||
// 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 rendezvous_points_clone = rendezvous_points.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
|
||||
// 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 namespace = namespace;
|
||||
let identity = identity.clone();
|
||||
let db = db.clone();
|
||||
let db = db_for_fetch.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 {
|
||||
fetch_quotes_task(
|
||||
|
|
@ -999,7 +980,7 @@ pub async fn buy_xmr(
|
|||
namespace,
|
||||
sellers,
|
||||
identity,
|
||||
db,
|
||||
Some(db),
|
||||
tor_client,
|
||||
tauri_handle,
|
||||
).await
|
||||
|
|
@ -1027,10 +1008,10 @@ pub async fn buy_xmr(
|
|||
async move { w.sync().await }
|
||||
}
|
||||
},
|
||||
context.tauri_handle.clone(),
|
||||
tauri_handle_for_determine,
|
||||
swap_id,
|
||||
|quote_with_address| {
|
||||
let tauri_handle = context.tauri_handle.clone();
|
||||
let tauri_handle_clone = tauri_handle_for_selection.clone();
|
||||
Box::new(async move {
|
||||
let details = SelectMakerDetails {
|
||||
swap_id,
|
||||
|
|
@ -1038,7 +1019,7 @@ pub async fn buy_xmr(
|
|||
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>
|
||||
},
|
||||
) => {
|
||||
|
|
@ -1046,55 +1027,51 @@ pub async fn buy_xmr(
|
|||
}
|
||||
_ = 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.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");
|
||||
},
|
||||
};
|
||||
|
||||
// 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?;
|
||||
|
||||
let behaviour = cli::Behaviour::new(
|
||||
seller_peer_id,
|
||||
env_config,
|
||||
bitcoin_wallet.clone(),
|
||||
(seed.derive_libp2p_identity(), context.config.namespace),
|
||||
(seed.derive_libp2p_identity(), namespace),
|
||||
);
|
||||
|
||||
let mut swarm = swarm::cli(
|
||||
seed.derive_libp2p_identity(),
|
||||
context.tor_client.clone(),
|
||||
tor_client_for_swarm,
|
||||
behaviour,
|
||||
)
|
||||
.await?;
|
||||
|
||||
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?;
|
||||
|
||||
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,
|
||||
TauriSwapProgressEvent::ReceivedQuote(quote.clone()),
|
||||
);
|
||||
|
||||
// Now create the event loop we use for the swap
|
||||
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());
|
||||
|
||||
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 {
|
||||
tokio::select! {
|
||||
|
|
@ -1103,7 +1080,7 @@ pub async fn buy_xmr(
|
|||
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.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||
tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||
|
||||
bail!("Shutdown signal received");
|
||||
},
|
||||
|
|
@ -1120,9 +1097,9 @@ pub async fn buy_xmr(
|
|||
},
|
||||
swap_result = async {
|
||||
let swap = Swap::new(
|
||||
Arc::clone(&context.db),
|
||||
db.clone(),
|
||||
swap_id,
|
||||
Arc::clone(&bitcoin_wallet),
|
||||
bitcoin_wallet.clone(),
|
||||
monero_wallet,
|
||||
env_config,
|
||||
event_loop_handle,
|
||||
|
|
@ -1130,7 +1107,7 @@ pub async fn buy_xmr(
|
|||
bitcoin_change_address_for_spawn,
|
||||
tx_lock_amount,
|
||||
tx_lock_fee
|
||||
).with_event_emitter(context.tauri_handle.clone());
|
||||
).with_event_emitter(tauri_handle.clone());
|
||||
|
||||
bob::run(swap).await
|
||||
} => {
|
||||
|
|
@ -1152,7 +1129,7 @@ pub async fn buy_xmr(
|
|||
.await
|
||||
.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>(())
|
||||
}.in_current_span()).await;
|
||||
|
|
@ -1167,11 +1144,16 @@ pub async fn resume_swap(
|
|||
) -> Result<ResumeSwapResponse> {
|
||||
let ResumeSwapArgs { swap_id } = resume;
|
||||
|
||||
let seller_peer_id = context.db.get_peer_id(swap_id).await?;
|
||||
let seller_addresses = context.db.get_addresses(seller_peer_id).await?;
|
||||
let db = context.try_get_db().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
|
||||
.config
|
||||
let seller_peer_id = db.get_peer_id(swap_id).await?;
|
||||
let seller_addresses = db.get_addresses(seller_peer_id).await?;
|
||||
|
||||
let seed = config
|
||||
.seed
|
||||
.as_ref()
|
||||
.context("Could not get seed")?
|
||||
|
|
@ -1179,16 +1161,11 @@ pub async fn resume_swap(
|
|||
|
||||
let behaviour = cli::Behaviour::new(
|
||||
seller_peer_id,
|
||||
context.config.env_config,
|
||||
Arc::clone(
|
||||
context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?,
|
||||
),
|
||||
(seed.clone(), context.config.namespace),
|
||||
config.env_config,
|
||||
bitcoin_wallet.clone(),
|
||||
(seed.clone(), 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();
|
||||
|
||||
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) =
|
||||
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(
|
||||
Arc::clone(&context.db),
|
||||
db.clone(),
|
||||
swap_id,
|
||||
Arc::clone(
|
||||
context
|
||||
.bitcoin_wallet
|
||||
.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,
|
||||
bitcoin_wallet,
|
||||
monero_manager,
|
||||
config.env_config,
|
||||
event_loop_handle,
|
||||
monero_receive_pool,
|
||||
)
|
||||
.await?
|
||||
.with_event_emitter(context.tauri_handle.clone());
|
||||
.with_event_emitter(tauri_handle.clone());
|
||||
|
||||
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(
|
||||
async move {
|
||||
|
|
@ -1239,7 +1207,7 @@ pub async fn resume_swap(
|
|||
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.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||
tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||
|
||||
bail!("Shutdown signal received");
|
||||
},
|
||||
|
|
@ -1272,7 +1240,7 @@ pub async fn resume_swap(
|
|||
.await
|
||||
.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>(())
|
||||
}
|
||||
|
|
@ -1290,15 +1258,12 @@ pub async fn cancel_and_refund(
|
|||
context: Arc<Context>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let CancelAndRefundArgs { swap_id } = cancel_and_refund;
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||
let db = context.try_get_db().await?;
|
||||
|
||||
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
||||
|
||||
let state =
|
||||
cli::cancel_and_refund(swap_id, Arc::clone(bitcoin_wallet), Arc::clone(&context.db)).await;
|
||||
let state = cli::cancel_and_refund(swap_id, bitcoin_wallet, db).await;
|
||||
|
||||
context
|
||||
.swap_lock
|
||||
|
|
@ -1319,7 +1284,8 @@ pub async fn cancel_and_refund(
|
|||
|
||||
#[tracing::instrument(fields(method = "get_history"), skip(context))]
|
||||
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();
|
||||
for (swap_id, state) in swaps {
|
||||
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))]
|
||||
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=%format!("{}/logs", data_dir_display), "Log files directory");
|
||||
tracing::info!(path=%format!("{}/sqlite", data_dir_display), "Sqlite file location");
|
||||
|
|
@ -1357,10 +1324,7 @@ pub async fn withdraw_btc(
|
|||
context: Arc<Context>,
|
||||
) -> Result<WithdrawBtcResponse> {
|
||||
let WithdrawBtcArgs { address, amount } = withdraw_btc;
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||
|
||||
let (withdraw_tx_unsigned, amount) = match amount {
|
||||
Some(amount) => {
|
||||
|
|
@ -1402,10 +1366,7 @@ pub async fn withdraw_btc(
|
|||
#[tracing::instrument(fields(method = "get_balance"), skip(context))]
|
||||
pub async fn get_balance(balance: BalanceArgs, context: Arc<Context>) -> Result<BalanceResponse> {
|
||||
let BalanceArgs { force_refresh } = balance;
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||
|
||||
if force_refresh {
|
||||
bitcoin_wallet.sync().await?;
|
||||
|
|
@ -1441,8 +1402,12 @@ pub async fn list_sellers(
|
|||
.filter_map(|rendezvous_point| rendezvous_point.split_peer_id())
|
||||
.collect();
|
||||
|
||||
let identity = context
|
||||
.config
|
||||
let config = context.try_get_config().await?;
|
||||
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
|
||||
.as_ref()
|
||||
.context("Cannot extract seed")?
|
||||
|
|
@ -1450,11 +1415,11 @@ pub async fn list_sellers(
|
|||
|
||||
let sellers = list_sellers_impl(
|
||||
rendezvous_nodes,
|
||||
context.config.namespace,
|
||||
context.tor_client.clone(),
|
||||
config.namespace,
|
||||
tor_client,
|
||||
identity,
|
||||
Some(context.db.clone()),
|
||||
context.tauri_handle(),
|
||||
Some(db.clone()),
|
||||
tauri_handle,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
@ -1480,10 +1445,7 @@ pub async fn list_sellers(
|
|||
// Add the peer as known to the database
|
||||
// This'll allow us to later request a quote again
|
||||
// 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 }) => {
|
||||
tracing::trace!(
|
||||
|
|
@ -1500,10 +1462,7 @@ pub async fn list_sellers(
|
|||
|
||||
#[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))]
|
||||
pub async fn export_bitcoin_wallet(context: Arc<Context>) -> Result<serde_json::Value> {
|
||||
let bitcoin_wallet = context
|
||||
.bitcoin_wallet
|
||||
.as_ref()
|
||||
.context("Could not get Bitcoin wallet")?;
|
||||
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||
|
||||
let wallet_export = bitcoin_wallet.wallet_export("cli").await?;
|
||||
tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet");
|
||||
|
|
@ -1518,14 +1477,17 @@ pub async fn monero_recovery(
|
|||
context: Arc<Context>,
|
||||
) -> Result<serde_json::Value> {
|
||||
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 {
|
||||
let (spend_key, view_key) = state5.xmr_keys();
|
||||
let restore_height = state5.monero_wallet_restore_blockheight.height;
|
||||
|
||||
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(view_key.public()),
|
||||
);
|
||||
|
|
@ -1978,10 +1940,7 @@ impl Request for GetMoneroSyncProgressArgs {
|
|||
type Response = GetMoneroSyncProgressResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
let wallet_manager = ctx
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||
let wallet = wallet_manager.main_wallet().await;
|
||||
|
||||
let sync_progress = wallet.call(|wallet| wallet.sync_progress()).await;
|
||||
|
|
@ -2008,10 +1967,7 @@ impl Request for GetMoneroSeedArgs {
|
|||
type Response = GetMoneroSeedResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
let wallet_manager = ctx
|
||||
.monero_manager
|
||||
.as_ref()
|
||||
.context("Monero wallet manager not available")?;
|
||||
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||
let wallet = wallet_manager.main_wallet().await;
|
||||
|
||||
let seed = wallet.seed().await?;
|
||||
|
|
|
|||
|
|
@ -21,12 +21,13 @@ use tokio::sync::oneshot;
|
|||
use typeshare::typeshare;
|
||||
use uuid::Uuid;
|
||||
|
||||
const TAURI_UNIFIED_EVENT_NAME: &str = "tauri-unified-event";
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(tag = "channelName", content = "event")]
|
||||
pub enum TauriEvent {
|
||||
SwapProgress(TauriSwapProgressEventWrapper),
|
||||
ContextInitProgress(TauriContextStatusEvent),
|
||||
CliLog(TauriLogEvent),
|
||||
BalanceChange(BalanceResponse),
|
||||
SwapDatabaseStateUpdate(TauriDatabaseStateEvent),
|
||||
|
|
@ -46,7 +47,14 @@ pub enum MoneroWalletUpdate {
|
|||
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]
|
||||
#[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) {
|
||||
self.emit_unified_event(TauriEvent::CliLog(event));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ where
|
|||
.transpose()?
|
||||
.map(|address| address.into_unchecked());
|
||||
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_tor(tor.enable_tor)
|
||||
.with_bitcoin(bitcoin)
|
||||
|
|
@ -87,9 +87,8 @@ where
|
|||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
BuyXmrArgs {
|
||||
rendezvous_points: vec![],
|
||||
|
|
@ -103,14 +102,13 @@ where
|
|||
Ok(context)
|
||||
}
|
||||
CliCommand::History => {
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
GetHistoryArgs {}.request(context.clone()).await?;
|
||||
|
||||
|
|
@ -121,14 +119,13 @@ where
|
|||
redact,
|
||||
swap_id,
|
||||
} => {
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
GetLogsArgs {
|
||||
logs_dir,
|
||||
|
|
@ -141,29 +138,27 @@ where
|
|||
Ok(context)
|
||||
}
|
||||
CliCommand::Config => {
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
GetConfigArgs {}.request(context.clone()).await?;
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
CliCommand::Balance { bitcoin } => {
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_bitcoin(bitcoin)
|
||||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
BalanceArgs {
|
||||
force_refresh: true,
|
||||
|
|
@ -180,15 +175,14 @@ where
|
|||
} => {
|
||||
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)
|
||||
.with_bitcoin(bitcoin)
|
||||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
WithdrawBtcArgs { amount, address }
|
||||
.request(context.clone())
|
||||
|
|
@ -202,7 +196,7 @@ where
|
|||
monero,
|
||||
tor,
|
||||
} => {
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_tor(tor.enable_tor)
|
||||
.with_bitcoin(bitcoin)
|
||||
|
|
@ -210,9 +204,8 @@ where
|
|||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
ResumeSwapArgs { swap_id }.request(context.clone()).await?;
|
||||
|
||||
|
|
@ -222,15 +215,14 @@ where
|
|||
swap_id: SwapId { swap_id },
|
||||
bitcoin,
|
||||
} => {
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_bitcoin(bitcoin)
|
||||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
CancelAndRefundArgs { swap_id }
|
||||
.request(context.clone())
|
||||
|
|
@ -242,15 +234,14 @@ where
|
|||
rendezvous_point,
|
||||
tor,
|
||||
} => {
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_tor(tor.enable_tor)
|
||||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
ListSellersArgs {
|
||||
rendezvous_points: vec![rendezvous_point],
|
||||
|
|
@ -261,15 +252,14 @@ where
|
|||
Ok(context)
|
||||
}
|
||||
CliCommand::ExportBitcoinWallet { bitcoin } => {
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_bitcoin(bitcoin)
|
||||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
ExportBitcoinWalletArgs {}.request(context.clone()).await?;
|
||||
|
||||
|
|
@ -278,14 +268,13 @@ where
|
|||
CliCommand::MoneroRecovery {
|
||||
swap_id: SwapId { swap_id },
|
||||
} => {
|
||||
let context = Arc::new(
|
||||
let context = Arc::new(Context::new_without_tauri_handle());
|
||||
ContextBuilder::new(is_testnet)
|
||||
.with_data_dir(data)
|
||||
.with_debug(debug)
|
||||
.with_json(json)
|
||||
.build()
|
||||
.await?,
|
||||
);
|
||||
.build(context.clone())
|
||||
.await?;
|
||||
|
||||
MoneroRecoveryArgs { swap_id }
|
||||
.request(context.clone())
|
||||
|
|
|
|||
|
|
@ -33,17 +33,55 @@ pub fn init(
|
|||
tauri_handle: Option<TauriHandle>,
|
||||
trace_stdout: bool,
|
||||
) -> 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![
|
||||
// Main libp2p crates
|
||||
"libp2p",
|
||||
"libp2p_swarm",
|
||||
"libp2p_core",
|
||||
"libp2p_tcp",
|
||||
"libp2p_noise",
|
||||
"libp2p_tor",
|
||||
// Specific libp2p module targets that appear in logs
|
||||
"libp2p_core::transport",
|
||||
"libp2p_core::transport::choice",
|
||||
"libp2p_core::transport::dummy",
|
||||
|
|
@ -67,6 +105,7 @@ pub fn init(
|
|||
"libp2p_dcutr",
|
||||
"monero_cpp",
|
||||
];
|
||||
|
||||
let OUR_CRATES: Vec<&str> = vec![
|
||||
"swap",
|
||||
"asb",
|
||||
|
|
@ -79,8 +118,6 @@ pub fn init(
|
|||
"monero_rpc_pool",
|
||||
];
|
||||
|
||||
let INFO_LEVEL_CRATES: Vec<&str> = vec![];
|
||||
|
||||
// General log file for non-verbose logs
|
||||
let file_appender: RollingFileAppender = tracing_appender::rolling::never(&dir, "swap-all.log");
|
||||
|
||||
|
|
@ -104,11 +141,10 @@ pub fn init(
|
|||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.json()
|
||||
.with_filter(env_filter_with_info_crates(
|
||||
level_filter,
|
||||
.with_filter(env_filter_with_all_crates(vec![(
|
||||
OUR_CRATES.clone(),
|
||||
INFO_LEVEL_CRATES.clone(),
|
||||
)?);
|
||||
level_filter,
|
||||
)])?);
|
||||
|
||||
// Layer for writing to the verbose log file
|
||||
// Crates: All crates with different levels (libp2p at INFO+, others at TRACE)
|
||||
|
|
@ -121,13 +157,11 @@ pub fn init(
|
|||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.json()
|
||||
.with_filter(env_filter_with_all_crates(
|
||||
LevelFilter::TRACE,
|
||||
OUR_CRATES.clone(),
|
||||
LIBP2P_CRATES.clone(),
|
||||
TOR_CRATES.clone(),
|
||||
INFO_LEVEL_CRATES.clone(),
|
||||
)?);
|
||||
.with_filter(env_filter_with_all_crates(vec![
|
||||
(OUR_CRATES.clone(), LevelFilter::TRACE),
|
||||
(LIBP2P_CRATES.clone(), LevelFilter::TRACE),
|
||||
(TOR_CRATES.clone(), LevelFilter::TRACE),
|
||||
])?);
|
||||
|
||||
// Layer for writing to the terminal
|
||||
// Crates: swap, asb
|
||||
|
|
@ -152,29 +186,21 @@ pub fn init(
|
|||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.json()
|
||||
.with_filter(env_filter_with_all_crates(
|
||||
level_filter,
|
||||
OUR_CRATES.clone(),
|
||||
LIBP2P_CRATES.clone(),
|
||||
TOR_CRATES.clone(),
|
||||
INFO_LEVEL_CRATES.clone(),
|
||||
)?);
|
||||
.with_filter(env_filter_with_all_crates(vec![
|
||||
(OUR_CRATES.clone(), LevelFilter::DEBUG),
|
||||
(LIBP2P_CRATES.clone(), LevelFilter::INFO),
|
||||
(TOR_CRATES.clone(), LevelFilter::INFO),
|
||||
])?);
|
||||
|
||||
// If trace_stdout is true, we log all messages to the terminal
|
||||
// Otherwise, we only log the bare minimum
|
||||
let terminal_layer_env_filter = match trace_stdout {
|
||||
true => env_filter_with_all_crates(
|
||||
LevelFilter::TRACE,
|
||||
OUR_CRATES.clone(),
|
||||
LIBP2P_CRATES.clone(),
|
||||
TOR_CRATES.clone(),
|
||||
INFO_LEVEL_CRATES.clone(),
|
||||
)?,
|
||||
false => env_filter_with_info_crates(
|
||||
level_filter,
|
||||
OUR_CRATES.clone(),
|
||||
INFO_LEVEL_CRATES.clone(),
|
||||
)?,
|
||||
true => env_filter_with_all_crates(vec![
|
||||
(OUR_CRATES.clone(), level_filter),
|
||||
(TOR_CRATES.clone(), level_filter),
|
||||
(LIBP2P_CRATES.clone(), LevelFilter::INFO),
|
||||
])?,
|
||||
false => env_filter_with_all_crates(vec![(OUR_CRATES.clone(), level_filter)])?,
|
||||
};
|
||||
|
||||
let final_terminal_layer = match format {
|
||||
|
|
@ -201,60 +227,18 @@ pub fn init(
|
|||
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.
|
||||
fn env_filter_with_all_crates(
|
||||
level_filter: LevelFilter,
|
||||
our_crates: Vec<&str>,
|
||||
libp2p_crates: Vec<&str>,
|
||||
tor_crates: Vec<&str>,
|
||||
info_level_crates: Vec<&str>,
|
||||
) -> Result<EnvFilter> {
|
||||
fn env_filter_with_all_crates(crates: Vec<(Vec<&str>, LevelFilter)>) -> Result<EnvFilter> {
|
||||
let mut filter = EnvFilter::from_default_env();
|
||||
|
||||
// Add directives for each crate in the provided list
|
||||
for crate_name in our_crates {
|
||||
// Add directives for each group of crates with their specified level filter
|
||||
for (crate_names, level_filter) in crates {
|
||||
for crate_name in crate_names {
|
||||
filter = filter.add_directive(Directive::from_str(&format!(
|
||||
"{}={}",
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue