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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 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]] [[package]]
name = "enum-as-inner" name = "enum-as-inner"
version = "0.3.4" version = "0.3.4"
@ -2763,25 +2754,6 @@ dependencies = [
"nom", "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]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@ -4089,17 +4061,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" 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]] [[package]]
name = "open-metrics-client" name = "open-metrics-client"
version = "0.14.0" version = "0.14.0"
@ -4135,16 +4096,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 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]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -5932,16 +5883,6 @@ dependencies = [
"lazy_static", "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]] [[package]]
name = "shell-words" name = "shell-words"
version = "1.1.0" version = "1.1.0"
@ -6729,44 +6670,6 @@ dependencies = [
"tauri-utils", "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]] [[package]]
name = "tauri-runtime" name = "tauri-runtime"
version = "2.0.0-rc.1" version = "2.0.0-rc.1"
@ -7683,7 +7586,7 @@ dependencies = [
"swap", "swap",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-shell", "uuid",
] ]
[[package]] [[package]]
@ -7731,9 +7634,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.9.1" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [ dependencies = [
"getrandom 0.2.15", "getrandom 0.2.15",
"serde", "serde",

View File

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

View File

@ -1,97 +1,103 @@
import { makeStyles } from '@material-ui/core'; import { makeStyles } from "@material-ui/core";
import { Alert, AlertTitle } from '@material-ui/lab'; import { Alert, AlertTitle } from "@material-ui/lab";
import { useActiveSwapInfo } from 'store/hooks'; import { useActiveSwapInfo } from "store/hooks";
import { import {
isSwapTimelockInfoCancelled, isSwapTimelockInfoCancelled,
isSwapTimelockInfoNone, isSwapTimelockInfoNone,
} from 'models/rpcModel'; } from "models/rpcModel";
import HumanizedBitcoinBlockDuration from '../other/HumanizedBitcoinBlockDuration'; import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {
marginBottom: theme.spacing(1), marginBottom: theme.spacing(1),
}, },
list: { list: {
margin: theme.spacing(0.25), margin: theme.spacing(0.25),
}, },
})); }));
export default function SwapMightBeCancelledAlert({ export default function SwapMightBeCancelledAlert({
bobBtcLockTxConfirmations, bobBtcLockTxConfirmations,
}: { }: {
bobBtcLockTxConfirmations: number; bobBtcLockTxConfirmations: number;
}) { }) {
const classes = useStyles(); const classes = useStyles();
const swap = useActiveSwapInfo(); const swap = useActiveSwapInfo();
if ( if (
bobBtcLockTxConfirmations < 5 || bobBtcLockTxConfirmations < 5 ||
swap === null || swap === null ||
swap.timelock === null swap.timelock === null
) { ) {
return <></>; return <></>;
} }
const { timelock } = swap; const { timelock } = swap;
const punishTimelockOffset = swap.punishTimelock; const punishTimelockOffset = swap.punish_timelock;
return ( return (
<Alert severity="warning" className={classes.outer} variant="filled"> <Alert severity="warning" className={classes.outer} variant="filled">
<AlertTitle>Be careful!</AlertTitle> <AlertTitle>Be careful!</AlertTitle>
The swap provider has taken a long time to lock their Monero. This might The swap provider has taken a long time to lock their Monero. This
mean that: might mean that:
<ul className={classes.list}> <ul className={classes.list}>
<li> <li>
There is a technical issue that prevents them from locking their funds There is a technical issue that prevents them from locking
</li> their funds
<li>They are a malicious actor (unlikely)</li> </li>
</ul> <li>They are a malicious actor (unlikely)</li>
<br /> </ul>
There is still hope for the swap to be successful but you have to be extra <br />
careful. Regardless of why it has taken them so long, it is important that There is still hope for the swap to be successful but you have to be
you refund the swap within the required time period if the swap is not extra careful. Regardless of why it has taken them so long, it is
completed. If you fail to to do so, you will be punished and lose your important that you refund the swap within the required time period
money. if the swap is not completed. If you fail to to do so, you will be
<ul className={classes.list}> punished and lose your money.
{isSwapTimelockInfoNone(timelock) && ( <ul className={classes.list}>
<> {isSwapTimelockInfoNone(timelock) && (
<li> <>
<strong> <li>
You will be able to refund in about{' '} <strong>
<HumanizedBitcoinBlockDuration You will be able to refund in about{" "}
blocks={timelock.None.blocks_left} <HumanizedBitcoinBlockDuration
/> blocks={timelock.None.blocks_left}
</strong> />
</li> </strong>
</li>
<li> <li>
<strong> <strong>
If you have not refunded or completed the swap in about{' '} If you have not refunded or completed the swap
<HumanizedBitcoinBlockDuration in about{" "}
blocks={timelock.None.blocks_left + punishTimelockOffset} <HumanizedBitcoinBlockDuration
/> blocks={
, you will lose your funds. timelock.None.blocks_left +
</strong> punishTimelockOffset
</li> }
</> />
)} , you will lose your funds.
{isSwapTimelockInfoCancelled(timelock) && ( </strong>
<li> </li>
<strong> </>
If you have not refunded or completed the swap in about{' '} )}
<HumanizedBitcoinBlockDuration {isSwapTimelockInfoCancelled(timelock) && (
blocks={timelock.Cancel.blocks_left} <li>
/> <strong>
, you will lose your funds. If you have not refunded or completed the swap in
</strong> about{" "}
</li> <HumanizedBitcoinBlockDuration
)} blocks={timelock.Cancel.blocks_left}
<li> />
As long as you see this screen, the swap will be refunded , you will lose your funds.
automatically when the time comes. If this fails, you have to manually </strong>
refund by navigating to the History page. </li>
</li> )}
</ul> <li>
</Alert> 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 { Alert, AlertTitle } from "@material-ui/lab/";
import { Box, makeStyles } from '@material-ui/core'; import { Box, makeStyles } from "@material-ui/core";
import { ReactNode } from 'react'; import { ReactNode } from "react";
import { exhaustiveGuard } from 'utils/typescriptUtils'; import { exhaustiveGuard } from "utils/typescriptUtils";
import { import {
SwapCancelRefundButton, SwapCancelRefundButton,
SwapResumeButton, SwapResumeButton,
} from '../pages/history/table/HistoryRowActions'; } from "../pages/history/table/HistoryRowActions";
import HumanizedBitcoinBlockDuration from '../other/HumanizedBitcoinBlockDuration'; import HumanizedBitcoinBlockDuration from "../other/HumanizedBitcoinBlockDuration";
import { import {
GetSwapInfoResponse, GetSwapInfoResponse,
GetSwapInfoResponseRunningSwap, GetSwapInfoResponseRunningSwap,
isGetSwapInfoResponseRunningSwap, isGetSwapInfoResponseRunningSwap,
isSwapTimelockInfoCancelled, isSwapTimelockInfoCancelled,
isSwapTimelockInfoNone, isSwapTimelockInfoNone,
isSwapTimelockInfoPunished, isSwapTimelockInfoPunished,
SwapStateName, SwapStateName,
SwapTimelockInfoCancelled, SwapTimelockInfoCancelled,
SwapTimelockInfoNone, SwapTimelockInfoNone,
} from '../../../models/rpcModel'; } from "../../../models/rpcModel";
import { SwapMoneroRecoveryButton } from '../pages/history/table/SwapMoneroRecoveryButton'; import { SwapMoneroRecoveryButton } from "../pages/history/table/SwapMoneroRecoveryButton";
const useStyles = makeStyles({ const useStyles = makeStyles({
box: { box: {
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: '0.5rem', gap: "0.5rem",
}, },
list: { list: {
padding: '0px', padding: "0px",
margin: '0px', margin: "0px",
}, },
}); });
/** /**
@ -38,15 +38,15 @@ const useStyles = makeStyles({
* @returns JSX.Element * @returns JSX.Element
*/ */
const MessageList = ({ messages }: { messages: ReactNode[] }) => { const MessageList = ({ messages }: { messages: ReactNode[] }) => {
const classes = useStyles(); const classes = useStyles();
return ( return (
<ul className={classes.list}> <ul className={classes.list}>
{messages.map((msg, i) => ( {messages.map((msg, i) => (
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
<li key={i}>{msg}</li> <li key={i}>{msg}</li>
))} ))}
</ul> </ul>
); );
}; };
/** /**
@ -55,20 +55,24 @@ const MessageList = ({ messages }: { messages: ReactNode[] }) => {
* @returns JSX.Element * @returns JSX.Element
*/ */
const BitcoinRedeemedStateAlert = ({ swap }: { swap: GetSwapInfoResponse }) => { const BitcoinRedeemedStateAlert = ({ swap }: { swap: GetSwapInfoResponse }) => {
const classes = useStyles(); const classes = useStyles();
return ( return (
<Box className={classes.box}> <Box className={classes.box}>
<MessageList <MessageList
messages={[ messages={[
'The Bitcoin has been redeemed by the other party', "The Bitcoin has been redeemed by the other party",
'There is no risk of losing funds. You can take your time', "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', "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', "If this step fails, you can manually redeem the funds",
]} ]}
/> />
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" /> <SwapMoneroRecoveryButton
</Box> swap={swap}
); size="small"
variant="contained"
/>
</Box>
);
}; };
/** /**
@ -78,28 +82,31 @@ const BitcoinRedeemedStateAlert = ({ swap }: { swap: GetSwapInfoResponse }) => {
* @returns JSX.Element * @returns JSX.Element
*/ */
const BitcoinLockedNoTimelockExpiredStateAlert = ({ const BitcoinLockedNoTimelockExpiredStateAlert = ({
timelock, timelock,
punishTimelockOffset, punishTimelockOffset,
}: { }: {
timelock: SwapTimelockInfoNone; timelock: SwapTimelockInfoNone;
punishTimelockOffset: number; punishTimelockOffset: number;
}) => ( }) => (
<MessageList <MessageList
messages={[ messages={[
<> <>
Your Bitcoin is locked. If the swap is not completed in approximately{' '} Your Bitcoin is locked. If the swap is not completed in
<HumanizedBitcoinBlockDuration blocks={timelock.None.blocks_left} />, approximately{" "}
you need to refund <HumanizedBitcoinBlockDuration
</>, blocks={timelock.None.blocks_left}
<> />
You will lose your funds if you do not refund or complete the swap , you need to refund
within{' '} </>,
<HumanizedBitcoinBlockDuration <>
blocks={timelock.None.blocks_left + punishTimelockOffset} 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 * @returns JSX.Element
*/ */
const BitcoinPossiblyCancelledAlert = ({ const BitcoinPossiblyCancelledAlert = ({
swap, swap,
timelock, timelock,
}: { }: {
swap: GetSwapInfoResponse; swap: GetSwapInfoResponse;
timelock: SwapTimelockInfoCancelled; timelock: SwapTimelockInfoCancelled;
}) => { }) => {
const classes = useStyles(); const classes = useStyles();
return ( return (
<Box className={classes.box}> <Box className={classes.box}>
<MessageList <MessageList
messages={[ messages={[
'The swap was cancelled because it did not complete in time', "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 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{' '} You will lose your funds if you do not refund within{" "}
<HumanizedBitcoinBlockDuration <HumanizedBitcoinBlockDuration
blocks={timelock.Cancel.blocks_left} blocks={timelock.Cancel.blocks_left}
/>
</>,
]}
/> />
</>, <SwapCancelRefundButton
]} swap={swap}
/> size="small"
<SwapCancelRefundButton swap={swap} size="small" variant="contained" /> variant="contained"
</Box> />
); </Box>
);
}; };
/** /**
@ -141,7 +152,7 @@ const BitcoinPossiblyCancelledAlert = ({
* @returns JSX.Element * @returns JSX.Element
*/ */
const ImmediateActionAlert = () => ( 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 * @returns JSX.Element | null
*/ */
function SwapAlertStatusText({ function SwapAlertStatusText({
swap, swap,
}: { }: {
swap: GetSwapInfoResponseRunningSwap; swap: GetSwapInfoResponseRunningSwap;
}) { }) {
switch (swap.stateName) { switch (swap.state_name) {
// This is the state where the swap is safe because the other party has redeemed the Bitcoin // This is the state where the swap is safe because the other party has redeemed the Bitcoin
// It cannot be punished anymore // It cannot be punished anymore
case SwapStateName.BtcRedeemed: case SwapStateName.BtcRedeemed:
return <BitcoinRedeemedStateAlert swap={swap} />; return <BitcoinRedeemedStateAlert swap={swap} />;
// These are states that are at risk of punishment because the Bitcoin have been locked // 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 // but has not been redeemed yet by the other party
case SwapStateName.BtcLocked: case SwapStateName.BtcLocked:
case SwapStateName.XmrLockProofReceived: case SwapStateName.XmrLockProofReceived:
case SwapStateName.XmrLocked: case SwapStateName.XmrLocked:
case SwapStateName.EncSigSent: case SwapStateName.EncSigSent:
case SwapStateName.CancelTimelockExpired: case SwapStateName.CancelTimelockExpired:
case SwapStateName.BtcCancelled: case SwapStateName.BtcCancelled:
if (swap.timelock !== null) { if (swap.timelock !== null) {
if (isSwapTimelockInfoNone(swap.timelock)) { if (isSwapTimelockInfoNone(swap.timelock)) {
return ( return (
<BitcoinLockedNoTimelockExpiredStateAlert <BitcoinLockedNoTimelockExpiredStateAlert
punishTimelockOffset={swap.punishTimelock} punishTimelockOffset={swap.punish_timelock}
timelock={swap.timelock} timelock={swap.timelock}
/> />
); );
} }
if (isSwapTimelockInfoCancelled(swap.timelock)) { if (isSwapTimelockInfoCancelled(swap.timelock)) {
return ( return (
<BitcoinPossiblyCancelledAlert <BitcoinPossiblyCancelledAlert
timelock={swap.timelock} timelock={swap.timelock}
swap={swap} swap={swap}
/> />
); );
} }
if (isSwapTimelockInfoPunished(swap.timelock)) { if (isSwapTimelockInfoPunished(swap.timelock)) {
return <ImmediateActionAlert />; return <ImmediateActionAlert />;
} }
// We have covered all possible timelock states above // We have covered all possible timelock states above
// If we reach this point, it means we have missed a case // If we reach this point, it means we have missed a case
return exhaustiveGuard(swap.timelock); return exhaustiveGuard(swap.timelock);
} }
return <ImmediateActionAlert />; return <ImmediateActionAlert />;
default: default:
return exhaustiveGuard(swap.stateName); return exhaustiveGuard(swap.state_name);
} }
} }
/** /**
@ -207,27 +218,27 @@ function SwapAlertStatusText({
* @returns JSX.Element | null * @returns JSX.Element | null
*/ */
export default function SwapStatusAlert({ export default function SwapStatusAlert({
swap, swap,
}: { }: {
swap: GetSwapInfoResponse; swap: GetSwapInfoResponse;
}): JSX.Element | null { }): JSX.Element | null {
// If the swap is not running, there is no need to display the alert // 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) // 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)) { if (!isGetSwapInfoResponseRunningSwap(swap)) {
return null; return null;
} }
return ( return (
<Alert <Alert
key={swap.swapId} key={swap.swap_id}
severity="warning" severity="warning"
action={<SwapResumeButton swap={swap} />} action={<SwapResumeButton swap={swap} />}
variant="filled" variant="filled"
> >
<AlertTitle> <AlertTitle>
Swap {swap.swapId.substring(0, 5)}... is unfinished Swap {swap.swap_id.substring(0, 5)}... is unfinished
</AlertTitle> </AlertTitle>
<SwapAlertStatusText swap={swap} /> <SwapAlertStatusText swap={swap} />
</Alert> </Alert>
); );
} }

View File

@ -1,28 +1,28 @@
import { Box, makeStyles } from '@material-ui/core'; import { Box, makeStyles } from "@material-ui/core";
import { useSwapInfosSortedByDate } from 'store/hooks'; import { useSwapInfosSortedByDate } from "store/hooks";
import SwapStatusAlert from './SwapStatusAlert'; import SwapStatusAlert from "./SwapStatusAlert";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: theme.spacing(1), gap: theme.spacing(1),
}, },
})); }));
export default function SwapTxLockAlertsBox() { export default function SwapTxLockAlertsBox() {
const classes = useStyles(); const classes = useStyles();
// We specifically choose ALL swaps here // 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) // 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 // the SwapStatusAlert component will not render an Alert
const swaps = useSwapInfosSortedByDate(); const swaps = useSwapInfosSortedByDate();
return ( return (
<Box className={classes.outer}> <Box className={classes.outer}>
{swaps.map((swap) => ( {swaps.map((swap) => (
<SwapStatusAlert key={swap.swapId} swap={swap} /> <SwapStatusAlert key={swap.swap_id} swap={swap} />
))} ))}
</Box> </Box>
); );
} }

View File

@ -1,47 +1,44 @@
import { import {
Box, Box,
Button, Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
MenuItem, MenuItem,
Select, Select,
TextField, TextField,
} from '@material-ui/core'; } from "@material-ui/core";
import { useState } from 'react'; import { useState } from "react";
import { useSnackbar } from 'notistack'; import { useSnackbar } from "notistack";
import { import { useActiveSwapInfo, useAppSelector } from "store/hooks";
useActiveSwapInfo, import { parseDateString } from "utils/parseUtils";
useAppSelector, import { store } from "renderer/store/storeRenderer";
} from 'store/hooks'; import { CliLog } from "models/cliModel";
import { parseDateString } from 'utils/parseUtils'; import { submitFeedbackViaHttp } from "../../../api";
import { store } from 'renderer/store/storeRenderer'; import { PiconeroAmount } from "../../other/Units";
import { CliLog } from 'models/cliModel'; import LoadingButton from "../../other/LoadingButton";
import { submitFeedbackViaHttp } from '../../../api';
import { PiconeroAmount } from '../../other/Units';
import LoadingButton from '../../other/LoadingButton';
async function submitFeedback(body: string, swapId: string | number) { async function submitFeedback(body: string, swapId: string | number) {
let attachedBody = ''; let attachedBody = "";
if (swapId !== 0 && typeof swapId === 'string') { if (swapId !== 0 && typeof swapId === "string") {
const swapInfo = store.getState().rpc.state.swapInfos[swapId]; const swapInfo = store.getState().rpc.state.swapInfos[swapId];
const logs = [] as CliLog[]; const logs = [] as CliLog[];
throw new Error('Not implemented'); throw new Error("Not implemented");
if (swapInfo === undefined) { if (swapInfo === undefined) {
throw new Error(`Swap with id ${swapId} not found`); 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 await submitFeedbackViaHttp(body, attachedBody);
.map((l) => JSON.stringify(l))
.join('\n====\n')}`;
}
await submitFeedbackViaHttp(body, attachedBody);
} }
/* /*
@ -51,120 +48,136 @@ async function submitFeedback(body: string, swapId: string | number) {
* selectedSwap = 0 means no swap is attached * selectedSwap = 0 means no swap is attached
*/ */
function SwapSelectDropDown({ function SwapSelectDropDown({
selectedSwap, selectedSwap,
setSelectedSwap, setSelectedSwap,
}: { }: {
selectedSwap: string | number; selectedSwap: string | number;
setSelectedSwap: (swapId: string | number) => void; setSelectedSwap: (swapId: string | number) => void;
}) { }) {
const swaps = useAppSelector((state) => const swaps = useAppSelector((state) =>
Object.values(state.rpc.state.swapInfos), Object.values(state.rpc.state.swapInfos),
); );
return ( return (
<Select <Select
value={selectedSwap} value={selectedSwap}
label="Attach logs" label="Attach logs"
variant="outlined" variant="outlined"
onChange={(e) => setSelectedSwap(e.target.value as string)} onChange={(e) => setSelectedSwap(e.target.value as string)}
> >
<MenuItem value={0}>Do not attach logs</MenuItem> <MenuItem value={0}>Do not attach logs</MenuItem>
{swaps.map((swap) => ( {swaps.map((swap) => (
<MenuItem value={swap.swapId}> <MenuItem value={swap.swap_id}>
Swap {swap.swapId.substring(0, 5)}... from{' '} Swap {swap.swap_id.substring(0, 5)}... from{" "}
{new Date(parseDateString(swap.startDate)).toDateString()} ( {new Date(parseDateString(swap.start_date)).toDateString()}{" "}
<PiconeroAmount amount={swap.xmrAmount} />) (
</MenuItem> <PiconeroAmount amount={swap.xmr_amount} />)
))} </MenuItem>
</Select> ))}
); </Select>
);
} }
const MAX_FEEDBACK_LENGTH = 4000; const MAX_FEEDBACK_LENGTH = 4000;
export default function FeedbackDialog({ export default function FeedbackDialog({
open, open,
onClose, onClose,
}: { }: {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
}) { }) {
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
const [bodyText, setBodyText] = useState(''); const [bodyText, setBodyText] = useState("");
const currentSwapId = useActiveSwapInfo(); const currentSwapId = useActiveSwapInfo();
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
const [selectedAttachedSwap, setSelectedAttachedSwap] = useState< const [selectedAttachedSwap, setSelectedAttachedSwap] = useState<
string | number string | number
>(currentSwapId?.swapId || 0); >(currentSwapId?.swap_id || 0);
const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH; const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH;
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose}>
<DialogTitle>Submit Feedback</DialogTitle> <DialogTitle>Submit Feedback</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> <DialogContentText>
Got something to say? Drop us a message below. If you had an issue Got something to say? Drop us a message below. If you had an
with a specific swap, select it from the dropdown to attach the logs. issue with a specific swap, select it from the dropdown to
It will help us figure out what went wrong. Hit that submit button attach the logs. It will help us figure out what went wrong.
when you are ready. We appreciate you taking the time to share your Hit that submit button when you are ready. We appreciate you
thoughts! taking the time to share your thoughts!
</DialogContentText> </DialogContentText>
<Box style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> <Box
<TextField style={{
variant="outlined" display: "flex",
value={bodyText} flexDirection: "column",
onChange={(e) => setBodyText(e.target.value)} gap: "1rem",
label={ }}
bodyTooLong >
? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})` <TextField
: 'Feedback' variant="outlined"
} value={bodyText}
multiline onChange={(e) => setBodyText(e.target.value)}
minRows={4} label={
maxRows={4} bodyTooLong
fullWidth ? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
error={bodyTooLong} : "Feedback"
/> }
<SwapSelectDropDown multiline
selectedSwap={selectedAttachedSwap} minRows={4}
setSelectedSwap={setSelectedAttachedSwap} maxRows={4}
/> fullWidth
</Box> error={bodyTooLong}
</DialogContent> />
<DialogActions> <SwapSelectDropDown
<Button onClick={onClose}>Cancel</Button> selectedSwap={selectedAttachedSwap}
<LoadingButton setSelectedSwap={setSelectedAttachedSwap}
color="primary" />
variant="contained" </Box>
onClick={async () => { </DialogContent>
if (pending) { <DialogActions>
return; <Button onClick={onClose}>Cancel</Button>
} <LoadingButton
color="primary"
variant="contained"
onClick={async () => {
if (pending) {
return;
}
try { try {
setPending(true); setPending(true);
await submitFeedback(bodyText, selectedAttachedSwap); await submitFeedback(
enqueueSnackbar('Feedback submitted successfully!', { bodyText,
variant: 'success', selectedAttachedSwap,
}); );
} catch (e) { enqueueSnackbar(
console.error(`Failed to submit feedback: ${e}`); "Feedback submitted successfully!",
enqueueSnackbar(`Failed to submit feedback (${e})`, { {
variant: 'error', variant: "success",
}); },
} finally { );
setPending(false); } catch (e) {
} console.error(`Failed to submit feedback: ${e}`);
onClose(); enqueueSnackbar(
}} `Failed to submit feedback (${e})`,
loading={pending} {
> variant: "error",
Submit },
</LoadingButton> );
</DialogActions> } finally {
</Dialog> 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 { Step, StepLabel, Stepper, Typography } from "@material-ui/core";
import { SwapSpawnType } from 'models/cliModel'; import { SwapSpawnType } from "models/cliModel";
import { SwapStateName } from 'models/rpcModel'; import { SwapStateName } from "models/rpcModel";
import { useActiveSwapInfo, useAppSelector } from 'store/hooks'; import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { exhaustiveGuard } from 'utils/typescriptUtils'; import { exhaustiveGuard } from "utils/typescriptUtils";
export enum PathType { export enum PathType {
HAPPY_PATH = 'happy path', HAPPY_PATH = "happy path",
UNHAPPY_PATH = 'unhappy path', UNHAPPY_PATH = "unhappy path",
} }
function getActiveStep( function getActiveStep(
stateName: SwapStateName | null, stateName: SwapStateName | null,
processExited: boolean, processExited: boolean,
): [PathType, number, boolean] { ): [PathType, number, boolean] {
switch (stateName) { switch (stateName) {
/// // Happy Path /// // Happy Path
// Step: 0 (Waiting for Bitcoin lock tx to be published) // Step: 0 (Waiting for Bitcoin lock tx to be published)
case null: case null:
return [PathType.HAPPY_PATH, 0, false]; return [PathType.HAPPY_PATH, 0, false];
case SwapStateName.Started: case SwapStateName.Started:
case SwapStateName.SwapSetupCompleted: case SwapStateName.SwapSetupCompleted:
return [PathType.HAPPY_PATH, 0, processExited]; return [PathType.HAPPY_PATH, 0, processExited];
// Step: 1 (Waiting for Bitcoin Lock confirmation and XMR Lock Publication) // 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 // We have locked the Bitcoin and are waiting for the other party to lock their XMR
case SwapStateName.BtcLocked: case SwapStateName.BtcLocked:
return [PathType.HAPPY_PATH, 1, processExited]; return [PathType.HAPPY_PATH, 1, processExited];
// Step: 2 (Waiting for XMR Lock confirmation) // Step: 2 (Waiting for XMR Lock confirmation)
// We have locked the Bitcoin and the other party has locked their XMR // We have locked the Bitcoin and the other party has locked their XMR
case SwapStateName.XmrLockProofReceived: case SwapStateName.XmrLockProofReceived:
return [PathType.HAPPY_PATH, 1, processExited]; return [PathType.HAPPY_PATH, 1, processExited];
// Step: 3 (Sending Encrypted Signature and waiting for Bitcoin Redemption) // Step: 3 (Sending Encrypted Signature and waiting for Bitcoin Redemption)
// The XMR lock transaction has been confirmed // 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 // 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.XmrLocked:
case SwapStateName.EncSigSent: case SwapStateName.EncSigSent:
return [PathType.HAPPY_PATH, 2, processExited]; return [PathType.HAPPY_PATH, 2, processExited];
// Step: 4 (Waiting for XMR Redemption) // Step: 4 (Waiting for XMR Redemption)
case SwapStateName.BtcRedeemed: case SwapStateName.BtcRedeemed:
return [PathType.HAPPY_PATH, 3, processExited]; return [PathType.HAPPY_PATH, 3, processExited];
// Step: 4 (Completed) (Swap completed, XMR redeemed) // Step: 4 (Completed) (Swap completed, XMR redeemed)
case SwapStateName.XmrRedeemed: case SwapStateName.XmrRedeemed:
return [PathType.HAPPY_PATH, 4, false]; return [PathType.HAPPY_PATH, 4, false];
// Edge Case of Happy Path where the swap is safely aborted. We "fail" at the first step. // Edge Case of Happy Path where the swap is safely aborted. We "fail" at the first step.
case SwapStateName.SafelyAborted: case SwapStateName.SafelyAborted:
return [PathType.HAPPY_PATH, 0, true]; return [PathType.HAPPY_PATH, 0, true];
// // Unhappy Path // // Unhappy Path
// Step: 1 (Cancelling swap, checking if cancel transaction has been published already by the other party) // Step: 1 (Cancelling swap, checking if cancel transaction has been published already by the other party)
case SwapStateName.CancelTimelockExpired: case SwapStateName.CancelTimelockExpired:
return [PathType.UNHAPPY_PATH, 0, processExited]; return [PathType.UNHAPPY_PATH, 0, processExited];
// Step: 2 (Attempt to publish the Bitcoin refund transaction) // Step: 2 (Attempt to publish the Bitcoin refund transaction)
case SwapStateName.BtcCancelled: case SwapStateName.BtcCancelled:
return [PathType.UNHAPPY_PATH, 1, processExited]; return [PathType.UNHAPPY_PATH, 1, processExited];
// Step: 2 (Completed) (Bitcoin refunded) // Step: 2 (Completed) (Bitcoin refunded)
case SwapStateName.BtcRefunded: case SwapStateName.BtcRefunded:
return [PathType.UNHAPPY_PATH, 2, false]; return [PathType.UNHAPPY_PATH, 2, false];
// Step: 2 (We failed to publish the Bitcoin refund transaction) // Step: 2 (We failed to publish the Bitcoin refund transaction)
// We failed to publish the Bitcoin refund transaction because the timelock has expired. // We failed to publish the Bitcoin refund transaction because the timelock has expired.
// We will be punished. Nothing we can do about it now. // We will be punished. Nothing we can do about it now.
case SwapStateName.BtcPunished: case SwapStateName.BtcPunished:
return [PathType.UNHAPPY_PATH, 1, true]; return [PathType.UNHAPPY_PATH, 1, true];
default: default:
return exhaustiveGuard(stateName); return exhaustiveGuard(stateName);
} }
} }
function HappyPathStepper({ function HappyPathStepper({
activeStep, activeStep,
error, error,
}: { }: {
activeStep: number; activeStep: number;
error: boolean; error: boolean;
}) { }) {
return ( return (
<Stepper activeStep={activeStep}> <Stepper activeStep={activeStep}>
<Step key={0}> <Step key={0}>
<StepLabel <StepLabel
optional={<Typography variant="caption">~12min</Typography>} optional={<Typography variant="caption">~12min</Typography>}
error={error && activeStep === 0} error={error && activeStep === 0}
> >
Locking your BTC Locking your BTC
</StepLabel> </StepLabel>
</Step> </Step>
<Step key={1}> <Step key={1}>
<StepLabel <StepLabel
optional={<Typography variant="caption">~18min</Typography>} optional={<Typography variant="caption">~18min</Typography>}
error={error && activeStep === 1} error={error && activeStep === 1}
> >
They lock their XMR They lock their XMR
</StepLabel> </StepLabel>
</Step> </Step>
<Step key={2}> <Step key={2}>
<StepLabel <StepLabel
optional={<Typography variant="caption">~2min</Typography>} optional={<Typography variant="caption">~2min</Typography>}
error={error && activeStep === 2} error={error && activeStep === 2}
> >
They redeem the BTC They redeem the BTC
</StepLabel> </StepLabel>
</Step> </Step>
<Step key={3}> <Step key={3}>
<StepLabel <StepLabel
optional={<Typography variant="caption">~2min</Typography>} optional={<Typography variant="caption">~2min</Typography>}
error={error && activeStep === 3} error={error && activeStep === 3}
> >
Redeeming your XMR Redeeming your XMR
</StepLabel> </StepLabel>
</Step> </Step>
</Stepper> </Stepper>
); );
} }
function UnhappyPathStepper({ function UnhappyPathStepper({
activeStep, activeStep,
error, error,
}: { }: {
activeStep: number; activeStep: number;
error: boolean; error: boolean;
}) { }) {
return ( return (
<Stepper activeStep={activeStep}> <Stepper activeStep={activeStep}>
<Step key={0}> <Step key={0}>
<StepLabel <StepLabel
optional={<Typography variant="caption">~20min</Typography>} optional={<Typography variant="caption">~20min</Typography>}
error={error && activeStep === 0} error={error && activeStep === 0}
> >
Cancelling swap Cancelling swap
</StepLabel> </StepLabel>
</Step> </Step>
<Step key={1}> <Step key={1}>
<StepLabel <StepLabel
optional={<Typography variant="caption">~20min</Typography>} optional={<Typography variant="caption">~20min</Typography>}
error={error && activeStep === 1} error={error && activeStep === 1}
> >
Refunding your BTC Refunding your BTC
</StepLabel> </StepLabel>
</Step> </Step>
</Stepper> </Stepper>
); );
} }
export default function SwapStateStepper() { export default function SwapStateStepper() {
const currentSwapSpawnType = useAppSelector((s) => s.swap.spawnType); const currentSwapSpawnType = useAppSelector((s) => s.swap.spawnType);
const stateName = useActiveSwapInfo()?.stateName ?? null; const stateName = useActiveSwapInfo()?.state_name ?? null;
const processExited = useAppSelector((s) => !s.swap.processRunning); const processExited = useAppSelector((s) => !s.swap.processRunning);
const [pathType, activeStep, error] = getActiveStep(stateName, processExited); 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 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) { if (currentSwapSpawnType === SwapSpawnType.CANCEL_REFUND) {
return <UnhappyPathStepper activeStep={0} error={error} />; return <UnhappyPathStepper activeStep={0} error={error} />;
} }
if (pathType === PathType.HAPPY_PATH) { if (pathType === PathType.HAPPY_PATH) {
return <HappyPathStepper activeStep={activeStep} error={error} />; return <HappyPathStepper activeStep={activeStep} error={error} />;
} }
return <UnhappyPathStepper activeStep={activeStep} error={error} />; return <UnhappyPathStepper activeStep={activeStep} error={error} />;
} }

View File

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

View File

@ -1,71 +1,71 @@
import { Box, DialogContentText } from '@material-ui/core'; import { Box, DialogContentText } from "@material-ui/core";
import { useActiveSwapInfo, useAppSelector } from 'store/hooks'; import { useActiveSwapInfo, useAppSelector } from "store/hooks";
import { SwapStateProcessExited } from 'models/storeModel'; import { SwapStateProcessExited } from "models/storeModel";
import CliLogsBox from '../../../../other/RenderedCliLog'; import CliLogsBox from "../../../../other/RenderedCliLog";
import { SwapSpawnType } from 'models/cliModel'; import { SwapSpawnType } from "models/cliModel";
export default function ProcessExitedAndNotDonePage({ export default function ProcessExitedAndNotDonePage({
state, state,
}: { }: {
state: SwapStateProcessExited; state: SwapStateProcessExited;
}) { }) {
const swap = useActiveSwapInfo(); const swap = useActiveSwapInfo();
const logs = useAppSelector((s) => s.swap.logs); const logs = useAppSelector((s) => s.swap.logs);
const spawnType = useAppSelector((s) => s.swap.spawnType); const spawnType = useAppSelector((s) => s.swap.spawnType);
function getText() { function getText() {
const isCancelRefund = spawnType === SwapSpawnType.CANCEL_REFUND; const isCancelRefund = spawnType === SwapSpawnType.CANCEL_REFUND;
const hasRpcError = state.rpcError != null; const hasRpcError = state.rpcError != null;
const hasSwap = swap != 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( 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>
return ( <Box
<Box> style={{
<DialogContentText>{getText()}</DialogContentText> display: "flex",
<Box flexDirection: "column",
style={{ gap: "0.5rem",
display: 'flex', }}
flexDirection: 'column', >
gap: '0.5rem', {state.rpcError && (
}} <CliLogsBox
> logs={[state.rpcError]}
{state.rpcError && ( label="Error returned by the Swap Daemon"
<CliLogsBox />
logs={[state.rpcError]} )}
label="Error returned by the Swap Daemon" <CliLogsBox logs={logs} label="Logs relevant to the swap" />
/> </Box>
)} </Box>
<CliLogsBox logs={logs} label="Logs relevant to the swap" /> );
</Box>
</Box>
);
} }

View File

@ -1,47 +1,47 @@
import { useActiveSwapInfo } from 'store/hooks'; import { useActiveSwapInfo } from "store/hooks";
import { SwapStateName } from 'models/rpcModel'; import { SwapStateName } from "models/rpcModel";
import { import {
isSwapStateBtcPunished, isSwapStateBtcPunished,
isSwapStateBtcRefunded, isSwapStateBtcRefunded,
isSwapStateXmrRedeemInMempool, isSwapStateXmrRedeemInMempool,
SwapStateProcessExited, SwapStateProcessExited,
} from '../../../../../../models/storeModel'; } from "../../../../../../models/storeModel";
import XmrRedeemInMempoolPage from '../done/XmrRedeemInMempoolPage'; import XmrRedeemInMempoolPage from "../done/XmrRedeemInMempoolPage";
import BitcoinPunishedPage from '../done/BitcoinPunishedPage'; import BitcoinPunishedPage from "../done/BitcoinPunishedPage";
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import SwapStatePage from '../SwapStatePage'; import SwapStatePage from "../SwapStatePage";
import BitcoinRefundedPage from '../done/BitcoinRefundedPage'; import BitcoinRefundedPage from "../done/BitcoinRefundedPage";
import ProcessExitedAndNotDonePage from './ProcessExitedAndNotDonePage'; import ProcessExitedAndNotDonePage from "./ProcessExitedAndNotDonePage";
type ProcessExitedPageProps = { type ProcessExitedPageProps = {
state: SwapStateProcessExited; state: SwapStateProcessExited;
}; };
export default function ProcessExitedPage({ state }: ProcessExitedPageProps) { 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 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 ( if (
isSwapStateXmrRedeemInMempool(state.prevState) || isSwapStateXmrRedeemInMempool(state.prevState) ||
isSwapStateBtcRefunded(state.prevState) || isSwapStateBtcRefunded(state.prevState) ||
isSwapStateBtcPunished(state.prevState) isSwapStateBtcPunished(state.prevState)
) { ) {
return <SwapStatePage swapState={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 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) {
if (swap.stateName === SwapStateName.XmrRedeemed) { if (swap.state_name === SwapStateName.XmrRedeemed) {
return <XmrRedeemInMempoolPage state={null} />; 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 // 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} />; return <ProcessExitedAndNotDonePage state={state} />;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,119 +1,120 @@
import { ButtonProps } from '@material-ui/core/Button/Button'; import { ButtonProps } from "@material-ui/core/Button/Button";
import { import {
Box, Box,
Button, Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogContentText, DialogContentText,
Link, Link,
} from '@material-ui/core'; } from "@material-ui/core";
import { useAppDispatch, useAppSelector } from 'store/hooks'; import { useAppDispatch, useAppSelector } from "store/hooks";
import { rpcResetMoneroRecoveryKeys } from 'store/features/rpcSlice'; import { rpcResetMoneroRecoveryKeys } from "store/features/rpcSlice";
import { import {
GetSwapInfoResponse, GetSwapInfoResponse,
isSwapMoneroRecoverable, isSwapMoneroRecoverable,
} from '../../../../../models/rpcModel'; } from "../../../../../models/rpcModel";
import IpcInvokeButton from '../../../IpcInvokeButton'; import IpcInvokeButton from "../../../IpcInvokeButton";
import DialogHeader from '../../../modal/DialogHeader'; import DialogHeader from "../../../modal/DialogHeader";
import ScrollablePaperTextBox from '../../../other/ScrollablePaperTextBox'; import ScrollablePaperTextBox from "../../../other/ScrollablePaperTextBox";
function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) { function MoneroRecoveryKeysDialog({ swap }: { swap: GetSwapInfoResponse }) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const keys = useAppSelector((s) => s.rpc.state.moneroRecovery); const keys = useAppSelector((s) => s.rpc.state.moneroRecovery);
function onClose() { function onClose() {
dispatch(rpcResetMoneroRecoveryKeys()); dispatch(rpcResetMoneroRecoveryKeys());
} }
if (keys === null || keys.swapId !== swap.swapId) { if (keys === null || keys.swapId !== swap.swap_id) {
return <></>; return <></>;
} }
return ( return (
<Dialog open onClose={onClose} maxWidth="sm" fullWidth> <Dialog open onClose={onClose} maxWidth="sm" fullWidth>
<DialogHeader <DialogHeader
title={`Recovery Keys for swap ${swap.swapId.substring(0, 5)}...`} title={`Recovery Keys for swap ${swap.swap_id.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]}
/> />
))} <DialogContent>
</Box> <DialogContentText>
</DialogContent> You can use the keys below to manually redeem the Monero
<DialogActions> funds from the multi-signature wallet.
<Button onClick={onClose} color="primary" variant="contained"> <ul>
Done <li>
</Button> This is useful if the swap daemon fails to redeem
</DialogActions> the funds itself
</Dialog> </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({ export function SwapMoneroRecoveryButton({
swap, swap,
...props ...props
}: { swap: GetSwapInfoResponse } & ButtonProps) { }: { swap: GetSwapInfoResponse } & ButtonProps) {
const isRecoverable = isSwapMoneroRecoverable(swap.stateName); const isRecoverable = isSwapMoneroRecoverable(swap.state_name);
if (!isRecoverable) { if (!isRecoverable) {
return <></>; return <></>;
} }
return ( return (
<> <>
<IpcInvokeButton <IpcInvokeButton
ipcChannel="spawn-monero-recovery" ipcChannel="spawn-monero-recovery"
ipcArgs={[swap.swapId]} ipcArgs={[swap.swap_id]}
requiresRpc requiresRpc
{...props} {...props}
> >
Display Monero Recovery Keys Display Monero Recovery Keys
</IpcInvokeButton> </IpcInvokeButton>
<MoneroRecoveryKeysDialog swap={swap} /> <MoneroRecoveryKeysDialog swap={swap} />
</> </>
); );
} }

View File

@ -1,7 +1,7 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { BalanceBitcoinResponse } from "models/rpcModel"; import { BalanceBitcoinResponse } from "models/rpcModel";
import { store } from "./store/storeRenderer"; import { store } from "./store/storeRenderer";
import { rpcSetBalance } from "store/features/rpcSlice"; import { rpcSetBalance, rpcSetSwapInfo } from "store/features/rpcSlice";
export async function checkBitcoinBalance() { export async function checkBitcoinBalance() {
// TODO: use tauri-bindgen here // TODO: use tauri-bindgen here
@ -13,6 +13,7 @@ export async function checkBitcoinBalance() {
} }
export async function getRawSwapInfos() { export async function getRawSwapInfos() {
const response = await invoke("swap_infos"); const response = await invoke("swap_infos_all");
console.log(response); 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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ExtendedProviderStatus, ProviderStatus } from 'models/apiModel'; import { ExtendedProviderStatus, ProviderStatus } from "models/apiModel";
import { MoneroWalletRpcUpdateState } from 'models/storeModel'; import { MoneroWalletRpcUpdateState } from "models/storeModel";
import { import {
GetSwapInfoResponse, GetSwapInfoResponse,
MoneroRecoveryResponse, MoneroRecoveryResponse,
RpcProcessStateType, RpcProcessStateType,
} from '../../models/rpcModel'; } from "../../models/rpcModel";
import { import {
CliLog, CliLog,
isCliLog, isCliLog,
@ -14,8 +14,8 @@ import {
isCliLogFinishedSyncingMoneroWallet, isCliLogFinishedSyncingMoneroWallet,
isCliLogStartedRpcServer, isCliLogStartedRpcServer,
isCliLogStartedSyncingMoneroWallet, isCliLogStartedSyncingMoneroWallet,
} from '../../models/cliModel'; } from "../../models/cliModel";
import { getLogsAndStringsFromRawFileString } from 'utils/parseUtils'; import { getLogsAndStringsFromRawFileString } from "utils/parseUtils";
type Process = type Process =
| { | {
@ -82,7 +82,7 @@ const initialState: RPCSlice = {
}; };
export const rpcSlice = createSlice({ export const rpcSlice = createSlice({
name: 'rpc', name: "rpc",
initialState, initialState,
reducers: { reducers: {
rpcAddLogs(slice, action: PayloadAction<(CliLog | string)[]>) { rpcAddLogs(slice, action: PayloadAction<(CliLog | string)[]>) {
@ -110,7 +110,7 @@ export const rpcSlice = createSlice({
downloadUrl: log.fields.download_url, downloadUrl: log.fields.download_url,
}; };
if (log.fields.progress === '100%') { if (log.fields.progress === "100%") {
slice.state.moneroWalletRpc.updateState = false; slice.state.moneroWalletRpc.updateState = false;
} }
} else if (isCliLogStartedSyncingMoneroWallet(log)) { } else if (isCliLogStartedSyncingMoneroWallet(log)) {
@ -169,7 +169,7 @@ export const rpcSlice = createSlice({
slice.state.withdrawTxId = null; slice.state.withdrawTxId = null;
}, },
rpcSetSwapInfo(slice, action: PayloadAction<GetSwapInfoResponse>) { 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>) { rpcSetEndpointBusy(slice, action: PayloadAction<string>) {
if (!slice.busyEndpoints.includes(action.payload)) { if (!slice.busyEndpoints.includes(action.payload)) {

View File

@ -1,7 +1,7 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { AppDispatch, RootState } from 'renderer/store/storeRenderer'; import type { AppDispatch, RootState } from "renderer/store/storeRenderer";
import { sortBy } from 'lodash'; import { sortBy } from "lodash";
import { parseDateString } from 'utils/parseUtils'; import { parseDateString } from "utils/parseUtils";
// Use throughout your app instead of plain `useDispatch` and `useSelector` // Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>(); export const useAppDispatch = () => useDispatch<AppDispatch>();
@ -22,7 +22,7 @@ export function useIsSwapRunning() {
export function useSwapInfo(swapId: string | null) { export function useSwapInfo(swapId: string | null) {
return useAppSelector((state) => 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); const swapInfos = useAppSelector((state) => state.rpc.state.swapInfos);
return sortBy( return sortBy(
Object.values(swapInfos), 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" serde_json = "1"
swap = { path = "../swap" } swap = { path = "../swap" }
tauri = { version = "2.0.0-rc.1", features = ["config-json5"] } 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", "identifier": "default",
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": ["main"],
"permissions": ["shell:allow-open"] "permissions": []
} }

View File

@ -3,11 +3,15 @@ use std::sync::Arc;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use swap::{ use swap::{
api::{ api::{
request::{get_balance, BalanceArgs, BalanceResponse}, request::{
get_balance, get_swap_info, get_swap_infos_all, BalanceArgs, BalanceResponse,
GetSwapInfoResponse,
},
Context, Context,
}, },
cli::command::{Bitcoin, Monero}, cli::command::{Bitcoin, Monero},
}; };
use uuid::Uuid;
// Lazy load the Context // Lazy load the Context
static CONTEXT: OnceCell<Arc<Context>> = OnceCell::new(); static CONTEXT: OnceCell<Arc<Context>> = OnceCell::new();
@ -26,6 +30,15 @@ async fn balance() -> Result<BalanceResponse, String> {
.map_err(|e| e.to_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>> { fn setup<'a>(app: &'a mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
tauri::async_runtime::block_on(async { tauri::async_runtime::block_on(async {
let context = Context::build( 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)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![balance, swap_infos_all])
.invoke_handler(tauri::generate_handler![balance])
.setup(setup) .setup(setup)
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .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::{BobState, Swap};
use crate::protocol::{bob, State}; use crate::protocol::{bob, State};
use crate::{bitcoin, cli, monero, rpc}; use crate::{bitcoin, cli, monero, rpc};
use ::bitcoin::Txid;
use anyhow::{bail, Context as AnyContext, Result}; use anyhow::{bail, Context as AnyContext, Result};
use libp2p::core::Multiaddr; use libp2p::core::Multiaddr;
use qrcode::render::unicode; use qrcode::render::unicode;
@ -52,22 +53,6 @@ pub struct MoneroRecoveryArgs {
pub swap_id: Uuid, 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)] #[derive(Debug, Eq, PartialEq)]
pub struct WithdrawBtcArgs { pub struct WithdrawBtcArgs {
pub amount: Option<Amount>, pub amount: Option<Amount>,
@ -94,6 +79,52 @@ pub struct GetSwapInfoArgs {
pub swap_id: Uuid, 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. // TODO: We probably dont even need this.
// We can just call the method directly from the RPC server, the CLI and the Tauri connector // We can just call the method directly from the RPC server, the CLI and the Tauri connector
#[derive(Debug, PartialEq)] #[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))] // #[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 let bitcoin_wallet = context
.bitcoin_wallet .bitcoin_wallet
.as_ref() .as_ref()
@ -311,38 +357,38 @@ async fn get_swap_info(args: GetSwapInfoArgs, context: Arc<Context>) -> Result<s
} }
BobState::BtcLocked { state3: state, .. } BobState::BtcLocked { state3: state, .. }
| BobState::XmrLockProofReceived { state, .. } => { | BobState::XmrLockProofReceived { state, .. } => {
Some(state.expired_timelock(bitcoin_wallet).await) Some(state.expired_timelock(bitcoin_wallet).await?)
} }
BobState::XmrLocked(state) | BobState::EncSigSent(state) => { 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) => { 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, BobState::BtcRefunded(_) | BobState::BtcRedeemed(_) | BobState::XmrRedeemed { .. } => None,
}; };
Ok(json!({ Ok(GetSwapInfoResponse {
"swapId": args.swap_id, swap_id: args.swap_id,
"seller": { seller: Seller {
"peerId": peerId.to_string(), peer_id: peerId.to_string(),
"addresses": addresses addresses,
}, },
"completed": is_completed, completed: is_completed,
"startDate": start_date, start_date,
"stateName": state_name, state_name,
"xmrAmount": xmr_amount, xmr_amount: xmr_amount.as_piconero(),
"btcAmount": btc_amount, btc_amount,
"txLockId": tx_lock_id, tx_lock_id,
"txCancelFee": tx_cancel_fee, tx_cancel_fee,
"txRefundFee": tx_refund_fee, tx_refund_fee,
"txLockFee": tx_lock_fee, tx_lock_fee,
"btcRefundAddress": btc_refund_address.to_string(), btc_refund_address: btc_refund_address.to_string(),
"cancelTimelock": cancel_timelock, cancel_timelock: cancel_timelock.into(),
"punishTimelock": punish_timelock, punish_timelock: punish_timelock.into(),
"timelock": timelock.map(|tl| tl.map(|tl| json!(tl)).unwrap_or(json!(null))).unwrap_or(json!(null)), timelock,
})) })
} }
async fn buy_xmr(buy_xmr: BuyXmrArgs, context: Arc<Context>) -> Result<serde_json::Value> { 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 swaps = context.db.all().await?;
let mut vec: Vec<(Uuid, String)> = Vec::new(); let mut vec: Vec<(Uuid, String)> = Vec::new();
for (swap_id, state) in swaps { 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())); 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> { async fn get_raw_states(context: Arc<Context>) -> Result<serde_json::Value> {