mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-05-02 06:46:06 -04:00
refactor(gui): Seperate background refresh logic (#193)
This commit is contained in:
parent
b409db35d0
commit
d953114c49
9 changed files with 151 additions and 159 deletions
|
@ -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");
|
||||
}
|
||||
}
|
87
src-gui/src/renderer/background.ts
Normal file
87
src-gui/src/renderer/background.ts
Normal 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));
|
||||
})
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
});
|
||||
|
|
|
@ -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`} />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }>) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue