mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-04-27 11:19:18 -04:00
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:
parent
639f540876
commit
2bffe40a37
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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",
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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:
|
||||
|
@ -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'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>
|
||||
|
@ -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 (
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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) =>
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user