import { invoke as invokeUnsafe } from "@tauri-apps/api/core"; import { BalanceArgs, BalanceResponse, BuyXmrArgs, BuyXmrResponse, GetLogsArgs, GetLogsResponse, GetSwapInfoResponse, ListSellersArgs, MoneroRecoveryArgs, ResumeSwapArgs, ResumeSwapResponse, SuspendCurrentSwapResponse, WithdrawBtcArgs, WithdrawBtcResponse, GetSwapInfoArgs, ExportBitcoinWalletResponse, GetBitcoinAddressResponse, CheckMoneroNodeArgs, CheckSeedArgs, CheckSeedResponse, CheckMoneroNodeResponse, TauriSettings, CheckElectrumNodeArgs, CheckElectrumNodeResponse, GetMoneroAddressesResponse, GetDataDirArgs, ResolveApprovalArgs, ResolveApprovalResponse, RedactArgs, RedactResponse, GetCurrentSwapResponse, LabeledMoneroAddress, GetMoneroHistoryResponse, GetMoneroMainAddressResponse, GetMoneroBalanceResponse, SendMoneroArgs, SendMoneroResponse, GetMoneroSyncProgressResponse, GetPendingApprovalsResponse, DfxAuthenticateResponse, RejectApprovalArgs, RejectApprovalResponse, SetRestoreHeightArgs, SetRestoreHeightResponse, GetRestoreHeightResponse, MoneroNodeConfig, GetMoneroSeedResponse, ContextStatus, GetSwapTimelockArgs, GetSwapTimelockResponse, } from "models/tauriModel"; import { rpcSetSwapInfo, approvalRequestsReplaced, contextInitializationFailed, timelockChangeEventReceived, } from "store/features/rpcSlice"; import { selectAllSwapIds } from "store/selectors"; import { setBitcoinBalance } from "store/features/bitcoinWalletSlice"; import { setMainAddress, setBalance, setSyncProgress, setHistory, setRestoreHeight, } from "store/features/walletSlice"; import { store } from "./store/storeRenderer"; import { providerToConcatenatedMultiAddr } from "utils/multiAddrUtils"; import { MoneroRecoveryResponse } from "models/rpcModel"; import { ListSellersResponse } from "../models/tauriModel"; import logger from "utils/logger"; import { getNetwork, isTestnet } from "store/config"; import { DonateToDevelopmentTip } from "store/features/settingsSlice"; import { Blockchain, Network } from "store/types"; import { setStatus } from "store/features/nodesSlice"; import { discoveredMakersByRendezvous } from "store/features/makersSlice"; import { CliLog } from "models/cliModel"; import { logsToRawString, parseLogsFromString } from "utils/parseUtils"; import { DEFAULT_RENDEZVOUS_POINTS } from "store/defaults"; /// These are the official donation address for the eigenwallet/core project const DONATION_ADDRESS_MAINNET = "4A1tNBcsxhQA7NkswREXTD1QGz8mRyA7fGnCzPyTwqzKdDFMNje7iHUbGhCetfVUZa1PTuZCoPKj8gnJuRrFYJ2R2CEzqbJ"; const DONATION_ADDRESS_STAGENET = "56E274CJxTyVuuFG651dLURKyneoJ5LsSA5jMq4By9z9GBNYQKG8y5ejTYkcvZxarZW6if14ve8xXav2byK4aRnvNdKyVxp"; /// Signature by binarybaron for the donation address /// https://github.com/binarybaron/ /// /// Get the key from: /// - https://github.com/eigenwallet/core/blob/master/utils/gpg_keys/binarybaron.asc /// - https://unstoppableswap.net/binarybaron.asc const DONATION_ADDRESS_MAINNET_SIG = ` -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 4A1tNBcsxhQA7NkswREXTD1QGz8mRyA7fGnCzPyTwqzKdDFMNje7iHUbGhCetfVUZa1PTuZCoPKj8gnJuRrFYJ2R2CEzqbJ is our donation address (signed by binarybaron) -----BEGIN PGP SIGNATURE----- iQGzBAEBCAAdFiEEBRhGD+vsHaFKFVp7RK5vCxZqrVoFAmjxV4YACgkQRK5vCxZq rVrFogv9F650Um1TsPlqQ+7kdobCwa7yH5uXOp1p22YaiwWGHKRU5rUSb6Ac+zI0 3Io39VEoZufQqXqEqaiH7Q/08ABQR5r0TTPtSLNjOSEQ+ecClwv7MeF5CIXZYDdB AlEOnlL0CPfA24GQMhfp9lvjNiTBA2NikLARWJrc1JsLrFMK5rHesv7VHJEtm/gu We5eAuNOM2k3nAABTWzLiMJkH+G1amJmfkCKkBCk04inA6kZ5COUikMupyQDtsE4 hrr/KrskMuXzGY+rjP6NhWqr/twKj819TrOxlYD4vK68cZP+jx9m+vSBE6mxgMbN tBVdo9xFVCVymOYQCV8BRY8ScqP+YPNV5d6BMyDH9tvHJrGqZTNQiFhVX03Tw6mg hccEqYP1J/TaAlFg/P4HtqsxPBZD6x3IdSxXhrJ0IjrqLpVtKyQlTZGsJuNjFWG8 LKixaxxR7iWsyRZVCnEqCgDN8hzKZIE3Ph+kLTa4z4mTNEYyWUNeKRrFrSxKvEOK KM0Pp53f =O/zf -----END PGP SIGNATURE----- `; 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 fetchSellersAtPresetRendezvousPoints() { await Promise.all( DEFAULT_RENDEZVOUS_POINTS.map(async (rendezvousPoint) => { const response = await listSellersAtRendezvousPoint([rendezvousPoint]); store.dispatch(discoveredMakersByRendezvous(response.sellers)); logger.info( `Discovered ${response.sellers.length} sellers at rendezvous point ${rendezvousPoint} during startup fetch`, ); }), ); } export async function checkBitcoinBalance() { // If we are already syncing, don't start a new sync if ( Object.values(store.getState().rpc?.state.background ?? {}).some( (progress) => progress.componentName === "SyncingBitcoinWallet" && progress.progress.type === "Pending", ) ) { console.log( "checkBitcoinBalance() was called but we are already syncing Bitcoin, skipping", ); return; } const response = await invoke("get_balance", { force_refresh: true, }); store.dispatch(setBitcoinBalance(response.balance)); } export async function buyXmr() { const state = store.getState(); // Determine based on redeem and refund policy which addresses to pass in // // null means internal wallet const bitcoinChangeAddress = state.settings.bitcoinRefundPolicy === "external" ? state.settings.externalBitcoinRefundAddress : null; const moneroReceiveAddress = state.settings.moneroRedeemPolicy === "external" ? state.settings.externalMoneroRedeemAddress : null; const donationPercentage = state.settings.donateToDevelopment; // Get all available makers from the Redux store const allMakers = [ ...(state.makers.registry.makers || []), ...state.makers.rendezvous.makers, ]; // Convert all makers to multiaddr format const sellers = allMakers.map((maker) => providerToConcatenatedMultiAddr(maker), ); const address_pool: LabeledMoneroAddress[] = []; if (donationPercentage !== false && donationPercentage > 0) { const donation_address = isTestnet() ? DONATION_ADDRESS_STAGENET : DONATION_ADDRESS_MAINNET; address_pool.push( { // We need to assert this as being not null even though it can be null // // This is correct because a LabeledMoneroAddress can actually have a null address but // typeshare cannot express that yet (easily) // // TODO: Let typescript do its job here and not assert it address: moneroReceiveAddress!, percentage: 1 - donationPercentage, label: "Your wallet", }, { address: donation_address, percentage: donationPercentage, label: "Tip to the developers", }, ); } else { address_pool.push({ // We need to assert this as being not null even though it can be null // // This is correct because a LabeledMoneroAddress can actually have a null address but // typeshare cannot express that yet (easily) // // TODO: Let typescript do its job here and not assert it address: moneroReceiveAddress!, percentage: 1, label: "Your wallet", }); } await invoke("buy_xmr", { rendezvous_points: DEFAULT_RENDEZVOUS_POINTS, sellers, monero_receive_pool: address_pool, // We convert null to undefined because typescript // expects undefined if the field is optional and does not accept null here bitcoin_change_address: bitcoinChangeAddress ?? undefined, }); } export async function initializeContext() { const network = getNetwork(); const testnet = isTestnet(); const useTor = store.getState().settings.enableTor; // Get all Bitcoin nodes without checking availability // The backend ElectrumBalancer will handle load balancing and failover const bitcoinNodes = store.getState().settings.nodes[network][Blockchain.Bitcoin]; // For Monero nodes, determine whether to use pool or custom node const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool; const useMoneroTor = store.getState().settings.enableMoneroTor; const moneroNodeUrl = store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null; // Check the state of the Monero node const moneroNodeConfig = useMoneroRpcPool || moneroNodeUrl == null || !(await getMoneroNodeStatus(moneroNodeUrl, network)) ? { type: "Pool" as const } : { type: "SingleNode" as const, content: { url: moneroNodeUrl, }, }; // Initialize Tauri settings const tauriSettings: TauriSettings = { electrum_rpc_urls: bitcoinNodes, monero_node_config: moneroNodeConfig, use_tor: useTor, enable_monero_tor: useMoneroTor, }; logger.info({ tauriSettings }, "Initializing context with settings"); try { await invokeUnsafe("initialize_context", { settings: tauriSettings, testnet, }); logger.info("Initialized context"); } catch (error) { throw new Error(String(error)); } } export async function updateAllNodeStatuses() { const network = getNetwork(); const settings = store.getState().settings; // Only check Monero nodes if we're using custom nodes (not RPC pool) // Skip Bitcoin nodes since we pass all electrum servers to the backend without checking them (ElectrumBalancer handles failover) if (!settings.useMoneroRpcPool) { await Promise.all( settings.nodes[network][Blockchain.Monero].map((node) => updateNodeStatus(node, Blockchain.Monero, network), ), ); } } export async function cheapCheckBitcoinBalance() { const response = await invoke("get_balance", { force_refresh: false, }); store.dispatch(setBitcoinBalance(response.balance)); } export async function getBitcoinAddress() { const response = await invokeNoArgs( "get_bitcoin_address", ); return response.address; } 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 getSwapTimelock(swapId: string) { const response = await invoke( "get_swap_timelock", { swap_id: swapId, }, ); store.dispatch( timelockChangeEventReceived({ swap_id: response.swap_id, timelock: response.timelock, }), ); } export async function getAllSwapTimelocks() { const swapIds = selectAllSwapIds(store.getState()); await Promise.all( swapIds.map(async (swapId) => { try { await getSwapTimelock(swapId); } catch (error) { logger.debug(`Failed to fetch timelock for swap ${swapId}: ${error}`); } }), ); } export async function sweepBtc(address: string): Promise { const response = await invoke( "withdraw_btc", { address, amount: undefined, }, ); // We check the balance, this is cheap and does not sync the wallet // but instead uses our local cached balance await cheapCheckBitcoinBalance(); return response.txid; } 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 getCurrentSwapId() { return await invokeNoArgs("get_current_swap"); } export async function getMoneroRecoveryKeys( swapId: string, ): Promise { return await invoke( "monero_recovery", { swap_id: swapId, }, ); } export async function checkContextStatus(): Promise { return await invokeNoArgs("get_context_status"); } export async function getLogsOfSwap( swapId: string, redact: boolean, ): Promise { return await invoke("get_logs", { swap_id: swapId, redact, }); } /// Call the rust backend to redact logs. export async function redactLogs( logs: (string | CliLog)[], ): Promise<(string | CliLog)[]> { const response = await invoke("redact", { text: logsToRawString(logs), }); return parseLogsFromString(response.text); } export async function listSellersAtRendezvousPoint( rendezvousPointAddresses: string[], ): Promise { return await invoke("list_sellers", { rendezvous_points: rendezvousPointAddresses, }); } export async function getWalletDescriptor() { return await invokeNoArgs( "get_wallet_descriptor", ); } export async function getMoneroNodeStatus( node: string, network: Network, ): Promise { const response = await invoke( "check_monero_node", { url: node, network, }, ); return response.available; } export async function getElectrumNodeStatus(url: string): Promise { const response = await invoke< CheckElectrumNodeArgs, CheckElectrumNodeResponse >("check_electrum_node", { url, }); return response.available; } export async function getNodeStatus( url: string, blockchain: Blockchain, network: Network, ): Promise { switch (blockchain) { case Blockchain.Monero: return await getMoneroNodeStatus(url, network); case Blockchain.Bitcoin: return await getElectrumNodeStatus(url); default: throw new Error(`Unsupported blockchain: ${blockchain}`); } } async function updateNodeStatus( node: string, blockchain: Blockchain, network: Network, ) { const status = await getNodeStatus(node, blockchain, network); store.dispatch(setStatus({ node, status, blockchain })); } export async function getMoneroAddresses(): Promise { return await invokeNoArgs("get_monero_addresses"); } export async function getRestoreHeight(): Promise { const restoreHeight = await invokeNoArgs("get_restore_height"); store.dispatch(setRestoreHeight(restoreHeight)); return restoreHeight; } export async function setMoneroRestoreHeight( height: number | Date, ): Promise { const args: SetRestoreHeightArgs = typeof height === "number" ? { type: "Height", height: height } : { type: "Date", height: { year: height.getFullYear(), month: height.getMonth() + 1, // JavaScript months are 0-indexed, but we want 1-indexed day: height.getDate(), }, }; return await invoke( "set_monero_restore_height", args, ); } export async function getMoneroHistory(): Promise { return await invokeNoArgs("get_monero_history"); } export async function getMoneroMainAddress(): Promise { return await invokeNoArgs( "get_monero_main_address", ); } export async function getMoneroBalance(): Promise { return await invokeNoArgs("get_monero_balance"); } export async function sendMonero( args: SendMoneroArgs, ): Promise { return await invoke("send_monero", args); } export async function getMoneroSyncProgress(): Promise { return await invokeNoArgs( "get_monero_sync_progress", ); } export async function getMoneroSeed(): Promise { return await invokeNoArgs("get_monero_seed"); } export async function getMoneroSeedAndRestoreHeight(): Promise< [GetMoneroSeedResponse, GetRestoreHeightResponse] > { return Promise.all([getMoneroSeed(), getRestoreHeight()]); } // Wallet management functions that handle Redux dispatching export async function initializeMoneroWallet() { try { await Promise.all([ getMoneroMainAddress().then((response) => { store.dispatch(setMainAddress(response.address)); }), getMoneroBalance().then((response) => { store.dispatch(setBalance(response)); }), getMoneroSyncProgress().then((response) => { store.dispatch(setSyncProgress(response)); }), getMoneroHistory().then((response) => { store.dispatch(setHistory(response)); }), getRestoreHeight().then((response) => { store.dispatch(setRestoreHeight(response)); }), ]); } catch (err) { console.error("Failed to fetch Monero wallet data:", err); } } export async function sendMoneroTransaction( args: SendMoneroArgs, ): Promise { try { const response = await sendMonero(args); // Refresh balance and history after sending - but don't let this block the response Promise.all([getMoneroBalance(), getMoneroHistory()]) .then(([newBalance, newHistory]) => { store.dispatch(setBalance(newBalance)); store.dispatch(setHistory(newHistory)); }) .catch((refreshErr) => { console.error("Failed to refresh wallet data after send:", refreshErr); }); return response; } catch (err) { console.error("Failed to send Monero:", err); throw err; } } export async function updateMoneroSyncProgress() { try { const response = await getMoneroSyncProgress(); store.dispatch(setSyncProgress(response)); } catch (err) { console.error("Failed to fetch sync progress:", err); } } export async function getDataDir(): Promise { const testnet = isTestnet(); return await invoke("get_data_dir", { is_testnet: testnet, }); } export async function resolveApproval( requestId: string, accept: T, ): Promise { try { await invoke( "resolve_approval_request", { request_id: requestId, accept: accept as object }, ); } finally { // Always refresh the approval list await refreshApprovals(); // Refresh the approval list a few miliseconds later to again // Just to make sure :) setTimeout(() => { refreshApprovals(); }, 200); } } export async function rejectApproval( requestId: string, reject: T, ): Promise { await invoke( "reject_approval_request", { request_id: requestId }, ); } export async function refreshApprovals(): Promise { const response = await invokeNoArgs( "get_pending_approvals", ); store.dispatch(approvalRequestsReplaced(response.approvals)); } export async function checkSeed(seed: string): Promise { const response = await invoke( "check_seed", { seed, }, ); return response.available; } export async function saveLogFiles( zipFileName: string, content: Record, ): Promise { await invokeUnsafe("save_txt_files", { zipFileName, content }); } export async function saveFilesInDialog(files: Record) { await invokeUnsafe("save_txt_files", { files, }); } export async function dfxAuthenticate(): Promise { return await invokeNoArgs("dfx_authenticate"); } export async function changeMoneroNode( nodeConfig: MoneroNodeConfig, ): Promise { await invoke<{ node_config: MoneroNodeConfig }, void>("change_monero_node", { node_config: nodeConfig, }); } // Helper function to create MoneroNodeConfig from current settings export async function getCurrentMoneroNodeConfig(): Promise { const network = getNetwork(); const useMoneroRpcPool = store.getState().settings.useMoneroRpcPool; const moneroNodeUrl = store.getState().settings.nodes[network][Blockchain.Monero][0] ?? null; const moneroNodeConfig = useMoneroRpcPool || moneroNodeUrl == null || !(await getMoneroNodeStatus(moneroNodeUrl, network)) ? { type: "Pool" as const } : { type: "SingleNode" as const, content: { url: moneroNodeUrl, }, }; return moneroNodeConfig; }