diff --git a/src-gui/src/models/tauriModelExt.ts b/src-gui/src/models/tauriModelExt.ts index 70cbbd36..de5a7caa 100644 --- a/src-gui/src/models/tauriModelExt.ts +++ b/src-gui/src/models/tauriModelExt.ts @@ -10,6 +10,8 @@ export type TauriSwapProgressEventContent< T extends TauriSwapProgressEventType, > = Extract["content"]; +export type TauriSwapProgressEventExt = Extract; + // See /swap/src/protocol/bob/state.rs#L57 // TODO: Replace this with a typeshare definition export enum BobStateName { diff --git a/src-gui/src/renderer/components/alert/UnfinishedSwapsAlert.tsx b/src-gui/src/renderer/components/alert/UnfinishedSwapsAlert.tsx index 25be5273..2f75c51b 100644 --- a/src-gui/src/renderer/components/alert/UnfinishedSwapsAlert.tsx +++ b/src-gui/src/renderer/components/alert/UnfinishedSwapsAlert.tsx @@ -1,10 +1,10 @@ import { Button } from "@material-ui/core"; import Alert from "@material-ui/lab/Alert"; import { useNavigate } from "react-router-dom"; -import { useResumeableSwapsCount } from "store/hooks"; +import { useResumeableSwapsCountExcludingPunished } from "store/hooks"; export default function UnfinishedSwapsAlert() { - const resumableSwapsCount = useResumeableSwapsCount(); + const resumableSwapsCount = useResumeableSwapsCountExcludingPunished(); const navigate = useNavigate(); if (resumableSwapsCount > 0) { diff --git a/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx b/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx index b4733f17..44135734 100644 --- a/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx +++ b/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx @@ -1,12 +1,16 @@ import { Box, Button, + Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, + FormControl, + FormControlLabel, MenuItem, + Paper, Select, TextField, } from "@material-ui/core"; @@ -21,7 +25,7 @@ import LoadingButton from "../../other/LoadingButton"; import { PiconeroAmount } from "../../other/Units"; import { getLogsOfSwap } from "renderer/rpc"; -async function submitFeedback(body: string, swapId: string | number) { +async function submitFeedback(body: string, swapId: string | number, submitDaemonLogs: boolean) { let attachedBody = ""; if (swapId !== 0 && typeof swapId === "string") { @@ -39,6 +43,13 @@ async function submitFeedback(body: string, swapId: string | number) { .join("\n====\n")}`; } + if (submitDaemonLogs) { + const logs = store.getState().rpc?.logs ?? []; + attachedBody += `\n\nDaemon Logs: ${logs + .map((l) => JSON.stringify(l)) + .join("\n====\n")}`; + } + await submitFeedbackViaHttp(body, attachedBody); } @@ -66,7 +77,7 @@ function SwapSelectDropDown({ variant="outlined" onChange={(e) => setSelectedSwap(e.target.value as string)} > - Do not attach logs + Do not attach a swap {swaps.map((swap) => ( Swap {swap.swap_id} from{" "} @@ -96,6 +107,7 @@ export default function FeedbackDialog({ const [selectedAttachedSwap, setSelectedAttachedSwap] = useState< string | number >(currentSwapId?.swap_id || 0); + const [attachDaemonLogs, setAttachDaemonLogs] = useState(true); const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH; @@ -106,9 +118,9 @@ export default function FeedbackDialog({ Got something to say? Drop us a message below. If you had an issue with a specific swap, select it from the dropdown to attach the logs. - It will help us figure out what went wrong. Hit that submit button - when you are ready. We appreciate you taking the time to share your - thoughts! + It will help us figure out what went wrong. +
+ We appreciate you taking the time to share your thoughts! Every feedback is read by a core developer!
+ + setAttachDaemonLogs(e.target.checked)} + /> + } + label="Attach daemon logs" + /> + @@ -150,7 +174,7 @@ export default function FeedbackDialog({ try { setPending(true); - await submitFeedback(bodyText, selectedAttachedSwap); + await submitFeedback(bodyText, selectedAttachedSwap, attachDaemonLogs); enqueueSnackbar("Feedback submitted successfully!", { variant: "success", }); diff --git a/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx b/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx index 85538b82..e9bb8103 100644 --- a/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx +++ b/src-gui/src/renderer/components/modal/provider/ProviderInfo.tsx @@ -7,6 +7,8 @@ import { SatsAmount, } from "renderer/components/other/Units"; import { satsToBtc, secondsToDays } from "utils/conversionUtils"; +import { isProviderOutdated } from 'utils/multiAddrUtils'; +import WarningIcon from '@material-ui/icons/Warning'; const useStyles = makeStyles((theme) => ({ content: { @@ -29,6 +31,7 @@ export default function ProviderInfo({ provider: ExtendedProviderStatus; }) { const classes = useStyles(); + const isOutdated = isProviderOutdated(provider); return ( @@ -70,6 +73,11 @@ export default function ProviderInfo({ } color="primary" /> )} + {isOutdated && ( + + } color="primary" /> + + )} ); diff --git a/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx b/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx index f0253aba..c954473c 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/SwapStatePage.tsx @@ -1,5 +1,5 @@ import { Box } from "@material-ui/core"; -import { SwapSlice, SwapState } from "models/storeModel"; +import { SwapState } from "models/storeModel"; import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle"; import BitcoinPunishedPage from "./done/BitcoinPunishedPage"; import BitcoinRefundedPage from "./done/BitcoinRefundedPage"; @@ -52,7 +52,7 @@ export default function SwapStatePage({ state }: { state: SwapState | null }) { case "BtcRefunded": return ; case "BtcPunished": - return ; + return ; case "AttemptingCooperativeRedeem": return ( @@ -62,7 +62,7 @@ export default function SwapStatePage({ state }: { state: SwapState | null }) { ); case "CooperativeRedeemRejected": - return ; + return ; case "Released": return ; default: diff --git a/src-gui/src/renderer/components/modal/swap/pages/done/BitcoinPunishedPage.tsx b/src-gui/src/renderer/components/modal/swap/pages/done/BitcoinPunishedPage.tsx index a8507c3e..43eaa9a7 100644 --- a/src-gui/src/renderer/components/modal/swap/pages/done/BitcoinPunishedPage.tsx +++ b/src-gui/src/renderer/components/modal/swap/pages/done/BitcoinPunishedPage.tsx @@ -1,13 +1,27 @@ -import { Box, DialogContentText } from "@material-ui/core"; -import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox"; +import { Box, DialogContentText } from '@material-ui/core'; +import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox'; +import { TauriSwapProgressEventExt } from 'models/tauriModelExt'; -export default function BitcoinPunishedPage() { +export default function BitcoinPunishedPage({ + state, +}: { + state: TauriSwapProgressEventExt<"BtcPunished"> | TauriSwapProgressEventExt<"CooperativeRedeemRejected"> +}) { return ( - Unfortunately, the swap was not successful, and you've incurred a - penalty because the swap was not refunded in time. Both the Bitcoin and - Monero are irretrievable. + Unfortunately, the swap was unsuccessful. Since you did not refund in + time, the Bitcoin has been lost. However, with the cooperation of the + other party, you might still be able to redeem the Monero, although this + is not guaranteed.{' '} + {state.type === "CooperativeRedeemRejected" && ( + <> +
+ We tried to redeem the Monero with the other party's help, but it + was unsuccessful (reason: {state.content.reason}). Attempting again at a + later time might yield success.
+ + )}
diff --git a/src-gui/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx b/src-gui/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx index 973b123c..aec699fc 100644 --- a/src-gui/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx +++ b/src-gui/src/renderer/components/navigation/UnfinishedSwapsCountBadge.tsx @@ -1,12 +1,12 @@ import { Badge } from "@material-ui/core"; -import { useResumeableSwapsCount } from "store/hooks"; +import { useResumeableSwapsCountExcludingPunished } from "store/hooks"; export default function UnfinishedSwapsBadge({ children, }: { children: JSX.Element; }) { - const resumableSwapsCount = useResumeableSwapsCount(); + const resumableSwapsCount = useResumeableSwapsCountExcludingPunished(); if (resumableSwapsCount > 0) { return ( diff --git a/src-gui/src/renderer/components/pages/history/table/HistoryRowActions.tsx b/src-gui/src/renderer/components/pages/history/table/HistoryRowActions.tsx index 2ae0cab0..7a7b3517 100644 --- a/src-gui/src/renderer/components/pages/history/table/HistoryRowActions.tsx +++ b/src-gui/src/renderer/components/pages/history/table/HistoryRowActions.tsx @@ -16,6 +16,7 @@ import { resumeSwap } from "renderer/rpc"; export function SwapResumeButton({ swap, + children, ...props }: ButtonProps & { swap: GetSwapInfoResponse }) { return ( @@ -27,7 +28,7 @@ export function SwapResumeButton({ onInvoke={() => resumeSwap(swap.swap_id)} {...props} > - Resume + { children } ); } @@ -75,15 +76,13 @@ export default function HistoryRowActions(swap: GetSwapInfoResponse) { ); } - // TODO: Display a button here to attempt a cooperative redeem - // See this PR: https://github.com/UnstoppableSwap/unstoppableswap-gui/pull/212 if (swap.state_name === BobStateName.BtcPunished) { return ( - - + + Attempt recovery ); } - return ; + return Resume; } diff --git a/src-gui/src/store/hooks.ts b/src-gui/src/store/hooks.ts index 66268f3a..bd77592c 100644 --- a/src-gui/src/store/hooks.ts +++ b/src-gui/src/store/hooks.ts @@ -1,5 +1,5 @@ import { sortBy } from "lodash"; -import { GetSwapInfoResponseExt } from "models/tauriModelExt"; +import { BobStateName, GetSwapInfoResponseExt } from "models/tauriModelExt"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import type { AppDispatch, RootState } from "renderer/store/storeRenderer"; import { parseDateString } from "utils/parseUtils"; @@ -10,15 +10,25 @@ import { TauriSettings } from "models/tauriModel"; export const useAppDispatch = () => useDispatch(); export const useAppSelector: TypedUseSelectorHook = useSelector; -export function useResumeableSwapsCount() { +export function useResumeableSwapsCount( + additionalFilter?: (s: GetSwapInfoResponseExt) => boolean, +) { return useAppSelector( (state) => Object.values(state.rpc.state.swapInfos).filter( - (swapInfo) => !swapInfo.completed, + (swapInfo: GetSwapInfoResponseExt) => + !swapInfo.completed && (additionalFilter == null || additionalFilter(swapInfo)) ).length, ); } + +export function useResumeableSwapsCountExcludingPunished() { + return useResumeableSwapsCount( + (s) => s.state_name !== BobStateName.BtcPunished, + ); +} + export function useIsSwapRunning() { return useAppSelector( (state) => diff --git a/src-gui/src/utils/conversionUtils.ts b/src-gui/src/utils/conversionUtils.ts index 82f984ae..776a1a3a 100644 --- a/src-gui/src/utils/conversionUtils.ts +++ b/src-gui/src/utils/conversionUtils.ts @@ -30,9 +30,9 @@ export function isBtcAddressValid(address: string, testnet: boolean) { } export function getBitcoinTxExplorerUrl(txid: string, testnet: boolean) { - return `https://blockchair.com/bitcoin${ + return `https://mempool.space/${ testnet ? "/testnet" : "" - }/transaction/${txid}`; + }/tx/${txid}`; } export function getMoneroTxExplorerUrl(txid: string, stagenet: boolean) { diff --git a/src-gui/src/utils/multiAddrUtils.ts b/src-gui/src/utils/multiAddrUtils.ts index 119969f2..297c1087 100644 --- a/src-gui/src/utils/multiAddrUtils.ts +++ b/src-gui/src/utils/multiAddrUtils.ts @@ -3,7 +3,7 @@ import { Multiaddr } from "multiaddr"; import semver from "semver"; import { isTestnet } from "store/config"; -const MIN_ASB_VERSION = "0.12.0"; +const MIN_ASB_VERSION = "0.13.3"; export function providerToConcatenatedMultiAddr(provider: Provider) { return new Multiaddr(provider.multiAddr) @@ -14,11 +14,16 @@ export function providerToConcatenatedMultiAddr(provider: Provider) { export function isProviderCompatible( provider: ExtendedProviderStatus, ): boolean { - if (provider.version) { - if (!semver.satisfies(provider.version, `>=${MIN_ASB_VERSION}`)) + return provider.testnet === isTestnet(); +} + +export function isProviderOutdated(provider: ExtendedProviderStatus): boolean { + if (provider.version != null) { + if (semver.satisfies(provider.version, `>=${MIN_ASB_VERSION}`)) return false; + } else { + return false; } - if (provider.testnet !== isTestnet()) return false; return true; -} +} \ No newline at end of file diff --git a/src-gui/src/utils/sortUtils.ts b/src-gui/src/utils/sortUtils.ts index 186ecae3..0df96046 100644 --- a/src-gui/src/utils/sortUtils.ts +++ b/src-gui/src/utils/sortUtils.ts @@ -1,19 +1,23 @@ import { ExtendedProviderStatus } from "models/apiModel"; +import { isProviderCompatible } from "./multiAddrUtils"; export function sortProviderList(list: ExtendedProviderStatus[]) { - return list.concat().sort((firstEl, secondEl) => { - // If neither of them have a relevancy score, sort by max swap amount - if (firstEl.relevancy === undefined && secondEl.relevancy === undefined) { - if (firstEl.maxSwapAmount > secondEl.maxSwapAmount) { + return list + .filter(isProviderCompatible) + .concat() + .sort((firstEl, secondEl) => { + // If neither of them have a relevancy score, sort by max swap amount + if (firstEl.relevancy === undefined && secondEl.relevancy === undefined) { + if (firstEl.maxSwapAmount > secondEl.maxSwapAmount) { + return -1; + } + } + // If only on of the two don't have a relevancy score, prioritize the one that does + if (firstEl.relevancy === undefined) return 1; + if (secondEl.relevancy === undefined) return -1; + if (firstEl.relevancy > secondEl.relevancy) { return -1; } - } - // If only on of the two don't have a relevancy score, prioritize the one that does - if (firstEl.relevancy === undefined) return 1; - if (secondEl.relevancy === undefined) return -1; - if (firstEl.relevancy > secondEl.relevancy) { - return -1; - } - return 1; - }); -} + return 1; + }); +} \ No newline at end of file