import { Table, TableBody, TableCell, TableContainer, TableRow, Typography, IconButton, Box, Tooltip, Select, MenuItem, TableHead, Paper, Button, Dialog, DialogContent, DialogActions, DialogTitle, useTheme, Switch, SelectChangeEvent, ToggleButton, ToggleButtonGroup, } from "@mui/material"; import { addNode, addRendezvousPoint, Blockchain, DonateToDevelopmentTip, FiatCurrency, moveUpNode, Network, removeNode, removeRendezvousPoint, resetSettings, setFetchFiatPrices, setFiatCurrency, setTheme, setTorEnabled, setEnableMoneroTor, setUseMoneroRpcPool, setDonateToDevelopment, setMoneroRedeemPolicy, setMoneroRedeemAddress, setBitcoinRefundAddress, setBitcoinRefundPolicy, RedeemPolicy, RefundPolicy, } from "store/features/settingsSlice"; import { useAppDispatch, useNodes, useSettings } from "store/hooks"; import ValidatedTextField from "renderer/components/other/ValidatedTextField"; import HelpIcon from "@mui/icons-material/HelpOutline"; import { ReactNode, useState } from "react"; import { Theme } from "renderer/components/theme"; import { Add, ArrowUpward, Delete, Edit, HourglassEmpty, Refresh, } from "@mui/icons-material"; import { getNetwork } from "store/config"; import { currencySymbol } from "utils/formatUtils"; import InfoBox from "renderer/components/pages/swap/swap/components/InfoBox"; import { isValidMultiAddressWithPeerId } from "utils/parseUtils"; import { getNodeStatus } from "renderer/rpc"; import { setStatus } from "store/features/nodesSlice"; import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField"; import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField"; const PLACEHOLDER_ELECTRUM_RPC_URL = "ssl://blockstream.info:700"; const PLACEHOLDER_MONERO_NODE_URL = "http://xmr-node.cakewallet.com:18081"; /** * The settings box, containing the settings for the GUI. */ export default function SettingsBox() { const theme = useTheme(); return ( Settings } mainContent={ Customize the settings of the GUI. Some of these require a restart to take effect. } additionalContent={ <> {/* Table containing the settings */}
{/* Reset button with a bit of spacing */} ({ mt: theme.spacing(2), })} /> } icon={null} loading={false} /> ); } /** * A button that allows you to reset the settings. * Opens a modal that asks for confirmation first. */ function ResetButton() { const dispatch = useAppDispatch(); const [modalOpen, setModalOpen] = useState(false); const onReset = () => { dispatch(resetSettings()); setModalOpen(false); }; return ( <> setModalOpen(false)}> Reset Settings Are you sure you want to reset the settings? ); } /** * A setting that allows you to enable or disable the fetching of fiat prices. */ function FetchFiatPricesSetting() { const fetchFiatPrices = useSettings((s) => s.fetchFiatPrices); const dispatch = useAppDispatch(); return ( <> dispatch(setFetchFiatPrices(event.currentTarget.checked)) } /> {fetchFiatPrices ? : <>} ); } /** * A setting that allows you to select the fiat currency to display prices in. */ function FiatCurrencySetting() { const fiatCurrency = useSettings((s) => s.fiatCurrency); const dispatch = useAppDispatch(); const onChange = (e: SelectChangeEvent) => dispatch(setFiatCurrency(e.target.value as FiatCurrency)); return ( ); } /** * URL validation function, forces the URL to be in the format of "protocol://host:port/" */ function isValidUrl(url: string, allowedProtocols: string[]): boolean { const urlPattern = new RegExp( `^(${allowedProtocols.join("|")})://[^\\s]+:\\d+/?$`, ); return urlPattern.test(url); } /** * A setting that allows you to select the Electrum RPC URL to use. */ function ElectrumRpcUrlSetting() { const [tableVisible, setTableVisible] = useState(false); const network = getNetwork(); const isValid = (url: string) => isValidUrl(url, ["ssl", "tcp"]); return ( setTableVisible(true)} size="large"> {} {tableVisible ? ( setTableVisible(false)} network={network} blockchain={Blockchain.Bitcoin} isValid={isValid} placeholder={PLACEHOLDER_ELECTRUM_RPC_URL} /> ) : ( <> )} ); } /** * A label for a setting, with a tooltip icon. */ function SettingLabel({ label, tooltip, disabled = false, }: { label: ReactNode; tooltip: string | null; disabled?: boolean; }) { const opacity = disabled ? 0.5 : 1; return ( {label} ); } /** * 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, newValue: string, ) => { if (newValue !== null) { dispatch(setUseMoneroRpcPool(newValue === "pool")); } }; return ( Pool (Recommended) Manual ); } /** * 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 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 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 ( value && handleNodeUrlChange(value)} placeholder={PLACEHOLDER_MONERO_NODE_URL} disabled={useMoneroRpcPool} fullWidth isValid={isValid} variant="outlined" noErrorWhenEmpty /> <> {isRefreshing ? : } ); } /** * A setting that allows you to select the theme of the GUI. */ function ThemeSetting() { const theme = useSettings((s) => s.theme); const dispatch = useAppDispatch(); return ( ); } /** * A modal containing a NodeTable for a given network and blockchain. * It allows you to add, remove, and move nodes up the list. */ function NodeTableModal({ open, onClose, network, isValid, placeholder, blockchain, }: { network: Network; blockchain: Blockchain; isValid: (url: string) => boolean; placeholder: string; open: boolean; onClose: () => void; }) { return ( Available Nodes 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. ); } // Create a circle SVG with a given color and radius function Circle({ color, radius = 6 }: { color: string; radius?: number }) { return ( ); } /** * 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. * It fetches the nodes from the store (nodesSlice) and the statuses of all nodes every 15 seconds. */ function NodeTable({ network, blockchain, isValid, placeholder, }: { network: Network; blockchain: Blockchain; isValid: (url: string) => boolean; placeholder: string; }) { const availableNodes = useSettings((s) => s.nodes[network][blockchain]); const currentNode = availableNodes[0]; const nodeStatuses = useNodes((s) => s.nodes); const [newNode, setNewNode] = useState(""); const dispatch = useAppDispatch(); const onAddNewNode = () => { dispatch(addNode({ network, type: blockchain, node: newNode })); setNewNode(""); }; const onRemoveNode = (node: string) => dispatch(removeNode({ network, type: blockchain, node })); const onMoveUpNode = (node: string) => dispatch(moveUpNode({ network, type: blockchain, node })); const moveUpButton = (node: string) => { if (currentNode === node) return <>; return ( onMoveUpNode(node)} size="large"> ); }; return ( {/* Table header row */} Node URL Status Actions {/* Table body rows: one for each node */} {availableNodes.map((node, index) => ( {/* Node URL */} {node} {/* Node status icon */} {/* Remove and move buttons */} onRemoveNode(node)} children={} size="large" /> } /> {moveUpButton(node)} ))} {/* Last row: add a new node */} setNewNode(value ?? "")} placeholder={placeholder} fullWidth isValid={isValid} variant="outlined" noErrorWhenEmpty />
); } export function TorSettings() { const dispatch = useAppDispatch(); const torEnabled = useSettings((settings) => settings.enableTor); const handleChange = (event: React.ChangeEvent) => dispatch(setTorEnabled(event.target.checked)); const status = (state: boolean) => (state === true ? "enabled" : "disabled"); return ( ); } /** * A setting that allows you to enable or disable routing Monero wallet traffic through Tor. * This setting is only visible when Tor is enabled. */ function MoneroTorSettings() { const dispatch = useAppDispatch(); const torEnabled = useSettings((settings) => settings.enableTor); const enableMoneroTor = useSettings((settings) => settings.enableMoneroTor); const handleChange = (event: React.ChangeEvent) => dispatch(setEnableMoneroTor(event.target.checked)); // Hide this setting if Tor is disabled entirely if (!torEnabled) { return null; } return ( ); } /** * A setting that allows you to manage rendezvous points for maker discovery */ function RendezvousPointsSetting() { const [tableVisible, setTableVisible] = useState(false); const rendezvousPoints = useSettings((s) => s.rendezvousPoints); const dispatch = useAppDispatch(); const [newPoint, setNewPoint] = useState(""); const onAddNewPoint = () => { dispatch(addRendezvousPoint(newPoint)); setNewPoint(""); }; const onRemovePoint = (point: string) => { dispatch(removeRendezvousPoint(point)); }; return ( setTableVisible(true)}> {tableVisible && ( setTableVisible(false)} maxWidth="md" fullWidth > Rendezvous Points Add or remove rendezvous points where makers can be discovered. These points help you find trading partners in a decentralized way. Rendezvous Point Actions {rendezvousPoints.map((point, index) => ( {point} onRemovePoint(point)}> ))} setNewPoint(value ?? "") } placeholder="/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa" fullWidth isValid={isValidMultiAddressWithPeerId} variant="outlined" noErrorWhenEmpty />
)}
); } /** * A setting that allows you to set a development donation tip amount */ function DonationTipSetting() { const donateToDevelopment = useSettings((s) => s.donateToDevelopment); const dispatch = useAppDispatch(); const handleTipSelect = (tipAmount: DonateToDevelopmentTip) => { dispatch(setDonateToDevelopment(tipAmount)); }; const formatTipLabel = (tip: DonateToDevelopmentTip) => { if (tip === false) return "0%"; return `${(tip * 100).toFixed(2)}%`; }; const getTipButtonColor = ( tip: DonateToDevelopmentTip, isSelected: boolean, ) => { // Only show colored if selected and > 0 if (isSelected && tip !== false) { return "#198754"; // Green for any tip > 0 } return "#6c757d"; // Gray for all unselected or no tip }; const getTipButtonSelectedColor = (tip: DonateToDevelopmentTip) => { if (tip === false) return "#5c636a"; // Darker gray return "#146c43"; // Darker green for any tip > 0 }; return ( { if (newValue !== null) { handleTipSelect(newValue); } }} aria-label="Development tip amount" size="small" sx={{ width: "100%", gap: 1, "& .MuiToggleButton-root": { flex: 1, borderRadius: "8px", fontWeight: "600", textTransform: "none", border: "2px solid", "&:not(:first-of-type)": { marginLeft: "8px", borderLeft: "2px solid", }, }, }} > {([false, 0.0005, 0.0075] as const).map((tipAmount) => ( {formatTipLabel(tipAmount)} ))}
  • Tips go directly towards paying for infrastructure costs and developers
  • Only ever sent for successful swaps
  • {" "} (refunds are not counted)
  • Monero is used for the tips, giving you full anonymity
); } function RedeemPolicySetting() { const moneroRedeemPolicy = useSettings( (settings) => settings.moneroRedeemPolicy, ); const moneroRedeemAddress = useSettings( (settings) => settings.externalMoneroRedeemAddress, ); const dispatch = useAppDispatch(); return ( <> { if ( newPolicy == RedeemPolicy.Internal || newPolicy == RedeemPolicy.External ) { dispatch(setMoneroRedeemPolicy(newPolicy)); } }} exclusive size="small" > Internal (Recommended) External External Monero redeem address { dispatch(setMoneroRedeemAddress(address)); }} fullWidth variant="outlined" allowEmpty={moneroRedeemPolicy === RedeemPolicy.Internal} /> ); } function RefundPolicySetting() { const bitcoinRefundPolicy = useSettings( (settings) => settings.bitcoinRefundPolicy, ); const bitcoinRefundAddress = useSettings( (settings) => settings.externalBitcoinRefundAddress, ); const dispatch = useAppDispatch(); return ( <> { if ( newPolicy == RefundPolicy.Internal || newPolicy == RefundPolicy.External ) { dispatch(setBitcoinRefundPolicy(newPolicy)); } }} exclusive size="small" > Internal (Recommended) External External Bitcoin refund address { dispatch(setBitcoinRefundAddress(address)); }} fullWidth variant="outlined" disabled={bitcoinRefundPolicy !== RefundPolicy.External} helperText="" /> ); }