From d953114c49e6bdb6e245cbed964025cfd23b9a1e Mon Sep 17 00:00:00 2001 From: binarybaron <86064887+binarybaron@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:03:55 +0100 Subject: [PATCH] refactor(gui): Seperate background refresh logic (#193) --- src-gui/src/renderer/api.ts | 87 +++++++++++-------- src-gui/src/renderer/background.ts | 87 +++++++++++++++++++ src-gui/src/renderer/components/App.tsx | 57 +----------- .../modal/feedback/FeedbackDialog.tsx | 5 +- .../modal/provider/ProviderInfo.tsx | 2 +- .../modal/swap/SwapStateStepper.tsx | 3 +- src-gui/src/renderer/rpc.ts | 64 +------------- src-gui/src/store/features/settingsSlice.ts | 1 - src-gui/src/store/middleware/storeListener.ts | 4 +- 9 files changed, 151 insertions(+), 159 deletions(-) create mode 100644 src-gui/src/renderer/background.ts diff --git a/src-gui/src/renderer/api.ts b/src-gui/src/renderer/api.ts index 687b66e9..8e5315ea 100644 --- a/src-gui/src/renderer/api.ts +++ b/src-gui/src/renderer/api.ts @@ -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 { - const response = await fetch(`${API_BASE_URL}/api/alerts`); +async function fetchAlertsViaHttp(): Promise { + 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 { - 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 { - 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 { +function fetchBtcPrice(fiatCurrency: FiatCurrency): Promise { return fetchCurrencyPrice("bitcoin", fiatCurrency); } @@ -95,21 +89,42 @@ async function fetchXmrPrice(fiatCurrency: FiatCurrency): Promise { */ export async function updateRates(): Promise { 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 { + 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"); + } +} \ No newline at end of file diff --git a/src-gui/src/renderer/background.ts b/src-gui/src/renderer/background.ts new file mode 100644 index 00000000..70a0b496 --- /dev/null +++ b/src-gui/src/renderer/background.ts @@ -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 { + // // 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("swap-progress-update", (event) => { + logger.info("Received swap progress event", event.payload); + store.dispatch(swapProgressEventReceived(event.payload)); + }); + + listen("context-init-progress-update", (event) => { + logger.info("Received context init progress event", event.payload); + store.dispatch(contextStatusEventReceived(event.payload)); + }); + + listen("cli-log-emitted", (event) => { + logger.info("Received cli log event", event.payload); + store.dispatch(receivedCliLog(event.payload)); + }); + + listen("balance-change", (event) => { + logger.info("Received balance change event", event.payload); + store.dispatch(rpcSetBalance(event.payload.balance)); + }); + + listen("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('timelock-change', (event) => { + logger.info('Received timelock change event', event.payload); + store.dispatch(timelockChangeEventReceived(event.payload)); + }) + + listen('background-refund', (event) => { + logger.info('Received background refund event', event.payload); + store.dispatch(rpcSetBackgroundRefundState(event.payload)); + }) +} \ No newline at end of file diff --git a/src-gui/src/renderer/components/App.tsx b/src-gui/src/renderer/components/App.tsx index f9972b46..f564b0fb 100644 --- a/src-gui/src/renderer/components/App.tsx +++ b/src-gui/src/renderer/components/App.tsx @@ -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() { ); -} - -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); -} +} \ No newline at end of file diff --git a/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx b/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx index 44135734..ab9631e1 100644 --- a/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx +++ b/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx @@ -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", }); diff --git a/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx b/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx index 37e13b27..574799ad 100644 --- a/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx +++ b/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx @@ -74,7 +74,7 @@ export default function ProviderInfo({ Maximum swap amount: - + {provider.testnet && } {provider.uptime && ( diff --git a/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx b/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx index 8b45de64..8254b002 100644 --- a/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx +++ b/src-gui/src/renderer/components/modal/swap/SwapStateStepper.tsx @@ -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; diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 43d9ed5f..c577e71f 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -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("swap-progress-update", (event) => { - console.log("Received swap progress event", event.payload); - store.dispatch(swapProgressEventReceived(event.payload)); - }); - - listen("context-init-progress-update", (event) => { - console.log("Received context init progress event", event.payload); - store.dispatch(contextStatusEventReceived(event.payload)); - }); - - listen("cli-log-emitted", (event) => { - console.log("Received cli log event", event.payload); - store.dispatch(receivedCliLog(event.payload)); - }); - - listen("balance-change", (event) => { - console.log("Received balance change event", event.payload); - store.dispatch(rpcSetBalance(event.payload.balance)); - }); - - listen("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('timelock-change', (event) => { - console.log('Received timelock change event', event.payload); - store.dispatch(timelockChangeEventReceived(event.payload)); - }) - - listen('background-refund', (event) => { - console.log('Received background refund event', event.payload); - store.dispatch(rpcSetBackgroundRefundState(event.payload)); - }) -} - async function invoke( 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("initialize_context", { settings: tauriSettings, diff --git a/src-gui/src/store/features/settingsSlice.ts b/src-gui/src/store/features/settingsSlice.ts index b9439a6b..10b5769a 100644 --- a/src-gui/src/store/features/settingsSlice.ts +++ b/src-gui/src/store/features/settingsSlice.ts @@ -119,7 +119,6 @@ const alertsSlice = createSlice({ slice.fetchFiatPrices = action.payload; }, setFiatCurrency(slice, action: PayloadAction) { - console.log("setFiatCurrency", action.payload); slice.fiatCurrency = action.payload; }, addNode(slice, action: PayloadAction<{ network: Network, type: Blockchain, node: string }>) { diff --git a/src-gui/src/store/middleware/storeListener.ts b/src-gui/src/store/middleware/storeListener.ts index 1108f6f9..81115b82 100644 --- a/src-gui/src/store/middleware/storeListener.ts +++ b/src-gui/src/store/middleware/storeListener.ts @@ -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(); } },