mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-18 10:02:45 -05:00
* 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
347 lines
11 KiB
TypeScript
347 lines
11 KiB
TypeScript
import { sortBy, sum } from "lodash";
|
|
import {
|
|
BobStateName,
|
|
GetSwapInfoResponseExt,
|
|
isBitcoinSyncProgress,
|
|
isPendingBackgroundProcess,
|
|
isPendingLockBitcoinApprovalEvent,
|
|
isPendingSeedSelectionApprovalEvent,
|
|
PendingApprovalRequest,
|
|
PendingLockBitcoinApprovalRequest,
|
|
PendingSelectMakerApprovalRequest,
|
|
isPendingSelectMakerApprovalEvent,
|
|
haveFundsBeenLocked,
|
|
PendingSeedSelectionApprovalRequest,
|
|
PendingSendMoneroApprovalRequest,
|
|
isPendingSendMoneroApprovalEvent,
|
|
PendingPasswordApprovalRequest,
|
|
isPendingPasswordApprovalEvent,
|
|
isContextFullyInitialized,
|
|
} from "models/tauriModelExt";
|
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
|
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
|
|
import { parseDateString } from "utils/parseUtils";
|
|
import { useMemo } from "react";
|
|
import { isCliLogRelatedToSwap } from "models/cliModel";
|
|
import { SettingsState } from "./features/settingsSlice";
|
|
import { NodesSlice } from "./features/nodesSlice";
|
|
import { RatesState } from "./features/ratesSlice";
|
|
import {
|
|
TauriBackgroundProgress,
|
|
TauriBitcoinSyncProgress,
|
|
} 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;
|
|
|
|
export function useResumeableSwapsCount(
|
|
additionalFilter?: (s: GetSwapInfoResponseExt) => boolean,
|
|
) {
|
|
const saneSwapInfos = useSaneSwapInfos();
|
|
|
|
return useAppSelector(
|
|
(state) =>
|
|
saneSwapInfos.filter(
|
|
(swapInfo: GetSwapInfoResponseExt) =>
|
|
!swapInfo.completed &&
|
|
(additionalFilter == null || additionalFilter(swapInfo)),
|
|
).length,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Counts the number of resumeable swaps excluding:
|
|
* - Punished swaps
|
|
* - Swaps where the sanity check was not passed (e.g. they were aborted)
|
|
*/
|
|
export function useResumeableSwapsCountExcludingPunished() {
|
|
return useResumeableSwapsCount(
|
|
(s) =>
|
|
s.state_name !== BobStateName.BtcPunished &&
|
|
s.state_name !== BobStateName.SwapSetupCompleted,
|
|
);
|
|
}
|
|
|
|
/// Returns true if we have any swap that is running
|
|
export function useIsSwapRunning() {
|
|
return useAppSelector(
|
|
(state) =>
|
|
state.swap.state !== null && state.swap.state.curr.type !== "Released",
|
|
);
|
|
}
|
|
|
|
/// Returns true if we have a swap that is running and
|
|
/// that swap has any funds locked
|
|
export function useIsSwapRunningAndHasFundsLocked() {
|
|
const swapInfo = useActiveSwapInfo();
|
|
const swapTauriState = useAppSelector(
|
|
(state) => state.swap.state?.curr ?? null,
|
|
);
|
|
|
|
// If the swap is in the Released state, we return false
|
|
if (swapTauriState?.type === "Released") {
|
|
return false;
|
|
}
|
|
|
|
// If the tauri state tells us that funds have been locked, we return true
|
|
if (haveFundsBeenLocked(swapTauriState)) {
|
|
return true;
|
|
}
|
|
|
|
// If we have a database entry (swapInfo) for this swap, we return true
|
|
if (swapInfo != null) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Returns true if we have a swap that is running
|
|
export function useIsSpecificSwapRunning(swapId: string | null) {
|
|
if (swapId == null) {
|
|
return false;
|
|
}
|
|
|
|
return useAppSelector(
|
|
(state) =>
|
|
state.swap.state !== null &&
|
|
state.swap.state.swapId === swapId &&
|
|
state.swap.state.curr.type !== "Released",
|
|
);
|
|
}
|
|
|
|
export function useIsContextAvailable() {
|
|
return useAppSelector((state) => isContextFullyInitialized(state.rpc.status));
|
|
}
|
|
|
|
/// We do not use a sanity check here, as opposed to the other useSwapInfo hooks,
|
|
/// because we are explicitly asking for a specific swap
|
|
export function useSwapInfo(
|
|
swapId: string | null,
|
|
): GetSwapInfoResponseExt | null {
|
|
return useAppSelector((state) =>
|
|
swapId ? (state.rpc.state.swapInfos[swapId] ?? null) : null,
|
|
);
|
|
}
|
|
|
|
export function useActiveSwapId(): string | null {
|
|
return useAppSelector((s) => s.swap.state?.swapId ?? null);
|
|
}
|
|
|
|
export function useActiveSwapInfo(): GetSwapInfoResponseExt | null {
|
|
const swapId = useActiveSwapId();
|
|
return useSwapInfo(swapId);
|
|
}
|
|
|
|
export function useActiveSwapLogs() {
|
|
const swapId = useActiveSwapId();
|
|
const logs = useAppSelector((s) => s.logs.state.logs);
|
|
|
|
return useMemo(() => {
|
|
if (swapId == null) {
|
|
return [];
|
|
}
|
|
|
|
return logs.filter((log) => isCliLogRelatedToSwap(log.log, swapId));
|
|
}, [logs, swapId]);
|
|
}
|
|
|
|
export function useAllMakers() {
|
|
return useAppSelector((state) => {
|
|
const registryMakers = state.makers.registry.makers || [];
|
|
const listSellersMakers = state.makers.rendezvous.makers || [];
|
|
return [...registryMakers, ...listSellersMakers];
|
|
});
|
|
}
|
|
|
|
/// 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(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
|
|
// 2. We where therefore unable to transition to SafelyAborted
|
|
if (swap.state_name === BobStateName.SwapSetupCompleted) {
|
|
return false;
|
|
}
|
|
|
|
// We hide swaps that were safely aborted
|
|
// No funds were locked. Cannot be resumed.
|
|
// Wouldn't be beneficial to show them to the user
|
|
if (swap.state_name === BobStateName.SafelyAborted) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/// This hook returns the swap infos sorted by date
|
|
export function useSwapInfosSortedByDate() {
|
|
const swapInfos = useSaneSwapInfos();
|
|
|
|
return sortBy(swapInfos, (swap) => -parseDateString(swap.start_date));
|
|
}
|
|
|
|
export function useRates<T>(selector: (rates: RatesState) => T): T {
|
|
const rates = useAppSelector((state) => state.rates);
|
|
return selector(rates);
|
|
}
|
|
|
|
export function useSettings<T>(selector: (settings: SettingsState) => T): T {
|
|
const settings = useAppSelector((state) => state.settings);
|
|
return selector(settings);
|
|
}
|
|
|
|
export function useNodes<T>(selector: (nodes: NodesSlice) => T): T {
|
|
const nodes = useAppSelector((state) => state.nodes);
|
|
return selector(nodes);
|
|
}
|
|
|
|
export function usePendingApprovals(): PendingApprovalRequest[] {
|
|
return useAppSelector(selectPendingApprovals) as PendingApprovalRequest[];
|
|
}
|
|
|
|
export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] {
|
|
const approvals = usePendingApprovals();
|
|
return approvals.filter((c) => isPendingLockBitcoinApprovalEvent(c));
|
|
}
|
|
|
|
export function usePendingSendMoneroApproval(): PendingSendMoneroApprovalRequest[] {
|
|
const approvals = usePendingApprovals();
|
|
return approvals.filter((c) => isPendingSendMoneroApprovalEvent(c));
|
|
}
|
|
|
|
export function usePendingSelectMakerApproval(): PendingSelectMakerApprovalRequest[] {
|
|
const approvals = usePendingApprovals();
|
|
return approvals.filter((c) => isPendingSelectMakerApprovalEvent(c));
|
|
}
|
|
|
|
export function usePendingSeedSelectionApproval(): PendingSeedSelectionApprovalRequest[] {
|
|
const approvals = usePendingApprovals();
|
|
return approvals.filter((c) => isPendingSeedSelectionApprovalEvent(c));
|
|
}
|
|
|
|
export function usePendingPasswordApproval(): PendingPasswordApprovalRequest[] {
|
|
const approvals = usePendingApprovals();
|
|
return approvals.filter((c) => isPendingPasswordApprovalEvent(c));
|
|
}
|
|
|
|
/// Returns all the pending background processes
|
|
/// In the format [id, {componentName, {type: "Pending", content: {consumed, total}}}]
|
|
export function usePendingBackgroundProcesses(): [
|
|
string,
|
|
TauriBackgroundProgress,
|
|
][] {
|
|
const background = useAppSelector((state) => state.rpc.state.background);
|
|
return Object.entries(background).filter(([_, c]) =>
|
|
isPendingBackgroundProcess(c),
|
|
);
|
|
}
|
|
|
|
export function useBitcoinSyncProgress(): TauriBitcoinSyncProgress[] {
|
|
const pendingProcesses = usePendingBackgroundProcesses();
|
|
const syncingProcesses = pendingProcesses
|
|
.map(([_, c]) => c)
|
|
.filter(isBitcoinSyncProgress);
|
|
return syncingProcesses.map((c) => c.progress.content);
|
|
}
|
|
|
|
export function isSyncingBitcoin(): boolean {
|
|
const syncProgress = useBitcoinSyncProgress();
|
|
return syncProgress.length > 0;
|
|
}
|
|
|
|
/// This function returns the cumulative sync progress of all currently running Bitcoin wallet syncs
|
|
/// If all syncs are unknown, it returns {type: "Unknown"}
|
|
/// If at least one sync is known, it returns {type: "Known", content: {consumed, total}}
|
|
/// where consumed and total are the sum of all the consumed and total values of the syncs
|
|
export function useConservativeBitcoinSyncProgress(): TauriBitcoinSyncProgress | null {
|
|
const syncingProcesses = useBitcoinSyncProgress();
|
|
const progressValues = syncingProcesses.map((c) => c.content?.consumed ?? 0);
|
|
const totalValues = syncingProcesses.map((c) => c.content?.total ?? 0);
|
|
|
|
const progress = sum(progressValues);
|
|
const total = sum(totalValues);
|
|
|
|
// If either the progress or the total is 0, we consider the sync to be unknown
|
|
if (progress === 0 || total === 0) {
|
|
return {
|
|
type: "Unknown",
|
|
};
|
|
}
|
|
|
|
return {
|
|
type: "Known",
|
|
content: {
|
|
consumed: progress,
|
|
total: total,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculates the number of unread messages from staff for a specific feedback conversation.
|
|
* @param feedbackId The ID of the feedback conversation.
|
|
* @returns The number of unread staff messages.
|
|
*/
|
|
export function useUnreadMessagesCount(feedbackId: string): number {
|
|
const { conversationsMap, seenMessagesSet } = useAppSelector((state) => ({
|
|
conversationsMap: state.conversations.conversations,
|
|
// Convert seenMessages array to a Set for efficient lookup
|
|
seenMessagesSet: new Set(state.conversations.seenMessages),
|
|
}));
|
|
|
|
const messages = conversationsMap[feedbackId] || [];
|
|
|
|
const unreadStaffMessages = messages.filter(
|
|
(msg) => msg.is_from_staff && !seenMessagesSet.has(msg.id.toString()),
|
|
);
|
|
|
|
return unreadStaffMessages.length;
|
|
}
|
|
|
|
/**
|
|
* Calculates the total number of unread messages from staff across all feedback conversations.
|
|
* @returns The total number of unread staff messages.
|
|
*/
|
|
export function useTotalUnreadMessagesCount(): number {
|
|
const { conversationsMap, seenMessagesSet } = useAppSelector((state) => ({
|
|
conversationsMap: state.conversations.conversations,
|
|
seenMessagesSet: new Set(state.conversations.seenMessages),
|
|
}));
|
|
|
|
let totalUnreadCount = 0;
|
|
for (const feedbackId in conversationsMap) {
|
|
const messages = conversationsMap[feedbackId] || [];
|
|
const unreadStaffMessages = messages.filter(
|
|
(msg) => msg.is_from_staff && !seenMessagesSet.has(msg.id.toString()),
|
|
);
|
|
totalUnreadCount += unreadStaffMessages.length;
|
|
}
|
|
|
|
return totalUnreadCount;
|
|
}
|
|
|
|
/// Returns all the alerts that have not been acknowledged
|
|
export function useAlerts(): Alert[] {
|
|
return useAppSelector((state) =>
|
|
state.alerts.alerts.filter(
|
|
(alert) =>
|
|
// Check if there is an acknowledgement with
|
|
// the same id and the same title hash
|
|
!state.alerts.acknowledgedAlerts.some(
|
|
(ack) => ack.id === alert.id && ack.titleHash === fnv1a(alert.title),
|
|
),
|
|
),
|
|
);
|
|
}
|