diff --git a/src-gui/src/models/tauriModelExt.ts b/src-gui/src/models/tauriModelExt.ts index 7e3f5c58..7773f672 100644 --- a/src-gui/src/models/tauriModelExt.ts +++ b/src-gui/src/models/tauriModelExt.ts @@ -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; }; diff --git a/src-gui/src/renderer/background.ts b/src-gui/src/renderer/background.ts index f5e606a6..8a814740 100644 --- a/src-gui/src/renderer/background.ts +++ b/src-gui/src/renderer/background.ts @@ -21,6 +21,7 @@ import { import { checkContextStatus, getSwapInfo, + getSwapTimelock, initializeContext, listSellersAtRendezvousPoint, refreshApprovals, @@ -122,12 +123,32 @@ listen(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": diff --git a/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx b/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx index c2112c7b..9b780fad 100644 --- a/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx +++ b/src-gui/src/renderer/components/alert/SwapStatusAlert/SwapStatusAlert.tsx @@ -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", ]} /> - + ); } @@ -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 ( ); case "Punish": return ; 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 ; @@ -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 {swap.swap_id} is not running + Swap {swap.swap_id} is + not running )} @@ -302,8 +302,8 @@ export default function SwapStatusAlert({ gap: 1, }} > - - + + ); diff --git a/src-gui/src/renderer/components/alert/SwapStatusAlert/TimelockTimeline.tsx b/src-gui/src/renderer/components/alert/SwapStatusAlert/TimelockTimeline.tsx index fea86f3c..0a3647bd 100644 --- a/src-gui/src/renderer/components/alert/SwapStatusAlert/TimelockTimeline.tsx +++ b/src-gui/src/renderer/components/alert/SwapStatusAlert/TimelockTimeline.tsx @@ -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, ); diff --git a/src-gui/src/renderer/components/pages/help/SettingsPage.tsx b/src-gui/src/renderer/components/pages/help/SettingsPage.tsx index dd3ed571..e608167b 100644 --- a/src-gui/src/renderer/components/pages/help/SettingsPage.tsx +++ b/src-gui/src/renderer/components/pages/help/SettingsPage.tsx @@ -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"; diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 75871fb5..4cfd41e6 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -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( command: string, args: ARGS, @@ -148,6 +139,19 @@ async function invokeNoArgs(command: string): Promise { return invokeUnsafe(command) as Promise; } +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("get_balance", { - force_refresh: false, - }); - - store.dispatch(setBitcoinBalance(response.balance)); -} - -export async function getBitcoinAddress() { - const response = await invokeNoArgs( - "get_bitcoin_address", - ); - - return response.address; -} - -export async function getAllSwapInfos() { - const response = - await invokeNoArgs("get_swap_infos_all"); - - response.forEach((swapInfo) => { - store.dispatch(rpcSetSwapInfo(swapInfo)); - }); -} - -export async function getSwapInfo(swapId: string) { - const response = await invoke( - "get_swap_info", - { - swap_id: swapId, - }, - ); - - store.dispatch(rpcSetSwapInfo(response)); -} - -export async function withdrawBtc(address: string): Promise { - const response = await invoke( - "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("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("get_balance", { + force_refresh: false, + }); + + store.dispatch(setBitcoinBalance(response.balance)); +} + +export async function getBitcoinAddress() { + const response = await invokeNoArgs( + "get_bitcoin_address", + ); + + return response.address; +} + +export async function getAllSwapInfos() { + const response = + await invokeNoArgs("get_swap_infos_all"); + + response.forEach((swapInfo) => { + store.dispatch(rpcSetSwapInfo(swapInfo)); + }); +} + +export async function getSwapInfo(swapId: string) { + const response = await invoke( + "get_swap_info", + { + swap_id: swapId, + }, + ); + + store.dispatch(rpcSetSwapInfo(response)); +} + +export async function getSwapTimelock(swapId: string) { + const response = await invoke< + 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 { + const response = await invoke( + "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("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("initialize_context", { - settings: tauriSettings, - testnet, - }); - logger.info("Initialized context"); - } catch (error) { - throw new Error(error); - } -} - export async function getWalletDescriptor() { return await invokeNoArgs( "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 { return await invokeNoArgs("get_monero_addresses"); } diff --git a/src-gui/src/store/features/rpcSlice.ts b/src-gui/src/store/features/rpcSlice.ts index 1e2d080f..15858632 100644 --- a/src-gui/src/store/features/rpcSlice.ts +++ b/src-gui/src/store/features/rpcSlice.ts @@ -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, ) { - 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) { slice.state.withdrawTxId = action.payload; diff --git a/src-gui/src/store/hooks.ts b/src-gui/src/store/hooks.ts index df6db140..15aeda51 100644 --- a/src-gui/src/store/hooks.ts +++ b/src-gui/src/store/hooks.ts @@ -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(); export const useAppSelector: TypedUseSelectorHook = 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(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[] { diff --git a/src-gui/src/store/middleware/storeListener.ts b/src-gui/src/store/middleware/storeListener.ts index 5e3e77aa..3d546abc 100644 --- a/src-gui/src/store/middleware/storeListener.ts +++ b/src-gui/src/store/middleware/storeListener.ts @@ -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 diff --git a/src-gui/src/store/selectors.ts b/src-gui/src/store/selectors.ts new file mode 100644 index 00000000..9cf67d12 --- /dev/null +++ b/src-gui/src/store/selectors.ts @@ -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", + ), +); diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs index 8db84941..b7af2645 100644 --- a/swap/src/cli/api/request.rs +++ b/swap/src/cli/api/request.rs @@ -244,7 +244,6 @@ pub struct GetSwapInfoResponse { pub btc_refund_address: String, pub cancel_timelock: CancelTimelock, pub punish_timelock: PunishTimelock, - pub timelock: Option, 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, +} + +impl Request for GetSwapTimelockArgs { + type Response = GetSwapTimelockResponse; + + async fn request(self, ctx: Arc) -> Result { + 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, ) -> Result { - 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, +) -> Result { + 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,