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