feat: swap history tauri connector

This commit is contained in:
binarybaron 2024-08-08 12:02:59 +02:00
parent cdd6635c8f
commit 2e1b6f6b43
No known key found for this signature in database
GPG key ID: 99B75D3E1476A26E
22 changed files with 1315 additions and 1297 deletions

View file

@ -1,166 +1,169 @@
import { Step, StepLabel, Stepper, Typography } from '@material-ui/core';
import { SwapSpawnType } from 'models/cliModel';
import { SwapStateName } from 'models/rpcModel';
import { useActiveSwapInfo, useAppSelector } from 'store/hooks';
import { exhaustiveGuard } from 'utils/typescriptUtils';
import { Step, StepLabel, Stepper, Typography } from "@material-ui/core";
import { SwapSpawnType } from "models/cliModel";
import { SwapStateName } from "models/rpcModel";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { exhaustiveGuard } from "utils/typescriptUtils";
export enum PathType {
HAPPY_PATH = 'happy path',
UNHAPPY_PATH = 'unhappy path',
HAPPY_PATH = "happy path",
UNHAPPY_PATH = "unhappy path",
}
function getActiveStep(
stateName: SwapStateName | null,
processExited: boolean,
stateName: SwapStateName | null,
processExited: boolean,
): [PathType, number, boolean] {
switch (stateName) {
/// // Happy Path
// Step: 0 (Waiting for Bitcoin lock tx to be published)
case null:
return [PathType.HAPPY_PATH, 0, false];
case SwapStateName.Started:
case SwapStateName.SwapSetupCompleted:
return [PathType.HAPPY_PATH, 0, processExited];
switch (stateName) {
/// // Happy Path
// Step: 0 (Waiting for Bitcoin lock tx to be published)
case null:
return [PathType.HAPPY_PATH, 0, false];
case SwapStateName.Started:
case SwapStateName.SwapSetupCompleted:
return [PathType.HAPPY_PATH, 0, processExited];
// Step: 1 (Waiting for Bitcoin Lock confirmation and XMR Lock Publication)
// We have locked the Bitcoin and are waiting for the other party to lock their XMR
case SwapStateName.BtcLocked:
return [PathType.HAPPY_PATH, 1, processExited];
// Step: 1 (Waiting for Bitcoin Lock confirmation and XMR Lock Publication)
// We have locked the Bitcoin and are waiting for the other party to lock their XMR
case SwapStateName.BtcLocked:
return [PathType.HAPPY_PATH, 1, processExited];
// Step: 2 (Waiting for XMR Lock confirmation)
// We have locked the Bitcoin and the other party has locked their XMR
case SwapStateName.XmrLockProofReceived:
return [PathType.HAPPY_PATH, 1, processExited];
// Step: 2 (Waiting for XMR Lock confirmation)
// We have locked the Bitcoin and the other party has locked their XMR
case SwapStateName.XmrLockProofReceived:
return [PathType.HAPPY_PATH, 1, processExited];
// Step: 3 (Sending Encrypted Signature and waiting for Bitcoin Redemption)
// The XMR lock transaction has been confirmed
// We now need to send the encrypted signature to the other party and wait for them to redeem the Bitcoin
case SwapStateName.XmrLocked:
case SwapStateName.EncSigSent:
return [PathType.HAPPY_PATH, 2, processExited];
// Step: 3 (Sending Encrypted Signature and waiting for Bitcoin Redemption)
// The XMR lock transaction has been confirmed
// We now need to send the encrypted signature to the other party and wait for them to redeem the Bitcoin
case SwapStateName.XmrLocked:
case SwapStateName.EncSigSent:
return [PathType.HAPPY_PATH, 2, processExited];
// Step: 4 (Waiting for XMR Redemption)
case SwapStateName.BtcRedeemed:
return [PathType.HAPPY_PATH, 3, processExited];
// Step: 4 (Waiting for XMR Redemption)
case SwapStateName.BtcRedeemed:
return [PathType.HAPPY_PATH, 3, processExited];
// Step: 4 (Completed) (Swap completed, XMR redeemed)
case SwapStateName.XmrRedeemed:
return [PathType.HAPPY_PATH, 4, false];
// Step: 4 (Completed) (Swap completed, XMR redeemed)
case SwapStateName.XmrRedeemed:
return [PathType.HAPPY_PATH, 4, false];
// Edge Case of Happy Path where the swap is safely aborted. We "fail" at the first step.
case SwapStateName.SafelyAborted:
return [PathType.HAPPY_PATH, 0, true];
// Edge Case of Happy Path where the swap is safely aborted. We "fail" at the first step.
case SwapStateName.SafelyAborted:
return [PathType.HAPPY_PATH, 0, true];
// // Unhappy Path
// Step: 1 (Cancelling swap, checking if cancel transaction has been published already by the other party)
case SwapStateName.CancelTimelockExpired:
return [PathType.UNHAPPY_PATH, 0, processExited];
// // Unhappy Path
// Step: 1 (Cancelling swap, checking if cancel transaction has been published already by the other party)
case SwapStateName.CancelTimelockExpired:
return [PathType.UNHAPPY_PATH, 0, processExited];
// Step: 2 (Attempt to publish the Bitcoin refund transaction)
case SwapStateName.BtcCancelled:
return [PathType.UNHAPPY_PATH, 1, processExited];
// Step: 2 (Attempt to publish the Bitcoin refund transaction)
case SwapStateName.BtcCancelled:
return [PathType.UNHAPPY_PATH, 1, processExited];
// Step: 2 (Completed) (Bitcoin refunded)
case SwapStateName.BtcRefunded:
return [PathType.UNHAPPY_PATH, 2, false];
// Step: 2 (Completed) (Bitcoin refunded)
case SwapStateName.BtcRefunded:
return [PathType.UNHAPPY_PATH, 2, false];
// Step: 2 (We failed to publish the Bitcoin refund transaction)
// We failed to publish the Bitcoin refund transaction because the timelock has expired.
// We will be punished. Nothing we can do about it now.
case SwapStateName.BtcPunished:
return [PathType.UNHAPPY_PATH, 1, true];
default:
return exhaustiveGuard(stateName);
}
// Step: 2 (We failed to publish the Bitcoin refund transaction)
// We failed to publish the Bitcoin refund transaction because the timelock has expired.
// We will be punished. Nothing we can do about it now.
case SwapStateName.BtcPunished:
return [PathType.UNHAPPY_PATH, 1, true];
default:
return exhaustiveGuard(stateName);
}
}
function HappyPathStepper({
activeStep,
error,
activeStep,
error,
}: {
activeStep: number;
error: boolean;
activeStep: number;
error: boolean;
}) {
return (
<Stepper activeStep={activeStep}>
<Step key={0}>
<StepLabel
optional={<Typography variant="caption">~12min</Typography>}
error={error && activeStep === 0}
>
Locking your BTC
</StepLabel>
</Step>
<Step key={1}>
<StepLabel
optional={<Typography variant="caption">~18min</Typography>}
error={error && activeStep === 1}
>
They lock their XMR
</StepLabel>
</Step>
<Step key={2}>
<StepLabel
optional={<Typography variant="caption">~2min</Typography>}
error={error && activeStep === 2}
>
They redeem the BTC
</StepLabel>
</Step>
<Step key={3}>
<StepLabel
optional={<Typography variant="caption">~2min</Typography>}
error={error && activeStep === 3}
>
Redeeming your XMR
</StepLabel>
</Step>
</Stepper>
);
return (
<Stepper activeStep={activeStep}>
<Step key={0}>
<StepLabel
optional={<Typography variant="caption">~12min</Typography>}
error={error && activeStep === 0}
>
Locking your BTC
</StepLabel>
</Step>
<Step key={1}>
<StepLabel
optional={<Typography variant="caption">~18min</Typography>}
error={error && activeStep === 1}
>
They lock their XMR
</StepLabel>
</Step>
<Step key={2}>
<StepLabel
optional={<Typography variant="caption">~2min</Typography>}
error={error && activeStep === 2}
>
They redeem the BTC
</StepLabel>
</Step>
<Step key={3}>
<StepLabel
optional={<Typography variant="caption">~2min</Typography>}
error={error && activeStep === 3}
>
Redeeming your XMR
</StepLabel>
</Step>
</Stepper>
);
}
function UnhappyPathStepper({
activeStep,
error,
activeStep,
error,
}: {
activeStep: number;
error: boolean;
activeStep: number;
error: boolean;
}) {
return (
<Stepper activeStep={activeStep}>
<Step key={0}>
<StepLabel
optional={<Typography variant="caption">~20min</Typography>}
error={error && activeStep === 0}
>
Cancelling swap
</StepLabel>
</Step>
<Step key={1}>
<StepLabel
optional={<Typography variant="caption">~20min</Typography>}
error={error && activeStep === 1}
>
Refunding your BTC
</StepLabel>
</Step>
</Stepper>
);
return (
<Stepper activeStep={activeStep}>
<Step key={0}>
<StepLabel
optional={<Typography variant="caption">~20min</Typography>}
error={error && activeStep === 0}
>
Cancelling swap
</StepLabel>
</Step>
<Step key={1}>
<StepLabel
optional={<Typography variant="caption">~20min</Typography>}
error={error && activeStep === 1}
>
Refunding your BTC
</StepLabel>
</Step>
</Stepper>
);
}
export default function SwapStateStepper() {
const currentSwapSpawnType = useAppSelector((s) => s.swap.spawnType);
const stateName = useActiveSwapInfo()?.stateName ?? null;
const processExited = useAppSelector((s) => !s.swap.processRunning);
const [pathType, activeStep, error] = getActiveStep(stateName, processExited);
const currentSwapSpawnType = useAppSelector((s) => s.swap.spawnType);
const stateName = useActiveSwapInfo()?.state_name ?? null;
const processExited = useAppSelector((s) => !s.swap.processRunning);
const [pathType, activeStep, error] = getActiveStep(
stateName,
processExited,
);
// If the current swap is being manually cancelled and refund, we want to show the unhappy path even though the current state is not a "unhappy" state
if (currentSwapSpawnType === SwapSpawnType.CANCEL_REFUND) {
return <UnhappyPathStepper activeStep={0} error={error} />;
}
// If the current swap is being manually cancelled and refund, we want to show the unhappy path even though the current state is not a "unhappy" state
if (currentSwapSpawnType === SwapSpawnType.CANCEL_REFUND) {
return <UnhappyPathStepper activeStep={0} error={error} />;
}
if (pathType === PathType.HAPPY_PATH) {
return <HappyPathStepper activeStep={activeStep} error={error} />;
}
return <UnhappyPathStepper activeStep={activeStep} error={error} />;
if (pathType === PathType.HAPPY_PATH) {
return <HappyPathStepper activeStep={activeStep} error={error} />;
}
return <UnhappyPathStepper activeStep={activeStep} error={error} />;
}

View file

@ -1,43 +1,44 @@
import { Box, DialogContentText } from '@material-ui/core';
import { SwapStateBtcRefunded } from 'models/storeModel';
import { useActiveSwapInfo } from 'store/hooks';
import BitcoinTransactionInfoBox from '../../BitcoinTransactionInfoBox';
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox';
import { Box, DialogContentText } from "@material-ui/core";
import { SwapStateBtcRefunded } from "models/storeModel";
import { useActiveSwapInfo } from "store/hooks";
import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
export default function BitcoinRefundedPage({
state,
state,
}: {
state: SwapStateBtcRefunded | null;
state: SwapStateBtcRefunded | null;
}) {
const swap = useActiveSwapInfo();
const additionalContent = swap
? `Refund address: ${swap.btcRefundAddress}`
: null;
const swap = useActiveSwapInfo();
const additionalContent = swap
? `Refund address: ${swap.btc_refund_address}`
: null;
return (
<Box>
<DialogContentText>
Unfortunately, the swap was not successful. However, rest assured that
all your Bitcoin has been refunded to the specified address. The swap
process is now complete, and you are free to exit the application.
</DialogContentText>
<Box
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
}}
>
{state && (
<BitcoinTransactionInfoBox
title="Bitcoin Refund Transaction"
txId={state.bobBtcRefundTxId}
loading={false}
additionalContent={additionalContent}
/>
)}
<FeedbackInfoBox />
</Box>
</Box>
);
return (
<Box>
<DialogContentText>
Unfortunately, the swap was not successful. However, rest
assured that all your Bitcoin has been refunded to the specified
address. The swap process is now complete, and you are free to
exit the application.
</DialogContentText>
<Box
style={{
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
>
{state && (
<BitcoinTransactionInfoBox
title="Bitcoin Refund Transaction"
txId={state.bobBtcRefundTxId}
loading={false}
additionalContent={additionalContent}
/>
)}
<FeedbackInfoBox />
</Box>
</Box>
);
}

View file

@ -1,71 +1,71 @@
import { Box, DialogContentText } from '@material-ui/core';
import { useActiveSwapInfo, useAppSelector } from 'store/hooks';
import { SwapStateProcessExited } from 'models/storeModel';
import CliLogsBox from '../../../../other/RenderedCliLog';
import { SwapSpawnType } from 'models/cliModel';
import { Box, DialogContentText } from "@material-ui/core";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { SwapStateProcessExited } from "models/storeModel";
import CliLogsBox from "../../../../other/RenderedCliLog";
import { SwapSpawnType } from "models/cliModel";
export default function ProcessExitedAndNotDonePage({
state,
state,
}: {
state: SwapStateProcessExited;
state: SwapStateProcessExited;
}) {
const swap = useActiveSwapInfo();
const logs = useAppSelector((s) => s.swap.logs);
const spawnType = useAppSelector((s) => s.swap.spawnType);
const swap = useActiveSwapInfo();
const logs = useAppSelector((s) => s.swap.logs);
const spawnType = useAppSelector((s) => s.swap.spawnType);
function getText() {
const isCancelRefund = spawnType === SwapSpawnType.CANCEL_REFUND;
const hasRpcError = state.rpcError != null;
const hasSwap = swap != null;
function getText() {
const isCancelRefund = spawnType === SwapSpawnType.CANCEL_REFUND;
const hasRpcError = state.rpcError != null;
const hasSwap = swap != null;
let messages = [];
let messages = [];
messages.push(
isCancelRefund
? 'The manual cancel and refund was unsuccessful.'
: 'The swap exited unexpectedly without completing.',
);
if (!hasSwap && !isCancelRefund) {
messages.push('No funds were locked.');
}
messages.push(
hasRpcError
? 'Check the error and the logs below for more information.'
: 'Check the logs below for more information.',
);
if (hasSwap) {
messages.push(`The swap is in the "${swap.stateName}" state.`);
if (!isCancelRefund) {
messages.push(
'Try resuming the swap or attempt to initiate a manual cancel and refund.',
isCancelRefund
? "The manual cancel and refund was unsuccessful."
: "The swap exited unexpectedly without completing.",
);
}
if (!hasSwap && !isCancelRefund) {
messages.push("No funds were locked.");
}
messages.push(
hasRpcError
? "Check the error and the logs below for more information."
: "Check the logs below for more information.",
);
if (hasSwap) {
messages.push(`The swap is in the "${swap.state_name}" state.`);
if (!isCancelRefund) {
messages.push(
"Try resuming the swap or attempt to initiate a manual cancel and refund.",
);
}
}
return messages.join(" ");
}
return messages.join(' ');
}
return (
<Box>
<DialogContentText>{getText()}</DialogContentText>
<Box
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
}}
>
{state.rpcError && (
<CliLogsBox
logs={[state.rpcError]}
label="Error returned by the Swap Daemon"
/>
)}
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
</Box>
</Box>
);
return (
<Box>
<DialogContentText>{getText()}</DialogContentText>
<Box
style={{
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
>
{state.rpcError && (
<CliLogsBox
logs={[state.rpcError]}
label="Error returned by the Swap Daemon"
/>
)}
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
</Box>
</Box>
);
}

View file

@ -1,47 +1,47 @@
import { useActiveSwapInfo } from 'store/hooks';
import { SwapStateName } from 'models/rpcModel';
import { useActiveSwapInfo } from "store/hooks";
import { SwapStateName } from "models/rpcModel";
import {
isSwapStateBtcPunished,
isSwapStateBtcRefunded,
isSwapStateXmrRedeemInMempool,
SwapStateProcessExited,
} from '../../../../../../models/storeModel';
import XmrRedeemInMempoolPage from '../done/XmrRedeemInMempoolPage';
import BitcoinPunishedPage from '../done/BitcoinPunishedPage';
isSwapStateBtcPunished,
isSwapStateBtcRefunded,
isSwapStateXmrRedeemInMempool,
SwapStateProcessExited,
} from "../../../../../../models/storeModel";
import XmrRedeemInMempoolPage from "../done/XmrRedeemInMempoolPage";
import BitcoinPunishedPage from "../done/BitcoinPunishedPage";
// eslint-disable-next-line import/no-cycle
import SwapStatePage from '../SwapStatePage';
import BitcoinRefundedPage from '../done/BitcoinRefundedPage';
import ProcessExitedAndNotDonePage from './ProcessExitedAndNotDonePage';
import SwapStatePage from "../SwapStatePage";
import BitcoinRefundedPage from "../done/BitcoinRefundedPage";
import ProcessExitedAndNotDonePage from "./ProcessExitedAndNotDonePage";
type ProcessExitedPageProps = {
state: SwapStateProcessExited;
state: SwapStateProcessExited;
};
export default function ProcessExitedPage({ state }: ProcessExitedPageProps) {
const swap = useActiveSwapInfo();
const swap = useActiveSwapInfo();
// If we have a swap state, for a "done" state we should use it to display additional information that can't be extracted from the database
if (
isSwapStateXmrRedeemInMempool(state.prevState) ||
isSwapStateBtcRefunded(state.prevState) ||
isSwapStateBtcPunished(state.prevState)
) {
return <SwapStatePage swapState={state.prevState} />;
}
// If we have a swap state, for a "done" state we should use it to display additional information that can't be extracted from the database
if (
isSwapStateXmrRedeemInMempool(state.prevState) ||
isSwapStateBtcRefunded(state.prevState) ||
isSwapStateBtcPunished(state.prevState)
) {
return <SwapStatePage swapState={state.prevState} />;
}
// If we don't have a swap state for a "done" state, we should fall back to using the database to display as much information as we can
if (swap) {
if (swap.stateName === SwapStateName.XmrRedeemed) {
return <XmrRedeemInMempoolPage state={null} />;
// If we don't have a swap state for a "done" state, we should fall back to using the database to display as much information as we can
if (swap) {
if (swap.state_name === SwapStateName.XmrRedeemed) {
return <XmrRedeemInMempoolPage state={null} />;
}
if (swap.state_name === SwapStateName.BtcRefunded) {
return <BitcoinRefundedPage state={null} />;
}
if (swap.state_name === SwapStateName.BtcPunished) {
return <BitcoinPunishedPage />;
}
}
if (swap.stateName === SwapStateName.BtcRefunded) {
return <BitcoinRefundedPage state={null} />;
}
if (swap.stateName === SwapStateName.BtcPunished) {
return <BitcoinPunishedPage />;
}
}
// If the swap is not a "done" state (or we don't have a db state because the swap did complete the SwapSetup yet) we should tell the user and show logs
return <ProcessExitedAndNotDonePage state={state} />;
// If the swap is not a "done" state (or we don't have a db state because the swap did complete the SwapSetup yet) we should tell the user and show logs
return <ProcessExitedAndNotDonePage state={state} />;
}