import { invoke as invokeUnsafe } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { BalanceArgs, BalanceResponse, BuyXmrArgs, BuyXmrResponse, TauriLogEvent, GetLogsArgs, GetLogsResponse, GetSwapInfoResponse, ListSellersArgs, MoneroRecoveryArgs, ResumeSwapArgs, ResumeSwapResponse, SuspendCurrentSwapResponse, TauriContextStatusEvent, TauriSwapProgressEventWrapper, WithdrawBtcArgs, WithdrawBtcResponse, TauriDatabaseStateEvent, TauriTimelockChangeEvent, GetSwapInfoArgs, ExportBitcoinWalletResponse, CheckMoneroNodeArgs, CheckMoneroNodeResponse, TauriSettings, CheckElectrumNodeArgs, CheckElectrumNodeResponse, GetMoneroAddressesResponse, TauriBackgroundRefundEvent, } from "models/tauriModel"; import { contextStatusEventReceived, receivedCliLog, rpcSetBackgroundRefundState, rpcSetBalance, rpcSetSwapInfo, timelockChangeEventReceived, } from "store/features/rpcSlice"; import { swapProgressEventReceived } from "store/features/swapSlice"; import { store } from "./store/storeRenderer"; import { Provider } from "models/apiModel"; import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils"; import { MoneroRecoveryResponse } from "models/rpcModel"; import { ListSellersResponse } from "../models/tauriModel"; import logger from "utils/logger"; import { getNetwork, getNetworkName, isTestnet } from "store/config"; import { Blockchain } from "store/features/settingsSlice"; import { setStatus } from "store/features/nodesSlice"; import { discoveredProvidersByRendezvous } from "store/features/providersSlice"; export const PRESET_RENDEZVOUS_POINTS = [ "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE", ]; export async function fetchSellersAtPresetRendezvousPoints() { await Promise.all(PRESET_RENDEZVOUS_POINTS.map(async (rendezvousPoint) => { const response = await listSellersAtRendezvousPoint(rendezvousPoint); store.dispatch(discoveredProvidersByRendezvous(response.sellers)); console.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, ): Promise { return invokeUnsafe(command, { args: args as Record, }) as Promise; } async function invokeNoArgs(command: string): Promise { return invokeUnsafe(command) as Promise; } export async function checkBitcoinBalance() { const response = await invoke("get_balance", { force_refresh: true, }); store.dispatch(rpcSetBalance(response.balance)); } export async function getAllSwapInfos() { const response = await invokeNoArgs("get_swap_infos_all"); response.forEach((swapInfo) => { store.dispatch(rpcSetSwapInfo(swapInfo)); }); } export async function getSwapInfo(swapId: string) { const response = await invoke( "get_swap_info", { swap_id: swapId, }, ); store.dispatch(rpcSetSwapInfo(response)); } export async function withdrawBtc(address: string): Promise { const response = await invoke( "withdraw_btc", { address, amount: null, }, ); return response.txid; } export async function buyXmr( seller: Provider, bitcoin_change_address: string | null, monero_receive_address: string, ) { await invoke( "buy_xmr", bitcoin_change_address == null ? { seller: providerToConcatenatedMultiAddr(seller), monero_receive_address, } : { seller: providerToConcatenatedMultiAddr(seller), monero_receive_address, bitcoin_change_address, }, ); } export async function resumeSwap(swapId: string) { await invoke("resume_swap", { swap_id: swapId, }); } export async function suspendCurrentSwap() { await invokeNoArgs("suspend_current_swap"); } export async function getMoneroRecoveryKeys( swapId: string, ): Promise { return await invoke( "monero_recovery", { swap_id: swapId, }, ); } export async function checkContextAvailability(): Promise { const available = await invokeNoArgs("is_context_available"); return available; } export async function getLogsOfSwap( swapId: string, redact: boolean, ): Promise { return await invoke("get_logs", { swap_id: swapId, redact, }); } export async function listSellersAtRendezvousPoint( rendezvousPointAddress: string, ): Promise { return await invoke("list_sellers", { rendezvous_point: rendezvousPointAddress, }); } export async function initializeContext() { const network = getNetwork(); const testnet = isTestnet(); // Initialize Tauri settings with null values const tauriSettings: TauriSettings = { electrum_rpc_url: null, monero_node_url: null, }; // If are missing any statuses, update them if (Object.values(Blockchain).some(blockchain => Object.values(store.getState().nodes.nodes[blockchain]).length < store.getState().settings.nodes[network][blockchain].length )) { try { console.log("Updating node statuses"); await updateAllNodeStatuses(); } catch (e) { logger.error(e, "Failed to update node statuses"); } } const { bitcoin: bitcoinNodes, monero: moneroNodes } = store.getState().nodes.nodes; const firstAvailableElectrumNode = Object.keys(bitcoinNodes).find(node => bitcoinNodes[node] === true); const firstAvailableMoneroNode = Object.keys(moneroNodes).find(node => moneroNodes[node] === true); tauriSettings.electrum_rpc_url = firstAvailableElectrumNode ?? null; tauriSettings.monero_node_url = firstAvailableMoneroNode ?? null; console.log("Initializing context with settings", tauriSettings); await invokeUnsafe("initialize_context", { settings: tauriSettings, testnet, }); } export async function getWalletDescriptor() { return await invokeNoArgs("get_wallet_descriptor"); } export async function getMoneroNodeStatus(node: string): Promise { const response = await invoke("check_monero_node", { url: node, network: getNetworkName(), }); return response.available; } export async function getElectrumNodeStatus(url: string): Promise { const response = await invoke("check_electrum_node", { url, }); return response.available; } export async function getNodeStatus(url: string, blockchain: Blockchain): Promise { switch (blockchain) { case Blockchain.Monero: return await getMoneroNodeStatus(url); case Blockchain.Bitcoin: return await getElectrumNodeStatus(url); } } async function updateNodeStatus(node: string, blockchain: Blockchain) { const status = await getNodeStatus(node, blockchain); store.dispatch(setStatus({ node, status, blockchain })); } export async function updateAllNodeStatuses() { const network = getNetwork(); const settings = store.getState().settings; // For all nodes, check if they are available and store the new status (in parallel) await Promise.all( Object.values(Blockchain).flatMap(blockchain => settings.nodes[network][blockchain].map(node => updateNodeStatus(node, blockchain)) ) ); } export async function getMoneroAddresses(): Promise { return await invokeNoArgs("get_monero_addresses"); }