mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-25 06:39:53 -04:00
feat(monero): Remote node load balancing (#420)
This commit is contained in:
parent
a201c13b5d
commit
ff5e1c02bc
55 changed files with 4537 additions and 154 deletions
|
@ -4,9 +4,9 @@
|
|||
"version": "0.7.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check-bindings": "typeshare --lang=typescript --output-file __temp_bindings.ts ../swap/src && dprint fmt __temp_bindings.ts && diff -wbB __temp_bindings.ts ./src/models/tauriModel.ts && rm __temp_bindings.ts",
|
||||
"gen-bindings-verbose": "RUST_LOG=debug RUST_BACKTRACE=1 typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src && dprint fmt ./src/models/tauriModel.ts",
|
||||
"gen-bindings": "typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src && dprint fmt ./src/models/tauriModel.ts",
|
||||
"check-bindings": "typeshare --lang=typescript --output-file __temp_bindings.ts ../swap/src ../monero-rpc-pool/src && dprint fmt __temp_bindings.ts && diff -wbB __temp_bindings.ts ./src/models/tauriModel.ts && rm __temp_bindings.ts",
|
||||
"gen-bindings-verbose": "RUST_LOG=debug RUST_BACKTRACE=1 typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src && dprint fmt ./src/models/tauriModel.ts",
|
||||
"gen-bindings": "typeshare --lang=typescript --output-file ./src/models/tauriModel.ts ../swap/src ../monero-rpc-pool/src && dprint fmt ./src/models/tauriModel.ts",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"dev": "vite",
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
approvalEventReceived,
|
||||
backgroundProgressEventReceived,
|
||||
} from "store/features/rpcSlice";
|
||||
import { poolStatusReceived } from "store/features/poolSlice";
|
||||
import { swapProgressEventReceived } from "store/features/swapSlice";
|
||||
import logger from "utils/logger";
|
||||
import {
|
||||
|
@ -127,6 +128,10 @@ export async function setupBackgroundTasks(): Promise<void> {
|
|||
store.dispatch(backgroundProgressEventReceived(eventData));
|
||||
break;
|
||||
|
||||
case "PoolStatusUpdate":
|
||||
store.dispatch(poolStatusReceived(eventData));
|
||||
break;
|
||||
|
||||
default:
|
||||
exhaustiveGuard(channelName);
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ export default function UnfinishedSwapsAlert() {
|
|||
>
|
||||
You have{" "}
|
||||
{resumableSwapsCount > 1
|
||||
? `${resumableSwapsCount} unfinished swaps`
|
||||
: "one unfinished swap"}
|
||||
? `${resumableSwapsCount} pending swaps`
|
||||
: "one pending swap"}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -63,7 +63,10 @@ function getActiveStep(state: SwapState | null): PathStep | null {
|
|||
// Bitcoin has been locked, waiting for the counterparty to lock their XMR
|
||||
case "BtcLockTxInMempool":
|
||||
// We only display the first step as completed if the Bitcoin lock has been confirmed
|
||||
if (latestState.content.btc_lock_confirmations > 0) {
|
||||
if (
|
||||
latestState.content.btc_lock_confirmations !== undefined &&
|
||||
latestState.content.btc_lock_confirmations > 0
|
||||
) {
|
||||
return [PathType.HAPPY_PATH, 1, isReleased];
|
||||
}
|
||||
return [PathType.HAPPY_PATH, 0, isReleased];
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||
import { formatConfirmations } from "utils/formatUtils";
|
||||
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
|
||||
import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert";
|
||||
import { useActiveSwapInfo } from "store/hooks";
|
||||
|
@ -15,10 +16,11 @@ export default function BitcoinLockTxInMempoolPage({
|
|||
|
||||
return (
|
||||
<Box>
|
||||
{btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
||||
{(btc_lock_confirmations === undefined ||
|
||||
btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD) && (
|
||||
<DialogContentText>
|
||||
Your Bitcoin has been locked.{" "}
|
||||
{btc_lock_confirmations > 0
|
||||
{btc_lock_confirmations !== undefined && btc_lock_confirmations > 0
|
||||
? "We are waiting for the other party to lock their Monero."
|
||||
: "We are waiting for the blockchain to confirm the transaction. Once confirmed, the other party will lock their Monero."}
|
||||
</DialogContentText>
|
||||
|
@ -30,9 +32,10 @@ export default function BitcoinLockTxInMempoolPage({
|
|||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
{btc_lock_confirmations >= BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
||||
<SwapStatusAlert swap={swapInfo} isRunning={true} />
|
||||
)}
|
||||
{btc_lock_confirmations !== undefined &&
|
||||
btc_lock_confirmations >= BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
||||
<SwapStatusAlert swap={swapInfo} isRunning={true} />
|
||||
)}
|
||||
<BitcoinTransactionInfoBox
|
||||
title="Bitcoin Lock Transaction"
|
||||
txId={btc_lock_txid}
|
||||
|
@ -43,7 +46,7 @@ export default function BitcoinLockTxInMempoolPage({
|
|||
After they lock their funds and the Monero transaction receives
|
||||
one confirmation, the swap will proceed to the next step.
|
||||
<br />
|
||||
Confirmations: {btc_lock_confirmations}
|
||||
Confirmations: {formatConfirmations(btc_lock_confirmations)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Box, DialogContentText } from "@mui/material";
|
||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||
import { formatConfirmations } from "utils/formatUtils";
|
||||
import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
|
||||
|
||||
export default function XmrLockTxInMempoolPage({
|
||||
xmr_lock_tx_confirmations,
|
||||
xmr_lock_txid,
|
||||
}: TauriSwapProgressEventContent<"XmrLockTxInMempool">) {
|
||||
const additionalContent = `Confirmations: ${xmr_lock_tx_confirmations}/10`;
|
||||
const additionalContent = `Confirmations: ${formatConfirmations(xmr_lock_tx_confirmations, 10)}`;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
|
|
@ -0,0 +1,211 @@
|
|||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||
import { ReliableNodeInfo } from "models/tauriModel";
|
||||
import NetworkWifiIcon from "@mui/icons-material/NetworkWifi";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
|
||||
export default function MoneroPoolHealthBox() {
|
||||
const { poolStatus, isLoading } = useAppSelector((state) => ({
|
||||
poolStatus: state.pool.status,
|
||||
isLoading: state.pool.isLoading,
|
||||
}));
|
||||
const theme = useTheme();
|
||||
|
||||
const formatLatency = (latencyMs?: number) => {
|
||||
if (latencyMs === undefined || latencyMs === null) return "N/A";
|
||||
return `${Math.round(latencyMs)}ms`;
|
||||
};
|
||||
|
||||
const formatSuccessRate = (rate: number) => {
|
||||
return `${(rate * 100).toFixed(1)}%`;
|
||||
};
|
||||
|
||||
const getHealthColor = (healthyCount: number, reliableCount: number) => {
|
||||
if (reliableCount === 0) return theme.palette.error.main;
|
||||
if (reliableCount < 3) return theme.palette.warning.main;
|
||||
return theme.palette.success.main;
|
||||
};
|
||||
|
||||
const renderHealthSummary = () => {
|
||||
if (!poolStatus) return null;
|
||||
|
||||
const totalChecks =
|
||||
poolStatus.successful_health_checks +
|
||||
poolStatus.unsuccessful_health_checks;
|
||||
const overallSuccessRate =
|
||||
totalChecks > 0
|
||||
? (poolStatus.successful_health_checks / totalChecks) * 100
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap" }}>
|
||||
<Chip
|
||||
label={`${poolStatus.total_node_count} Total Known`}
|
||||
color="info"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
<Chip
|
||||
label={`${poolStatus.healthy_node_count} Healthy`}
|
||||
color={poolStatus.healthy_node_count > 0 ? "success" : "error"}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
<Chip
|
||||
label={`${(100 - overallSuccessRate).toFixed(1)}% Retry Rate`}
|
||||
color={
|
||||
overallSuccessRate > 80
|
||||
? "success"
|
||||
: overallSuccessRate > 60
|
||||
? "warning"
|
||||
: "error"
|
||||
}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTopNodes = () => {
|
||||
if (!poolStatus || poolStatus.top_reliable_nodes.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontSize: "1rem" }}>
|
||||
🚧
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Bootstrapping remote Monero node registry... But you can already
|
||||
start swapping!
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Node URL</TableCell>
|
||||
<TableCell align="right">Success Rate</TableCell>
|
||||
<TableCell align="right">Avg Latency</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{poolStatus.top_reliable_nodes.map(
|
||||
(node: ReliableNodeInfo, index: number) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{ wordBreak: "break-all" }}
|
||||
>
|
||||
{node.url}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="caption">
|
||||
{formatSuccessRate(node.success_rate)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="caption">
|
||||
{formatLatency(node.avg_latency_ms)}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
// Show bootstrapping message when no data is available
|
||||
if (!poolStatus && !isLoading) {
|
||||
return (
|
||||
<InfoBox
|
||||
title={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<NetworkWifiIcon />
|
||||
Monero Pool Health
|
||||
</Box>
|
||||
}
|
||||
mainContent={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<Typography variant="h2" sx={{ fontSize: "1.5rem" }}>
|
||||
🚧
|
||||
</Typography>
|
||||
<Typography variant="subtitle2">
|
||||
Bootstrapping pool health monitoring. You can already start using
|
||||
the app!
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
additionalContent={null}
|
||||
icon={null}
|
||||
loading={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title={
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<NetworkWifiIcon />
|
||||
Monero Pool Health
|
||||
</Box>
|
||||
}
|
||||
mainContent={
|
||||
<Typography variant="subtitle2">
|
||||
Real-time health monitoring of the Monero node pool. Shows node
|
||||
availability, success rates, and performance metrics.
|
||||
</Typography>
|
||||
}
|
||||
additionalContent={
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{poolStatus && renderHealthSummary()}
|
||||
|
||||
{poolStatus && (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ mb: 1, fontWeight: "medium" }}>
|
||||
Health Check Statistics
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap" }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Successful:{" "}
|
||||
{poolStatus.successful_health_checks.toLocaleString()}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Failed:{" "}
|
||||
{poolStatus.unsuccessful_health_checks.toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>{renderTopNodes()}</Box>
|
||||
</Box>
|
||||
}
|
||||
icon={null}
|
||||
loading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -20,6 +20,11 @@ import {
|
|||
useTheme,
|
||||
Switch,
|
||||
SelectChangeEvent,
|
||||
TextField,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
addNode,
|
||||
|
@ -35,11 +40,13 @@ import {
|
|||
setFiatCurrency,
|
||||
setTheme,
|
||||
setTorEnabled,
|
||||
setUseMoneroRpcPool,
|
||||
} from "store/features/settingsSlice";
|
||||
import { useAppDispatch, useNodes, useSettings } from "store/hooks";
|
||||
import ValidatedTextField from "renderer/components/other/ValidatedTextField";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import HelpIcon from "@mui/icons-material/HelpOutline";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { ReactNode, useState, useEffect } from "react";
|
||||
import { Theme } from "renderer/components/theme";
|
||||
import {
|
||||
Add,
|
||||
|
@ -47,12 +54,18 @@ import {
|
|||
Delete,
|
||||
Edit,
|
||||
HourglassEmpty,
|
||||
Refresh,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import { getNetwork } from "store/config";
|
||||
import { currencySymbol } from "utils/formatUtils";
|
||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||
import { isValidMultiAddressWithPeerId } from "utils/parseUtils";
|
||||
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import { getNodeStatus } from "renderer/rpc";
|
||||
import { setStatus } from "store/features/nodesSlice";
|
||||
|
||||
const PLACEHOLDER_ELECTRUM_RPC_URL = "ssl://blockstream.info:700";
|
||||
const PLACEHOLDER_MONERO_NODE_URL = "http://xmr-node.cakewallet.com:18081";
|
||||
|
||||
|
@ -83,6 +96,7 @@ export default function SettingsBox() {
|
|||
<TableBody>
|
||||
<TorSettings />
|
||||
<ElectrumRpcUrlSetting />
|
||||
<MoneroRpcPoolSetting />
|
||||
<MoneroNodeUrlSetting />
|
||||
<FetchFiatPricesSetting />
|
||||
<ThemeSetting />
|
||||
|
@ -268,15 +282,21 @@ function ElectrumRpcUrlSetting() {
|
|||
function SettingLabel({
|
||||
label,
|
||||
tooltip,
|
||||
disabled = false,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
tooltip: string | null;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const opacity = disabled ? 0.5 : 1;
|
||||
|
||||
return (
|
||||
<Box style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||
<Box
|
||||
style={{ display: "flex", alignItems: "center", gap: "0.5rem", opacity }}
|
||||
>
|
||||
<Box>{label}</Box>
|
||||
<Tooltip title={tooltip}>
|
||||
<IconButton size="small">
|
||||
<IconButton size="small" disabled={disabled}>
|
||||
<HelpIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
@ -285,38 +305,147 @@ function SettingLabel({
|
|||
}
|
||||
|
||||
/**
|
||||
* A setting that allows you to select the Monero Node URL to use.
|
||||
* A setting that allows you to toggle between using the Monero RPC Pool and custom nodes.
|
||||
*/
|
||||
function MoneroRpcPoolSetting() {
|
||||
const useMoneroRpcPool = useSettings((s) => s.useMoneroRpcPool);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChange = (
|
||||
event: React.MouseEvent<HTMLElement>,
|
||||
newValue: string,
|
||||
) => {
|
||||
if (newValue !== null) {
|
||||
dispatch(setUseMoneroRpcPool(newValue === "pool"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel
|
||||
label="Monero Node Selection"
|
||||
tooltip="Choose between using a load-balanced pool of Monero nodes for better reliability, or configure custom Monero nodes."
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
value={useMoneroRpcPool ? "pool" : "custom"}
|
||||
exclusive
|
||||
onChange={handleChange}
|
||||
aria-label="Monero node selection"
|
||||
size="small"
|
||||
>
|
||||
<ToggleButton value="pool">Pool (Recommended)</ToggleButton>
|
||||
<ToggleButton value="custom">Manual</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A setting that allows you to configure a single Monero Node URL.
|
||||
* Gets disabled when RPC pool is enabled.
|
||||
*/
|
||||
function MoneroNodeUrlSetting() {
|
||||
const network = getNetwork();
|
||||
const [tableVisible, setTableVisible] = useState(false);
|
||||
const useMoneroRpcPool = useSettings((s) => s.useMoneroRpcPool);
|
||||
const moneroNodeUrl = useSettings((s) => s.nodes[network][Blockchain.Monero][0] || "");
|
||||
const nodeStatuses = useNodes((s) => s.nodes);
|
||||
const dispatch = useAppDispatch();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const isValid = (url: string) => isValidUrl(url, ["http"]);
|
||||
const currentNodes = useSettings((s) => s.nodes[network][Blockchain.Monero]);
|
||||
|
||||
const handleNodeUrlChange = (newUrl: string) => {
|
||||
// Remove existing nodes and add the new one
|
||||
currentNodes.forEach(node => {
|
||||
dispatch(removeNode({ network, type: Blockchain.Monero, node }));
|
||||
});
|
||||
|
||||
if (newUrl.trim()) {
|
||||
dispatch(addNode({ network, type: Blockchain.Monero, node: newUrl.trim() }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefreshStatus = async () => {
|
||||
// Don't refresh if pool is enabled or no node URL is configured
|
||||
if (!moneroNodeUrl || useMoneroRpcPool) return;
|
||||
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
const status = await getNodeStatus(moneroNodeUrl, Blockchain.Monero, network);
|
||||
|
||||
// Update the status in the store
|
||||
dispatch(setStatus({ node: moneroNodeUrl, status, blockchain: Blockchain.Monero }));
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh node status:", error);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isValid = (url: string) => url === "" || isValidUrl(url, ["http"]);
|
||||
const nodeStatus = moneroNodeUrl ? nodeStatuses[Blockchain.Monero][moneroNodeUrl] : null;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel
|
||||
label="Custom Monero Node URL"
|
||||
tooltip="This is the URL of the Monero node that the GUI will connect to. Ensure the node is listening for RPC connections over HTTP. If you leave this field empty, the GUI will choose from a list of known nodes at random."
|
||||
tooltip={
|
||||
useMoneroRpcPool
|
||||
? "This setting is disabled because Monero RPC pool is enabled. Disable the RPC pool to configure a custom node."
|
||||
: "This is the URL of the Monero node that the GUI will connect to. It is used to sync Monero transactions. If you leave this field empty, the GUI will choose from a list of known servers at random."
|
||||
}
|
||||
disabled={useMoneroRpcPool}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton onClick={() => setTableVisible(!tableVisible)} size="large">
|
||||
<Edit />
|
||||
</IconButton>
|
||||
{tableVisible ? (
|
||||
<NodeTableModal
|
||||
open={tableVisible}
|
||||
onClose={() => setTableVisible(false)}
|
||||
network={network}
|
||||
blockchain={Blockchain.Monero}
|
||||
isValid={isValid}
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<ValidatedTextField
|
||||
value={moneroNodeUrl}
|
||||
onValidatedChange={handleNodeUrlChange}
|
||||
placeholder={PLACEHOLDER_MONERO_NODE_URL}
|
||||
disabled={useMoneroRpcPool}
|
||||
fullWidth
|
||||
isValid={isValid}
|
||||
variant="outlined"
|
||||
noErrorWhenEmpty
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<>
|
||||
<Tooltip title={
|
||||
useMoneroRpcPool
|
||||
? "Node status checking is disabled when using the pool"
|
||||
: !moneroNodeUrl
|
||||
? "Enter a node URL to check status"
|
||||
: "Node status"
|
||||
}>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Circle
|
||||
color={useMoneroRpcPool || !moneroNodeUrl ? "gray" : (nodeStatus ? "green" : "red")}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Tooltip title={
|
||||
useMoneroRpcPool
|
||||
? "Node status refresh is disabled when using the pool"
|
||||
: !moneroNodeUrl
|
||||
? "Enter a node URL to refresh status"
|
||||
: "Refresh node status"
|
||||
}>
|
||||
<IconButton
|
||||
onClick={handleRefreshStatus}
|
||||
disabled={isRefreshing || useMoneroRpcPool || !moneroNodeUrl}
|
||||
size="small"
|
||||
>
|
||||
{isRefreshing ? <HourglassEmpty /> : <Refresh />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
@ -380,7 +509,7 @@ function NodeTableModal({
|
|||
When the daemon is started, it will attempt to connect to the first
|
||||
available {blockchain} node in this list. If you leave this field
|
||||
empty or all nodes are unavailable, it will choose from a list of
|
||||
known nodes at random. Requires a restart to take effect.
|
||||
known nodes at random.
|
||||
</Typography>
|
||||
<NodeTable
|
||||
network={network}
|
||||
|
@ -413,38 +542,6 @@ function Circle({ color, radius = 6 }: { color: string; radius?: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a status indicator for a node
|
||||
*/
|
||||
function NodeStatus({ status }: { status: boolean | undefined }) {
|
||||
const theme = useTheme();
|
||||
|
||||
switch (status) {
|
||||
case true:
|
||||
return (
|
||||
<Tooltip
|
||||
title={"This node is available and responding to RPC requests"}
|
||||
>
|
||||
<Circle color={theme.palette.success.dark} />
|
||||
</Tooltip>
|
||||
);
|
||||
case false:
|
||||
return (
|
||||
<Tooltip
|
||||
title={"This node is not available or not responding to RPC requests"}
|
||||
>
|
||||
<Circle color={theme.palette.error.dark} />
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tooltip title={"The status of this node is currently unknown"}>
|
||||
<HourglassEmpty />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A table that displays the available nodes for a given network and blockchain.
|
||||
* It allows you to add, remove, and move nodes up the list.
|
||||
|
@ -515,7 +612,9 @@ function NodeTable({
|
|||
</TableCell>
|
||||
{/* Node status icon */}
|
||||
<TableCell align="center">
|
||||
<NodeStatus status={nodeStatuses[blockchain][node]} />
|
||||
<Circle
|
||||
color={nodeStatuses[blockchain][node] ? "green" : "red"}
|
||||
/>
|
||||
</TableCell>
|
||||
{/* Remove and move buttons */}
|
||||
<TableCell>
|
||||
|
@ -582,7 +681,7 @@ export function TorSettings() {
|
|||
<TableCell>
|
||||
<SettingLabel
|
||||
label="Use Tor"
|
||||
tooltip="Tor (The Onion Router) is a decentralized network allowing for anonymous browsing. If enabled, the app will use its internal Tor client to hide your IP address from the maker. Requires a restart to take effect."
|
||||
tooltip="Route network traffic through Tor to hide your IP address from the maker."
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import DaemonControlBox from "./DaemonControlBox";
|
|||
import SettingsBox from "./SettingsBox";
|
||||
import ExportDataBox from "./ExportDataBox";
|
||||
import DiscoveryBox from "./DiscoveryBox";
|
||||
import MoneroPoolHealthBox from "./MoneroPoolHealthBox";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
|
@ -29,6 +30,7 @@ export default function SettingsPage() {
|
|||
>
|
||||
<SettingsBox />
|
||||
<DiscoveryBox />
|
||||
<MoneroPoolHealthBox />
|
||||
<ExportDataBox />
|
||||
<DaemonControlBox />
|
||||
<DonateInfoBox />
|
||||
|
|
|
@ -223,37 +223,20 @@ export async function initializeContext() {
|
|||
const bitcoinNodes =
|
||||
store.getState().settings.nodes[network][Blockchain.Bitcoin];
|
||||
|
||||
// For Monero nodes, check availability and use the first working one
|
||||
const moneroNodes =
|
||||
store.getState().settings.nodes[network][Blockchain.Monero];
|
||||
let moneroNode = null;
|
||||
|
||||
if (moneroNodes.length > 0) {
|
||||
try {
|
||||
moneroNode = await Promise.any(
|
||||
moneroNodes.map(async (node) => {
|
||||
const isAvailable = await getNodeStatus(
|
||||
node,
|
||||
Blockchain.Monero,
|
||||
network,
|
||||
);
|
||||
if (isAvailable) {
|
||||
return node;
|
||||
}
|
||||
throw new Error(`Monero node ${node} is not available`);
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// If no Monero node is available, use null
|
||||
moneroNode = null;
|
||||
}
|
||||
}
|
||||
// For Monero nodes, get the configured node URL and pool setting
|
||||
const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool;
|
||||
const moneroNodes = store.getState().settings.nodes[network][Blockchain.Monero];
|
||||
|
||||
// Always pass the first configured monero node URL directly without checking availability
|
||||
// The backend will handle whether to use the pool or the custom node
|
||||
const moneroNode = moneroNodes.length > 0 ? moneroNodes[0] : null;
|
||||
|
||||
// Initialize Tauri settings
|
||||
const tauriSettings: TauriSettings = {
|
||||
electrum_rpc_urls: bitcoinNodes,
|
||||
monero_node_url: moneroNode,
|
||||
use_tor: useTor,
|
||||
use_monero_rpc_pool: useMoneroRpcPool,
|
||||
};
|
||||
|
||||
logger.info("Initializing context with settings", tauriSettings);
|
||||
|
@ -325,13 +308,15 @@ export async function updateAllNodeStatuses() {
|
|||
const network = getNetwork();
|
||||
const settings = store.getState().settings;
|
||||
|
||||
// Only check Monero nodes, skip Bitcoin nodes since we pass all electrum servers
|
||||
// to the backend without checking them (ElectrumBalancer handles failover)
|
||||
await Promise.all(
|
||||
settings.nodes[network][Blockchain.Monero].map((node) =>
|
||||
updateNodeStatus(node, Blockchain.Monero, network),
|
||||
),
|
||||
);
|
||||
// Only check Monero nodes if we're using custom nodes (not RPC pool)
|
||||
// Skip Bitcoin nodes since we pass all electrum servers to the backend without checking them (ElectrumBalancer handles failover)
|
||||
if (!settings.useMoneroRpcPool) {
|
||||
await Promise.all(
|
||||
settings.nodes[network][Blockchain.Monero].map((node) =>
|
||||
updateNodeStatus(node, Blockchain.Monero, network),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMoneroAddresses(): Promise<GetMoneroAddressesResponse> {
|
||||
|
@ -361,3 +346,9 @@ export async function saveLogFiles(
|
|||
): Promise<void> {
|
||||
await invokeUnsafe<void>("save_txt_files", { zipFileName, content });
|
||||
}
|
||||
|
||||
export async function saveFilesInDialog(files: Record<string, string>) {
|
||||
await invokeUnsafe<void>("save_txt_files", {
|
||||
files,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import torSlice from "./features/torSlice";
|
|||
import settingsSlice from "./features/settingsSlice";
|
||||
import nodesSlice from "./features/nodesSlice";
|
||||
import conversationsSlice from "./features/conversationsSlice";
|
||||
import poolSlice from "./features/poolSlice";
|
||||
|
||||
export const reducers = {
|
||||
swap: swapReducer,
|
||||
|
@ -18,4 +19,5 @@ export const reducers = {
|
|||
settings: settingsSlice,
|
||||
nodes: nodesSlice,
|
||||
conversations: conversationsSlice,
|
||||
pool: poolSlice,
|
||||
};
|
||||
|
|
31
src-gui/src/store/features/poolSlice.ts
Normal file
31
src-gui/src/store/features/poolSlice.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { PoolStatus } from "models/tauriModel";
|
||||
|
||||
interface PoolSlice {
|
||||
status: PoolStatus | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const initialState: PoolSlice = {
|
||||
status: null,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
export const poolSlice = createSlice({
|
||||
name: "pool",
|
||||
initialState,
|
||||
reducers: {
|
||||
poolStatusReceived(slice, action: PayloadAction<PoolStatus>) {
|
||||
slice.status = action.payload;
|
||||
slice.isLoading = false;
|
||||
},
|
||||
poolStatusReset(slice) {
|
||||
slice.status = null;
|
||||
slice.isLoading = true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { poolStatusReceived, poolStatusReset } = poolSlice.actions;
|
||||
|
||||
export default poolSlice.reducer;
|
|
@ -17,6 +17,8 @@ export interface SettingsState {
|
|||
fiatCurrency: FiatCurrency;
|
||||
/// Whether to enable Tor for p2p connections
|
||||
enableTor: boolean;
|
||||
/// Whether to use the Monero RPC pool for load balancing (true) or custom nodes (false)
|
||||
useMoneroRpcPool: boolean;
|
||||
userHasSeenIntroduction: boolean;
|
||||
/// List of rendezvous points
|
||||
rendezvousPoints: string[];
|
||||
|
@ -119,6 +121,7 @@ const initialState: SettingsState = {
|
|||
fetchFiatPrices: false,
|
||||
fiatCurrency: FiatCurrency.Usd,
|
||||
enableTor: true,
|
||||
useMoneroRpcPool: true, // Default to using RPC pool
|
||||
userHasSeenIntroduction: false,
|
||||
rendezvousPoints: DEFAULT_RENDEZVOUS_POINTS,
|
||||
};
|
||||
|
@ -206,6 +209,9 @@ const alertsSlice = createSlice({
|
|||
setTorEnabled(slice, action: PayloadAction<boolean>) {
|
||||
slice.enableTor = action.payload;
|
||||
},
|
||||
setUseMoneroRpcPool(slice, action: PayloadAction<boolean>) {
|
||||
slice.useMoneroRpcPool = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -218,6 +224,7 @@ export const {
|
|||
setFetchFiatPrices,
|
||||
setFiatCurrency,
|
||||
setTorEnabled,
|
||||
setUseMoneroRpcPool,
|
||||
setUserHasSeenIntroduction,
|
||||
addRendezvousPoint,
|
||||
removeRendezvousPoint,
|
||||
|
|
|
@ -83,3 +83,24 @@ export function currencySymbol(currency: FiatCurrency): string | null {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats confirmation count, displaying "?" when the transaction state is unknown
|
||||
* @param confirmations - The number of confirmations, or undefined if unknown
|
||||
* @param maxConfirmations - Optional maximum confirmations to show as "X/Y" format
|
||||
* @returns Formatted string showing confirmations or "?" if unknown
|
||||
*/
|
||||
export function formatConfirmations(
|
||||
confirmations: number | undefined | null,
|
||||
maxConfirmations?: number,
|
||||
): string {
|
||||
if (confirmations === undefined || confirmations === null) {
|
||||
return maxConfirmations !== undefined ? `?/${maxConfirmations}` : "?";
|
||||
}
|
||||
|
||||
if (maxConfirmations !== undefined) {
|
||||
return `${confirmations}/${maxConfirmations}`;
|
||||
}
|
||||
|
||||
return confirmations.toString();
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue