Skip to content

Commit 3e79bb3

Browse files
feat(GUI): Add settings for theme, fiat currency and remote nodes (#128)
1 parent 27d6e23 commit 3e79bb3

37 files changed

+1131
-265
lines changed

src-gui/.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ dist-ssr
2828
src/models/tauriModel.ts
2929

3030
# Env files
31-
.env.*
31+
.env*

src-gui/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
2525
"@tauri-apps/plugin-process": "^2.0.0",
2626
"@tauri-apps/plugin-shell": "^2.0.0",
27-
"@tauri-apps/plugin-store": "2.1.0",
27+
"@tauri-apps/plugin-store": "^2.1.0",
2828
"@tauri-apps/plugin-updater": "^2.0.0",
2929
"humanize-duration": "^3.32.1",
3030
"lodash": "^4.17.21",

src-gui/src/renderer/api.ts

+37-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
// - and to submit feedback
66
// - fetch currency rates from CoinGecko
77
import { Alert, ExtendedProviderStatus } from "models/apiModel";
8+
import { store } from "./store/storeRenderer";
9+
import { setBtcPrice, setXmrBtcRate, setXmrPrice } from "store/features/ratesSlice";
10+
import { FiatCurrency } from "store/features/settingsSlice";
811

912
const API_BASE_URL = "https://api.unstoppableswap.net";
1013

@@ -45,20 +48,20 @@ export async function submitFeedbackViaHttp(
4548
return responseBody.feedbackId;
4649
}
4750

48-
async function fetchCurrencyUsdPrice(currency: string): Promise<number> {
51+
async function fetchCurrencyPrice(currency: string, fiatCurrency: FiatCurrency): Promise<number> {
4952
try {
5053
const response = await fetch(
51-
`https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=usd`,
54+
`https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=${fiatCurrency.toLowerCase()}`,
5255
);
5356
const data = await response.json();
54-
return data[currency].usd;
57+
return data[currency][fiatCurrency.toLowerCase()];
5558
} catch (error) {
5659
console.error(`Error fetching ${currency} price:`, error);
5760
throw error;
5861
}
5962
}
6063

61-
export async function fetchXmrBtcRate(): Promise<number> {
64+
async function fetchXmrBtcRate(): Promise<number> {
6265
try {
6366
const response = await fetch('https://api.kraken.com/0/public/Ticker?pair=XMRXBT');
6467
const data = await response.json();
@@ -78,10 +81,35 @@ export async function fetchXmrBtcRate(): Promise<number> {
7881
}
7982

8083

81-
export async function fetchBtcPrice(): Promise<number> {
82-
return fetchCurrencyUsdPrice("bitcoin");
84+
async function fetchBtcPrice(fiatCurrency: FiatCurrency): Promise<number> {
85+
return fetchCurrencyPrice("bitcoin", fiatCurrency);
8386
}
8487

85-
export async function fetchXmrPrice(): Promise<number> {
86-
return fetchCurrencyUsdPrice("monero");
87-
}
88+
async function fetchXmrPrice(fiatCurrency: FiatCurrency): Promise<number> {
89+
return fetchCurrencyPrice("monero", fiatCurrency);
90+
}
91+
92+
/**
93+
* If enabled by the user, fetch the XMR, BTC and XMR/BTC rates
94+
* and store them in the Redux store.
95+
*/
96+
export async function updateRates(): Promise<void> {
97+
const settings = store.getState().settings;
98+
if (!settings.fetchFiatPrices)
99+
return;
100+
101+
try {
102+
const xmrBtcRate = await fetchXmrBtcRate();
103+
store.dispatch(setXmrBtcRate(xmrBtcRate));
104+
105+
const btcPrice = await fetchBtcPrice(settings.fiatCurrency);
106+
store.dispatch(setBtcPrice(btcPrice));
107+
108+
const xmrPrice = await fetchXmrPrice(settings.fiatCurrency);
109+
store.dispatch(setXmrPrice(xmrPrice));
110+
111+
console.log(`Fetched rates for ${settings.fiatCurrency}`);
112+
} catch (error) {
113+
console.error("Error fetching rates:", error);
114+
}
115+
}
+46-57
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Box, CssBaseline, makeStyles } from "@material-ui/core";
2-
import { indigo } from "@material-ui/core/colors";
3-
import { createTheme, ThemeProvider } from "@material-ui/core/styles";
2+
import { ThemeProvider } from "@material-ui/core/styles";
43
import "@tauri-apps/plugin-shell";
54
import { Route, MemoryRouter as Router, Routes } from "react-router-dom";
65
import Navigation, { drawerWidth } from "./navigation/Navigation";
@@ -9,15 +8,17 @@ import HistoryPage from "./pages/history/HistoryPage";
98
import SwapPage from "./pages/swap/SwapPage";
109
import WalletPage from "./pages/wallet/WalletPage";
1110
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
12-
import { useEffect } from "react";
13-
import { fetchProvidersViaHttp, fetchAlertsViaHttp, fetchXmrPrice, fetchBtcPrice, fetchXmrBtcRate } from "renderer/api";
14-
import { initEventListeners } from "renderer/rpc";
15-
import { store } from "renderer/store/storeRenderer";
1611
import UpdaterDialog from "./modal/updater/UpdaterDialog";
17-
import { setAlerts } from "store/features/alertsSlice";
18-
import { setRegistryProviders, registryConnectionFailed } from "store/features/providersSlice";
19-
import { setXmrPrice, setBtcPrice, setXmrBtcRate } from "store/features/ratesSlice";
12+
import { useSettings } from "store/hooks";
13+
import { themes } from "./theme";
14+
import { initEventListeners, updateAllNodeStatuses } from "renderer/rpc";
15+
import { fetchAlertsViaHttp, fetchProvidersViaHttp, updateRates } from "renderer/api";
16+
import { store } from "renderer/store/storeRenderer";
2017
import logger from "utils/logger";
18+
import { setAlerts } from "store/features/alertsSlice";
19+
import { setRegistryProviders } from "store/features/providersSlice";
20+
import { registryConnectionFailed } from "store/features/providersSlice";
21+
import { useEffect } from "react";
2122

2223
const useStyles = makeStyles((theme) => ({
2324
innerContent: {
@@ -28,57 +29,44 @@ const useStyles = makeStyles((theme) => ({
2829
},
2930
}));
3031

31-
const theme = createTheme({
32-
palette: {
33-
type: "dark",
34-
primary: {
35-
main: "#f4511e",
36-
},
37-
secondary: indigo,
38-
},
39-
typography: {
40-
overline: {
41-
textTransform: "none", // This prevents the text from being all caps
42-
},
43-
},
44-
});
45-
46-
function InnerContent() {
47-
const classes = useStyles();
48-
49-
return (
50-
<Box className={classes.innerContent}>
51-
<Routes>
52-
<Route path="/swap" element={<SwapPage />} />
53-
<Route path="/history" element={<HistoryPage />} />
54-
<Route path="/wallet" element={<WalletPage />} />
55-
<Route path="/help" element={<HelpPage />} />
56-
<Route path="/" element={<SwapPage />} />
57-
</Routes>
58-
</Box>
59-
);
60-
}
61-
6232
export default function App() {
6333
useEffect(() => {
6434
fetchInitialData();
6535
initEventListeners();
6636
}, []);
6737

38+
const theme = useSettings((s) => s.theme);
39+
6840
return (
69-
<ThemeProvider theme={theme}>
41+
<ThemeProvider theme={themes[theme]}>
7042
<GlobalSnackbarProvider>
7143
<CssBaseline />
7244
<Router>
7345
<Navigation />
7446
<InnerContent />
75-
<UpdaterDialog/>
47+
<UpdaterDialog />
7648
</Router>
7749
</GlobalSnackbarProvider>
7850
</ThemeProvider>
7951
);
8052
}
8153

54+
function InnerContent() {
55+
const classes = useStyles();
56+
57+
return (
58+
<Box className={classes.innerContent}>
59+
<Routes>
60+
<Route path="/swap" element={<SwapPage />} />
61+
<Route path="/history" element={<HistoryPage />} />
62+
<Route path="/wallet" element={<WalletPage />} />
63+
<Route path="/help" element={<HelpPage />} />
64+
<Route path="/" element={<SwapPage />} />
65+
</Routes>
66+
</Box>
67+
);
68+
}
69+
8270
async function fetchInitialData() {
8371
try {
8472
const providerList = await fetchProvidersViaHttp();
@@ -94,30 +82,31 @@ async function fetchInitialData() {
9482
}
9583

9684
try {
97-
const alerts = await fetchAlertsViaHttp();
98-
store.dispatch(setAlerts(alerts));
99-
logger.info({ alerts }, "Fetched alerts via UnstoppableSwap HTTP API");
85+
await updateAllNodeStatuses()
10086
} catch (e) {
101-
logger.error(e, "Failed to fetch alerts via UnstoppableSwap HTTP API");
87+
logger.error(e, "Failed to update node statuses")
10288
}
10389

104-
try {
105-
const xmrPrice = await fetchXmrPrice();
106-
store.dispatch(setXmrPrice(xmrPrice));
107-
logger.info({ xmrPrice }, "Fetched XMR price");
90+
// Update node statuses every 2 minutes
91+
const STATUS_UPDATE_INTERVAL = 2 * 60 * 1_000;
92+
setInterval(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
10893

109-
const btcPrice = await fetchBtcPrice();
110-
store.dispatch(setBtcPrice(btcPrice));
111-
logger.info({ btcPrice }, "Fetched BTC price");
94+
try {
95+
const alerts = await fetchAlertsViaHttp();
96+
store.dispatch(setAlerts(alerts));
97+
logger.info({ alerts }, "Fetched alerts via UnstoppableSwap HTTP API");
11298
} catch (e) {
113-
logger.error(e, "Error retrieving fiat prices");
99+
logger.error(e, "Failed to fetch alerts via UnstoppableSwap HTTP API");
114100
}
115101

116102
try {
117-
const xmrBtcRate = await fetchXmrBtcRate();
118-
store.dispatch(setXmrBtcRate(xmrBtcRate));
119-
logger.info({ xmrBtcRate }, "Fetched XMR/BTC rate");
103+
await updateRates();
104+
logger.info("Fetched XMR/BTC rate");
120105
} catch (e) {
121106
logger.error(e, "Error retrieving XMR/BTC rate");
122107
}
108+
109+
// Update the rates every 5 minutes (to respect the coingecko rate limit)
110+
const RATE_UPDATE_INTERVAL = 5 * 60 * 1_000;
111+
setInterval(updateRates, RATE_UPDATE_INTERVAL);
123112
}

src-gui/src/renderer/components/alert/DaemonStatusAlert.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default function DaemonStatusAlert() {
5050
<Button
5151
size="small"
5252
variant="outlined"
53-
onClick={() => navigate("/help")}
53+
onClick={() => navigate("/help#daemon-control-box")}
5454
>
5555
View Logs
5656
</Button>

src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx

+15-13
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,21 @@ const useStyles = makeStyles((theme) => ({
2626
},
2727
}));
2828

29-
function ProviderSpreadChip({ provider }: { provider: ExtendedProviderStatus }) {
30-
const xmrBtcPrice = useAppSelector(s => s.rates?.xmrBtcRate);
31-
32-
if (xmrBtcPrice === null) {
29+
/**
30+
* A chip that displays the markup of the provider's exchange rate compared to the market rate.
31+
*/
32+
function ProviderMarkupChip({ provider }: { provider: ExtendedProviderStatus }) {
33+
const marketExchangeRate = useAppSelector(s => s.rates?.xmrBtcRate);
34+
if (marketExchangeRate === null)
3335
return null;
34-
}
3536

36-
const providerPrice = satsToBtc(provider.price);
37-
const spread = ((providerPrice - xmrBtcPrice) / xmrBtcPrice) * 100;
37+
const providerExchangeRate = satsToBtc(provider.price);
38+
/** The markup of the exchange rate compared to the market rate in percent */
39+
const markup = (providerExchangeRate - marketExchangeRate) / marketExchangeRate * 100;
3840

3941
return (
40-
<Tooltip title="The spread is the difference between the provider's exchange rate and the market rate. A high spread indicates that the provider is charging more than the market rate.">
41-
<Chip label={`Spread: ${spread.toFixed(2)} %`} />
42+
<Tooltip title="The markup this provider charges compared to centralized markets. A lower markup means that you get more Monero for your Bitcoin.">
43+
<Chip label={`Markup ${markup.toFixed(2)}%`} />
4244
</Tooltip>
4345
);
4446

@@ -74,8 +76,8 @@ export default function ProviderInfo({
7476
<Box className={classes.chipsOuter}>
7577
<Chip label={provider.testnet ? "Testnet" : "Mainnet"} />
7678
{provider.uptime && (
77-
<Tooltip title="A high uptime indicates reliability. Providers with low uptime may be unreliable and cause swaps to take longer to complete or fail entirely.">
78-
<Chip label={`${Math.round(provider.uptime * 100)} % uptime`} />
79+
<Tooltip title="A high uptime (>90%) indicates reliability. Providers with very low uptime may be unreliable and cause swaps to take longer to complete or fail entirely.">
80+
<Chip label={`${Math.round(provider.uptime * 100)}% uptime`} />
7981
</Tooltip>
8082
)}
8183
{provider.age ? (
@@ -93,11 +95,11 @@ export default function ProviderInfo({
9395
</Tooltip>
9496
)}
9597
{isOutdated && (
96-
<Tooltip title="This provider is running an outdated version of the software. Outdated providers may be unreliable and cause swaps to take longer to complete or fail entirely.">
98+
<Tooltip title="This provider is running an older version of the software. Outdated providers may be unreliable and cause swaps to take longer to complete or fail entirely.">
9799
<Chip label="Outdated" icon={<WarningIcon />} color="primary" />
98100
</Tooltip>
99101
)}
100-
<ProviderSpreadChip provider={provider} />
102+
<ProviderMarkupChip provider={provider} />
101103
</Box>
102104
</Box>
103105
);

src-gui/src/renderer/components/modal/swap/InfoBox.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { ReactNode } from "react";
99

1010
type Props = {
11+
id?: string;
1112
title: ReactNode;
1213
mainContent: ReactNode;
1314
additionalContent: ReactNode;
@@ -31,6 +32,7 @@ const useStyles = makeStyles((theme) => ({
3132
}));
3233

3334
export default function InfoBox({
35+
id = null,
3436
title,
3537
mainContent,
3638
additionalContent,
@@ -40,7 +42,7 @@ export default function InfoBox({
4042
const classes = useStyles();
4143

4244
return (
43-
<Paper variant="outlined" className={classes.outer}>
45+
<Paper variant="outlined" className={classes.outer} id={id}>
4446
<Typography variant="subtitle1">{title}</Typography>
4547
<Box className={classes.upperContent}>
4648
{icon}

src-gui/src/renderer/components/navigation/NavigationHeader.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default function NavigationHeader() {
2121
<RouteListItemIconButton name="Wallet" route="/wallet">
2222
<AccountBalanceWalletIcon />
2323
</RouteListItemIconButton>
24-
<RouteListItemIconButton name="Help" route="/help">
24+
<RouteListItemIconButton name="Help & Settings" route="/help">
2525
<HelpOutlineIcon />
2626
</RouteListItemIconButton>
2727
</List>

0 commit comments

Comments
 (0)