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(
|
export function isCliLogRelatedToSwap(
|
||||||
log: CliLog | string,
|
log: CliLog | string | null | undefined,
|
||||||
swapId: string,
|
swapId: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (log === null || log === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// If we only have a string, simply check if the string contains the swap id
|
// If we only have a string, simply check if the string contains the swap id
|
||||||
// This provides reasonable backwards compatability
|
// This provides reasonable backwards compatability
|
||||||
if (typeof log === "string") {
|
if (typeof log === "string") {
|
||||||
|
|
@ -44,7 +48,7 @@ export function isCliLogRelatedToSwap(
|
||||||
// - the log has the swap id as an attribute
|
// - the log has the swap id as an attribute
|
||||||
// - there exists a span which has the swap id as an attribute
|
// - there exists a span which has the swap id as an attribute
|
||||||
return (
|
return (
|
||||||
log.fields["swap_id"] === swapId ||
|
("fields" in log && log.fields["swap_id"] === swapId) ||
|
||||||
(log.spans?.some((span) => span["swap_id"] === swapId) ?? false)
|
(log.spans?.some((span) => span["swap_id"] === swapId) ?? false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@ import {
|
||||||
ApprovalRequest,
|
ApprovalRequest,
|
||||||
ExpiredTimelocks,
|
ExpiredTimelocks,
|
||||||
GetSwapInfoResponse,
|
GetSwapInfoResponse,
|
||||||
PendingCompleted,
|
|
||||||
QuoteWithAddress,
|
|
||||||
SelectMakerDetails,
|
SelectMakerDetails,
|
||||||
TauriBackgroundProgress,
|
TauriBackgroundProgress,
|
||||||
TauriSwapProgressEvent,
|
TauriSwapProgressEvent,
|
||||||
SendMoneroDetails,
|
SendMoneroDetails,
|
||||||
|
ContextStatus,
|
||||||
} from "./tauriModel";
|
} from "./tauriModel";
|
||||||
|
import {
|
||||||
|
ContextStatusType,
|
||||||
|
ResultContextStatus,
|
||||||
|
RPCSlice,
|
||||||
|
} from "store/features/rpcSlice";
|
||||||
|
|
||||||
export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"];
|
export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"];
|
||||||
|
|
||||||
|
|
@ -382,3 +386,30 @@ export function haveFundsBeenLocked(
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isContextFullyInitialized(
|
||||||
|
status: ResultContextStatus,
|
||||||
|
): boolean {
|
||||||
|
if (status == null || status.type === ContextStatusType.Error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
status.status.bitcoin_wallet_available &&
|
||||||
|
status.status.monero_wallet_available &&
|
||||||
|
status.status.database_available &&
|
||||||
|
status.status.tor_available
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isContextWithBitcoinWallet(
|
||||||
|
status: ContextStatus | null,
|
||||||
|
): boolean {
|
||||||
|
return status?.bitcoin_wallet_available ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isContextWithMoneroWallet(
|
||||||
|
status: ContextStatus | null,
|
||||||
|
): boolean {
|
||||||
|
return status?.monero_wallet_available ?? false;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -227,9 +227,8 @@ export async function fetchAllConversations(): Promise<void> {
|
||||||
store.dispatch(setConversation({ feedbackId, messages }));
|
store.dispatch(setConversation({ feedbackId, messages }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
error,
|
{ error, feedbackId },
|
||||||
"Error fetching messages for feedback id",
|
"Error fetching messages for feedback",
|
||||||
feedbackId,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { TauriContextStatusEvent, TauriEvent } from "models/tauriModel";
|
import { TauriEvent } from "models/tauriModel";
|
||||||
import {
|
import {
|
||||||
contextStatusEventReceived,
|
contextStatusEventReceived,
|
||||||
|
contextInitializationFailed,
|
||||||
rpcSetBalance,
|
rpcSetBalance,
|
||||||
timelockChangeEventReceived,
|
timelockChangeEventReceived,
|
||||||
approvalEventReceived,
|
approvalEventReceived,
|
||||||
|
|
@ -18,7 +19,7 @@ import {
|
||||||
updateRates,
|
updateRates,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import {
|
import {
|
||||||
checkContextAvailability,
|
checkContextStatus,
|
||||||
getSwapInfo,
|
getSwapInfo,
|
||||||
initializeContext,
|
initializeContext,
|
||||||
listSellersAtRendezvousPoint,
|
listSellersAtRendezvousPoint,
|
||||||
|
|
@ -53,6 +54,9 @@ const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
|
||||||
// Fetch pending approvals every 2 seconds
|
// Fetch pending approvals every 2 seconds
|
||||||
const FETCH_PENDING_APPROVALS_INTERVAL = 2 * 1_000;
|
const FETCH_PENDING_APPROVALS_INTERVAL = 2 * 1_000;
|
||||||
|
|
||||||
|
// Check context status every 2 seconds
|
||||||
|
const CHECK_CONTEXT_STATUS_INTERVAL = 2 * 1_000;
|
||||||
|
|
||||||
function setIntervalImmediate(callback: () => void, interval: number): void {
|
function setIntervalImmediate(callback: () => void, interval: number): void {
|
||||||
callback();
|
callback();
|
||||||
setInterval(callback, interval);
|
setInterval(callback, interval);
|
||||||
|
|
@ -76,87 +80,86 @@ export async function setupBackgroundTasks(): Promise<void> {
|
||||||
|
|
||||||
// Setup Tauri event listeners
|
// Setup Tauri event listeners
|
||||||
// Check if the context is already available. This is to prevent unnecessary re-initialization
|
// Check if the context is already available. This is to prevent unnecessary re-initialization
|
||||||
if (await checkContextAvailability()) {
|
setIntervalImmediate(async () => {
|
||||||
store.dispatch(
|
const contextStatus = await checkContextStatus();
|
||||||
contextStatusEventReceived(TauriContextStatusEvent.Available),
|
store.dispatch(contextStatusEventReceived(contextStatus));
|
||||||
);
|
}, CHECK_CONTEXT_STATUS_INTERVAL);
|
||||||
} else {
|
|
||||||
|
const contextStatus = await checkContextStatus();
|
||||||
|
|
||||||
|
// If all components are unavailable, we need to initialize the context
|
||||||
|
if (
|
||||||
|
!contextStatus.bitcoin_wallet_available &&
|
||||||
|
!contextStatus.monero_wallet_available &&
|
||||||
|
!contextStatus.database_available &&
|
||||||
|
!contextStatus.tor_available
|
||||||
|
)
|
||||||
// Warning: If we reload the page while the Context is being initialized, this function will throw an error
|
// Warning: If we reload the page while the Context is being initialized, this function will throw an error
|
||||||
initializeContext().catch((e) => {
|
initializeContext().catch((e) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
e,
|
e,
|
||||||
"Failed to initialize context on page load. This might be because we reloaded the page while the context was being initialized",
|
"Failed to initialize context on page load. This might be because we reloaded the page while the context was being initialized",
|
||||||
);
|
);
|
||||||
// Wait a short time before retrying
|
store.dispatch(contextInitializationFailed(String(e)));
|
||||||
setTimeout(() => {
|
|
||||||
initializeContext().catch((e) => {
|
|
||||||
logger.error(e, "Failed to initialize context even after retry");
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for the unified event
|
|
||||||
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
|
|
||||||
const { channelName, event: eventData } = event.payload;
|
|
||||||
|
|
||||||
switch (channelName) {
|
|
||||||
case "SwapProgress":
|
|
||||||
store.dispatch(swapProgressEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "ContextInitProgress":
|
|
||||||
store.dispatch(contextStatusEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "CliLog":
|
|
||||||
store.dispatch(receivedCliLog(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "BalanceChange":
|
|
||||||
store.dispatch(rpcSetBalance(eventData.balance));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "SwapDatabaseStateUpdate":
|
|
||||||
getSwapInfo(eventData.swap_id);
|
|
||||||
|
|
||||||
// This is ugly but it's the best we can do for now
|
|
||||||
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
|
|
||||||
// in the database. So we wait a bit before fetching the new state
|
|
||||||
setTimeout(() => getSwapInfo(eventData.swap_id), 3000);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "TimelockChange":
|
|
||||||
store.dispatch(timelockChangeEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Approval":
|
|
||||||
store.dispatch(approvalEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "BackgroundProgress":
|
|
||||||
store.dispatch(backgroundProgressEventReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "PoolStatusUpdate":
|
|
||||||
store.dispatch(poolStatusReceived(eventData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "MoneroWalletUpdate":
|
|
||||||
console.log("MoneroWalletUpdate", eventData);
|
|
||||||
if (eventData.type === "BalanceChange") {
|
|
||||||
store.dispatch(setBalance(eventData.content));
|
|
||||||
}
|
|
||||||
if (eventData.type === "HistoryUpdate") {
|
|
||||||
store.dispatch(setHistory(eventData.content));
|
|
||||||
}
|
|
||||||
if (eventData.type === "SyncProgress") {
|
|
||||||
store.dispatch(setSyncProgress(eventData.content));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
exhaustiveGuard(channelName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for the unified event
|
||||||
|
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
|
||||||
|
const { channelName, event: eventData } = event.payload;
|
||||||
|
|
||||||
|
switch (channelName) {
|
||||||
|
case "SwapProgress":
|
||||||
|
store.dispatch(swapProgressEventReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "CliLog":
|
||||||
|
store.dispatch(receivedCliLog(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "BalanceChange":
|
||||||
|
store.dispatch(rpcSetBalance(eventData.balance));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "SwapDatabaseStateUpdate":
|
||||||
|
getSwapInfo(eventData.swap_id);
|
||||||
|
|
||||||
|
// This is ugly but it's the best we can do for now
|
||||||
|
// Sometimes we are too quick to fetch the swap info and the new state is not yet reflected
|
||||||
|
// in the database. So we wait a bit before fetching the new state
|
||||||
|
setTimeout(() => getSwapInfo(eventData.swap_id), 3000);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "TimelockChange":
|
||||||
|
store.dispatch(timelockChangeEventReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Approval":
|
||||||
|
store.dispatch(approvalEventReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "BackgroundProgress":
|
||||||
|
store.dispatch(backgroundProgressEventReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "PoolStatusUpdate":
|
||||||
|
store.dispatch(poolStatusReceived(eventData));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "MoneroWalletUpdate":
|
||||||
|
console.log("MoneroWalletUpdate", eventData);
|
||||||
|
if (eventData.type === "BalanceChange") {
|
||||||
|
store.dispatch(setBalance(eventData.content));
|
||||||
|
}
|
||||||
|
if (eventData.type === "HistoryUpdate") {
|
||||||
|
store.dispatch(setHistory(eventData.content));
|
||||||
|
}
|
||||||
|
if (eventData.type === "SyncProgress") {
|
||||||
|
store.dispatch(setSyncProgress(eventData.content));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
exhaustiveGuard(channelName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import SeedSelectionDialog from "./modal/seed-selection/SeedSelectionDialog";
|
||||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
import PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog";
|
import PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog";
|
||||||
|
import ContextErrorDialog from "./modal/context-error/ContextErrorDialog";
|
||||||
|
|
||||||
declare module "@mui/material/styles" {
|
declare module "@mui/material/styles" {
|
||||||
interface Theme {
|
interface Theme {
|
||||||
|
|
@ -54,6 +55,7 @@ export default function App() {
|
||||||
<IntroductionModal />
|
<IntroductionModal />
|
||||||
<SeedSelectionDialog />
|
<SeedSelectionDialog />
|
||||||
<PasswordEntryDialog />
|
<PasswordEntryDialog />
|
||||||
|
<ContextErrorDialog />
|
||||||
<Router>
|
<Router>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<InnerContent />
|
<InnerContent />
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,12 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import CircularProgress from "@mui/material/CircularProgress";
|
import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
import { ContextStatus } from "models/tauriModel";
|
||||||
|
import { isContextFullyInitialized } from "models/tauriModelExt";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import { useIsContextAvailable } from "store/hooks";
|
import { ContextStatusType } from "store/features/rpcSlice";
|
||||||
|
import { useAppSelector, useIsContextAvailable } from "store/hooks";
|
||||||
|
|
||||||
interface PromiseInvokeButtonProps<T> {
|
interface PromiseInvokeButtonProps<T> {
|
||||||
onSuccess?: (data: T) => void | null;
|
onSuccess?: (data: T) => void | null;
|
||||||
|
|
@ -23,7 +26,10 @@ interface PromiseInvokeButtonProps<T> {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
displayErrorSnackbar?: boolean;
|
displayErrorSnackbar?: boolean;
|
||||||
tooltipTitle?: string | null;
|
tooltipTitle?: string | null;
|
||||||
requiresContext?: boolean;
|
// true means that the entire context must be available
|
||||||
|
// false means that the context doesn't have to be available at all
|
||||||
|
// a custom function means that the context must satisfy the function
|
||||||
|
contextRequirement?: ((status: ContextStatus) => boolean) | false | true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PromiseInvokeButton<T>({
|
export default function PromiseInvokeButton<T>({
|
||||||
|
|
@ -39,13 +45,11 @@ export default function PromiseInvokeButton<T>({
|
||||||
isChipButton = false,
|
isChipButton = false,
|
||||||
displayErrorSnackbar = false,
|
displayErrorSnackbar = false,
|
||||||
onPendingChange = null,
|
onPendingChange = null,
|
||||||
requiresContext = true,
|
contextRequirement = true,
|
||||||
tooltipTitle = null,
|
tooltipTitle = null,
|
||||||
...rest
|
...rest
|
||||||
}: PromiseInvokeButtonProps<T> & ButtonProps) {
|
}: PromiseInvokeButtonProps<T> & ButtonProps) {
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
const isContextAvailable = useIsContextAvailable();
|
|
||||||
|
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
|
||||||
const isLoading = isPending || isLoadingOverride;
|
const isLoading = isPending || isLoadingOverride;
|
||||||
|
|
@ -73,7 +77,23 @@ export default function PromiseInvokeButton<T>({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiresContextButNotAvailable = requiresContext && !isContextAvailable;
|
const requiresContextButNotAvailable = useAppSelector((state) => {
|
||||||
|
const status = state.rpc.status;
|
||||||
|
|
||||||
|
if (contextRequirement === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextRequirement === true || contextRequirement == null) {
|
||||||
|
return !isContextFullyInitialized(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == null || status.type === ContextStatusType.Error) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !contextRequirement(status.status);
|
||||||
|
});
|
||||||
const isDisabled = disabled || isLoading || requiresContextButNotAvailable;
|
const isDisabled = disabled || isLoading || requiresContextButNotAvailable;
|
||||||
|
|
||||||
const actualTooltipTitle =
|
const actualTooltipTitle =
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,7 @@ import { useAppSelector, usePendingBackgroundProcesses } from "store/hooks";
|
||||||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||||
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
|
import { LoadingSpinnerAlert } from "./LoadingSpinnerAlert";
|
||||||
import { bytesToMb } from "utils/conversionUtils";
|
import { bytesToMb } from "utils/conversionUtils";
|
||||||
import {
|
import { TauriBackgroundProgress } from "models/tauriModel";
|
||||||
TauriBackgroundProgress,
|
|
||||||
TauriContextStatusEvent,
|
|
||||||
} from "models/tauriModel";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import TruncatedText from "../other/TruncatedText";
|
import TruncatedText from "../other/TruncatedText";
|
||||||
import BitcoinIcon from "../icons/BitcoinIcon";
|
import BitcoinIcon from "../icons/BitcoinIcon";
|
||||||
|
|
@ -182,41 +179,6 @@ function PartialInitStatus({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DaemonStatusAlert() {
|
|
||||||
const contextStatus = useAppSelector((s) => s.rpc.status);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
switch (contextStatus) {
|
|
||||||
case null:
|
|
||||||
return null;
|
|
||||||
case TauriContextStatusEvent.NotInitialized:
|
|
||||||
return null;
|
|
||||||
case TauriContextStatusEvent.Initializing:
|
|
||||||
return null;
|
|
||||||
case TauriContextStatusEvent.Available:
|
|
||||||
return <Alert severity="success">The daemon is running</Alert>;
|
|
||||||
case TauriContextStatusEvent.Failed:
|
|
||||||
return (
|
|
||||||
<Alert
|
|
||||||
severity="error"
|
|
||||||
action={
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => navigate("/settings#daemon-control-box")}
|
|
||||||
>
|
|
||||||
View Logs
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
The daemon has stopped unexpectedly
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return exhaustiveGuard(contextStatus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function BackgroundProgressAlerts() {
|
export function BackgroundProgressAlerts() {
|
||||||
const backgroundProgress = usePendingBackgroundProcesses();
|
const backgroundProgress = usePendingBackgroundProcesses();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ export default function SwapSuspendAlert({
|
||||||
color="primary"
|
color="primary"
|
||||||
onSuccess={onClose}
|
onSuccess={onClose}
|
||||||
onInvoke={suspendCurrentSwap}
|
onInvoke={suspendCurrentSwap}
|
||||||
|
contextRequirement={false}
|
||||||
>
|
>
|
||||||
Suspend
|
Suspend
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
<DialogActions>
|
||||||
<Button onClick={handleClose}>Cancel</Button>
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
requiresContext={false}
|
contextRequirement={false}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onInvoke={submitFeedback}
|
onInvoke={submitFeedback}
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ import {
|
||||||
Switch,
|
Switch,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { CliLog } from "models/cliModel";
|
|
||||||
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||||
|
import { HashedLog } from "store/features/logsSlice";
|
||||||
|
|
||||||
interface LogViewerProps {
|
interface LogViewerProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (_: boolean) => void;
|
setOpen: (_: boolean) => void;
|
||||||
logs: (string | CliLog)[] | null;
|
logs: HashedLog[];
|
||||||
setIsRedacted: (_: boolean) => void;
|
setIsRedacted: (_: boolean) => void;
|
||||||
isRedacted: boolean;
|
isRedacted: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ import { store } from "renderer/store/storeRenderer";
|
||||||
import { useActiveSwapInfo } from "store/hooks";
|
import { useActiveSwapInfo } from "store/hooks";
|
||||||
import { logsToRawString } from "utils/parseUtils";
|
import { logsToRawString } from "utils/parseUtils";
|
||||||
import { getLogsOfSwap, redactLogs } from "renderer/rpc";
|
import { getLogsOfSwap, redactLogs } from "renderer/rpc";
|
||||||
import { CliLog, parseCliLogString } from "models/cliModel";
|
import { parseCliLogString } from "models/cliModel";
|
||||||
import logger from "utils/logger";
|
import logger from "utils/logger";
|
||||||
import { submitFeedbackViaHttp } from "renderer/api";
|
import { submitFeedbackViaHttp } from "renderer/api";
|
||||||
import { addFeedbackId } from "store/features/conversationsSlice";
|
import { addFeedbackId } from "store/features/conversationsSlice";
|
||||||
import { AttachmentInput } from "models/apiModel";
|
import { AttachmentInput } from "models/apiModel";
|
||||||
import { useSnackbar } from "notistack";
|
import { useSnackbar } from "notistack";
|
||||||
|
import { HashedLog, hashLogs } from "store/features/logsSlice";
|
||||||
|
|
||||||
export const MAX_FEEDBACK_LENGTH = 4000;
|
export const MAX_FEEDBACK_LENGTH = 4000;
|
||||||
|
|
||||||
|
|
@ -21,8 +22,8 @@ interface FeedbackInputState {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FeedbackLogsState {
|
interface FeedbackLogsState {
|
||||||
swapLogs: (string | CliLog)[] | null;
|
swapLogs: HashedLog[];
|
||||||
daemonLogs: (string | CliLog)[] | null;
|
daemonLogs: HashedLog[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialInputState: FeedbackInputState = {
|
const initialInputState: FeedbackInputState = {
|
||||||
|
|
@ -34,8 +35,8 @@ const initialInputState: FeedbackInputState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialLogsState: FeedbackLogsState = {
|
const initialLogsState: FeedbackLogsState = {
|
||||||
swapLogs: null,
|
swapLogs: [],
|
||||||
daemonLogs: null,
|
daemonLogs: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useFeedback() {
|
export function useFeedback() {
|
||||||
|
|
@ -55,56 +56,60 @@ export function useFeedback() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputState.selectedSwap === null) {
|
if (inputState.selectedSwap === null) {
|
||||||
setLogsState((prev) => ({ ...prev, swapLogs: null }));
|
setLogsState((prev) => ({ ...prev, swapLogs: [] }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted)
|
getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
const parsedLogs = response.logs.map(parseCliLogString);
|
||||||
setLogsState((prev) => ({
|
setLogsState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
swapLogs: response.logs.map(parseCliLogString),
|
swapLogs: hashLogs(parsedLogs),
|
||||||
}));
|
}));
|
||||||
setError(null);
|
setError(null);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
logger.error(`Failed to fetch swap logs: ${e}`);
|
logger.error(`Failed to fetch swap logs: ${e}`);
|
||||||
setLogsState((prev) => ({ ...prev, swapLogs: null }));
|
setLogsState((prev) => ({ ...prev, swapLogs: [] }));
|
||||||
setError(`Failed to fetch swap logs: ${e}`);
|
setError(`Failed to fetch swap logs: ${e}`);
|
||||||
});
|
});
|
||||||
}, [inputState.selectedSwap, inputState.isSwapLogsRedacted]);
|
}, [inputState.selectedSwap, inputState.isSwapLogsRedacted]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inputState.attachDaemonLogs) {
|
if (!inputState.attachDaemonLogs) {
|
||||||
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
|
setLogsState((prev) => ({ ...prev, daemonLogs: [] }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const hashedLogs = store.getState().logs?.state.logs ?? [];
|
||||||
|
|
||||||
if (inputState.isDaemonLogsRedacted) {
|
if (inputState.isDaemonLogsRedacted) {
|
||||||
redactLogs(store.getState().logs?.state.logs)
|
const logs = hashedLogs.map((h) => h.log);
|
||||||
|
redactLogs(logs)
|
||||||
.then((redactedLogs) => {
|
.then((redactedLogs) => {
|
||||||
setLogsState((prev) => ({
|
setLogsState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
daemonLogs: redactedLogs,
|
daemonLogs: hashLogs(redactedLogs),
|
||||||
}));
|
}));
|
||||||
setError(null);
|
setError(null);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
logger.error(`Failed to redact daemon logs: ${e}`);
|
logger.error(`Failed to redact daemon logs: ${e}`);
|
||||||
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
|
setLogsState((prev) => ({ ...prev, daemonLogs: [] }));
|
||||||
setError(`Failed to redact daemon logs: ${e}`);
|
setError(`Failed to redact daemon logs: ${e}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setLogsState((prev) => ({
|
setLogsState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
daemonLogs: store.getState().logs?.state.logs,
|
daemonLogs: hashedLogs,
|
||||||
}));
|
}));
|
||||||
setError(null);
|
setError(null);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to fetch daemon logs: ${e}`);
|
logger.error(`Failed to fetch daemon logs: ${e}`);
|
||||||
setLogsState((prev) => ({ ...prev, daemonLogs: null }));
|
setLogsState((prev) => ({ ...prev, daemonLogs: [] }));
|
||||||
setError(`Failed to fetch daemon logs: ${e}`);
|
setError(`Failed to fetch daemon logs: ${e}`);
|
||||||
}
|
}
|
||||||
}, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted]);
|
}, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted]);
|
||||||
|
|
@ -123,18 +128,18 @@ export function useFeedback() {
|
||||||
|
|
||||||
const attachments: AttachmentInput[] = [];
|
const attachments: AttachmentInput[] = [];
|
||||||
// Add swap logs as an attachment
|
// Add swap logs as an attachment
|
||||||
if (logsState.swapLogs) {
|
if (logsState.swapLogs.length > 0) {
|
||||||
attachments.push({
|
attachments.push({
|
||||||
key: `swap_logs_${inputState.selectedSwap}.txt`,
|
key: `swap_logs_${inputState.selectedSwap}.txt`,
|
||||||
content: logsToRawString(logsState.swapLogs),
|
content: logsToRawString(logsState.swapLogs.map((h) => h.log)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle daemon logs
|
// Handle daemon logs
|
||||||
if (logsState.daemonLogs) {
|
if (logsState.daemonLogs.length > 0) {
|
||||||
attachments.push({
|
attachments.push({
|
||||||
key: "daemon_logs.txt",
|
key: "daemon_logs.txt",
|
||||||
content: logsToRawString(logsState.daemonLogs),
|
content: logsToRawString(logsState.daemonLogs.map((h) => h.log)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export default function PasswordEntryDialog() {
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
onInvoke={accept}
|
onInvoke={accept}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
requiresContext={false}
|
contextRequirement={false}
|
||||||
>
|
>
|
||||||
Unlock
|
Unlock
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
||||||
|
|
@ -364,7 +364,7 @@ export default function SeedSelectionDialog() {
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
variant="text"
|
variant="text"
|
||||||
onInvoke={Legacy}
|
onInvoke={Legacy}
|
||||||
requiresContext={false}
|
contextRequirement={false}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
>
|
>
|
||||||
No wallet (Legacy)
|
No wallet (Legacy)
|
||||||
|
|
@ -373,7 +373,7 @@ export default function SeedSelectionDialog() {
|
||||||
onInvoke={accept}
|
onInvoke={accept}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
requiresContext={false}
|
contextRequirement={false}
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,9 @@
|
||||||
import { Box, DialogContentText } from "@mui/material";
|
import { Box, DialogContentText } from "@mui/material";
|
||||||
import {
|
import { useActiveSwapLogs } from "store/hooks";
|
||||||
useActiveSwapInfo,
|
|
||||||
useActiveSwapLogs,
|
|
||||||
useAppSelector,
|
|
||||||
} from "store/hooks";
|
|
||||||
import JsonTreeView from "../../../other/JSONViewTree";
|
|
||||||
import CliLogsBox from "../../../other/RenderedCliLog";
|
import CliLogsBox from "../../../other/RenderedCliLog";
|
||||||
|
|
||||||
export default function DebugPage() {
|
export default function DebugPage() {
|
||||||
const logs = useActiveSwapLogs();
|
const logs = useActiveSwapLogs();
|
||||||
const cliState = useActiveSwapInfo();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ padding: 2, display: "flex", flexDirection: "column", gap: 2 }}>
|
<Box sx={{ padding: 2, display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import DialogHeader from "../DialogHeader";
|
||||||
import AddressInputPage from "./pages/AddressInputPage";
|
import AddressInputPage from "./pages/AddressInputPage";
|
||||||
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
|
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
|
||||||
import WithdrawDialogContent from "./WithdrawDialogContent";
|
import WithdrawDialogContent from "./WithdrawDialogContent";
|
||||||
|
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
|
||||||
|
|
||||||
export default function WithdrawDialog({
|
export default function WithdrawDialog({
|
||||||
open,
|
open,
|
||||||
|
|
@ -61,6 +62,7 @@ export default function WithdrawDialog({
|
||||||
onInvoke={() => withdrawBtc(withdrawAddress)}
|
onInvoke={() => withdrawBtc(withdrawAddress)}
|
||||||
onPendingChange={setPending}
|
onPendingChange={setPending}
|
||||||
onSuccess={setWithdrawTxId}
|
onSuccess={setWithdrawTxId}
|
||||||
|
contextRequirement={isContextWithBitcoinWallet}
|
||||||
>
|
>
|
||||||
Withdraw
|
Withdraw
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import { Box, Tooltip } from "@mui/material";
|
import { Box, Tooltip } from "@mui/material";
|
||||||
import GitHubIcon from "@mui/icons-material/GitHub";
|
import { BackgroundProgressAlerts } from "../alert/DaemonStatusAlert";
|
||||||
import DaemonStatusAlert, {
|
|
||||||
BackgroundProgressAlerts,
|
|
||||||
} from "../alert/DaemonStatusAlert";
|
|
||||||
import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
|
import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
|
||||||
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
|
import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
|
||||||
import LinkIconButton from "../icons/LinkIconButton";
|
|
||||||
import BackgroundRefundAlert from "../alert/BackgroundRefundAlert";
|
import BackgroundRefundAlert from "../alert/BackgroundRefundAlert";
|
||||||
import MatrixIcon from "../icons/MatrixIcon";
|
import ContactInfoBox from "../other/ContactInfoBox";
|
||||||
import { MenuBook } from "@mui/icons-material";
|
|
||||||
|
|
||||||
export default function NavigationFooter() {
|
export default function NavigationFooter() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -23,36 +18,8 @@ export default function NavigationFooter() {
|
||||||
<FundsLeftInWalletAlert />
|
<FundsLeftInWalletAlert />
|
||||||
<UnfinishedSwapsAlert />
|
<UnfinishedSwapsAlert />
|
||||||
<BackgroundRefundAlert />
|
<BackgroundRefundAlert />
|
||||||
<DaemonStatusAlert />
|
|
||||||
<BackgroundProgressAlerts />
|
<BackgroundProgressAlerts />
|
||||||
<Box
|
<ContactInfoBox />
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-evenly",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip title="Check out the GitHub repository">
|
|
||||||
<span>
|
|
||||||
<LinkIconButton url="https://github.com/eigenwallet/core">
|
|
||||||
<GitHubIcon />
|
|
||||||
</LinkIconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Join the Matrix room">
|
|
||||||
<span>
|
|
||||||
<LinkIconButton url="https://matrix.to/#/#unstoppableswap-space:matrix.org">
|
|
||||||
<MatrixIcon />
|
|
||||||
</LinkIconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Read our official documentation">
|
|
||||||
<span>
|
|
||||||
<LinkIconButton url="https://docs.unstoppableswap.net">
|
|
||||||
<MenuBook />
|
|
||||||
</LinkIconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 { Box, Chip, Typography } from "@mui/material";
|
||||||
import { CliLog } from "models/cliModel";
|
import { CliLog } from "models/cliModel";
|
||||||
|
import { HashedLog } from "store/features/logsSlice";
|
||||||
import { ReactNode, useMemo, useState } from "react";
|
import { ReactNode, useMemo, useState } from "react";
|
||||||
import { logsToRawString } from "utils/parseUtils";
|
import { logsToRawString } from "utils/parseUtils";
|
||||||
import ScrollablePaperTextBox from "./ScrollablePaperTextBox";
|
import ScrollablePaperTextBox from "./ScrollablePaperTextBox";
|
||||||
|
|
@ -62,44 +63,54 @@ export default function CliLogsBox({
|
||||||
label,
|
label,
|
||||||
logs,
|
logs,
|
||||||
topRightButton = null,
|
topRightButton = null,
|
||||||
autoScroll = false,
|
autoScroll = true,
|
||||||
minHeight,
|
minHeight,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
logs: (CliLog | string)[];
|
logs: HashedLog[];
|
||||||
topRightButton?: ReactNode;
|
topRightButton?: ReactNode;
|
||||||
autoScroll?: boolean;
|
autoScroll?: boolean;
|
||||||
minHeight?: string;
|
minHeight?: string;
|
||||||
}) {
|
}) {
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
|
|
||||||
const memoizedLogs = useMemo(() => {
|
const filteredLogs = useMemo(() => {
|
||||||
if (searchQuery.length === 0) {
|
if (searchQuery.length === 0) {
|
||||||
return logs;
|
return logs;
|
||||||
}
|
}
|
||||||
return logs.filter((log) =>
|
|
||||||
|
return logs.filter(({ log }) =>
|
||||||
JSON.stringify(log).toLowerCase().includes(searchQuery.toLowerCase()),
|
JSON.stringify(log).toLowerCase().includes(searchQuery.toLowerCase()),
|
||||||
);
|
);
|
||||||
}, [logs, searchQuery]);
|
}, [logs, searchQuery]);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
return filteredLogs.map(({ log, hash }) =>
|
||||||
|
typeof log === "string" ? (
|
||||||
|
<Typography key={hash} component="pre">
|
||||||
|
{log}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<RenderedCliLog log={log} key={hash} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [filteredLogs]);
|
||||||
|
|
||||||
|
const rawStrings = useMemo(
|
||||||
|
() => filteredLogs.map(({ log }) => log),
|
||||||
|
[filteredLogs],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollablePaperTextBox
|
<ScrollablePaperTextBox
|
||||||
minHeight={minHeight}
|
minHeight={minHeight}
|
||||||
title={label}
|
title={label}
|
||||||
copyValue={logsToRawString(logs)}
|
copyValue={logsToRawString(rawStrings)}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
setSearchQuery={setSearchQuery}
|
setSearchQuery={setSearchQuery}
|
||||||
topRightButton={topRightButton}
|
topRightButton={topRightButton}
|
||||||
autoScroll={autoScroll}
|
autoScroll={autoScroll}
|
||||||
rows={memoizedLogs.map((log) =>
|
rows={rows}
|
||||||
typeof log === "string" ? (
|
|
||||||
<Typography key={log} component="pre">
|
|
||||||
{log}
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<RenderedCliLog log={log} key={JSON.stringify(log)} />
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ function ConversationModal({
|
||||||
// Fetch updated conversations
|
// Fetch updated conversations
|
||||||
fetchAllConversations();
|
fetchAllConversations();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error sending message:", error);
|
logger.error(`Error sending message: ${error}`);
|
||||||
enqueueSnackbar("Failed to send message. Please try again.", {
|
enqueueSnackbar("Failed to send message. Please try again.", {
|
||||||
variant: "error",
|
variant: "error",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,27 @@
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
|
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
|
||||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox";
|
||||||
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
import CliLogsBox from "renderer/components/other/RenderedCliLog";
|
||||||
import { getDataDir, initializeContext } from "renderer/rpc";
|
import { getDataDir } from "renderer/rpc";
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||||
import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
||||||
import { TauriContextStatusEvent } from "models/tauriModel";
|
import { ContextStatusType } from "store/features/rpcSlice";
|
||||||
|
|
||||||
export default function DaemonControlBox() {
|
export default function DaemonControlBox() {
|
||||||
const logs = useAppSelector((s) => s.logs.state.logs);
|
const logs = useAppSelector((s) => s.logs.state.logs);
|
||||||
|
|
||||||
// The daemon can be manually started if it has failed or if it has not been started yet
|
const stringifiedDaemonStatus = useAppSelector((s) => {
|
||||||
const canContextBeManuallyStarted = useAppSelector(
|
if (s.rpc.status === null) {
|
||||||
(s) =>
|
return "not started";
|
||||||
s.rpc.status === TauriContextStatusEvent.Failed || s.rpc.status === null,
|
}
|
||||||
);
|
if (s.rpc.status.type === ContextStatusType.Error) {
|
||||||
const isContextInitializing = useAppSelector(
|
return "failed";
|
||||||
(s) => s.rpc.status === TauriContextStatusEvent.Initializing,
|
}
|
||||||
);
|
return "running";
|
||||||
|
});
|
||||||
const stringifiedDaemonStatus = useAppSelector(
|
|
||||||
(s) => s.rpc.status ?? "not started",
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfoBox
|
<InfoBox
|
||||||
|
|
@ -36,22 +32,11 @@ export default function DaemonControlBox() {
|
||||||
}
|
}
|
||||||
additionalContent={
|
additionalContent={
|
||||||
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
|
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
|
||||||
<PromiseInvokeButton
|
|
||||||
variant="contained"
|
|
||||||
endIcon={<PlayArrowIcon />}
|
|
||||||
onInvoke={initializeContext}
|
|
||||||
requiresContext={false}
|
|
||||||
disabled={!canContextBeManuallyStarted}
|
|
||||||
isLoadingOverride={isContextInitializing}
|
|
||||||
displayErrorSnackbar
|
|
||||||
>
|
|
||||||
Start Daemon
|
|
||||||
</PromiseInvokeButton>
|
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
endIcon={<RotateLeftIcon />}
|
endIcon={<RotateLeftIcon />}
|
||||||
onInvoke={relaunch}
|
onInvoke={relaunch}
|
||||||
requiresContext={false}
|
contextRequirement={false}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
>
|
>
|
||||||
Restart GUI
|
Restart GUI
|
||||||
|
|
@ -59,7 +44,7 @@ export default function DaemonControlBox() {
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
endIcon={<FolderOpenIcon />}
|
endIcon={<FolderOpenIcon />}
|
||||||
isIconButton
|
isIconButton
|
||||||
requiresContext={false}
|
contextRequirement={false}
|
||||||
size="small"
|
size="small"
|
||||||
tooltipTitle="Open the data directory in your file explorer"
|
tooltipTitle="Open the data directory in your file explorer"
|
||||||
onInvoke={async () => {
|
onInvoke={async () => {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { getWalletDescriptor } from "renderer/rpc";
|
||||||
import { ExportBitcoinWalletResponse } from "models/tauriModel";
|
import { ExportBitcoinWalletResponse } from "models/tauriModel";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||||
|
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
|
||||||
|
|
||||||
export default function ExportDataBox() {
|
export default function ExportDataBox() {
|
||||||
const [walletDescriptor, setWalletDescriptor] =
|
const [walletDescriptor, setWalletDescriptor] =
|
||||||
|
|
@ -52,6 +53,7 @@ export default function ExportDataBox() {
|
||||||
onInvoke={getWalletDescriptor}
|
onInvoke={getWalletDescriptor}
|
||||||
onSuccess={setWalletDescriptor}
|
onSuccess={setWalletDescriptor}
|
||||||
displayErrorSnackbar={true}
|
displayErrorSnackbar={true}
|
||||||
|
contextRequirement={isContextWithBitcoinWallet}
|
||||||
>
|
>
|
||||||
Reveal Bitcoin Wallet Private Key
|
Reveal Bitcoin Wallet Private Key
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ export default function ExportLogsButton({
|
||||||
}: ExportLogsButtonProps) {
|
}: ExportLogsButtonProps) {
|
||||||
async function handleExportLogs() {
|
async function handleExportLogs() {
|
||||||
const swapLogs = await getLogsOfSwap(swap_id, false);
|
const swapLogs = await getLogsOfSwap(swap_id, false);
|
||||||
const daemonLogs = store.getState().logs?.state.logs;
|
const hashedDaemonLogs = store.getState().logs?.state.logs ?? [];
|
||||||
|
const daemonLogs = hashedDaemonLogs.map((h) => h.log);
|
||||||
|
|
||||||
const logContent = {
|
const logContent = {
|
||||||
swap_logs: logsToRawString(swapLogs.logs),
|
swap_logs: logsToRawString(swapLogs.logs),
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,23 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { ButtonProps } from "@mui/material/Button";
|
import { ButtonProps } from "@mui/material/Button";
|
||||||
import { CliLog, parseCliLogString } from "models/cliModel";
|
import { parseCliLogString } from "models/cliModel";
|
||||||
import { GetLogsResponse } from "models/tauriModel";
|
import { GetLogsResponse } from "models/tauriModel";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { getLogsOfSwap } from "renderer/rpc";
|
import { getLogsOfSwap } from "renderer/rpc";
|
||||||
import CliLogsBox from "../../../other/RenderedCliLog";
|
import CliLogsBox from "../../../other/RenderedCliLog";
|
||||||
|
import { HashedLog, hashLogs } from "store/features/logsSlice";
|
||||||
|
|
||||||
export default function SwapLogFileOpenButton({
|
export default function SwapLogFileOpenButton({
|
||||||
swapId,
|
swapId,
|
||||||
...props
|
...props
|
||||||
}: { swapId: string } & ButtonProps) {
|
}: { swapId: string } & ButtonProps) {
|
||||||
const [logs, setLogs] = useState<(CliLog | string)[] | null>(null);
|
const [logs, setLogs] = useState<HashedLog[] | null>(null);
|
||||||
|
|
||||||
function onLogsReceived(response: GetLogsResponse) {
|
function onLogsReceived(response: GetLogsResponse) {
|
||||||
setLogs(response.logs.map(parseCliLogString));
|
const parsedLogs = response.logs.map(parseCliLogString);
|
||||||
|
setLogs(hashLogs(parsedLogs));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
GetMoneroSeedResponse,
|
GetMoneroSeedResponse,
|
||||||
GetRestoreHeightResponse,
|
GetRestoreHeightResponse,
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
|
import { isContextWithMoneroWallet } from "models/tauriModelExt";
|
||||||
|
|
||||||
interface SeedPhraseButtonProps {
|
interface SeedPhraseButtonProps {
|
||||||
onMenuClose: () => void;
|
onMenuClose: () => void;
|
||||||
|
|
@ -32,6 +33,7 @@ export default function SeedPhraseButton({
|
||||||
onSuccess={handleSeedPhraseSuccess}
|
onSuccess={handleSeedPhraseSuccess}
|
||||||
displayErrorSnackbar={true}
|
displayErrorSnackbar={true}
|
||||||
variant="text"
|
variant="text"
|
||||||
|
contextRequirement={isContextWithMoneroWallet}
|
||||||
sx={{
|
sx={{
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { getRestoreHeight, setMoneroRestoreHeight } from "renderer/rpc";
|
||||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
||||||
import { Dayjs } from "dayjs";
|
import { Dayjs } from "dayjs";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
import { isContextWithMoneroWallet } from "models/tauriModelExt";
|
||||||
|
|
||||||
enum RestoreOption {
|
enum RestoreOption {
|
||||||
BlockHeight = "blockHeight",
|
BlockHeight = "blockHeight",
|
||||||
|
|
@ -133,6 +134,7 @@ export default function SetRestoreHeightModal({
|
||||||
onSuccess={onClose}
|
onSuccess={onClose}
|
||||||
displayErrorSnackbar={true}
|
displayErrorSnackbar={true}
|
||||||
onPendingChange={setIsPending}
|
onPendingChange={setIsPending}
|
||||||
|
contextRequirement={isContextWithMoneroWallet}
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import DFXSwissLogo from "assets/dfx-logo.svg";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { dfxAuthenticate } from "renderer/rpc";
|
import { dfxAuthenticate } from "renderer/rpc";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
|
import { isContextWithMoneroWallet } from "models/tauriModelExt";
|
||||||
|
|
||||||
function DFXLogo({ height = 24 }: { height?: number }) {
|
function DFXLogo({ height = 24 }: { height?: number }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -53,6 +54,7 @@ export default function DfxButton() {
|
||||||
tooltipTitle="Buy Monero with fiat using DFX"
|
tooltipTitle="Buy Monero with fiat using DFX"
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
isChipButton
|
isChipButton
|
||||||
|
contextRequirement={isContextWithMoneroWallet}
|
||||||
>
|
>
|
||||||
Buy Monero
|
Buy Monero
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ export default function SendApprovalContent({
|
||||||
color="error"
|
color="error"
|
||||||
startIcon={<CloseIcon />}
|
startIcon={<CloseIcon />}
|
||||||
displayErrorSnackbar={true}
|
displayErrorSnackbar={true}
|
||||||
requiresContext={false}
|
contextRequirement={false}
|
||||||
>
|
>
|
||||||
Reject
|
Reject
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
@ -141,7 +141,7 @@ export default function SendApprovalContent({
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<CheckIcon />}
|
startIcon={<CheckIcon />}
|
||||||
displayErrorSnackbar={true}
|
displayErrorSnackbar={true}
|
||||||
requiresContext={false}
|
contextRequirement={false}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { sendMoneroTransaction } from "renderer/rpc";
|
import { sendMoneroTransaction } from "renderer/rpc";
|
||||||
import { useAppSelector } from "store/hooks";
|
import { useAppSelector } from "store/hooks";
|
||||||
import { SendMoneroResponse } from "models/tauriModel";
|
import { SendMoneroResponse } from "models/tauriModel";
|
||||||
|
import { isContextWithMoneroWallet } from "models/tauriModelExt";
|
||||||
|
|
||||||
interface SendTransactionContentProps {
|
interface SendTransactionContentProps {
|
||||||
balance: {
|
balance: {
|
||||||
|
|
@ -168,6 +169,7 @@ export default function SendTransactionContent({
|
||||||
disabled={isSendDisabled}
|
disabled={isSendDisabled}
|
||||||
onSuccess={handleSendSuccess}
|
onSuccess={handleSendSuccess}
|
||||||
onPendingChange={setIsSending}
|
onPendingChange={setIsSending}
|
||||||
|
contextRequirement={isContextWithMoneroWallet}
|
||||||
>
|
>
|
||||||
Send
|
Send
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,6 @@ export default function SwapSetupInflightPage({
|
||||||
resolveApproval(request.request_id, false as unknown as object)
|
resolveApproval(request.request_id, false as unknown as object)
|
||||||
}
|
}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
requiresContext
|
|
||||||
>
|
>
|
||||||
Deny
|
Deny
|
||||||
</PromiseInvokeButton>
|
</PromiseInvokeButton>
|
||||||
|
|
@ -158,7 +157,6 @@ export default function SwapSetupInflightPage({
|
||||||
resolveApproval(request.request_id, true as unknown as object)
|
resolveApproval(request.request_id, true as unknown as object)
|
||||||
}
|
}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
requiresContext
|
|
||||||
endIcon={<CheckIcon />}
|
endIcon={<CheckIcon />}
|
||||||
>
|
>
|
||||||
{`Confirm`}
|
{`Confirm`}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import RefreshIcon from "@mui/icons-material/Refresh";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { checkBitcoinBalance } from "renderer/rpc";
|
import { checkBitcoinBalance } from "renderer/rpc";
|
||||||
import { isSyncingBitcoin } from "store/hooks";
|
import { isSyncingBitcoin } from "store/hooks";
|
||||||
|
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
|
||||||
|
|
||||||
export default function WalletRefreshButton() {
|
export default function WalletRefreshButton() {
|
||||||
const isSyncing = isSyncingBitcoin();
|
const isSyncing = isSyncingBitcoin();
|
||||||
|
|
@ -14,6 +15,7 @@ export default function WalletRefreshButton() {
|
||||||
onInvoke={() => checkBitcoinBalance()}
|
onInvoke={() => checkBitcoinBalance()}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
size="small"
|
size="small"
|
||||||
|
contextRequirement={isContextWithBitcoinWallet}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,11 +46,13 @@ import {
|
||||||
GetRestoreHeightResponse,
|
GetRestoreHeightResponse,
|
||||||
MoneroNodeConfig,
|
MoneroNodeConfig,
|
||||||
GetMoneroSeedResponse,
|
GetMoneroSeedResponse,
|
||||||
|
ContextStatus,
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
import {
|
import {
|
||||||
rpcSetBalance,
|
rpcSetBalance,
|
||||||
rpcSetSwapInfo,
|
rpcSetSwapInfo,
|
||||||
approvalRequestsReplaced,
|
approvalRequestsReplaced,
|
||||||
|
contextInitializationFailed,
|
||||||
} from "store/features/rpcSlice";
|
} from "store/features/rpcSlice";
|
||||||
import {
|
import {
|
||||||
setMainAddress,
|
setMainAddress,
|
||||||
|
|
@ -282,9 +284,8 @@ export async function getMoneroRecoveryKeys(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkContextAvailability(): Promise<boolean> {
|
export async function checkContextStatus(): Promise<ContextStatus> {
|
||||||
const available = await invokeNoArgs<boolean>("is_context_available");
|
return await invokeNoArgs<ContextStatus>("get_context_status");
|
||||||
return available;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLogsOfSwap(
|
export async function getLogsOfSwap(
|
||||||
|
|
@ -335,7 +336,6 @@ export async function initializeContext() {
|
||||||
store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null;
|
store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null;
|
||||||
|
|
||||||
// Check the state of the Monero node
|
// Check the state of the Monero node
|
||||||
|
|
||||||
const moneroNodeConfig =
|
const moneroNodeConfig =
|
||||||
useMoneroRpcPool ||
|
useMoneroRpcPool ||
|
||||||
moneroNodeUrl == null ||
|
moneroNodeUrl == null ||
|
||||||
|
|
@ -356,18 +356,17 @@ export async function initializeContext() {
|
||||||
enable_monero_tor: useMoneroTor,
|
enable_monero_tor: useMoneroTor,
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info("Initializing context with settings", tauriSettings);
|
logger.info({ tauriSettings }, "Initializing context with settings");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invokeUnsafe<void>("initialize_context", {
|
await invokeUnsafe<void>("initialize_context", {
|
||||||
settings: tauriSettings,
|
settings: tauriSettings,
|
||||||
testnet,
|
testnet,
|
||||||
});
|
});
|
||||||
|
logger.info("Initialized context");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error("Couldn't initialize context: " + error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Initialized context");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWalletDescriptor() {
|
export async function getWalletDescriptor() {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,13 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { TauriLogEvent } from "models/tauriModel";
|
import { TauriLogEvent } from "models/tauriModel";
|
||||||
import { parseLogsFromString } from "utils/parseUtils";
|
import { parseLogsFromString } from "utils/parseUtils";
|
||||||
import { CliLog } from "models/cliModel";
|
import { CliLog } from "models/cliModel";
|
||||||
|
import { fnv1a } from "utils/hash";
|
||||||
|
|
||||||
|
/// We only keep the last 5000 logs in the store
|
||||||
|
const MAX_LOG_ENTRIES = 5000;
|
||||||
|
|
||||||
interface LogsState {
|
interface LogsState {
|
||||||
logs: (CliLog | string)[];
|
logs: HashedLog[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogsSlice {
|
export interface LogsSlice {
|
||||||
|
|
@ -17,21 +21,67 @@ const initialState: LogsSlice = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HashedLog = {
|
||||||
|
log: CliLog | string;
|
||||||
|
hash: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const logsSlice = createSlice({
|
export const logsSlice = createSlice({
|
||||||
name: "logs",
|
name: "logs",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
receivedCliLog(slice, action: PayloadAction<TauriLogEvent>) {
|
receivedCliLog(slice, action: PayloadAction<TauriLogEvent>) {
|
||||||
const buffer = action.payload.buffer;
|
const parsedLogs = parseLogsFromString(action.payload.buffer);
|
||||||
const logs = parseLogsFromString(buffer);
|
const hashedLogs = parsedLogs.map(createHashedLog);
|
||||||
const logsWithoutExisting = logs.filter(
|
for (const entry of hashedLogs) {
|
||||||
(log) => !slice.state.logs.includes(log),
|
slice.state.logs.push(entry);
|
||||||
);
|
}
|
||||||
slice.state.logs = slice.state.logs.concat(logsWithoutExisting);
|
|
||||||
|
// If we have too many logs, discard 1/10 of them (oldest logs)
|
||||||
|
// We explictly discard more than we need to, such that we don't have to
|
||||||
|
// do this too often
|
||||||
|
if (slice.state.logs.length > MAX_LOG_ENTRIES) {
|
||||||
|
const removeCount = Math.floor(slice.state.logs.length / 10);
|
||||||
|
slice.state.logs = slice.state.logs.slice(removeCount);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearLogs(slice) {
|
||||||
|
slice.state.logs = [];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { receivedCliLog } = logsSlice.actions;
|
function serializeLog(log: CliLog | string): string {
|
||||||
|
if (typeof log === "string") {
|
||||||
|
return `str:${log}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
"obj",
|
||||||
|
log.timestamp,
|
||||||
|
log.level,
|
||||||
|
log.target ?? "",
|
||||||
|
JSON.stringify(log.fields),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (log.spans != null && log.spans.length > 0) {
|
||||||
|
parts.push(JSON.stringify(log.spans));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHashedLog(log: CliLog | string): HashedLog {
|
||||||
|
return {
|
||||||
|
log,
|
||||||
|
hash: fnv1a(serializeLog(log)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hashLogs(logs: (CliLog | string)[]): HashedLog[] {
|
||||||
|
return logs.map(createHashedLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { receivedCliLog, clearLogs } = logsSlice.actions;
|
||||||
|
|
||||||
export default logsSlice.reducer;
|
export default logsSlice.reducer;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { ExtendedMakerStatus, MakerStatus } from "models/apiModel";
|
import { ExtendedMakerStatus, MakerStatus } from "models/apiModel";
|
||||||
import {
|
import {
|
||||||
GetSwapInfoResponse,
|
GetSwapInfoResponse,
|
||||||
TauriContextStatusEvent,
|
ContextStatus,
|
||||||
TauriTimelockChangeEvent,
|
TauriTimelockChangeEvent,
|
||||||
BackgroundRefundState,
|
BackgroundRefundState,
|
||||||
ApprovalRequest,
|
ApprovalRequest,
|
||||||
|
|
@ -37,8 +37,17 @@ interface State {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ContextStatusType {
|
||||||
|
Status = "status",
|
||||||
|
Error = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ResultContextStatus =
|
||||||
|
| { type: ContextStatusType.Status; status: ContextStatus }
|
||||||
|
| { type: ContextStatusType.Error; error: string };
|
||||||
|
|
||||||
export interface RPCSlice {
|
export interface RPCSlice {
|
||||||
status: TauriContextStatusEvent | null;
|
status: ResultContextStatus | null;
|
||||||
state: State;
|
state: State;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,11 +69,18 @@ export const rpcSlice = createSlice({
|
||||||
name: "rpc",
|
name: "rpc",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
contextStatusEventReceived(
|
contextStatusEventReceived(slice, action: PayloadAction<ContextStatus>) {
|
||||||
slice,
|
// Don't overwrite error state
|
||||||
action: PayloadAction<TauriContextStatusEvent>,
|
//
|
||||||
) {
|
// Once we're in an error state, stay there
|
||||||
slice.status = action.payload;
|
if (slice.status?.type === ContextStatusType.Error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
slice.status = { type: ContextStatusType.Status, status: action.payload };
|
||||||
|
},
|
||||||
|
contextInitializationFailed(slice, action: PayloadAction<string>) {
|
||||||
|
slice.status = { type: ContextStatusType.Error, error: action.payload };
|
||||||
},
|
},
|
||||||
timelockChangeEventReceived(
|
timelockChangeEventReceived(
|
||||||
slice: RPCSlice,
|
slice: RPCSlice,
|
||||||
|
|
@ -160,6 +176,7 @@ export const rpcSlice = createSlice({
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
contextStatusEventReceived,
|
contextStatusEventReceived,
|
||||||
|
contextInitializationFailed,
|
||||||
rpcSetBalance,
|
rpcSetBalance,
|
||||||
rpcSetWithdrawTxId,
|
rpcSetWithdrawTxId,
|
||||||
rpcResetWithdrawTxId,
|
rpcResetWithdrawTxId,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
isPendingSendMoneroApprovalEvent,
|
isPendingSendMoneroApprovalEvent,
|
||||||
PendingPasswordApprovalRequest,
|
PendingPasswordApprovalRequest,
|
||||||
isPendingPasswordApprovalEvent,
|
isPendingPasswordApprovalEvent,
|
||||||
|
isContextFullyInitialized,
|
||||||
} from "models/tauriModelExt";
|
} from "models/tauriModelExt";
|
||||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
||||||
|
|
@ -28,7 +29,6 @@ import { RatesState } from "./features/ratesSlice";
|
||||||
import {
|
import {
|
||||||
TauriBackgroundProgress,
|
TauriBackgroundProgress,
|
||||||
TauriBitcoinSyncProgress,
|
TauriBitcoinSyncProgress,
|
||||||
TauriContextStatusEvent,
|
|
||||||
} from "models/tauriModel";
|
} from "models/tauriModel";
|
||||||
|
|
||||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
|
|
@ -111,9 +111,7 @@ export function useIsSpecificSwapRunning(swapId: string | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsContextAvailable() {
|
export function useIsContextAvailable() {
|
||||||
return useAppSelector(
|
return useAppSelector((state) => isContextFullyInitialized(state.rpc.status));
|
||||||
(state) => state.rpc.status === TauriContextStatusEvent.Available,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// We do not use a sanity check here, as opposed to the other useSwapInfo hooks,
|
/// We do not use a sanity check here, as opposed to the other useSwapInfo hooks,
|
||||||
|
|
@ -139,10 +137,13 @@ export function useActiveSwapLogs() {
|
||||||
const swapId = useActiveSwapId();
|
const swapId = useActiveSwapId();
|
||||||
const logs = useAppSelector((s) => s.logs.state.logs);
|
const logs = useAppSelector((s) => s.logs.state.logs);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(() => {
|
||||||
() => logs.filter((log) => isCliLogRelatedToSwap(log, swapId)),
|
if (swapId == null) {
|
||||||
[logs, swapId],
|
return [];
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return logs.filter((log) => isCliLogRelatedToSwap(log.log, swapId));
|
||||||
|
}, [logs, swapId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAllMakers() {
|
export function useAllMakers() {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,10 @@ import {
|
||||||
getCurrentMoneroNodeConfig,
|
getCurrentMoneroNodeConfig,
|
||||||
} from "renderer/rpc";
|
} from "renderer/rpc";
|
||||||
import logger from "utils/logger";
|
import logger from "utils/logger";
|
||||||
import { contextStatusEventReceived } from "store/features/rpcSlice";
|
import {
|
||||||
|
contextStatusEventReceived,
|
||||||
|
ContextStatusType,
|
||||||
|
} from "store/features/rpcSlice";
|
||||||
import {
|
import {
|
||||||
addNode,
|
addNode,
|
||||||
setFetchFiatPrices,
|
setFetchFiatPrices,
|
||||||
|
|
@ -21,14 +24,12 @@ import {
|
||||||
Network,
|
Network,
|
||||||
} from "store/features/settingsSlice";
|
} from "store/features/settingsSlice";
|
||||||
import { fetchFeedbackMessagesViaHttp, updateRates } from "renderer/api";
|
import { fetchFeedbackMessagesViaHttp, updateRates } from "renderer/api";
|
||||||
import { store } from "renderer/store/storeRenderer";
|
import { RootState, store } from "renderer/store/storeRenderer";
|
||||||
import { swapProgressEventReceived } from "store/features/swapSlice";
|
import { swapProgressEventReceived } from "store/features/swapSlice";
|
||||||
import {
|
import {
|
||||||
addFeedbackId,
|
addFeedbackId,
|
||||||
setConversation,
|
setConversation,
|
||||||
} from "store/features/conversationsSlice";
|
} from "store/features/conversationsSlice";
|
||||||
import { TauriContextStatusEvent, MoneroNodeConfig } from "models/tauriModel";
|
|
||||||
import { getNetwork } from "store/config";
|
|
||||||
|
|
||||||
// Create a Map to store throttled functions per swap_id
|
// Create a Map to store throttled functions per swap_id
|
||||||
const throttledGetSwapInfoFunctions = new Map<
|
const throttledGetSwapInfoFunctions = new Map<
|
||||||
|
|
@ -60,30 +61,80 @@ const getThrottledSwapInfoUpdater = (swapId: string) => {
|
||||||
export function createMainListeners() {
|
export function createMainListeners() {
|
||||||
const listener = createListenerMiddleware();
|
const listener = createListenerMiddleware();
|
||||||
|
|
||||||
// Listener for when the Context becomes available
|
// Listener for when the Context status state changes
|
||||||
// When the context becomes available, we check the bitcoin balance, fetch all swap infos and connect to the rendezvous point
|
// When the context becomes available, we check the bitcoin balance, fetch all swap infos and connect to the rendezvous point
|
||||||
listener.startListening({
|
listener.startListening({
|
||||||
actionCreator: contextStatusEventReceived,
|
predicate: (action, currentState, previousState) => {
|
||||||
effect: async (action) => {
|
const currentStatus = (currentState as RootState).rpc.status;
|
||||||
const status = action.payload;
|
const previousStatus = (previousState as RootState).rpc.status;
|
||||||
|
|
||||||
// If the context is available, check the Bitcoin balance and fetch all swap infos
|
// Only trigger if the status actually changed
|
||||||
if (status === TauriContextStatusEvent.Available) {
|
return currentStatus !== previousStatus;
|
||||||
logger.debug(
|
},
|
||||||
"Context is available, checking Bitcoin balance and history",
|
effect: async (action, api) => {
|
||||||
|
const currentStatus = (api.getState() as RootState).rpc.status;
|
||||||
|
const previousStatus = (api.getOriginalState() as RootState).rpc.status;
|
||||||
|
|
||||||
|
const status =
|
||||||
|
currentStatus?.type === ContextStatusType.Status
|
||||||
|
? currentStatus.status
|
||||||
|
: null;
|
||||||
|
const previousContextStatus =
|
||||||
|
previousStatus?.type === ContextStatusType.Status
|
||||||
|
? previousStatus.status
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!status) return;
|
||||||
|
|
||||||
|
// If the Bitcoin wallet just came available, check the Bitcoin balance
|
||||||
|
if (
|
||||||
|
status.bitcoin_wallet_available &&
|
||||||
|
!previousContextStatus?.bitcoin_wallet_available
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
"Bitcoin wallet just became available, checking balance...",
|
||||||
);
|
);
|
||||||
await Promise.allSettled([
|
await checkBitcoinBalance();
|
||||||
checkBitcoinBalance(),
|
}
|
||||||
getAllSwapInfos(),
|
|
||||||
fetchSellersAtPresetRendezvousPoints(),
|
// If the Monero wallet just came available, initialize the Monero wallet
|
||||||
initializeMoneroWallet(),
|
if (
|
||||||
]);
|
status.monero_wallet_available &&
|
||||||
|
!previousContextStatus?.monero_wallet_available
|
||||||
|
) {
|
||||||
|
logger.info("Monero wallet just became available, initializing...");
|
||||||
|
await initializeMoneroWallet();
|
||||||
|
|
||||||
// Also set the Monero node to the current one
|
// Also set the Monero node to the current one
|
||||||
// In case the user changed this WHILE the context was unavailable
|
|
||||||
const nodeConfig = await getCurrentMoneroNodeConfig();
|
const nodeConfig = await getCurrentMoneroNodeConfig();
|
||||||
await changeMoneroNode(nodeConfig);
|
await changeMoneroNode(nodeConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the database and Bitcoin wallet just came available, fetch all swap infos
|
||||||
|
if (
|
||||||
|
status.database_available &&
|
||||||
|
status.bitcoin_wallet_available &&
|
||||||
|
!(
|
||||||
|
previousContextStatus?.database_available &&
|
||||||
|
previousContextStatus?.bitcoin_wallet_available
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
"Database & Bitcoin wallet just became available, fetching swap infos...",
|
||||||
|
);
|
||||||
|
await getAllSwapInfos();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the database just became availiable, fetch sellers at preset rendezvous points
|
||||||
|
if (
|
||||||
|
status.database_available &&
|
||||||
|
!previousContextStatus?.database_available
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
"Database just became available, fetching sellers at preset rendezvous points...",
|
||||||
|
);
|
||||||
|
await fetchSellersAtPresetRendezvousPoints();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -151,9 +202,9 @@ export function createMainListeners() {
|
||||||
try {
|
try {
|
||||||
const nodeConfig = await getCurrentMoneroNodeConfig();
|
const nodeConfig = await getCurrentMoneroNodeConfig();
|
||||||
await changeMoneroNode(nodeConfig);
|
await changeMoneroNode(nodeConfig);
|
||||||
logger.info("Changed Monero node configuration to: ", nodeConfig);
|
logger.info({ nodeConfig }, "Changed Monero node configuration to: ");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to change Monero node configuration:", error);
|
logger.error({ error }, "Failed to change Monero node configuration:");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
13
src-gui/src/utils/hash.ts
Normal file
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",
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
"identifier": "desktop-capability",
|
"identifier": "desktop-capability",
|
||||||
"description": "Capabilities for desktop windows",
|
"description": "Capabilities for desktop windows",
|
||||||
"platforms": [
|
"platforms": ["macOS", "windows", "linux"],
|
||||||
"macOS",
|
"windows": ["main"],
|
||||||
"windows",
|
"permissions": ["cli:default", "cli:allow-cli-matches"]
|
||||||
"linux"
|
}
|
||||||
],
|
|
||||||
"windows": [
|
|
||||||
"main"
|
|
||||||
],
|
|
||||||
"permissions": [
|
|
||||||
"cli:default",
|
|
||||||
"cli:allow-cli-matches"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,116 +1,116 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images": [
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"size": "20x20",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-20x20@2x.png",
|
"filename": "AppIcon-20x20@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"size": "20x20",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-20x20@3x.png",
|
"filename": "AppIcon-20x20@3x.png",
|
||||||
"scale" : "3x"
|
"scale": "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
"size": "29x29",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-29x29@2x-1.png",
|
"filename": "AppIcon-29x29@2x-1.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
"size": "29x29",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-29x29@3x.png",
|
"filename": "AppIcon-29x29@3x.png",
|
||||||
"scale" : "3x"
|
"scale": "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
"size": "40x40",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-40x40@2x.png",
|
"filename": "AppIcon-40x40@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
"size": "40x40",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-40x40@3x.png",
|
"filename": "AppIcon-40x40@3x.png",
|
||||||
"scale" : "3x"
|
"scale": "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "60x60",
|
"size": "60x60",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-60x60@2x.png",
|
"filename": "AppIcon-60x60@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "60x60",
|
"size": "60x60",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-60x60@3x.png",
|
"filename": "AppIcon-60x60@3x.png",
|
||||||
"scale" : "3x"
|
"scale": "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"size": "20x20",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-20x20@1x.png",
|
"filename": "AppIcon-20x20@1x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"size": "20x20",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-20x20@2x-1.png",
|
"filename": "AppIcon-20x20@2x-1.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
"size": "29x29",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-29x29@1x.png",
|
"filename": "AppIcon-29x29@1x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
"size": "29x29",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-29x29@2x.png",
|
"filename": "AppIcon-29x29@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
"size": "40x40",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-40x40@1x.png",
|
"filename": "AppIcon-40x40@1x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
"size": "40x40",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-40x40@2x-1.png",
|
"filename": "AppIcon-40x40@2x-1.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "76x76",
|
"size": "76x76",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-76x76@1x.png",
|
"filename": "AppIcon-76x76@1x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "76x76",
|
"size": "76x76",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-76x76@2x.png",
|
"filename": "AppIcon-76x76@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "83.5x83.5",
|
"size": "83.5x83.5",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-83.5x83.5@2x.png",
|
"filename": "AppIcon-83.5x83.5@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "1024x1024",
|
"size": "1024x1024",
|
||||||
"idiom" : "ios-marketing",
|
"idiom": "ios-marketing",
|
||||||
"filename" : "AppIcon-512@2x.png",
|
"filename": "AppIcon-512@2x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info": {
|
||||||
"version" : 1,
|
"version": 1,
|
||||||
"author" : "xcode"
|
"author": "xcode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"info" : {
|
"info": {
|
||||||
"version" : 1,
|
"version": 1,
|
||||||
"author" : "xcode"
|
"author": "xcode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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::result::Result;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use swap::cli::{
|
use swap::cli::api::{tauri_bindings::TauriHandle, Context};
|
||||||
api::{
|
use tauri::{Manager, RunEvent};
|
||||||
data,
|
use tokio::sync::Mutex;
|
||||||
request::{
|
|
||||||
BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ChangeMoneroNodeArgs,
|
|
||||||
CheckElectrumNodeArgs, CheckElectrumNodeResponse, CheckMoneroNodeArgs,
|
|
||||||
CheckMoneroNodeResponse, CheckSeedArgs, CheckSeedResponse, DfxAuthenticateResponse,
|
|
||||||
ExportBitcoinWalletArgs, GetCurrentSwapArgs, GetDataDirArgs, GetHistoryArgs,
|
|
||||||
GetLogsArgs, GetMoneroAddressesArgs, GetMoneroBalanceArgs, GetMoneroHistoryArgs,
|
|
||||||
GetMoneroMainAddressArgs, GetMoneroSeedArgs, GetMoneroSyncProgressArgs,
|
|
||||||
GetPendingApprovalsResponse, GetRestoreHeightArgs, GetSwapInfoArgs,
|
|
||||||
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, RedactArgs,
|
|
||||||
RejectApprovalArgs, RejectApprovalResponse, ResolveApprovalArgs, ResumeSwapArgs,
|
|
||||||
SendMoneroArgs, SetRestoreHeightArgs, SuspendCurrentSwapArgs, WithdrawBtcArgs,
|
|
||||||
},
|
|
||||||
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
|
|
||||||
Context, ContextBuilder,
|
|
||||||
},
|
|
||||||
command::Bitcoin,
|
|
||||||
};
|
|
||||||
use tauri::{async_runtime::RwLock, Manager, RunEvent};
|
|
||||||
use tauri_plugin_dialog::DialogExt;
|
|
||||||
use zip::{write::SimpleFileOptions, ZipWriter};
|
|
||||||
|
|
||||||
/// Trait to convert Result<T, E> to Result<T, String>
|
mod commands;
|
||||||
/// Tauri commands require the error type to be a string
|
|
||||||
trait ToStringResult<T> {
|
|
||||||
fn to_string_result(self) -> Result<T, String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T, E: ToString> ToStringResult<T> for Result<T, E> {
|
use commands::*;
|
||||||
fn to_string_result(self) -> Result<T, String> {
|
|
||||||
self.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This macro is used to create boilerplate functions as tauri commands
|
|
||||||
/// that simply delegate handling to the respective request type.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```ignored
|
|
||||||
/// tauri_command!(get_balance, BalanceArgs);
|
|
||||||
/// ```
|
|
||||||
/// will resolve to
|
|
||||||
/// ```ignored
|
|
||||||
/// #[tauri::command]
|
|
||||||
/// async fn get_balance(context: tauri::State<'...>, args: BalanceArgs) -> Result<BalanceArgs::Response, String> {
|
|
||||||
/// args.handle(context.inner().clone()).await.to_string_result()
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// # Example 2
|
|
||||||
/// ```ignored
|
|
||||||
/// tauri_command!(get_balance, BalanceArgs, no_args);
|
|
||||||
/// ```
|
|
||||||
/// will resolve to
|
|
||||||
/// ```ignored
|
|
||||||
/// #[tauri::command]
|
|
||||||
/// async fn get_balance(context: tauri::State<'...>) -> Result<BalanceArgs::Response, String> {
|
|
||||||
/// BalanceArgs {}.handle(context.inner().clone()).await.to_string_result()
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
macro_rules! tauri_command {
|
|
||||||
($fn_name:ident, $request_name:ident) => {
|
|
||||||
#[tauri::command]
|
|
||||||
async fn $fn_name(
|
|
||||||
state: tauri::State<'_, State>,
|
|
||||||
args: $request_name,
|
|
||||||
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
|
||||||
// Throw error if context is not available
|
|
||||||
let context = state.try_get_context()?;
|
|
||||||
|
|
||||||
<$request_name as swap::cli::api::request::Request>::request(args, context)
|
|
||||||
.await
|
|
||||||
.to_string_result()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($fn_name:ident, $request_name:ident, no_args) => {
|
|
||||||
#[tauri::command]
|
|
||||||
async fn $fn_name(
|
|
||||||
state: tauri::State<'_, State>,
|
|
||||||
) -> Result<<$request_name as swap::cli::api::request::Request>::Response, String> {
|
|
||||||
// Throw error if context is not available
|
|
||||||
let context = state.try_get_context()?;
|
|
||||||
|
|
||||||
<$request_name as swap::cli::api::request::Request>::request($request_name {}, context)
|
|
||||||
.await
|
|
||||||
.to_string_result()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
/// Represents the shared Tauri state. It is accessed by Tauri commands
|
||||||
struct State {
|
struct State {
|
||||||
pub context: RwLock<Option<Arc<Context>>>,
|
pub context: Arc<Context>,
|
||||||
|
/// Whenever someone wants to modify the context, they should acquire this lock
|
||||||
|
///
|
||||||
|
/// [`Context`] uses RwLock internally which means we do not need write access to the context
|
||||||
|
/// to modify its internal state.
|
||||||
|
///
|
||||||
|
/// However, we want to avoid multiple processes intializing the context at the same time.
|
||||||
|
pub context_lock: Mutex<()>,
|
||||||
pub handle: TauriHandle,
|
pub handle: TauriHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
/// Creates a new State instance with no Context
|
/// Creates a new State instance with no Context
|
||||||
fn new(handle: TauriHandle) -> Self {
|
fn new(handle: TauriHandle) -> Self {
|
||||||
|
let context = Arc::new(Context::new_with_tauri_handle(handle.clone()));
|
||||||
|
let context_lock = Mutex::new(());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
context: RwLock::new(None),
|
context,
|
||||||
|
context_lock,
|
||||||
handle,
|
handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to retrieve the context
|
/// Attempts to retrieve the context
|
||||||
/// Returns an error if the context is not available
|
/// Returns an error if the context is not available
|
||||||
fn try_get_context(&self) -> Result<Arc<Context>, String> {
|
fn context(&self) -> Arc<Context> {
|
||||||
self.context
|
self.context.clone()
|
||||||
.try_read()
|
|
||||||
.map_err(|_| "Context is being modified".to_string())?
|
|
||||||
.clone()
|
|
||||||
.ok_or("Context not available".to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,44 +99,7 @@ pub fn run() {
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(generate_command_handlers!())
|
||||||
get_balance,
|
|
||||||
get_monero_addresses,
|
|
||||||
get_swap_info,
|
|
||||||
get_swap_infos_all,
|
|
||||||
withdraw_btc,
|
|
||||||
buy_xmr,
|
|
||||||
resume_swap,
|
|
||||||
get_history,
|
|
||||||
monero_recovery,
|
|
||||||
get_logs,
|
|
||||||
list_sellers,
|
|
||||||
suspend_current_swap,
|
|
||||||
cancel_and_refund,
|
|
||||||
is_context_available,
|
|
||||||
initialize_context,
|
|
||||||
check_monero_node,
|
|
||||||
check_electrum_node,
|
|
||||||
get_wallet_descriptor,
|
|
||||||
get_current_swap,
|
|
||||||
get_data_dir,
|
|
||||||
resolve_approval_request,
|
|
||||||
redact,
|
|
||||||
save_txt_files,
|
|
||||||
get_monero_history,
|
|
||||||
get_monero_main_address,
|
|
||||||
get_monero_balance,
|
|
||||||
send_monero,
|
|
||||||
get_monero_sync_progress,
|
|
||||||
get_monero_seed,
|
|
||||||
check_seed,
|
|
||||||
get_pending_approvals,
|
|
||||||
set_monero_restore_height,
|
|
||||||
reject_approval_request,
|
|
||||||
get_restore_height,
|
|
||||||
dfx_authenticate,
|
|
||||||
change_monero_node,
|
|
||||||
])
|
|
||||||
.setup(setup)
|
.setup(setup)
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
|
|
@ -225,335 +110,13 @@ pub fn run() {
|
||||||
// If the application is forcibly closed, this may not be called.
|
// If the application is forcibly closed, this may not be called.
|
||||||
// TODO: fix that
|
// TODO: fix that
|
||||||
let state = app.state::<State>();
|
let state = app.state::<State>();
|
||||||
let context_to_cleanup = if let Ok(context_lock) = state.context.try_read() {
|
let lock = state.context_lock.try_lock();
|
||||||
context_lock.clone()
|
if let Ok(_) = lock {
|
||||||
} else {
|
if let Err(e) = state.context().cleanup() {
|
||||||
println!("Failed to acquire lock on context");
|
println!("Failed to cleanup context: {}", e);
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(context) = context_to_cleanup {
|
|
||||||
if let Err(err) = context.cleanup() {
|
|
||||||
println!("Cleanup failed {}", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here we define the Tauri commands that will be available to the frontend
|
|
||||||
// The commands are defined using the `tauri_command!` macro.
|
|
||||||
// Implementations are handled by the Request trait
|
|
||||||
tauri_command!(get_balance, BalanceArgs);
|
|
||||||
tauri_command!(buy_xmr, BuyXmrArgs);
|
|
||||||
tauri_command!(resume_swap, ResumeSwapArgs);
|
|
||||||
tauri_command!(withdraw_btc, WithdrawBtcArgs);
|
|
||||||
tauri_command!(monero_recovery, MoneroRecoveryArgs);
|
|
||||||
tauri_command!(get_logs, GetLogsArgs);
|
|
||||||
tauri_command!(list_sellers, ListSellersArgs);
|
|
||||||
tauri_command!(cancel_and_refund, CancelAndRefundArgs);
|
|
||||||
tauri_command!(redact, RedactArgs);
|
|
||||||
tauri_command!(send_monero, SendMoneroArgs);
|
|
||||||
tauri_command!(change_monero_node, ChangeMoneroNodeArgs);
|
|
||||||
|
|
||||||
// These commands require no arguments
|
|
||||||
tauri_command!(get_wallet_descriptor, ExportBitcoinWalletArgs, no_args);
|
|
||||||
tauri_command!(suspend_current_swap, SuspendCurrentSwapArgs, no_args);
|
|
||||||
tauri_command!(get_swap_info, GetSwapInfoArgs);
|
|
||||||
tauri_command!(get_swap_infos_all, GetSwapInfosAllArgs, no_args);
|
|
||||||
tauri_command!(get_history, GetHistoryArgs, no_args);
|
|
||||||
tauri_command!(get_monero_addresses, GetMoneroAddressesArgs, no_args);
|
|
||||||
tauri_command!(get_monero_history, GetMoneroHistoryArgs, no_args);
|
|
||||||
tauri_command!(get_current_swap, GetCurrentSwapArgs, no_args);
|
|
||||||
tauri_command!(set_monero_restore_height, SetRestoreHeightArgs);
|
|
||||||
tauri_command!(get_restore_height, GetRestoreHeightArgs, no_args);
|
|
||||||
tauri_command!(get_monero_main_address, GetMoneroMainAddressArgs, no_args);
|
|
||||||
tauri_command!(get_monero_balance, GetMoneroBalanceArgs, no_args);
|
|
||||||
tauri_command!(get_monero_sync_progress, GetMoneroSyncProgressArgs, no_args);
|
|
||||||
tauri_command!(get_monero_seed, GetMoneroSeedArgs, no_args);
|
|
||||||
|
|
||||||
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
|
|
||||||
#[tauri::command]
|
|
||||||
async fn is_context_available(state: tauri::State<'_, State>) -> Result<bool, String> {
|
|
||||||
// TODO: Here we should return more information about status of the context (e.g. initializing, failed)
|
|
||||||
Ok(state.try_get_context().is_ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn check_monero_node(
|
|
||||||
args: CheckMoneroNodeArgs,
|
|
||||||
_: tauri::State<'_, State>,
|
|
||||||
) -> Result<CheckMoneroNodeResponse, String> {
|
|
||||||
args.request().await.to_string_result()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn check_electrum_node(
|
|
||||||
args: CheckElectrumNodeArgs,
|
|
||||||
_: tauri::State<'_, State>,
|
|
||||||
) -> Result<CheckElectrumNodeResponse, String> {
|
|
||||||
args.request().await.to_string_result()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn check_seed(
|
|
||||||
args: CheckSeedArgs,
|
|
||||||
_: tauri::State<'_, State>,
|
|
||||||
) -> Result<CheckSeedResponse, String> {
|
|
||||||
args.request().await.to_string_result()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the data directory
|
|
||||||
// This is independent of the context to ensure the user can open the directory even if the context cannot
|
|
||||||
// be initialized (for troubleshooting purposes)
|
|
||||||
#[tauri::command]
|
|
||||||
async fn get_data_dir(args: GetDataDirArgs, _: tauri::State<'_, State>) -> Result<String, String> {
|
|
||||||
Ok(data::data_dir_from(None, args.is_testnet)
|
|
||||||
.to_string_result()?
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn save_txt_files(
|
|
||||||
app: tauri::AppHandle,
|
|
||||||
zip_file_name: String,
|
|
||||||
content: HashMap<String, String>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
// Step 1: Get the owned PathBuf from the dialog
|
|
||||||
let path_buf_from_dialog: tauri_plugin_dialog::FilePath = app
|
|
||||||
.dialog()
|
|
||||||
.file()
|
|
||||||
.set_file_name(format!("{}.zip", &zip_file_name).as_str())
|
|
||||||
.add_filter(&zip_file_name, &["zip"])
|
|
||||||
.blocking_save_file() // This returns Option<PathBuf>
|
|
||||||
.ok_or_else(|| "Dialog cancelled or file path not selected".to_string())?; // Converts to Result<PathBuf, String> and unwraps to PathBuf
|
|
||||||
|
|
||||||
// Step 2: Now get a &Path reference from the owned PathBuf.
|
|
||||||
// The user's code structure implied an .as_path().ok_or_else(...) chain which was incorrect for &Path.
|
|
||||||
// We'll directly use the PathBuf, or if &Path is strictly needed:
|
|
||||||
let selected_file_path: &std::path::Path = path_buf_from_dialog
|
|
||||||
.as_path()
|
|
||||||
.ok_or_else(|| "Could not convert file path".to_string())?;
|
|
||||||
|
|
||||||
let zip_file = std::fs::File::create(selected_file_path)
|
|
||||||
.map_err(|e| format!("Failed to create file: {}", e))?;
|
|
||||||
|
|
||||||
let mut zip = ZipWriter::new(zip_file);
|
|
||||||
|
|
||||||
for (filename, file_content_str) in content.iter() {
|
|
||||||
zip.start_file(
|
|
||||||
format!("{}.txt", filename).as_str(),
|
|
||||||
SimpleFileOptions::default(),
|
|
||||||
) // Pass &str to start_file
|
|
||||||
.map_err(|e| format!("Failed to start file {}: {}", &filename, e))?; // Use &filename
|
|
||||||
|
|
||||||
zip.write_all(file_content_str.as_bytes())
|
|
||||||
.map_err(|e| format!("Failed to write to file {}: {}", &filename, e))?;
|
|
||||||
// Use &filename
|
|
||||||
}
|
|
||||||
|
|
||||||
zip.finish()
|
|
||||||
.map_err(|e| format!("Failed to finish zip: {}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn resolve_approval_request(
|
|
||||||
args: ResolveApprovalArgs,
|
|
||||||
state: tauri::State<'_, State>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let request_id = args
|
|
||||||
.request_id
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| format!("Invalid request ID '{}': {}", args.request_id, e))?;
|
|
||||||
|
|
||||||
state
|
|
||||||
.handle
|
|
||||||
.resolve_approval(request_id, args.accept)
|
|
||||||
.await
|
|
||||||
.to_string_result()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn reject_approval_request(
|
|
||||||
args: RejectApprovalArgs,
|
|
||||||
state: tauri::State<'_, State>,
|
|
||||||
) -> Result<RejectApprovalResponse, String> {
|
|
||||||
let request_id = args
|
|
||||||
.request_id
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| format!("Invalid request ID '{}': {}", args.request_id, e))?;
|
|
||||||
|
|
||||||
state
|
|
||||||
.handle
|
|
||||||
.reject_approval(request_id)
|
|
||||||
.await
|
|
||||||
.to_string_result()?;
|
|
||||||
|
|
||||||
Ok(RejectApprovalResponse { success: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn get_pending_approvals(
|
|
||||||
state: tauri::State<'_, State>,
|
|
||||||
) -> Result<GetPendingApprovalsResponse, String> {
|
|
||||||
let approvals = state
|
|
||||||
.handle
|
|
||||||
.get_pending_approvals()
|
|
||||||
.await
|
|
||||||
.to_string_result()?;
|
|
||||||
|
|
||||||
Ok(GetPendingApprovalsResponse { approvals })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tauri command to initialize the Context
|
|
||||||
#[tauri::command]
|
|
||||||
async fn initialize_context(
|
|
||||||
settings: TauriSettings,
|
|
||||||
testnet: bool,
|
|
||||||
state: tauri::State<'_, State>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
// Lock at the beginning - fail immediately if already locked
|
|
||||||
let mut context_lock = state
|
|
||||||
.context
|
|
||||||
.try_write()
|
|
||||||
.map_err(|_| "Context is already being initialized".to_string())?;
|
|
||||||
|
|
||||||
// Fail if the context is already initialized
|
|
||||||
if context_lock.is_some() {
|
|
||||||
return Err("Context is already initialized".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tauri handle from the state
|
|
||||||
let tauri_handle = state.handle.clone();
|
|
||||||
|
|
||||||
// Notify frontend that the context is being initialized
|
|
||||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Initializing);
|
|
||||||
|
|
||||||
let context_result = ContextBuilder::new(testnet)
|
|
||||||
.with_bitcoin(Bitcoin {
|
|
||||||
bitcoin_electrum_rpc_urls: settings.electrum_rpc_urls.clone(),
|
|
||||||
bitcoin_target_block: None,
|
|
||||||
})
|
|
||||||
.with_monero(settings.monero_node_config)
|
|
||||||
.with_json(false)
|
|
||||||
.with_debug(true)
|
|
||||||
.with_tor(settings.use_tor)
|
|
||||||
.with_enable_monero_tor(settings.enable_monero_tor)
|
|
||||||
.with_tauri(tauri_handle.clone())
|
|
||||||
.build()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match context_result {
|
|
||||||
Ok(context_instance) => {
|
|
||||||
*context_lock = Some(Arc::new(context_instance));
|
|
||||||
|
|
||||||
tracing::info!("Context initialized");
|
|
||||||
|
|
||||||
// Emit event to frontend
|
|
||||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(error = ?e, "Failed to initialize context");
|
|
||||||
|
|
||||||
// Emit event to frontend
|
|
||||||
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Failed);
|
|
||||||
Err(e.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn dfx_authenticate(
|
|
||||||
state: tauri::State<'_, State>,
|
|
||||||
) -> Result<DfxAuthenticateResponse, String> {
|
|
||||||
use dfx_swiss_sdk::{DfxClient, SignRequest};
|
|
||||||
use tokio::sync::{mpsc, oneshot};
|
|
||||||
use tokio_util::task::AbortOnDropHandle;
|
|
||||||
|
|
||||||
let context = state.try_get_context()?;
|
|
||||||
|
|
||||||
// Get the monero wallet manager
|
|
||||||
let monero_manager = context
|
|
||||||
.monero_manager
|
|
||||||
.as_ref()
|
|
||||||
.ok_or("Monero wallet manager not available for DFX authentication")?;
|
|
||||||
|
|
||||||
let wallet = monero_manager.main_wallet().await;
|
|
||||||
let address = wallet.main_address().await.to_string();
|
|
||||||
|
|
||||||
// Create channel for authentication
|
|
||||||
let (auth_tx, mut auth_rx) = mpsc::channel::<(SignRequest, oneshot::Sender<String>)>(10);
|
|
||||||
|
|
||||||
// Create DFX client
|
|
||||||
let mut client = DfxClient::new(address, Some("https://api.dfx.swiss".to_string()), auth_tx);
|
|
||||||
|
|
||||||
// Start signing task with AbortOnDropHandle
|
|
||||||
let signing_task = tokio::spawn(async move {
|
|
||||||
tracing::info!("DFX signing service started and listening for requests");
|
|
||||||
|
|
||||||
while let Some((sign_request, response_tx)) = auth_rx.recv().await {
|
|
||||||
tracing::debug!(
|
|
||||||
message = %sign_request.message,
|
|
||||||
blockchains = ?sign_request.blockchains,
|
|
||||||
"Received DFX signing request"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sign the message using the main Monero wallet
|
|
||||||
let signature = match wallet
|
|
||||||
.sign_message(&sign_request.message, None, false)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(sig) => {
|
|
||||||
tracing::debug!(
|
|
||||||
signature_preview = %&sig[..std::cmp::min(50, sig.len())],
|
|
||||||
"Message signed successfully for DFX"
|
|
||||||
);
|
|
||||||
sig
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!(error = ?e, "Failed to sign message for DFX");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send signature back to DFX client
|
|
||||||
if let Err(_) = response_tx.send(signature) {
|
|
||||||
tracing::warn!("Failed to send signature response through channel to DFX client");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!("DFX signing service stopped");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create AbortOnDropHandle so the task gets cleaned up
|
|
||||||
let _abort_handle = AbortOnDropHandle::new(signing_task);
|
|
||||||
|
|
||||||
// Authenticate with DFX
|
|
||||||
tracing::info!("Starting DFX authentication...");
|
|
||||||
client
|
|
||||||
.authenticate()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to authenticate with DFX: {}", e))?;
|
|
||||||
|
|
||||||
let access_token = client
|
|
||||||
.access_token
|
|
||||||
.as_ref()
|
|
||||||
.ok_or("No access token available after authentication")?
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
let kyc_url = format!("https://app.dfx.swiss/buy?session={}", access_token);
|
|
||||||
|
|
||||||
tracing::info!("DFX authentication completed successfully");
|
|
||||||
|
|
||||||
Ok(DfxAuthenticateResponse {
|
|
||||||
access_token,
|
|
||||||
kyc_url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
||||||
1841
swap/src/cli/api.rs
1841
swap/src/cli/api.rs
File diff suppressed because it is too large
Load diff
|
|
@ -421,7 +421,8 @@ impl Request for GetLogsArgs {
|
||||||
type Response = GetLogsResponse;
|
type Response = GetLogsResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let dir = self.logs_dir.unwrap_or(ctx.config.log_dir.clone());
|
let config = ctx.try_get_config().await?;
|
||||||
|
let dir = self.logs_dir.unwrap_or(config.log_dir.clone());
|
||||||
let logs = get_logs(dir, self.swap_id, self.redact).await?;
|
let logs = get_logs(dir, self.swap_id, self.redact).await?;
|
||||||
|
|
||||||
for msg in &logs {
|
for msg in &logs {
|
||||||
|
|
@ -470,11 +471,8 @@ impl Request for GetRestoreHeightArgs {
|
||||||
type Response = GetRestoreHeightResponse;
|
type Response = GetRestoreHeightResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let wallet = ctx
|
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||||
.monero_manager
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
.as_ref()
|
|
||||||
.context("Monero wallet manager not available")?;
|
|
||||||
let wallet = wallet.main_wallet().await;
|
|
||||||
let height = wallet.get_restore_height().await?;
|
let height = wallet.get_restore_height().await?;
|
||||||
|
|
||||||
Ok(GetRestoreHeightResponse { height })
|
Ok(GetRestoreHeightResponse { height })
|
||||||
|
|
@ -496,7 +494,8 @@ impl Request for GetMoneroAddressesArgs {
|
||||||
type Response = GetMoneroAddressesResponse;
|
type Response = GetMoneroAddressesResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let addresses = ctx.db.get_monero_addresses().await?;
|
let db = ctx.try_get_db().await?;
|
||||||
|
let addresses = db.get_monero_addresses().await?;
|
||||||
Ok(GetMoneroAddressesResponse { addresses })
|
Ok(GetMoneroAddressesResponse { addresses })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -515,11 +514,8 @@ impl Request for GetMoneroHistoryArgs {
|
||||||
type Response = GetMoneroHistoryResponse;
|
type Response = GetMoneroHistoryResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let wallet = ctx
|
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||||
.monero_manager
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
.as_ref()
|
|
||||||
.context("Monero wallet manager not available")?;
|
|
||||||
let wallet = wallet.main_wallet().await;
|
|
||||||
|
|
||||||
let transactions = wallet.history().await;
|
let transactions = wallet.history().await;
|
||||||
Ok(GetMoneroHistoryResponse { transactions })
|
Ok(GetMoneroHistoryResponse { transactions })
|
||||||
|
|
@ -541,11 +537,8 @@ impl Request for GetMoneroMainAddressArgs {
|
||||||
type Response = GetMoneroMainAddressResponse;
|
type Response = GetMoneroMainAddressResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let wallet = ctx
|
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||||
.monero_manager
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
.as_ref()
|
|
||||||
.context("Monero wallet manager not available")?;
|
|
||||||
let wallet = wallet.main_wallet().await;
|
|
||||||
let address = wallet.main_address().await;
|
let address = wallet.main_address().await;
|
||||||
Ok(GetMoneroMainAddressResponse { address })
|
Ok(GetMoneroMainAddressResponse { address })
|
||||||
}
|
}
|
||||||
|
|
@ -582,11 +575,8 @@ impl Request for SetRestoreHeightArgs {
|
||||||
type Response = SetRestoreHeightResponse;
|
type Response = SetRestoreHeightResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let wallet = ctx
|
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||||
.monero_manager
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
.as_ref()
|
|
||||||
.context("Monero wallet manager not available")?;
|
|
||||||
let wallet = wallet.main_wallet().await;
|
|
||||||
|
|
||||||
let height = match self {
|
let height = match self {
|
||||||
SetRestoreHeightArgs::Height(height) => height as u64,
|
SetRestoreHeightArgs::Height(height) => height as u64,
|
||||||
|
|
@ -661,10 +651,7 @@ impl Request for GetMoneroBalanceArgs {
|
||||||
type Response = GetMoneroBalanceResponse;
|
type Response = GetMoneroBalanceResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let wallet_manager = ctx
|
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||||
.monero_manager
|
|
||||||
.as_ref()
|
|
||||||
.context("Monero wallet manager not available")?;
|
|
||||||
let wallet = wallet_manager.main_wallet().await;
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
|
|
||||||
let total_balance = wallet.total_balance().await;
|
let total_balance = wallet.total_balance().await;
|
||||||
|
|
@ -706,10 +693,7 @@ impl Request for SendMoneroArgs {
|
||||||
type Response = SendMoneroResponse;
|
type Response = SendMoneroResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let wallet_manager = ctx
|
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||||
.monero_manager
|
|
||||||
.as_ref()
|
|
||||||
.context("Monero wallet manager not available")?;
|
|
||||||
let wallet = wallet_manager.main_wallet().await;
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
|
|
||||||
// Parse the address
|
// Parse the address
|
||||||
|
|
@ -717,7 +701,8 @@ impl Request for SendMoneroArgs {
|
||||||
.map_err(|e| anyhow::anyhow!("Invalid Monero address: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Invalid Monero address: {}", e))?;
|
||||||
|
|
||||||
let tauri_handle = ctx
|
let tauri_handle = ctx
|
||||||
.tauri_handle()
|
.tauri_handle
|
||||||
|
.clone()
|
||||||
.context("Tauri needs to be available to approve transactions")?;
|
.context("Tauri needs to be available to approve transactions")?;
|
||||||
|
|
||||||
// This is a closure that will be called by the monero-sys library to get approval for the transaction
|
// This is a closure that will be called by the monero-sys library to get approval for the transaction
|
||||||
|
|
@ -793,7 +778,8 @@ pub async fn suspend_current_swap(context: Arc<Context>) -> Result<SuspendCurren
|
||||||
|
|
||||||
#[tracing::instrument(fields(method = "get_swap_infos_all"), skip(context))]
|
#[tracing::instrument(fields(method = "get_swap_infos_all"), skip(context))]
|
||||||
pub async fn get_swap_infos_all(context: Arc<Context>) -> Result<Vec<GetSwapInfoResponse>> {
|
pub async fn get_swap_infos_all(context: Arc<Context>) -> Result<Vec<GetSwapInfoResponse>> {
|
||||||
let swap_ids = context.db.all().await?;
|
let db = context.try_get_db().await?;
|
||||||
|
let swap_ids = db.all().await?;
|
||||||
let mut swap_infos = Vec::new();
|
let mut swap_infos = Vec::new();
|
||||||
|
|
||||||
for (swap_id, _) in swap_ids {
|
for (swap_id, _) in swap_ids {
|
||||||
|
|
@ -813,27 +799,23 @@ pub async fn get_swap_info(
|
||||||
args: GetSwapInfoArgs,
|
args: GetSwapInfoArgs,
|
||||||
context: Arc<Context>,
|
context: Arc<Context>,
|
||||||
) -> Result<GetSwapInfoResponse> {
|
) -> Result<GetSwapInfoResponse> {
|
||||||
let bitcoin_wallet = context
|
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||||
.bitcoin_wallet
|
let db = context.try_get_db().await?;
|
||||||
.as_ref()
|
|
||||||
.context("Could not get Bitcoin wallet")?;
|
|
||||||
|
|
||||||
let state = context.db.get_state(args.swap_id).await?;
|
let state = db.get_state(args.swap_id).await?;
|
||||||
let is_completed = state.swap_finished();
|
let is_completed = state.swap_finished();
|
||||||
|
|
||||||
let peer_id = context
|
let peer_id = db
|
||||||
.db
|
|
||||||
.get_peer_id(args.swap_id)
|
.get_peer_id(args.swap_id)
|
||||||
.await
|
.await
|
||||||
.with_context(|| "Could not get PeerID")?;
|
.with_context(|| "Could not get PeerID")?;
|
||||||
|
|
||||||
let addresses = context
|
let addresses = db
|
||||||
.db
|
|
||||||
.get_addresses(peer_id)
|
.get_addresses(peer_id)
|
||||||
.await
|
.await
|
||||||
.with_context(|| "Could not get addressess")?;
|
.with_context(|| "Could not get addressess")?;
|
||||||
|
|
||||||
let start_date = context.db.get_swap_start_date(args.swap_id).await?;
|
let start_date = db.get_swap_start_date(args.swap_id).await?;
|
||||||
|
|
||||||
let swap_state: BobState = state.try_into()?;
|
let swap_state: BobState = state.try_into()?;
|
||||||
|
|
||||||
|
|
@ -847,8 +829,7 @@ pub async fn get_swap_info(
|
||||||
btc_refund_address,
|
btc_refund_address,
|
||||||
cancel_timelock,
|
cancel_timelock,
|
||||||
punish_timelock,
|
punish_timelock,
|
||||||
) = context
|
) = db
|
||||||
.db
|
|
||||||
.get_states(args.swap_id)
|
.get_states(args.swap_id)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -884,7 +865,7 @@ pub async fn get_swap_info(
|
||||||
|
|
||||||
let timelock = swap_state.expired_timelocks(bitcoin_wallet.clone()).await?;
|
let timelock = swap_state.expired_timelocks(bitcoin_wallet.clone()).await?;
|
||||||
|
|
||||||
let monero_receive_pool = context.db.get_monero_address_pool(args.swap_id).await?;
|
let monero_receive_pool = db.get_monero_address_pool(args.swap_id).await?;
|
||||||
|
|
||||||
Ok(GetSwapInfoResponse {
|
Ok(GetSwapInfoResponse {
|
||||||
swap_id: args.swap_id,
|
swap_id: args.swap_id,
|
||||||
|
|
@ -924,15 +905,13 @@ pub async fn buy_xmr(
|
||||||
monero_receive_pool,
|
monero_receive_pool,
|
||||||
} = buy_xmr;
|
} = buy_xmr;
|
||||||
|
|
||||||
monero_receive_pool.assert_network(context.config.env_config.monero_network)?;
|
let config = context.try_get_config().await?;
|
||||||
|
let db = context.try_get_db().await?;
|
||||||
|
|
||||||
|
monero_receive_pool.assert_network(config.env_config.monero_network)?;
|
||||||
monero_receive_pool.assert_sum_to_one()?;
|
monero_receive_pool.assert_sum_to_one()?;
|
||||||
|
|
||||||
let bitcoin_wallet = Arc::clone(
|
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||||
context
|
|
||||||
.bitcoin_wallet
|
|
||||||
.as_ref()
|
|
||||||
.expect("Could not find Bitcoin wallet"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let bitcoin_change_address = match bitcoin_change_address {
|
let bitcoin_change_address = match bitcoin_change_address {
|
||||||
Some(addr) => addr
|
Some(addr) => addr
|
||||||
|
|
@ -950,21 +929,15 @@ pub async fn buy_xmr(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let monero_wallet = Arc::clone(
|
let monero_wallet = context.try_get_monero_manager().await?;
|
||||||
context
|
|
||||||
.monero_manager
|
|
||||||
.as_ref()
|
|
||||||
.context("Could not get Monero wallet")?,
|
|
||||||
);
|
|
||||||
|
|
||||||
let env_config = context.config.env_config;
|
let env_config = config.env_config;
|
||||||
let seed = context.config.seed.clone().context("Could not get seed")?;
|
let seed = config.seed.clone().context("Could not get seed")?;
|
||||||
|
|
||||||
// Prepare variables for the quote fetching process
|
// Prepare variables for the quote fetching process
|
||||||
let identity = seed.derive_libp2p_identity();
|
let identity = seed.derive_libp2p_identity();
|
||||||
let namespace = context.config.namespace;
|
let namespace = config.namespace;
|
||||||
let tor_client = context.tor_client.clone();
|
let tor_client = context.tor_client.read().await.clone();
|
||||||
let db = Some(context.db.clone());
|
|
||||||
let tauri_handle = context.tauri_handle.clone();
|
let tauri_handle = context.tauri_handle.clone();
|
||||||
|
|
||||||
// Wait for the user to approve a seller and to deposit coins
|
// Wait for the user to approve a seller and to deposit coins
|
||||||
|
|
@ -973,10 +946,18 @@ pub async fn buy_xmr(
|
||||||
|
|
||||||
let bitcoin_wallet_for_closures = Arc::clone(&bitcoin_wallet);
|
let bitcoin_wallet_for_closures = Arc::clone(&bitcoin_wallet);
|
||||||
|
|
||||||
// Clone bitcoin_change_address before moving it in the emit call
|
// Clone variables before moving them into closures
|
||||||
let bitcoin_change_address_for_spawn = bitcoin_change_address.clone();
|
let bitcoin_change_address_for_spawn = bitcoin_change_address.clone();
|
||||||
let rendezvous_points_clone = rendezvous_points.clone();
|
let rendezvous_points_clone = rendezvous_points.clone();
|
||||||
let sellers_clone = sellers.clone();
|
let sellers_clone = sellers.clone();
|
||||||
|
let db_for_fetch = db.clone();
|
||||||
|
let tor_client_for_swarm = tor_client.clone();
|
||||||
|
|
||||||
|
// Clone tauri_handle for different closures
|
||||||
|
let tauri_handle_for_fetch = tauri_handle.clone();
|
||||||
|
let tauri_handle_for_determine = tauri_handle.clone();
|
||||||
|
let tauri_handle_for_selection = tauri_handle.clone();
|
||||||
|
let tauri_handle_for_suspension = tauri_handle.clone();
|
||||||
|
|
||||||
// Acquire the lock before the user has selected a maker and we already have funds in the wallet
|
// Acquire the lock before the user has selected a maker and we already have funds in the wallet
|
||||||
// because we need to be able to cancel the determine_btc_to_swap(..)
|
// because we need to be able to cancel the determine_btc_to_swap(..)
|
||||||
|
|
@ -989,9 +970,9 @@ pub async fn buy_xmr(
|
||||||
let sellers = sellers_clone.clone();
|
let sellers = sellers_clone.clone();
|
||||||
let namespace = namespace;
|
let namespace = namespace;
|
||||||
let identity = identity.clone();
|
let identity = identity.clone();
|
||||||
let db = db.clone();
|
let db = db_for_fetch.clone();
|
||||||
let tor_client = tor_client.clone();
|
let tor_client = tor_client.clone();
|
||||||
let tauri_handle = tauri_handle.clone();
|
let tauri_handle = tauri_handle_for_fetch.clone();
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
fetch_quotes_task(
|
fetch_quotes_task(
|
||||||
|
|
@ -999,7 +980,7 @@ pub async fn buy_xmr(
|
||||||
namespace,
|
namespace,
|
||||||
sellers,
|
sellers,
|
||||||
identity,
|
identity,
|
||||||
db,
|
Some(db),
|
||||||
tor_client,
|
tor_client,
|
||||||
tauri_handle,
|
tauri_handle,
|
||||||
).await
|
).await
|
||||||
|
|
@ -1027,10 +1008,10 @@ pub async fn buy_xmr(
|
||||||
async move { w.sync().await }
|
async move { w.sync().await }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
context.tauri_handle.clone(),
|
tauri_handle_for_determine,
|
||||||
swap_id,
|
swap_id,
|
||||||
|quote_with_address| {
|
|quote_with_address| {
|
||||||
let tauri_handle = context.tauri_handle.clone();
|
let tauri_handle_clone = tauri_handle_for_selection.clone();
|
||||||
Box::new(async move {
|
Box::new(async move {
|
||||||
let details = SelectMakerDetails {
|
let details = SelectMakerDetails {
|
||||||
swap_id,
|
swap_id,
|
||||||
|
|
@ -1038,7 +1019,7 @@ pub async fn buy_xmr(
|
||||||
maker: quote_with_address,
|
maker: quote_with_address,
|
||||||
};
|
};
|
||||||
|
|
||||||
tauri_handle.request_maker_selection(details, 300).await
|
tauri_handle_clone.request_maker_selection(details, 300).await
|
||||||
}) as Box<dyn Future<Output = Result<bool>> + Send>
|
}) as Box<dyn Future<Output = Result<bool>> + Send>
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
|
|
@ -1046,55 +1027,51 @@ pub async fn buy_xmr(
|
||||||
}
|
}
|
||||||
_ = context.swap_lock.listen_for_swap_force_suspension() => {
|
_ = context.swap_lock.listen_for_swap_force_suspension() => {
|
||||||
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
||||||
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
if let Some(handle) = tauri_handle_for_suspension {
|
||||||
|
handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||||
|
}
|
||||||
bail!("Shutdown signal received");
|
bail!("Shutdown signal received");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insert the peer_id into the database
|
// Insert the peer_id into the database
|
||||||
context.db.insert_peer_id(swap_id, seller_peer_id).await?;
|
db.insert_peer_id(swap_id, seller_peer_id).await?;
|
||||||
|
|
||||||
context
|
db.insert_address(seller_peer_id, seller_multiaddr.clone())
|
||||||
.db
|
|
||||||
.insert_address(seller_peer_id, seller_multiaddr.clone())
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let behaviour = cli::Behaviour::new(
|
let behaviour = cli::Behaviour::new(
|
||||||
seller_peer_id,
|
seller_peer_id,
|
||||||
env_config,
|
env_config,
|
||||||
bitcoin_wallet.clone(),
|
bitcoin_wallet.clone(),
|
||||||
(seed.derive_libp2p_identity(), context.config.namespace),
|
(seed.derive_libp2p_identity(), namespace),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut swarm = swarm::cli(
|
let mut swarm = swarm::cli(
|
||||||
seed.derive_libp2p_identity(),
|
seed.derive_libp2p_identity(),
|
||||||
context.tor_client.clone(),
|
tor_client_for_swarm,
|
||||||
behaviour,
|
behaviour,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
swarm.add_peer_address(seller_peer_id, seller_multiaddr.clone());
|
swarm.add_peer_address(seller_peer_id, seller_multiaddr.clone());
|
||||||
|
|
||||||
context
|
db.insert_monero_address_pool(swap_id, monero_receive_pool.clone())
|
||||||
.db
|
|
||||||
.insert_monero_address_pool(swap_id, monero_receive_pool.clone())
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
|
tracing::debug!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
|
||||||
|
|
||||||
context.tauri_handle.emit_swap_progress_event(
|
tauri_handle.emit_swap_progress_event(
|
||||||
swap_id,
|
swap_id,
|
||||||
TauriSwapProgressEvent::ReceivedQuote(quote.clone()),
|
TauriSwapProgressEvent::ReceivedQuote(quote.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Now create the event loop we use for the swap
|
// Now create the event loop we use for the swap
|
||||||
let (event_loop, event_loop_handle) =
|
let (event_loop, event_loop_handle) =
|
||||||
EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?;
|
EventLoop::new(swap_id, swarm, seller_peer_id, db.clone())?;
|
||||||
let event_loop = tokio::spawn(event_loop.run().in_current_span());
|
let event_loop = tokio::spawn(event_loop.run().in_current_span());
|
||||||
|
|
||||||
context
|
tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(quote));
|
||||||
.tauri_handle
|
|
||||||
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::ReceivedQuote(quote));
|
|
||||||
|
|
||||||
context.tasks.clone().spawn(async move {
|
context.tasks.clone().spawn(async move {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|
@ -1103,7 +1080,7 @@ pub async fn buy_xmr(
|
||||||
tracing::debug!("Shutdown signal received, exiting");
|
tracing::debug!("Shutdown signal received, exiting");
|
||||||
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
||||||
|
|
||||||
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||||
|
|
||||||
bail!("Shutdown signal received");
|
bail!("Shutdown signal received");
|
||||||
},
|
},
|
||||||
|
|
@ -1120,9 +1097,9 @@ pub async fn buy_xmr(
|
||||||
},
|
},
|
||||||
swap_result = async {
|
swap_result = async {
|
||||||
let swap = Swap::new(
|
let swap = Swap::new(
|
||||||
Arc::clone(&context.db),
|
db.clone(),
|
||||||
swap_id,
|
swap_id,
|
||||||
Arc::clone(&bitcoin_wallet),
|
bitcoin_wallet.clone(),
|
||||||
monero_wallet,
|
monero_wallet,
|
||||||
env_config,
|
env_config,
|
||||||
event_loop_handle,
|
event_loop_handle,
|
||||||
|
|
@ -1130,7 +1107,7 @@ pub async fn buy_xmr(
|
||||||
bitcoin_change_address_for_spawn,
|
bitcoin_change_address_for_spawn,
|
||||||
tx_lock_amount,
|
tx_lock_amount,
|
||||||
tx_lock_fee
|
tx_lock_fee
|
||||||
).with_event_emitter(context.tauri_handle.clone());
|
).with_event_emitter(tauri_handle.clone());
|
||||||
|
|
||||||
bob::run(swap).await
|
bob::run(swap).await
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -1152,7 +1129,7 @@ pub async fn buy_xmr(
|
||||||
.await
|
.await
|
||||||
.expect("Could not release swap lock");
|
.expect("Could not release swap lock");
|
||||||
|
|
||||||
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
}.in_current_span()).await;
|
}.in_current_span()).await;
|
||||||
|
|
@ -1167,11 +1144,16 @@ pub async fn resume_swap(
|
||||||
) -> Result<ResumeSwapResponse> {
|
) -> Result<ResumeSwapResponse> {
|
||||||
let ResumeSwapArgs { swap_id } = resume;
|
let ResumeSwapArgs { swap_id } = resume;
|
||||||
|
|
||||||
let seller_peer_id = context.db.get_peer_id(swap_id).await?;
|
let db = context.try_get_db().await?;
|
||||||
let seller_addresses = context.db.get_addresses(seller_peer_id).await?;
|
let config = context.try_get_config().await?;
|
||||||
|
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||||
|
let monero_manager = context.try_get_monero_manager().await?;
|
||||||
|
let tor_client = context.tor_client.read().await.clone();
|
||||||
|
|
||||||
let seed = context
|
let seller_peer_id = db.get_peer_id(swap_id).await?;
|
||||||
.config
|
let seller_addresses = db.get_addresses(seller_peer_id).await?;
|
||||||
|
|
||||||
|
let seed = config
|
||||||
.seed
|
.seed
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.context("Could not get seed")?
|
.context("Could not get seed")?
|
||||||
|
|
@ -1179,16 +1161,11 @@ pub async fn resume_swap(
|
||||||
|
|
||||||
let behaviour = cli::Behaviour::new(
|
let behaviour = cli::Behaviour::new(
|
||||||
seller_peer_id,
|
seller_peer_id,
|
||||||
context.config.env_config,
|
config.env_config,
|
||||||
Arc::clone(
|
bitcoin_wallet.clone(),
|
||||||
context
|
(seed.clone(), config.namespace),
|
||||||
.bitcoin_wallet
|
|
||||||
.as_ref()
|
|
||||||
.context("Could not get Bitcoin wallet")?,
|
|
||||||
),
|
|
||||||
(seed.clone(), context.config.namespace),
|
|
||||||
);
|
);
|
||||||
let mut swarm = swarm::cli(seed.clone(), context.tor_client.clone(), behaviour).await?;
|
let mut swarm = swarm::cli(seed.clone(), tor_client, behaviour).await?;
|
||||||
let our_peer_id = swarm.local_peer_id();
|
let our_peer_id = swarm.local_peer_id();
|
||||||
|
|
||||||
tracing::debug!(peer_id = %our_peer_id, "Network layer initialized");
|
tracing::debug!(peer_id = %our_peer_id, "Network layer initialized");
|
||||||
|
|
@ -1199,36 +1176,27 @@ pub async fn resume_swap(
|
||||||
}
|
}
|
||||||
|
|
||||||
let (event_loop, event_loop_handle) =
|
let (event_loop, event_loop_handle) =
|
||||||
EventLoop::new(swap_id, swarm, seller_peer_id, context.db.clone())?;
|
EventLoop::new(swap_id, swarm, seller_peer_id, db.clone())?;
|
||||||
|
|
||||||
let monero_receive_pool = context.db.get_monero_address_pool(swap_id).await?;
|
let monero_receive_pool = db.get_monero_address_pool(swap_id).await?;
|
||||||
|
|
||||||
|
let tauri_handle = context.tauri_handle.clone();
|
||||||
|
|
||||||
let swap = Swap::from_db(
|
let swap = Swap::from_db(
|
||||||
Arc::clone(&context.db),
|
db.clone(),
|
||||||
swap_id,
|
swap_id,
|
||||||
Arc::clone(
|
bitcoin_wallet,
|
||||||
context
|
monero_manager,
|
||||||
.bitcoin_wallet
|
config.env_config,
|
||||||
.as_ref()
|
|
||||||
.context("Could not get Bitcoin wallet")?,
|
|
||||||
),
|
|
||||||
context
|
|
||||||
.monero_manager
|
|
||||||
.as_ref()
|
|
||||||
.context("Could not get Monero wallet manager")?
|
|
||||||
.clone(),
|
|
||||||
context.config.env_config,
|
|
||||||
event_loop_handle,
|
event_loop_handle,
|
||||||
monero_receive_pool,
|
monero_receive_pool,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.with_event_emitter(context.tauri_handle.clone());
|
.with_event_emitter(tauri_handle.clone());
|
||||||
|
|
||||||
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
||||||
|
|
||||||
context
|
tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Resuming);
|
||||||
.tauri_handle
|
|
||||||
.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Resuming);
|
|
||||||
|
|
||||||
context.tasks.clone().spawn(
|
context.tasks.clone().spawn(
|
||||||
async move {
|
async move {
|
||||||
|
|
@ -1239,7 +1207,7 @@ pub async fn resume_swap(
|
||||||
tracing::debug!("Shutdown signal received, exiting");
|
tracing::debug!("Shutdown signal received, exiting");
|
||||||
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
context.swap_lock.release_swap_lock().await.expect("Shutdown signal received but failed to release swap lock. The swap process has been terminated but the swap lock is still active.");
|
||||||
|
|
||||||
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||||
|
|
||||||
bail!("Shutdown signal received");
|
bail!("Shutdown signal received");
|
||||||
},
|
},
|
||||||
|
|
@ -1272,7 +1240,7 @@ pub async fn resume_swap(
|
||||||
.await
|
.await
|
||||||
.expect("Could not release swap lock");
|
.expect("Could not release swap lock");
|
||||||
|
|
||||||
context.tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
tauri_handle.emit_swap_progress_event(swap_id, TauriSwapProgressEvent::Released);
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok::<(), anyhow::Error>(())
|
||||||
}
|
}
|
||||||
|
|
@ -1290,15 +1258,12 @@ pub async fn cancel_and_refund(
|
||||||
context: Arc<Context>,
|
context: Arc<Context>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let CancelAndRefundArgs { swap_id } = cancel_and_refund;
|
let CancelAndRefundArgs { swap_id } = cancel_and_refund;
|
||||||
let bitcoin_wallet = context
|
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||||
.bitcoin_wallet
|
let db = context.try_get_db().await?;
|
||||||
.as_ref()
|
|
||||||
.context("Could not get Bitcoin wallet")?;
|
|
||||||
|
|
||||||
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
context.swap_lock.acquire_swap_lock(swap_id).await?;
|
||||||
|
|
||||||
let state =
|
let state = cli::cancel_and_refund(swap_id, bitcoin_wallet, db).await;
|
||||||
cli::cancel_and_refund(swap_id, Arc::clone(bitcoin_wallet), Arc::clone(&context.db)).await;
|
|
||||||
|
|
||||||
context
|
context
|
||||||
.swap_lock
|
.swap_lock
|
||||||
|
|
@ -1319,7 +1284,8 @@ pub async fn cancel_and_refund(
|
||||||
|
|
||||||
#[tracing::instrument(fields(method = "get_history"), skip(context))]
|
#[tracing::instrument(fields(method = "get_history"), skip(context))]
|
||||||
pub async fn get_history(context: Arc<Context>) -> Result<GetHistoryResponse> {
|
pub async fn get_history(context: Arc<Context>) -> Result<GetHistoryResponse> {
|
||||||
let swaps = context.db.all().await?;
|
let db = context.try_get_db().await?;
|
||||||
|
let swaps = db.all().await?;
|
||||||
let mut vec: Vec<GetHistoryEntry> = Vec::new();
|
let mut vec: Vec<GetHistoryEntry> = Vec::new();
|
||||||
for (swap_id, state) in swaps {
|
for (swap_id, state) in swaps {
|
||||||
let state: BobState = state.try_into()?;
|
let state: BobState = state.try_into()?;
|
||||||
|
|
@ -1334,7 +1300,8 @@ pub async fn get_history(context: Arc<Context>) -> Result<GetHistoryResponse> {
|
||||||
|
|
||||||
#[tracing::instrument(fields(method = "get_config"), skip(context))]
|
#[tracing::instrument(fields(method = "get_config"), skip(context))]
|
||||||
pub async fn get_config(context: Arc<Context>) -> Result<serde_json::Value> {
|
pub async fn get_config(context: Arc<Context>) -> Result<serde_json::Value> {
|
||||||
let data_dir_display = context.config.data_dir.display();
|
let config = context.try_get_config().await?;
|
||||||
|
let data_dir_display = config.data_dir.display();
|
||||||
tracing::info!(path=%data_dir_display, "Data directory");
|
tracing::info!(path=%data_dir_display, "Data directory");
|
||||||
tracing::info!(path=%format!("{}/logs", data_dir_display), "Log files directory");
|
tracing::info!(path=%format!("{}/logs", data_dir_display), "Log files directory");
|
||||||
tracing::info!(path=%format!("{}/sqlite", data_dir_display), "Sqlite file location");
|
tracing::info!(path=%format!("{}/sqlite", data_dir_display), "Sqlite file location");
|
||||||
|
|
@ -1357,10 +1324,7 @@ pub async fn withdraw_btc(
|
||||||
context: Arc<Context>,
|
context: Arc<Context>,
|
||||||
) -> Result<WithdrawBtcResponse> {
|
) -> Result<WithdrawBtcResponse> {
|
||||||
let WithdrawBtcArgs { address, amount } = withdraw_btc;
|
let WithdrawBtcArgs { address, amount } = withdraw_btc;
|
||||||
let bitcoin_wallet = context
|
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||||
.bitcoin_wallet
|
|
||||||
.as_ref()
|
|
||||||
.context("Could not get Bitcoin wallet")?;
|
|
||||||
|
|
||||||
let (withdraw_tx_unsigned, amount) = match amount {
|
let (withdraw_tx_unsigned, amount) = match amount {
|
||||||
Some(amount) => {
|
Some(amount) => {
|
||||||
|
|
@ -1402,10 +1366,7 @@ pub async fn withdraw_btc(
|
||||||
#[tracing::instrument(fields(method = "get_balance"), skip(context))]
|
#[tracing::instrument(fields(method = "get_balance"), skip(context))]
|
||||||
pub async fn get_balance(balance: BalanceArgs, context: Arc<Context>) -> Result<BalanceResponse> {
|
pub async fn get_balance(balance: BalanceArgs, context: Arc<Context>) -> Result<BalanceResponse> {
|
||||||
let BalanceArgs { force_refresh } = balance;
|
let BalanceArgs { force_refresh } = balance;
|
||||||
let bitcoin_wallet = context
|
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||||
.bitcoin_wallet
|
|
||||||
.as_ref()
|
|
||||||
.context("Could not get Bitcoin wallet")?;
|
|
||||||
|
|
||||||
if force_refresh {
|
if force_refresh {
|
||||||
bitcoin_wallet.sync().await?;
|
bitcoin_wallet.sync().await?;
|
||||||
|
|
@ -1441,8 +1402,12 @@ pub async fn list_sellers(
|
||||||
.filter_map(|rendezvous_point| rendezvous_point.split_peer_id())
|
.filter_map(|rendezvous_point| rendezvous_point.split_peer_id())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let identity = context
|
let config = context.try_get_config().await?;
|
||||||
.config
|
let db = context.try_get_db().await?;
|
||||||
|
let tor_client = context.tor_client.read().await.clone();
|
||||||
|
let tauri_handle = context.tauri_handle.clone();
|
||||||
|
|
||||||
|
let identity = config
|
||||||
.seed
|
.seed
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.context("Cannot extract seed")?
|
.context("Cannot extract seed")?
|
||||||
|
|
@ -1450,11 +1415,11 @@ pub async fn list_sellers(
|
||||||
|
|
||||||
let sellers = list_sellers_impl(
|
let sellers = list_sellers_impl(
|
||||||
rendezvous_nodes,
|
rendezvous_nodes,
|
||||||
context.config.namespace,
|
config.namespace,
|
||||||
context.tor_client.clone(),
|
tor_client,
|
||||||
identity,
|
identity,
|
||||||
Some(context.db.clone()),
|
Some(db.clone()),
|
||||||
context.tauri_handle(),
|
tauri_handle,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -1480,10 +1445,7 @@ pub async fn list_sellers(
|
||||||
// Add the peer as known to the database
|
// Add the peer as known to the database
|
||||||
// This'll allow us to later request a quote again
|
// This'll allow us to later request a quote again
|
||||||
// without having to re-discover the peer at the rendezvous point
|
// without having to re-discover the peer at the rendezvous point
|
||||||
context
|
db.insert_address(*peer_id, multiaddr.clone()).await?;
|
||||||
.db
|
|
||||||
.insert_address(*peer_id, multiaddr.clone())
|
|
||||||
.await?;
|
|
||||||
}
|
}
|
||||||
SellerStatus::Unreachable(UnreachableSeller { peer_id }) => {
|
SellerStatus::Unreachable(UnreachableSeller { peer_id }) => {
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
|
|
@ -1500,10 +1462,7 @@ pub async fn list_sellers(
|
||||||
|
|
||||||
#[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))]
|
#[tracing::instrument(fields(method = "export_bitcoin_wallet"), skip(context))]
|
||||||
pub async fn export_bitcoin_wallet(context: Arc<Context>) -> Result<serde_json::Value> {
|
pub async fn export_bitcoin_wallet(context: Arc<Context>) -> Result<serde_json::Value> {
|
||||||
let bitcoin_wallet = context
|
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||||
.bitcoin_wallet
|
|
||||||
.as_ref()
|
|
||||||
.context("Could not get Bitcoin wallet")?;
|
|
||||||
|
|
||||||
let wallet_export = bitcoin_wallet.wallet_export("cli").await?;
|
let wallet_export = bitcoin_wallet.wallet_export("cli").await?;
|
||||||
tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet");
|
tracing::info!(descriptor=%wallet_export.to_string(), "Exported bitcoin wallet");
|
||||||
|
|
@ -1518,14 +1477,17 @@ pub async fn monero_recovery(
|
||||||
context: Arc<Context>,
|
context: Arc<Context>,
|
||||||
) -> Result<serde_json::Value> {
|
) -> Result<serde_json::Value> {
|
||||||
let MoneroRecoveryArgs { swap_id } = monero_recovery;
|
let MoneroRecoveryArgs { swap_id } = monero_recovery;
|
||||||
let swap_state: BobState = context.db.get_state(swap_id).await?.try_into()?;
|
let db = context.try_get_db().await?;
|
||||||
|
let config = context.try_get_config().await?;
|
||||||
|
|
||||||
|
let swap_state: BobState = db.get_state(swap_id).await?.try_into()?;
|
||||||
|
|
||||||
if let BobState::BtcRedeemed(state5) = swap_state {
|
if let BobState::BtcRedeemed(state5) = swap_state {
|
||||||
let (spend_key, view_key) = state5.xmr_keys();
|
let (spend_key, view_key) = state5.xmr_keys();
|
||||||
let restore_height = state5.monero_wallet_restore_blockheight.height;
|
let restore_height = state5.monero_wallet_restore_blockheight.height;
|
||||||
|
|
||||||
let address = monero::Address::standard(
|
let address = monero::Address::standard(
|
||||||
context.config.env_config.monero_network,
|
config.env_config.monero_network,
|
||||||
monero::PublicKey::from_private_key(&spend_key),
|
monero::PublicKey::from_private_key(&spend_key),
|
||||||
monero::PublicKey::from(view_key.public()),
|
monero::PublicKey::from(view_key.public()),
|
||||||
);
|
);
|
||||||
|
|
@ -1978,10 +1940,7 @@ impl Request for GetMoneroSyncProgressArgs {
|
||||||
type Response = GetMoneroSyncProgressResponse;
|
type Response = GetMoneroSyncProgressResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let wallet_manager = ctx
|
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||||
.monero_manager
|
|
||||||
.as_ref()
|
|
||||||
.context("Monero wallet manager not available")?;
|
|
||||||
let wallet = wallet_manager.main_wallet().await;
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
|
|
||||||
let sync_progress = wallet.call(|wallet| wallet.sync_progress()).await;
|
let sync_progress = wallet.call(|wallet| wallet.sync_progress()).await;
|
||||||
|
|
@ -2008,10 +1967,7 @@ impl Request for GetMoneroSeedArgs {
|
||||||
type Response = GetMoneroSeedResponse;
|
type Response = GetMoneroSeedResponse;
|
||||||
|
|
||||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||||
let wallet_manager = ctx
|
let wallet_manager = ctx.try_get_monero_manager().await?;
|
||||||
.monero_manager
|
|
||||||
.as_ref()
|
|
||||||
.context("Monero wallet manager not available")?;
|
|
||||||
let wallet = wallet_manager.main_wallet().await;
|
let wallet = wallet_manager.main_wallet().await;
|
||||||
|
|
||||||
let seed = wallet.seed().await?;
|
let seed = wallet.seed().await?;
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,13 @@ use tokio::sync::oneshot;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const TAURI_UNIFIED_EVENT_NAME: &str = "tauri-unified-event";
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
#[serde(tag = "channelName", content = "event")]
|
#[serde(tag = "channelName", content = "event")]
|
||||||
pub enum TauriEvent {
|
pub enum TauriEvent {
|
||||||
SwapProgress(TauriSwapProgressEventWrapper),
|
SwapProgress(TauriSwapProgressEventWrapper),
|
||||||
ContextInitProgress(TauriContextStatusEvent),
|
|
||||||
CliLog(TauriLogEvent),
|
CliLog(TauriLogEvent),
|
||||||
BalanceChange(BalanceResponse),
|
BalanceChange(BalanceResponse),
|
||||||
SwapDatabaseStateUpdate(TauriDatabaseStateEvent),
|
SwapDatabaseStateUpdate(TauriDatabaseStateEvent),
|
||||||
|
|
@ -46,7 +47,14 @@ pub enum MoneroWalletUpdate {
|
||||||
HistoryUpdate(GetMoneroHistoryResponse),
|
HistoryUpdate(GetMoneroHistoryResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
const TAURI_UNIFIED_EVENT_NAME: &str = "tauri-unified-event";
|
#[typeshare]
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct ContextStatus {
|
||||||
|
pub bitcoin_wallet_available: bool,
|
||||||
|
pub monero_wallet_available: bool,
|
||||||
|
pub database_available: bool,
|
||||||
|
pub tor_available: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
|
@ -456,10 +464,6 @@ pub trait TauriEmitter {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_context_init_progress_event(&self, event: TauriContextStatusEvent) {
|
|
||||||
self.emit_unified_event(TauriEvent::ContextInitProgress(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_cli_log_event(&self, event: TauriLogEvent) {
|
fn emit_cli_log_event(&self, event: TauriLogEvent) {
|
||||||
self.emit_unified_event(TauriEvent::CliLog(event));
|
self.emit_unified_event(TauriEvent::CliLog(event));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,17 +79,16 @@ where
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.map(|address| address.into_unchecked());
|
.map(|address| address.into_unchecked());
|
||||||
|
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_tor(tor.enable_tor)
|
.with_tor(tor.enable_tor)
|
||||||
.with_bitcoin(bitcoin)
|
.with_bitcoin(bitcoin)
|
||||||
.with_monero(monero)
|
.with_monero(monero)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
BuyXmrArgs {
|
BuyXmrArgs {
|
||||||
rendezvous_points: vec![],
|
rendezvous_points: vec![],
|
||||||
|
|
@ -103,14 +102,13 @@ where
|
||||||
Ok(context)
|
Ok(context)
|
||||||
}
|
}
|
||||||
CliCommand::History => {
|
CliCommand::History => {
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
GetHistoryArgs {}.request(context.clone()).await?;
|
GetHistoryArgs {}.request(context.clone()).await?;
|
||||||
|
|
||||||
|
|
@ -121,14 +119,13 @@ where
|
||||||
redact,
|
redact,
|
||||||
swap_id,
|
swap_id,
|
||||||
} => {
|
} => {
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
GetLogsArgs {
|
GetLogsArgs {
|
||||||
logs_dir,
|
logs_dir,
|
||||||
|
|
@ -141,29 +138,27 @@ where
|
||||||
Ok(context)
|
Ok(context)
|
||||||
}
|
}
|
||||||
CliCommand::Config => {
|
CliCommand::Config => {
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
GetConfigArgs {}.request(context.clone()).await?;
|
GetConfigArgs {}.request(context.clone()).await?;
|
||||||
|
|
||||||
Ok(context)
|
Ok(context)
|
||||||
}
|
}
|
||||||
CliCommand::Balance { bitcoin } => {
|
CliCommand::Balance { bitcoin } => {
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_bitcoin(bitcoin)
|
.with_bitcoin(bitcoin)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
BalanceArgs {
|
BalanceArgs {
|
||||||
force_refresh: true,
|
force_refresh: true,
|
||||||
|
|
@ -180,15 +175,14 @@ where
|
||||||
} => {
|
} => {
|
||||||
let address = bitcoin_address::validate(address, is_testnet)?;
|
let address = bitcoin_address::validate(address, is_testnet)?;
|
||||||
|
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_bitcoin(bitcoin)
|
.with_bitcoin(bitcoin)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
WithdrawBtcArgs { amount, address }
|
WithdrawBtcArgs { amount, address }
|
||||||
.request(context.clone())
|
.request(context.clone())
|
||||||
|
|
@ -202,17 +196,16 @@ where
|
||||||
monero,
|
monero,
|
||||||
tor,
|
tor,
|
||||||
} => {
|
} => {
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_tor(tor.enable_tor)
|
.with_tor(tor.enable_tor)
|
||||||
.with_bitcoin(bitcoin)
|
.with_bitcoin(bitcoin)
|
||||||
.with_monero(monero)
|
.with_monero(monero)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
ResumeSwapArgs { swap_id }.request(context.clone()).await?;
|
ResumeSwapArgs { swap_id }.request(context.clone()).await?;
|
||||||
|
|
||||||
|
|
@ -222,15 +215,14 @@ where
|
||||||
swap_id: SwapId { swap_id },
|
swap_id: SwapId { swap_id },
|
||||||
bitcoin,
|
bitcoin,
|
||||||
} => {
|
} => {
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_bitcoin(bitcoin)
|
.with_bitcoin(bitcoin)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
CancelAndRefundArgs { swap_id }
|
CancelAndRefundArgs { swap_id }
|
||||||
.request(context.clone())
|
.request(context.clone())
|
||||||
|
|
@ -242,15 +234,14 @@ where
|
||||||
rendezvous_point,
|
rendezvous_point,
|
||||||
tor,
|
tor,
|
||||||
} => {
|
} => {
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_tor(tor.enable_tor)
|
.with_tor(tor.enable_tor)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
ListSellersArgs {
|
ListSellersArgs {
|
||||||
rendezvous_points: vec![rendezvous_point],
|
rendezvous_points: vec![rendezvous_point],
|
||||||
|
|
@ -261,15 +252,14 @@ where
|
||||||
Ok(context)
|
Ok(context)
|
||||||
}
|
}
|
||||||
CliCommand::ExportBitcoinWallet { bitcoin } => {
|
CliCommand::ExportBitcoinWallet { bitcoin } => {
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_bitcoin(bitcoin)
|
.with_bitcoin(bitcoin)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
ExportBitcoinWalletArgs {}.request(context.clone()).await?;
|
ExportBitcoinWalletArgs {}.request(context.clone()).await?;
|
||||||
|
|
||||||
|
|
@ -278,14 +268,13 @@ where
|
||||||
CliCommand::MoneroRecovery {
|
CliCommand::MoneroRecovery {
|
||||||
swap_id: SwapId { swap_id },
|
swap_id: SwapId { swap_id },
|
||||||
} => {
|
} => {
|
||||||
let context = Arc::new(
|
let context = Arc::new(Context::new_without_tauri_handle());
|
||||||
ContextBuilder::new(is_testnet)
|
ContextBuilder::new(is_testnet)
|
||||||
.with_data_dir(data)
|
.with_data_dir(data)
|
||||||
.with_debug(debug)
|
.with_debug(debug)
|
||||||
.with_json(json)
|
.with_json(json)
|
||||||
.build()
|
.build(context.clone())
|
||||||
.await?,
|
.await?;
|
||||||
);
|
|
||||||
|
|
||||||
MoneroRecoveryArgs { swap_id }
|
MoneroRecoveryArgs { swap_id }
|
||||||
.request(context.clone())
|
.request(context.clone())
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,55 @@ pub fn init(
|
||||||
tauri_handle: Option<TauriHandle>,
|
tauri_handle: Option<TauriHandle>,
|
||||||
trace_stdout: bool,
|
trace_stdout: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let TOR_CRATES: Vec<&str> = vec!["arti"];
|
let TOR_CRATES: Vec<&str> = vec![
|
||||||
|
"arti",
|
||||||
|
"arti-client",
|
||||||
|
"arti-fork",
|
||||||
|
"tor-api2",
|
||||||
|
"tor-async-utils",
|
||||||
|
"tor-basic-utils",
|
||||||
|
"tor-bytes",
|
||||||
|
"tor-cell",
|
||||||
|
"tor-cert",
|
||||||
|
"tor-chanmgr",
|
||||||
|
"tor-checkable",
|
||||||
|
"tor-circmgr",
|
||||||
|
"tor-config",
|
||||||
|
"tor-config-path",
|
||||||
|
"tor-consdiff",
|
||||||
|
"tor-dirclient",
|
||||||
|
"tor-dirmgr",
|
||||||
|
"tor-error",
|
||||||
|
"tor-general-addr",
|
||||||
|
"tor-guardmgr",
|
||||||
|
"tor-hsclient",
|
||||||
|
"tor-hscrypto",
|
||||||
|
"tor-hsservice",
|
||||||
|
"tor-key-forge",
|
||||||
|
"tor-keymgr",
|
||||||
|
"tor-linkspec",
|
||||||
|
"tor-llcrypto",
|
||||||
|
"tor-log-ratelim",
|
||||||
|
"tor-memquota",
|
||||||
|
"tor-netdir",
|
||||||
|
"tor-netdoc",
|
||||||
|
"tor-persist",
|
||||||
|
"tor-proto",
|
||||||
|
"tor-protover",
|
||||||
|
"tor-relay-selection",
|
||||||
|
"tor-rtcompat",
|
||||||
|
"tor-rtmock",
|
||||||
|
"tor-socksproto",
|
||||||
|
"tor-units",
|
||||||
|
];
|
||||||
|
|
||||||
let LIBP2P_CRATES: Vec<&str> = vec![
|
let LIBP2P_CRATES: Vec<&str> = vec![
|
||||||
// Main libp2p crates
|
|
||||||
"libp2p",
|
"libp2p",
|
||||||
"libp2p_swarm",
|
"libp2p_swarm",
|
||||||
"libp2p_core",
|
"libp2p_core",
|
||||||
"libp2p_tcp",
|
"libp2p_tcp",
|
||||||
"libp2p_noise",
|
"libp2p_noise",
|
||||||
"libp2p_tor",
|
"libp2p_tor",
|
||||||
// Specific libp2p module targets that appear in logs
|
|
||||||
"libp2p_core::transport",
|
"libp2p_core::transport",
|
||||||
"libp2p_core::transport::choice",
|
"libp2p_core::transport::choice",
|
||||||
"libp2p_core::transport::dummy",
|
"libp2p_core::transport::dummy",
|
||||||
|
|
@ -67,6 +105,7 @@ pub fn init(
|
||||||
"libp2p_dcutr",
|
"libp2p_dcutr",
|
||||||
"monero_cpp",
|
"monero_cpp",
|
||||||
];
|
];
|
||||||
|
|
||||||
let OUR_CRATES: Vec<&str> = vec![
|
let OUR_CRATES: Vec<&str> = vec![
|
||||||
"swap",
|
"swap",
|
||||||
"asb",
|
"asb",
|
||||||
|
|
@ -79,8 +118,6 @@ pub fn init(
|
||||||
"monero_rpc_pool",
|
"monero_rpc_pool",
|
||||||
];
|
];
|
||||||
|
|
||||||
let INFO_LEVEL_CRATES: Vec<&str> = vec![];
|
|
||||||
|
|
||||||
// General log file for non-verbose logs
|
// General log file for non-verbose logs
|
||||||
let file_appender: RollingFileAppender = tracing_appender::rolling::never(&dir, "swap-all.log");
|
let file_appender: RollingFileAppender = tracing_appender::rolling::never(&dir, "swap-all.log");
|
||||||
|
|
||||||
|
|
@ -104,11 +141,10 @@ pub fn init(
|
||||||
.with_file(true)
|
.with_file(true)
|
||||||
.with_line_number(true)
|
.with_line_number(true)
|
||||||
.json()
|
.json()
|
||||||
.with_filter(env_filter_with_info_crates(
|
.with_filter(env_filter_with_all_crates(vec![(
|
||||||
level_filter,
|
|
||||||
OUR_CRATES.clone(),
|
OUR_CRATES.clone(),
|
||||||
INFO_LEVEL_CRATES.clone(),
|
level_filter,
|
||||||
)?);
|
)])?);
|
||||||
|
|
||||||
// Layer for writing to the verbose log file
|
// Layer for writing to the verbose log file
|
||||||
// Crates: All crates with different levels (libp2p at INFO+, others at TRACE)
|
// Crates: All crates with different levels (libp2p at INFO+, others at TRACE)
|
||||||
|
|
@ -121,13 +157,11 @@ pub fn init(
|
||||||
.with_file(true)
|
.with_file(true)
|
||||||
.with_line_number(true)
|
.with_line_number(true)
|
||||||
.json()
|
.json()
|
||||||
.with_filter(env_filter_with_all_crates(
|
.with_filter(env_filter_with_all_crates(vec![
|
||||||
LevelFilter::TRACE,
|
(OUR_CRATES.clone(), LevelFilter::TRACE),
|
||||||
OUR_CRATES.clone(),
|
(LIBP2P_CRATES.clone(), LevelFilter::TRACE),
|
||||||
LIBP2P_CRATES.clone(),
|
(TOR_CRATES.clone(), LevelFilter::TRACE),
|
||||||
TOR_CRATES.clone(),
|
])?);
|
||||||
INFO_LEVEL_CRATES.clone(),
|
|
||||||
)?);
|
|
||||||
|
|
||||||
// Layer for writing to the terminal
|
// Layer for writing to the terminal
|
||||||
// Crates: swap, asb
|
// Crates: swap, asb
|
||||||
|
|
@ -152,29 +186,21 @@ pub fn init(
|
||||||
.with_file(true)
|
.with_file(true)
|
||||||
.with_line_number(true)
|
.with_line_number(true)
|
||||||
.json()
|
.json()
|
||||||
.with_filter(env_filter_with_all_crates(
|
.with_filter(env_filter_with_all_crates(vec![
|
||||||
level_filter,
|
(OUR_CRATES.clone(), LevelFilter::DEBUG),
|
||||||
OUR_CRATES.clone(),
|
(LIBP2P_CRATES.clone(), LevelFilter::INFO),
|
||||||
LIBP2P_CRATES.clone(),
|
(TOR_CRATES.clone(), LevelFilter::INFO),
|
||||||
TOR_CRATES.clone(),
|
])?);
|
||||||
INFO_LEVEL_CRATES.clone(),
|
|
||||||
)?);
|
|
||||||
|
|
||||||
// If trace_stdout is true, we log all messages to the terminal
|
// If trace_stdout is true, we log all messages to the terminal
|
||||||
// Otherwise, we only log the bare minimum
|
// Otherwise, we only log the bare minimum
|
||||||
let terminal_layer_env_filter = match trace_stdout {
|
let terminal_layer_env_filter = match trace_stdout {
|
||||||
true => env_filter_with_all_crates(
|
true => env_filter_with_all_crates(vec![
|
||||||
LevelFilter::TRACE,
|
(OUR_CRATES.clone(), level_filter),
|
||||||
OUR_CRATES.clone(),
|
(TOR_CRATES.clone(), level_filter),
|
||||||
LIBP2P_CRATES.clone(),
|
(LIBP2P_CRATES.clone(), LevelFilter::INFO),
|
||||||
TOR_CRATES.clone(),
|
])?,
|
||||||
INFO_LEVEL_CRATES.clone(),
|
false => env_filter_with_all_crates(vec![(OUR_CRATES.clone(), level_filter)])?,
|
||||||
)?,
|
|
||||||
false => env_filter_with_info_crates(
|
|
||||||
level_filter,
|
|
||||||
OUR_CRATES.clone(),
|
|
||||||
INFO_LEVEL_CRATES.clone(),
|
|
||||||
)?,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let final_terminal_layer = match format {
|
let final_terminal_layer = match format {
|
||||||
|
|
@ -201,60 +227,18 @@ pub fn init(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This function controls which crate's logs actually get logged and from which level, with info-level crates at INFO level or higher.
|
|
||||||
fn env_filter_with_info_crates(
|
|
||||||
level_filter: LevelFilter,
|
|
||||||
our_crates: Vec<&str>,
|
|
||||||
info_level_crates: Vec<&str>,
|
|
||||||
) -> Result<EnvFilter> {
|
|
||||||
let mut filter = EnvFilter::from_default_env();
|
|
||||||
|
|
||||||
// Add directives for each crate in the provided list
|
|
||||||
for crate_name in our_crates {
|
|
||||||
filter = filter.add_directive(Directive::from_str(&format!(
|
|
||||||
"{}={}",
|
|
||||||
crate_name, &level_filter
|
|
||||||
))?);
|
|
||||||
}
|
|
||||||
|
|
||||||
for crate_name in info_level_crates {
|
|
||||||
filter = filter.add_directive(Directive::from_str(&format!("{}=INFO", crate_name))?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This function controls which crate's logs actually get logged and from which level, including all crate categories.
|
/// This function controls which crate's logs actually get logged and from which level, including all crate categories.
|
||||||
fn env_filter_with_all_crates(
|
fn env_filter_with_all_crates(crates: Vec<(Vec<&str>, LevelFilter)>) -> Result<EnvFilter> {
|
||||||
level_filter: LevelFilter,
|
|
||||||
our_crates: Vec<&str>,
|
|
||||||
libp2p_crates: Vec<&str>,
|
|
||||||
tor_crates: Vec<&str>,
|
|
||||||
info_level_crates: Vec<&str>,
|
|
||||||
) -> Result<EnvFilter> {
|
|
||||||
let mut filter = EnvFilter::from_default_env();
|
let mut filter = EnvFilter::from_default_env();
|
||||||
|
|
||||||
// Add directives for each crate in the provided list
|
// Add directives for each group of crates with their specified level filter
|
||||||
for crate_name in our_crates {
|
for (crate_names, level_filter) in crates {
|
||||||
filter = filter.add_directive(Directive::from_str(&format!(
|
for crate_name in crate_names {
|
||||||
"{}={}",
|
filter = filter.add_directive(Directive::from_str(&format!(
|
||||||
crate_name, &level_filter
|
"{}={}",
|
||||||
))?);
|
crate_name, &level_filter
|
||||||
}
|
))?);
|
||||||
|
}
|
||||||
for crate_name in libp2p_crates {
|
|
||||||
filter = filter.add_directive(Directive::from_str(&format!("{}=INFO", crate_name))?);
|
|
||||||
}
|
|
||||||
|
|
||||||
for crate_name in tor_crates {
|
|
||||||
filter = filter.add_directive(Directive::from_str(&format!(
|
|
||||||
"{}={}",
|
|
||||||
crate_name, &level_filter
|
|
||||||
))?);
|
|
||||||
}
|
|
||||||
|
|
||||||
for crate_name in info_level_crates {
|
|
||||||
filter = filter.add_directive(Directive::from_str(&format!("{}=INFO", crate_name))?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(filter)
|
Ok(filter)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue