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

103
Cargo.lock generated
View File

@ -1601,15 +1601,6 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "encoding_rs"
version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.3.4"
@ -2763,25 +2754,6 @@ dependencies = [
"nom",
]
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "itertools"
version = "0.10.5"
@ -4089,17 +4061,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "open"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "open-metrics-client"
version = "0.14.0"
@ -4135,16 +4096,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_pipe"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -5932,16 +5883,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "shared_child"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "shell-words"
version = "1.1.0"
@ -6729,44 +6670,6 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin"
version = "2.0.0-rc.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51a5c65ab8536a7e27b70ecbb0713ab42e8508acd9af1bc4a0817ccf7caf3165"
dependencies = [
"anyhow",
"glob",
"plist",
"schemars",
"serde",
"serde_json",
"tauri-utils",
"toml 0.8.2",
"walkdir",
]
[[package]]
name = "tauri-plugin-shell"
version = "2.0.0-rc.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9209f6c32caec61e156a5616f7d80ba7683ca4a0a5641cbe5d3086ab371aaab2"
dependencies = [
"encoding_rs",
"log",
"open",
"os_pipe",
"regex",
"schemars",
"serde",
"serde_json",
"shared_child",
"tauri",
"tauri-plugin",
"thiserror",
"tokio",
]
[[package]]
name = "tauri-runtime"
version = "2.0.0-rc.1"
@ -7683,7 +7586,7 @@ dependencies = [
"swap",
"tauri",
"tauri-build",
"tauri-plugin-shell",
"uuid",
]
[[package]]
@ -7731,9 +7634,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "1.9.1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [
"getrandom 0.2.15",
"serde",

View File

@ -1,24 +1,24 @@
import { piconerosToXmr, satsToBtc } from 'utils/conversionUtils';
import { exhaustiveGuard } from 'utils/typescriptUtils';
import { piconerosToXmr, satsToBtc } from "utils/conversionUtils";
import { exhaustiveGuard } from "utils/typescriptUtils";
export enum RpcMethod {
GET_BTC_BALANCE = 'get_bitcoin_balance',
WITHDRAW_BTC = 'withdraw_btc',
BUY_XMR = 'buy_xmr',
RESUME_SWAP = 'resume_swap',
LIST_SELLERS = 'list_sellers',
CANCEL_REFUND_SWAP = 'cancel_refund_swap',
GET_SWAP_INFO = 'get_swap_info',
SUSPEND_CURRENT_SWAP = 'suspend_current_swap',
GET_HISTORY = 'get_history',
GET_MONERO_RECOVERY_KEYS = 'get_monero_recovery_info',
GET_BTC_BALANCE = "get_bitcoin_balance",
WITHDRAW_BTC = "withdraw_btc",
BUY_XMR = "buy_xmr",
RESUME_SWAP = "resume_swap",
LIST_SELLERS = "list_sellers",
CANCEL_REFUND_SWAP = "cancel_refund_swap",
GET_SWAP_INFO = "get_swap_info",
SUSPEND_CURRENT_SWAP = "suspend_current_swap",
GET_HISTORY = "get_history",
GET_MONERO_RECOVERY_KEYS = "get_monero_recovery_info",
}
export enum RpcProcessStateType {
STARTED = 'starting...',
LISTENING_FOR_CONNECTIONS = 'running',
EXITED = 'exited',
NOT_STARTED = 'not started',
STARTED = "starting...",
LISTENING_FOR_CONNECTIONS = "running",
EXITED = "exited",
NOT_STARTED = "not started",
}
export type RawRpcResponseSuccess<T> = {
@ -38,13 +38,13 @@ export type RawRpcResponse<T> = RawRpcResponseSuccess<T> | RawRpcResponseError;
export function isSuccessResponse<T>(
response: RawRpcResponse<T>,
): response is RawRpcResponseSuccess<T> {
return 'result' in response;
return "result" in response;
}
export function isErrorResponse<T>(
response: RawRpcResponse<T>,
): response is RawRpcResponseError {
return 'error' in response;
return "error" in response;
}
export interface RpcSellerStatus {
@ -56,7 +56,7 @@ export interface RpcSellerStatus {
max_quantity: number;
};
}
| 'Unreachable';
| "Unreachable";
multiaddr: string;
}
@ -80,7 +80,7 @@ export type SwapTimelockInfoCancelled = {
};
};
export type SwapTimelockInfoPunished = 'Punish';
export type SwapTimelockInfoPunished = "Punish";
export type SwapTimelockInfo =
| SwapTimelockInfoNone
@ -90,19 +90,19 @@ export type SwapTimelockInfo =
export function isSwapTimelockInfoNone(
info: SwapTimelockInfo,
): info is SwapTimelockInfoNone {
return typeof info === 'object' && 'None' in info;
return typeof info === "object" && "None" in info;
}
export function isSwapTimelockInfoCancelled(
info: SwapTimelockInfo,
): info is SwapTimelockInfoCancelled {
return typeof info === 'object' && 'Cancel' in info;
return typeof info === "object" && "Cancel" in info;
}
export function isSwapTimelockInfoPunished(
info: SwapTimelockInfo,
): info is SwapTimelockInfoPunished {
return info === 'Punish';
return info === "Punish";
}
export type SwapSellerInfo = {
@ -111,21 +111,21 @@ export type SwapSellerInfo = {
};
export interface GetSwapInfoResponse {
swapId: string;
swap_id: string;
completed: boolean;
seller: SwapSellerInfo;
startDate: string;
stateName: SwapStateName;
start_date: string;
state_name: SwapStateName;
timelock: null | SwapTimelockInfo;
txLockId: string;
txCancelFee: number;
txRefundFee: number;
txLockFee: number;
btcAmount: number;
xmrAmount: number;
btcRefundAddress: string;
cancelTimelock: number;
punishTimelock: number;
tx_lock_id: string;
tx_cancel_fee: number;
tx_refund_fee: number;
tx_lock_fee: number;
btc_amount: number;
xmr_amount: number;
btc_refund_address: string;
cancel_timelock: number;
punish_timelock: number;
}
export type MoneroRecoveryResponse = {
@ -144,19 +144,19 @@ export interface GetHistoryResponse {
}
export enum SwapStateName {
Started = 'quote has been requested',
SwapSetupCompleted = 'execution setup done',
BtcLocked = 'btc is locked',
XmrLockProofReceived = 'XMR lock transaction transfer proof received',
XmrLocked = 'xmr is locked',
EncSigSent = 'encrypted signature is sent',
BtcRedeemed = 'btc is redeemed',
CancelTimelockExpired = 'cancel timelock is expired',
BtcCancelled = 'btc is cancelled',
BtcRefunded = 'btc is refunded',
XmrRedeemed = 'xmr is redeemed',
BtcPunished = 'btc is punished',
SafelyAborted = 'safely aborted',
Started = "quote has been requested",
SwapSetupCompleted = "execution setup done",
BtcLocked = "btc is locked",
XmrLockProofReceived = "XMR lock transaction transfer proof received",
XmrLocked = "xmr is locked",
EncSigSent = "encrypted signature is sent",
BtcRedeemed = "btc is redeemed",
CancelTimelockExpired = "cancel timelock is expired",
BtcCancelled = "btc is cancelled",
BtcRefunded = "btc is refunded",
XmrRedeemed = "xmr is redeemed",
BtcPunished = "btc is punished",
SafelyAborted = "safely aborted",
}
export type SwapStateNameRunningSwap = Exclude<
@ -275,7 +275,7 @@ export function isSwapStateNamePossiblyRefundableSwap(
export function isGetSwapInfoResponseRunningSwap(
response: GetSwapInfoResponse,
): response is GetSwapInfoResponseRunningSwap {
return isSwapStateNameRunningSwap(response.stateName);
return isSwapStateNameRunningSwap(response.state_name);
}
export function isSwapMoneroRecoverable(swapStateName: SwapStateName): boolean {
@ -286,46 +286,46 @@ export function isSwapMoneroRecoverable(swapStateName: SwapStateName): boolean {
export function getHumanReadableDbStateType(type: SwapStateName): string {
switch (type) {
case SwapStateName.Started:
return 'Quote has been requested';
return "Quote has been requested";
case SwapStateName.SwapSetupCompleted:
return 'Swap has been initiated';
return "Swap has been initiated";
case SwapStateName.BtcLocked:
return 'Bitcoin has been locked';
return "Bitcoin has been locked";
case SwapStateName.XmrLockProofReceived:
return 'Monero lock transaction transfer proof has been received';
return "Monero lock transaction transfer proof has been received";
case SwapStateName.XmrLocked:
return 'Monero has been locked';
return "Monero has been locked";
case SwapStateName.EncSigSent:
return 'Encrypted signature has been sent';
return "Encrypted signature has been sent";
case SwapStateName.BtcRedeemed:
return 'Bitcoin has been redeemed';
return "Bitcoin has been redeemed";
case SwapStateName.CancelTimelockExpired:
return 'Cancel timelock has expired';
return "Cancel timelock has expired";
case SwapStateName.BtcCancelled:
return 'Swap has been cancelled';
return "Swap has been cancelled";
case SwapStateName.BtcRefunded:
return 'Bitcoin has been refunded';
return "Bitcoin has been refunded";
case SwapStateName.XmrRedeemed:
return 'Monero has been redeemed';
return "Monero has been redeemed";
case SwapStateName.BtcPunished:
return 'Bitcoin has been punished';
return "Bitcoin has been punished";
case SwapStateName.SafelyAborted:
return 'Swap has been safely aborted';
return "Swap has been safely aborted";
default:
return exhaustiveGuard(type);
}
}
export function getSwapTxFees(swap: GetSwapInfoResponse): number {
return satsToBtc(swap.txLockFee);
return satsToBtc(swap.tx_lock_fee);
}
export function getSwapBtcAmount(swap: GetSwapInfoResponse): number {
return satsToBtc(swap.btcAmount);
return satsToBtc(swap.btc_amount);
}
export function getSwapXmrAmount(swap: GetSwapInfoResponse): number {
return piconerosToXmr(swap.xmrAmount);
return piconerosToXmr(swap.xmr_amount);
}
export function getSwapExchangeRate(swap: GetSwapInfoResponse): number {

View File

@ -1,97 +1,103 @@
import { makeStyles } from '@material-ui/core';
import { Alert, AlertTitle } from '@material-ui/lab';
import { useActiveSwapInfo } from 'store/hooks';
import { makeStyles } from "@material-ui/core";
import { Alert, AlertTitle } from "@material-ui/lab";
import { useActiveSwapInfo } from "store/hooks";
import {
isSwapTimelockInfoCancelled,
isSwapTimelockInfoNone,
} from 'models/rpcModel';
import HumanizedBitcoinBlockDuration from '../other/HumanizedBitcoinBlockDuration';
isSwapTimelockInfoCancelled,
isSwapTimelockInfoNone,
} from "models/rpcModel";
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
const useStyles = makeStyles((theme) => ({
outer: {
marginBottom: theme.spacing(1),
},
list: {
margin: theme.spacing(0.25),
},
outer: {
marginBottom: theme.spacing(1),
},
list: {
margin: theme.spacing(0.25),
},
}));
export default function SwapMightBeCancelledAlert({
bobBtcLockTxConfirmations,
bobBtcLockTxConfirmations,
}: {
bobBtcLockTxConfirmations: number;
bobBtcLockTxConfirmations: number;
}) {
const classes = useStyles();
const swap = useActiveSwapInfo();
const classes = useStyles();
const swap = useActiveSwapInfo();
if (
bobBtcLockTxConfirmations < 5 ||
swap === null ||
swap.timelock === null
) {
return <></>;
}
if (
bobBtcLockTxConfirmations < 5 ||
swap === null ||
swap.timelock === null
) {
return <></>;
}
const { timelock } = swap;
const punishTimelockOffset = swap.punishTimelock;
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>
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>
);
<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>
);
}

View File

@ -1,35 +1,35 @@
import { Alert, AlertTitle } from '@material-ui/lab/';
import { Box, makeStyles } from '@material-ui/core';
import { ReactNode } from 'react';
import { exhaustiveGuard } from 'utils/typescriptUtils';
import { Alert, AlertTitle } from "@material-ui/lab/";
import { Box, makeStyles } from "@material-ui/core";
import { ReactNode } from "react";
import { exhaustiveGuard } from "utils/typescriptUtils";
import {
SwapCancelRefundButton,
SwapResumeButton,
} from '../pages/history/table/HistoryRowActions';
import HumanizedBitcoinBlockDuration from '../other/HumanizedBitcoinBlockDuration';
SwapCancelRefundButton,
SwapResumeButton,
} from "../pages/history/table/HistoryRowActions";
import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
import {
GetSwapInfoResponse,
GetSwapInfoResponseRunningSwap,
isGetSwapInfoResponseRunningSwap,
isSwapTimelockInfoCancelled,
isSwapTimelockInfoNone,
isSwapTimelockInfoPunished,
SwapStateName,
SwapTimelockInfoCancelled,
SwapTimelockInfoNone,
} from '../../../models/rpcModel';
import { SwapMoneroRecoveryButton } from '../pages/history/table/SwapMoneroRecoveryButton';
GetSwapInfoResponse,
GetSwapInfoResponseRunningSwap,
isGetSwapInfoResponseRunningSwap,
isSwapTimelockInfoCancelled,
isSwapTimelockInfoNone,
isSwapTimelockInfoPunished,
SwapStateName,
SwapTimelockInfoCancelled,
SwapTimelockInfoNone,
} from "../../../models/rpcModel";
import { SwapMoneroRecoveryButton } from "../pages/history/table/SwapMoneroRecoveryButton";
const useStyles = makeStyles({
box: {
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
},
list: {
padding: '0px',
margin: '0px',
},
box: {
display: "flex",
flexDirection: "column",
gap: "0.5rem",
},
list: {
padding: "0px",
margin: "0px",
},
});
/**
@ -38,15 +38,15 @@ const useStyles = makeStyles({
* @returns JSX.Element
*/
const MessageList = ({ messages }: { messages: ReactNode[] }) => {
const classes = useStyles();
return (
<ul className={classes.list}>
{messages.map((msg, i) => (
// eslint-disable-next-line react/no-array-index-key
<li key={i}>{msg}</li>
))}
</ul>
);
const classes = useStyles();
return (
<ul className={classes.list}>
{messages.map((msg, i) => (
// eslint-disable-next-line react/no-array-index-key
<li key={i}>{msg}</li>
))}
</ul>
);
};
/**
@ -55,20 +55,24 @@ const MessageList = ({ messages }: { messages: ReactNode[] }) => {
* @returns JSX.Element
*/
const BitcoinRedeemedStateAlert = ({ swap }: { swap: GetSwapInfoResponse }) => {
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',
]}
/>
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
</Box>
);
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",
]}
/>
<SwapMoneroRecoveryButton
swap={swap}
size="small"
variant="contained"
/>
</Box>
);
};
/**
@ -78,28 +82,31 @@ const BitcoinRedeemedStateAlert = ({ swap }: { swap: GetSwapInfoResponse }) => {
* @returns JSX.Element
*/
const BitcoinLockedNoTimelockExpiredStateAlert = ({
timelock,
punishTimelockOffset,
timelock,
punishTimelockOffset,
}: {
timelock: SwapTimelockInfoNone;
punishTimelockOffset: number;
timelock: SwapTimelockInfoNone;
punishTimelockOffset: number;
}) => (
<MessageList
messages={[
<>
Your Bitcoin is locked. If the swap is not completed in approximately{' '}
<HumanizedBitcoinBlockDuration blocks={timelock.None.blocks_left} />,
you need to refund
</>,
<>
You will lose your funds if you do not refund or complete the swap
within{' '}
<HumanizedBitcoinBlockDuration
blocks={timelock.None.blocks_left + punishTimelockOffset}
/>
</>,
]}
/>
<MessageList
messages={[
<>
Your Bitcoin is locked. If the swap is not completed in
approximately{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.None.blocks_left}
/>
, you need to refund
</>,
<>
You will lose your funds if you do not refund or complete the
swap within{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.None.blocks_left + punishTimelockOffset}
/>
</>,
]}
/>
);
/**
@ -110,30 +117,34 @@ const BitcoinLockedNoTimelockExpiredStateAlert = ({
* @returns JSX.Element
*/
const BitcoinPossiblyCancelledAlert = ({
swap,
timelock,
swap,
timelock,
}: {
swap: GetSwapInfoResponse;
timelock: SwapTimelockInfoCancelled;
swap: GetSwapInfoResponse;
timelock: SwapTimelockInfoCancelled;
}) => {
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. If that fails, you can manually refund it',
<>
You will lose your funds if you do not refund within{' '}
<HumanizedBitcoinBlockDuration
blocks={timelock.Cancel.blocks_left}
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. If that fails, you can manually refund it",
<>
You will lose your funds if you do not refund within{" "}
<HumanizedBitcoinBlockDuration
blocks={timelock.Cancel.blocks_left}
/>
</>,
]}
/>
</>,
]}
/>
<SwapCancelRefundButton swap={swap} size="small" variant="contained" />
</Box>
);
<SwapCancelRefundButton
swap={swap}
size="small"
variant="contained"
/>
</Box>
);
};
/**
@ -141,7 +152,7 @@ const BitcoinPossiblyCancelledAlert = ({
* @returns JSX.Element
*/
const ImmediateActionAlert = () => (
<>Resume the swap immediately to avoid losing your funds</>
<>Resume the swap immediately to avoid losing your funds</>
);
/**
@ -150,55 +161,55 @@ const ImmediateActionAlert = () => (
* @returns JSX.Element | null
*/
function SwapAlertStatusText({
swap,
swap,
}: {
swap: GetSwapInfoResponseRunningSwap;
swap: GetSwapInfoResponseRunningSwap;
}) {
switch (swap.stateName) {
// This is the state where the swap is safe because the other party has redeemed the Bitcoin
// It cannot be punished anymore
case SwapStateName.BtcRedeemed:
return <BitcoinRedeemedStateAlert swap={swap} />;
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
case SwapStateName.BtcRedeemed:
return <BitcoinRedeemedStateAlert swap={swap} />;
// These are states that are at risk of punishment because the Bitcoin have been locked
// but has not been redeemed yet by the other party
case SwapStateName.BtcLocked:
case SwapStateName.XmrLockProofReceived:
case SwapStateName.XmrLocked:
case SwapStateName.EncSigSent:
case SwapStateName.CancelTimelockExpired:
case SwapStateName.BtcCancelled:
if (swap.timelock !== null) {
if (isSwapTimelockInfoNone(swap.timelock)) {
return (
<BitcoinLockedNoTimelockExpiredStateAlert
punishTimelockOffset={swap.punishTimelock}
timelock={swap.timelock}
/>
);
}
// These are states that are at risk of punishment because the Bitcoin have been locked
// but has not been redeemed yet by the other party
case SwapStateName.BtcLocked:
case SwapStateName.XmrLockProofReceived:
case SwapStateName.XmrLocked:
case SwapStateName.EncSigSent:
case SwapStateName.CancelTimelockExpired:
case SwapStateName.BtcCancelled:
if (swap.timelock !== null) {
if (isSwapTimelockInfoNone(swap.timelock)) {
return (
<BitcoinLockedNoTimelockExpiredStateAlert
punishTimelockOffset={swap.punish_timelock}
timelock={swap.timelock}
/>
);
}
if (isSwapTimelockInfoCancelled(swap.timelock)) {
return (
<BitcoinPossiblyCancelledAlert
timelock={swap.timelock}
swap={swap}
/>
);
}
if (isSwapTimelockInfoCancelled(swap.timelock)) {
return (
<BitcoinPossiblyCancelledAlert
timelock={swap.timelock}
swap={swap}
/>
);
}
if (isSwapTimelockInfoPunished(swap.timelock)) {
return <ImmediateActionAlert />;
}
if (isSwapTimelockInfoPunished(swap.timelock)) {
return <ImmediateActionAlert />;
}
// We have covered all possible timelock states above
// If we reach this point, it means we have missed a case
return exhaustiveGuard(swap.timelock);
}
return <ImmediateActionAlert />;
default:
return exhaustiveGuard(swap.stateName);
}
// We have covered all possible timelock states above
// If we reach this point, it means we have missed a case
return exhaustiveGuard(swap.timelock);
}
return <ImmediateActionAlert />;
default:
return exhaustiveGuard(swap.state_name);
}
}
/**
@ -207,27 +218,27 @@ function SwapAlertStatusText({
* @returns JSX.Element | null
*/
export default function SwapStatusAlert({
swap,
swap,
}: {
swap: GetSwapInfoResponse;
swap: GetSwapInfoResponse;
}): JSX.Element | null {
// If the swap is not running, there is no need to display the alert
// This is either because the swap is finished or has not started yet (e.g. in the setup phase, no Bitcoin locked)
if (!isGetSwapInfoResponseRunningSwap(swap)) {
return null;
}
// If the swap is not running, there is no need to display the alert
// This is either because the swap is finished or has not started yet (e.g. in the setup phase, no Bitcoin locked)
if (!isGetSwapInfoResponseRunningSwap(swap)) {
return null;
}
return (
<Alert
key={swap.swapId}
severity="warning"
action={<SwapResumeButton swap={swap} />}
variant="filled"
>
<AlertTitle>
Swap {swap.swapId.substring(0, 5)}... is unfinished
</AlertTitle>
<SwapAlertStatusText swap={swap} />
</Alert>
);
return (
<Alert
key={swap.swap_id}
severity="warning"
action={<SwapResumeButton swap={swap} />}
variant="filled"
>
<AlertTitle>
Swap {swap.swap_id.substring(0, 5)}... is unfinished
</AlertTitle>
<SwapAlertStatusText swap={swap} />
</Alert>
);
}

View File

@ -1,28 +1,28 @@
import { Box, makeStyles } from '@material-ui/core';
import { useSwapInfosSortedByDate } from 'store/hooks';
import SwapStatusAlert from './SwapStatusAlert';
import { Box, makeStyles } from "@material-ui/core";
import { useSwapInfosSortedByDate } from "store/hooks";
import SwapStatusAlert from "./SwapStatusAlert";
const useStyles = makeStyles((theme) => ({
outer: {
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
},
outer: {
display: "flex",
flexDirection: "column",
gap: theme.spacing(1),
},
}));
export default function SwapTxLockAlertsBox() {
const classes = useStyles();
const classes = useStyles();
// We specifically choose ALL swaps here
// If a swap is in a state where an Alert is not needed (becaue no Bitcoin have been locked or because the swap has been completed)
// the SwapStatusAlert component will not render an Alert
const swaps = useSwapInfosSortedByDate();
// We specifically choose ALL swaps here
// If a swap is in a state where an Alert is not needed (becaue no Bitcoin have been locked or because the swap has been completed)
// the SwapStatusAlert component will not render an Alert
const swaps = useSwapInfosSortedByDate();
return (
<Box className={classes.outer}>
{swaps.map((swap) => (
<SwapStatusAlert key={swap.swapId} swap={swap} />
))}
</Box>
);
return (
<Box className={classes.outer}>
{swaps.map((swap) => (
<SwapStatusAlert key={swap.swap_id} swap={swap} />
))}
</Box>
);
}

View File

@ -1,47 +1,44 @@
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
MenuItem,
Select,
TextField,
} from '@material-ui/core';
import { useState } from 'react';
import { useSnackbar } from 'notistack';
import {
useActiveSwapInfo,
useAppSelector,
} from 'store/hooks';
import { parseDateString } from 'utils/parseUtils';
import { store } from 'renderer/store/storeRenderer';
import { CliLog } from 'models/cliModel';
import { submitFeedbackViaHttp } from '../../../api';
import { PiconeroAmount } from '../../other/Units';
import LoadingButton from '../../other/LoadingButton';
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
MenuItem,
Select,
TextField,
} from "@material-ui/core";
import { useState } from "react";
import { useSnackbar } from "notistack";
import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { parseDateString } from "utils/parseUtils";
import { store } from "renderer/store/storeRenderer";
import { CliLog } from "models/cliModel";
import { submitFeedbackViaHttp } from "../../../api";
import { PiconeroAmount } from "../../other/Units";
import LoadingButton from "../../other/LoadingButton";
async function submitFeedback(body: string, swapId: string | number) {
let attachedBody = '';
let attachedBody = "";
if (swapId !== 0 && typeof swapId === 'string') {
const swapInfo = store.getState().rpc.state.swapInfos[swapId];
const logs = [] as CliLog[];
if (swapId !== 0 && typeof swapId === "string") {
const swapInfo = store.getState().rpc.state.swapInfos[swapId];
const logs = [] as CliLog[];
throw new Error('Not implemented');
throw new Error("Not implemented");
if (swapInfo === undefined) {
throw new Error(`Swap with id ${swapId} not found`);
if (swapInfo === undefined) {
throw new Error(`Swap with id ${swapId} not found`);
}
attachedBody = `${JSON.stringify(swapInfo, null, 4)} \n\nLogs: ${logs
.map((l) => JSON.stringify(l))
.join("\n====\n")}`;
}
attachedBody = `${JSON.stringify(swapInfo, null, 4)} \n\nLogs: ${logs
.map((l) => JSON.stringify(l))
.join('\n====\n')}`;
}
await submitFeedbackViaHttp(body, attachedBody);
await submitFeedbackViaHttp(body, attachedBody);
}
/*
@ -51,120 +48,136 @@ async function submitFeedback(body: string, swapId: string | number) {
* selectedSwap = 0 means no swap is attached
*/
function SwapSelectDropDown({
selectedSwap,
setSelectedSwap,
selectedSwap,
setSelectedSwap,
}: {
selectedSwap: string | number;
setSelectedSwap: (swapId: string | number) => void;
selectedSwap: string | number;
setSelectedSwap: (swapId: string | number) => void;
}) {
const swaps = useAppSelector((state) =>
Object.values(state.rpc.state.swapInfos),
);
const swaps = useAppSelector((state) =>
Object.values(state.rpc.state.swapInfos),
);
return (
<Select
value={selectedSwap}
label="Attach logs"
variant="outlined"
onChange={(e) => setSelectedSwap(e.target.value as string)}
>
<MenuItem value={0}>Do not attach logs</MenuItem>
{swaps.map((swap) => (
<MenuItem value={swap.swapId}>
Swap {swap.swapId.substring(0, 5)}... from{' '}
{new Date(parseDateString(swap.startDate)).toDateString()} (
<PiconeroAmount amount={swap.xmrAmount} />)
</MenuItem>
))}
</Select>
);
return (
<Select
value={selectedSwap}
label="Attach logs"
variant="outlined"
onChange={(e) => setSelectedSwap(e.target.value as string)}
>
<MenuItem value={0}>Do not attach logs</MenuItem>
{swaps.map((swap) => (
<MenuItem value={swap.swap_id}>
Swap {swap.swap_id.substring(0, 5)}... from{" "}
{new Date(parseDateString(swap.start_date)).toDateString()}{" "}
(
<PiconeroAmount amount={swap.xmr_amount} />)
</MenuItem>
))}
</Select>
);
}
const MAX_FEEDBACK_LENGTH = 4000;
export default function FeedbackDialog({
open,
onClose,
open,
onClose,
}: {
open: boolean;
onClose: () => void;
open: boolean;
onClose: () => void;
}) {
const [pending, setPending] = useState(false);
const [bodyText, setBodyText] = useState('');
const currentSwapId = useActiveSwapInfo();
const [pending, setPending] = useState(false);
const [bodyText, setBodyText] = useState("");
const currentSwapId = useActiveSwapInfo();
const { enqueueSnackbar } = useSnackbar();
const { enqueueSnackbar } = useSnackbar();
const [selectedAttachedSwap, setSelectedAttachedSwap] = useState<
string | number
>(currentSwapId?.swapId || 0);
const [selectedAttachedSwap, setSelectedAttachedSwap] = useState<
string | number
>(currentSwapId?.swap_id || 0);
const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH;
const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH;
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Submit Feedback</DialogTitle>
<DialogContent>
<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!
</DialogContentText>
<Box style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<TextField
variant="outlined"
value={bodyText}
onChange={(e) => setBodyText(e.target.value)}
label={
bodyTooLong
? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: 'Feedback'
}
multiline
minRows={4}
maxRows={4}
fullWidth
error={bodyTooLong}
/>
<SwapSelectDropDown
selectedSwap={selectedAttachedSwap}
setSelectedSwap={setSelectedAttachedSwap}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<LoadingButton
color="primary"
variant="contained"
onClick={async () => {
if (pending) {
return;
}
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Submit Feedback</DialogTitle>
<DialogContent>
<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!
</DialogContentText>
<Box
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
>
<TextField
variant="outlined"
value={bodyText}
onChange={(e) => setBodyText(e.target.value)}
label={
bodyTooLong
? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: "Feedback"
}
multiline
minRows={4}
maxRows={4}
fullWidth
error={bodyTooLong}
/>
<SwapSelectDropDown
selectedSwap={selectedAttachedSwap}
setSelectedSwap={setSelectedAttachedSwap}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<LoadingButton
color="primary"
variant="contained"
onClick={async () => {
if (pending) {
return;
}
try {
setPending(true);
await submitFeedback(bodyText, selectedAttachedSwap);
enqueueSnackbar('Feedback submitted successfully!', {
variant: 'success',
});
} catch (e) {
console.error(`Failed to submit feedback: ${e}`);
enqueueSnackbar(`Failed to submit feedback (${e})`, {
variant: 'error',
});
} finally {
setPending(false);
}
onClose();
}}
loading={pending}
>
Submit
</LoadingButton>
</DialogActions>
</Dialog>
);
try {
setPending(true);
await submitFeedback(
bodyText,
selectedAttachedSwap,
);
enqueueSnackbar(
"Feedback submitted successfully!",
{
variant: "success",
},
);
} catch (e) {
console.error(`Failed to submit feedback: ${e}`);
enqueueSnackbar(
`Failed to submit feedback (${e})`,
{
variant: "error",
},
);
} finally {
setPending(false);
}
onClose();
}}
loading={pending}
>
Submit
</LoadingButton>
</DialogActions>
</Dialog>
);
}

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

View File

@ -1,86 +1,98 @@
import {
Box,
Collapse,
IconButton,
makeStyles,
TableCell,
TableRow,
} from '@material-ui/core';
import { useState } from 'react';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
Box,
Collapse,
IconButton,
makeStyles,
TableCell,
TableRow,
} from "@material-ui/core";
import { useState } from "react";
import ArrowForwardIcon from "@material-ui/icons/ArrowForward";
import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp";
import {
getHumanReadableDbStateType,
getSwapBtcAmount,
getSwapXmrAmount,
GetSwapInfoResponse,
} from '../../../../../models/rpcModel';
import HistoryRowActions from './HistoryRowActions';
import HistoryRowExpanded from './HistoryRowExpanded';
import { BitcoinAmount, MoneroAmount } from '../../../other/Units';
getHumanReadableDbStateType,
getSwapBtcAmount,
getSwapXmrAmount,
GetSwapInfoResponse,
} from "../../../../../models/rpcModel";
import HistoryRowActions from "./HistoryRowActions";
import HistoryRowExpanded from "./HistoryRowExpanded";
import { BitcoinAmount, MoneroAmount } from "../../../other/Units";
type HistoryRowProps = {
swap: GetSwapInfoResponse;
swap: GetSwapInfoResponse;
};
const useStyles = makeStyles((theme) => ({
amountTransferContainer: {
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
},
amountTransferContainer: {
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
},
}));
function AmountTransfer({
btcAmount,
xmrAmount,
btcAmount,
xmrAmount,
}: {
xmrAmount: number;
btcAmount: number;
xmrAmount: number;
btcAmount: number;
}) {
const classes = useStyles();
const classes = useStyles();
return (
<Box className={classes.amountTransferContainer}>
<BitcoinAmount amount={btcAmount} />
<ArrowForwardIcon />
<MoneroAmount amount={xmrAmount} />
</Box>
);
return (
<Box className={classes.amountTransferContainer}>
<BitcoinAmount amount={btcAmount} />
<ArrowForwardIcon />
<MoneroAmount amount={xmrAmount} />
</Box>
);
}
export default function HistoryRow({ swap }: HistoryRowProps) {
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
const [expanded, setExpanded] = useState(false);
const [expanded, setExpanded] = useState(false);
return (
<>
<TableRow>
<TableCell>
<IconButton size="small" onClick={() => setExpanded(!expanded)}>
{expanded ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
</TableCell>
<TableCell>{swap.swapId.substring(0, 5)}...</TableCell>
<TableCell>
<AmountTransfer xmrAmount={xmrAmount} btcAmount={btcAmount} />
</TableCell>
<TableCell>{getHumanReadableDbStateType(swap.stateName)}</TableCell>
<TableCell>
<HistoryRowActions swap={swap} />
</TableCell>
</TableRow>
return (
<>
<TableRow>
<TableCell>
<IconButton
size="small"
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<KeyboardArrowUpIcon />
) : (
<KeyboardArrowDownIcon />
)}
</IconButton>
</TableCell>
<TableCell>{swap.swap_id.substring(0, 5)}...</TableCell>
<TableCell>
<AmountTransfer
xmrAmount={xmrAmount}
btcAmount={btcAmount}
/>
</TableCell>
<TableCell>
{getHumanReadableDbStateType(swap.state_name)}
</TableCell>
<TableCell>
<HistoryRowActions swap={swap} />
</TableCell>
</TableRow>
<TableRow>
<TableCell style={{ padding: 0 }} colSpan={6}>
<Collapse in={expanded} timeout="auto">
{expanded && <HistoryRowExpanded swap={swap} />}
</Collapse>
</TableCell>
</TableRow>
</>
);
<TableRow>
<TableCell style={{ padding: 0 }} colSpan={6}>
<Collapse in={expanded} timeout="auto">
{expanded && <HistoryRowExpanded swap={swap} />}
</Collapse>
</TableCell>
</TableRow>
</>
);
}

View File

@ -1,90 +1,90 @@
import { Tooltip } from '@material-ui/core';
import Button, { ButtonProps } from '@material-ui/core/Button/Button';
import DoneIcon from '@material-ui/icons/Done';
import ErrorIcon from '@material-ui/icons/Error';
import { green, red } from '@material-ui/core/colors';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import IpcInvokeButton from '../../../IpcInvokeButton';
import { Tooltip } from "@material-ui/core";
import Button, { ButtonProps } from "@material-ui/core/Button/Button";
import DoneIcon from "@material-ui/icons/Done";
import ErrorIcon from "@material-ui/icons/Error";
import { green, red } from "@material-ui/core/colors";
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import IpcInvokeButton from "../../../IpcInvokeButton";
import {
GetSwapInfoResponse,
SwapStateName,
isSwapStateNamePossiblyCancellableSwap,
isSwapStateNamePossiblyRefundableSwap,
} from '../../../../../models/rpcModel';
GetSwapInfoResponse,
SwapStateName,
isSwapStateNamePossiblyCancellableSwap,
isSwapStateNamePossiblyRefundableSwap,
} from "../../../../../models/rpcModel";
export function SwapResumeButton({
swap,
...props
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
return (
<IpcInvokeButton
variant="contained"
color="primary"
disabled={swap.completed}
ipcChannel="spawn-resume-swap"
ipcArgs={[swap.swapId]}
endIcon={<PlayArrowIcon />}
requiresRpc
{...props}
>
Resume
</IpcInvokeButton>
);
return (
<IpcInvokeButton
variant="contained"
color="primary"
disabled={swap.completed}
ipcChannel="spawn-resume-swap"
ipcArgs={[swap.swap_id]}
endIcon={<PlayArrowIcon />}
requiresRpc
{...props}
>
Resume
</IpcInvokeButton>
);
}
export function SwapCancelRefundButton({
swap,
...props
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
const cancelOrRefundable =
isSwapStateNamePossiblyCancellableSwap(swap.stateName) ||
isSwapStateNamePossiblyRefundableSwap(swap.stateName);
const cancelOrRefundable =
isSwapStateNamePossiblyCancellableSwap(swap.state_name) ||
isSwapStateNamePossiblyRefundableSwap(swap.state_name);
if (!cancelOrRefundable) {
return <></>;
}
if (!cancelOrRefundable) {
return <></>;
}
return (
<IpcInvokeButton
ipcChannel="spawn-cancel-refund"
ipcArgs={[swap.swapId]}
requiresRpc
displayErrorSnackbar={false}
{...props}
>
Attempt manual Cancel & Refund
</IpcInvokeButton>
);
return (
<IpcInvokeButton
ipcChannel="spawn-cancel-refund"
ipcArgs={[swap.swap_id]}
requiresRpc
displayErrorSnackbar={false}
{...props}
>
Attempt manual Cancel & Refund
</IpcInvokeButton>
);
}
export default function HistoryRowActions({
swap,
swap,
}: {
swap: GetSwapInfoResponse;
swap: GetSwapInfoResponse;
}) {
if (swap.stateName === SwapStateName.XmrRedeemed) {
return (
<Tooltip title="The swap is completed because you have redeemed the XMR">
<DoneIcon style={{ color: green[500] }} />
</Tooltip>
);
}
if (swap.state_name === SwapStateName.XmrRedeemed) {
return (
<Tooltip title="The swap is completed because you have redeemed the XMR">
<DoneIcon style={{ color: green[500] }} />
</Tooltip>
);
}
if (swap.stateName === SwapStateName.BtcRefunded) {
return (
<Tooltip title="The swap is completed because your BTC have been refunded">
<DoneIcon style={{ color: green[500] }} />
</Tooltip>
);
}
if (swap.state_name === SwapStateName.BtcRefunded) {
return (
<Tooltip title="The swap is completed because your BTC have been refunded">
<DoneIcon style={{ color: green[500] }} />
</Tooltip>
);
}
if (swap.stateName === SwapStateName.BtcPunished) {
return (
<Tooltip title="The swap is completed because you have been punished">
<ErrorIcon style={{ color: red[500] }} />
</Tooltip>
);
}
if (swap.state_name === SwapStateName.BtcPunished) {
return (
<Tooltip title="The swap is completed because you have been punished">
<ErrorIcon style={{ color: red[500] }} />
</Tooltip>
);
}
return <SwapResumeButton swap={swap} />;
return <SwapResumeButton swap={swap} />;
}

View File

@ -1,134 +1,143 @@
import {
Box,
Link,
makeStyles,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
} from '@material-ui/core';
import { getBitcoinTxExplorerUrl } from 'utils/conversionUtils';
import { isTestnet } from 'store/config';
Box,
Link,
makeStyles,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
} from "@material-ui/core";
import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import { isTestnet } from "store/config";
import {
getHumanReadableDbStateType,
getSwapBtcAmount,
getSwapExchangeRate,
getSwapTxFees,
getSwapXmrAmount,
GetSwapInfoResponse,
} from '../../../../../models/rpcModel';
import SwapLogFileOpenButton from './SwapLogFileOpenButton';
import { SwapCancelRefundButton } from './HistoryRowActions';
import { SwapMoneroRecoveryButton } from './SwapMoneroRecoveryButton';
getHumanReadableDbStateType,
getSwapBtcAmount,
getSwapExchangeRate,
getSwapTxFees,
getSwapXmrAmount,
GetSwapInfoResponse,
} from "../../../../../models/rpcModel";
import SwapLogFileOpenButton from "./SwapLogFileOpenButton";
import { SwapCancelRefundButton } from "./HistoryRowActions";
import { SwapMoneroRecoveryButton } from "./SwapMoneroRecoveryButton";
import {
BitcoinAmount,
MoneroAmount,
MoneroBitcoinExchangeRate,
} from 'renderer/components/other/Units';
BitcoinAmount,
MoneroAmount,
MoneroBitcoinExchangeRate,
} from "renderer/components/other/Units";
const useStyles = makeStyles((theme) => ({
outer: {
display: 'grid',
padding: theme.spacing(1),
gap: theme.spacing(1),
},
actionsOuter: {
display: 'flex',
flexDirection: 'row',
gap: theme.spacing(1),
},
outer: {
display: "grid",
padding: theme.spacing(1),
gap: theme.spacing(1),
},
actionsOuter: {
display: "flex",
flexDirection: "row",
gap: theme.spacing(1),
},
}));
export default function HistoryRowExpanded({
swap,
swap,
}: {
swap: GetSwapInfoResponse;
swap: GetSwapInfoResponse;
}) {
const classes = useStyles();
const classes = useStyles();
const { seller, startDate } = swap;
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
const txFees = getSwapTxFees(swap);
const exchangeRate = getSwapExchangeRate(swap);
const { seller, start_date: startDate } = swap;
const btcAmount = getSwapBtcAmount(swap);
const xmrAmount = getSwapXmrAmount(swap);
const txFees = getSwapTxFees(swap);
const exchangeRate = getSwapExchangeRate(swap);
return (
<Box className={classes.outer}>
<TableContainer>
<Table>
<TableBody>
<TableRow>
<TableCell>Started on</TableCell>
<TableCell>{startDate}</TableCell>
</TableRow>
<TableRow>
<TableCell>Swap ID</TableCell>
<TableCell>{swap.swapId}</TableCell>
</TableRow>
<TableRow>
<TableCell>State Name</TableCell>
<TableCell>
{getHumanReadableDbStateType(swap.stateName)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Monero Amount</TableCell>
<TableCell>
<MoneroAmount amount={xmrAmount} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin Amount</TableCell>
<TableCell>
<BitcoinAmount amount={btcAmount} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Exchange Rate</TableCell>
<TableCell>
<MoneroBitcoinExchangeRate rate={exchangeRate} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin Network Fees</TableCell>
<TableCell>
<BitcoinAmount amount={txFees} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Provider Address</TableCell>
<TableCell>
<Box>{seller.addresses.join(', ')}</Box>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin lock transaction</TableCell>
<TableCell>
<Link
href={getBitcoinTxExplorerUrl(swap.txLockId, isTestnet())}
target="_blank"
>
{swap.txLockId}
</Link>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Box className={classes.actionsOuter}>
<SwapLogFileOpenButton
swapId={swap.swapId}
variant="outlined"
size="small"
/>
<SwapCancelRefundButton swap={swap} variant="contained" size="small" />
<SwapMoneroRecoveryButton
swap={swap}
variant="contained"
size="small"
/>
</Box>
</Box>
);
return (
<Box className={classes.outer}>
<TableContainer>
<Table>
<TableBody>
<TableRow>
<TableCell>Started on</TableCell>
<TableCell>{startDate}</TableCell>
</TableRow>
<TableRow>
<TableCell>Swap ID</TableCell>
<TableCell>{swap.swap_id}</TableCell>
</TableRow>
<TableRow>
<TableCell>State Name</TableCell>
<TableCell>
{getHumanReadableDbStateType(swap.state_name)}
</TableCell>
</TableRow>
<TableRow>
<TableCell>Monero Amount</TableCell>
<TableCell>
<MoneroAmount amount={xmrAmount} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin Amount</TableCell>
<TableCell>
<BitcoinAmount amount={btcAmount} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Exchange Rate</TableCell>
<TableCell>
<MoneroBitcoinExchangeRate
rate={exchangeRate}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin Network Fees</TableCell>
<TableCell>
<BitcoinAmount amount={txFees} />
</TableCell>
</TableRow>
<TableRow>
<TableCell>Provider Address</TableCell>
<TableCell>
<Box>{seller.addresses.join(", ")}</Box>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Bitcoin lock transaction</TableCell>
<TableCell>
<Link
href={getBitcoinTxExplorerUrl(
swap.tx_lock_id,
isTestnet(),
)}
target="_blank"
>
{swap.tx_lock_id}
</Link>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<Box className={classes.actionsOuter}>
<SwapLogFileOpenButton
swapId={swap.swap_id}
variant="outlined"
size="small"
/>
<SwapCancelRefundButton
swap={swap}
variant="contained"
size="small"
/>
<SwapMoneroRecoveryButton
swap={swap}
variant="contained"
size="small"
/>
</Box>
</Box>
);
}

View File

@ -1,53 +1,53 @@
import {
Box,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from '@material-ui/core';
import { sortBy } from 'lodash';
import { parseDateString } from 'utils/parseUtils';
Box,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@material-ui/core";
import { sortBy } from "lodash";
import { parseDateString } from "utils/parseUtils";
import {
useAppSelector,
useSwapInfosSortedByDate,
} from '../../../../../store/hooks';
import HistoryRow from './HistoryRow';
useAppSelector,
useSwapInfosSortedByDate,
} from "../../../../../store/hooks";
import HistoryRow from "./HistoryRow";
const useStyles = makeStyles((theme) => ({
outer: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
},
outer: {
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
},
}));
export default function HistoryTable() {
const classes = useStyles();
const swapSortedByDate = useSwapInfosSortedByDate();
const classes = useStyles();
const swapSortedByDate = useSwapInfosSortedByDate();
return (
<Box className={classes.outer}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell />
<TableCell>ID</TableCell>
<TableCell>Amount</TableCell>
<TableCell>State</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{swapSortedByDate.map((swap) => (
<HistoryRow swap={swap} key={swap.swapId} />
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
return (
<Box className={classes.outer}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell />
<TableCell>ID</TableCell>
<TableCell>Amount</TableCell>
<TableCell>State</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{swapSortedByDate.map((swap) => (
<HistoryRow swap={swap} key={swap.swap_id} />
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
}

View File

@ -1,119 +1,120 @@
import { ButtonProps } from '@material-ui/core/Button/Button';
import { ButtonProps } from "@material-ui/core/Button/Button";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
Link,
} from '@material-ui/core';
import { useAppDispatch, useAppSelector } from 'store/hooks';
import { rpcResetMoneroRecoveryKeys } from 'store/features/rpcSlice';
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
Link,
} from "@material-ui/core";
import { useAppDispatch, useAppSelector } from "store/hooks";
import { rpcResetMoneroRecoveryKeys } from "store/features/rpcSlice";
import {
GetSwapInfoResponse,
isSwapMoneroRecoverable,
} from '../../../../../models/rpcModel';
import IpcInvokeButton from '../../../IpcInvokeButton';
import DialogHeader from '../../../modal/DialogHeader';
import ScrollablePaperTextBox from '../../../other/ScrollablePaperTextBox';
GetSwapInfoResponse,
isSwapMoneroRecoverable,
} from "../../../../../models/rpcModel";
import IpcInvokeButton from "../../../IpcInvokeButton";
import DialogHeader from "../../../modal/DialogHeader";
import ScrollablePaperTextBox from "../../../other/ScrollablePaperTextBox";
function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
const dispatch = useAppDispatch();
const keys = useAppSelector((s) => s.rpc.state.moneroRecovery);
const dispatch = useAppDispatch();
const keys = useAppSelector((s) => s.rpc.state.moneroRecovery);
function onClose() {
dispatch(rpcResetMoneroRecoveryKeys());
}
function onClose() {
dispatch(rpcResetMoneroRecoveryKeys());
}
if (keys === null || keys.swapId !== swap.swapId) {
return <></>;
}
if (keys === null || keys.swapId !== swap.swap_id) {
return <></>;
}
return (
<Dialog open onClose={onClose} maxWidth="sm" fullWidth>
<DialogHeader
title={`Recovery Keys for swap ${swap.swapId.substring(0, 5)}...`}
/>
<DialogContent>
<DialogContentText>
You can use the keys below to manually redeem the Monero funds from
the multi-signature wallet.
<ul>
<li>
This is useful if the swap daemon fails to redeem the funds itself
</li>
<li>
If you have come this far, there is no risk of losing funds. You
are the only one with access to these keys and can use them to
access your funds
</li>
<li>
View{' '}
<Link
href="https://www.getmonero.org/resources/user-guides/restore_from_keys.html"
target="_blank"
rel="noreferrer"
>
this guide
</Link>{' '}
for a detailed description on how to import the keys and spend the
funds.
</li>
</ul>
</DialogContentText>
<Box
style={{
display: 'flex',
gap: '0.5rem',
flexDirection: 'column',
}}
>
{[
['Primary Address', keys.keys.address],
['View Key', keys.keys.view_key],
['Spend Key', keys.keys.spend_key],
['Restore Height', keys.keys.restore_height.toString()],
].map(([title, value]) => (
<ScrollablePaperTextBox
minHeight="2rem"
title={title}
copyValue={value}
rows={[value]}
return (
<Dialog open onClose={onClose} maxWidth="sm" fullWidth>
<DialogHeader
title={`Recovery Keys for swap ${swap.swap_id.substring(0, 5)}...`}
/>
))}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary" variant="contained">
Done
</Button>
</DialogActions>
</Dialog>
);
<DialogContent>
<DialogContentText>
You can use the keys below to manually redeem the Monero
funds from the multi-signature wallet.
<ul>
<li>
This is useful if the swap daemon fails to redeem
the funds itself
</li>
<li>
If you have come this far, there is no risk of
losing funds. You are the only one with access to
these keys and can use them to access your funds
</li>
<li>
View{" "}
<Link
href="https://www.getmonero.org/resources/user-guides/restore_from_keys.html"
target="_blank"
rel="noreferrer"
>
this guide
</Link>{" "}
for a detailed description on how to import the keys
and spend the funds.
</li>
</ul>
</DialogContentText>
<Box
style={{
display: "flex",
gap: "0.5rem",
flexDirection: "column",
}}
>
{[
["Primary Address", keys.keys.address],
["View Key", keys.keys.view_key],
["Spend Key", keys.keys.spend_key],
["Restore Height", keys.keys.restore_height.toString()],
].map(([title, value]) => (
<ScrollablePaperTextBox
minHeight="2rem"
title={title}
copyValue={value}
rows={[value]}
/>
))}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary" variant="contained">
Done
</Button>
</DialogActions>
</Dialog>
);
}
export function SwapMoneroRecoveryButton({
swap,
...props
swap,
...props
}: { swap: GetSwapInfoResponse } & ButtonProps) {
const isRecoverable = isSwapMoneroRecoverable(swap.stateName);
const isRecoverable = isSwapMoneroRecoverable(swap.state_name);
if (!isRecoverable) {
return <></>;
}
if (!isRecoverable) {
return <></>;
}
return (
<>
<IpcInvokeButton
ipcChannel="spawn-monero-recovery"
ipcArgs={[swap.swapId]}
requiresRpc
{...props}
>
Display Monero Recovery Keys
</IpcInvokeButton>
<MoneroRecoveryKeysDialog swap={swap} />
</>
);
return (
<>
<IpcInvokeButton
ipcChannel="spawn-monero-recovery"
ipcArgs={[swap.swap_id]}
requiresRpc
{...props}
>
Display Monero Recovery Keys
</IpcInvokeButton>
<MoneroRecoveryKeysDialog swap={swap} />
</>
);
}

View File

@ -1,7 +1,7 @@
import { invoke } from "@tauri-apps/api/core";
import { BalanceBitcoinResponse } from "models/rpcModel";
import { store } from "./store/storeRenderer";
import { rpcSetBalance } from "store/features/rpcSlice";
import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
export async function checkBitcoinBalance() {
// TODO: use tauri-bindgen here
@ -13,6 +13,7 @@ export async function checkBitcoinBalance() {
}
export async function getRawSwapInfos() {
const response = await invoke("swap_infos");
const response = await invoke("swap_infos_all");
console.log(response);
(response as any[]).forEach((info) => store.dispatch(rpcSetSwapInfo(info)));
}

View File

@ -1,11 +1,11 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ExtendedProviderStatus, ProviderStatus } from 'models/apiModel';
import { MoneroWalletRpcUpdateState } from 'models/storeModel';
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
import { MoneroWalletRpcUpdateState } from "models/storeModel";
import {
GetSwapInfoResponse,
MoneroRecoveryResponse,
RpcProcessStateType,
} from '../../models/rpcModel';
} from "../../models/rpcModel";
import {
CliLog,
isCliLog,
@ -14,8 +14,8 @@ import {
isCliLogFinishedSyncingMoneroWallet,
isCliLogStartedRpcServer,
isCliLogStartedSyncingMoneroWallet,
} from '../../models/cliModel';
import { getLogsAndStringsFromRawFileString } from 'utils/parseUtils';
} from "../../models/cliModel";
import { getLogsAndStringsFromRawFileString } from "utils/parseUtils";
type Process =
| {
@ -82,7 +82,7 @@ const initialState: RPCSlice = {
};
export const rpcSlice = createSlice({
name: 'rpc',
name: "rpc",
initialState,
reducers: {
rpcAddLogs(slice, action: PayloadAction<(CliLog | string)[]>) {
@ -110,7 +110,7 @@ export const rpcSlice = createSlice({
downloadUrl: log.fields.download_url,
};
if (log.fields.progress === '100%') {
if (log.fields.progress === "100%") {
slice.state.moneroWalletRpc.updateState = false;
}
} else if (isCliLogStartedSyncingMoneroWallet(log)) {
@ -169,7 +169,7 @@ export const rpcSlice = createSlice({
slice.state.withdrawTxId = null;
},
rpcSetSwapInfo(slice, action: PayloadAction<GetSwapInfoResponse>) {
slice.state.swapInfos[action.payload.swapId] = action.payload;
slice.state.swapInfos[action.payload.swap_id] = action.payload;
},
rpcSetEndpointBusy(slice, action: PayloadAction<string>) {
if (!slice.busyEndpoints.includes(action.payload)) {

View File

@ -1,7 +1,7 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from 'renderer/store/storeRenderer';
import { sortBy } from 'lodash';
import { parseDateString } from 'utils/parseUtils';
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
import { sortBy } from "lodash";
import { parseDateString } from "utils/parseUtils";
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
@ -22,7 +22,7 @@ export function useIsSwapRunning() {
export function useSwapInfo(swapId: string | null) {
return useAppSelector((state) =>
swapId ? state.rpc.state.swapInfos[swapId] ?? null : null,
swapId ? (state.rpc.state.swapInfos[swapId] ?? null) : null,
);
}
@ -51,6 +51,6 @@ export function useSwapInfosSortedByDate() {
const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos);
return sortBy(
Object.values(swapInfos),
(swap) => -parseDateString(swap.startDate),
(swap) => -parseDateString(swap.start_date),
);
}

View File

@ -21,4 +21,4 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
swap = { path = "../swap" }
tauri = { version = "2.0.0-rc.1", features = ["config-json5"] }
tauri-plugin-shell = "2.0.0-rc.0"
uuid = "1.10.0"

View File

@ -3,5 +3,5 @@
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": ["shell:allow-open"]
"permissions": []
}

View File

@ -3,11 +3,15 @@ use std::sync::Arc;
use once_cell::sync::OnceCell;
use swap::{
api::{
request::{get_balance, BalanceArgs, BalanceResponse},
request::{
get_balance, get_swap_info, get_swap_infos_all, BalanceArgs, BalanceResponse,
GetSwapInfoResponse,
},
Context,
},
cli::command::{Bitcoin, Monero},
};
use uuid::Uuid;
// Lazy load the Context
static CONTEXT: OnceCell<Arc<Context>> = OnceCell::new();
@ -26,6 +30,15 @@ async fn balance() -> Result<BalanceResponse, String> {
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn swap_infos_all() -> Result<Vec<GetSwapInfoResponse>, String> {
let context = CONTEXT.get().unwrap();
get_swap_infos_all(context.clone())
.await
.map_err(|e| e.to_string())
}
fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
tauri::async_runtime::block_on(async {
let context = Context::build(
@ -56,8 +69,7 @@ fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box<dyn std::error::Error>>
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![balance])
.invoke_handler(tauri::generate_handler![balance, swap_infos_all])
.setup(setup)
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -7,6 +7,7 @@ use crate::network::swarm;
use crate::protocol::bob::{BobState, Swap};
use crate::protocol::{bob, State};
use crate::{bitcoin, cli, monero, rpc};
use ::bitcoin::Txid;
use anyhow::{bail, Context as AnyContext, Result};
use libp2p::core::Multiaddr;
use qrcode::render::unicode;
@ -52,22 +53,6 @@ pub struct MoneroRecoveryArgs {
pub swap_id: Uuid,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ResumeSwapResponse {
pub result: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BalanceResponse {
pub balance: u64, // in satoshis
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BuyXmrResponse {
pub swap_id: String,
pub quote: BidQuote, // You'll need to import or define BidQuote
}
#[derive(Debug, Eq, PartialEq)]
pub struct WithdrawBtcArgs {
pub amount: Option<Amount>,
@ -94,6 +79,52 @@ pub struct GetSwapInfoArgs {
pub swap_id: Uuid,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ResumeSwapResponse {
pub result: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BalanceResponse {
pub balance: u64, // in satoshis
}
#[derive(Serialize, Deserialize, Debug)]
pub struct BuyXmrResponse {
pub swap_id: String,
pub quote: BidQuote, // You'll need to import or define BidQuote
}
#[derive(Serialize, Deserialize, Debug)]
pub struct GetHistoryResponse {
swaps: Vec<(Uuid, String)>,
}
#[derive(Serialize)]
pub struct GetSwapInfoResponse {
pub swap_id: Uuid,
pub seller: Seller,
pub completed: bool,
pub start_date: String,
pub state_name: String,
pub xmr_amount: u64,
pub btc_amount: u64,
pub tx_lock_id: Txid,
pub tx_cancel_fee: u64,
pub tx_refund_fee: u64,
pub tx_lock_fee: u64,
pub btc_refund_address: String,
pub cancel_timelock: u32,
pub punish_timelock: u32,
pub timelock: Option<ExpiredTimelocks>,
}
#[derive(Serialize, Deserialize)]
pub struct Seller {
pub peer_id: String,
pub addresses: Vec<Multiaddr>,
}
// TODO: We probably dont even need this.
// We can just call the method directly from the RPC server, the CLI and the Tauri connector
#[derive(Debug, PartialEq)]
@ -231,8 +262,23 @@ async fn suspend_current_swap(context: Arc<Context>) -> Result<serde_json::Value
}
}
pub async fn get_swap_infos_all(context: Arc<Context>) -> Result<Vec<GetSwapInfoResponse>> {
let swap_ids = context.db.all().await?;
let mut swap_infos = Vec::new();
for (swap_id, _) in swap_ids {
let swap_info = get_swap_info(GetSwapInfoArgs { swap_id }, context.clone()).await?;
swap_infos.push(swap_info);
}
Ok(swap_infos)
}
// #[tracing::instrument(fields(method="get_swap_info", swap_id = args.swap_id), skip(context))]
async fn get_swap_info(args: GetSwapInfoArgs, context: Arc<Context>) -> Result<serde_json::Value> {
pub async fn get_swap_info(
args: GetSwapInfoArgs,
context: Arc<Context>,
) -> Result<GetSwapInfoResponse> {
let bitcoin_wallet = context
.bitcoin_wallet
.as_ref()
@ -311,38 +357,38 @@ async fn get_swap_info(args: GetSwapInfoArgs, context: Arc<Context>) -> Result<s
}
BobState::BtcLocked { state3: state, .. }
| BobState::XmrLockProofReceived { state, .. } => {
Some(state.expired_timelock(bitcoin_wallet).await)
Some(state.expired_timelock(bitcoin_wallet).await?)
}
BobState::XmrLocked(state) | BobState::EncSigSent(state) => {
Some(state.expired_timelock(bitcoin_wallet).await)
Some(state.expired_timelock(bitcoin_wallet).await?)
}
BobState::CancelTimelockExpired(state) | BobState::BtcCancelled(state) => {
Some(state.expired_timelock(bitcoin_wallet).await)
Some(state.expired_timelock(bitcoin_wallet).await?)
}
BobState::BtcPunished { .. } => Some(Ok(ExpiredTimelocks::Punish)),
BobState::BtcPunished { .. } => Some(ExpiredTimelocks::Punish),
BobState::BtcRefunded(_) | BobState::BtcRedeemed(_) | BobState::XmrRedeemed { .. } => None,
};
Ok(json!({
"swapId": args.swap_id,
"seller": {
"peerId": peerId.to_string(),
"addresses": addresses
Ok(GetSwapInfoResponse {
swap_id: args.swap_id,
seller: Seller {
peer_id: peerId.to_string(),
addresses,
},
"completed": is_completed,
"startDate": start_date,
"stateName": state_name,
"xmrAmount": xmr_amount,
"btcAmount": btc_amount,
"txLockId": tx_lock_id,
"txCancelFee": tx_cancel_fee,
"txRefundFee": tx_refund_fee,
"txLockFee": tx_lock_fee,
"btcRefundAddress": btc_refund_address.to_string(),
"cancelTimelock": cancel_timelock,
"punishTimelock": punish_timelock,
"timelock": timelock.map(|tl| tl.map(|tl| json!(tl)).unwrap_or(json!(null))).unwrap_or(json!(null)),
}))
completed: is_completed,
start_date,
state_name,
xmr_amount: xmr_amount.as_piconero(),
btc_amount,
tx_lock_id,
tx_cancel_fee,
tx_refund_fee,
tx_lock_fee,
btc_refund_address: btc_refund_address.to_string(),
cancel_timelock: cancel_timelock.into(),
punish_timelock: punish_timelock.into(),
timelock,
})
}
async fn buy_xmr(buy_xmr: BuyXmrArgs, context: Arc<Context>) -> Result<serde_json::Value> {
@ -653,7 +699,7 @@ async fn cancel_and_refund(
})
}
async fn get_history(context: Arc<Context>) -> Result<serde_json::Value> {
async fn get_history(context: Arc<Context>) -> Result<GetHistoryResponse> {
let swaps = context.db.all().await?;
let mut vec: Vec<(Uuid, String)> = Vec::new();
for (swap_id, state) in swaps {
@ -661,7 +707,7 @@ async fn get_history(context: Arc<Context>) -> Result<serde_json::Value> {
vec.push((swap_id, state.to_string()));
}
Ok(json!({ "swaps": vec }))
Ok(GetHistoryResponse { swaps: vec })
}
async fn get_raw_states(context: Arc<Context>) -> Result<serde_json::Value> {