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, T extends TauriSwapProgressEventType,
> = Extract<TauriSwapProgressEvent, { type: T }>["content"]; > = Extract<TauriSwapProgressEvent, { type: T }>["content"];
export type TauriSwapProgressEventExt<T extends TauriSwapProgressEventType> = Extract<TauriSwapProgressEvent, { type: T }>;
// See /swap/src/protocol/bob/state.rs#L57 // See /swap/src/protocol/bob/state.rs#L57
// TODO: Replace this with a typeshare definition // TODO: Replace this with a typeshare definition
export enum BobStateName { export enum BobStateName {

View File

@ -1,10 +1,10 @@
import { Button } from "@material-ui/core"; import { Button } from "@material-ui/core";
import Alert from "@material-ui/lab/Alert"; import Alert from "@material-ui/lab/Alert";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useResumeableSwapsCount } from "store/hooks"; import { useResumeableSwapsCountExcludingPunished } from "store/hooks";
export default function UnfinishedSwapsAlert() { export default function UnfinishedSwapsAlert() {
const resumableSwapsCount = useResumeableSwapsCount(); const resumableSwapsCount = useResumeableSwapsCountExcludingPunished();
const navigate = useNavigate(); const navigate = useNavigate();
if (resumableSwapsCount > 0) { if (resumableSwapsCount > 0) {

View File

@ -1,12 +1,16 @@
import { import {
Box, Box,
Button, Button,
Checkbox,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
FormControl,
FormControlLabel,
MenuItem, MenuItem,
Paper,
Select, Select,
TextField, TextField,
} from "@material-ui/core"; } from "@material-ui/core";
@ -21,7 +25,7 @@ import LoadingButton from "../../other/LoadingButton";
import { PiconeroAmount } from "../../other/Units"; import { PiconeroAmount } from "../../other/Units";
import { getLogsOfSwap } from "renderer/rpc"; 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 = ""; let attachedBody = "";
if (swapId !== 0 && typeof swapId === "string") { if (swapId !== 0 && typeof swapId === "string") {
@ -39,6 +43,13 @@ async function submitFeedback(body: string, swapId: string | number) {
.join("\n====\n")}`; .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); await submitFeedbackViaHttp(body, attachedBody);
} }
@ -66,7 +77,7 @@ function SwapSelectDropDown({
variant="outlined" variant="outlined"
onChange={(e) => setSelectedSwap(e.target.value as string)} 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) => ( {swaps.map((swap) => (
<MenuItem value={swap.swap_id} key={swap.swap_id}> <MenuItem value={swap.swap_id} key={swap.swap_id}>
Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{" "} Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{" "}
@ -96,6 +107,7 @@ export default function FeedbackDialog({
const [selectedAttachedSwap, setSelectedAttachedSwap] = useState< const [selectedAttachedSwap, setSelectedAttachedSwap] = useState<
string | number string | number
>(currentSwapId?.swap_id || 0); >(currentSwapId?.swap_id || 0);
const [attachDaemonLogs, setAttachDaemonLogs] = useState(true);
const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH; const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH;
@ -106,9 +118,9 @@ export default function FeedbackDialog({
<DialogContentText> <DialogContentText>
Got something to say? Drop us a message below. If you had an issue 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. 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 It will help us figure out what went wrong.
when you are ready. We appreciate you taking the time to share your <br />
thoughts! We appreciate you taking the time to share your thoughts! Every feedback is read by a core developer!
</DialogContentText> </DialogContentText>
<Box <Box
style={{ style={{
@ -124,7 +136,7 @@ export default function FeedbackDialog({
label={ label={
bodyTooLong bodyTooLong
? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})` ? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: "Feedback" : "Message"
} }
multiline multiline
minRows={4} minRows={4}
@ -136,6 +148,18 @@ export default function FeedbackDialog({
selectedSwap={selectedAttachedSwap} selectedSwap={selectedAttachedSwap}
setSelectedSwap={setSelectedAttachedSwap} 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> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@ -150,7 +174,7 @@ export default function FeedbackDialog({
try { try {
setPending(true); setPending(true);
await submitFeedback(bodyText, selectedAttachedSwap); await submitFeedback(bodyText, selectedAttachedSwap, attachDaemonLogs);
enqueueSnackbar("Feedback submitted successfully!", { enqueueSnackbar("Feedback submitted successfully!", {
variant: "success", variant: "success",
}); });

View File

@ -7,6 +7,8 @@ import {
SatsAmount, SatsAmount,
} from "renderer/components/other/Units"; } from "renderer/components/other/Units";
import { satsToBtc, secondsToDays } from "utils/conversionUtils"; import { satsToBtc, secondsToDays } from "utils/conversionUtils";
import { isProviderOutdated } from 'utils/multiAddrUtils';
import WarningIcon from '@material-ui/icons/Warning';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
content: { content: {
@ -29,6 +31,7 @@ export default function ProviderInfo({
provider: ExtendedProviderStatus; provider: ExtendedProviderStatus;
}) { }) {
const classes = useStyles(); const classes = useStyles();
const isOutdated = isProviderOutdated(provider);
return ( return (
<Box className={classes.content}> <Box className={classes.content}>
@ -70,6 +73,11 @@ export default function ProviderInfo({
<Chip label="Recommended" icon={<VerifiedUser />} color="primary" /> <Chip label="Recommended" icon={<VerifiedUser />} color="primary" />
</Tooltip> </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>
</Box> </Box>
); );

View File

@ -1,5 +1,5 @@
import { Box } from "@material-ui/core"; import { Box } from "@material-ui/core";
import { SwapSlice, SwapState } from "models/storeModel"; import { SwapState } from "models/storeModel";
import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle"; import CircularProgressWithSubtitle from "../CircularProgressWithSubtitle";
import BitcoinPunishedPage from "./done/BitcoinPunishedPage"; import BitcoinPunishedPage from "./done/BitcoinPunishedPage";
import BitcoinRefundedPage from "./done/BitcoinRefundedPage"; import BitcoinRefundedPage from "./done/BitcoinRefundedPage";
@ -52,7 +52,7 @@ export default function SwapStatePage({ state }: { state: SwapState | null }) {
case "BtcRefunded": case "BtcRefunded":
return <BitcoinRefundedPage {...state.curr.content} />; return <BitcoinRefundedPage {...state.curr.content} />;
case "BtcPunished": case "BtcPunished":
return <BitcoinPunishedPage />; return <BitcoinPunishedPage state={state.curr} />;
case "AttemptingCooperativeRedeem": case "AttemptingCooperativeRedeem":
return ( return (
<CircularProgressWithSubtitle description="Attempting to redeem the Monero with the help of the other party" /> <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..." /> <CircularProgressWithSubtitle description="The other party is cooperating with us to redeem the Monero..." />
); );
case "CooperativeRedeemRejected": case "CooperativeRedeemRejected":
return <BitcoinPunishedPage />; return <BitcoinPunishedPage state={state.curr} />;
case "Released": case "Released":
return <ProcessExitedPage prevState={state.prev} swapId={state.swapId} />; return <ProcessExitedPage prevState={state.prev} swapId={state.swapId} />;
default: default:

View File

@ -1,13 +1,27 @@
import { Box, DialogContentText } from "@material-ui/core"; import { Box, DialogContentText } from '@material-ui/core';
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox"; 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 ( return (
<Box> <Box>
<DialogContentText> <DialogContentText>
Unfortunately, the swap was not successful, and you&apos;ve incurred a Unfortunately, the swap was unsuccessful. Since you did not refund in
penalty because the swap was not refunded in time. Both the Bitcoin and time, the Bitcoin has been lost. However, with the cooperation of the
Monero are irretrievable. 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> </DialogContentText>
<FeedbackInfoBox /> <FeedbackInfoBox />
</Box> </Box>

View File

@ -1,12 +1,12 @@
import { Badge } from "@material-ui/core"; import { Badge } from "@material-ui/core";
import { useResumeableSwapsCount } from "store/hooks"; import { useResumeableSwapsCountExcludingPunished } from "store/hooks";
export default function UnfinishedSwapsBadge({ export default function UnfinishedSwapsBadge({
children, children,
}: { }: {
children: JSX.Element; children: JSX.Element;
}) { }) {
const resumableSwapsCount = useResumeableSwapsCount(); const resumableSwapsCount = useResumeableSwapsCountExcludingPunished();
if (resumableSwapsCount > 0) { if (resumableSwapsCount > 0) {
return ( return (

View File

@ -16,6 +16,7 @@ import { resumeSwap } from "renderer/rpc";
export function SwapResumeButton({ export function SwapResumeButton({
swap, swap,
children,
...props ...props
}: ButtonProps & { swap: GetSwapInfoResponse }) { }: ButtonProps & { swap: GetSwapInfoResponse }) {
return ( return (
@ -27,7 +28,7 @@ export function SwapResumeButton({
onInvoke={() => resumeSwap(swap.swap_id)} onInvoke={() => resumeSwap(swap.swap_id)}
{...props} {...props}
> >
Resume { children }
</PromiseInvokeButton> </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) { if (swap.state_name === BobStateName.BtcPunished) {
return ( return (
<Tooltip title="This swap is completed. You have been punished."> <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">
<ErrorIcon style={{ color: red[500] }} /> <SwapResumeButton swap={swap} size="small">Attempt recovery</SwapResumeButton>
</Tooltip> </Tooltip>
); );
} }
return <SwapResumeButton swap={swap} />; return <SwapResumeButton swap={swap}>Resume</SwapResumeButton>;
} }

View File

@ -1,5 +1,5 @@
import { sortBy } from "lodash"; import { sortBy } from "lodash";
import { GetSwapInfoResponseExt } from "models/tauriModelExt"; import { BobStateName, GetSwapInfoResponseExt } from "models/tauriModelExt";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from "renderer/store/storeRenderer"; import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
import { parseDateString } from "utils/parseUtils"; import { parseDateString } from "utils/parseUtils";
@ -10,15 +10,25 @@ import { TauriSettings } from "models/tauriModel";
export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
export function useResumeableSwapsCount() { export function useResumeableSwapsCount(
additionalFilter?: (s: GetSwapInfoResponseExt) => boolean,
) {
return useAppSelector( return useAppSelector(
(state) => (state) =>
Object.values(state.rpc.state.swapInfos).filter( Object.values(state.rpc.state.swapInfos).filter(
(swapInfo) => !swapInfo.completed, (swapInfo: GetSwapInfoResponseExt) =>
!swapInfo.completed && (additionalFilter == null || additionalFilter(swapInfo))
).length, ).length,
); );
} }
export function useResumeableSwapsCountExcludingPunished() {
return useResumeableSwapsCount(
(s) => s.state_name !== BobStateName.BtcPunished,
);
}
export function useIsSwapRunning() { export function useIsSwapRunning() {
return useAppSelector( return useAppSelector(
(state) => (state) =>

View File

@ -30,9 +30,9 @@ export function isBtcAddressValid(address: string, testnet: boolean) {
} }
export function getBitcoinTxExplorerUrl(txid: string, testnet: boolean) { export function getBitcoinTxExplorerUrl(txid: string, testnet: boolean) {
return `https://blockchair.com/bitcoin${ return `https://mempool.space/${
testnet ? "/testnet" : "" testnet ? "/testnet" : ""
}/transaction/${txid}`; }/tx/${txid}`;
} }
export function getMoneroTxExplorerUrl(txid: string, stagenet: boolean) { export function getMoneroTxExplorerUrl(txid: string, stagenet: boolean) {

View File

@ -3,7 +3,7 @@ import { Multiaddr } from "multiaddr";
import semver from "semver"; import semver from "semver";
import { isTestnet } from "store/config"; import { isTestnet } from "store/config";
const MIN_ASB_VERSION = "0.12.0"; const MIN_ASB_VERSION = "0.13.3";
export function providerToConcatenatedMultiAddr(provider: Provider) { export function providerToConcatenatedMultiAddr(provider: Provider) {
return new Multiaddr(provider.multiAddr) return new Multiaddr(provider.multiAddr)
@ -14,11 +14,16 @@ export function providerToConcatenatedMultiAddr(provider: Provider) {
export function isProviderCompatible( export function isProviderCompatible(
provider: ExtendedProviderStatus, provider: ExtendedProviderStatus,
): boolean { ): boolean {
if (provider.version) { return provider.testnet === isTestnet();
if (!semver.satisfies(provider.version, `>=${MIN_ASB_VERSION}`)) }
export function isProviderOutdated(provider: ExtendedProviderStatus): boolean {
if (provider.version != null) {
if (semver.satisfies(provider.version, `>=${MIN_ASB_VERSION}`))
return false;
} else {
return false; return false;
} }
if (provider.testnet !== isTestnet()) return false;
return true; return true;
} }

View File

@ -1,7 +1,11 @@
import { ExtendedProviderStatus } from "models/apiModel"; import { ExtendedProviderStatus } from "models/apiModel";
import { isProviderCompatible } from "./multiAddrUtils";
export function sortProviderList(list: ExtendedProviderStatus[]) { export function sortProviderList(list: ExtendedProviderStatus[]) {
return list.concat().sort((firstEl, secondEl) => { return list
.filter(isProviderCompatible)
.concat()
.sort((firstEl, secondEl) => {
// If neither of them have a relevancy score, sort by max swap amount // If neither of them have a relevancy score, sort by max swap amount
if (firstEl.relevancy === undefined && secondEl.relevancy === undefined) { if (firstEl.relevancy === undefined && secondEl.relevancy === undefined) {
if (firstEl.maxSwapAmount > secondEl.maxSwapAmount) { if (firstEl.maxSwapAmount > secondEl.maxSwapAmount) {