mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-02 19:46:13 -04:00
feat(gui): Display timelock status using a timeline (#153)
This commit is contained in:
parent
3e79bb3712
commit
4cf5cf719a
22 changed files with 499 additions and 343 deletions
|
@ -18,7 +18,7 @@
|
|||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-cli": "^2.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||
import {
|
||||
ExpiredTimelocks,
|
||||
GetSwapInfoResponse,
|
||||
|
@ -30,6 +31,26 @@ export enum BobStateName {
|
|||
SafelyAborted = "safely aborted",
|
||||
}
|
||||
|
||||
export function bobStateNameToHumanReadable(stateName: BobStateName): string {
|
||||
switch (stateName) {
|
||||
case BobStateName.Started: return "Started";
|
||||
case BobStateName.SwapSetupCompleted: return "Setup completed";
|
||||
case BobStateName.BtcLocked: return "Bitcoin locked";
|
||||
case BobStateName.XmrLockProofReceived: return "Monero locked";
|
||||
case BobStateName.XmrLocked: return "Monero locked and fully confirmed";
|
||||
case BobStateName.EncSigSent: return "Encrypted signature sent";
|
||||
case BobStateName.BtcRedeemed: return "Bitcoin redeemed";
|
||||
case BobStateName.CancelTimelockExpired: return "Cancel timelock expired";
|
||||
case BobStateName.BtcCancelled: return "Bitcoin cancelled";
|
||||
case BobStateName.BtcRefunded: return "Bitcoin refunded";
|
||||
case BobStateName.XmrRedeemed: return "Monero redeemed";
|
||||
case BobStateName.BtcPunished: return "Bitcoin punished";
|
||||
case BobStateName.SafelyAborted: return "Safely aborted";
|
||||
default:
|
||||
return exhaustiveGuard(stateName);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This is a temporary solution until we have a typeshare definition for BobStateName
|
||||
export type GetSwapInfoResponseExt = GetSwapInfoResponse & {
|
||||
state_name: BobStateName;
|
||||
|
@ -39,6 +60,22 @@ export type TimelockNone = Extract<ExpiredTimelocks, { type: "None" }>;
|
|||
export type TimelockCancel = Extract<ExpiredTimelocks, { type: "Cancel" }>;
|
||||
export type TimelockPunish = Extract<ExpiredTimelocks, { type: "Punish" }>;
|
||||
|
||||
// This function returns the absolute block number of the timelock relative to the block the tx_lock was included in
|
||||
export function getAbsoluteBlock(timelock: ExpiredTimelocks, cancelTimelock: number, punishTimelock: number): number {
|
||||
if (timelock.type === "None") {
|
||||
return cancelTimelock - timelock.content.blocks_left;
|
||||
}
|
||||
if (timelock.type === "Cancel") {
|
||||
return cancelTimelock + punishTimelock - timelock.content.blocks_left;
|
||||
}
|
||||
if (timelock.type === "Punish") {
|
||||
return cancelTimelock + punishTimelock;
|
||||
}
|
||||
|
||||
// We match all cases
|
||||
return exhaustiveGuard(timelock);
|
||||
}
|
||||
|
||||
export type BobStateNameRunningSwap = Exclude<
|
||||
BobStateName,
|
||||
| BobStateName.Started
|
||||
|
@ -50,7 +87,11 @@ export type BobStateNameRunningSwap = Exclude<
|
|||
>;
|
||||
|
||||
export type GetSwapInfoResponseExtRunningSwap = GetSwapInfoResponseExt & {
|
||||
stateName: BobStateNameRunningSwap;
|
||||
state_name: BobStateNameRunningSwap;
|
||||
};
|
||||
|
||||
export type GetSwapInfoResponseExtWithTimelock = GetSwapInfoResponseExt & {
|
||||
timelock: ExpiredTimelocks;
|
||||
};
|
||||
|
||||
export function isBobStateNameRunningSwap(
|
||||
|
@ -157,3 +198,14 @@ export function isGetSwapInfoResponseRunningSwap(
|
|||
): response is GetSwapInfoResponseExtRunningSwap {
|
||||
return isBobStateNameRunningSwap(response.state_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for GetSwapInfoResponseExt to ensure timelock is not null
|
||||
* @param response The swap info response to check
|
||||
* @returns True if the timelock exists, false otherwise
|
||||
*/
|
||||
export function isGetSwapInfoResponseWithTimelock(
|
||||
response: GetSwapInfoResponseExt
|
||||
): response is GetSwapInfoResponseExtWithTimelock {
|
||||
return response.timelock !== null;
|
||||
}
|
||||
|
|
|
@ -5,31 +5,31 @@ import { SatsAmount } from "../other/Units";
|
|||
import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
outer: {
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function RemainingFundsWillBeUsedAlert() {
|
||||
const classes = useStyles();
|
||||
const balance = useAppSelector((s) => s.rpc.state.balance);
|
||||
const classes = useStyles();
|
||||
const balance = useAppSelector((s) => s.rpc.state.balance);
|
||||
|
||||
if (balance == null || balance <= 0) {
|
||||
return <></>;
|
||||
}
|
||||
if (balance == null || balance <= 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={<WalletRefreshButton />}
|
||||
variant="filled"
|
||||
>
|
||||
The remaining funds of <SatsAmount amount={balance} /> in the wallet
|
||||
will be used for the next swap. If the remaining funds exceed the
|
||||
minimum swap amount of the provider, a swap will be initiated
|
||||
instantaneously.
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Alert
|
||||
severity="warning"
|
||||
action={<WalletRefreshButton />}
|
||||
variant="filled"
|
||||
>
|
||||
The remaining funds of <SatsAmount amount={balance} /> in the wallet
|
||||
will be used for the next swap. If the remaining funds exceed the
|
||||
minimum swap amount of the provider, a swap will be initiated
|
||||
instantaneously.
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
import { makeStyles } from "@material-ui/core";
|
||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||
import {
|
||||
isSwapTimelockInfoCancelled,
|
||||
isSwapTimelockInfoNone,
|
||||
} from "models/rpcModel";
|
||||
import { useActiveSwapInfo } from "store/hooks";
|
||||
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
list: {
|
||||
margin: theme.spacing(0.25),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SwapMightBeCancelledAlert({
|
||||
bobBtcLockTxConfirmations,
|
||||
}: {
|
||||
bobBtcLockTxConfirmations: number;
|
||||
}) {
|
||||
// TODO: Reimplement this using Tauri
|
||||
return <></>;
|
||||
|
||||
const classes = useStyles();
|
||||
const swap = useActiveSwapInfo();
|
||||
|
||||
if (
|
||||
bobBtcLockTxConfirmations < 5 ||
|
||||
swap === null ||
|
||||
swap.timelock === null
|
||||
) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const { timelock } = swap;
|
||||
const punishTimelockOffset = swap.punish_timelock;
|
||||
|
||||
return (
|
||||
<Alert severity="warning" className={classes.outer} variant="filled">
|
||||
<AlertTitle>Be careful!</AlertTitle>
|
||||
The swap provider has taken a long time to lock their Monero. This might
|
||||
mean that:
|
||||
<ul className={classes.list}>
|
||||
<li>
|
||||
There is a technical issue that prevents them from locking their funds
|
||||
</li>
|
||||
<li>They are a malicious actor (unlikely)</li>
|
||||
</ul>
|
||||
<br />
|
||||
There is still hope for the swap to be successful but you have to be extra
|
||||
careful. Regardless of why it has taken them so long, it is important that
|
||||
you refund the swap within the required time period if the swap is not
|
||||
completed. If you fail to to do so, you will be punished and lose your
|
||||
money.
|
||||
<ul className={classes.list}>
|
||||
{isSwapTimelockInfoNone(timelock) && (
|
||||
<>
|
||||
<li>
|
||||
<strong>
|
||||
You will be able to refund in about{" "}
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={timelock.None.blocks_left}
|
||||
/>
|
||||
</strong>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<strong>
|
||||
If you have not refunded or completed the swap in about{" "}
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={timelock.None.blocks_left + punishTimelockOffset}
|
||||
/>
|
||||
, you will lose your funds.
|
||||
</strong>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{isSwapTimelockInfoCancelled(timelock) && (
|
||||
<li>
|
||||
<strong>
|
||||
If you have not refunded or completed the swap in about{" "}
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={timelock.Cancel.blocks_left}
|
||||
/>
|
||||
, you will lose your funds.
|
||||
</strong>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
As long as you see this screen, the swap will be refunded
|
||||
automatically when the time comes. If this fails, you have to manually
|
||||
refund by navigating to the History page.
|
||||
</li>
|
||||
</ul>
|
||||
</Alert>
|
||||
);
|
||||
}
|
|
@ -1,72 +1,79 @@
|
|||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import { Alert, AlertTitle } from "@material-ui/lab/";
|
||||
import { GetSwapInfoResponse } from "models/tauriModel";
|
||||
import {
|
||||
BobStateName,
|
||||
GetSwapInfoResponseExt,
|
||||
GetSwapInfoResponseExtRunningSwap,
|
||||
isGetSwapInfoResponseRunningSwap,
|
||||
isGetSwapInfoResponseWithTimelock,
|
||||
TimelockCancel,
|
||||
TimelockNone,
|
||||
} from "models/tauriModelExt";
|
||||
import { ReactNode } from "react";
|
||||
import { exhaustiveGuard } from "utils/typescriptUtils";
|
||||
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
|
||||
import TruncatedText from "../other/TruncatedText";
|
||||
import {
|
||||
SwapCancelRefundButton,
|
||||
SwapResumeButton,
|
||||
} from "../pages/history/table/HistoryRowActions";
|
||||
import { SwapMoneroRecoveryButton } from "../pages/history/table/SwapMoneroRecoveryButton";
|
||||
import HumanizedBitcoinBlockDuration from "../../other/HumanizedBitcoinBlockDuration";
|
||||
import TruncatedText from "../../other/TruncatedText";
|
||||
import { SwapMoneroRecoveryButton } from "../../pages/history/table/SwapMoneroRecoveryButton";
|
||||
import { TimelockTimeline } from "./TimelockTimeline";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
box: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.5rem",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
list: {
|
||||
padding: "0px",
|
||||
margin: "0px",
|
||||
"& li": {
|
||||
marginBottom: theme.spacing(0.5),
|
||||
"&:last-child": {
|
||||
marginBottom: 0
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
alertMessage: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Component for displaying a list of messages.
|
||||
* @param messages - Array of messages to display.
|
||||
* @returns JSX.Element
|
||||
*/
|
||||
const MessageList = ({ messages }: { messages: ReactNode[] }) => {
|
||||
function MessageList({ messages }: { messages: ReactNode[]; }) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<ul className={classes.list}>
|
||||
{messages.map((msg, i) => (
|
||||
{messages.filter(msg => msg != null).map((msg, i) => (
|
||||
<li key={i}>{msg}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-component for displaying alerts when the swap is in a safe state.
|
||||
* @param swap - The swap information.
|
||||
* @returns JSX.Element
|
||||
*/
|
||||
const BitcoinRedeemedStateAlert = ({ swap }: { swap: GetSwapInfoResponse }) => {
|
||||
function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt; }) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Box className={classes.box}>
|
||||
<MessageList
|
||||
messages={[
|
||||
"The Bitcoin has been redeemed by the other party",
|
||||
"There is no risk of losing funds. You can take your time",
|
||||
"The Monero will be automatically redeemed to the address you provided as soon as you resume the swap",
|
||||
"If this step fails, you can manually redeem the funds",
|
||||
]}
|
||||
/>
|
||||
"There is no risk of losing funds. Take as much time as you need",
|
||||
"The Monero will automatically be redeemed to your provided address once you resume the swap",
|
||||
"If this step fails, you can manually redeem your funds",
|
||||
]} />
|
||||
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-component for displaying alerts when the swap is in a state with no timelock info.
|
||||
|
@ -74,30 +81,33 @@ const BitcoinRedeemedStateAlert = ({ swap }: { swap: GetSwapInfoResponse }) => {
|
|||
* @param punishTimelockOffset - The punish timelock offset.
|
||||
* @returns JSX.Element
|
||||
*/
|
||||
const BitcoinLockedNoTimelockExpiredStateAlert = ({
|
||||
timelock,
|
||||
punishTimelockOffset,
|
||||
function BitcoinLockedNoTimelockExpiredStateAlert({
|
||||
timelock, cancelTimelockOffset, punishTimelockOffset, isRunning,
|
||||
}: {
|
||||
timelock: TimelockNone;
|
||||
cancelTimelockOffset: number;
|
||||
punishTimelockOffset: number;
|
||||
}) => (
|
||||
<MessageList
|
||||
messages={[
|
||||
<>
|
||||
Your Bitcoin is locked. If the swap is not completed in approximately{" "}
|
||||
<HumanizedBitcoinBlockDuration blocks={timelock.content.blocks_left} />,
|
||||
you need to refund
|
||||
</>,
|
||||
<>
|
||||
You might lose your funds if you do not refund or complete the swap
|
||||
within{" "}
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={timelock.content.blocks_left + punishTimelockOffset}
|
||||
/>
|
||||
</>,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
isRunning: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MessageList
|
||||
messages={[
|
||||
isRunning ? "We are waiting for the other party to lock their Monero" : null,
|
||||
<>
|
||||
If the swap isn't completed in {" "}
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={timelock.content.blocks_left}
|
||||
displayBlocks={false}
|
||||
/>, it needs to be refunded
|
||||
</>,
|
||||
"For that, you need to have the app open sometime within the refund period",
|
||||
<>
|
||||
After that, cooperation from the other party would be required to recover the funds
|
||||
</>,
|
||||
isRunning ? null : "Please resume the swap to continue"
|
||||
]} />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-component for displaying alerts when the swap timelock is expired
|
||||
|
@ -106,46 +116,49 @@ const BitcoinLockedNoTimelockExpiredStateAlert = ({
|
|||
* @param swap - The swap information.
|
||||
* @returns JSX.Element
|
||||
*/
|
||||
const BitcoinPossiblyCancelledAlert = ({
|
||||
swap,
|
||||
timelock,
|
||||
function BitcoinPossiblyCancelledAlert({
|
||||
swap, timelock,
|
||||
}: {
|
||||
swap: GetSwapInfoResponseExt;
|
||||
timelock: TimelockCancel;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
}) {
|
||||
return (
|
||||
<Box className={classes.box}>
|
||||
<MessageList
|
||||
messages={[
|
||||
"The swap was cancelled because it did not complete in time",
|
||||
"You must resume the swap immediately to refund your Bitcoin",
|
||||
<>
|
||||
You might lose your funds if you do not refund within{" "}
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={timelock.content.blocks_left}
|
||||
/>
|
||||
</>,
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
<MessageList
|
||||
messages={[
|
||||
"The swap is being cancelled because it was not completed in time",
|
||||
"To refund your Bitcoin, resume the swap",
|
||||
<>
|
||||
If we haven't refunded in{" "}
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={timelock.content.blocks_left} />
|
||||
, cooperation from the other party will be required to recover the funds
|
||||
</>
|
||||
]} />
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-component for displaying alerts requiring immediate action.
|
||||
* @returns JSX.Element
|
||||
*/
|
||||
const ImmediateActionAlert = () => (
|
||||
<>Resume the swap immediately to avoid losing your funds</>
|
||||
);
|
||||
function PunishTimelockExpiredAlert() {
|
||||
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",
|
||||
]} />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main component for displaying the appropriate swap alert status text.
|
||||
* @param swap - The swap information.
|
||||
* @returns JSX.Element | null
|
||||
*/
|
||||
function SwapAlertStatusText({ swap }: { swap: GetSwapInfoResponseExt }) {
|
||||
export function StateAlert({ swap, isRunning }: { swap: GetSwapInfoResponseExtRunningSwap; isRunning: boolean; }) {
|
||||
|
||||
switch (swap.state_name) {
|
||||
// This is the state where the swap is safe because the other party has redeemed the Bitcoin
|
||||
// It cannot be punished anymore
|
||||
|
@ -165,11 +178,12 @@ function SwapAlertStatusText({ swap }: { swap: GetSwapInfoResponseExt }) {
|
|||
case "None":
|
||||
return (
|
||||
<BitcoinLockedNoTimelockExpiredStateAlert
|
||||
punishTimelockOffset={swap.punish_timelock}
|
||||
timelock={swap.timelock}
|
||||
cancelTimelockOffset={swap.cancel_timelock}
|
||||
punishTimelockOffset={swap.punish_timelock}
|
||||
isRunning={isRunning}
|
||||
/>
|
||||
);
|
||||
|
||||
case "Cancel":
|
||||
return (
|
||||
<BitcoinPossiblyCancelledAlert
|
||||
|
@ -178,19 +192,17 @@ function SwapAlertStatusText({ swap }: { swap: GetSwapInfoResponseExt }) {
|
|||
/>
|
||||
);
|
||||
case "Punish":
|
||||
return <ImmediateActionAlert />;
|
||||
|
||||
return <PunishTimelockExpiredAlert />;
|
||||
default:
|
||||
// We have covered all possible timelock states above
|
||||
// If we reach this point, it means we have missed a case
|
||||
exhaustiveGuard(swap.timelock);
|
||||
}
|
||||
}
|
||||
return <ImmediateActionAlert />;
|
||||
return <PunishTimelockExpiredAlert />;
|
||||
|
||||
default:
|
||||
// TODO: fix the exhaustive guard
|
||||
// return exhaustiveGuard(swap.state_name);
|
||||
return <></>;
|
||||
exhaustiveGuard(swap.state_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,27 +213,37 @@ function SwapAlertStatusText({ swap }: { swap: GetSwapInfoResponseExt }) {
|
|||
*/
|
||||
export default function SwapStatusAlert({
|
||||
swap,
|
||||
isRunning,
|
||||
}: {
|
||||
swap: GetSwapInfoResponseExt;
|
||||
isRunning: boolean;
|
||||
}): JSX.Element | null {
|
||||
// If the swap is completed, there is no need to display the alert
|
||||
// TODO: Here we should also check if the swap is in a state where any funds can be lost
|
||||
// TODO: If the no Bitcoin have been locked yet, we can safely ignore the swap
|
||||
const classes = useStyles();
|
||||
|
||||
// If the swap is completed, we do not need to display anything
|
||||
if (!isGetSwapInfoResponseRunningSwap(swap)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we don't have a timelock for the swap, we cannot display the alert
|
||||
if (!isGetSwapInfoResponseWithTimelock(swap)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
key={swap.swap_id}
|
||||
severity="warning"
|
||||
action={<SwapResumeButton swap={swap}>Resume Swap</SwapResumeButton>}
|
||||
variant="filled"
|
||||
classes={{ message: classes.alertMessage }}
|
||||
>
|
||||
<AlertTitle>
|
||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is unfinished
|
||||
{isRunning ? "Swap has been running for a while" : <>Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running</>}
|
||||
</AlertTitle>
|
||||
<SwapAlertStatusText swap={swap} />
|
||||
<Box className={classes.box}>
|
||||
<StateAlert swap={swap} isRunning={isRunning} />
|
||||
<TimelockTimeline swap={swap} />
|
||||
</Box>
|
||||
</Alert>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
import { useTheme, Tooltip, Typography, Box, LinearProgress, Paper } from "@material-ui/core";
|
||||
import { ExpiredTimelocks } from "models/tauriModel";
|
||||
import { GetSwapInfoResponseExt, getAbsoluteBlock } from "models/tauriModelExt";
|
||||
import HumanizedBitcoinBlockDuration from "renderer/components/other/HumanizedBitcoinBlockDuration";
|
||||
|
||||
interface TimelineSegment {
|
||||
title: string;
|
||||
label: string;
|
||||
bgcolor: string;
|
||||
startBlock: number;
|
||||
}
|
||||
|
||||
interface TimelineSegmentProps {
|
||||
segment: TimelineSegment;
|
||||
isActive: boolean;
|
||||
absoluteBlock: number;
|
||||
durationOfSegment: number | null;
|
||||
totalBlocks: number;
|
||||
}
|
||||
|
||||
function TimelineSegment({
|
||||
segment,
|
||||
isActive,
|
||||
absoluteBlock,
|
||||
durationOfSegment,
|
||||
totalBlocks
|
||||
}: TimelineSegmentProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Tooltip title={<Typography variant="caption">{segment.title}</Typography>}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bgcolor: segment.bgcolor,
|
||||
width: `${durationOfSegment ? ((durationOfSegment / totalBlocks) * 85) : 15}%`,
|
||||
position: 'relative',
|
||||
}} style={{
|
||||
opacity: isActive ? 1 : 0.3
|
||||
}}>
|
||||
{isActive && (
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
width: `${Math.max(5, ((absoluteBlock - segment.startBlock) / durationOfSegment) * 100)}%`,
|
||||
zIndex: 1,
|
||||
}}>
|
||||
<LinearProgress
|
||||
variant="indeterminate"
|
||||
color="primary"
|
||||
style={{
|
||||
height: '100%',
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Typography variant="subtitle2" color="inherit" align="center" style={{ zIndex: 2 }}>
|
||||
{segment.label}
|
||||
</Typography>
|
||||
{durationOfSegment && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="inherit"
|
||||
align="center"
|
||||
style={{
|
||||
zIndex: 2,
|
||||
opacity: 0.8
|
||||
}}
|
||||
>
|
||||
{isActive && (
|
||||
<>
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={durationOfSegment - (absoluteBlock - segment.startBlock)}
|
||||
/>{" "}left
|
||||
</>
|
||||
)}
|
||||
{!isActive && (
|
||||
<HumanizedBitcoinBlockDuration
|
||||
blocks={durationOfSegment}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function TimelockTimeline({ swap }: {
|
||||
// This forces the timelock to not be null
|
||||
swap: GetSwapInfoResponseExt & { timelock: ExpiredTimelocks }
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
const timelineSegments: TimelineSegment[] = [
|
||||
{
|
||||
title: "Normally a swap is completed during this period",
|
||||
label: "Normal",
|
||||
bgcolor: theme.palette.success.main,
|
||||
startBlock: 0,
|
||||
},
|
||||
{
|
||||
title: "If the swap hasn't been completed before we reach this period, the Bitcoin needs to be refunded. For that, you need to have the app open sometime within the refund period",
|
||||
label: "Refund",
|
||||
bgcolor: theme.palette.warning.main,
|
||||
startBlock: swap.cancel_timelock,
|
||||
},
|
||||
{
|
||||
title: "If you didn't refund within the refund window, you will enter this period. At this point, the Bitcoin can no longer be refunded. It may still be possible to redeem the Monero with cooperation from the other party but this cannot be guaranteed.",
|
||||
label: "Danger",
|
||||
bgcolor: theme.palette.error.main,
|
||||
startBlock: swap.cancel_timelock + swap.punish_timelock,
|
||||
}
|
||||
];
|
||||
|
||||
const totalBlocks = swap.cancel_timelock + swap.punish_timelock;
|
||||
const absoluteBlock = getAbsoluteBlock(swap.timelock, swap.cancel_timelock, swap.punish_timelock);
|
||||
|
||||
// This calculates the duration of a segment
|
||||
// by getting the the difference to the next segment
|
||||
function durationOfSegment(index: number): number | null {
|
||||
const nextSegment = timelineSegments[index + 1];
|
||||
if (nextSegment == null) {
|
||||
return null;
|
||||
}
|
||||
return nextSegment.startBlock - timelineSegments[index].startBlock;
|
||||
}
|
||||
|
||||
// This function returns the index of the active segment based on the current block
|
||||
// We iterate in reverse to find the first segment that has a start block less than the current block
|
||||
function getActiveSegmentIndex() {
|
||||
return Array.from(timelineSegments
|
||||
.slice()
|
||||
// We use .entries() to keep the indexes despite reversing
|
||||
.entries())
|
||||
.reverse()
|
||||
.find(([_, segment]) => absoluteBlock >= segment.startBlock)?.[0] ?? 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
width: '100%',
|
||||
minWidth: '100%',
|
||||
flexGrow: 1
|
||||
}}>
|
||||
<Paper style={{
|
||||
position: 'relative',
|
||||
height: '5rem',
|
||||
overflow: 'hidden',
|
||||
}} elevation={3} variant="outlined">
|
||||
<Box sx={{
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
display: 'flex'
|
||||
}}>
|
||||
{timelineSegments.map((segment, index) => (
|
||||
<TimelineSegment
|
||||
key={index}
|
||||
segment={segment}
|
||||
isActive={getActiveSegmentIndex() === index}
|
||||
absoluteBlock={absoluteBlock}
|
||||
durationOfSegment={durationOfSegment(index)}
|
||||
totalBlocks={totalBlocks}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import { useSwapInfosSortedByDate } from "store/hooks";
|
||||
import SwapStatusAlert from "./SwapStatusAlert";
|
||||
import SwapStatusAlert from "./SwapStatusAlert/SwapStatusAlert";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
|
@ -21,7 +21,7 @@ export default function SwapTxLockAlertsBox() {
|
|||
return (
|
||||
<Box className={classes.outer}>
|
||||
{swaps.map((swap) => (
|
||||
<SwapStatusAlert key={swap.swap_id} swap={swap} />
|
||||
<SwapStatusAlert key={swap.swap_id} swap={swap} isRunning={false} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -1,35 +1,51 @@
|
|||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
import { TauriSwapProgressEventContent } from "models/tauriModelExt";
|
||||
import SwapMightBeCancelledAlert from "../../../../alert/SwapMightBeCancelledAlert";
|
||||
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
|
||||
import SwapStatusAlert from "renderer/components/alert/SwapStatusAlert/SwapStatusAlert";
|
||||
import { useActiveSwapInfo } from "store/hooks";
|
||||
import { Box, DialogContentText } from "@material-ui/core";
|
||||
|
||||
// This is the number of blocks after which we consider the swap to be at risk of being unsuccessful
|
||||
const BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD = 2;
|
||||
|
||||
export default function BitcoinLockTxInMempoolPage({
|
||||
btc_lock_confirmations,
|
||||
btc_lock_txid,
|
||||
}: TauriSwapProgressEventContent<"BtcLockTxInMempool">) {
|
||||
const swapInfo = useActiveSwapInfo();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<SwapMightBeCancelledAlert
|
||||
bobBtcLockTxConfirmations={btc_lock_confirmations}
|
||||
/>
|
||||
<DialogContentText>
|
||||
The Bitcoin lock transaction has been published. The swap will proceed
|
||||
once the transaction is confirmed and the swap provider locks their
|
||||
Monero.
|
||||
</DialogContentText>
|
||||
<BitcoinTransactionInfoBox
|
||||
title="Bitcoin Lock Transaction"
|
||||
txId={btc_lock_txid}
|
||||
loading
|
||||
additionalContent={
|
||||
<>
|
||||
Most swap providers require one confirmation before locking their
|
||||
Monero
|
||||
<br />
|
||||
Confirmations: {btc_lock_confirmations}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{btc_lock_confirmations < BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
||||
<DialogContentText>
|
||||
Your Bitcoin has been locked. {btc_lock_confirmations > 0 ?
|
||||
"We are waiting for the other party to lock their Monero." :
|
||||
"We are waiting for the blockchain to confirm the transaction. Once confirmed, the other party will lock their Monero."
|
||||
}
|
||||
</DialogContentText>
|
||||
)}
|
||||
<Box style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1rem",
|
||||
}}>
|
||||
{btc_lock_confirmations >= BITCOIN_CONFIRMATIONS_WARNING_THRESHOLD && (
|
||||
<SwapStatusAlert swap={swapInfo} isRunning={true} />
|
||||
)}
|
||||
<BitcoinTransactionInfoBox
|
||||
title="Bitcoin Lock Transaction"
|
||||
txId={btc_lock_txid}
|
||||
loading
|
||||
additionalContent={
|
||||
<>
|
||||
Most swap providers require one confirmation before locking their
|
||||
Monero. After they lock their funds and the Monero transaction
|
||||
receives one confirmation, the swap will proceed to the next step.
|
||||
<br />
|
||||
Confirmations: {btc_lock_confirmations}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,12 +8,12 @@ import {
|
|||
} from "@material-ui/core";
|
||||
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||
import { useState } from "react";
|
||||
import RemainingFundsWillBeUsedAlert from "renderer/components/alert/RemainingFundsWillBeUsedAlert";
|
||||
import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField";
|
||||
import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { buyXmr } from "renderer/rpc";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import RemainingFundsWillBeUsedAlert from "../../../../alert/RemainingFundsWillBeUsedAlert";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
initButton: {
|
||||
|
|
|
@ -4,14 +4,16 @@ const AVG_BLOCK_TIME_MS = 10 * 60 * 1000;
|
|||
|
||||
export default function HumanizedBitcoinBlockDuration({
|
||||
blocks,
|
||||
displayBlocks = true,
|
||||
}: {
|
||||
blocks: number;
|
||||
displayBlocks?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{`${humanizeDuration(blocks * AVG_BLOCK_TIME_MS, {
|
||||
{`≈ ${humanizeDuration(blocks * AVG_BLOCK_TIME_MS, {
|
||||
conjunction: " and ",
|
||||
})} (${blocks} blocks)`}
|
||||
})}${displayBlocks ? ` (${blocks} blocks)` : ""}`}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -347,6 +347,37 @@ function NodeTableModal({
|
|||
)
|
||||
}
|
||||
|
||||
// Create a circle SVG with a given color and radius
|
||||
function Circle({ color, radius = 6 }: { color: string, radius?: number }) {
|
||||
return <span>
|
||||
<svg width={radius * 2} height={radius * 2} viewBox={`0 0 ${radius * 2} ${radius * 2}`}>
|
||||
<circle cx={radius} cy={radius} r={radius} fill={color} />
|
||||
</svg>
|
||||
</span>
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a status indicator for a node
|
||||
*/
|
||||
function NodeStatus({ status }: { status: boolean | undefined }) {
|
||||
const theme = useTheme();
|
||||
|
||||
switch (status) {
|
||||
case true:
|
||||
return <Tooltip title={"This node is available and responding to RPC requests"}>
|
||||
<Circle color={theme.palette.success.dark} />
|
||||
</Tooltip>;
|
||||
case false:
|
||||
return <Tooltip title={"This node is not available or not responding to RPC requests"}>
|
||||
<Circle color={theme.palette.error.dark} />
|
||||
</Tooltip>;
|
||||
default:
|
||||
return <Tooltip title={"The status of this node is currently unknown"}>
|
||||
<HourglassEmpty />
|
||||
</Tooltip>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A table that displays the available nodes for a given network and blockchain.
|
||||
* It allows you to add, remove, and move nodes up the list.
|
||||
|
@ -368,31 +399,6 @@ function NodeTable({
|
|||
const nodeStatuses = useNodes((s) => s.nodes);
|
||||
const [newNode, setNewNode] = useState("");
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
|
||||
// Create a circle SVG with a given color and radius
|
||||
const circle = (color: string, radius: number = 6) => <svg width={radius * 2} height={radius * 2} viewBox={`0 0 ${radius * 2} ${radius * 2}`}>
|
||||
<circle cx={radius} cy={radius} r={radius} fill={color} />
|
||||
</svg>;
|
||||
|
||||
// Show a green/red circle or a hourglass icon depending on the status of the node
|
||||
const statusIcon = (node: string) => {
|
||||
switch (nodeStatuses[blockchain][node]) {
|
||||
case true:
|
||||
return <Tooltip title={"This node is available and responding to RPC requests"}>
|
||||
{circle(theme.palette.success.dark)}
|
||||
</Tooltip>;
|
||||
case false:
|
||||
return <Tooltip title={"This node is not available or not responding to RPC requests"}>
|
||||
{circle(theme.palette.error.dark)}
|
||||
</Tooltip>;
|
||||
default:
|
||||
console.log(`Unknown status for node ${node}: ${nodeStatuses[node]}`);
|
||||
return <Tooltip title={"The status of this node is currently unknown"}>
|
||||
<HourglassEmpty />
|
||||
</Tooltip>;
|
||||
}
|
||||
}
|
||||
|
||||
const onAddNewNode = () => {
|
||||
dispatch(addNode({ network, type: blockchain, node: newNode }));
|
||||
|
@ -438,7 +444,9 @@ function NodeTable({
|
|||
<Typography variant="overline">{node}</Typography>
|
||||
</TableCell>
|
||||
{/* Node status icon */}
|
||||
<TableCell align="center" children={statusIcon(node)} />
|
||||
<TableCell align="center">
|
||||
<NodeStatus status={nodeStatuses[blockchain][node]} />
|
||||
</TableCell>
|
||||
{/* Remove and move buttons */}
|
||||
<TableCell>
|
||||
<Box style={{ display: "flex" }}>
|
||||
|
|
|
@ -15,6 +15,7 @@ import TruncatedText from "renderer/components/other/TruncatedText";
|
|||
import { PiconeroAmount, SatsAmount } from "../../../other/Units";
|
||||
import HistoryRowActions from "./HistoryRowActions";
|
||||
import HistoryRowExpanded from "./HistoryRowExpanded";
|
||||
import { bobStateNameToHumanReadable, GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
amountTransferContainer: {
|
||||
|
@ -42,7 +43,7 @@ function AmountTransfer({
|
|||
);
|
||||
}
|
||||
|
||||
export default function HistoryRow(swap: GetSwapInfoResponse) {
|
||||
export default function HistoryRow(swap: GetSwapInfoResponseExt) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
|
@ -62,7 +63,7 @@ export default function HistoryRow(swap: GetSwapInfoResponse) {
|
|||
btcAmount={swap.btc_amount}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{swap.state_name.toString()}</TableCell>
|
||||
<TableCell>{bobStateNameToHumanReadable(swap.state_name)}</TableCell>
|
||||
<TableCell>
|
||||
<HistoryRowActions {...swap} />
|
||||
</TableCell>
|
||||
|
|
|
@ -27,6 +27,11 @@ const useStyles = makeStyles((theme) => ({
|
|||
padding: theme.spacing(1),
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
outerAddressBox: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
actionsOuter: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
|
@ -88,7 +93,7 @@ export default function HistoryRowExpanded({
|
|||
<TableRow>
|
||||
<TableCell>Provider Address</TableCell>
|
||||
<TableCell>
|
||||
<Box>
|
||||
<Box className={classes.outerAddressBox}>
|
||||
{swap.seller.addresses.map((addr) => (
|
||||
<ActionableMonospaceTextBox
|
||||
key={addr}
|
||||
|
@ -122,16 +127,6 @@ export default function HistoryRowExpanded({
|
|||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
{/*
|
||||
// TOOD: reimplement these buttons using Tauri
|
||||
|
||||
<SwapCancelRefundButton swap={swap} variant="contained" size="small" />
|
||||
<SwapMoneroRecoveryButton
|
||||
swap={swap}
|
||||
variant="contained"
|
||||
size="small"
|
||||
/>
|
||||
*/}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -44,8 +44,8 @@ import { MoneroRecoveryResponse } from "models/rpcModel";
|
|||
import { ListSellersResponse } from "../models/tauriModel";
|
||||
import logger from "utils/logger";
|
||||
import { getNetwork, getNetworkName, isTestnet } from "store/config";
|
||||
import { Blockchain, Network } from "store/features/settingsSlice";
|
||||
import { resetStatuses, setPromise, setStatus, setStatuses } from "store/features/nodesSlice";
|
||||
import { Blockchain } from "store/features/settingsSlice";
|
||||
import { setStatus } from "store/features/nodesSlice";
|
||||
|
||||
export async function initEventListeners() {
|
||||
// This operation is in-expensive
|
||||
|
@ -164,14 +164,14 @@ export async function buyXmr(
|
|||
"buy_xmr",
|
||||
bitcoin_change_address == null
|
||||
? {
|
||||
seller: providerToConcatenatedMultiAddr(seller),
|
||||
monero_receive_address,
|
||||
}
|
||||
seller: providerToConcatenatedMultiAddr(seller),
|
||||
monero_receive_address,
|
||||
}
|
||||
: {
|
||||
seller: providerToConcatenatedMultiAddr(seller),
|
||||
monero_receive_address,
|
||||
bitcoin_change_address,
|
||||
},
|
||||
seller: providerToConcatenatedMultiAddr(seller),
|
||||
monero_receive_address,
|
||||
bitcoin_change_address,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -220,11 +220,8 @@ export async function listSellersAtRendezvousPoint(
|
|||
}
|
||||
|
||||
export async function initializeContext() {
|
||||
console.log("Prepare: Initializing context with settings");
|
||||
|
||||
const network = getNetwork();
|
||||
const settings = store.getState().settings;
|
||||
let statuses = store.getState().nodes.nodes;
|
||||
const testnet = isTestnet();
|
||||
|
||||
// Initialize Tauri settings with null values
|
||||
const tauriSettings: TauriSettings = {
|
||||
|
@ -232,29 +229,24 @@ export async function initializeContext() {
|
|||
monero_node_url: null,
|
||||
};
|
||||
|
||||
// Set the first available node, if set
|
||||
if (Object.keys(statuses.bitcoin).length === 0) {
|
||||
await updateAllNodeStatuses();
|
||||
statuses = store.getState().nodes.nodes;
|
||||
// If are missing any statuses, update them
|
||||
if (Object.values(Blockchain).some(blockchain =>
|
||||
Object.values(store.getState().nodes.nodes[blockchain]).length < store.getState().settings.nodes[network][blockchain].length
|
||||
)) {
|
||||
try {
|
||||
console.log("Updating node statuses");
|
||||
await updateAllNodeStatuses();
|
||||
} catch (e) {
|
||||
logger.error(e, "Failed to update node statuses");
|
||||
}
|
||||
}
|
||||
const { bitcoin: bitcoinNodes, monero: moneroNodes } = store.getState().nodes.nodes;
|
||||
|
||||
let firstAvailableElectrumNode = settings.nodes[network][Blockchain.Bitcoin]
|
||||
.find(node => statuses.bitcoin[node] === true);
|
||||
const firstAvailableElectrumNode = Object.keys(bitcoinNodes).find(node => bitcoinNodes[node] === true);
|
||||
const firstAvailableMoneroNode = Object.keys(moneroNodes).find(node => moneroNodes[node] === true);
|
||||
|
||||
if (firstAvailableElectrumNode !== undefined)
|
||||
tauriSettings.electrum_rpc_url = firstAvailableElectrumNode;
|
||||
else
|
||||
logger.info("No custom Electrum node available, falling back to default.");
|
||||
|
||||
let firstAvailableMoneroNode = settings.nodes[network][Blockchain.Monero]
|
||||
.find(node => statuses.monero[node] === true);
|
||||
|
||||
if (firstAvailableMoneroNode !== undefined)
|
||||
tauriSettings.monero_node_url = firstAvailableMoneroNode;
|
||||
else
|
||||
logger.info("No custom Monero node available, falling back to default.");
|
||||
|
||||
const testnet = isTestnet();
|
||||
tauriSettings.electrum_rpc_url = firstAvailableElectrumNode ?? null;
|
||||
tauriSettings.monero_node_url = firstAvailableMoneroNode ?? null;
|
||||
|
||||
console.log("Initializing context with settings", tauriSettings);
|
||||
|
||||
|
@ -269,7 +261,7 @@ export async function getWalletDescriptor() {
|
|||
}
|
||||
|
||||
export async function getMoneroNodeStatus(node: string): Promise<boolean> {
|
||||
const response =await invoke<CheckMoneroNodeArgs, CheckMoneroNodeResponse>("check_monero_node", {
|
||||
const response = await invoke<CheckMoneroNodeArgs, CheckMoneroNodeResponse>("check_monero_node", {
|
||||
url: node,
|
||||
network: getNetworkName(),
|
||||
});
|
||||
|
@ -289,33 +281,27 @@ export async function getNodeStatus(url: string, blockchain: Blockchain): Promis
|
|||
switch (blockchain) {
|
||||
case Blockchain.Monero: return await getMoneroNodeStatus(url);
|
||||
case Blockchain.Bitcoin: return await getElectrumNodeStatus(url);
|
||||
default: throw new Error(`Unknown blockchain: ${blockchain}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateNodeStatus(node: string, blockchain: Blockchain) {
|
||||
const status = await getNodeStatus(node, blockchain);
|
||||
|
||||
store.dispatch(setStatus({ node, status, blockchain }));
|
||||
}
|
||||
|
||||
export async function updateAllNodeStatuses() {
|
||||
const network = getNetwork();
|
||||
const settings = store.getState().settings;
|
||||
|
||||
// We will update the statuses in batches
|
||||
const newStatuses: Record<Blockchain, Record<string, boolean>> = {
|
||||
[Blockchain.Bitcoin]: {},
|
||||
[Blockchain.Monero]: {},
|
||||
};
|
||||
|
||||
// For all nodes, check if they are available and store the new status (in parallel)
|
||||
await Promise.all(
|
||||
Object.values(Blockchain).flatMap(blockchain =>
|
||||
settings.nodes[network][blockchain].map(async node => {
|
||||
const status = await getNodeStatus(node, blockchain);
|
||||
newStatuses[blockchain][node] = status;
|
||||
})
|
||||
settings.nodes[network][blockchain].map(node => updateNodeStatus(node, blockchain))
|
||||
)
|
||||
);
|
||||
|
||||
// When we are done, we update the statuses in the store
|
||||
store.dispatch(setStatuses(newStatuses));
|
||||
}
|
||||
|
||||
export async function getMoneroAddresses(): Promise<GetMoneroAddressesResponse> {
|
||||
return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses");
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ import { persistReducer, persistStore } from "redux-persist";
|
|||
import sessionStorage from "redux-persist/lib/storage/session";
|
||||
import { reducers } from "store/combinedReducer";
|
||||
import { createMainListeners } from "store/middleware/storeListener";
|
||||
import { LazyStore } from "@tauri-apps/plugin-store";
|
||||
import { getNetworkName } from "store/config";
|
||||
import { LazyStore } from "@tauri-apps/plugin-store";
|
||||
|
||||
// Goal: Maintain application state across page reloads while allowing a clean slate on application restart
|
||||
// Settings are persisted across application restarts, while the rest of the state is cleared
|
||||
|
|
|
@ -47,7 +47,7 @@ export function getStubTestnetProvider(): ExtendedProviderStatus | null {
|
|||
export function getNetworkName(): string {
|
||||
if (isTestnet()) {
|
||||
return "Testnet";
|
||||
}else {
|
||||
} else {
|
||||
return "Mainnet";
|
||||
}
|
||||
}
|
|
@ -6,21 +6,18 @@ export interface NodesSlice {
|
|||
}
|
||||
|
||||
function initialState(): NodesSlice {
|
||||
return {
|
||||
nodes: {
|
||||
[Blockchain.Bitcoin]: {},
|
||||
[Blockchain.Monero]: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
nodes: {
|
||||
[Blockchain.Bitcoin]: {},
|
||||
[Blockchain.Monero]: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const nodesSlice = createSlice({
|
||||
name: "nodes",
|
||||
initialState: initialState(),
|
||||
reducers: {
|
||||
setStatuses(slice, action: PayloadAction<Record<Blockchain, Record<string, boolean>>>) {
|
||||
slice.nodes = action.payload;
|
||||
},
|
||||
setStatus(slice, action: PayloadAction<{
|
||||
node: string,
|
||||
status: boolean,
|
||||
|
@ -29,13 +26,13 @@ const nodesSlice = createSlice({
|
|||
slice.nodes[action.payload.blockchain][action.payload.node] = action.payload.status;
|
||||
},
|
||||
resetStatuses(slice) {
|
||||
slice.nodes = {
|
||||
[Blockchain.Bitcoin]: {},
|
||||
[Blockchain.Monero]: {},
|
||||
}
|
||||
slice.nodes = {
|
||||
[Blockchain.Bitcoin]: {},
|
||||
[Blockchain.Monero]: {},
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { setStatus, setStatuses, resetStatuses } = nodesSlice.actions;
|
||||
export const { setStatus, resetStatuses } = nodesSlice.actions;
|
||||
export default nodesSlice.reducer;
|
||||
|
|
|
@ -148,4 +148,5 @@ export const {
|
|||
setFetchFiatPrices,
|
||||
setFiatCurrency,
|
||||
} = alertsSlice.actions;
|
||||
|
||||
export default alertsSlice.reducer;
|
||||
|
|
|
@ -24,14 +24,12 @@ export const swapSlice = createSlice({
|
|||
//
|
||||
// Then we create a new swap state object that stores the current and previous events
|
||||
if (swap.state === null || action.payload.swap_id !== swap.state.swapId) {
|
||||
console.log("Creating new swap state object");
|
||||
swap.state = {
|
||||
curr: action.payload.event,
|
||||
prev: null,
|
||||
swapId: action.payload.swap_id,
|
||||
};
|
||||
} else {
|
||||
console.log("Updating existing swap state object");
|
||||
swap.state.prev = swap.state.curr;
|
||||
swap.state.curr = action.payload.event;
|
||||
}
|
||||
|
|
|
@ -542,10 +542,10 @@
|
|||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@reduxjs/toolkit@^2.2.6":
|
||||
version "2.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.2.6.tgz#4a8356dad9d0c1ab255607a555d492168e0e3bc1"
|
||||
integrity sha512-kH0r495c5z1t0g796eDQAkYbEQ3a1OLYN9o8jQQVZyKyw367pfRGS+qZLkHYvFHiUUdafpoSlQ2QYObIApjPWA==
|
||||
"@reduxjs/toolkit@^2.3.0":
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.3.0.tgz#d00134634d6c1678e8563ac50026e429e3b64420"
|
||||
integrity sha512-WC7Yd6cNGfHx8zf+iu+Q1UPTfEcXhQ+ATi7CV1hlrSAaQBdlPzg7Ww/wJHNQem7qG9rxmWoFCDCPubSvFObGzA==
|
||||
dependencies:
|
||||
immer "^10.0.3"
|
||||
redux "^5.0.1"
|
||||
|
|
|
@ -1327,7 +1327,8 @@ impl CheckMoneroNodeArgs {
|
|||
|
||||
static CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
// This function is called very frequently, so we set the timeout to be short
|
||||
.timeout(Duration::from_secs(5))
|
||||
.https_only(false)
|
||||
.build()
|
||||
.expect("reqwest client to work")
|
||||
|
|
|
@ -343,6 +343,7 @@ impl EventLoop {
|
|||
if self.swarm.behaviour_mut().transfer_proof.send_response(response_channel, ()).is_err() {
|
||||
tracing::warn!("Failed to send acknowledgment to Alice that we have received the transfer proof");
|
||||
} else {
|
||||
tracing::info!("Sent acknowledgment to Alice that we have received the transfer proof");
|
||||
self.pending_transfer_proof = OptionFuture::from(None);
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue