Merge changes from legacy GUI, allow daemon logs to be attached to feedback (#115)

This PR applies all remaining changes from https://github.com/UnstoppableSwap/unstoppableswap-gui/pull/210


- Added checkbox option to attach daemon logs when submitting feedback
- Added "Outdated" chip with warning icon for providers running outdated asb versions
- Updated `BitcoinPunishedPage` to display different messages for BtcPunished and CooperativeRedeemRejected states (including reason for failed cooperative redeem)
- Added "Attempt recovery" button for swaps in BtcPunished state
- Modified `getBitcoinTxExplorerUrl` to use mempool.space instead of blockchair.com
- Added `useResumeableSwapsCountExcludingPunished` hook to count resumable swaps excluding punished ones, use it for the badge and alert
- Updated `sortProviderList` function to filter out incompatible providers before sorting
- Added `TauriSwapProgressEventExt` type to extract specific event types from TauriSwapProgressEvent
This commit is contained in:
binarybaron 2024-10-13 22:04:47 +06:00 committed by GitHub
parent 639f540876
commit 2bffe40a37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 116 additions and 50 deletions

View File

@ -10,6 +10,8 @@ export type TauriSwapProgressEventContent<
T extends TauriSwapProgressEventType,
> = Extract<TauriSwapProgressEvent, { type: T }>["content"];
export type TauriSwapProgressEventExt<T extends TauriSwapProgressEventType> = Extract<TauriSwapProgressEvent, { type: T }>;
// See /swap/src/protocol/bob/state.rs#L57
// TODO: Replace this with a typeshare definition
export enum BobStateName {

View File

@ -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) {

View File

@ -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)}
>
<MenuItem value={0}>Do not attach logs</MenuItem>
<MenuItem value={0}>Do not attach a swap</MenuItem>
{swaps.map((swap) => (
<MenuItem value={swap.swap_id} key={swap.swap_id}>
Swap <TruncatedText>{swap.swap_id}</TruncatedText> 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({
<DialogContentText>
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.
<br />
We appreciate you taking the time to share your thoughts! Every feedback is read by a core developer!
</DialogContentText>
<Box
style={{
@ -124,7 +136,7 @@ export default function FeedbackDialog({
label={
bodyTooLong
? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: "Feedback"
: "Message"
}
multiline
minRows={4}
@ -136,6 +148,18 @@ export default function FeedbackDialog({
selectedSwap={selectedAttachedSwap}
setSelectedSwap={setSelectedAttachedSwap}
/>
<Paper variant="outlined" style={{ padding: "0.5rem" }}>
<FormControlLabel
control={
<Checkbox
color="primary"
checked={attachDaemonLogs}
onChange={(e) => setAttachDaemonLogs(e.target.checked)}
/>
}
label="Attach daemon logs"
/>
</Paper>
</Box>
</DialogContent>
<DialogActions>
@ -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",
});

View File

@ -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 (
<Box className={classes.content}>
@ -70,6 +73,11 @@ export default function ProviderInfo({
<Chip label="Recommended" icon={<VerifiedUser />} color="primary" />
</Tooltip>
)}
{isOutdated && (
<Tooltip title="This provider is running an outdated version of the software. Outdated providers may be unreliable and cause swaps to take longer to complete or fail entirely.">
<Chip label="Outdated" icon={<WarningIcon />} color="primary" />
</Tooltip>
)}
</Box>
</Box>
);

View File

@ -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 <BitcoinRefundedPage {...state.curr.content} />;
case "BtcPunished":
return <BitcoinPunishedPage />;
return <BitcoinPunishedPage state={state.curr} />;
case "AttemptingCooperativeRedeem":
return (
<CircularProgressWithSubtitle description="Attempting to redeem the Monero with the help of the other party" />
@ -62,7 +62,7 @@ export default function SwapStatePage({ state }: { state: SwapState | null }) {
<CircularProgressWithSubtitle description="The other party is cooperating with us to redeem the Monero..." />
);
case "CooperativeRedeemRejected":
return <BitcoinPunishedPage />;
return <BitcoinPunishedPage state={state.curr} />;
case "Released":
return <ProcessExitedPage prevState={state.prev} swapId={state.swapId} />;
default:

View File

@ -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 (
<Box>
<DialogContentText>
Unfortunately, the swap was not successful, and you&apos;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" && (
<>
<br />
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. <br />
</>
)}
</DialogContentText>
<FeedbackInfoBox />
</Box>

View File

@ -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 (

View File

@ -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 }
</PromiseInvokeButton>
);
}
@ -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 (
<Tooltip title="This swap is completed. You have been punished.">
<ErrorIcon style={{ color: red[500] }} />
<Tooltip title="You have been punished. You can attempt to recover the Monero with the help of the other party but that is not guaranteed to work">
<SwapResumeButton swap={swap} size="small">Attempt recovery</SwapResumeButton>
</Tooltip>
);
}
return <SwapResumeButton swap={swap} />;
return <SwapResumeButton swap={swap}>Resume</SwapResumeButton>;
}

View File

@ -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<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = 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) =>

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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;
});
}