mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-18 01:54:29 -05:00
refactor(gui): seperate get info and get timelock to speed up display of swaps (#661)
* refactor(gui): seperate get info and get timelock to speed up display of swaps * progress * progress * remove unused function useSwapInfoWithTimelock * use GetSwapTimelockArgs and GetSwapTimelockResponse types
This commit is contained in:
parent
0fec5d556d
commit
33662b0a06
11 changed files with 333 additions and 203 deletions
|
|
@ -131,10 +131,6 @@ export type GetSwapInfoResponseExtRunningSwap = GetSwapInfoResponseExt & {
|
|||
state_name: BobStateNameRunningSwap;
|
||||
};
|
||||
|
||||
export type GetSwapInfoResponseExtWithTimelock = GetSwapInfoResponseExt & {
|
||||
timelock: ExpiredTimelocks;
|
||||
};
|
||||
|
||||
export function isBobStateNameRunningSwap(
|
||||
state: BobStateName,
|
||||
): state is BobStateNameRunningSwap {
|
||||
|
|
@ -252,17 +248,6 @@ export function isGetSwapInfoResponseRunningSwap(
|
|||
return isBobStateNameRunningSwap(response.state_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for GetSwapInfoResponseExt to ensure timelock is not null
|
||||
* @param response The swap info response to check
|
||||
* @returns True if the timelock exists, false otherwise
|
||||
*/
|
||||
export function isGetSwapInfoResponseWithTimelock(
|
||||
response: GetSwapInfoResponseExt,
|
||||
): response is GetSwapInfoResponseExtWithTimelock {
|
||||
return response.timelock !== null;
|
||||
}
|
||||
|
||||
export type PendingApprovalRequest = ApprovalRequest & {
|
||||
content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
import {
|
||||
checkContextStatus,
|
||||
getSwapInfo,
|
||||
getSwapTimelock,
|
||||
initializeContext,
|
||||
listSellersAtRendezvousPoint,
|
||||
refreshApprovals,
|
||||
|
|
@ -122,12 +123,32 @@ listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
|
|||
break;
|
||||
|
||||
case "SwapDatabaseStateUpdate":
|
||||
getSwapInfo(eventData.swap_id);
|
||||
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), 3000);
|
||||
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":
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
GetSwapInfoResponseExt,
|
||||
GetSwapInfoResponseExtRunningSwap,
|
||||
isGetSwapInfoResponseRunningSwap,
|
||||
isGetSwapInfoResponseWithTimelock,
|
||||
TimelockCancel,
|
||||
TimelockNone,
|
||||
} from "models/tauriModelExt";
|
||||
|
|
@ -15,7 +14,9 @@ import HumanizedBitcoinBlockDuration from "../../other/HumanizedBitcoinBlockDura
|
|||
import TruncatedText from "../../other/TruncatedText";
|
||||
import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton";
|
||||
import { TimelockTimeline } from "./TimelockTimeline";
|
||||
import { useIsSpecificSwapRunning } from "store/hooks";
|
||||
import { useIsSpecificSwapRunning, useAppSelector } from "store/hooks";
|
||||
import { selectSwapTimelock } from "store/selectors";
|
||||
import { ExpiredTimelocks } from "models/tauriModel";
|
||||
|
||||
/**
|
||||
* Component for displaying a list of messages.
|
||||
|
|
@ -68,7 +69,11 @@ function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt }) {
|
|||
"If this step fails, you can manually redeem your funds",
|
||||
]}
|
||||
/>
|
||||
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
|
||||
<SwapMoneroRecoveryButton
|
||||
swap={swap}
|
||||
size="small"
|
||||
variant="contained"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -167,9 +172,11 @@ function PunishTimelockExpiredAlert() {
|
|||
*/
|
||||
export function StateAlert({
|
||||
swap,
|
||||
timelock,
|
||||
isRunning,
|
||||
}: {
|
||||
swap: GetSwapInfoResponseExtRunningSwap;
|
||||
timelock: ExpiredTimelocks | null;
|
||||
isRunning: boolean;
|
||||
}) {
|
||||
switch (swap.state_name) {
|
||||
|
|
@ -188,12 +195,12 @@ export function StateAlert({
|
|||
case BobStateName.BtcCancelled:
|
||||
case BobStateName.BtcRefundPublished: // Even if the transactions have been published, it cannot be
|
||||
case BobStateName.BtcEarlyRefundPublished: // guaranteed that they will be confirmed in time
|
||||
if (swap.timelock != null) {
|
||||
switch (swap.timelock.type) {
|
||||
if (timelock != null) {
|
||||
switch (timelock.type) {
|
||||
case "None":
|
||||
return (
|
||||
<BitcoinLockedNoTimelockExpiredStateAlert
|
||||
timelock={swap.timelock}
|
||||
timelock={timelock}
|
||||
cancelTimelockOffset={swap.cancel_timelock}
|
||||
punishTimelockOffset={swap.punish_timelock}
|
||||
isRunning={isRunning}
|
||||
|
|
@ -202,16 +209,14 @@ export function StateAlert({
|
|||
case "Cancel":
|
||||
return (
|
||||
<BitcoinPossiblyCancelledAlert
|
||||
timelock={swap.timelock}
|
||||
timelock={timelock}
|
||||
swap={swap}
|
||||
/>
|
||||
);
|
||||
case "Punish":
|
||||
return <PunishTimelockExpiredAlert />;
|
||||
default:
|
||||
// We have covered all possible timelock states above
|
||||
// If we reach this point, it means we have missed a case
|
||||
exhaustiveGuard(swap.timelock);
|
||||
exhaustiveGuard(timelock);
|
||||
}
|
||||
}
|
||||
return <PunishTimelockExpiredAlert />;
|
||||
|
|
@ -244,26 +249,20 @@ export default function SwapStatusAlert({
|
|||
swap: GetSwapInfoResponseExt;
|
||||
onlyShowIfUnusualAmountOfTimeHasPassed?: boolean;
|
||||
}) {
|
||||
if (swap == null) {
|
||||
return null;
|
||||
}
|
||||
const timelock = useAppSelector(selectSwapTimelock(swap.swap_id));
|
||||
|
||||
// If the swap is completed, we do not need to display anything
|
||||
if (!isGetSwapInfoResponseRunningSwap(swap)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we don't have a timelock for the swap, we cannot display the alert
|
||||
if (!isGetSwapInfoResponseWithTimelock(swap)) {
|
||||
if (timelock == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasUnusualAmountOfTimePassed =
|
||||
swap.timelock.type === "None" &&
|
||||
swap.timelock.content.blocks_left >
|
||||
UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD;
|
||||
timelock.type === "None" &&
|
||||
timelock.content.blocks_left > UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD;
|
||||
|
||||
// If we are only showing if an unusual amount of time has passed, we need to check if the swap has been running for a while
|
||||
if (onlyShowIfUnusualAmountOfTimeHasPassed && hasUnusualAmountOfTimePassed) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -291,7 +290,8 @@ export default function SwapStatusAlert({
|
|||
)
|
||||
) : (
|
||||
<>
|
||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running
|
||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is
|
||||
not running
|
||||
</>
|
||||
)}
|
||||
</AlertTitle>
|
||||
|
|
@ -302,8 +302,8 @@ export default function SwapStatusAlert({
|
|||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<StateAlert swap={swap} isRunning={isRunning} />
|
||||
<TimelockTimeline swap={swap} />
|
||||
<StateAlert swap={swap} timelock={timelock} isRunning={isRunning} />
|
||||
<TimelockTimeline swap={swap} timelock={timelock} />
|
||||
</Box>
|
||||
</Alert>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -112,9 +112,10 @@ function TimelineSegment({
|
|||
|
||||
export function TimelockTimeline({
|
||||
swap,
|
||||
timelock,
|
||||
}: {
|
||||
// This forces the timelock to not be null
|
||||
swap: GetSwapInfoResponseExt & { timelock: ExpiredTimelocks };
|
||||
swap: GetSwapInfoResponseExt;
|
||||
timelock: ExpiredTimelocks;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
|
|
@ -143,7 +144,7 @@ export function TimelockTimeline({
|
|||
|
||||
const totalBlocks = swap.cancel_timelock + swap.punish_timelock;
|
||||
const absoluteBlock = getAbsoluteBlock(
|
||||
swap.timelock,
|
||||
timelock,
|
||||
swap.cancel_timelock,
|
||||
swap.punish_timelock,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { Box } from "@mui/material";
|
||||
import ContactInfoBox from "./ContactInfoBox";
|
||||
import DonateInfoBox from "./DonateInfoBox";
|
||||
import DaemonControlBox from "./DaemonControlBox";
|
||||
import SettingsBox from "./SettingsBox";
|
||||
import ExportDataBox from "./ExportDataBox";
|
||||
import DiscoveryBox from "./DiscoveryBox";
|
||||
import MoneroPoolHealthBox from "./MoneroPoolHealthBox";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
|
|
|||
|
|
@ -48,12 +48,16 @@ import {
|
|||
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,
|
||||
|
|
@ -122,19 +126,6 @@ export const PRESET_RENDEZVOUS_POINTS = [
|
|||
"/dns4/getxmr.st/tcp/8888/p2p/12D3KooWHHwiz6WDThPT8cEurstomg3kDSxzL2L8pwxfyX2fpxVk",
|
||||
];
|
||||
|
||||
export async function fetchSellersAtPresetRendezvousPoints() {
|
||||
await Promise.all(
|
||||
PRESET_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`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function invoke<ARGS, RESPONSE>(
|
||||
command: string,
|
||||
args: ARGS,
|
||||
|
|
@ -148,6 +139,19 @@ async function invokeNoArgs<RESPONSE>(command: string): Promise<RESPONSE> {
|
|||
return invokeUnsafe(command) as Promise<RESPONSE>;
|
||||
}
|
||||
|
||||
export async function fetchSellersAtPresetRendezvousPoints() {
|
||||
await Promise.all(
|
||||
PRESET_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 (
|
||||
|
|
@ -170,58 +174,6 @@ export async function checkBitcoinBalance() {
|
|||
store.dispatch(setBitcoinBalance(response.balance));
|
||||
}
|
||||
|
||||
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 withdrawBtc(address: string): Promise<string> {
|
||||
const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
|
||||
"withdraw_btc",
|
||||
{
|
||||
address,
|
||||
amount: null,
|
||||
},
|
||||
);
|
||||
|
||||
// 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 buyXmr() {
|
||||
const state = store.getState();
|
||||
|
||||
|
|
@ -284,6 +236,155 @@ export async function buyXmr() {
|
|||
});
|
||||
}
|
||||
|
||||
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(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 withdrawBtc(address: string): Promise<string> {
|
||||
const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
|
||||
"withdraw_btc",
|
||||
{
|
||||
address,
|
||||
amount: null,
|
||||
},
|
||||
);
|
||||
|
||||
// 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,
|
||||
|
|
@ -342,58 +443,6 @@ export async function listSellersAtRendezvousPoint(
|
|||
});
|
||||
}
|
||||
|
||||
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(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWalletDescriptor() {
|
||||
return await invokeNoArgs<ExportBitcoinWalletResponse>(
|
||||
"get_wallet_descriptor",
|
||||
|
|
@ -451,21 +500,6 @@ async function updateNodeStatus(
|
|||
store.dispatch(setStatus({ node, status, blockchain }));
|
||||
}
|
||||
|
||||
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 getMoneroAddresses(): Promise<GetMoneroAddressesResponse> {
|
||||
return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
ApprovalRequest,
|
||||
TauriBackgroundProgressWrapper,
|
||||
TauriBackgroundProgress,
|
||||
ExpiredTimelocks,
|
||||
} from "models/tauriModel";
|
||||
import { MoneroRecoveryResponse } from "../../models/rpcModel";
|
||||
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||
|
|
@ -19,6 +20,9 @@ interface State {
|
|||
swapInfos: {
|
||||
[swapId: string]: GetSwapInfoResponseExt;
|
||||
};
|
||||
swapTimelocks: {
|
||||
[swapId: string]: ExpiredTimelocks;
|
||||
};
|
||||
moneroRecovery: {
|
||||
swapId: string;
|
||||
keys: MoneroRecoveryResponse;
|
||||
|
|
@ -56,6 +60,7 @@ const initialState: RPCSlice = {
|
|||
withdrawTxId: null,
|
||||
rendezvousDiscoveredSellers: [],
|
||||
swapInfos: {},
|
||||
swapTimelocks: {},
|
||||
moneroRecovery: null,
|
||||
background: {},
|
||||
backgroundRefund: null,
|
||||
|
|
@ -84,14 +89,8 @@ export const rpcSlice = createSlice({
|
|||
slice: RPCSlice,
|
||||
action: PayloadAction<TauriTimelockChangeEvent>,
|
||||
) {
|
||||
if (slice.state.swapInfos[action.payload.swap_id]) {
|
||||
slice.state.swapInfos[action.payload.swap_id].timelock =
|
||||
action.payload.timelock;
|
||||
} else {
|
||||
logger.warn(
|
||||
`Received timelock change event for unknown swap ${action.payload.swap_id}`,
|
||||
);
|
||||
}
|
||||
slice.state.swapTimelocks[action.payload.swap_id] =
|
||||
action.payload.timelock;
|
||||
},
|
||||
rpcSetWithdrawTxId(slice, action: PayloadAction<string>) {
|
||||
slice.state.withdrawTxId = action.payload;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ import {
|
|||
} from "models/tauriModel";
|
||||
import { Alert } from "models/apiModel";
|
||||
import { fnv1a } from "utils/hash";
|
||||
import {
|
||||
selectAllSwapInfos,
|
||||
selectPendingApprovals,
|
||||
selectSwapInfoWithTimelock,
|
||||
} from "./selectors";
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
||||
|
|
@ -159,8 +164,8 @@ export function useAllMakers() {
|
|||
/// This hook returns the all swap infos, as an array
|
||||
/// Excluding those who are in a state where it's better to hide them from the user
|
||||
export function useSaneSwapInfos() {
|
||||
const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos);
|
||||
return Object.values(swapInfos).filter((swap) => {
|
||||
const swapInfos = useAppSelector(selectAllSwapInfos);
|
||||
return swapInfos.filter((swap) => {
|
||||
// We hide swaps that are in the SwapSetupCompleted state
|
||||
// This is because they are probably ones where:
|
||||
// 1. The user force stopped the swap while we were waiting for their confirmation of the offer
|
||||
|
|
@ -203,10 +208,7 @@ export function useNodes<T>(selector: (nodes: NodesSlice) => T): T {
|
|||
}
|
||||
|
||||
export function usePendingApprovals(): PendingApprovalRequest[] {
|
||||
const approvals = useAppSelector((state) => state.rpc.state.approvalRequests);
|
||||
return Object.values(approvals).filter(
|
||||
(c) => c.request_status.state === "Pending",
|
||||
) as PendingApprovalRequest[];
|
||||
return useAppSelector(selectPendingApprovals) as PendingApprovalRequest[];
|
||||
}
|
||||
|
||||
export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] {
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ import { createListenerMiddleware } from "@reduxjs/toolkit";
|
|||
import { throttle, debounce } from "lodash";
|
||||
import {
|
||||
getAllSwapInfos,
|
||||
getAllSwapTimelocks,
|
||||
checkBitcoinBalance,
|
||||
getBitcoinAddress,
|
||||
updateAllNodeStatuses,
|
||||
fetchSellersAtPresetRendezvousPoints,
|
||||
getSwapInfo,
|
||||
getSwapTimelock,
|
||||
initializeMoneroWallet,
|
||||
changeMoneroNode,
|
||||
getCurrentMoneroNodeConfig,
|
||||
|
|
@ -46,7 +48,12 @@ const getThrottledSwapInfoUpdater = (swapId: string) => {
|
|||
// but will wait for 3 seconds of quiet during rapid calls (using debounce)
|
||||
const debouncedGetSwapInfo = debounce(() => {
|
||||
logger.debug(`Executing getSwapInfo for swap ${swapId}`);
|
||||
getSwapInfo(swapId);
|
||||
getSwapInfo(swapId).catch((error) => {
|
||||
logger.debug(`Failed to fetch swap info for swap ${swapId}: ${error}`);
|
||||
});
|
||||
getSwapTimelock(swapId).catch((error) => {
|
||||
logger.debug(`Failed to fetch timelock for swap ${swapId}: ${error}`);
|
||||
});
|
||||
}, 3000); // 3 seconds debounce for rapid calls
|
||||
|
||||
const throttledFunction = throttle(debouncedGetSwapInfo, 2000, {
|
||||
|
|
@ -131,6 +138,7 @@ export function createMainListeners() {
|
|||
"Database & Bitcoin wallet just became available, fetching swap infos...",
|
||||
);
|
||||
await getAllSwapInfos();
|
||||
await getAllSwapTimelocks();
|
||||
}
|
||||
|
||||
// If the database just became availiable, fetch sellers at preset rendezvous points
|
||||
|
|
|
|||
50
src-gui/src/store/selectors.ts
Normal file
50
src-gui/src/store/selectors.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { createSelector } from "@reduxjs/toolkit";
|
||||
import { RootState } from "renderer/store/storeRenderer";
|
||||
import { GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||
import { ExpiredTimelocks } from "models/tauriModel";
|
||||
|
||||
const selectRpcState = (state: RootState) => state.rpc.state;
|
||||
|
||||
export const selectAllSwapIds = createSelector([selectRpcState], (rpcState) =>
|
||||
Object.keys(rpcState.swapInfos),
|
||||
);
|
||||
|
||||
export const selectAllSwapInfos = createSelector([selectRpcState], (rpcState) =>
|
||||
Object.values(rpcState.swapInfos),
|
||||
);
|
||||
|
||||
export const selectSwapTimelocks = createSelector(
|
||||
[selectRpcState],
|
||||
(rpcState) => rpcState.swapTimelocks,
|
||||
);
|
||||
|
||||
export const selectSwapTimelock = (swapId: string) =>
|
||||
createSelector(
|
||||
[selectSwapTimelocks],
|
||||
(timelocks) => timelocks[swapId] ?? null,
|
||||
);
|
||||
|
||||
export const selectSwapInfoWithTimelock = (swapId: string) =>
|
||||
createSelector(
|
||||
[selectRpcState],
|
||||
(
|
||||
rpcState,
|
||||
):
|
||||
| (GetSwapInfoResponseExt & { timelock: ExpiredTimelocks | null })
|
||||
| null => {
|
||||
const swapInfo = rpcState.swapInfos[swapId];
|
||||
if (!swapInfo) return null;
|
||||
return {
|
||||
...swapInfo,
|
||||
timelock: rpcState.swapTimelocks[swapId] ?? null,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const selectPendingApprovals = createSelector(
|
||||
[selectRpcState],
|
||||
(rpcState) =>
|
||||
Object.values(rpcState.approvalRequests).filter(
|
||||
(c) => c.request_status.state === "Pending",
|
||||
),
|
||||
);
|
||||
|
|
@ -244,7 +244,6 @@ pub struct GetSwapInfoResponse {
|
|||
pub btc_refund_address: String,
|
||||
pub cancel_timelock: CancelTimelock,
|
||||
pub punish_timelock: PunishTimelock,
|
||||
pub timelock: Option<ExpiredTimelocks>,
|
||||
pub monero_receive_pool: MoneroAddressPool,
|
||||
}
|
||||
|
||||
|
|
@ -256,6 +255,30 @@ impl Request for GetSwapInfoArgs {
|
|||
}
|
||||
}
|
||||
|
||||
// GetSwapTimelock
|
||||
#[typeshare]
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GetSwapTimelockArgs {
|
||||
#[typeshare(serialized_as = "string")]
|
||||
pub swap_id: Uuid,
|
||||
}
|
||||
|
||||
#[typeshare]
|
||||
#[derive(Serialize)]
|
||||
pub struct GetSwapTimelockResponse {
|
||||
#[typeshare(serialized_as = "string")]
|
||||
pub swap_id: Uuid,
|
||||
pub timelock: Option<ExpiredTimelocks>,
|
||||
}
|
||||
|
||||
impl Request for GetSwapTimelockArgs {
|
||||
type Response = GetSwapTimelockResponse;
|
||||
|
||||
async fn request(self, ctx: Arc<Context>) -> Result<Self::Response> {
|
||||
get_swap_timelock(self, ctx).await
|
||||
}
|
||||
}
|
||||
|
||||
// Balance
|
||||
#[typeshare]
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -826,7 +849,6 @@ pub async fn get_swap_info(
|
|||
args: GetSwapInfoArgs,
|
||||
context: Arc<Context>,
|
||||
) -> Result<GetSwapInfoResponse> {
|
||||
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||
let db = context.try_get_db().await?;
|
||||
|
||||
let state = db.get_state(args.swap_id).await?;
|
||||
|
|
@ -890,14 +912,6 @@ pub async fn get_swap_info(
|
|||
})
|
||||
.with_context(|| "Did not find SwapSetupCompleted state for swap")?;
|
||||
|
||||
let timelock = match swap_state.expired_timelocks(bitcoin_wallet.clone()).await {
|
||||
Ok(timelock) => timelock,
|
||||
Err(err) => {
|
||||
error!(swap_id = %args.swap_id, error = ?err, "Failed to fetch expired timelock status");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let monero_receive_pool = db.get_monero_address_pool(args.swap_id).await?;
|
||||
|
||||
Ok(GetSwapInfoResponse {
|
||||
|
|
@ -918,11 +932,29 @@ pub async fn get_swap_info(
|
|||
btc_refund_address: btc_refund_address.to_string(),
|
||||
cancel_timelock,
|
||||
punish_timelock,
|
||||
timelock,
|
||||
monero_receive_pool,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(fields(method = "get_swap_timelock"), skip(context))]
|
||||
pub async fn get_swap_timelock(
|
||||
args: GetSwapTimelockArgs,
|
||||
context: Arc<Context>,
|
||||
) -> Result<GetSwapTimelockResponse> {
|
||||
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
|
||||
let db = context.try_get_db().await?;
|
||||
|
||||
let state = db.get_state(args.swap_id).await?;
|
||||
let swap_state: BobState = state.try_into()?;
|
||||
|
||||
let timelock = swap_state.expired_timelocks(bitcoin_wallet.clone()).await?;
|
||||
|
||||
Ok(GetSwapTimelockResponse {
|
||||
swap_id: args.swap_id,
|
||||
timelock,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(fields(method = "buy_xmr"), skip(context))]
|
||||
pub async fn buy_xmr(
|
||||
buy_xmr: BuyXmrArgs,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue