Skip to content
Draft
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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions src-gui/src/dev/mockSwapEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ const happyPath: TauriSwapProgressEvent[] = [
xmr_receive_pool: MOCK_RECEIVE_POOL,
},
},
{ type: "Released" },
{ type: "Released", content: {} },
];

const cooperativeRedeem: TauriSwapProgressEvent[] = [
Expand All @@ -238,7 +238,7 @@ const cooperativeRedeem: TauriSwapProgressEvent[] = [
xmr_receive_pool: MOCK_RECEIVE_POOL,
},
},
{ type: "Released" },
{ type: "Released", content: {} },
];

const cooperativeRedeemRejected: TauriSwapProgressEvent[] = [
Expand All @@ -261,7 +261,7 @@ const cooperativeRedeemRejected: TauriSwapProgressEvent[] = [
content: { btc_refund_txid: MOCK_BTC_REFUND_TXID },
},
{ type: "BtcRefunded", content: { btc_refund_txid: MOCK_BTC_REFUND_TXID } },
{ type: "Released" },
{ type: "Released", content: {} },
];

const earlyRefund: TauriSwapProgressEvent[] = [
Expand All @@ -274,7 +274,7 @@ const earlyRefund: TauriSwapProgressEvent[] = [
type: "BtcEarlyRefunded",
content: { btc_early_refund_txid: MOCK_BTC_EARLY_REFUND_TXID },
},
{ type: "Released" },
{ type: "Released", content: {} },
];

const partialRefundWithAmnesty: TauriSwapProgressEvent[] = [
Expand Down Expand Up @@ -333,7 +333,7 @@ const partialRefundWithAmnesty: TauriSwapProgressEvent[] = [
btc_amnesty_amount: MOCK_BTC_AMNESTY_AMOUNT,
},
},
{ type: "Released" },
{ type: "Released", content: {} },
];

const partialRefundWithBurn: TauriSwapProgressEvent[] = [
Expand Down Expand Up @@ -392,7 +392,7 @@ const partialRefundWithBurn: TauriSwapProgressEvent[] = [
btc_amnesty_amount: MOCK_BTC_AMNESTY_AMOUNT,
},
},
{ type: "Released" },
{ type: "Released", content: {} },
];

const partialRefundWithWithholdAndMercy: TauriSwapProgressEvent[] = [
Expand Down Expand Up @@ -467,7 +467,7 @@ const partialRefundWithWithholdAndMercy: TauriSwapProgressEvent[] = [
btc_amnesty_amount: MOCK_BTC_AMNESTY_AMOUNT,
},
},
{ type: "Released" },
{ type: "Released", content: {} },
];

export const scenarios: Record<string, TauriSwapProgressEvent[]> = {
Expand Down
2 changes: 1 addition & 1 deletion src-gui/src/models/storeModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type SwapState = {
};

export interface SwapSlice {
state: SwapState | null;
swaps: Record<string, SwapState>;
logs: CliLog[];
spawnType: SwapSpawnType | null;
/** DEV ONLY: When true, prevents Tauri calls in the swap progress listener */
Expand Down
16 changes: 16 additions & 0 deletions src-gui/src/models/tauriModelExt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,22 @@ export function haveFundsBeenLocked(
return true;
}

/**
* A swap is in the "offer phase" while the user is still picking/accepting an
* offer (no funds committed yet). Once setup completes and we move to locking
* Bitcoin, the swap belongs to the "swaps" tab instead.
*/
export function isOfferPhase(event: TauriSwapProgressEvent): boolean {
switch (event.type) {
case "ReceivedQuote":
case "WaitingForBtcDeposit":
case "SwapSetupInflight":
return true;
default:
return false;
}
}

export function isContextFullyInitialized(
status: ResultContextStatus | null,
): boolean {
Expand Down
26 changes: 26 additions & 0 deletions src-gui/src/renderer/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getSwapTimelock,
initializeContext,
refreshApprovals,
resumeAllSwaps,
updateAllNodeStatuses,
} from "./rpc";
import { store } from "./store/storeRenderer";
Expand Down Expand Up @@ -62,6 +63,28 @@ const FETCH_PENDING_APPROVALS_INTERVAL = 2 * 1_000;
// Check context status every 2 seconds
const CHECK_CONTEXT_STATUS_INTERVAL = 2 * 1_000;

// Retry resume_all_swaps every 3 seconds until it succeeds once
const RESUME_ALL_SWAPS_RETRY_INTERVAL = 3 * 1_000;

async function resumeAllSwapsUntilSuccess(): Promise<void> {
while (true) {
try {
const response = await resumeAllSwaps();
logger.info(
`Resumed ${response.resumed_swap_ids.length} swap(s) on startup`,
);
return;
} catch (error) {
logger.debug(
`resume_all_swaps not ready yet, retrying in ${RESUME_ALL_SWAPS_RETRY_INTERVAL}ms: ${error}`,
);
await new Promise((resolve) =>
setTimeout(resolve, RESUME_ALL_SWAPS_RETRY_INTERVAL),
);
}
}
}

function setIntervalImmediate(callback: () => void, interval: number): void {
callback();
setInterval(callback, interval);
Expand Down Expand Up @@ -89,6 +112,9 @@ export async function setupBackgroundTasks(): Promise<void> {
// Fetch all alerts
updateAlerts();

// Kick off resume-all-swaps; it will retry itself until the context is ready
resumeAllSwapsUntilSuccess();

// Setup Tauri event listeners
// Check if the context is already available. This is to prevent unnecessary re-initialization
setIntervalImmediate(async () => {
Expand Down
10 changes: 9 additions & 1 deletion src-gui/src/renderer/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,19 @@ function InnerContent() {
</ErrorBoundary>
}
/>
<Route
path="/offers"
element={
<ErrorBoundary>
<SwapPage mode="offers" />
</ErrorBoundary>
}
/>
<Route
path="/swap"
element={
<ErrorBoundary>
<SwapPage />
<SwapPage mode="swaps" />
</ErrorBoundary>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TimelockTimeline } from "./TimelockTimeline";
import { useIsSpecificSwapRunning, useAppSelector } from "store/hooks";
import { selectSwapTimelock } from "store/selectors";
import { ExpiredTimelocks } from "models/tauriModel";
import { swapIdColor } from "utils/swapColor";

/**
* Component for displaying a list of messages.
Expand Down Expand Up @@ -123,15 +124,19 @@ function BitcoinLockedNoTimelockExpiredStateAlert({
function BitcoinPossiblyCancelledAlert({
swap,
timelock,
isRunning,
}: {
swap: GetSwapInfoResponseExt;
timelock: TimelockCancel;
isRunning: boolean;
}) {
return (
<MessageList
messages={[
"The swap is being cancelled because it was not completed in time",
"To refund your Bitcoin, resume the swap",
isRunning
? "We will refund the Bitcoin automatically"
: "To refund your Bitcoin, resume the swap",
<>
If we haven't refunded in{" "}
<HumanizedBitcoinBlockDuration
Expand All @@ -149,13 +154,13 @@ function BitcoinPossiblyCancelledAlert({
* Sub-component for displaying alerts requiring immediate action.
* @returns JSX.Element
*/
function PunishTimelockExpiredAlert() {
function PunishTimelockExpiredAlert({ isRunning }: { isRunning: boolean }) {
return (
<MessageList
messages={[
"We couldn't refund within the refund period",
"We might still be able to redeem the Monero. However, this will require cooperation from the other party",
"Resume the swap as soon as possible",
isRunning ? null : "Resume the swap as soon as possible",
]}
/>
);
Expand All @@ -167,8 +172,10 @@ function PunishTimelockExpiredAlert() {
*/
function WaitingForRemainingRefundTimelockAlert({
blocksLeft,
isRunning,
}: {
blocksLeft: number;
isRunning: boolean;
}) {
return (
<MessageList
Expand All @@ -181,7 +188,9 @@ function WaitingForRemainingRefundTimelockAlert({
</>,
"The maker can withhold the Bitcoin anti-spam deposit before the timelock expires",
"We will refund the Bitcoin anti-spam deposit once the timelock expires",
"Keep the app running or resume the swap once the timelock expires",
isRunning
? null
: "Keep the app running or resume the swap once the timelock expires",
]}
/>
);
Expand Down Expand Up @@ -290,10 +299,14 @@ export function StateAlert({
);
case "Cancel":
return (
<BitcoinPossiblyCancelledAlert timelock={timelock} swap={swap} />
<BitcoinPossiblyCancelledAlert
timelock={timelock}
swap={swap}
isRunning={isRunning}
/>
);
case "Punish":
return <PunishTimelockExpiredAlert />;
return <PunishTimelockExpiredAlert isRunning={isRunning} />;
// These two timelock types only exist once the partial refund tx has been confirmed
// They shouldn't occur for these states, so return null
case "WaitingForRemainingRefund":
Expand All @@ -303,7 +316,7 @@ export function StateAlert({
exhaustiveGuard(timelock);
}
}
return <PunishTimelockExpiredAlert />;
return <PunishTimelockExpiredAlert isRunning={isRunning} />;

case BobStateName.BtcPartiallyRefunded:
// Reuse existing timelock alerts for the amnesty waiting period
Expand All @@ -313,6 +326,7 @@ export function StateAlert({
return (
<WaitingForRemainingRefundTimelockAlert
blocksLeft={timelock.content.blocks_left}
isRunning={isRunning}
/>
);
case "RemainingRefund":
Expand Down Expand Up @@ -403,18 +417,26 @@ export default function SwapStatusAlert({
},
}}
>
<AlertTitle>
{isRunning ? (
hasUnusualAmountOfTimePassed ? (
"Swap has been running for a while"
) : (
"Swap is running"
)
) : (
<>
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running
</>
)}
<AlertTitle
sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}
>
<Box
component="span"
sx={{
fontFamily: "monospace",
borderBottom: `2px solid ${swapIdColor(swap.swap_id)}`,
paddingBottom: "2px",
}}
>
<TruncatedText>{swap.swap_id}</TruncatedText>
</Box>
<span>
{isRunning
? hasUnusualAmountOfTimePassed
? "has been running for a while"
: "is running"
: "is not running"}
</span>
</AlertTitle>
<Box
sx={{
Expand Down
5 changes: 2 additions & 3 deletions src-gui/src/renderer/components/alert/SwapTxLockAlertsBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { useSwapInfosSortedByDate } from "store/hooks";
import SwapStatusAlert from "./SwapStatusAlert/SwapStatusAlert";

export default function SwapTxLockAlertsBox() {
// We specifically choose ALL swaps here
// If a swap is in a state where an Alert is not needed (becaue no Bitcoin have been locked or because the swap has been completed)
// the SwapStatusAlert component will not render an Alert
// We specifically choose ALL swaps here. SwapStatusAlert renders nothing for
// swaps without a relevant timelock alert (no funds locked / already done).
const swaps = useSwapInfosSortedByDate();

return (
Expand Down
5 changes: 3 additions & 2 deletions src-gui/src/renderer/components/modal/SwapSuspendAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ import {
Typography,
} from "@mui/material";
import CircleIcon from "@mui/icons-material/Circle";
import { suspendCurrentSwap } from "renderer/rpc";
import PromiseInvokeButton from "../PromiseInvokeButton";

type SwapCancelAlertProps = {
open: boolean;
onClose: () => void;
onSuspend: () => Promise<void>;
};

export default function SwapSuspendAlert({
open,
onClose,
onSuspend,
}: SwapCancelAlertProps) {
return (
<Dialog open={open} onClose={onClose}>
Expand Down Expand Up @@ -71,7 +72,7 @@ export default function SwapSuspendAlert({
<PromiseInvokeButton
color="primary"
onSuccess={onClose}
onInvoke={suspendCurrentSwap}
onInvoke={onSuspend}
contextRequirement={false}
>
Suspend
Expand Down
8 changes: 2 additions & 6 deletions src-gui/src/renderer/components/modal/feedback/useFeedback.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useState, useEffect } from "react";
import { store } from "renderer/store/storeRenderer";
import { useActiveSwapInfo } from "store/hooks";
import { logsToRawString } from "utils/parseUtils";
import { getLogsOfSwap, redactLogs } from "renderer/rpc";
import { parseCliLogString } from "models/cliModel";
Expand Down Expand Up @@ -40,13 +39,10 @@ const initialLogsState: FeedbackLogsState = {
};

export function useFeedback() {
const currentSwapId = useActiveSwapInfo();
const { enqueueSnackbar } = useSnackbar();

const [inputState, setInputState] = useState<FeedbackInputState>({
...initialInputState,
selectedSwap: currentSwapId?.swap_id || null,
});
const [inputState, setInputState] =
useState<FeedbackInputState>(initialInputState);
const [logsState, setLogsState] =
useState<FeedbackLogsState>(initialLogsState);
const [error, setError] = useState<string | null>(null);
Expand Down
Loading
Loading