wip: WithdrawDialog migrated to Tauri IPC

This commit is contained in:
binarybaron 2024-08-09 19:46:58 +02:00
parent 47821cbe79
commit 3d16ff6d5c
No known key found for this signature in database
GPG key ID: 99B75D3E1476A26E
118 changed files with 1779 additions and 1868 deletions

View file

@ -24,5 +24,5 @@ export interface Alert {
id: number; id: number;
title: string; title: string;
body: string; body: string;
severity: 'info' | 'warning' | 'error'; severity: "info" | "warning" | "error";
} }

View file

@ -1,14 +1,14 @@
export enum SwapSpawnType { export enum SwapSpawnType {
INIT = 'init', INIT = "init",
RESUME = 'resume', RESUME = "resume",
CANCEL_REFUND = 'cancel-refund', CANCEL_REFUND = "cancel-refund",
} }
export type CliLogSpanType = string | 'BitcoinWalletSubscription'; export type CliLogSpanType = string | "BitcoinWalletSubscription";
export interface CliLog { export interface CliLog {
timestamp: string; timestamp: string;
level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'TRACE'; level: "DEBUG" | "INFO" | "WARN" | "ERROR" | "TRACE";
fields: { fields: {
message: string; message: string;
[index: string]: unknown; [index: string]: unknown;
@ -20,12 +20,12 @@ export interface CliLog {
} }
export function isCliLog(log: unknown): log is CliLog { export function isCliLog(log: unknown): log is CliLog {
if (log && typeof log === 'object') { if (log && typeof log === "object") {
return ( return (
'timestamp' in (log as CliLog) && "timestamp" in (log as CliLog) &&
'level' in (log as CliLog) && "level" in (log as CliLog) &&
'fields' in (log as CliLog) && "fields" in (log as CliLog) &&
typeof (log as CliLog).fields?.message === 'string' typeof (log as CliLog).fields?.message === "string"
); );
} }
return false; return false;
@ -33,7 +33,7 @@ export function isCliLog(log: unknown): log is CliLog {
export interface CliLogStartedRpcServer extends CliLog { export interface CliLogStartedRpcServer extends CliLog {
fields: { fields: {
message: 'Started RPC server'; message: "Started RPC server";
addr: string; addr: string;
}; };
} }
@ -41,12 +41,12 @@ export interface CliLogStartedRpcServer extends CliLog {
export function isCliLogStartedRpcServer( export function isCliLogStartedRpcServer(
log: CliLog, log: CliLog,
): log is CliLogStartedRpcServer { ): log is CliLogStartedRpcServer {
return log.fields.message === 'Started RPC server'; return log.fields.message === "Started RPC server";
} }
export interface CliLogReleasingSwapLockLog extends CliLog { export interface CliLogReleasingSwapLockLog extends CliLog {
fields: { fields: {
message: 'Releasing swap lock'; message: "Releasing swap lock";
swap_id: string; swap_id: string;
}; };
} }
@ -54,23 +54,23 @@ export interface CliLogReleasingSwapLockLog extends CliLog {
export function isCliLogReleasingSwapLockLog( export function isCliLogReleasingSwapLockLog(
log: CliLog, log: CliLog,
): log is CliLogReleasingSwapLockLog { ): log is CliLogReleasingSwapLockLog {
return log.fields.message === 'Releasing swap lock'; return log.fields.message === "Releasing swap lock";
} }
export interface CliLogApiCallError extends CliLog { export interface CliLogApiCallError extends CliLog {
fields: { fields: {
message: 'API call resulted in an error'; message: "API call resulted in an error";
err: string; err: string;
}; };
} }
export function isCliLogApiCallError(log: CliLog): log is CliLogApiCallError { export function isCliLogApiCallError(log: CliLog): log is CliLogApiCallError {
return log.fields.message === 'API call resulted in an error'; return log.fields.message === "API call resulted in an error";
} }
export interface CliLogAcquiringSwapLockLog extends CliLog { export interface CliLogAcquiringSwapLockLog extends CliLog {
fields: { fields: {
message: 'Acquiring swap lock'; message: "Acquiring swap lock";
swap_id: string; swap_id: string;
}; };
} }
@ -78,12 +78,12 @@ export interface CliLogAcquiringSwapLockLog extends CliLog {
export function isCliLogAcquiringSwapLockLog( export function isCliLogAcquiringSwapLockLog(
log: CliLog, log: CliLog,
): log is CliLogAcquiringSwapLockLog { ): log is CliLogAcquiringSwapLockLog {
return log.fields.message === 'Acquiring swap lock'; return log.fields.message === "Acquiring swap lock";
} }
export interface CliLogReceivedQuote extends CliLog { export interface CliLogReceivedQuote extends CliLog {
fields: { fields: {
message: 'Received quote'; message: "Received quote";
price: string; price: string;
minimum_amount: string; minimum_amount: string;
maximum_amount: string; maximum_amount: string;
@ -91,12 +91,12 @@ export interface CliLogReceivedQuote extends CliLog {
} }
export function isCliLogReceivedQuote(log: CliLog): log is CliLogReceivedQuote { export function isCliLogReceivedQuote(log: CliLog): log is CliLogReceivedQuote {
return log.fields.message === 'Received quote'; return log.fields.message === "Received quote";
} }
export interface CliLogWaitingForBtcDeposit extends CliLog { export interface CliLogWaitingForBtcDeposit extends CliLog {
fields: { fields: {
message: 'Waiting for Bitcoin deposit'; message: "Waiting for Bitcoin deposit";
deposit_address: string; deposit_address: string;
min_deposit_until_swap_will_start: string; min_deposit_until_swap_will_start: string;
max_deposit_until_maximum_amount_is_reached: string; max_deposit_until_maximum_amount_is_reached: string;
@ -111,24 +111,24 @@ export interface CliLogWaitingForBtcDeposit extends CliLog {
export function isCliLogWaitingForBtcDeposit( export function isCliLogWaitingForBtcDeposit(
log: CliLog, log: CliLog,
): log is CliLogWaitingForBtcDeposit { ): log is CliLogWaitingForBtcDeposit {
return log.fields.message === 'Waiting for Bitcoin deposit'; return log.fields.message === "Waiting for Bitcoin deposit";
} }
export interface CliLogReceivedBtc extends CliLog { export interface CliLogReceivedBtc extends CliLog {
fields: { fields: {
message: 'Received Bitcoin'; message: "Received Bitcoin";
max_giveable: string; max_giveable: string;
new_balance: string; new_balance: string;
}; };
} }
export function isCliLogReceivedBtc(log: CliLog): log is CliLogReceivedBtc { export function isCliLogReceivedBtc(log: CliLog): log is CliLogReceivedBtc {
return log.fields.message === 'Received Bitcoin'; return log.fields.message === "Received Bitcoin";
} }
export interface CliLogDeterminedSwapAmount extends CliLog { export interface CliLogDeterminedSwapAmount extends CliLog {
fields: { fields: {
message: 'Determined swap amount'; message: "Determined swap amount";
amount: string; amount: string;
fees: string; fees: string;
}; };
@ -137,49 +137,49 @@ export interface CliLogDeterminedSwapAmount extends CliLog {
export function isCliLogDeterminedSwapAmount( export function isCliLogDeterminedSwapAmount(
log: CliLog, log: CliLog,
): log is CliLogDeterminedSwapAmount { ): log is CliLogDeterminedSwapAmount {
return log.fields.message === 'Determined swap amount'; return log.fields.message === "Determined swap amount";
} }
export interface CliLogStartedSwap extends CliLog { export interface CliLogStartedSwap extends CliLog {
fields: { fields: {
message: 'Starting new swap'; message: "Starting new swap";
swap_id: string; swap_id: string;
}; };
} }
export function isCliLogStartedSwap(log: CliLog): log is CliLogStartedSwap { export function isCliLogStartedSwap(log: CliLog): log is CliLogStartedSwap {
return log.fields.message === 'Starting new swap'; return log.fields.message === "Starting new swap";
} }
export interface CliLogPublishedBtcTx extends CliLog { export interface CliLogPublishedBtcTx extends CliLog {
fields: { fields: {
message: 'Published Bitcoin transaction'; message: "Published Bitcoin transaction";
txid: string; txid: string;
kind: 'lock' | 'cancel' | 'withdraw' | 'refund'; kind: "lock" | "cancel" | "withdraw" | "refund";
}; };
} }
export function isCliLogPublishedBtcTx( export function isCliLogPublishedBtcTx(
log: CliLog, log: CliLog,
): log is CliLogPublishedBtcTx { ): log is CliLogPublishedBtcTx {
return log.fields.message === 'Published Bitcoin transaction'; return log.fields.message === "Published Bitcoin transaction";
} }
export interface CliLogBtcTxFound extends CliLog { export interface CliLogBtcTxFound extends CliLog {
fields: { fields: {
message: 'Found relevant Bitcoin transaction'; message: "Found relevant Bitcoin transaction";
txid: string; txid: string;
status: string; status: string;
}; };
} }
export function isCliLogBtcTxFound(log: CliLog): log is CliLogBtcTxFound { export function isCliLogBtcTxFound(log: CliLog): log is CliLogBtcTxFound {
return log.fields.message === 'Found relevant Bitcoin transaction'; return log.fields.message === "Found relevant Bitcoin transaction";
} }
export interface CliLogBtcTxStatusChanged extends CliLog { export interface CliLogBtcTxStatusChanged extends CliLog {
fields: { fields: {
message: 'Bitcoin transaction status changed'; message: "Bitcoin transaction status changed";
txid: string; txid: string;
new_status: string; new_status: string;
}; };
@ -188,12 +188,12 @@ export interface CliLogBtcTxStatusChanged extends CliLog {
export function isCliLogBtcTxStatusChanged( export function isCliLogBtcTxStatusChanged(
log: CliLog, log: CliLog,
): log is CliLogBtcTxStatusChanged { ): log is CliLogBtcTxStatusChanged {
return log.fields.message === 'Bitcoin transaction status changed'; return log.fields.message === "Bitcoin transaction status changed";
} }
export interface CliLogAliceLockedXmr extends CliLog { export interface CliLogAliceLockedXmr extends CliLog {
fields: { fields: {
message: 'Alice locked Monero'; message: "Alice locked Monero";
txid: string; txid: string;
}; };
} }
@ -201,12 +201,12 @@ export interface CliLogAliceLockedXmr extends CliLog {
export function isCliLogAliceLockedXmr( export function isCliLogAliceLockedXmr(
log: CliLog, log: CliLog,
): log is CliLogAliceLockedXmr { ): log is CliLogAliceLockedXmr {
return log.fields.message === 'Alice locked Monero'; return log.fields.message === "Alice locked Monero";
} }
export interface CliLogReceivedXmrLockTxConfirmation extends CliLog { export interface CliLogReceivedXmrLockTxConfirmation extends CliLog {
fields: { fields: {
message: 'Received new confirmation for Monero lock tx'; message: "Received new confirmation for Monero lock tx";
txid: string; txid: string;
seen_confirmations: string; seen_confirmations: string;
needed_confirmations: string; needed_confirmations: string;
@ -216,50 +216,50 @@ export interface CliLogReceivedXmrLockTxConfirmation extends CliLog {
export function isCliLogReceivedXmrLockTxConfirmation( export function isCliLogReceivedXmrLockTxConfirmation(
log: CliLog, log: CliLog,
): log is CliLogReceivedXmrLockTxConfirmation { ): log is CliLogReceivedXmrLockTxConfirmation {
return log.fields.message === 'Received new confirmation for Monero lock tx'; return log.fields.message === "Received new confirmation for Monero lock tx";
} }
export interface CliLogAdvancingState extends CliLog { export interface CliLogAdvancingState extends CliLog {
fields: { fields: {
message: 'Advancing state'; message: "Advancing state";
state: state:
| 'quote has been requested' | "quote has been requested"
| 'execution setup done' | "execution setup done"
| 'btc is locked' | "btc is locked"
| 'XMR lock transaction transfer proof received' | "XMR lock transaction transfer proof received"
| 'xmr is locked' | "xmr is locked"
| 'encrypted signature is sent' | "encrypted signature is sent"
| 'btc is redeemed' | "btc is redeemed"
| 'cancel timelock is expired' | "cancel timelock is expired"
| 'btc is cancelled' | "btc is cancelled"
| 'btc is refunded' | "btc is refunded"
| 'xmr is redeemed' | "xmr is redeemed"
| 'btc is punished' | "btc is punished"
| 'safely aborted'; | "safely aborted";
}; };
} }
export function isCliLogAdvancingState( export function isCliLogAdvancingState(
log: CliLog, log: CliLog,
): log is CliLogAdvancingState { ): log is CliLogAdvancingState {
return log.fields.message === 'Advancing state'; return log.fields.message === "Advancing state";
} }
export interface CliLogRedeemedXmr extends CliLog { export interface CliLogRedeemedXmr extends CliLog {
fields: { fields: {
message: 'Successfully transferred XMR to wallet'; message: "Successfully transferred XMR to wallet";
monero_receive_address: string; monero_receive_address: string;
txid: string; txid: string;
}; };
} }
export function isCliLogRedeemedXmr(log: CliLog): log is CliLogRedeemedXmr { export function isCliLogRedeemedXmr(log: CliLog): log is CliLogRedeemedXmr {
return log.fields.message === 'Successfully transferred XMR to wallet'; return log.fields.message === "Successfully transferred XMR to wallet";
} }
export interface YouHaveBeenPunishedCliLog extends CliLog { export interface YouHaveBeenPunishedCliLog extends CliLog {
fields: { fields: {
message: 'You have been punished for not refunding in time'; message: "You have been punished for not refunding in time";
}; };
} }
@ -267,7 +267,7 @@ export function isYouHaveBeenPunishedCliLog(
log: CliLog, log: CliLog,
): log is YouHaveBeenPunishedCliLog { ): log is YouHaveBeenPunishedCliLog {
return ( return (
log.fields.message === 'You have been punished for not refunding in time' log.fields.message === "You have been punished for not refunding in time"
); );
} }
@ -280,14 +280,14 @@ function getCliLogSpanAttribute<T>(log: CliLog, key: string): T | null {
} }
export function getCliLogSpanSwapId(log: CliLog): string | null { export function getCliLogSpanSwapId(log: CliLog): string | null {
return getCliLogSpanAttribute<string>(log, 'swap_id'); return getCliLogSpanAttribute<string>(log, "swap_id");
} }
export function getCliLogSpanLogReferenceId(log: CliLog): string | null { export function getCliLogSpanLogReferenceId(log: CliLog): string | null {
return ( return (
getCliLogSpanAttribute<string>(log, 'log_reference_id')?.replace( getCliLogSpanAttribute<string>(log, "log_reference_id")?.replace(
/"/g, /"/g,
'', "",
) || null ) || null
); );
} }
@ -301,7 +301,7 @@ export function hasCliLogOneOfMultipleSpans(
export interface CliLogStartedSyncingMoneroWallet extends CliLog { export interface CliLogStartedSyncingMoneroWallet extends CliLog {
fields: { fields: {
message: 'Syncing Monero wallet'; message: "Syncing Monero wallet";
current_sync_height?: boolean; current_sync_height?: boolean;
}; };
} }
@ -309,18 +309,18 @@ export interface CliLogStartedSyncingMoneroWallet extends CliLog {
export function isCliLogStartedSyncingMoneroWallet( export function isCliLogStartedSyncingMoneroWallet(
log: CliLog, log: CliLog,
): log is CliLogStartedSyncingMoneroWallet { ): log is CliLogStartedSyncingMoneroWallet {
return log.fields.message === 'Syncing Monero wallet'; return log.fields.message === "Syncing Monero wallet";
} }
export interface CliLogFinishedSyncingMoneroWallet extends CliLog { export interface CliLogFinishedSyncingMoneroWallet extends CliLog {
fields: { fields: {
message: 'Synced Monero wallet'; message: "Synced Monero wallet";
}; };
} }
export interface CliLogFailedToSyncMoneroWallet extends CliLog { export interface CliLogFailedToSyncMoneroWallet extends CliLog {
fields: { fields: {
message: 'Failed to sync Monero wallet'; message: "Failed to sync Monero wallet";
error: string; error: string;
}; };
} }
@ -328,18 +328,18 @@ export interface CliLogFailedToSyncMoneroWallet extends CliLog {
export function isCliLogFailedToSyncMoneroWallet( export function isCliLogFailedToSyncMoneroWallet(
log: CliLog, log: CliLog,
): log is CliLogFailedToSyncMoneroWallet { ): log is CliLogFailedToSyncMoneroWallet {
return log.fields.message === 'Failed to sync Monero wallet'; return log.fields.message === "Failed to sync Monero wallet";
} }
export function isCliLogFinishedSyncingMoneroWallet( export function isCliLogFinishedSyncingMoneroWallet(
log: CliLog, log: CliLog,
): log is CliLogFinishedSyncingMoneroWallet { ): log is CliLogFinishedSyncingMoneroWallet {
return log.fields.message === 'Monero wallet synced'; return log.fields.message === "Monero wallet synced";
} }
export interface CliLogDownloadingMoneroWalletRpc extends CliLog { export interface CliLogDownloadingMoneroWalletRpc extends CliLog {
fields: { fields: {
message: 'Downloading monero-wallet-rpc'; message: "Downloading monero-wallet-rpc";
progress: string; progress: string;
size: string; size: string;
download_url: string; download_url: string;
@ -349,19 +349,19 @@ export interface CliLogDownloadingMoneroWalletRpc extends CliLog {
export function isCliLogDownloadingMoneroWalletRpc( export function isCliLogDownloadingMoneroWalletRpc(
log: CliLog, log: CliLog,
): log is CliLogDownloadingMoneroWalletRpc { ): log is CliLogDownloadingMoneroWalletRpc {
return log.fields.message === 'Downloading monero-wallet-rpc'; return log.fields.message === "Downloading monero-wallet-rpc";
} }
export interface CliLogStartedSyncingMoneroWallet extends CliLog { export interface CliLogStartedSyncingMoneroWallet extends CliLog {
fields: { fields: {
message: 'Syncing Monero wallet'; message: "Syncing Monero wallet";
current_sync_height?: boolean; current_sync_height?: boolean;
}; };
} }
export interface CliLogDownloadingMoneroWalletRpc extends CliLog { export interface CliLogDownloadingMoneroWalletRpc extends CliLog {
fields: { fields: {
message: 'Downloading monero-wallet-rpc'; message: "Downloading monero-wallet-rpc";
progress: string; progress: string;
size: string; size: string;
download_url: string; download_url: string;
@ -370,7 +370,7 @@ export interface CliLogDownloadingMoneroWalletRpc extends CliLog {
export interface CliLogGotNotificationForNewBlock extends CliLog { export interface CliLogGotNotificationForNewBlock extends CliLog {
fields: { fields: {
message: 'Got notification for new block'; message: "Got notification for new block";
block_height: string; block_height: string;
}; };
} }
@ -378,29 +378,36 @@ export interface CliLogGotNotificationForNewBlock extends CliLog {
export function isCliLogGotNotificationForNewBlock( export function isCliLogGotNotificationForNewBlock(
log: CliLog, log: CliLog,
): log is CliLogGotNotificationForNewBlock { ): log is CliLogGotNotificationForNewBlock {
return log.fields.message === 'Got notification for new block'; return log.fields.message === "Got notification for new block";
} }
export interface CliLogAttemptingToCooperativelyRedeemXmr extends CliLog { export interface CliLogAttemptingToCooperativelyRedeemXmr extends CliLog {
fields: { fields: {
message: 'Attempting to cooperatively redeem XMR after being punished'; message: "Attempting to cooperatively redeem XMR after being punished";
}; };
} }
export function isCliLogAttemptingToCooperativelyRedeemXmr( export function isCliLogAttemptingToCooperativelyRedeemXmr(
log: CliLog, log: CliLog,
): log is CliLogAttemptingToCooperativelyRedeemXmr { ): log is CliLogAttemptingToCooperativelyRedeemXmr {
return log.fields.message === 'Attempting to cooperatively redeem XMR after being punished'; return (
log.fields.message ===
"Attempting to cooperatively redeem XMR after being punished"
);
} }
export interface CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr extends CliLog { export interface CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr
extends CliLog {
fields: { fields: {
message: 'Alice has accepted our request to cooperatively redeem the XMR'; message: "Alice has accepted our request to cooperatively redeem the XMR";
}; };
} }
export function isCliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr( export function isCliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr(
log: CliLog, log: CliLog,
): log is CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr { ): log is CliLogAliceHasAcceptedOurRequestToCooperativelyRedeemTheXmr {
return log.fields.message === 'Alice has accepted our request to cooperatively redeem the XMR'; return (
} log.fields.message ===
"Alice has accepted our request to cooperatively redeem the XMR"
);
}

View file

@ -1,5 +1,5 @@
import { CliLog, SwapSpawnType } from './cliModel'; import { CliLog, SwapSpawnType } from "./cliModel";
import { Provider } from './apiModel'; import { Provider } from "./apiModel";
export interface SwapSlice { export interface SwapSlice {
state: SwapState | null; state: SwapState | null;
@ -20,21 +20,21 @@ export interface SwapState {
} }
export enum SwapStateType { export enum SwapStateType {
INITIATED = 'initiated', INITIATED = "initiated",
RECEIVED_QUOTE = 'received quote', RECEIVED_QUOTE = "received quote",
WAITING_FOR_BTC_DEPOSIT = 'waiting for btc deposit', WAITING_FOR_BTC_DEPOSIT = "waiting for btc deposit",
STARTED = 'started', STARTED = "started",
BTC_LOCK_TX_IN_MEMPOOL = 'btc lock tx is in mempool', BTC_LOCK_TX_IN_MEMPOOL = "btc lock tx is in mempool",
XMR_LOCK_TX_IN_MEMPOOL = 'xmr lock tx is in mempool', XMR_LOCK_TX_IN_MEMPOOL = "xmr lock tx is in mempool",
XMR_LOCKED = 'xmr is locked', XMR_LOCKED = "xmr is locked",
BTC_REDEEMED = 'btc redeemed', BTC_REDEEMED = "btc redeemed",
XMR_REDEEM_IN_MEMPOOL = 'xmr redeem tx is in mempool', XMR_REDEEM_IN_MEMPOOL = "xmr redeem tx is in mempool",
PROCESS_EXITED = 'process exited', PROCESS_EXITED = "process exited",
BTC_CANCELLED = 'btc cancelled', BTC_CANCELLED = "btc cancelled",
BTC_REFUNDED = 'btc refunded', BTC_REFUNDED = "btc refunded",
BTC_PUNISHED = 'btc punished', BTC_PUNISHED = "btc punished",
ATTEMPTING_COOPERATIVE_REDEEM = 'attempting cooperative redeem', ATTEMPTING_COOPERATIVE_REDEEM = "attempting cooperative redeem",
COOPERATIVE_REDEEM_REJECTED = 'cooperative redeem rejected', COOPERATIVE_REDEEM_REJECTED = "cooperative redeem rejected",
} }
export function isSwapState(state?: SwapState | null): state is SwapState { export function isSwapState(state?: SwapState | null): state is SwapState {

View file

@ -1,6 +1,6 @@
import { Alert, ExtendedProviderStatus } from 'models/apiModel'; import { Alert, ExtendedProviderStatus } from "models/apiModel";
const API_BASE_URL = 'https://api.unstoppableswap.net'; const API_BASE_URL = "https://api.unstoppableswap.net";
export async function fetchProvidersViaHttp(): Promise< export async function fetchProvidersViaHttp(): Promise<
ExtendedProviderStatus[] ExtendedProviderStatus[]
@ -23,9 +23,9 @@ export async function submitFeedbackViaHttp(
}; };
const response = await fetch(`${API_BASE_URL}/api/submit-feedback`, { const response = await fetch(`${API_BASE_URL}/api/submit-feedback`, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ body, attachedData }), body: JSON.stringify({ body, attachedData }),
}); });
@ -53,9 +53,9 @@ async function fetchCurrencyUsdPrice(currency: string): Promise<number> {
} }
export async function fetchBtcPrice(): Promise<number> { export async function fetchBtcPrice(): Promise<number> {
return fetchCurrencyUsdPrice('bitcoin'); return fetchCurrencyUsdPrice("bitcoin");
} }
export async function fetchXmrPrice(): Promise<number> { export async function fetchXmrPrice(): Promise<number> {
return fetchCurrencyUsdPrice('monero'); return fetchCurrencyUsdPrice("monero");
} }

View file

@ -1,13 +1,13 @@
import { Box, makeStyles, CssBaseline } from '@material-ui/core'; import { Box, makeStyles, CssBaseline } from "@material-ui/core";
import { createTheme, ThemeProvider } from '@material-ui/core/styles'; import { createTheme, ThemeProvider } from "@material-ui/core/styles";
import { indigo } from '@material-ui/core/colors'; import { indigo } from "@material-ui/core/colors";
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; import { MemoryRouter as Router, Routes, Route } from "react-router-dom";
import Navigation, { drawerWidth } from './navigation/Navigation'; import Navigation, { drawerWidth } from "./navigation/Navigation";
import HistoryPage from './pages/history/HistoryPage'; import HistoryPage from "./pages/history/HistoryPage";
import SwapPage from './pages/swap/SwapPage'; import SwapPage from "./pages/swap/SwapPage";
import WalletPage from './pages/wallet/WalletPage'; import WalletPage from "./pages/wallet/WalletPage";
import HelpPage from './pages/help/HelpPage'; import HelpPage from "./pages/help/HelpPage";
import GlobalSnackbarProvider from './snackbar/GlobalSnackbarProvider'; import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
innerContent: { innerContent: {
@ -20,14 +20,14 @@ const useStyles = makeStyles((theme) => ({
const theme = createTheme({ const theme = createTheme({
palette: { palette: {
type: 'dark', type: "dark",
primary: { primary: {
main: '#f4511e', main: "#f4511e",
}, },
secondary: indigo, secondary: indigo,
}, },
transitions: { transitions: {
create: () => 'none', create: () => "none",
}, },
props: { props: {
MuiButtonBase: { MuiButtonBase: {

View file

@ -4,12 +4,12 @@ import {
CircularProgress, CircularProgress,
IconButton, IconButton,
Tooltip, Tooltip,
} from '@material-ui/core'; } from "@material-ui/core";
import { ReactElement, ReactNode, useEffect, useState } from 'react'; import { ReactElement, ReactNode, useEffect, useState } from "react";
import { useSnackbar } from 'notistack'; import { useSnackbar } from "notistack";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import { RpcProcessStateType } from 'models/rpcModel'; import { RpcProcessStateType } from "models/rpcModel";
import { isExternalRpc } from 'store/config'; import { isExternalRpc } from "store/config";
function IpcButtonTooltip({ function IpcButtonTooltip({
requiresRpcAndNotReady, requiresRpcAndNotReady,
@ -27,19 +27,19 @@ function IpcButtonTooltip({
} }
const getMessage = () => { const getMessage = () => {
if (!requiresRpcAndNotReady) return ''; if (!requiresRpcAndNotReady) return "";
switch (processType) { switch (processType) {
case RpcProcessStateType.LISTENING_FOR_CONNECTIONS: case RpcProcessStateType.LISTENING_FOR_CONNECTIONS:
return ''; return "";
case RpcProcessStateType.STARTED: case RpcProcessStateType.STARTED:
return 'Cannot execute this action because the Swap Daemon is still starting and not yet ready to accept connections. Please wait a moment and try again'; return "Cannot execute this action because the Swap Daemon is still starting and not yet ready to accept connections. Please wait a moment and try again";
case RpcProcessStateType.EXITED: case RpcProcessStateType.EXITED:
return 'Cannot execute this action because the Swap Daemon has been stopped. Please start the Swap Daemon again to continue'; return "Cannot execute this action because the Swap Daemon has been stopped. Please start the Swap Daemon again to continue";
case RpcProcessStateType.NOT_STARTED: case RpcProcessStateType.NOT_STARTED:
return 'Cannot execute this action because the Swap Daemon has not been started yet. Please start the Swap Daemon first'; return "Cannot execute this action because the Swap Daemon has not been started yet. Please start the Swap Daemon first";
default: default:
return ''; return "";
} }
}; };
@ -108,13 +108,13 @@ export default function IpcInvokeButton<T>({
setIsPending(true); setIsPending(true);
try { try {
// const result = await ipcRenderer.invoke(ipcChannel, ...ipcArgs); // const result = await ipcRenderer.invoke(ipcChannel, ...ipcArgs);
throw new Error('Not implemented'); throw new Error("Not implemented");
// onSuccess?.(result); // onSuccess?.(result);
} catch (e: unknown) { } catch (e: unknown) {
if (displayErrorSnackbar) { if (displayErrorSnackbar) {
enqueueSnackbar((e as Error).message, { enqueueSnackbar((e as Error).message, {
autoHideDuration: 60 * 1000, autoHideDuration: 60 * 1000,
variant: 'error', variant: "error",
}); });
} }
} finally { } finally {

View file

@ -6,7 +6,7 @@ import { ReactNode, useEffect, useState } from "react";
interface IpcInvokeButtonProps<T> { interface IpcInvokeButtonProps<T> {
onSuccess?: (data: T) => void; onSuccess?: (data: T) => void;
onClick: () => Promise<T>; onClick: () => Promise<T>;
onPendingChange?: (bool) => void; onPendingChange?: (isPending: boolean) => void;
isLoadingOverride?: boolean; isLoadingOverride?: boolean;
isIconButton?: boolean; isIconButton?: boolean;
loadIcon?: ReactNode; loadIcon?: ReactNode;
@ -46,7 +46,7 @@ export default function PromiseInvokeButton<T>({
onSuccess?.(result); onSuccess?.(result);
} catch (e: unknown) { } catch (e: unknown) {
if (displayErrorSnackbar) { if (displayErrorSnackbar) {
enqueueSnackbar((e as Error).message, { enqueueSnackbar(e as String, {
autoHideDuration: 60 * 1000, autoHideDuration: 60 * 1000,
variant: "error", variant: "error",
}); });

View file

@ -1,7 +1,7 @@
import { Button } from '@material-ui/core'; import { Button } from "@material-ui/core";
import Alert from '@material-ui/lab/Alert'; import Alert from "@material-ui/lab/Alert";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
export default function FundsLeftInWalletAlert() { export default function FundsLeftInWalletAlert() {
const fundsLeft = useAppSelector((state) => state.rpc.state.balance); const fundsLeft = useAppSelector((state) => state.rpc.state.balance);
@ -16,7 +16,7 @@ export default function FundsLeftInWalletAlert() {
<Button <Button
color="inherit" color="inherit"
size="small" size="small"
onClick={() => navigate('/wallet')} onClick={() => navigate("/wallet")}
> >
View View
</Button> </Button>

View file

@ -1,6 +1,6 @@
import { Alert } from '@material-ui/lab'; import { Alert } from "@material-ui/lab";
import { Box, LinearProgress } from '@material-ui/core'; import { Box, LinearProgress } from "@material-ui/core";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
export default function MoneroWalletRpcUpdatingAlert() { export default function MoneroWalletRpcUpdatingAlert() {
const updateState = useAppSelector( const updateState = useAppSelector(
@ -17,7 +17,7 @@ export default function MoneroWalletRpcUpdatingAlert() {
return ( return (
<Alert severity="info"> <Alert severity="info">
<Box style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}> <Box style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<span>The Monero wallet is updating. This may take a few moments</span> <span>The Monero wallet is updating. This may take a few moments</span>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"

View file

@ -1,8 +1,8 @@
import { Alert } from '@material-ui/lab'; import { Alert } from "@material-ui/lab";
import { Box, makeStyles } from '@material-ui/core'; import { Box, makeStyles } from "@material-ui/core";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import WalletRefreshButton from '../pages/wallet/WalletRefreshButton'; import WalletRefreshButton from "../pages/wallet/WalletRefreshButton";
import { SatsAmount } from '../other/Units'; import { SatsAmount } from "../other/Units";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {

View file

@ -1,7 +1,7 @@
import { Alert } from '@material-ui/lab'; import { Alert } from "@material-ui/lab";
import { CircularProgress } from '@material-ui/core'; import { CircularProgress } from "@material-ui/core";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import { RpcProcessStateType } from 'models/rpcModel'; import { RpcProcessStateType } from "models/rpcModel";
export default function RpcStatusAlert() { export default function RpcStatusAlert() {
const rpcProcess = useAppSelector((s) => s.rpc.process); const rpcProcess = useAppSelector((s) => s.rpc.process);

View file

@ -2,102 +2,96 @@ 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.punish_timelock; 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 The swap provider has taken a long time to lock their Monero. This might
might mean that: mean that:
<ul className={classes.list}> <ul className={classes.list}>
<li> <li>
There is a technical issue that prevents them from locking There is a technical issue that prevents them from locking their funds
their funds </li>
</li> <li>They are a malicious actor (unlikely)</li>
<li>They are a malicious actor (unlikely)</li> </ul>
</ul> <br />
<br /> There is still hope for the swap to be successful but you have to be extra
There is still hope for the swap to be successful but you have to be careful. Regardless of why it has taken them so long, it is important that
extra careful. Regardless of why it has taken them so long, it is you refund the swap within the required time period if the swap is not
important that you refund the swap within the required time period completed. If you fail to to do so, you will be punished and lose your
if the swap is not completed. If you fail to to do so, you will be money.
punished and lose your money. <ul className={classes.list}>
<ul className={classes.list}> {isSwapTimelockInfoNone(timelock) && (
{isSwapTimelockInfoNone(timelock) && ( <>
<> <li>
<li> <strong>
<strong> You will be able to refund in about{" "}
You will be able to refund in about{" "} <HumanizedBitcoinBlockDuration
<HumanizedBitcoinBlockDuration blocks={timelock.None.blocks_left}
blocks={timelock.None.blocks_left} />
/> </strong>
</strong> </li>
</li>
<li> <li>
<strong> <strong>
If you have not refunded or completed the swap If you have not refunded or completed the swap in about{" "}
in about{" "} <HumanizedBitcoinBlockDuration
<HumanizedBitcoinBlockDuration blocks={timelock.None.blocks_left + punishTimelockOffset}
blocks={ />
timelock.None.blocks_left + , you will lose your funds.
punishTimelockOffset </strong>
} </li>
/> </>
, you will lose your funds. )}
</strong> {isSwapTimelockInfoCancelled(timelock) && (
</li> <li>
</> <strong>
)} If you have not refunded or completed the swap in about{" "}
{isSwapTimelockInfoCancelled(timelock) && ( <HumanizedBitcoinBlockDuration
<li> blocks={timelock.Cancel.blocks_left}
<strong> />
If you have not refunded or completed the swap in , you will lose your funds.
about{" "} </strong>
<HumanizedBitcoinBlockDuration </li>
blocks={timelock.Cancel.blocks_left} )}
/> <li>
, you will lose your funds. As long as you see this screen, the swap will be refunded
</strong> automatically when the time comes. If this fails, you have to manually
</li> refund by navigating to the History page.
)} </li>
<li> </ul>
As long as you see this screen, the swap will be refunded </Alert>
automatically when the time comes. If this fails, you have );
to manually refund by navigating to the History page.
</li>
</ul>
</Alert>
);
} }

View file

@ -3,33 +3,33 @@ 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,24 +55,20 @@ 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 <SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
swap={swap} </Box>
size="small" );
variant="contained"
/>
</Box>
);
}; };
/** /**
@ -82,31 +78,28 @@ 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 Your Bitcoin is locked. If the swap is not completed in approximately{" "}
approximately{" "} <HumanizedBitcoinBlockDuration blocks={timelock.None.blocks_left} />,
<HumanizedBitcoinBlockDuration you need to refund
blocks={timelock.None.blocks_left} </>,
/> <>
, you need to refund You will lose your funds if you do not refund or complete the swap
</>, within{" "}
<> <HumanizedBitcoinBlockDuration
You will lose your funds if you do not refund or complete the blocks={timelock.None.blocks_left + punishTimelockOffset}
swap within{" "} />
<HumanizedBitcoinBlockDuration </>,
blocks={timelock.None.blocks_left + punishTimelockOffset} ]}
/> />
</>,
]}
/>
); );
/** /**
@ -117,34 +110,30 @@ 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" />
variant="contained" <SwapCancelRefundButton swap={swap} size="small" variant="contained" />
/> </Box>
</Box> );
);
}; };
/** /**
@ -152,7 +141,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</>
); );
/** /**
@ -161,55 +150,55 @@ const ImmediateActionAlert = () => (
* @returns JSX.Element | null * @returns JSX.Element | null
*/ */
function SwapAlertStatusText({ function SwapAlertStatusText({
swap, swap,
}: { }: {
swap: GetSwapInfoResponseRunningSwap; swap: GetSwapInfoResponseRunningSwap;
}) { }) {
switch (swap.state_name) { 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.punish_timelock} 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.state_name); return exhaustiveGuard(swap.state_name);
} }
} }
/** /**
@ -218,27 +207,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.swap_id} key={swap.swap_id}
severity="warning" severity="warning"
action={<SwapResumeButton swap={swap} />} action={<SwapResumeButton swap={swap} />}
variant="filled" variant="filled"
> >
<AlertTitle> <AlertTitle>
Swap {swap.swap_id.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

@ -3,26 +3,26 @@ 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.swap_id} swap={swap} /> <SwapStatusAlert key={swap.swap_id} swap={swap} />
))} ))}
</Box> </Box>
); );
} }

View file

@ -1,7 +1,7 @@
import { Button } from '@material-ui/core'; import { Button } from "@material-ui/core";
import Alert from '@material-ui/lab/Alert'; import Alert from "@material-ui/lab/Alert";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { useResumeableSwapsCount } from 'store/hooks'; import { useResumeableSwapsCount } from "store/hooks";
export default function UnfinishedSwapsAlert() { export default function UnfinishedSwapsAlert() {
const resumableSwapsCount = useResumeableSwapsCount(); const resumableSwapsCount = useResumeableSwapsCount();
@ -16,16 +16,16 @@ export default function UnfinishedSwapsAlert() {
<Button <Button
color="inherit" color="inherit"
size="small" size="small"
onClick={() => navigate('/history')} onClick={() => navigate("/history")}
> >
VIEW VIEW
</Button> </Button>
} }
> >
You have{' '} You have{" "}
{resumableSwapsCount > 1 {resumableSwapsCount > 1
? `${resumableSwapsCount} unfinished swaps` ? `${resumableSwapsCount} unfinished swaps`
: 'one unfinished swap'} : "one unfinished swap"}
</Alert> </Alert>
); );
} }

View file

@ -1,5 +1,5 @@
import { SvgIcon } from '@material-ui/core'; import { SvgIcon } from "@material-ui/core";
import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function BitcoinIcon(props: SvgIconProps) { export default function BitcoinIcon(props: SvgIconProps) {
return ( return (

View file

@ -1,5 +1,5 @@
import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
import { SvgIcon } from '@material-ui/core'; import { SvgIcon } from "@material-ui/core";
export default function DiscordIcon(props: SvgIconProps) { export default function DiscordIcon(props: SvgIconProps) {
return ( return (

View file

@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from "react";
import { IconButton } from '@material-ui/core'; import { IconButton } from "@material-ui/core";
export default function LinkIconButton({ export default function LinkIconButton({
url, url,
@ -9,7 +9,7 @@ export default function LinkIconButton({
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
<IconButton component="span" onClick={() => window.open(url, '_blank')}> <IconButton component="span" onClick={() => window.open(url, "_blank")}>
{children} {children}
</IconButton> </IconButton>
); );

View file

@ -1,5 +1,5 @@
import { SvgIcon } from '@material-ui/core'; import { SvgIcon } from "@material-ui/core";
import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function MoneroIcon(props: SvgIconProps) { export default function MoneroIcon(props: SvgIconProps) {
return ( return (

View file

@ -1,5 +1,5 @@
import { SvgIcon } from '@material-ui/core'; import { SvgIcon } from "@material-ui/core";
import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; import { SvgIconProps } from "@material-ui/core/SvgIcon/SvgIcon";
export default function TorIcon(props: SvgIconProps) { export default function TorIcon(props: SvgIconProps) {
return ( return (

View file

@ -1,8 +1,8 @@
import { useEffect } from 'react'; import { useEffect } from "react";
import { TextField } from '@material-ui/core'; import { TextField } from "@material-ui/core";
import { TextFieldProps } from '@material-ui/core/TextField/TextField'; import { TextFieldProps } from "@material-ui/core/TextField/TextField";
import { isBtcAddressValid } from 'utils/conversionUtils'; import { isBtcAddressValid } from "utils/conversionUtils";
import { isTestnet } from 'store/config'; import { isTestnet } from "store/config";
export default function BitcoinAddressTextField({ export default function BitcoinAddressTextField({
address, address,
@ -16,11 +16,11 @@ export default function BitcoinAddressTextField({
onAddressValidityChange: (valid: boolean) => void; onAddressValidityChange: (valid: boolean) => void;
helperText: string; helperText: string;
} & TextFieldProps) { } & TextFieldProps) {
const placeholder = isTestnet() ? 'tb1q4aelwalu...' : 'bc18ociqZ9mZ...'; const placeholder = isTestnet() ? "tb1q4aelwalu..." : "bc18ociqZ9mZ...";
const errorText = isBtcAddressValid(address, isTestnet()) const errorText = isBtcAddressValid(address, isTestnet())
? null ? null
: `Only bech32 addresses are supported. They begin with "${ : `Only bech32 addresses are supported. They begin with "${
isTestnet() ? 'tb1' : 'bc1' isTestnet() ? "tb1" : "bc1"
}"`; }"`;
useEffect(() => { useEffect(() => {

View file

@ -1,8 +1,8 @@
import { useEffect } from 'react'; import { useEffect } from "react";
import { TextField } from '@material-ui/core'; import { TextField } from "@material-ui/core";
import { TextFieldProps } from '@material-ui/core/TextField/TextField'; import { TextFieldProps } from "@material-ui/core/TextField/TextField";
import { isXmrAddressValid } from 'utils/conversionUtils'; import { isXmrAddressValid } from "utils/conversionUtils";
import { isTestnet } from 'store/config'; import { isTestnet } from "store/config";
export default function MoneroAddressTextField({ export default function MoneroAddressTextField({
address, address,
@ -16,10 +16,10 @@ export default function MoneroAddressTextField({
onAddressValidityChange: (valid: boolean) => void; onAddressValidityChange: (valid: boolean) => void;
helperText: string; helperText: string;
} & TextFieldProps) { } & TextFieldProps) {
const placeholder = isTestnet() ? '59McWTPGc745...' : '888tNkZrPN6J...'; const placeholder = isTestnet() ? "59McWTPGc745..." : "888tNkZrPN6J...";
const errorText = isXmrAddressValid(address, isTestnet()) const errorText = isXmrAddressValid(address, isTestnet())
? null ? null
: 'Not a valid Monero address'; : "Not a valid Monero address";
useEffect(() => { useEffect(() => {
onAddressValidityChange(!errorText); onAddressValidityChange(!errorText);

View file

@ -1,9 +1,9 @@
import { DialogTitle, makeStyles, Typography } from '@material-ui/core'; import { DialogTitle, makeStyles, Typography } from "@material-ui/core";
const useStyles = makeStyles({ const useStyles = makeStyles({
root: { root: {
display: 'flex', display: "flex",
justifyContent: 'space-between', justifyContent: "space-between",
}, },
}); });

View file

@ -1,12 +1,12 @@
import { Button, makeStyles, Paper, Typography } from '@material-ui/core'; import { Button, makeStyles, Paper, Typography } from "@material-ui/core";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
logsOuter: { logsOuter: {
overflow: 'auto', overflow: "auto",
padding: theme.spacing(1), padding: theme.spacing(1),
marginTop: theme.spacing(1), marginTop: theme.spacing(1),
marginBottom: theme.spacing(1), marginBottom: theme.spacing(1),
maxHeight: '10rem', maxHeight: "10rem",
}, },
copyButton: { copyButton: {
marginTop: theme.spacing(1), marginTop: theme.spacing(1),
@ -17,7 +17,7 @@ export default function PaperTextBox({ stdOut }: { stdOut: string }) {
const classes = useStyles(); const classes = useStyles();
function handleCopyLogs() { function handleCopyLogs() {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
return ( return (

View file

@ -5,8 +5,8 @@ import {
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
} from '@material-ui/core'; } from "@material-ui/core";
import IpcInvokeButton from '../IpcInvokeButton'; import IpcInvokeButton from "../IpcInvokeButton";
type SwapCancelAlertProps = { type SwapCancelAlertProps = {
open: boolean; open: boolean;

View file

@ -1,14 +1,14 @@
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";
@ -21,24 +21,24 @@ import { PiconeroAmount } from "../../other/Units";
import LoadingButton from "../../other/LoadingButton"; 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")}`;
} }
await submitFeedbackViaHttp(body, attachedBody); attachedBody = `${JSON.stringify(swapInfo, null, 4)} \n\nLogs: ${logs
.map((l) => JSON.stringify(l))
.join("\n====\n")}`;
}
await submitFeedbackViaHttp(body, attachedBody);
} }
/* /*
@ -48,136 +48,126 @@ 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.swap_id}> <MenuItem value={swap.swap_id}>
Swap {swap.swap_id.substring(0, 5)}... from{" "} Swap {swap.swap_id.substring(0, 5)}... from{" "}
{new Date(parseDateString(swap.start_date)).toDateString()}{" "} {new Date(parseDateString(swap.start_date)).toDateString()} (
( <PiconeroAmount amount={swap.xmr_amount} />)
<PiconeroAmount amount={swap.xmr_amount} />) </MenuItem>
</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?.swap_id || 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 Got something to say? Drop us a message below. If you had an issue
issue with a specific swap, select it from the dropdown to with a specific swap, select it from the dropdown to attach the logs.
attach the logs. It will help us figure out what went wrong. It will help us figure out what went wrong. Hit that submit button
Hit that submit button when you are ready. We appreciate you when you are ready. We appreciate you taking the time to share your
taking the time to share your thoughts! thoughts!
</DialogContentText> </DialogContentText>
<Box <Box
style={{ style={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "1rem", gap: "1rem",
}} }}
> >
<TextField <TextField
variant="outlined" variant="outlined"
value={bodyText} value={bodyText}
onChange={(e) => setBodyText(e.target.value)} onChange={(e) => setBodyText(e.target.value)}
label={ label={
bodyTooLong bodyTooLong
? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})` ? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
: "Feedback" : "Feedback"
} }
multiline multiline
minRows={4} minRows={4}
maxRows={4} maxRows={4}
fullWidth fullWidth
error={bodyTooLong} error={bodyTooLong}
/> />
<SwapSelectDropDown <SwapSelectDropDown
selectedSwap={selectedAttachedSwap} selectedSwap={selectedAttachedSwap}
setSelectedSwap={setSelectedAttachedSwap} setSelectedSwap={setSelectedAttachedSwap}
/> />
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
<LoadingButton <LoadingButton
color="primary" color="primary"
variant="contained" variant="contained"
onClick={async () => { onClick={async () => {
if (pending) { if (pending) {
return; return;
} }
try { try {
setPending(true); setPending(true);
await submitFeedback( await submitFeedback(bodyText, selectedAttachedSwap);
bodyText, enqueueSnackbar("Feedback submitted successfully!", {
selectedAttachedSwap, variant: "success",
); });
enqueueSnackbar( } catch (e) {
"Feedback submitted successfully!", console.error(`Failed to submit feedback: ${e}`);
{ enqueueSnackbar(`Failed to submit feedback (${e})`, {
variant: "success", variant: "error",
}, });
); } finally {
} catch (e) { setPending(false);
console.error(`Failed to submit feedback: ${e}`); }
enqueueSnackbar( onClose();
`Failed to submit feedback (${e})`, }}
{ loading={pending}
variant: "error", >
}, Submit
); </LoadingButton>
} finally { </DialogActions>
setPending(false); </Dialog>
} );
onClose();
}}
loading={pending}
>
Submit
</LoadingButton>
</DialogActions>
</Dialog>
);
} }

View file

@ -1,4 +1,4 @@
import { ChangeEvent, useState } from 'react'; import { ChangeEvent, useState } from "react";
import { import {
DialogTitle, DialogTitle,
Dialog, Dialog,
@ -11,20 +11,20 @@ import {
Chip, Chip,
makeStyles, makeStyles,
Theme, Theme,
} from '@material-ui/core'; } from "@material-ui/core";
import { Multiaddr } from 'multiaddr'; import { Multiaddr } from "multiaddr";
import { useSnackbar } from 'notistack'; import { useSnackbar } from "notistack";
import IpcInvokeButton from '../../IpcInvokeButton'; import IpcInvokeButton from "../../IpcInvokeButton";
const PRESET_RENDEZVOUS_POINTS = [ const PRESET_RENDEZVOUS_POINTS = [
'/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE', "/dns4/discover.unstoppableswap.net/tcp/8888/p2p/12D3KooWA6cnqJpVnreBVnoro8midDL9Lpzmg8oJPoAGi7YYaamE",
'/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs', "/dns4/eratosthen.es/tcp/7798/p2p/12D3KooWAh7EXXa2ZyegzLGdjvj1W4G3EXrTGrf6trraoT1MEobs",
]; ];
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
chipOuter: { chipOuter: {
display: 'flex', display: "flex",
flexWrap: 'wrap', flexWrap: "wrap",
gap: theme.spacing(1), gap: theme.spacing(1),
}, },
})); }));
@ -39,7 +39,7 @@ export default function ListSellersDialog({
onClose, onClose,
}: ListSellersDialogProps) { }: ListSellersDialogProps) {
const classes = useStyles(); const classes = useStyles();
const [rendezvousAddress, setRendezvousAddress] = useState(''); const [rendezvousAddress, setRendezvousAddress] = useState("");
const { enqueueSnackbar } = useSnackbar(); const { enqueueSnackbar } = useSnackbar();
function handleMultiAddrChange(event: ChangeEvent<HTMLInputElement>) { function handleMultiAddrChange(event: ChangeEvent<HTMLInputElement>) {
@ -49,12 +49,12 @@ export default function ListSellersDialog({
function getMultiAddressError(): string | null { function getMultiAddressError(): string | null {
try { try {
const multiAddress = new Multiaddr(rendezvousAddress); const multiAddress = new Multiaddr(rendezvousAddress);
if (!multiAddress.protoNames().includes('p2p')) { if (!multiAddress.protoNames().includes("p2p")) {
return 'The multi address must contain the peer id (/p2p/)'; return "The multi address must contain the peer id (/p2p/)";
} }
return null; return null;
} catch (e) { } catch (e) {
return 'Not a valid multi address'; return "Not a valid multi address";
} }
} }
@ -73,7 +73,7 @@ export default function ListSellersDialog({
} }
enqueueSnackbar(message, { enqueueSnackbar(message, {
variant: 'success', variant: "success",
autoHideDuration: 5000, autoHideDuration: 5000,
}); });
@ -96,7 +96,7 @@ export default function ListSellersDialog({
label="Rendezvous point" label="Rendezvous point"
fullWidth fullWidth
helperText={ helperText={
getMultiAddressError() || 'Multiaddress of the rendezvous point' getMultiAddressError() || "Multiaddress of the rendezvous point"
} }
value={rendezvousAddress} value={rendezvousAddress}
onChange={handleMultiAddrChange} onChange={handleMultiAddrChange}

View file

@ -1,24 +1,24 @@
import { makeStyles, Box, Typography, Chip, Tooltip } from '@material-ui/core'; import { makeStyles, Box, Typography, Chip, Tooltip } from "@material-ui/core";
import { VerifiedUser } from '@material-ui/icons'; import { VerifiedUser } from "@material-ui/icons";
import { satsToBtc, secondsToDays } from 'utils/conversionUtils'; import { satsToBtc, secondsToDays } from "utils/conversionUtils";
import { ExtendedProviderStatus } from 'models/apiModel'; import { ExtendedProviderStatus } from "models/apiModel";
import { import {
MoneroBitcoinExchangeRate, MoneroBitcoinExchangeRate,
SatsAmount, SatsAmount,
} from 'renderer/components/other/Units'; } from "renderer/components/other/Units";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
content: { content: {
flex: 1, flex: 1,
'& *': { "& *": {
lineBreak: 'anywhere', lineBreak: "anywhere",
}, },
}, },
chipsOuter: { chipsOuter: {
display: 'flex', display: "flex",
marginTop: theme.spacing(1), marginTop: theme.spacing(1),
gap: theme.spacing(0.5), gap: theme.spacing(0.5),
flexWrap: 'wrap', flexWrap: "wrap",
}, },
})); }));
@ -41,7 +41,7 @@ export default function ProviderInfo({
{provider.peerId.substring(0, 8)}...{provider.peerId.slice(-8)} {provider.peerId.substring(0, 8)}...{provider.peerId.slice(-8)}
</Typography> </Typography>
<Typography variant="caption"> <Typography variant="caption">
Exchange rate:{' '} Exchange rate:{" "}
<MoneroBitcoinExchangeRate rate={satsToBtc(provider.price)} /> <MoneroBitcoinExchangeRate rate={satsToBtc(provider.price)} />
<br /> <br />
Minimum swap amount: <SatsAmount amount={provider.minSwapAmount} /> Minimum swap amount: <SatsAmount amount={provider.minSwapAmount} />
@ -49,7 +49,7 @@ export default function ProviderInfo({
Maximum swap amount: <SatsAmount amount={provider.maxSwapAmount} /> Maximum swap amount: <SatsAmount amount={provider.maxSwapAmount} />
</Typography> </Typography>
<Box className={classes.chipsOuter}> <Box className={classes.chipsOuter}>
<Chip label={provider.testnet ? 'Testnet' : 'Mainnet'} /> <Chip label={provider.testnet ? "Testnet" : "Mainnet"} />
{provider.uptime && ( {provider.uptime && (
<Tooltip title="A high uptime indicates reliability. Providers with low uptime may be unreliable and cause swaps to take longer to complete or fail entirely."> <Tooltip title="A high uptime indicates reliability. Providers with low uptime may be unreliable and cause swaps to take longer to complete or fail entirely.">
<Chip label={`${Math.round(provider.uptime * 100)} % uptime`} /> <Chip label={`${Math.round(provider.uptime * 100)} % uptime`} />
@ -58,7 +58,7 @@ export default function ProviderInfo({
{provider.age ? ( {provider.age ? (
<Chip <Chip
label={`Went online ${Math.round(secondsToDays(provider.age))} ${ label={`Went online ${Math.round(secondsToDays(provider.age))} ${
provider.age === 1 ? 'day' : 'days' provider.age === 1 ? "day" : "days"
} ago`} } ago`}
/> />
) : ( ) : (

View file

@ -11,21 +11,21 @@ import {
DialogContent, DialogContent,
makeStyles, makeStyles,
CircularProgress, CircularProgress,
} from '@material-ui/core'; } from "@material-ui/core";
import AddIcon from '@material-ui/icons/Add'; import AddIcon from "@material-ui/icons/Add";
import { useState } from 'react'; import { useState } from "react";
import SearchIcon from '@material-ui/icons/Search'; import SearchIcon from "@material-ui/icons/Search";
import { ExtendedProviderStatus } from 'models/apiModel'; import { ExtendedProviderStatus } from "models/apiModel";
import { import {
useAllProviders, useAllProviders,
useAppDispatch, useAppDispatch,
useIsRpcEndpointBusy, useIsRpcEndpointBusy,
} from 'store/hooks'; } from "store/hooks";
import { setSelectedProvider } from 'store/features/providersSlice'; import { setSelectedProvider } from "store/features/providersSlice";
import { RpcMethod } from 'models/rpcModel'; import { RpcMethod } from "models/rpcModel";
import ProviderSubmitDialog from './ProviderSubmitDialog'; import ProviderSubmitDialog from "./ProviderSubmitDialog";
import ListSellersDialog from '../listSellers/ListSellersDialog'; import ListSellersDialog from "../listSellers/ListSellersDialog";
import ProviderInfo from './ProviderInfo'; import ProviderInfo from "./ProviderInfo";
const useStyles = makeStyles({ const useStyles = makeStyles({
dialogContent: { dialogContent: {

View file

@ -4,25 +4,25 @@ import {
CardContent, CardContent,
Box, Box,
IconButton, IconButton,
} from '@material-ui/core'; } from "@material-ui/core";
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos";
import { useState } from 'react'; import { useState } from "react";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import ProviderInfo from './ProviderInfo'; import ProviderInfo from "./ProviderInfo";
import ProviderListDialog from './ProviderListDialog'; import ProviderListDialog from "./ProviderListDialog";
const useStyles = makeStyles({ const useStyles = makeStyles({
inner: { inner: {
textAlign: 'left', textAlign: "left",
width: '100%', width: "100%",
height: '100%', height: "100%",
}, },
providerCard: { providerCard: {
width: '100%', width: "100%",
}, },
providerCardContent: { providerCardContent: {
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
}, },
}); });

View file

@ -1,4 +1,4 @@
import { ChangeEvent, useState } from 'react'; import { ChangeEvent, useState } from "react";
import { import {
DialogTitle, DialogTitle,
Dialog, Dialog,
@ -7,8 +7,8 @@ import {
TextField, TextField,
DialogActions, DialogActions,
Button, Button,
} from '@material-ui/core'; } from "@material-ui/core";
import { Multiaddr } from 'multiaddr'; import { Multiaddr } from "multiaddr";
type ProviderSubmitDialogProps = { type ProviderSubmitDialogProps = {
open: boolean; open: boolean;
@ -19,23 +19,23 @@ export default function ProviderSubmitDialog({
open, open,
onClose, onClose,
}: ProviderSubmitDialogProps) { }: ProviderSubmitDialogProps) {
const [multiAddr, setMultiAddr] = useState(''); const [multiAddr, setMultiAddr] = useState("");
const [peerId, setPeerId] = useState(''); const [peerId, setPeerId] = useState("");
async function handleProviderSubmit() { async function handleProviderSubmit() {
if (multiAddr && peerId) { if (multiAddr && peerId) {
await fetch('https://api.unstoppableswap.net/api/submit-provider', { await fetch("https://api.unstoppableswap.net/api/submit-provider", {
method: 'post', method: "post",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
multiAddr, multiAddr,
peerId, peerId,
}), }),
}); });
setMultiAddr(''); setMultiAddr("");
setPeerId(''); setPeerId("");
onClose(); onClose();
} }
} }
@ -51,15 +51,15 @@ export default function ProviderSubmitDialog({
function getMultiAddressError(): string | null { function getMultiAddressError(): string | null {
try { try {
const multiAddress = new Multiaddr(multiAddr); const multiAddress = new Multiaddr(multiAddr);
if (multiAddress.protoNames().includes('p2p')) { if (multiAddress.protoNames().includes("p2p")) {
return 'The multi address should not contain the peer id (/p2p/)'; return "The multi address should not contain the peer id (/p2p/)";
} }
if (multiAddress.protoNames().find((name) => name.includes('onion'))) { if (multiAddress.protoNames().find((name) => name.includes("onion"))) {
return 'It is currently not possible to add a provider that is only reachable via Tor'; return "It is currently not possible to add a provider that is only reachable via Tor";
} }
return null; return null;
} catch (e) { } catch (e) {
return 'Not a valid multi address'; return "Not a valid multi address";
} }
} }
@ -78,7 +78,7 @@ export default function ProviderSubmitDialog({
fullWidth fullWidth
helperText={ helperText={
getMultiAddressError() || getMultiAddressError() ||
'Tells the swap client where the provider can be reached' "Tells the swap client where the provider can be reached"
} }
value={multiAddr} value={multiAddr}
onChange={handleMultiAddrChange} onChange={handleMultiAddrChange}

View file

@ -1,18 +1,18 @@
import QRCode from 'react-qr-code'; import QRCode from "react-qr-code";
import { Box } from '@material-ui/core'; import { Box } from "@material-ui/core";
export default function BitcoinQrCode({ address }: { address: string }) { export default function BitcoinQrCode({ address }: { address: string }) {
return ( return (
<Box <Box
style={{ style={{
height: '100%', height: "100%",
margin: '0 auto', margin: "0 auto",
}} }}
> >
<QRCode <QRCode
value={`bitcoin:${address}`} value={`bitcoin:${address}`}
size={256} size={256}
style={{ height: 'auto', maxWidth: '100%', width: '100%' }} style={{ height: "auto", maxWidth: "100%", width: "100%" }}
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
/* @ts-ignore */ /* @ts-ignore */
viewBox="0 0 256 256" viewBox="0 0 256 256"

View file

@ -1,8 +1,8 @@
import { isTestnet } from 'store/config'; import { isTestnet } from "store/config";
import { getBitcoinTxExplorerUrl } from 'utils/conversionUtils'; import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
import BitcoinIcon from 'renderer/components/icons/BitcoinIcon'; import BitcoinIcon from "renderer/components/icons/BitcoinIcon";
import { ReactNode } from 'react'; import { ReactNode } from "react";
import TransactionInfoBox from './TransactionInfoBox'; import TransactionInfoBox from "./TransactionInfoBox";
type Props = { type Props = {
title: string; title: string;

View file

@ -3,8 +3,8 @@ import {
CircularProgress, CircularProgress,
makeStyles, makeStyles,
Typography, Typography,
} from '@material-ui/core'; } from "@material-ui/core";
import { ReactNode } from 'react'; import { ReactNode } from "react";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
subtitle: { subtitle: {

View file

@ -1,12 +1,12 @@
import { Button } from '@material-ui/core'; import { Button } from "@material-ui/core";
import { ButtonProps } from '@material-ui/core/Button/Button'; import { ButtonProps } from "@material-ui/core/Button/Button";
export default function ClipboardIconButton({ export default function ClipboardIconButton({
text, text,
...props ...props
}: { text: string } & ButtonProps) { }: { text: string } & ButtonProps) {
function writeToClipboard() { function writeToClipboard() {
throw new Error('Not implemented'); throw new Error("Not implemented");
} }
return ( return (

View file

@ -1,9 +1,9 @@
import { ReactNode } from 'react'; import { ReactNode } from "react";
import { Box, Typography } from '@material-ui/core'; import { Box, Typography } from "@material-ui/core";
import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import InfoBox from './InfoBox'; import InfoBox from "./InfoBox";
import ClipboardIconButton from './ClipbiardIconButton'; import ClipboardIconButton from "./ClipbiardIconButton";
import BitcoinQrCode from './BitcoinQrCode'; import BitcoinQrCode from "./BitcoinQrCode";
type Props = { type Props = {
title: string; title: string;
@ -34,10 +34,10 @@ export default function DepositAddressInfoBox({
/> />
<Box <Box
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'row', flexDirection: "row",
gap: '0.5rem', gap: "0.5rem",
alignItems: 'center', alignItems: "center",
}} }}
> >
<Box>{additionalContent}</Box> <Box>{additionalContent}</Box>

View file

@ -4,8 +4,8 @@ import {
makeStyles, makeStyles,
Paper, Paper,
Typography, Typography,
} from '@material-ui/core'; } from "@material-ui/core";
import { ReactNode } from 'react'; import { ReactNode } from "react";
type Props = { type Props = {
title: ReactNode; title: ReactNode;
@ -18,14 +18,14 @@ type Props = {
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {
padding: theme.spacing(1.5), padding: theme.spacing(1.5),
overflow: 'hidden', overflow: "hidden",
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: theme.spacing(1), gap: theme.spacing(1),
}, },
upperContent: { upperContent: {
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
gap: theme.spacing(0.5), gap: theme.spacing(0.5),
}, },
})); }));

View file

@ -1,8 +1,8 @@
import { isTestnet } from 'store/config'; import { isTestnet } from "store/config";
import { getMoneroTxExplorerUrl } from 'utils/conversionUtils'; import { getMoneroTxExplorerUrl } from "utils/conversionUtils";
import MoneroIcon from 'renderer/components/icons/MoneroIcon'; import MoneroIcon from "renderer/components/icons/MoneroIcon";
import { ReactNode } from 'react'; import { ReactNode } from "react";
import TransactionInfoBox from './TransactionInfoBox'; import TransactionInfoBox from "./TransactionInfoBox";
type Props = { type Props = {
title: string; title: string;

View file

@ -1,25 +1,25 @@
import { useState } from 'react'; import { useState } from "react";
import { import {
Button, Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
makeStyles, makeStyles,
} from '@material-ui/core'; } from "@material-ui/core";
import { useAppDispatch, useAppSelector } from 'store/hooks'; import { useAppDispatch, useAppSelector } from "store/hooks";
import { swapReset } from 'store/features/swapSlice'; import { swapReset } from "store/features/swapSlice";
import SwapStatePage from './pages/SwapStatePage'; import SwapStatePage from "./pages/SwapStatePage";
import SwapStateStepper from './SwapStateStepper'; import SwapStateStepper from "./SwapStateStepper";
import SwapSuspendAlert from '../SwapSuspendAlert'; import SwapSuspendAlert from "../SwapSuspendAlert";
import SwapDialogTitle from './SwapDialogTitle'; import SwapDialogTitle from "./SwapDialogTitle";
import DebugPage from './pages/DebugPage'; import DebugPage from "./pages/DebugPage";
const useStyles = makeStyles({ const useStyles = makeStyles({
content: { content: {
minHeight: '25rem', minHeight: "25rem",
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
justifyContent: 'space-between', justifyContent: "space-between",
}, },
}); });

View file

@ -1,22 +1,17 @@
import { import { Box, DialogTitle, makeStyles, Typography } from "@material-ui/core";
Box, import TorStatusBadge from "./pages/TorStatusBadge";
DialogTitle, import FeedbackSubmitBadge from "./pages/FeedbackSubmitBadge";
makeStyles, import DebugPageSwitchBadge from "./pages/DebugPageSwitchBadge";
Typography,
} from '@material-ui/core';
import TorStatusBadge from './pages/TorStatusBadge';
import FeedbackSubmitBadge from './pages/FeedbackSubmitBadge';
import DebugPageSwitchBadge from './pages/DebugPageSwitchBadge';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
display: 'flex', display: "flex",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', alignItems: "center",
}, },
rightSide: { rightSide: {
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
gridGap: theme.spacing(1), gridGap: theme.spacing(1),
}, },
})); }));

View file

@ -5,165 +5,162 @@ 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()?.state_name ?? 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( const [pathType, activeStep, error] = getActiveStep(stateName, processExited);
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,6 +1,6 @@
import { Link, Typography } from '@material-ui/core'; import { Link, Typography } from "@material-ui/core";
import { ReactNode } from 'react'; import { ReactNode } from "react";
import InfoBox from './InfoBox'; import InfoBox from "./InfoBox";
type TransactionInfoBoxProps = { type TransactionInfoBoxProps = {
title: string; title: string;

View file

@ -1,6 +1,6 @@
import { Tooltip } from '@material-ui/core'; import { Tooltip } from "@material-ui/core";
import IconButton from '@material-ui/core/IconButton'; import IconButton from "@material-ui/core/IconButton";
import DeveloperBoardIcon from '@material-ui/icons/DeveloperBoard'; import DeveloperBoardIcon from "@material-ui/icons/DeveloperBoard";
export default function DebugPageSwitchBadge({ export default function DebugPageSwitchBadge({
enabled, enabled,
@ -14,10 +14,10 @@ export default function DebugPageSwitchBadge({
}; };
return ( return (
<Tooltip title={enabled ? 'Hide debug view' : 'Show debug view'}> <Tooltip title={enabled ? "Hide debug view" : "Show debug view"}>
<IconButton <IconButton
onClick={handleToggle} onClick={handleToggle}
color={enabled ? 'primary' : 'default'} color={enabled ? "primary" : "default"}
> >
<DeveloperBoardIcon /> <DeveloperBoardIcon />
</IconButton> </IconButton>

View file

@ -1,7 +1,7 @@
import { IconButton } from '@material-ui/core'; import { IconButton } from "@material-ui/core";
import FeedbackIcon from '@material-ui/icons/Feedback'; import FeedbackIcon from "@material-ui/icons/Feedback";
import FeedbackDialog from '../../feedback/FeedbackDialog'; import FeedbackDialog from "../../feedback/FeedbackDialog";
import { useState } from 'react'; import { useState } from "react";
export default function FeedbackSubmitBadge() { export default function FeedbackSubmitBadge() {
const [showFeedbackDialog, setShowFeedbackDialog] = useState(false); const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);

View file

@ -1,5 +1,5 @@
import { Box } from '@material-ui/core'; import { Box } from "@material-ui/core";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import { import {
isSwapStateBtcCancelled, isSwapStateBtcCancelled,
isSwapStateBtcLockInMempool, isSwapStateBtcLockInMempool,
@ -15,23 +15,23 @@ import {
isSwapStateXmrLockInMempool, isSwapStateXmrLockInMempool,
isSwapStateXmrRedeemInMempool, isSwapStateXmrRedeemInMempool,
SwapState, SwapState,
} from '../../../../../models/storeModel'; } from "../../../../../models/storeModel";
import InitiatedPage from './init/InitiatedPage'; import InitiatedPage from "./init/InitiatedPage";
import WaitingForBitcoinDepositPage from './init/WaitingForBitcoinDepositPage'; import WaitingForBitcoinDepositPage from "./init/WaitingForBitcoinDepositPage";
import StartedPage from './in_progress/StartedPage'; import StartedPage from "./in_progress/StartedPage";
import BitcoinLockTxInMempoolPage from './in_progress/BitcoinLockTxInMempoolPage'; import BitcoinLockTxInMempoolPage from "./in_progress/BitcoinLockTxInMempoolPage";
import XmrLockTxInMempoolPage from './in_progress/XmrLockInMempoolPage'; import XmrLockTxInMempoolPage from "./in_progress/XmrLockInMempoolPage";
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import ProcessExitedPage from './exited/ProcessExitedPage'; import ProcessExitedPage from "./exited/ProcessExitedPage";
import XmrRedeemInMempoolPage from './done/XmrRedeemInMempoolPage'; import XmrRedeemInMempoolPage from "./done/XmrRedeemInMempoolPage";
import ReceivedQuotePage from './in_progress/ReceivedQuotePage'; import ReceivedQuotePage from "./in_progress/ReceivedQuotePage";
import BitcoinRedeemedPage from './in_progress/BitcoinRedeemedPage'; import BitcoinRedeemedPage from "./in_progress/BitcoinRedeemedPage";
import InitPage from './init/InitPage'; import InitPage from "./init/InitPage";
import XmrLockedPage from './in_progress/XmrLockedPage'; import XmrLockedPage from "./in_progress/XmrLockedPage";
import BitcoinCancelledPage from './in_progress/BitcoinCancelledPage'; import BitcoinCancelledPage from "./in_progress/BitcoinCancelledPage";
import BitcoinRefundedPage from './done/BitcoinRefundedPage'; import BitcoinRefundedPage from "./done/BitcoinRefundedPage";
import BitcoinPunishedPage from './done/BitcoinPunishedPage'; import BitcoinPunishedPage from "./done/BitcoinPunishedPage";
import { SyncingMoneroWalletPage } from './in_progress/SyncingMoneroWalletPage'; import { SyncingMoneroWalletPage } from "./in_progress/SyncingMoneroWalletPage";
export default function SwapStatePage({ export default function SwapStatePage({
swapState, swapState,

View file

@ -1,6 +1,6 @@
import { IconButton, Tooltip } from '@material-ui/core'; import { IconButton, Tooltip } from "@material-ui/core";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import TorIcon from '../../../icons/TorIcon'; import TorIcon from "../../../icons/TorIcon";
export default function TorStatusBadge() { export default function TorStatusBadge() {
const tor = useAppSelector((s) => s.tor); const tor = useAppSelector((s) => s.tor);

View file

@ -1,5 +1,5 @@
import { Box, DialogContentText } from '@material-ui/core'; import { Box, DialogContentText } from "@material-ui/core";
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox'; import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
export default function BitcoinPunishedPage() { export default function BitcoinPunishedPage() {
return ( return (

View file

@ -5,40 +5,39 @@ 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.btc_refund_address}` ? `Refund address: ${swap.btc_refund_address}`
: null; : null;
return ( return (
<Box> <Box>
<DialogContentText> <DialogContentText>
Unfortunately, the swap was not successful. However, rest Unfortunately, the swap was not successful. However, rest assured that
assured that all your Bitcoin has been refunded to the specified all your Bitcoin has been refunded to the specified address. The swap
address. The swap process is now complete, and you are free to process is now complete, and you are free to exit the application.
exit the application. </DialogContentText>
</DialogContentText> <Box
<Box style={{
style={{ display: "flex",
display: "flex", flexDirection: "column",
flexDirection: "column", gap: "0.5rem",
gap: "0.5rem", }}
}} >
> {state && (
{state && ( <BitcoinTransactionInfoBox
<BitcoinTransactionInfoBox title="Bitcoin Refund Transaction"
title="Bitcoin Refund Transaction" txId={state.bobBtcRefundTxId}
txId={state.bobBtcRefundTxId} loading={false}
loading={false} additionalContent={additionalContent}
additionalContent={additionalContent} />
/> )}
)} <FeedbackInfoBox />
<FeedbackInfoBox /> </Box>
</Box> </Box>
</Box> );
);
} }

View file

@ -1,9 +1,9 @@
import { Box, DialogContentText } from '@material-ui/core'; import { Box, DialogContentText } from "@material-ui/core";
import { SwapStateXmrRedeemInMempool } from 'models/storeModel'; import { SwapStateXmrRedeemInMempool } from "models/storeModel";
import { useActiveSwapInfo } from 'store/hooks'; import { useActiveSwapInfo } from "store/hooks";
import { getSwapXmrAmount } from 'models/rpcModel'; import { getSwapXmrAmount } from "models/rpcModel";
import MoneroTransactionInfoBox from '../../MoneroTransactionInfoBox'; import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
import FeedbackInfoBox from '../../../../pages/help/FeedbackInfoBox'; import FeedbackInfoBox from "../../../../pages/help/FeedbackInfoBox";
type XmrRedeemInMempoolPageProps = { type XmrRedeemInMempoolPageProps = {
state: SwapStateXmrRedeemInMempool | null; state: SwapStateXmrRedeemInMempool | null;
@ -27,9 +27,9 @@ export default function XmrRedeemInMempoolPage({
</DialogContentText> </DialogContentText>
<Box <Box
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: '0.5rem', gap: "0.5rem",
}} }}
> >
{state && ( {state && (

View file

@ -5,67 +5,67 @@ 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( messages.push(
isCancelRefund isCancelRefund
? "The manual cancel and refund was unsuccessful." ? "The manual cancel and refund was unsuccessful."
: "The swap exited unexpectedly without completing.", : "The swap exited unexpectedly without completing.",
); );
if (!hasSwap && !isCancelRefund) { if (!hasSwap && !isCancelRefund) {
messages.push("No funds were locked."); 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.push(
<Box> hasRpcError
<DialogContentText>{getText()}</DialogContentText> ? "Check the error and the logs below for more information."
<Box : "Check the logs below for more information.",
style={{
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
>
{state.rpcError && (
<CliLogsBox
logs={[state.rpcError]}
label="Error returned by the Swap Daemon"
/>
)}
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
</Box>
</Box>
); );
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 (
<Box>
<DialogContentText>{getText()}</DialogContentText>
<Box
style={{
display: "flex",
flexDirection: "column",
gap: "0.5rem",
}}
>
{state.rpcError && (
<CliLogsBox
logs={[state.rpcError]}
label="Error returned by the Swap Daemon"
/>
)}
<CliLogsBox logs={logs} label="Logs relevant to the swap" />
</Box>
</Box>
);
} }

View file

@ -1,10 +1,10 @@
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";
@ -14,34 +14,34 @@ 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 (swap) {
if (swap.state_name === SwapStateName.XmrRedeemed) {
return <XmrRedeemInMempoolPage state={null} />;
} }
if (swap.state_name === SwapStateName.BtcRefunded) {
// 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 return <BitcoinRefundedPage state={null} />;
if (swap) {
if (swap.state_name === SwapStateName.XmrRedeemed) {
return <XmrRedeemInMempoolPage state={null} />;
}
if (swap.state_name === SwapStateName.BtcRefunded) {
return <BitcoinRefundedPage state={null} />;
}
if (swap.state_name === SwapStateName.BtcPunished) {
return <BitcoinPunishedPage />;
}
} }
if (swap.state_name === 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,4 +1,4 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function BitcoinCancelledPage() { export default function BitcoinCancelledPage() {
return <CircularProgressWithSubtitle description="Refunding your Bitcoin" />; return <CircularProgressWithSubtitle description="Refunding your Bitcoin" />;

View file

@ -1,7 +1,7 @@
import { Box, DialogContentText } from '@material-ui/core'; import { Box, DialogContentText } from "@material-ui/core";
import { SwapStateBtcLockInMempool } from 'models/storeModel'; import { SwapStateBtcLockInMempool } from "models/storeModel";
import BitcoinTransactionInfoBox from '../../BitcoinTransactionInfoBox'; import BitcoinTransactionInfoBox from "../../BitcoinTransactionInfoBox";
import SwapMightBeCancelledAlert from '../../../../alert/SwapMightBeCancelledAlert'; import SwapMightBeCancelledAlert from "../../../../alert/SwapMightBeCancelledAlert";
type BitcoinLockTxInMempoolPageProps = { type BitcoinLockTxInMempoolPageProps = {
state: SwapStateBtcLockInMempool; state: SwapStateBtcLockInMempool;

View file

@ -1,4 +1,4 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function BitcoinRedeemedPage() { export default function BitcoinRedeemedPage() {
return <CircularProgressWithSubtitle description="Redeeming your Monero" />; return <CircularProgressWithSubtitle description="Redeeming your Monero" />;

View file

@ -1,4 +1,4 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function ReceivedQuotePage() { export default function ReceivedQuotePage() {
return ( return (

View file

@ -1,6 +1,6 @@
import { SwapStateStarted } from 'models/storeModel'; import { SwapStateStarted } from "models/storeModel";
import { BitcoinAmount } from 'renderer/components/other/Units'; import { BitcoinAmount } from "renderer/components/other/Units";
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function StartedPage({ state }: { state: SwapStateStarted }) { export default function StartedPage({ state }: { state: SwapStateStarted }) {
const description = state.txLockDetails ? ( const description = state.txLockDetails ? (
@ -9,7 +9,7 @@ export default function StartedPage({ state }: { state: SwapStateStarted }) {
network fee of <BitcoinAmount amount={state.txLockDetails.fees} /> network fee of <BitcoinAmount amount={state.txLockDetails.fees} />
</> </>
) : ( ) : (
'Locking Bitcoin' "Locking Bitcoin"
); );
return <CircularProgressWithSubtitle description={description} />; return <CircularProgressWithSubtitle description={description} />;

View file

@ -1,4 +1,4 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export function SyncingMoneroWalletPage() { export function SyncingMoneroWalletPage() {
return ( return (

View file

@ -1,6 +1,6 @@
import { Box, DialogContentText } from '@material-ui/core'; import { Box, DialogContentText } from "@material-ui/core";
import { SwapStateXmrLockInMempool } from 'models/storeModel'; import { SwapStateXmrLockInMempool } from "models/storeModel";
import MoneroTransactionInfoBox from '../../MoneroTransactionInfoBox'; import MoneroTransactionInfoBox from "../../MoneroTransactionInfoBox";
type XmrLockTxInMempoolPageProps = { type XmrLockTxInMempoolPageProps = {
state: SwapStateXmrLockInMempool; state: SwapStateXmrLockInMempool;

View file

@ -1,4 +1,4 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function XmrLockedPage() { export default function XmrLockedPage() {
return ( return (

View file

@ -1,24 +1,24 @@
import { useState } from 'react'; import { useState } from "react";
import { Box, makeStyles, TextField, Typography } from '@material-ui/core'; import { Box, makeStyles, TextField, Typography } from "@material-ui/core";
import { SwapStateWaitingForBtcDeposit } from 'models/storeModel'; import { SwapStateWaitingForBtcDeposit } from "models/storeModel";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import { satsToBtc } from 'utils/conversionUtils'; import { satsToBtc } from "utils/conversionUtils";
import { MoneroAmount } from '../../../../other/Units'; import { MoneroAmount } from "../../../../other/Units";
const MONERO_FEE = 0.000016; const MONERO_FEE = 0.000016;
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
gap: theme.spacing(1), gap: theme.spacing(1),
}, },
textField: { textField: {
'& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button': { "& input::-webkit-outer-spin-button, & input::-webkit-inner-spin-button": {
display: 'none', display: "none",
}, },
'& input[type=number]': { "& input[type=number]": {
MozAppearance: 'textfield', MozAppearance: "textfield",
}, },
maxWidth: theme.spacing(16), maxWidth: theme.spacing(16),
}, },
@ -83,7 +83,7 @@ export default function DepositAmountHelper({
className={classes.textField} className={classes.textField}
/> />
<Typography variant="subtitle2"> <Typography variant="subtitle2">
BTC will give you approximately{' '} BTC will give you approximately{" "}
<MoneroAmount amount={calcXMRAmount()} />. <MoneroAmount amount={calcXMRAmount()} />.
</Typography> </Typography>
</Box> </Box>

View file

@ -1,5 +1,5 @@
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
import { MoneroWalletRpcUpdateState } from '../../../../../../models/storeModel'; import { MoneroWalletRpcUpdateState } from "../../../../../../models/storeModel";
export default function DownloadingMoneroWalletRpcPage({ export default function DownloadingMoneroWalletRpcPage({
updateState, updateState,

View file

@ -1,32 +1,28 @@
import { Box, DialogContentText, makeStyles } from '@material-ui/core'; import { Box, DialogContentText, makeStyles } from "@material-ui/core";
import { useState } from 'react'; import { useState } from "react";
import BitcoinAddressTextField from 'renderer/components/inputs/BitcoinAddressTextField'; import BitcoinAddressTextField from "renderer/components/inputs/BitcoinAddressTextField";
import MoneroAddressTextField from 'renderer/components/inputs/MoneroAddressTextField'; import MoneroAddressTextField from "renderer/components/inputs/MoneroAddressTextField";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import PlayArrowIcon from '@material-ui/icons/PlayArrow'; import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import { isTestnet } from 'store/config'; import { isTestnet } from "store/config";
import RemainingFundsWillBeUsedAlert from '../../../../alert/RemainingFundsWillBeUsedAlert'; import RemainingFundsWillBeUsedAlert from "../../../../alert/RemainingFundsWillBeUsedAlert";
import IpcInvokeButton from '../../../../IpcInvokeButton'; import IpcInvokeButton from "../../../../IpcInvokeButton";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
initButton: { initButton: {
marginTop: theme.spacing(1), marginTop: theme.spacing(1),
}, },
fieldsOuter: { fieldsOuter: {
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: theme.spacing(2), gap: theme.spacing(2),
}, },
})); }));
export default function InitPage() { export default function InitPage() {
const classes = useStyles(); const classes = useStyles();
const [redeemAddress, setRedeemAddress] = useState( const [redeemAddress, setRedeemAddress] = useState("");
'' const [refundAddress, setRefundAddress] = useState("");
);
const [refundAddress, setRefundAddress] = useState(
''
);
const [redeemAddressValid, setRedeemAddressValid] = useState(false); const [redeemAddressValid, setRedeemAddressValid] = useState(false);
const [refundAddressValid, setRefundAddressValid] = useState(false); const [refundAddressValid, setRefundAddressValid] = useState(false);
const selectedProvider = useAppSelector( const selectedProvider = useAppSelector(

View file

@ -1,19 +1,19 @@
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import { SwapSpawnType } from 'models/cliModel'; import { SwapSpawnType } from "models/cliModel";
import CircularProgressWithSubtitle from '../../CircularProgressWithSubtitle'; import CircularProgressWithSubtitle from "../../CircularProgressWithSubtitle";
export default function InitiatedPage() { export default function InitiatedPage() {
const description = useAppSelector((s) => { const description = useAppSelector((s) => {
switch (s.swap.spawnType) { switch (s.swap.spawnType) {
case SwapSpawnType.INIT: case SwapSpawnType.INIT:
return 'Requesting quote from provider...'; return "Requesting quote from provider...";
case SwapSpawnType.RESUME: case SwapSpawnType.RESUME:
return 'Resuming swap...'; return "Resuming swap...";
case SwapSpawnType.CANCEL_REFUND: case SwapSpawnType.CANCEL_REFUND:
return 'Attempting to cancel & refund swap...'; return "Attempting to cancel & refund swap...";
default: default:
// Should never be hit // Should never be hit
return 'Initiating swap...'; return "Initiating swap...";
} }
}); });

View file

@ -1,25 +1,25 @@
import { Box, makeStyles, Typography } from '@material-ui/core'; import { Box, makeStyles, Typography } from "@material-ui/core";
import { SwapStateWaitingForBtcDeposit } from 'models/storeModel'; import { SwapStateWaitingForBtcDeposit } from "models/storeModel";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import DepositAddressInfoBox from '../../DepositAddressInfoBox'; import DepositAddressInfoBox from "../../DepositAddressInfoBox";
import BitcoinIcon from '../../../../icons/BitcoinIcon'; import BitcoinIcon from "../../../../icons/BitcoinIcon";
import DepositAmountHelper from './DepositAmountHelper'; import DepositAmountHelper from "./DepositAmountHelper";
import { import {
BitcoinAmount, BitcoinAmount,
MoneroBitcoinExchangeRate, MoneroBitcoinExchangeRate,
SatsAmount, SatsAmount,
} from '../../../../other/Units'; } from "../../../../other/Units";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
amountHelper: { amountHelper: {
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
}, },
additionalContent: { additionalContent: {
paddingTop: theme.spacing(1), paddingTop: theme.spacing(1),
gap: theme.spacing(0.5), gap: theme.spacing(0.5),
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
}, },
})); }));
@ -45,13 +45,13 @@ export default function WaitingForBtcDepositPage({
<ul> <ul>
{bitcoinBalance > 0 ? ( {bitcoinBalance > 0 ? (
<li> <li>
You have already deposited{' '} You have already deposited{" "}
<SatsAmount amount={bitcoinBalance} /> <SatsAmount amount={bitcoinBalance} />
</li> </li>
) : null} ) : null}
<li> <li>
Send any amount between{' '} Send any amount between{" "}
<BitcoinAmount amount={state.minDeposit} /> and{' '} <BitcoinAmount amount={state.minDeposit} /> and{" "}
<BitcoinAmount amount={state.maxDeposit} /> to the address <BitcoinAmount amount={state.maxDeposit} /> to the address
above above
{bitcoinBalance > 0 && ( {bitcoinBalance > 0 && (
@ -60,11 +60,11 @@ export default function WaitingForBtcDepositPage({
</li> </li>
<li> <li>
All Bitcoin sent to this this address will converted into All Bitcoin sent to this this address will converted into
Monero at an exchance rate of{' '} Monero at an exchance rate of{" "}
<MoneroBitcoinExchangeRate rate={state.price} /> <MoneroBitcoinExchangeRate rate={state.price} />
</li> </li>
<li> <li>
The network fee of{' '} The network fee of{" "}
<BitcoinAmount amount={state.minBitcoinLockTxFee} /> will <BitcoinAmount amount={state.minBitcoinLockTxFee} /> will
automatically be deducted from the deposited coins automatically be deducted from the deposited coins
</li> </li>
@ -74,9 +74,7 @@ export default function WaitingForBtcDepositPage({
</li> </li>
</ul> </ul>
</Typography> </Typography>
<DepositAmountHelper <DepositAmountHelper state={state} />
state={state}
/>
</Box> </Box>
} }
icon={<BitcoinIcon />} icon={<BitcoinIcon />}

View file

@ -9,6 +9,7 @@ import { useState } from "react";
import { withdrawBtc } from "renderer/rpc"; import { withdrawBtc } from "renderer/rpc";
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage"; import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
import AddressInputPage from "./pages/AddressInputPage"; import AddressInputPage from "./pages/AddressInputPage";
import WithdrawDialogContent from "./WithdrawDialogContent";
export default function WithdrawDialog({ export default function WithdrawDialog({
open, open,
@ -30,27 +31,30 @@ export default function WithdrawDialog({
} }
} }
// This prevents an issue where the Dialog is shown for a split second without a present withdraw state
if (!open) return null;
return ( return (
<Dialog open onClose={onCancel} maxWidth="sm" fullWidth> <Dialog open={open} onClose={onCancel} maxWidth="sm" fullWidth>
<DialogHeader title="Withdraw Bitcoin" /> <DialogHeader title="Withdraw Bitcoin" />
{withdrawTxId === null ? ( <WithdrawDialogContent isPending={pending} withdrawTxId={withdrawTxId}>
<AddressInputPage
setWithdrawAddress={setWithdrawAddress}
withdrawAddress={withdrawAddress}
setWithdrawAddressValid={setWithdrawAddressValid}
/>
) : (
<BtcTxInMempoolPageContent
withdrawTxId={withdrawTxId}
onCancel={onCancel}
/>
)}
<DialogActions>
{withdrawTxId === null ? ( {withdrawTxId === null ? (
<AddressInputPage
setWithdrawAddress={setWithdrawAddress}
withdrawAddress={withdrawAddress}
setWithdrawAddressValid={setWithdrawAddressValid}
/>
) : (
<BtcTxInMempoolPageContent
withdrawTxId={withdrawTxId}
onCancel={onCancel}
/>
)}
</WithdrawDialogContent>
<DialogActions>
<Button onClick={onCancel} color="primary" disabled={pending}>
{withdrawTxId === null ? "Cancel" : "Done"}
</Button>
{withdrawTxId === null && (
<PromiseInvokeButton <PromiseInvokeButton
displayErrorSnackbar
variant="contained" variant="contained"
color="primary" color="primary"
disabled={!withdrawAddressValid} disabled={!withdrawAddressValid}
@ -65,10 +69,6 @@ export default function WithdrawDialog({
> >
Withdraw Withdraw
</PromiseInvokeButton> </PromiseInvokeButton>
) : (
<Button onClick={onCancel} color="primary" disabled={pending}>
Close
</Button>
)} )}
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View file

@ -1,27 +1,31 @@
import { ReactNode } from 'react'; import { ReactNode } from "react";
import { Box, DialogContent, makeStyles } from '@material-ui/core'; import { Box, DialogContent, makeStyles } from "@material-ui/core";
import WithdrawStepper from './WithdrawStepper'; import WithdrawStepper from "./WithdrawStepper";
const useStyles = makeStyles({ const useStyles = makeStyles({
outer: { outer: {
minHeight: '15rem', minHeight: "15rem",
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
justifyContent: 'space-between', justifyContent: "space-between",
}, },
}); });
export default function WithdrawDialogContent({ export default function WithdrawDialogContent({
children, children,
isPending,
withdrawTxId,
}: { }: {
children: ReactNode; children: ReactNode;
isPending: boolean;
withdrawTxId: string | null;
}) { }) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<DialogContent dividers className={classes.outer}> <DialogContent dividers className={classes.outer}>
<Box>{children}</Box> <Box>{children}</Box>
<WithdrawStepper /> <WithdrawStepper isPending={isPending} withdrawTxId={withdrawTxId} />
</DialogContent> </DialogContent>
); );
} }

View file

@ -1,27 +0,0 @@
import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks';
import { RpcMethod } from 'models/rpcModel';
import AddressInputPage from './pages/AddressInputPage';
import InitiatedPage from './pages/InitiatedPage';
import BtcTxInMempoolPageContent from './pages/BitcoinWithdrawTxInMempoolPage';
export default function WithdrawStatePage({
onCancel,
}: {
onCancel: () => void;
}) {
const isRpcEndpointBusy = useIsRpcEndpointBusy(RpcMethod.WITHDRAW_BTC);
const withdrawTxId = useAppSelector((state) => state.rpc.state.withdrawTxId);
if (withdrawTxId !== null) {
return (
<BtcTxInMempoolPageContent
withdrawTxId={withdrawTxId}
onCancel={onCancel}
/>
);
}
if (isRpcEndpointBusy) {
return <InitiatedPage onCancel={onCancel} />;
}
return <AddressInputPage onCancel={onCancel} />;
}

View file

@ -1,12 +1,8 @@
import { Step, StepLabel, Stepper } from '@material-ui/core'; import { Step, StepLabel, Stepper } from "@material-ui/core";
import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks'; import { useAppSelector, useIsRpcEndpointBusy } from "store/hooks";
import { RpcMethod } from 'models/rpcModel';
function getActiveStep( function getActiveStep(isPending: boolean, withdrawTxId: string | null) {
isWithdrawInProgress: boolean, if (isPending) {
withdrawTxId: string | null,
) {
if (isWithdrawInProgress) {
return 1; return 1;
} }
if (withdrawTxId !== null) { if (withdrawTxId !== null) {
@ -15,12 +11,15 @@ function getActiveStep(
return 0; return 0;
} }
export default function WithdrawStepper() { export default function WithdrawStepper({
const isWithdrawInProgress = useIsRpcEndpointBusy(RpcMethod.WITHDRAW_BTC); isPending,
const withdrawTxId = useAppSelector((s) => s.rpc.state.withdrawTxId); withdrawTxId,
}: {
isPending: boolean;
withdrawTxId: string | null;
}) {
return ( return (
<Stepper activeStep={getActiveStep(isWithdrawInProgress, withdrawTxId)}> <Stepper activeStep={getActiveStep(isPending, withdrawTxId)}>
<Step key={0}> <Step key={0}>
<StepLabel>Enter withdraw address</StepLabel> <StepLabel>Enter withdraw address</StepLabel>
</Step> </Step>

View file

@ -15,20 +15,18 @@ export default function AddressInputPage({
}) { }) {
return ( return (
<> <>
<WithdrawDialogContent> <DialogContentText>
<DialogContentText> To withdraw the Bitcoin inside the internal wallet, please enter an
To withdraw the BTC of the internal wallet, please enter an address. address. All funds will be sent to that address.
All funds will be sent to that address. </DialogContentText>
</DialogContentText>
<BitcoinAddressTextField <BitcoinAddressTextField
address={withdrawAddress} address={withdrawAddress}
onAddressChange={setWithdrawAddress} onAddressChange={setWithdrawAddress}
onAddressValidityChange={setWithdrawAddressValid} onAddressValidityChange={setWithdrawAddressValid}
helperText="All Bitcoin of the internal wallet will be transferred to this address" helperText="All Bitcoin of the internal wallet will be transferred to this address"
fullWidth fullWidth
/> />
</WithdrawDialogContent>
</> </>
); );
} }

View file

@ -11,18 +11,16 @@ export default function BtcTxInMempoolPageContent({
}) { }) {
return ( return (
<> <>
<WithdrawDialogContent> <DialogContentText>
<DialogContentText> All funds of the internal Bitcoin wallet have been transferred to your
All funds of the internal Bitcoin wallet have been transferred to your withdraw address.
withdraw address. </DialogContentText>
</DialogContentText> <BitcoinTransactionInfoBox
<BitcoinTransactionInfoBox txId={withdrawTxId}
txId={withdrawTxId} loading={false}
loading={false} title="Bitcoin Withdraw Transaction"
title="Bitcoin Withdraw Transaction" additionalContent={null}
additionalContent={null} />
/>
</WithdrawDialogContent>
</> </>
); );
} }

View file

@ -1,6 +1,6 @@
import { Drawer, makeStyles, Box } from '@material-ui/core'; import { Drawer, makeStyles, Box } from "@material-ui/core";
import NavigationHeader from './NavigationHeader'; import NavigationHeader from "./NavigationHeader";
import NavigationFooter from './NavigationFooter'; import NavigationFooter from "./NavigationFooter";
export const drawerWidth = 240; export const drawerWidth = 240;
@ -13,11 +13,11 @@ const useStyles = makeStyles({
width: drawerWidth, width: drawerWidth,
}, },
drawerContainer: { drawerContainer: {
overflow: 'auto', overflow: "auto",
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
justifyContent: 'space-between', justifyContent: "space-between",
height: '100%', height: "100%",
}, },
}); });

View file

@ -1,24 +1,24 @@
import RedditIcon from '@material-ui/icons/Reddit'; import RedditIcon from "@material-ui/icons/Reddit";
import GitHubIcon from '@material-ui/icons/GitHub'; import GitHubIcon from "@material-ui/icons/GitHub";
import { Box, makeStyles } from '@material-ui/core'; import { Box, makeStyles } from "@material-ui/core";
import LinkIconButton from '../icons/LinkIconButton'; import LinkIconButton from "../icons/LinkIconButton";
import UnfinishedSwapsAlert from '../alert/UnfinishedSwapsAlert'; import UnfinishedSwapsAlert from "../alert/UnfinishedSwapsAlert";
import FundsLeftInWalletAlert from '../alert/FundsLeftInWalletAlert'; import FundsLeftInWalletAlert from "../alert/FundsLeftInWalletAlert";
import RpcStatusAlert from '../alert/RpcStatusAlert'; import RpcStatusAlert from "../alert/RpcStatusAlert";
import DiscordIcon from '../icons/DiscordIcon'; import DiscordIcon from "../icons/DiscordIcon";
import { DISCORD_URL } from '../pages/help/ContactInfoBox'; import { DISCORD_URL } from "../pages/help/ContactInfoBox";
import MoneroWalletRpcUpdatingAlert from '../alert/MoneroWalletRpcUpdatingAlert'; import MoneroWalletRpcUpdatingAlert from "../alert/MoneroWalletRpcUpdatingAlert";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
padding: theme.spacing(1), padding: theme.spacing(1),
gap: theme.spacing(1), gap: theme.spacing(1),
}, },
linksOuter: { linksOuter: {
display: 'flex', display: "flex",
justifyContent: 'space-evenly', justifyContent: "space-evenly",
}, },
})); }));

View file

@ -1,10 +1,10 @@
import { Box, List } from '@material-ui/core'; import { Box, List } from "@material-ui/core";
import SwapHorizOutlinedIcon from '@material-ui/icons/SwapHorizOutlined'; import SwapHorizOutlinedIcon from "@material-ui/icons/SwapHorizOutlined";
import HistoryOutlinedIcon from '@material-ui/icons/HistoryOutlined'; import HistoryOutlinedIcon from "@material-ui/icons/HistoryOutlined";
import AccountBalanceWalletIcon from '@material-ui/icons/AccountBalanceWallet'; import AccountBalanceWalletIcon from "@material-ui/icons/AccountBalanceWallet";
import HelpOutlineIcon from '@material-ui/icons/HelpOutline'; import HelpOutlineIcon from "@material-ui/icons/HelpOutline";
import RouteListItemIconButton from './RouteListItemIconButton'; import RouteListItemIconButton from "./RouteListItemIconButton";
import UnfinishedSwapsBadge from './UnfinishedSwapsCountBadge'; import UnfinishedSwapsBadge from "./UnfinishedSwapsCountBadge";
export default function NavigationHeader() { export default function NavigationHeader() {
return ( return (

View file

@ -1,6 +1,6 @@
import { ReactNode } from 'react'; import { ReactNode } from "react";
import { useNavigate } from 'react-router-dom'; import { useNavigate } from "react-router-dom";
import { ListItem, ListItemIcon, ListItemText } from '@material-ui/core'; import { ListItem, ListItemIcon, ListItemText } from "@material-ui/core";
export default function RouteListItemIconButton({ export default function RouteListItemIconButton({
name, name,

View file

@ -1,5 +1,5 @@
import { Badge } from '@material-ui/core'; import { Badge } from "@material-ui/core";
import { useResumeableSwapsCount } from 'store/hooks'; import { useResumeableSwapsCount } from "store/hooks";
export default function UnfinishedSwapsBadge({ export default function UnfinishedSwapsBadge({
children, children,

View file

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from "react";
import { Box, IconButton, TextField } from '@material-ui/core'; import { Box, IconButton, TextField } from "@material-ui/core";
import SearchIcon from '@material-ui/icons/Search'; import SearchIcon from "@material-ui/icons/Search";
import CloseIcon from '@material-ui/icons/Close'; import CloseIcon from "@material-ui/icons/Close";
export function ExpandableSearchBox({ export function ExpandableSearchBox({
query, query,
@ -13,8 +13,8 @@ export function ExpandableSearchBox({
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
return ( return (
<Box style={{ display: 'flex', justifyContent: 'center' }}> <Box style={{ display: "flex", justifyContent: "center" }}>
<Box style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}> <Box style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
{expanded ? ( {expanded ? (
<> <>
<TextField <TextField
@ -26,7 +26,7 @@ export function ExpandableSearchBox({
<IconButton <IconButton
onClick={() => { onClick={() => {
setExpanded(false); setExpanded(false);
setQuery(''); setQuery("");
}} }}
size="small" size="small"
> >

View file

@ -1,4 +1,4 @@
import humanizeDuration from 'humanize-duration'; import humanizeDuration from "humanize-duration";
const AVG_BLOCK_TIME_MS = 10 * 60 * 1000; const AVG_BLOCK_TIME_MS = 10 * 60 * 1000;
@ -10,7 +10,7 @@ export default function HumanizedBitcoinBlockDuration({
return ( return (
<> <>
{`${humanizeDuration(blocks * AVG_BLOCK_TIME_MS, { {`${humanizeDuration(blocks * AVG_BLOCK_TIME_MS, {
conjunction: ' and ', conjunction: " and ",
})} (${blocks} blocks)`} })} (${blocks} blocks)`}
</> </>
); );

View file

@ -1,8 +1,8 @@
import TreeView from '@material-ui/lab/TreeView'; import TreeView from "@material-ui/lab/TreeView";
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import TreeItem from '@material-ui/lab/TreeItem'; import TreeItem from "@material-ui/lab/TreeItem";
import ScrollablePaperTextBox from './ScrollablePaperTextBox'; import ScrollablePaperTextBox from "./ScrollablePaperTextBox";
interface JsonTreeViewProps { interface JsonTreeViewProps {
data: any; data: any;
@ -13,7 +13,7 @@ export default function JsonTreeView({ data, label }: JsonTreeViewProps) {
const renderTree = (nodes: any, parentId: string) => { const renderTree = (nodes: any, parentId: string) => {
return Object.keys(nodes).map((key, _) => { return Object.keys(nodes).map((key, _) => {
const nodeId = `${parentId}.${key}`; const nodeId = `${parentId}.${key}`;
if (typeof nodes[key] === 'object' && nodes[key] !== null) { if (typeof nodes[key] === "object" && nodes[key] !== null) {
return ( return (
<TreeItem nodeId={nodeId} label={key} key={nodeId}> <TreeItem nodeId={nodeId} label={key} key={nodeId}>
{renderTree(nodes[key], nodeId)} {renderTree(nodes[key], nodeId)}
@ -38,10 +38,10 @@ export default function JsonTreeView({ data, label }: JsonTreeViewProps) {
<TreeView <TreeView
defaultCollapseIcon={<ExpandMoreIcon />} defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />} defaultExpandIcon={<ChevronRightIcon />}
defaultExpanded={['root']} defaultExpanded={["root"]}
> >
<TreeItem nodeId="root" label={label}> <TreeItem nodeId="root" label={label}>
{renderTree(data ?? {}, 'root')} {renderTree(data ?? {}, "root")}
</TreeItem> </TreeItem>
</TreeView>, </TreeView>,
]} ]}

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from "react";
import Button, { ButtonProps } from '@material-ui/core/Button'; import Button, { ButtonProps } from "@material-ui/core/Button";
import CircularProgress from '@material-ui/core/CircularProgress'; import CircularProgress from "@material-ui/core/CircularProgress";
interface LoadingButtonProps extends ButtonProps { interface LoadingButtonProps extends ButtonProps {
loading: boolean; loading: boolean;

View file

@ -1,47 +1,47 @@
import { Box, Chip, Typography } from '@material-ui/core'; import { Box, Chip, Typography } from "@material-ui/core";
import { useMemo, useState } from 'react'; import { useMemo, useState } from "react";
import { CliLog } from 'models/cliModel'; import { CliLog } from "models/cliModel";
import { logsToRawString } from 'utils/parseUtils'; import { logsToRawString } from "utils/parseUtils";
import ScrollablePaperTextBox from './ScrollablePaperTextBox'; import ScrollablePaperTextBox from "./ScrollablePaperTextBox";
function RenderedCliLog({ log }: { log: CliLog }) { function RenderedCliLog({ log }: { log: CliLog }) {
const { timestamp, level, fields } = log; const { timestamp, level, fields } = log;
const levelColorMap = { const levelColorMap = {
DEBUG: '#1976d2', // Blue DEBUG: "#1976d2", // Blue
INFO: '#388e3c', // Green INFO: "#388e3c", // Green
WARN: '#fbc02d', // Yellow WARN: "#fbc02d", // Yellow
ERROR: '#d32f2f', // Red ERROR: "#d32f2f", // Red
TRACE: '#8e24aa', // Purple TRACE: "#8e24aa", // Purple
}; };
return ( return (
<Box> <Box>
<Box <Box
style={{ style={{
display: 'flex', display: "flex",
gap: '0.3rem', gap: "0.3rem",
alignItems: 'center', alignItems: "center",
}} }}
> >
<Chip <Chip
label={level} label={level}
size="small" size="small"
style={{ backgroundColor: levelColorMap[level], color: 'white' }} style={{ backgroundColor: levelColorMap[level], color: "white" }}
/> />
<Chip label={timestamp} size="small" variant="outlined" /> <Chip label={timestamp} size="small" variant="outlined" />
<Typography variant="subtitle2">{fields.message}</Typography> <Typography variant="subtitle2">{fields.message}</Typography>
</Box> </Box>
<Box <Box
sx={{ sx={{
paddingLeft: '1rem', paddingLeft: "1rem",
paddingTop: '0.2rem', paddingTop: "0.2rem",
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
}} }}
> >
{Object.entries(fields).map(([key, value]) => { {Object.entries(fields).map(([key, value]) => {
if (key !== 'message') { if (key !== "message") {
return ( return (
<Typography variant="caption" key={key}> <Typography variant="caption" key={key}>
{key}: {JSON.stringify(value)} {key}: {JSON.stringify(value)}
@ -62,7 +62,7 @@ export default function CliLogsBox({
label: string; label: string;
logs: (CliLog | string)[]; logs: (CliLog | string)[];
}) { }) {
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>("");
const memoizedLogs = useMemo(() => { const memoizedLogs = useMemo(() => {
if (searchQuery.length === 0) { if (searchQuery.length === 0) {
@ -80,7 +80,7 @@ export default function CliLogsBox({
searchQuery={searchQuery} searchQuery={searchQuery}
setSearchQuery={setSearchQuery} setSearchQuery={setSearchQuery}
rows={memoizedLogs.map((log) => rows={memoizedLogs.map((log) =>
typeof log === 'string' ? ( typeof log === "string" ? (
<Typography component="pre">{log}</Typography> <Typography component="pre">{log}</Typography>
) : ( ) : (
<RenderedCliLog log={log} key={JSON.stringify(log)} /> <RenderedCliLog log={log} key={JSON.stringify(log)} />

View file

@ -1,12 +1,12 @@
import { Box, Divider, IconButton, Paper, Typography } from '@material-ui/core'; import { Box, Divider, IconButton, Paper, Typography } from "@material-ui/core";
import { ReactNode, useRef } from 'react'; import { ReactNode, useRef } from "react";
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 { VList, VListHandle } from 'virtua'; import { VList, VListHandle } from "virtua";
import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined";
import { ExpandableSearchBox } from './ExpandableSearchBox'; import { ExpandableSearchBox } from "./ExpandableSearchBox";
const MIN_HEIGHT = '10rem'; const MIN_HEIGHT = "10rem";
export default function ScrollablePaperTextBox({ export default function ScrollablePaperTextBox({
rows, rows,
@ -41,31 +41,31 @@ export default function ScrollablePaperTextBox({
<Paper <Paper
variant="outlined" variant="outlined"
style={{ style={{
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: '0.5rem', gap: "0.5rem",
padding: '0.5rem', padding: "0.5rem",
width: '100%', width: "100%",
}} }}
> >
<Typography>{title}</Typography> <Typography>{title}</Typography>
<Divider /> <Divider />
<Box <Box
style={{ style={{
overflow: 'auto', overflow: "auto",
whiteSpace: 'nowrap', whiteSpace: "nowrap",
maxHeight: minHeight, maxHeight: minHeight,
minHeight, minHeight,
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: '0.5rem', gap: "0.5rem",
}} }}
> >
<VList ref={virtuaEl} style={{ height: MIN_HEIGHT, width: '100%' }}> <VList ref={virtuaEl} style={{ height: MIN_HEIGHT, width: "100%" }}>
{rows} {rows}
</VList> </VList>
</Box> </Box>
<Box style={{ display: 'flex', gap: '0.5rem' }}> <Box style={{ display: "flex", gap: "0.5rem" }}>
<IconButton onClick={onCopy} size="small"> <IconButton onClick={onCopy} size="small">
<FileCopyOutlinedIcon /> <FileCopyOutlinedIcon />
</IconButton> </IconButton>

View file

@ -1,17 +1,17 @@
import { Box, Button, makeStyles, Typography } from '@material-ui/core'; import { Box, Button, makeStyles, Typography } from "@material-ui/core";
import InfoBox from '../../modal/swap/InfoBox'; import InfoBox from "../../modal/swap/InfoBox";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
spacedBox: { spacedBox: {
display: 'flex', display: "flex",
gap: theme.spacing(1), gap: theme.spacing(1),
}, },
})); }));
const GITHUB_ISSUE_URL = const GITHUB_ISSUE_URL =
'https://github.com/UnstoppableSwap/unstoppableswap-gui/issues/new/choose'; "https://github.com/UnstoppableSwap/unstoppableswap-gui/issues/new/choose";
const MATRIX_ROOM_URL = 'https://matrix.to/#/#unstoppableswap:matrix.org'; const MATRIX_ROOM_URL = "https://matrix.to/#/#unstoppableswap:matrix.org";
export const DISCORD_URL = 'https://discord.gg/APJ6rJmq'; export const DISCORD_URL = "https://discord.gg/APJ6rJmq";
export default function ContactInfoBox() { export default function ContactInfoBox() {
const classes = useStyles(); const classes = useStyles();

View file

@ -1,9 +1,9 @@
import { Typography } from '@material-ui/core'; import { Typography } from "@material-ui/core";
import DepositAddressInfoBox from '../../modal/swap/DepositAddressInfoBox'; import DepositAddressInfoBox from "../../modal/swap/DepositAddressInfoBox";
import MoneroIcon from '../../icons/MoneroIcon'; import MoneroIcon from "../../icons/MoneroIcon";
const XMR_DONATE_ADDRESS = const XMR_DONATE_ADDRESS =
'87jS4C7ngk9EHdqFFuxGFgg8AyH63dRUoULshWDybFJaP75UA89qsutG5B1L1QTc4w228nsqsv8EjhL7bz8fB3611Mh98mg'; "87jS4C7ngk9EHdqFFuxGFgg8AyH63dRUoULshWDybFJaP75UA89qsutG5B1L1QTc4w228nsqsv8EjhL7bz8fB3611Mh98mg";
export default function DonateInfoBox() { export default function DonateInfoBox() {
return ( return (

View file

@ -1,7 +1,7 @@
import { Button, Typography } from '@material-ui/core'; import { Button, Typography } from "@material-ui/core";
import { useState } from 'react'; import { useState } from "react";
import InfoBox from '../../modal/swap/InfoBox'; import InfoBox from "../../modal/swap/InfoBox";
import FeedbackDialog from '../../modal/feedback/FeedbackDialog'; import FeedbackDialog from "../../modal/feedback/FeedbackDialog";
export default function FeedbackInfoBox() { export default function FeedbackInfoBox() {
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);

View file

@ -1,15 +1,15 @@
import { Box, makeStyles } from '@material-ui/core'; import { Box, makeStyles } from "@material-ui/core";
import ContactInfoBox from './ContactInfoBox'; import ContactInfoBox from "./ContactInfoBox";
import FeedbackInfoBox from './FeedbackInfoBox'; import FeedbackInfoBox from "./FeedbackInfoBox";
import DonateInfoBox from './DonateInfoBox'; import DonateInfoBox from "./DonateInfoBox";
import TorInfoBox from './TorInfoBox'; import TorInfoBox from "./TorInfoBox";
import RpcControlBox from './RpcControlBox'; import RpcControlBox from "./RpcControlBox";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {
display: 'flex', display: "flex",
gap: theme.spacing(2), gap: theme.spacing(2),
flexDirection: 'column', flexDirection: "column",
}, },
})); }));

View file

@ -1,18 +1,18 @@
import { Box, makeStyles } from '@material-ui/core'; import { Box, makeStyles } from "@material-ui/core";
import IpcInvokeButton from 'renderer/components/IpcInvokeButton'; import IpcInvokeButton from "renderer/components/IpcInvokeButton";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import StopIcon from '@material-ui/icons/Stop'; import StopIcon from "@material-ui/icons/Stop";
import PlayArrowIcon from '@material-ui/icons/PlayArrow'; import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import { RpcProcessStateType } from 'models/rpcModel'; import { RpcProcessStateType } from "models/rpcModel";
import InfoBox from '../../modal/swap/InfoBox'; import InfoBox from "../../modal/swap/InfoBox";
import CliLogsBox from '../../other/RenderedCliLog'; import CliLogsBox from "../../other/RenderedCliLog";
import FolderOpenIcon from '@material-ui/icons/FolderOpen'; import FolderOpenIcon from "@material-ui/icons/FolderOpen";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
actionsOuter: { actionsOuter: {
display: 'flex', display: "flex",
gap: theme.spacing(1), gap: theme.spacing(1),
alignItems: 'center', alignItems: "center",
}, },
})); }));

View file

@ -1,14 +1,14 @@
import { Box, makeStyles, Typography } from '@material-ui/core'; import { Box, makeStyles, Typography } from "@material-ui/core";
import IpcInvokeButton from 'renderer/components/IpcInvokeButton'; import IpcInvokeButton from "renderer/components/IpcInvokeButton";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import StopIcon from '@material-ui/icons/Stop'; import StopIcon from "@material-ui/icons/Stop";
import PlayArrowIcon from '@material-ui/icons/PlayArrow'; import PlayArrowIcon from "@material-ui/icons/PlayArrow";
import InfoBox from '../../modal/swap/InfoBox'; import InfoBox from "../../modal/swap/InfoBox";
import CliLogsBox from '../../other/RenderedCliLog'; import CliLogsBox from "../../other/RenderedCliLog";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
actionsOuter: { actionsOuter: {
display: 'flex', display: "flex",
gap: theme.spacing(1), gap: theme.spacing(1),
}, },
})); }));
@ -24,10 +24,10 @@ export default function TorInfoBox() {
mainContent={ mainContent={
<Box <Box
style={{ style={{
width: '100%', width: "100%",
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: '8px', gap: "8px",
}} }}
> >
<Typography variant="subtitle2"> <Typography variant="subtitle2">
@ -37,7 +37,7 @@ export default function TorInfoBox() {
below. If Tor is running, all traffic will be routed through it and below. If Tor is running, all traffic will be routed through it and
the swap provider will not be able to see your IP address. the swap provider will not be able to see your IP address.
</Typography> </Typography>
<CliLogsBox label="Tor Daemon Logs" logs={torStdOut.split('\n')} /> <CliLogsBox label="Tor Daemon Logs" logs={torStdOut.split("\n")} />
</Box> </Box>
} }
additionalContent={ additionalContent={

View file

@ -1,8 +1,8 @@
import { Typography } from '@material-ui/core'; import { Typography } from "@material-ui/core";
import { useIsSwapRunning } from 'store/hooks'; import { useIsSwapRunning } from "store/hooks";
import HistoryTable from './table/HistoryTable'; import HistoryTable from "./table/HistoryTable";
import SwapDialog from '../../modal/swap/SwapDialog'; import SwapDialog from "../../modal/swap/SwapDialog";
import SwapTxLockAlertsBox from '../../alert/SwapTxLockAlertsBox'; import SwapTxLockAlertsBox from "../../alert/SwapTxLockAlertsBox";
export default function HistoryPage() { export default function HistoryPage() {
const showDialog = useIsSwapRunning(); const showDialog = useIsSwapRunning();

View file

@ -1,98 +1,86 @@
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 <IconButton size="small" onClick={() => setExpanded(!expanded)}>
size="small" {expanded ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
onClick={() => setExpanded(!expanded)} </IconButton>
> </TableCell>
{expanded ? ( <TableCell>{swap.swap_id.substring(0, 5)}...</TableCell>
<KeyboardArrowUpIcon /> <TableCell>
) : ( <AmountTransfer xmrAmount={xmrAmount} btcAmount={btcAmount} />
<KeyboardArrowDownIcon /> </TableCell>
)} <TableCell>{getHumanReadableDbStateType(swap.state_name)}</TableCell>
</IconButton> <TableCell>
</TableCell> <HistoryRowActions swap={swap} />
<TableCell>{swap.swap_id.substring(0, 5)}...</TableCell> </TableCell>
<TableCell> </TableRow>
<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

@ -6,85 +6,85 @@ 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.swap_id]} 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.state_name) || isSwapStateNamePossiblyCancellableSwap(swap.state_name) ||
isSwapStateNamePossiblyRefundableSwap(swap.state_name); isSwapStateNamePossiblyRefundableSwap(swap.state_name);
if (!cancelOrRefundable) { if (!cancelOrRefundable) {
return <></>; return <></>;
} }
return ( return (
<IpcInvokeButton <IpcInvokeButton
ipcChannel="spawn-cancel-refund" ipcChannel="spawn-cancel-refund"
ipcArgs={[swap.swap_id]} 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.state_name === 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.state_name === 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.state_name === 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,143 +1,134 @@
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, start_date: 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.swap_id}</TableCell> <TableCell>{swap.swap_id}</TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell>State Name</TableCell> <TableCell>State Name</TableCell>
<TableCell> <TableCell>
{getHumanReadableDbStateType(swap.state_name)} {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 <MoneroBitcoinExchangeRate rate={exchangeRate} />
rate={exchangeRate} </TableCell>
/> </TableRow>
</TableCell> <TableRow>
</TableRow> <TableCell>Bitcoin Network Fees</TableCell>
<TableRow> <TableCell>
<TableCell>Bitcoin Network Fees</TableCell> <BitcoinAmount amount={txFees} />
<TableCell> </TableCell>
<BitcoinAmount amount={txFees} /> </TableRow>
</TableCell> <TableRow>
</TableRow> <TableCell>Provider Address</TableCell>
<TableRow> <TableCell>
<TableCell>Provider Address</TableCell> <Box>{seller.addresses.join(", ")}</Box>
<TableCell> </TableCell>
<Box>{seller.addresses.join(", ")}</Box> </TableRow>
</TableCell> <TableRow>
</TableRow> <TableCell>Bitcoin lock transaction</TableCell>
<TableRow> <TableCell>
<TableCell>Bitcoin lock transaction</TableCell> <Link
<TableCell> href={getBitcoinTxExplorerUrl(swap.tx_lock_id, isTestnet())}
<Link target="_blank"
href={getBitcoinTxExplorerUrl( >
swap.tx_lock_id, {swap.tx_lock_id}
isTestnet(), </Link>
)} </TableCell>
target="_blank" </TableRow>
> </TableBody>
{swap.tx_lock_id} </Table>
</Link> </TableContainer>
</TableCell> <Box className={classes.actionsOuter}>
</TableRow> <SwapLogFileOpenButton
</TableBody> swapId={swap.swap_id}
</Table> variant="outlined"
</TableContainer> size="small"
<Box className={classes.actionsOuter}> />
<SwapLogFileOpenButton <SwapCancelRefundButton swap={swap} variant="contained" size="small" />
swapId={swap.swap_id} <SwapMoneroRecoveryButton
variant="outlined" swap={swap}
size="small" variant="contained"
/> size="small"
<SwapCancelRefundButton />
swap={swap} </Box>
variant="contained" </Box>
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.swap_id} /> <HistoryRow swap={swap} key={swap.swap_id} />
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
</Box> </Box>
); );
} }

View file

@ -1,15 +1,15 @@
import { ButtonProps } from '@material-ui/core/Button/Button'; import { ButtonProps } from "@material-ui/core/Button/Button";
import { import {
Button, Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
} from '@material-ui/core'; } from "@material-ui/core";
import { useState } from 'react'; import { useState } from "react";
import { CliLog } from 'models/cliModel'; import { CliLog } from "models/cliModel";
import IpcInvokeButton from '../../../IpcInvokeButton'; import IpcInvokeButton from "../../../IpcInvokeButton";
import CliLogsBox from '../../../other/RenderedCliLog'; import CliLogsBox from "../../../other/RenderedCliLog";
export default function SwapLogFileOpenButton({ export default function SwapLogFileOpenButton({
swapId, swapId,

View file

@ -1,120 +1,119 @@
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.swap_id) { 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.swap_id.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> ))}
<DialogContentText> </Box>
You can use the keys below to manually redeem the Monero </DialogContent>
funds from the multi-signature wallet. <DialogActions>
<ul> <Button onClick={onClose} color="primary" variant="contained">
<li> Done
This is useful if the swap daemon fails to redeem </Button>
the funds itself </DialogActions>
</li> </Dialog>
<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.state_name); 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.swap_id]} 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 { Box } from '@material-ui/core'; import { Box } from "@material-ui/core";
import { Alert, AlertTitle } from '@material-ui/lab'; import { Alert, AlertTitle } from "@material-ui/lab";
import { removeAlert } from 'store/features/alertsSlice'; import { removeAlert } from "store/features/alertsSlice";
import { useAppDispatch, useAppSelector } from 'store/hooks'; import { useAppDispatch, useAppSelector } from "store/hooks";
export default function ApiAlertsBox() { export default function ApiAlertsBox() {
const alerts = useAppSelector((state) => state.alerts.alerts); const alerts = useAppSelector((state) => state.alerts.alerts);
@ -14,7 +14,7 @@ export default function ApiAlertsBox() {
if (alerts.length === 0) return null; if (alerts.length === 0) return null;
return ( return (
<Box style={{ display: 'flex', justifyContent: 'center', gap: '1rem' }}> <Box style={{ display: "flex", justifyContent: "center", gap: "1rem" }}>
{alerts.map((alert) => ( {alerts.map((alert) => (
<Alert <Alert
variant="filled" variant="filled"

View file

@ -1,13 +1,13 @@
import { Box, makeStyles } from '@material-ui/core'; import { Box, makeStyles } from "@material-ui/core";
import SwapWidget from './SwapWidget'; import SwapWidget from "./SwapWidget";
import ApiAlertsBox from './ApiAlertsBox'; import ApiAlertsBox from "./ApiAlertsBox";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {
display: 'flex', display: "flex",
width: '100%', width: "100%",
flexDirection: 'column', flexDirection: "column",
alignItems: 'center', alignItems: "center",
paddingBottom: theme.spacing(1), paddingBottom: theme.spacing(1),
gap: theme.spacing(1), gap: theme.spacing(1),
}, },

View file

@ -1,4 +1,4 @@
import { ChangeEvent, useEffect, useState } from 'react'; import { ChangeEvent, useEffect, useState } from "react";
import { import {
makeStyles, makeStyles,
Box, Box,
@ -7,21 +7,21 @@ import {
TextField, TextField,
LinearProgress, LinearProgress,
Fab, Fab,
} from '@material-ui/core'; } from "@material-ui/core";
import InputAdornment from '@material-ui/core/InputAdornment'; import InputAdornment from "@material-ui/core/InputAdornment";
import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'; import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward";
import SwapHorizIcon from '@material-ui/icons/SwapHoriz'; import SwapHorizIcon from "@material-ui/icons/SwapHoriz";
import { Alert } from '@material-ui/lab'; import { Alert } from "@material-ui/lab";
import { satsToBtc } from 'utils/conversionUtils'; import { satsToBtc } from "utils/conversionUtils";
import { useAppSelector } from 'store/hooks'; import { useAppSelector } from "store/hooks";
import { ExtendedProviderStatus } from 'models/apiModel'; import { ExtendedProviderStatus } from "models/apiModel";
import { isSwapState } from 'models/storeModel'; import { isSwapState } from "models/storeModel";
import SwapDialog from '../../modal/swap/SwapDialog'; import SwapDialog from "../../modal/swap/SwapDialog";
import ProviderSelect from '../../modal/provider/ProviderSelect'; import ProviderSelect from "../../modal/provider/ProviderSelect";
import { import {
ListSellersDialogOpenButton, ListSellersDialogOpenButton,
ProviderSubmitDialogOpenButton, ProviderSubmitDialogOpenButton,
} from '../../modal/provider/ProviderListDialog'; } from "../../modal/provider/ProviderListDialog";
// After RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN failed reconnection attempts we can assume the public registry is down // After RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN failed reconnection attempts we can assume the public registry is down
const RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN = 1; const RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN = 1;
@ -32,9 +32,9 @@ function isRegistryDown(reconnectionAttempts: number): boolean {
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
inner: { inner: {
width: 'min(480px, 100%)', width: "min(480px, 100%)",
minHeight: '150px', minHeight: "150px",
display: 'grid', display: "grid",
padding: theme.spacing(1), padding: theme.spacing(1),
gridGap: theme.spacing(1), gridGap: theme.spacing(1),
}, },
@ -48,19 +48,19 @@ const useStyles = makeStyles((theme) => ({
padding: theme.spacing(1), padding: theme.spacing(1),
}, },
swapIconOuter: { swapIconOuter: {
display: 'flex', display: "flex",
justifyContent: 'center', justifyContent: "center",
}, },
swapIcon: { swapIcon: {
marginRight: theme.spacing(1), marginRight: theme.spacing(1),
}, },
noProvidersAlertOuter: { noProvidersAlertOuter: {
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gap: theme.spacing(1), gap: theme.spacing(1),
}, },
noProvidersAlertButtonsOuter: { noProvidersAlertButtonsOuter: {
display: 'flex', display: "flex",
gap: theme.spacing(1), gap: theme.spacing(1),
}, },
})); }));
@ -111,7 +111,7 @@ function HasProviderSwapWidget({
function getBtcFieldError(): string | null { function getBtcFieldError(): string | null {
const parsedBtcAmount = Number(btcFieldValue); const parsedBtcAmount = Number(btcFieldValue);
if (Number.isNaN(parsedBtcAmount)) { if (Number.isNaN(parsedBtcAmount)) {
return 'This is not a valid number'; return "This is not a valid number";
} }
if (parsedBtcAmount < satsToBtc(selectedProvider.minSwapAmount)) { if (parsedBtcAmount < satsToBtc(selectedProvider.minSwapAmount)) {
return `The minimum swap amount is ${satsToBtc( return `The minimum swap amount is ${satsToBtc(

View file

@ -1,11 +1,11 @@
import { Box, makeStyles, Typography } from '@material-ui/core'; import { Box, makeStyles, Typography } from "@material-ui/core";
import { Alert } from '@material-ui/lab'; import { Alert } from "@material-ui/lab";
import WithdrawWidget from './WithdrawWidget'; import WithdrawWidget from "./WithdrawWidget";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
outer: { outer: {
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
gridGap: theme.spacing(0.5), gridGap: theme.spacing(0.5),
}, },
})); }));

View file

@ -1,18 +1,18 @@
import { Box, Button, makeStyles, Typography } from '@material-ui/core'; import { Box, Button, makeStyles, Typography } from "@material-ui/core";
import { useState } from 'react'; import { useState } from "react";
import SendIcon from '@material-ui/icons/Send'; import SendIcon from "@material-ui/icons/Send";
import { useAppSelector, useIsRpcEndpointBusy } from 'store/hooks'; import { useAppSelector, useIsRpcEndpointBusy } from "store/hooks";
import { RpcMethod } from 'models/rpcModel'; import { RpcMethod } from "models/rpcModel";
import BitcoinIcon from '../../icons/BitcoinIcon'; import BitcoinIcon from "../../icons/BitcoinIcon";
import WithdrawDialog from '../../modal/wallet/WithdrawDialog'; import WithdrawDialog from "../../modal/wallet/WithdrawDialog";
import WalletRefreshButton from './WalletRefreshButton'; import WalletRefreshButton from "./WalletRefreshButton";
import InfoBox from '../../modal/swap/InfoBox'; import InfoBox from "../../modal/swap/InfoBox";
import { SatsAmount } from 'renderer/components/other/Units'; import { SatsAmount } from "renderer/components/other/Units";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
title: { title: {
alignItems: 'center', alignItems: "center",
display: 'flex', display: "flex",
gap: theme.spacing(0.5), gap: theme.spacing(0.5),
}, },
})); }));

View file

@ -3,14 +3,14 @@ import {
SnackbarKey, SnackbarKey,
SnackbarProvider, SnackbarProvider,
useSnackbar, useSnackbar,
} from 'notistack'; } from "notistack";
import { IconButton, styled } from '@material-ui/core'; import { IconButton, styled } from "@material-ui/core";
import { Close } from '@material-ui/icons'; import { Close } from "@material-ui/icons";
import { ReactNode } from 'react'; import { ReactNode } from "react";
const StyledMaterialDesignContent = styled(MaterialDesignContent)(() => ({ const StyledMaterialDesignContent = styled(MaterialDesignContent)(() => ({
'&.notistack-MuiContent': { "&.notistack-MuiContent": {
maxWidth: '50vw', maxWidth: "50vw",
}, },
})); }));

Some files were not shown because too many files have changed in this diff Show more