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:
Mohan 2025-11-02 19:41:21 +01:00 committed by GitHub
parent 0fec5d556d
commit 33662b0a06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 333 additions and 203 deletions

View file

@ -131,10 +131,6 @@ export type GetSwapInfoResponseExtRunningSwap = GetSwapInfoResponseExt & {
state_name: BobStateNameRunningSwap; state_name: BobStateNameRunningSwap;
}; };
export type GetSwapInfoResponseExtWithTimelock = GetSwapInfoResponseExt & {
timelock: ExpiredTimelocks;
};
export function isBobStateNameRunningSwap( export function isBobStateNameRunningSwap(
state: BobStateName, state: BobStateName,
): state is BobStateNameRunningSwap { ): state is BobStateNameRunningSwap {
@ -252,17 +248,6 @@ export function isGetSwapInfoResponseRunningSwap(
return isBobStateNameRunningSwap(response.state_name); 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 & { export type PendingApprovalRequest = ApprovalRequest & {
content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>; content: Extract<ApprovalRequest["request_status"], { state: "Pending" }>;
}; };

View file

@ -21,6 +21,7 @@ import {
import { import {
checkContextStatus, checkContextStatus,
getSwapInfo, getSwapInfo,
getSwapTimelock,
initializeContext, initializeContext,
listSellersAtRendezvousPoint, listSellersAtRendezvousPoint,
refreshApprovals, refreshApprovals,
@ -122,12 +123,32 @@ listen<TauriEvent>(TAURI_UNIFIED_EVENT_CHANNEL_NAME, (event) => {
break; break;
case "SwapDatabaseStateUpdate": 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 // 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 // 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 // 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; break;
case "TimelockChange": case "TimelockChange":

View file

@ -5,7 +5,6 @@ import {
GetSwapInfoResponseExt, GetSwapInfoResponseExt,
GetSwapInfoResponseExtRunningSwap, GetSwapInfoResponseExtRunningSwap,
isGetSwapInfoResponseRunningSwap, isGetSwapInfoResponseRunningSwap,
isGetSwapInfoResponseWithTimelock,
TimelockCancel, TimelockCancel,
TimelockNone, TimelockNone,
} from "models/tauriModelExt"; } from "models/tauriModelExt";
@ -15,7 +14,9 @@ import HumanizedBitcoinBlockDuration from "../../other/HumanizedBitcoinBlockDura
import TruncatedText from "../../other/TruncatedText"; import TruncatedText from "../../other/TruncatedText";
import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton"; import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton";
import { TimelockTimeline } from "./TimelockTimeline"; 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. * 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", "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> </Box>
); );
} }
@ -167,9 +172,11 @@ function PunishTimelockExpiredAlert() {
*/ */
export function StateAlert({ export function StateAlert({
swap, swap,
timelock,
isRunning, isRunning,
}: { }: {
swap: GetSwapInfoResponseExtRunningSwap; swap: GetSwapInfoResponseExtRunningSwap;
timelock: ExpiredTimelocks | null;
isRunning: boolean; isRunning: boolean;
}) { }) {
switch (swap.state_name) { switch (swap.state_name) {
@ -188,12 +195,12 @@ export function StateAlert({
case BobStateName.BtcCancelled: case BobStateName.BtcCancelled:
case BobStateName.BtcRefundPublished: // Even if the transactions have been published, it cannot be case BobStateName.BtcRefundPublished: // Even if the transactions have been published, it cannot be
case BobStateName.BtcEarlyRefundPublished: // guaranteed that they will be confirmed in time case BobStateName.BtcEarlyRefundPublished: // guaranteed that they will be confirmed in time
if (swap.timelock != null) { if (timelock != null) {
switch (swap.timelock.type) { switch (timelock.type) {
case "None": case "None":
return ( return (
<BitcoinLockedNoTimelockExpiredStateAlert <BitcoinLockedNoTimelockExpiredStateAlert
timelock={swap.timelock} timelock={timelock}
cancelTimelockOffset={swap.cancel_timelock} cancelTimelockOffset={swap.cancel_timelock}
punishTimelockOffset={swap.punish_timelock} punishTimelockOffset={swap.punish_timelock}
isRunning={isRunning} isRunning={isRunning}
@ -202,16 +209,14 @@ export function StateAlert({
case "Cancel": case "Cancel":
return ( return (
<BitcoinPossiblyCancelledAlert <BitcoinPossiblyCancelledAlert
timelock={swap.timelock} timelock={timelock}
swap={swap} swap={swap}
/> />
); );
case "Punish": case "Punish":
return <PunishTimelockExpiredAlert />; return <PunishTimelockExpiredAlert />;
default: default:
// We have covered all possible timelock states above exhaustiveGuard(timelock);
// If we reach this point, it means we have missed a case
exhaustiveGuard(swap.timelock);
} }
} }
return <PunishTimelockExpiredAlert />; return <PunishTimelockExpiredAlert />;
@ -244,26 +249,20 @@ export default function SwapStatusAlert({
swap: GetSwapInfoResponseExt; swap: GetSwapInfoResponseExt;
onlyShowIfUnusualAmountOfTimeHasPassed?: boolean; onlyShowIfUnusualAmountOfTimeHasPassed?: boolean;
}) { }) {
if (swap == null) { const timelock = useAppSelector(selectSwapTimelock(swap.swap_id));
return null;
}
// If the swap is completed, we do not need to display anything
if (!isGetSwapInfoResponseRunningSwap(swap)) { if (!isGetSwapInfoResponseRunningSwap(swap)) {
return null; return null;
} }
// If we don't have a timelock for the swap, we cannot display the alert if (timelock == null) {
if (!isGetSwapInfoResponseWithTimelock(swap)) {
return null; return null;
} }
const hasUnusualAmountOfTimePassed = const hasUnusualAmountOfTimePassed =
swap.timelock.type === "None" && timelock.type === "None" &&
swap.timelock.content.blocks_left > timelock.content.blocks_left > UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD;
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) { if (onlyShowIfUnusualAmountOfTimeHasPassed && hasUnusualAmountOfTimePassed) {
return null; 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> </AlertTitle>
@ -302,8 +302,8 @@ export default function SwapStatusAlert({
gap: 1, gap: 1,
}} }}
> >
<StateAlert swap={swap} isRunning={isRunning} /> <StateAlert swap={swap} timelock={timelock} isRunning={isRunning} />
<TimelockTimeline swap={swap} /> <TimelockTimeline swap={swap} timelock={timelock} />
</Box> </Box>
</Alert> </Alert>
); );

View file

@ -112,9 +112,10 @@ function TimelineSegment({
export function TimelockTimeline({ export function TimelockTimeline({
swap, swap,
timelock,
}: { }: {
// This forces the timelock to not be null swap: GetSwapInfoResponseExt;
swap: GetSwapInfoResponseExt & { timelock: ExpiredTimelocks }; timelock: ExpiredTimelocks;
}) { }) {
const theme = useTheme(); const theme = useTheme();
@ -143,7 +144,7 @@ export function TimelockTimeline({
const totalBlocks = swap.cancel_timelock + swap.punish_timelock; const totalBlocks = swap.cancel_timelock + swap.punish_timelock;
const absoluteBlock = getAbsoluteBlock( const absoluteBlock = getAbsoluteBlock(
swap.timelock, timelock,
swap.cancel_timelock, swap.cancel_timelock,
swap.punish_timelock, swap.punish_timelock,
); );

View file

@ -1,9 +1,7 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import ContactInfoBox from "./ContactInfoBox";
import DonateInfoBox from "./DonateInfoBox"; import DonateInfoBox from "./DonateInfoBox";
import DaemonControlBox from "./DaemonControlBox"; import DaemonControlBox from "./DaemonControlBox";
import SettingsBox from "./SettingsBox"; import SettingsBox from "./SettingsBox";
import ExportDataBox from "./ExportDataBox";
import DiscoveryBox from "./DiscoveryBox"; import DiscoveryBox from "./DiscoveryBox";
import MoneroPoolHealthBox from "./MoneroPoolHealthBox"; import MoneroPoolHealthBox from "./MoneroPoolHealthBox";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";

View file

@ -48,12 +48,16 @@ import {
MoneroNodeConfig, MoneroNodeConfig,
GetMoneroSeedResponse, GetMoneroSeedResponse,
ContextStatus, ContextStatus,
GetSwapTimelockArgs,
GetSwapTimelockResponse,
} from "models/tauriModel"; } from "models/tauriModel";
import { import {
rpcSetSwapInfo, rpcSetSwapInfo,
approvalRequestsReplaced, approvalRequestsReplaced,
contextInitializationFailed, contextInitializationFailed,
timelockChangeEventReceived,
} from "store/features/rpcSlice"; } from "store/features/rpcSlice";
import { selectAllSwapIds } from "store/selectors";
import { setBitcoinBalance } from "store/features/bitcoinWalletSlice"; import { setBitcoinBalance } from "store/features/bitcoinWalletSlice";
import { import {
setMainAddress, setMainAddress,
@ -122,19 +126,6 @@ export const PRESET_RENDEZVOUS_POINTS = [
"/dns4/getxmr.st/tcp/8888/p2p/12D3KooWHHwiz6WDThPT8cEurstomg3kDSxzL2L8pwxfyX2fpxVk", "/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>( async function invoke<ARGS, RESPONSE>(
command: string, command: string,
args: ARGS, args: ARGS,
@ -148,6 +139,19 @@ async function invokeNoArgs<RESPONSE>(command: string): Promise<RESPONSE> {
return invokeUnsafe(command) as 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() { export async function checkBitcoinBalance() {
// If we are already syncing, don't start a new sync // If we are already syncing, don't start a new sync
if ( if (
@ -170,58 +174,6 @@ export async function checkBitcoinBalance() {
store.dispatch(setBitcoinBalance(response.balance)); 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() { export async function buyXmr() {
const state = store.getState(); 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) { export async function resumeSwap(swapId: string) {
await invoke<ResumeSwapArgs, ResumeSwapResponse>("resume_swap", { await invoke<ResumeSwapArgs, ResumeSwapResponse>("resume_swap", {
swap_id: swapId, 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() { export async function getWalletDescriptor() {
return await invokeNoArgs<ExportBitcoinWalletResponse>( return await invokeNoArgs<ExportBitcoinWalletResponse>(
"get_wallet_descriptor", "get_wallet_descriptor",
@ -451,21 +500,6 @@ async function updateNodeStatus(
store.dispatch(setStatus({ node, status, blockchain })); 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> { export async function getMoneroAddresses(): Promise<GetMoneroAddressesResponse> {
return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses"); return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses");
} }

View file

@ -8,6 +8,7 @@ import {
ApprovalRequest, ApprovalRequest,
TauriBackgroundProgressWrapper, TauriBackgroundProgressWrapper,
TauriBackgroundProgress, TauriBackgroundProgress,
ExpiredTimelocks,
} from "models/tauriModel"; } from "models/tauriModel";
import { MoneroRecoveryResponse } from "../../models/rpcModel"; import { MoneroRecoveryResponse } from "../../models/rpcModel";
import { GetSwapInfoResponseExt } from "models/tauriModelExt"; import { GetSwapInfoResponseExt } from "models/tauriModelExt";
@ -19,6 +20,9 @@ interface State {
swapInfos: { swapInfos: {
[swapId: string]: GetSwapInfoResponseExt; [swapId: string]: GetSwapInfoResponseExt;
}; };
swapTimelocks: {
[swapId: string]: ExpiredTimelocks;
};
moneroRecovery: { moneroRecovery: {
swapId: string; swapId: string;
keys: MoneroRecoveryResponse; keys: MoneroRecoveryResponse;
@ -56,6 +60,7 @@ const initialState: RPCSlice = {
withdrawTxId: null, withdrawTxId: null,
rendezvousDiscoveredSellers: [], rendezvousDiscoveredSellers: [],
swapInfos: {}, swapInfos: {},
swapTimelocks: {},
moneroRecovery: null, moneroRecovery: null,
background: {}, background: {},
backgroundRefund: null, backgroundRefund: null,
@ -84,14 +89,8 @@ export const rpcSlice = createSlice({
slice: RPCSlice, slice: RPCSlice,
action: PayloadAction<TauriTimelockChangeEvent>, action: PayloadAction<TauriTimelockChangeEvent>,
) { ) {
if (slice.state.swapInfos[action.payload.swap_id]) { slice.state.swapTimelocks[action.payload.swap_id] =
slice.state.swapInfos[action.payload.swap_id].timelock =
action.payload.timelock; action.payload.timelock;
} else {
logger.warn(
`Received timelock change event for unknown swap ${action.payload.swap_id}`,
);
}
}, },
rpcSetWithdrawTxId(slice, action: PayloadAction<string>) { rpcSetWithdrawTxId(slice, action: PayloadAction<string>) {
slice.state.withdrawTxId = action.payload; slice.state.withdrawTxId = action.payload;

View file

@ -32,6 +32,11 @@ import {
} from "models/tauriModel"; } from "models/tauriModel";
import { Alert } from "models/apiModel"; import { Alert } from "models/apiModel";
import { fnv1a } from "utils/hash"; import { fnv1a } from "utils/hash";
import {
selectAllSwapInfos,
selectPendingApprovals,
selectSwapInfoWithTimelock,
} from "./selectors";
export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
@ -159,8 +164,8 @@ export function useAllMakers() {
/// This hook returns the all swap infos, as an array /// 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 /// Excluding those who are in a state where it's better to hide them from the user
export function useSaneSwapInfos() { export function useSaneSwapInfos() {
const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos); const swapInfos = useAppSelector(selectAllSwapInfos);
return Object.values(swapInfos).filter((swap) => { return swapInfos.filter((swap) => {
// We hide swaps that are in the SwapSetupCompleted state // We hide swaps that are in the SwapSetupCompleted state
// This is because they are probably ones where: // 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 // 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[] { export function usePendingApprovals(): PendingApprovalRequest[] {
const approvals = useAppSelector((state) => state.rpc.state.approvalRequests); return useAppSelector(selectPendingApprovals) as PendingApprovalRequest[];
return Object.values(approvals).filter(
(c) => c.request_status.state === "Pending",
) as PendingApprovalRequest[];
} }
export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] { export function usePendingLockBitcoinApproval(): PendingLockBitcoinApprovalRequest[] {

View file

@ -2,11 +2,13 @@ import { createListenerMiddleware } from "@reduxjs/toolkit";
import { throttle, debounce } from "lodash"; import { throttle, debounce } from "lodash";
import { import {
getAllSwapInfos, getAllSwapInfos,
getAllSwapTimelocks,
checkBitcoinBalance, checkBitcoinBalance,
getBitcoinAddress, getBitcoinAddress,
updateAllNodeStatuses, updateAllNodeStatuses,
fetchSellersAtPresetRendezvousPoints, fetchSellersAtPresetRendezvousPoints,
getSwapInfo, getSwapInfo,
getSwapTimelock,
initializeMoneroWallet, initializeMoneroWallet,
changeMoneroNode, changeMoneroNode,
getCurrentMoneroNodeConfig, getCurrentMoneroNodeConfig,
@ -46,7 +48,12 @@ const getThrottledSwapInfoUpdater = (swapId: string) => {
// but will wait for 3 seconds of quiet during rapid calls (using debounce) // but will wait for 3 seconds of quiet during rapid calls (using debounce)
const debouncedGetSwapInfo = debounce(() => { const debouncedGetSwapInfo = debounce(() => {
logger.debug(`Executing getSwapInfo for swap ${swapId}`); 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 }, 3000); // 3 seconds debounce for rapid calls
const throttledFunction = throttle(debouncedGetSwapInfo, 2000, { const throttledFunction = throttle(debouncedGetSwapInfo, 2000, {
@ -131,6 +138,7 @@ export function createMainListeners() {
"Database & Bitcoin wallet just became available, fetching swap infos...", "Database & Bitcoin wallet just became available, fetching swap infos...",
); );
await getAllSwapInfos(); await getAllSwapInfos();
await getAllSwapTimelocks();
} }
// If the database just became availiable, fetch sellers at preset rendezvous points // If the database just became availiable, fetch sellers at preset rendezvous points

View 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",
),
);

View file

@ -244,7 +244,6 @@ pub struct GetSwapInfoResponse {
pub btc_refund_address: String, pub btc_refund_address: String,
pub cancel_timelock: CancelTimelock, pub cancel_timelock: CancelTimelock,
pub punish_timelock: PunishTimelock, pub punish_timelock: PunishTimelock,
pub timelock: Option<ExpiredTimelocks>,
pub monero_receive_pool: MoneroAddressPool, 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 // Balance
#[typeshare] #[typeshare]
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
@ -826,7 +849,6 @@ pub async fn get_swap_info(
args: GetSwapInfoArgs, args: GetSwapInfoArgs,
context: Arc<Context>, context: Arc<Context>,
) -> Result<GetSwapInfoResponse> { ) -> Result<GetSwapInfoResponse> {
let bitcoin_wallet = context.try_get_bitcoin_wallet().await?;
let db = context.try_get_db().await?; let db = context.try_get_db().await?;
let state = db.get_state(args.swap_id).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")?; .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?; let monero_receive_pool = db.get_monero_address_pool(args.swap_id).await?;
Ok(GetSwapInfoResponse { Ok(GetSwapInfoResponse {
@ -918,11 +932,29 @@ pub async fn get_swap_info(
btc_refund_address: btc_refund_address.to_string(), btc_refund_address: btc_refund_address.to_string(),
cancel_timelock, cancel_timelock,
punish_timelock, punish_timelock,
timelock,
monero_receive_pool, 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))] #[tracing::instrument(fields(method = "buy_xmr"), skip(context))]
pub async fn buy_xmr( pub async fn buy_xmr(
buy_xmr: BuyXmrArgs, buy_xmr: BuyXmrArgs,