Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- GUI: Fix flickering of offers
- Remove three dead rendezvous points

## [4.7.10] - 2026-06-02
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import SortIcon from "@mui/icons-material/Sort";
import CheckIcon from "@mui/icons-material/Check";
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
import MakerOfferItem from "./MakerOfferItem";
import { usePendingSelectMakerApproval } from "store/hooks";
import MakerDiscoveryStatus from "./MakerDiscoveryStatus";
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
import { SatsAmount } from "renderer/components/other/Units";
import { useEffect, useState } from "react";
import { sortApprovalsAndKnownQuotes, OfferSortMode } from "utils/sortUtils";
import { OfferSortMode } from "utils/sortUtils";
import { useCachedMakerOffers } from "./useCachedMakerOffers";

const SORT_OPTIONS: { value: OfferSortMode; label: string }[] = [
{ value: "large", label: "Large swaps" },
Expand All @@ -32,14 +32,12 @@ export default function DepositAndChooseOfferPage({
max_giveable,
known_quotes,
}: TauriSwapProgressEventContent<"WaitingForBtcDeposit">) {
const pendingSelectMakerApprovals = usePendingSelectMakerApproval();
const [currentPage, setCurrentPage] = useState(1);
const [sortMode, setSortMode] = useState<OfferSortMode>("small");
const [sortAnchorEl, setSortAnchorEl] = useState<null | HTMLElement>(null);
const offersPerPage = 3;

const makerOffers = sortApprovalsAndKnownQuotes(
pendingSelectMakerApprovals,
const makerOffers = useCachedMakerOffers(
known_quotes,
sortMode,
offersPerPage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
SatsAmount,
} from "renderer/components/other/Units";
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
import { resolveApproval } from "renderer/rpc";
import { useResolveSelectMakerApproval } from "./useResolveSelectMakerApproval";
import WarningIcon from "@mui/icons-material/Warning";
import FavoriteIcon from "@mui/icons-material/Favorite";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
Expand Down Expand Up @@ -68,6 +68,7 @@ export default function MakerOfferItem({
const isOutOfLiquidity = quote.max_quantity == 0;
const isTooOld = isMakerVersionTooOld(version);
const priorityMaker = showAsPriority ? getPriorityMaker(peer_id) : undefined;
const resolveSelectMakerApproval = useResolveSelectMakerApproval();

return (
<Paper
Expand Down Expand Up @@ -163,12 +164,7 @@ export default function MakerOfferItem({
</Box>
<PromiseInvokeButton
variant="contained"
onInvoke={() => {
if (!requestId) {
throw new Error("Request ID is required");
}
return resolveApproval(requestId, true as unknown as object);
}}
onInvoke={() => resolveSelectMakerApproval(peer_id, requestId)}
displayErrorSnackbar
disabled={!requestId}
tooltipTitle={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { QuoteWithAddress } from "models/tauriModel";
import { useAppSelector, usePendingSelectMakerApproval } from "store/hooks";
import _ from "lodash";
import {
OfferSortMode,
SortedMakerEntry,
sortApprovalsAndKnownQuotes,
} from "utils/sortUtils";

const REFRESH_INTERVAL_MS = 5_000;

// The sorted list re-shuffles whenever the backend streams an approval or
// quote update. We snapshot it and only refresh on a fixed cadence so cards
// don't visibly flicker. The snapshot is also refreshed immediately on
// sort-mode change, on Bitcoin balance change, and whenever a new peer
// appears, so newly-discovered makers and freshly-deposited funds don't get
// stuck behind the cadence.
export function useCachedMakerOffers(
known_quotes: QuoteWithAddress[],
sortMode: OfferSortMode,
offersPerPage: number,
): SortedMakerEntry[] {
const pendingApprovals = usePendingSelectMakerApproval();
const bitcoinBalance = useAppSelector((state) => state.bitcoinWallet.balance);

const liveOffers = useMemo(
() =>
sortApprovalsAndKnownQuotes(
pendingApprovals,
known_quotes,
sortMode,
offersPerPage,
),
[pendingApprovals, known_quotes, sortMode, offersPerPage],
);

const liveOffersRef = useRef<SortedMakerEntry[]>(liveOffers);
liveOffersRef.current = liveOffers;

const [snapshot, setSnapshot] = useState<SortedMakerEntry[]>(liveOffers);

useEffect(() => {
setSnapshot(liveOffersRef.current);
}, [sortMode, bitcoinBalance]);

useEffect(() => {
const snapshotPeers = new Set(
snapshot.map((o) => o.quote_with_address.peer_id),
);
const hasNewPeer = liveOffers.some(
(o) => !snapshotPeers.has(o.quote_with_address.peer_id),
);
if (hasNewPeer) setSnapshot(liveOffers);
}, [snapshot, liveOffers]);

useEffect(() => {
const id = window.setInterval(() => {
setSnapshot((prev) =>
_.isEqual(prev, liveOffersRef.current) ? prev : liveOffersRef.current,
);
}, REFRESH_INTERVAL_MS);
return () => window.clearInterval(id);
}, []);
Comment thread
cursor[bot] marked this conversation as resolved.

return snapshot;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useCallback } from "react";
import { store, type RootState } from "renderer/store/storeRenderer";
import { resolveApproval } from "renderer/rpc";
import { isPendingSelectMakerApprovalEvent } from "models/tauriModelExt";

const WAIT_FOR_APPROVAL_MS = 5_000;

function findPendingApprovalForPeer(
state: RootState,
peerId: string,
): string | null {
const requests = Object.values(state.rpc.state.approvalRequests);
for (const req of requests) {
if (req.request_status.state !== "Pending") continue;
if (!isPendingSelectMakerApprovalEvent(req)) continue;
if (req.request.content.maker.peer_id === peerId) return req.request_id;
}
return null;
}

/**
* The snapshot list shown to the user can hold a request id that has already
* expired by the time they click Select. In that case we wait briefly for the
* backend to emit a fresh pending approval for the same peer and resolve that
* one instead, so a stale snapshot doesn't surface as a confusing error.
*/
export function useResolveSelectMakerApproval() {
return useCallback(
async (peerId: string, snapshotRequestId: string | undefined) => {
const tryResolve = (id: string) =>
resolveApproval(id, true as unknown as object);

if (snapshotRequestId) {
const stillPending =
store.getState().rpc.state.approvalRequests[snapshotRequestId]
?.request_status.state === "Pending";
if (stillPending) return tryResolve(snapshotRequestId);
}

const immediate = findPendingApprovalForPeer(store.getState(), peerId);
if (immediate) return tryResolve(immediate);

const freshId = await new Promise<string | null>((resolve) => {
const timeout = window.setTimeout(() => {
unsubscribe();
resolve(null);
}, WAIT_FOR_APPROVAL_MS);

const unsubscribe = store.subscribe(() => {
const found = findPendingApprovalForPeer(store.getState(), peerId);
if (found) {
window.clearTimeout(timeout);
unsubscribe();
resolve(found);
}
});
});

if (!freshId) {
throw new Error(
"This offer is no longer available. Please pick another maker.",
);
}
return tryResolve(freshId);
},
[],
);
}
Loading