mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-19 18:42:26 -05:00
* feat(defaults, gui): add bluewallet and cakewallet electrum servers * progress * fix circular dependency * detect import cycles * add changelog entry * move DEFAULT_RENDEZVOUS_POINTS into defaults.ts * move defaults.rs and types.ts one level higher * rename * collapse PRESET_RENDEZVOUS_POINTS and DEFAULT_RENDEZVOUS_POINTS
729 lines
21 KiB
TypeScript
729 lines
21 KiB
TypeScript
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<ARGS, RESPONSE>(
|
|
command: string,
|
|
args: ARGS,
|
|
): Promise<RESPONSE> {
|
|
return invokeUnsafe(command, {
|
|
args: args as Record<string, unknown>,
|
|
}) as Promise<RESPONSE>;
|
|
}
|
|
|
|
async function invokeNoArgs<RESPONSE>(command: string): Promise<RESPONSE> {
|
|
return invokeUnsafe(command) as Promise<RESPONSE>;
|
|
}
|
|
|
|
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<BalanceArgs, BalanceResponse>("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<BuyXmrArgs, BuyXmrResponse>("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<void>("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<BalanceArgs, BalanceResponse>("get_balance", {
|
|
force_refresh: false,
|
|
});
|
|
|
|
store.dispatch(setBitcoinBalance(response.balance));
|
|
}
|
|
|
|
export async function getBitcoinAddress() {
|
|
const response = await invokeNoArgs<GetBitcoinAddressResponse>(
|
|
"get_bitcoin_address",
|
|
);
|
|
|
|
return response.address;
|
|
}
|
|
|
|
export async function getAllSwapInfos() {
|
|
const response =
|
|
await invokeNoArgs<GetSwapInfoResponse[]>("get_swap_infos_all");
|
|
|
|
response.forEach((swapInfo) => {
|
|
store.dispatch(rpcSetSwapInfo(swapInfo));
|
|
});
|
|
}
|
|
|
|
export async function getSwapInfo(swapId: string) {
|
|
const response = await invoke<GetSwapInfoArgs, GetSwapInfoResponse>(
|
|
"get_swap_info",
|
|
{
|
|
swap_id: swapId,
|
|
},
|
|
);
|
|
|
|
store.dispatch(rpcSetSwapInfo(response));
|
|
}
|
|
|
|
export async function getSwapTimelock(swapId: string) {
|
|
const response = await invoke<GetSwapTimelockArgs, GetSwapTimelockResponse>(
|
|
"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<string> {
|
|
const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
|
|
"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<ResumeSwapArgs, ResumeSwapResponse>("resume_swap", {
|
|
swap_id: swapId,
|
|
});
|
|
}
|
|
|
|
export async function suspendCurrentSwap() {
|
|
await invokeNoArgs<SuspendCurrentSwapResponse>("suspend_current_swap");
|
|
}
|
|
|
|
export async function getCurrentSwapId() {
|
|
return await invokeNoArgs<GetCurrentSwapResponse>("get_current_swap");
|
|
}
|
|
|
|
export async function getMoneroRecoveryKeys(
|
|
swapId: string,
|
|
): Promise<MoneroRecoveryResponse> {
|
|
return await invoke<MoneroRecoveryArgs, MoneroRecoveryResponse>(
|
|
"monero_recovery",
|
|
{
|
|
swap_id: swapId,
|
|
},
|
|
);
|
|
}
|
|
|
|
export async function checkContextStatus(): Promise<ContextStatus> {
|
|
return await invokeNoArgs<ContextStatus>("get_context_status");
|
|
}
|
|
|
|
export async function getLogsOfSwap(
|
|
swapId: string,
|
|
redact: boolean,
|
|
): Promise<GetLogsResponse> {
|
|
return await invoke<GetLogsArgs, GetLogsResponse>("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<RedactArgs, RedactResponse>("redact", {
|
|
text: logsToRawString(logs),
|
|
});
|
|
|
|
return parseLogsFromString(response.text);
|
|
}
|
|
|
|
export async function listSellersAtRendezvousPoint(
|
|
rendezvousPointAddresses: string[],
|
|
): Promise<ListSellersResponse> {
|
|
return await invoke<ListSellersArgs, ListSellersResponse>("list_sellers", {
|
|
rendezvous_points: rendezvousPointAddresses,
|
|
});
|
|
}
|
|
|
|
export async function getWalletDescriptor() {
|
|
return await invokeNoArgs<ExportBitcoinWalletResponse>(
|
|
"get_wallet_descriptor",
|
|
);
|
|
}
|
|
|
|
export async function getMoneroNodeStatus(
|
|
node: string,
|
|
network: Network,
|
|
): Promise<boolean> {
|
|
const response = await invoke<CheckMoneroNodeArgs, CheckMoneroNodeResponse>(
|
|
"check_monero_node",
|
|
{
|
|
url: node,
|
|
network,
|
|
},
|
|
);
|
|
|
|
return response.available;
|
|
}
|
|
|
|
export async function getElectrumNodeStatus(url: string): Promise<boolean> {
|
|
const response = await invoke<
|
|
CheckElectrumNodeArgs,
|
|
CheckElectrumNodeResponse
|
|
>("check_electrum_node", {
|
|
url,
|
|
});
|
|
|
|
return response.available;
|
|
}
|
|
|
|
export async function getNodeStatus(
|
|
url: string,
|
|
blockchain: Blockchain,
|
|
network: Network,
|
|
): Promise<boolean> {
|
|
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<GetMoneroAddressesResponse> {
|
|
return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses");
|
|
}
|
|
|
|
export async function getRestoreHeight(): Promise<GetRestoreHeightResponse> {
|
|
const restoreHeight =
|
|
await invokeNoArgs<GetRestoreHeightResponse>("get_restore_height");
|
|
store.dispatch(setRestoreHeight(restoreHeight));
|
|
return restoreHeight;
|
|
}
|
|
|
|
export async function setMoneroRestoreHeight(
|
|
height: number | Date,
|
|
): Promise<SetRestoreHeightResponse> {
|
|
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<SetRestoreHeightArgs, SetRestoreHeightResponse>(
|
|
"set_monero_restore_height",
|
|
args,
|
|
);
|
|
}
|
|
|
|
export async function getMoneroHistory(): Promise<GetMoneroHistoryResponse> {
|
|
return await invokeNoArgs<GetMoneroHistoryResponse>("get_monero_history");
|
|
}
|
|
|
|
export async function getMoneroMainAddress(): Promise<GetMoneroMainAddressResponse> {
|
|
return await invokeNoArgs<GetMoneroMainAddressResponse>(
|
|
"get_monero_main_address",
|
|
);
|
|
}
|
|
|
|
export async function getMoneroBalance(): Promise<GetMoneroBalanceResponse> {
|
|
return await invokeNoArgs<GetMoneroBalanceResponse>("get_monero_balance");
|
|
}
|
|
|
|
export async function sendMonero(
|
|
args: SendMoneroArgs,
|
|
): Promise<SendMoneroResponse> {
|
|
return await invoke<SendMoneroArgs, SendMoneroResponse>("send_monero", args);
|
|
}
|
|
|
|
export async function getMoneroSyncProgress(): Promise<GetMoneroSyncProgressResponse> {
|
|
return await invokeNoArgs<GetMoneroSyncProgressResponse>(
|
|
"get_monero_sync_progress",
|
|
);
|
|
}
|
|
|
|
export async function getMoneroSeed(): Promise<GetMoneroSeedResponse> {
|
|
return await invokeNoArgs<GetMoneroSeedResponse>("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<SendMoneroResponse> {
|
|
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<string> {
|
|
const testnet = isTestnet();
|
|
return await invoke<GetDataDirArgs, string>("get_data_dir", {
|
|
is_testnet: testnet,
|
|
});
|
|
}
|
|
|
|
export async function resolveApproval<T>(
|
|
requestId: string,
|
|
accept: T,
|
|
): Promise<void> {
|
|
try {
|
|
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>(
|
|
"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<T>(
|
|
requestId: string,
|
|
reject: T,
|
|
): Promise<void> {
|
|
await invoke<RejectApprovalArgs, RejectApprovalResponse>(
|
|
"reject_approval_request",
|
|
{ request_id: requestId },
|
|
);
|
|
}
|
|
|
|
export async function refreshApprovals(): Promise<void> {
|
|
const response = await invokeNoArgs<GetPendingApprovalsResponse>(
|
|
"get_pending_approvals",
|
|
);
|
|
store.dispatch(approvalRequestsReplaced(response.approvals));
|
|
}
|
|
|
|
export async function checkSeed(seed: string): Promise<boolean> {
|
|
const response = await invoke<CheckSeedArgs, CheckSeedResponse>(
|
|
"check_seed",
|
|
{
|
|
seed,
|
|
},
|
|
);
|
|
return response.available;
|
|
}
|
|
|
|
export async function saveLogFiles(
|
|
zipFileName: string,
|
|
content: Record<string, string>,
|
|
): Promise<void> {
|
|
await invokeUnsafe<void>("save_txt_files", { zipFileName, content });
|
|
}
|
|
|
|
export async function saveFilesInDialog(files: Record<string, string>) {
|
|
await invokeUnsafe<void>("save_txt_files", {
|
|
files,
|
|
});
|
|
}
|
|
|
|
export async function dfxAuthenticate(): Promise<DfxAuthenticateResponse> {
|
|
return await invokeNoArgs<DfxAuthenticateResponse>("dfx_authenticate");
|
|
}
|
|
|
|
export async function changeMoneroNode(
|
|
nodeConfig: MoneroNodeConfig,
|
|
): Promise<void> {
|
|
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<MoneroNodeConfig> {
|
|
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;
|
|
}
|