refactor(gui): Seperate background refresh logic (#193)

This commit is contained in:
binarybaron 2024-11-19 15:03:55 +01:00 committed by GitHub
parent b409db35d0
commit d953114c49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 151 additions and 159 deletions

View file

@ -4,22 +4,26 @@
// - fetch alerts to be displayed to the user
// - 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";
import { setAlerts } from "store/features/alertsSlice";
import { registryConnectionFailed, setRegistryProviders } from "store/features/providersSlice";
import logger from "utils/logger";
const API_BASE_URL = "https://api.unstoppableswap.net";
const PUBLIC_REGISTRY_API_BASE_URL = "https://api.unstoppableswap.net";
export async function fetchProvidersViaHttp(): Promise<
async function fetchProvidersViaHttp(): Promise<
ExtendedProviderStatus[]
> {
const response = await fetch(`${API_BASE_URL}/api/list`);
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/list`);
return (await response.json()) as ExtendedProviderStatus[];
}
export async function fetchAlertsViaHttp(): Promise<Alert[]> {
const response = await fetch(`${API_BASE_URL}/api/alerts`);
async function fetchAlertsViaHttp(): Promise<Alert[]> {
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/alerts`);
return (await response.json()) as Alert[];
}
@ -31,7 +35,7 @@ export async function submitFeedbackViaHttp(
feedbackId: string;
};
const response = await fetch(`${API_BASE_URL}/api/submit-feedback`, {
const response = await fetch(`${PUBLIC_REGISTRY_API_BASE_URL}/api/submit-feedback`, {
method: "POST",
headers: {
"Content-Type": "application/json",
@ -49,39 +53,29 @@ export async function submitFeedbackViaHttp(
}
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=${fiatCurrency.toLowerCase()}`,
);
const data = await response.json();
return data[currency][fiatCurrency.toLowerCase()];
} catch (error) {
console.error(`Error fetching ${currency} price:`, error);
throw error;
}
const response = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${currency}&vs_currencies=${fiatCurrency.toLowerCase()}`,
);
const data = await response.json();
return data[currency][fiatCurrency.toLowerCase()];
}
async function fetchXmrBtcRate(): Promise<number> {
try {
const response = await fetch('https://api.kraken.com/0/public/Ticker?pair=XMRXBT');
const data = await response.json();
if (data.error && data.error.length > 0) {
throw new Error(`Kraken API error: ${data.error[0]}`);
}
const response = await fetch('https://api.kraken.com/0/public/Ticker?pair=XMRXBT');
const data = await response.json();
const result = data.result.XXMRXXBT;
const lastTradePrice = parseFloat(result.c[0]);
return lastTradePrice;
} catch (error) {
console.error('Error fetching XMR/BTC rate from Kraken:', error);
throw error;
if (data.error && data.error.length > 0) {
throw new Error(`Kraken API error: ${data.error[0]}`);
}
const result = data.result.XXMRXXBT;
const lastTradePrice = parseFloat(result.c[0]);
return lastTradePrice;
}
async function fetchBtcPrice(fiatCurrency: FiatCurrency): Promise<number> {
function fetchBtcPrice(fiatCurrency: FiatCurrency): Promise<number> {
return fetchCurrencyPrice("bitcoin", fiatCurrency);
}
@ -95,21 +89,42 @@ async function fetchXmrPrice(fiatCurrency: FiatCurrency): Promise<number> {
*/
export async function updateRates(): Promise<void> {
const settings = store.getState().settings;
if (!settings.fetchFiatPrices)
if (!settings.fetchFiatPrices)
return;
try {
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}`);
logger.info(`Fetched rates for ${settings.fiatCurrency}`);
} catch (error) {
console.error("Error fetching rates:", error);
logger.error(error, "Error fetching rates");
}
}
/**
* Update public registry
*/
export async function updatePublicRegistry(): Promise<void> {
try {
const providers = await fetchProvidersViaHttp();
store.dispatch(setRegistryProviders(providers));
} catch (error) {
store.dispatch(registryConnectionFailed());
logger.error(error, "Error fetching providers");
}
try {
const alerts = await fetchAlertsViaHttp();
store.dispatch(setAlerts(alerts));
} catch (error) {
logger.error(error, "Error fetching alerts");
}
}

View file

@ -0,0 +1,87 @@
import { listen } from "@tauri-apps/api/event";
import { TauriSwapProgressEventWrapper, TauriContextStatusEvent, TauriLogEvent, BalanceResponse, TauriDatabaseStateEvent, TauriTimelockChangeEvent, TauriBackgroundRefundEvent } from "models/tauriModel";
import { contextStatusEventReceived, receivedCliLog, rpcSetBalance, timelockChangeEventReceived, rpcSetBackgroundRefundState } from "store/features/rpcSlice";
import { swapProgressEventReceived } from "store/features/swapSlice";
import logger from "utils/logger";
import { updatePublicRegistry, updateRates } from "./api";
import { checkContextAvailability, getSwapInfo, initializeContext, updateAllNodeStatuses } from "./rpc";
import { store } from "./store/storeRenderer";
// Update the public registry every 5 minutes
const PROVIDER_UPDATE_INTERVAL = 5 * 60 * 1_000;
// Update node statuses every 2 minutes
const STATUS_UPDATE_INTERVAL = 2 * 60 * 1_000;
// Update the exchange rate every 5 minutes
const UPDATE_RATE_INTERVAL = 5 * 60 * 1_000;
function setIntervalImmediate(callback: () => void, interval: number): void {
callback();
setInterval(callback, interval);
}
export async function setupBackgroundTasks(): Promise<void> {
// // Setup periodic fetch tasks
setIntervalImmediate(updatePublicRegistry, PROVIDER_UPDATE_INTERVAL);
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
// // Setup Tauri event listeners
// Check if the context is already available. This is to prevent unnecessary re-initialization
if (await checkContextAvailability()) {
store.dispatch(contextStatusEventReceived({ type: "Available" }));
} else {
// 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);
});
}
listen<TauriSwapProgressEventWrapper>("swap-progress-update", (event) => {
logger.info("Received swap progress event", event.payload);
store.dispatch(swapProgressEventReceived(event.payload));
});
listen<TauriContextStatusEvent>("context-init-progress-update", (event) => {
logger.info("Received context init progress event", event.payload);
store.dispatch(contextStatusEventReceived(event.payload));
});
listen<TauriLogEvent>("cli-log-emitted", (event) => {
logger.info("Received cli log event", event.payload);
store.dispatch(receivedCliLog(event.payload));
});
listen<BalanceResponse>("balance-change", (event) => {
logger.info("Received balance change event", event.payload);
store.dispatch(rpcSetBalance(event.payload.balance));
});
listen<TauriDatabaseStateEvent>("swap-database-state-update", (event) => {
logger.info("Received swap database state update event", event.payload);
getSwapInfo(event.payload.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(event.payload.swap_id), 3000);
});
listen<TauriTimelockChangeEvent>('timelock-change', (event) => {
logger.info('Received timelock change event', event.payload);
store.dispatch(timelockChangeEventReceived(event.payload));
})
listen<TauriBackgroundRefundEvent>('background-refund', (event) => {
logger.info('Received background refund event', event.payload);
store.dispatch(rpcSetBackgroundRefundState(event.payload));
})
}

View file

@ -11,14 +11,8 @@ import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
import UpdaterDialog from "./modal/updater/UpdaterDialog";
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";
import { setupBackgroundTasks } from "renderer/background";
const useStyles = makeStyles((theme) => ({
innerContent: {
@ -31,8 +25,7 @@ const useStyles = makeStyles((theme) => ({
export default function App() {
useEffect(() => {
fetchInitialData();
initEventListeners();
setupBackgroundTasks();
}, []);
const theme = useSettings((s) => s.theme);
@ -65,48 +58,4 @@ function InnerContent() {
</Routes>
</Box>
);
}
async function fetchInitialData() {
try {
const providerList = await fetchProvidersViaHttp();
store.dispatch(setRegistryProviders(providerList));
logger.info(
{ providerList },
"Fetched providers via UnstoppableSwap HTTP API",
);
} catch (e) {
store.dispatch(registryConnectionFailed());
logger.error(e, "Failed to fetch providers via UnstoppableSwap HTTP API");
}
try {
await updateAllNodeStatuses()
} catch (e) {
logger.error(e, "Failed to update node statuses")
}
// Update node statuses every 2 minutes
const STATUS_UPDATE_INTERVAL = 2 * 60 * 1_000;
setInterval(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
try {
const alerts = await fetchAlertsViaHttp();
store.dispatch(setAlerts(alerts));
logger.info({ alerts }, "Fetched alerts via UnstoppableSwap HTTP API");
} catch (e) {
logger.error(e, "Failed to fetch alerts via UnstoppableSwap HTTP API");
}
try {
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);
}
}

View file

@ -24,13 +24,14 @@ import { submitFeedbackViaHttp } from "../../../api";
import LoadingButton from "../../other/LoadingButton";
import { PiconeroAmount } from "../../other/Units";
import { getLogsOfSwap } from "renderer/rpc";
import logger from "utils/logger";
async function submitFeedback(body: string, swapId: string | number, submitDaemonLogs: boolean) {
let attachedBody = "";
if (swapId !== 0 && typeof swapId === "string") {
const swapInfo = store.getState().rpc.state.swapInfos[swapId];
if (swapInfo === undefined) {
throw new Error(`Swap with id ${swapId} not found`);
}
@ -179,7 +180,7 @@ export default function FeedbackDialog({
variant: "success",
});
} catch (e) {
console.error(`Failed to submit feedback: ${e}`);
logger.error(`Failed to submit feedback: ${e}`);
enqueueSnackbar(`Failed to submit feedback (${e})`, {
variant: "error",
});

View file

@ -74,7 +74,7 @@ export default function ProviderInfo({
Maximum swap amount: <SatsAmount amount={provider.maxSwapAmount} />
</Typography>
<Box className={classes.chipsOuter}>
<Chip label={provider.testnet ? "Testnet" : "Mainnet"} />
{provider.testnet && <Chip label="Testnet" />}
{provider.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`} />

View file

@ -1,6 +1,7 @@
import { Step, StepLabel, Stepper, Typography } from "@material-ui/core";
import { SwapState } from "models/storeModel";
import { useAppSelector } from "store/hooks";
import logger from "utils/logger";
export enum PathType {
HAPPY_PATH = "happy path",
@ -18,7 +19,7 @@ type PathStep = [type: PathType, step: number, isError: boolean];
function getActiveStep(state: SwapState | null): PathStep | null {
// In case we cannot infer a correct step from the state
function fallbackStep(reason: string) {
console.error(
logger.error(
`Unable to choose correct stepper type (reason: ${reason}, state: ${JSON.stringify(state)}`,
);
return null;

View file

@ -59,71 +59,11 @@ export async function fetchSellersAtPresetRendezvousPoints() {
const response = await listSellersAtRendezvousPoint(rendezvousPoint);
store.dispatch(discoveredProvidersByRendezvous(response.sellers));
console.log(`Discovered ${response.sellers.length} sellers at rendezvous point ${rendezvousPoint} during startup fetch`);
logger.log(`Discovered ${response.sellers.length} sellers at rendezvous point ${rendezvousPoint} during startup fetch`);
}),
);
}
export async function initEventListeners() {
// This operation is in-expensive
// We do this in case we miss the context init progress event because the frontend took too long to load
// TOOD: Replace this with a more reliable mechanism (such as an event replay mechanism)
if (await checkContextAvailability()) {
store.dispatch(contextStatusEventReceived({ type: "Available" }));
} else {
// 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); // 2 second delay
});
}
listen<TauriSwapProgressEventWrapper>("swap-progress-update", (event) => {
console.log("Received swap progress event", event.payload);
store.dispatch(swapProgressEventReceived(event.payload));
});
listen<TauriContextStatusEvent>("context-init-progress-update", (event) => {
console.log("Received context init progress event", event.payload);
store.dispatch(contextStatusEventReceived(event.payload));
});
listen<TauriLogEvent>("cli-log-emitted", (event) => {
console.log("Received cli log event", event.payload);
store.dispatch(receivedCliLog(event.payload));
});
listen<BalanceResponse>("balance-change", (event) => {
console.log("Received balance change event", event.payload);
store.dispatch(rpcSetBalance(event.payload.balance));
});
listen<TauriDatabaseStateEvent>("swap-database-state-update", (event) => {
console.log("Received swap database state update event", event.payload);
getSwapInfo(event.payload.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(event.payload.swap_id), 3000);
});
listen<TauriTimelockChangeEvent>('timelock-change', (event) => {
console.log('Received timelock change event', event.payload);
store.dispatch(timelockChangeEventReceived(event.payload));
})
listen<TauriBackgroundRefundEvent>('background-refund', (event) => {
console.log('Received background refund event', event.payload);
store.dispatch(rpcSetBackgroundRefundState(event.payload));
})
}
async function invoke<ARGS, RESPONSE>(
command: string,
args: ARGS,
@ -279,7 +219,7 @@ export async function initializeContext() {
monero_node_url: moneroNode,
};
console.log("Initializing context with settings", tauriSettings);
logger.info("Initializing context with settings", tauriSettings);
await invokeUnsafe<void>("initialize_context", {
settings: tauriSettings,

View file

@ -119,7 +119,6 @@ const alertsSlice = createSlice({
slice.fetchFiatPrices = action.payload;
},
setFiatCurrency(slice, action: PayloadAction<FiatCurrency>) {
console.log("setFiatCurrency", action.payload);
slice.fiatCurrency = action.payload;
},
addNode(slice, action: PayloadAction<{ network: Network, type: Blockchain, node: string }>) {

View file

@ -35,7 +35,7 @@ export function createMainListeners() {
actionCreator: setFiatCurrency,
effect: async () => {
if (store.getState().settings.fetchFiatPrices) {
console.log("Fiat currency changed, updating rates...");
logger.info("Fiat currency changed, updating rates...");
await updateRates();
}
},
@ -46,7 +46,7 @@ export function createMainListeners() {
actionCreator: setFetchFiatPrices,
effect: async (action) => {
if (action.payload === true) {
console.log("Activated fetching fiat prices, updating rates...");
logger.info("Activated fetching fiat prices, updating rates...");
await updateRates();
}
},