Skip to content

Commit

Permalink
feat(GUI): Add settings for theme, fiat currency and remote nodes (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
Einliterflasche authored Nov 13, 2024
1 parent 27d6e23 commit 3e79bb3
Show file tree
Hide file tree
Showing 37 changed files with 1,131 additions and 265 deletions.
2 changes: 1 addition & 1 deletion src-gui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ dist-ssr
src/models/tauriModel.ts

# Env files
.env.*
.env*
2 changes: 1 addition & 1 deletion src-gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.0",
"@tauri-apps/plugin-store": "2.1.0",
"@tauri-apps/plugin-store": "^2.1.0",
"@tauri-apps/plugin-updater": "^2.0.0",
"humanize-duration": "^3.32.1",
"lodash": "^4.17.21",
Expand Down
46 changes: 37 additions & 9 deletions src-gui/src/renderer/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
// - and to submit feedback
// - fetch currency rates from CoinGecko
import { Alert, ExtendedProviderStatus } from "models/apiModel";
import { store } from "./store/storeRenderer";
import { setBtcPrice, setXmrBtcRate, setXmrPrice } from "store/features/ratesSlice";
import { FiatCurrency } from "store/features/settingsSlice";

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

Expand Down Expand Up @@ -45,20 +48,20 @@ export async function submitFeedbackViaHttp(
return responseBody.feedbackId;
}

async function fetchCurrencyUsdPrice(currency: string): Promise<number> {
async function fetchCurrencyPrice(currency: string, fiatCurrency: FiatCurrency): Promise<number> {
try {
const response = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=usd`,
`https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=${fiatCurrency.toLowerCase()}`,
);
const data = await response.json();
return data[currency].usd;
return data[currency][fiatCurrency.toLowerCase()];
} catch (error) {
console.error(`Error fetching ${currency} price:`, error);
throw error;
}
}

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


export async function fetchBtcPrice(): Promise<number> {
return fetchCurrencyUsdPrice("bitcoin");
async function fetchBtcPrice(fiatCurrency: FiatCurrency): Promise<number> {
return fetchCurrencyPrice("bitcoin", fiatCurrency);
}

export async function fetchXmrPrice(): Promise<number> {
return fetchCurrencyUsdPrice("monero");
}
async function fetchXmrPrice(fiatCurrency: FiatCurrency): Promise<number> {
return fetchCurrencyPrice("monero", fiatCurrency);
}

/**
* If enabled by the user, fetch the XMR, BTC and XMR/BTC rates
* and store them in the Redux store.
*/
export async function updateRates(): Promise<void> {
const settings = store.getState().settings;
if (!settings.fetchFiatPrices)
return;

try {
const xmrBtcRate = await fetchXmrBtcRate();
store.dispatch(setXmrBtcRate(xmrBtcRate));

const btcPrice = await fetchBtcPrice(settings.fiatCurrency);
store.dispatch(setBtcPrice(btcPrice));

const xmrPrice = await fetchXmrPrice(settings.fiatCurrency);
store.dispatch(setXmrPrice(xmrPrice));

console.log(`Fetched rates for ${settings.fiatCurrency}`);
} catch (error) {
console.error("Error fetching rates:", error);
}
}
103 changes: 46 additions & 57 deletions src-gui/src/renderer/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Box, CssBaseline, makeStyles } from "@material-ui/core";
import { indigo } from "@material-ui/core/colors";
import { createTheme, ThemeProvider } from "@material-ui/core/styles";
import { ThemeProvider } from "@material-ui/core/styles";
import "@tauri-apps/plugin-shell";
import { Route, MemoryRouter as Router, Routes } from "react-router-dom";
import Navigation, { drawerWidth } from "./navigation/Navigation";
Expand All @@ -9,15 +8,17 @@ import HistoryPage from "./pages/history/HistoryPage";
import SwapPage from "./pages/swap/SwapPage";
import WalletPage from "./pages/wallet/WalletPage";
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
import { useEffect } from "react";
import { fetchProvidersViaHttp, fetchAlertsViaHttp, fetchXmrPrice, fetchBtcPrice, fetchXmrBtcRate } from "renderer/api";
import { initEventListeners } from "renderer/rpc";
import { store } from "renderer/store/storeRenderer";
import UpdaterDialog from "./modal/updater/UpdaterDialog";
import { setAlerts } from "store/features/alertsSlice";
import { setRegistryProviders, registryConnectionFailed } from "store/features/providersSlice";
import { setXmrPrice, setBtcPrice, setXmrBtcRate } from "store/features/ratesSlice";
import { useSettings } from "store/hooks";
import { themes } from "./theme";
import { initEventListeners, updateAllNodeStatuses } from "renderer/rpc";
import { fetchAlertsViaHttp, fetchProvidersViaHttp, updateRates } from "renderer/api";
import { store } from "renderer/store/storeRenderer";
import logger from "utils/logger";
import { setAlerts } from "store/features/alertsSlice";
import { setRegistryProviders } from "store/features/providersSlice";
import { registryConnectionFailed } from "store/features/providersSlice";
import { useEffect } from "react";

const useStyles = makeStyles((theme) => ({
innerContent: {
Expand All @@ -28,57 +29,44 @@ const useStyles = makeStyles((theme) => ({
},
}));

const theme = createTheme({
palette: {
type: "dark",
primary: {
main: "#f4511e",
},
secondary: indigo,
},
typography: {
overline: {
textTransform: "none", // This prevents the text from being all caps
},
},
});

function InnerContent() {
const classes = useStyles();

return (
<Box className={classes.innerContent}>
<Routes>
<Route path="/swap" element={<SwapPage />} />
<Route path="/history" element={<HistoryPage />} />
<Route path="/wallet" element={<WalletPage />} />
<Route path="/help" element={<HelpPage />} />
<Route path="/" element={<SwapPage />} />
</Routes>
</Box>
);
}

export default function App() {
useEffect(() => {
fetchInitialData();
initEventListeners();
}, []);

const theme = useSettings((s) => s.theme);

return (
<ThemeProvider theme={theme}>
<ThemeProvider theme={themes[theme]}>
<GlobalSnackbarProvider>
<CssBaseline />
<Router>
<Navigation />
<InnerContent />
<UpdaterDialog/>
<UpdaterDialog />
</Router>
</GlobalSnackbarProvider>
</ThemeProvider>
);
}

function InnerContent() {
const classes = useStyles();

return (
<Box className={classes.innerContent}>
<Routes>
<Route path="/swap" element={<SwapPage />} />
<Route path="/history" element={<HistoryPage />} />
<Route path="/wallet" element={<WalletPage />} />
<Route path="/help" element={<HelpPage />} />
<Route path="/" element={<SwapPage />} />
</Routes>
</Box>
);
}

async function fetchInitialData() {
try {
const providerList = await fetchProvidersViaHttp();
Expand All @@ -94,30 +82,31 @@ async function fetchInitialData() {
}

try {
const alerts = await fetchAlertsViaHttp();
store.dispatch(setAlerts(alerts));
logger.info({ alerts }, "Fetched alerts via UnstoppableSwap HTTP API");
await updateAllNodeStatuses()
} catch (e) {
logger.error(e, "Failed to fetch alerts via UnstoppableSwap HTTP API");
logger.error(e, "Failed to update node statuses")
}

try {
const xmrPrice = await fetchXmrPrice();
store.dispatch(setXmrPrice(xmrPrice));
logger.info({ xmrPrice }, "Fetched XMR price");
// Update node statuses every 2 minutes
const STATUS_UPDATE_INTERVAL = 2 * 60 * 1_000;
setInterval(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);

const btcPrice = await fetchBtcPrice();
store.dispatch(setBtcPrice(btcPrice));
logger.info({ btcPrice }, "Fetched BTC price");
try {
const alerts = await fetchAlertsViaHttp();
store.dispatch(setAlerts(alerts));
logger.info({ alerts }, "Fetched alerts via UnstoppableSwap HTTP API");
} catch (e) {
logger.error(e, "Error retrieving fiat prices");
logger.error(e, "Failed to fetch alerts via UnstoppableSwap HTTP API");
}

try {
const xmrBtcRate = await fetchXmrBtcRate();
store.dispatch(setXmrBtcRate(xmrBtcRate));
logger.info({ xmrBtcRate }, "Fetched XMR/BTC rate");
await updateRates();
logger.info("Fetched XMR/BTC rate");
} catch (e) {
logger.error(e, "Error retrieving XMR/BTC rate");
}

// Update the rates every 5 minutes (to respect the coingecko rate limit)
const RATE_UPDATE_INTERVAL = 5 * 60 * 1_000;
setInterval(updateRates, RATE_UPDATE_INTERVAL);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default function DaemonStatusAlert() {
<Button
size="small"
variant="outlined"
onClick={() => navigate("/help")}
onClick={() => navigate("/help#daemon-control-box")}
>
View Logs
</Button>
Expand Down
28 changes: 15 additions & 13 deletions src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,21 @@ const useStyles = makeStyles((theme) => ({
},
}));

function ProviderSpreadChip({ provider }: { provider: ExtendedProviderStatus }) {
const xmrBtcPrice = useAppSelector(s => s.rates?.xmrBtcRate);

if (xmrBtcPrice === null) {
/**
* A chip that displays the markup of the provider's exchange rate compared to the market rate.
*/
function ProviderMarkupChip({ provider }: { provider: ExtendedProviderStatus }) {
const marketExchangeRate = useAppSelector(s => s.rates?.xmrBtcRate);
if (marketExchangeRate === null)
return null;
}

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

return (
<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.">
<Chip label={`Spread: ${spread.toFixed(2)} %`} />
<Tooltip title="The markup this provider charges compared to centralized markets. A lower markup means that you get more Monero for your Bitcoin.">
<Chip label={`Markup ${markup.toFixed(2)}%`} />
</Tooltip>
);

Expand Down Expand Up @@ -74,8 +76,8 @@ export default function ProviderInfo({
<Box className={classes.chipsOuter}>
<Chip label={provider.testnet ? "Testnet" : "Mainnet"} />
{provider.uptime && (
<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.">
<Chip label={`${Math.round(provider.uptime * 100)} % uptime`} />
<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.">
<Chip label={`${Math.round(provider.uptime * 100)}% uptime`} />
</Tooltip>
)}
{provider.age ? (
Expand All @@ -93,11 +95,11 @@ export default function ProviderInfo({
</Tooltip>
)}
{isOutdated && (
<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.">
<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.">
<Chip label="Outdated" icon={<WarningIcon />} color="primary" />
</Tooltip>
)}
<ProviderSpreadChip provider={provider} />
<ProviderMarkupChip provider={provider} />
</Box>
</Box>
);
Expand Down
4 changes: 3 additions & 1 deletion src-gui/src/renderer/components/modal/swap/InfoBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { ReactNode } from "react";

type Props = {
id?: string;
title: ReactNode;
mainContent: ReactNode;
additionalContent: ReactNode;
Expand All @@ -31,6 +32,7 @@ const useStyles = makeStyles((theme) => ({
}));

export default function InfoBox({
id = null,
title,
mainContent,
additionalContent,
Expand All @@ -40,7 +42,7 @@ export default function InfoBox({
const classes = useStyles();

return (
<Paper variant="outlined" className={classes.outer}>
<Paper variant="outlined" className={classes.outer} id={id}>
<Typography variant="subtitle1">{title}</Typography>
<Box className={classes.upperContent}>
{icon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function NavigationHeader() {
<RouteListItemIconButton name="Wallet" route="/wallet">
<AccountBalanceWalletIcon />
</RouteListItemIconButton>
<RouteListItemIconButton name="Help" route="/help">
<RouteListItemIconButton name="Help & Settings" route="/help">
<HelpOutlineIcon />
</RouteListItemIconButton>
</List>
Expand Down
Loading

0 comments on commit 3e79bb3

Please sign in to comment.