xmr-btc-swap/src-gui/src/renderer/background.ts
Mohan 801ce8fd9d
feat(taker): Improve maker discovery (#692)
* mvp

* only redial if DisconnectedAndNotDialing

* progress

* progress

* progress

* some progress

* progress

* extract common logic into behaviour_util::ConnectionTracker

* extract common logic into BackoffTracker helper struct

* add comment in quote::background behaviour

* BackoffTracker rename get_backoff to get(...)

* cleanup, fix some things that got lost during rebase

* properly propagate quote::background ToSwarm events

* actually persist event loop, add quotes and rendezvous::discovery to cli behaviour

* some progress, cleanup, comments

* progress

* redial all peers that we dont know dont support quote, use quotes_cached behaviour in example, add remove_peer(...) to redial behaviour, don't redial discovered rendezvous peers

* remove old todo

* quotes_cached.rs: cache last connected endpoint

* rename: add_peer_address -> queue_peer_address

* extract p2p defaults into swap-p2p/defaults.rs

* split rendezvous.rs into two sub-modules

* remove unused bob::BackgroundQuoteReceived

* replace usage of list_sellers with event loop

* prune: remove list_sellers command

* use backoff helper in swap-p2p/src/protocols/quotes.rs

* refactor rendezvous::register behaviour, getting some unit tests working again

* add all peer addresses to the swarm on init

* less agressive redials

* extract magic backoff numbers

* proof of concept: drill tracing span into event loop through channels

* add BackoffTracker::increment, re-schedule register when we lose connection to rendezvous point

* fetch identify version and propagate into UI

* forbid private/local/loopback ip addresses to be shared/accepted through Identify

* remove legacy list_sellers code

* ensure uuids are unique for alice during swap_setup

* formatting and nitpicks

* fix: allow multiple swap setup requests over the same connection handler

* small cleanups

* fix: protocols/quotes.rs unit tests

* revert: listen on 0.0.0.0 for asb p2p

* propagate handle_pending_inbound_connection and handle_pending_outbound_connection to identify patch source

* replace loop with repeated return Poll::Ready in discovery.rs

* format

* MultiAddrVecExt trait, emit rendezvous addresses to rendezvous-node swarm

* fix: strictly disallow concurrent swap setup requests for the same swap on the same connection

* fix tests etc

* remove slop from futures_util.rs

* address some comments

* behaviour_util.rs: track inflight dials, add tests, return Some(peer_id) if internal state was changed

* replace boring-avatars with jidenticon

* feat: add peer discovery status dialog

* remove buy-xmr cli command, remove "sellers" arg for BuyXmrArgs, add changelog

* disable body overscroll

* add changelog for jidenticon

* increase quote fetch interval to 45s

* fix rendezvous::register_and_discover_together test
2025-12-01 18:03:13 +01:00

202 lines
6.4 KiB
TypeScript

import { listen } from "@tauri-apps/api/event";
import { TauriEvent } from "models/tauriModel";
import {
contextStatusEventReceived,
contextInitializationFailed,
timelockChangeEventReceived,
approvalEventReceived,
backgroundProgressEventReceived,
} from "store/features/rpcSlice";
import { setBitcoinBalance } from "store/features/bitcoinWalletSlice";
import { receivedCliLog } from "store/features/logsSlice";
import { poolStatusReceived } from "store/features/poolSlice";
import { swapProgressEventReceived } from "store/features/swapSlice";
import {
quotesProgressReceived,
connectionChangeReceived,
} from "store/features/p2pSlice";
import logger from "utils/logger";
import { fetchAllConversations, updateAlerts, updateRates } from "./api";
import {
checkContextStatus,
getSwapInfo,
getSwapTimelock,
initializeContext,
refreshApprovals,
updateAllNodeStatuses,
} from "./rpc";
import { store } from "./store/storeRenderer";
import { exhaustiveGuard } from "utils/typescriptUtils";
import {
setBalance,
setHistory,
setSyncProgress,
} from "store/features/walletSlice";
import { applyDefaultNodes } from "store/features/settingsSlice";
import {
DEFAULT_NODES,
NEGATIVE_NODES_MAINNET,
NEGATIVE_NODES_TESTNET,
} from "store/defaults";
const TAURI_UNIFIED_EVENT_CHANNEL_NAME = "tauri-unified-event";
// Update the public registry every 5 minutes
const PROVIDER_UPDATE_INTERVAL = 5 * 60 * 1_000;
// Discover peers every 5 minutes
const DISCOVER_PEERS_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;
// Fetch all conversations every 10 minutes
const FETCH_CONVERSATIONS_INTERVAL = 10 * 60 * 1_000;
// Fetch pending approvals every 2 seconds
const FETCH_PENDING_APPROVALS_INTERVAL = 2 * 1_000;
// Check context status every 2 seconds
const CHECK_CONTEXT_STATUS_INTERVAL = 2 * 1_000;
function setIntervalImmediate(callback: () => void, interval: number): void {
callback();
setInterval(callback, interval);
}
export async function setupBackgroundTasks(): Promise<void> {
// Apply default nodes on startup (removes broken nodes, adds new ones)
store.dispatch(
applyDefaultNodes({
defaultNodes: DEFAULT_NODES,
negativeNodesMainnet: NEGATIVE_NODES_MAINNET,
negativeNodesTestnet: NEGATIVE_NODES_TESTNET,
}),
);
// Setup periodic fetch tasks
setIntervalImmediate(updateAllNodeStatuses, STATUS_UPDATE_INTERVAL);
setIntervalImmediate(updateRates, UPDATE_RATE_INTERVAL);
setIntervalImmediate(fetchAllConversations, FETCH_CONVERSATIONS_INTERVAL);
setIntervalImmediate(refreshApprovals, FETCH_PENDING_APPROVALS_INTERVAL);
// Fetch all alerts
updateAlerts();
// Setup Tauri event listeners
// Check if the context is already available. This is to prevent unnecessary re-initialization
setIntervalImmediate(async () => {
const contextStatus = await checkContextStatus();
store.dispatch(contextStatusEventReceived(contextStatus));
}, CHECK_CONTEXT_STATUS_INTERVAL);
const contextStatus = await checkContextStatus();
// If all components are unavailable, we need to initialize the context
if (
!contextStatus.bitcoin_wallet_available &&
!contextStatus.monero_wallet_available &&
!contextStatus.database_available &&
!contextStatus.tor_available
)
// 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",
);
store.dispatch(contextInitializationFailed(String(e)));
});
}
// Listen for the unified event
listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
const { channelName, event: eventData } = event.payload;
switch (channelName) {
case "SwapProgress":
store.dispatch(swapProgressEventReceived(eventData));
break;
case "CliLog":
store.dispatch(receivedCliLog(eventData));
break;
case "BalanceChange":
store.dispatch(setBitcoinBalance(eventData.balance));
break;
case "SwapDatabaseStateUpdate":
getSwapInfo(eventData.swap_id).catch((error) => {
logger.debug(
`Failed to fetch swap info for swap ${eventData.swap_id}: ${error}`,
);
});
getSwapTimelock(eventData.swap_id).catch((error) => {
logger.debug(
`Failed to fetch timelock for swap ${eventData.swap_id}: ${error}`,
);
});
// 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(eventData.swap_id).catch((error) => {
logger.debug(
`Failed to fetch swap info for swap ${eventData.swap_id}: ${error}`,
);
});
getSwapTimelock(eventData.swap_id).catch((error) => {
logger.debug(
`Failed to fetch timelock for swap ${eventData.swap_id}: ${error}`,
);
});
}, 3000);
break;
case "TimelockChange":
store.dispatch(timelockChangeEventReceived(eventData));
break;
case "Approval":
store.dispatch(approvalEventReceived(eventData));
break;
case "BackgroundProgress":
store.dispatch(backgroundProgressEventReceived(eventData));
break;
case "PoolStatusUpdate":
store.dispatch(poolStatusReceived(eventData));
break;
case "MoneroWalletUpdate":
console.log("MoneroWalletUpdate", eventData);
if (eventData.type === "BalanceChange") {
store.dispatch(setBalance(eventData.content));
}
if (eventData.type === "HistoryUpdate") {
store.dispatch(setHistory(eventData.content));
}
if (eventData.type === "SyncProgress") {
store.dispatch(setSyncProgress(eventData.content));
}
break;
case "P2P":
if (eventData.type === "ConnectionChange") {
store.dispatch(connectionChangeReceived(eventData.content));
}
if (eventData.type === "QuotesProgress") {
store.dispatch(quotesProgressReceived(eventData.content.peers));
}
break;
default:
exhaustiveGuard(channelName);
}
});