mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-20 10:55:37 -05:00
fix(gui): Strictly enforce UI types with Typescript (#678)
* fix(gui): we were not checking for null everywhere
* add more null checks, enable tsconfig strict
* remove dead code
* more nullish checks
* remove unused JSONViewTree.tsx
* fix a bunch of small typescript lints
* add explanations as to why LabeledMoneroAddress.address is non-nullish but we pass in null due to typeshare limitation
* remove @mui/lab from yarn.lock
* re-add SortableQuoteWithAddress
* add guard function for ExportBitcoinWalletResponseExt ("wallet_descriptor")
* fix remaining linter errors
* remove duplicate XMR
* fix hasUnusualAmountOfTimePassed in SwapStatusAlert.tsx
This commit is contained in:
parent
3ce8e360c5
commit
5948a40c8d
45 changed files with 312 additions and 648 deletions
|
|
@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
- GUI: Fix an issue where an error in the UI runtime would cause a white screen to be displayed and nothing would be rendered.
|
||||||
|
|
||||||
## [3.2.8] - 2025-11-02
|
## [3.2.8] - 2025-11-02
|
||||||
|
|
||||||
- ASB + CONTROLLER: Add a `registration-status` command to the controller shell. You can use it to get the registration status of the ASB at the configured rendezvous points.
|
- ASB + CONTROLLER: Add a `registration-status` command to the controller shell. You can use it to get the registration status of the ASB at the configured rendezvous points.
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@fontsource/roboto": "^5.1.0",
|
"@fontsource/roboto": "^5.1.0",
|
||||||
"@mui/icons-material": "^7.1.1",
|
"@mui/icons-material": "^7.1.1",
|
||||||
"@mui/lab": "^7.0.0-beta.13",
|
|
||||||
"@mui/material": "^7.1.1",
|
"@mui/material": "^7.1.1",
|
||||||
"@mui/x-date-pickers": "^8.8.0",
|
"@mui/x-date-pickers": "^8.8.0",
|
||||||
"@reduxjs/toolkit": "^2.3.0",
|
"@reduxjs/toolkit": "^2.3.0",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import {
|
||||||
TauriSwapProgressEvent,
|
TauriSwapProgressEvent,
|
||||||
SendMoneroDetails,
|
SendMoneroDetails,
|
||||||
ContextStatus,
|
ContextStatus,
|
||||||
|
QuoteWithAddress,
|
||||||
|
ExportBitcoinWalletResponse,
|
||||||
} from "./tauriModel";
|
} from "./tauriModel";
|
||||||
import {
|
import {
|
||||||
ContextStatusType,
|
ContextStatusType,
|
||||||
|
|
@ -17,6 +19,16 @@ import {
|
||||||
|
|
||||||
export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"];
|
export type TauriSwapProgressEventType = TauriSwapProgressEvent["type"];
|
||||||
|
|
||||||
|
// Wrapper for QuoteWithAddress with an optional approval request
|
||||||
|
// Approving that request will result in a swap being initiated with that maker
|
||||||
|
export type SortableQuoteWithAddress = {
|
||||||
|
quote_with_address: QuoteWithAddress;
|
||||||
|
approval: {
|
||||||
|
request_id: string;
|
||||||
|
expiration_ts: number;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type TauriSwapProgressEventContent<
|
export type TauriSwapProgressEventContent<
|
||||||
T extends TauriSwapProgressEventType,
|
T extends TauriSwapProgressEventType,
|
||||||
> = Extract<TauriSwapProgressEvent, { type: T }>["content"];
|
> = Extract<TauriSwapProgressEvent, { type: T }>["content"];
|
||||||
|
|
@ -354,9 +366,9 @@ export function isPendingPasswordApprovalEvent(
|
||||||
* @returns True if funds have been locked, false otherwise
|
* @returns True if funds have been locked, false otherwise
|
||||||
*/
|
*/
|
||||||
export function haveFundsBeenLocked(
|
export function haveFundsBeenLocked(
|
||||||
event: TauriSwapProgressEvent | null,
|
event: TauriSwapProgressEvent | null | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (event === null) {
|
if (event === null || event === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -372,7 +384,7 @@ export function haveFundsBeenLocked(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isContextFullyInitialized(
|
export function isContextFullyInitialized(
|
||||||
status: ResultContextStatus,
|
status: ResultContextStatus | null,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (status == null || status.type === ContextStatusType.Error) {
|
if (status == null || status.type === ContextStatusType.Error) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -396,3 +408,20 @@ export function isContextWithMoneroWallet(
|
||||||
): boolean {
|
): boolean {
|
||||||
return status?.monero_wallet_available ?? false;
|
return status?.monero_wallet_available ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ExportBitcoinWalletResponseExt = ExportBitcoinWalletResponse & {
|
||||||
|
wallet_descriptor: {
|
||||||
|
descriptor: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function hasDescriptorProperty(
|
||||||
|
response: ExportBitcoinWalletResponse,
|
||||||
|
): response is ExportBitcoinWalletResponseExt {
|
||||||
|
return (
|
||||||
|
typeof response.wallet_descriptor === "object" &&
|
||||||
|
response.wallet_descriptor !== null &&
|
||||||
|
"descriptor" in response.wallet_descriptor &&
|
||||||
|
typeof (response.wallet_descriptor as { descriptor?: unknown }).descriptor === "string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
||||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
import PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog";
|
import PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog";
|
||||||
import ContextErrorDialog from "./modal/context-error/ContextErrorDialog";
|
import ContextErrorDialog from "./modal/context-error/ContextErrorDialog";
|
||||||
|
import ErrorBoundary from "./other/ErrorBoundary";
|
||||||
|
|
||||||
declare module "@mui/material/styles" {
|
declare module "@mui/material/styles" {
|
||||||
interface Theme {
|
interface Theme {
|
||||||
|
|
@ -47,24 +48,26 @@ export default function App() {
|
||||||
console.log("Current theme:", { theme, currentTheme });
|
console.log("Current theme:", { theme, currentTheme });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledEngineProvider injectFirst>
|
<ErrorBoundary>
|
||||||
<ThemeProvider theme={currentTheme}>
|
<StyledEngineProvider injectFirst>
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
<ThemeProvider theme={currentTheme}>
|
||||||
<CssBaseline />
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
<GlobalSnackbarProvider>
|
<CssBaseline />
|
||||||
<IntroductionModal />
|
<GlobalSnackbarProvider>
|
||||||
<SeedSelectionDialog />
|
<IntroductionModal />
|
||||||
<PasswordEntryDialog />
|
<SeedSelectionDialog />
|
||||||
<ContextErrorDialog />
|
<PasswordEntryDialog />
|
||||||
<Router>
|
<ContextErrorDialog />
|
||||||
<Navigation />
|
<Router>
|
||||||
<InnerContent />
|
<Navigation />
|
||||||
<UpdaterDialog />
|
<InnerContent />
|
||||||
</Router>
|
<UpdaterDialog />
|
||||||
</GlobalSnackbarProvider>
|
</Router>
|
||||||
</LocalizationProvider>
|
</GlobalSnackbarProvider>
|
||||||
</ThemeProvider>
|
</LocalizationProvider>
|
||||||
</StyledEngineProvider>
|
</ThemeProvider>
|
||||||
|
</StyledEngineProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,13 +82,13 @@ function InnerContent() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<MoneroWalletPage />} />
|
<Route path="/" element={<ErrorBoundary><MoneroWalletPage /></ErrorBoundary>} />
|
||||||
<Route path="/monero-wallet" element={<MoneroWalletPage />} />
|
<Route path="/monero-wallet" element={<ErrorBoundary><MoneroWalletPage /></ErrorBoundary>} />
|
||||||
<Route path="/swap" element={<SwapPage />} />
|
<Route path="/swap" element={<ErrorBoundary><SwapPage /></ErrorBoundary>} />
|
||||||
<Route path="/history" element={<HistoryPage />} />
|
<Route path="/history" element={<ErrorBoundary><HistoryPage /></ErrorBoundary>} />
|
||||||
<Route path="/bitcoin-wallet" element={<WalletPage />} />
|
<Route path="/bitcoin-wallet" element={<ErrorBoundary><WalletPage /></ErrorBoundary>} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<ErrorBoundary><SettingsPage /></ErrorBoundary>} />
|
||||||
<Route path="/feedback" element={<FeedbackPage />} />
|
<Route path="/feedback" element={<ErrorBoundary><FeedbackPage /></ErrorBoundary>} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ interface PromiseInvokeButtonProps<T> {
|
||||||
|
|
||||||
export default function PromiseInvokeButton<T>({
|
export default function PromiseInvokeButton<T>({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
onSuccess = null,
|
onSuccess,
|
||||||
onInvoke,
|
onInvoke,
|
||||||
children,
|
children,
|
||||||
startIcon,
|
startIcon,
|
||||||
|
|
@ -44,7 +44,7 @@ export default function PromiseInvokeButton<T>({
|
||||||
isIconButton = false,
|
isIconButton = false,
|
||||||
isChipButton = false,
|
isChipButton = false,
|
||||||
displayErrorSnackbar = false,
|
displayErrorSnackbar = false,
|
||||||
onPendingChange = null,
|
onPendingChange,
|
||||||
contextRequirement = true,
|
contextRequirement = true,
|
||||||
tooltipTitle = null,
|
tooltipTitle = null,
|
||||||
...rest
|
...rest
|
||||||
|
|
|
||||||
|
|
@ -69,11 +69,7 @@ function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt }) {
|
||||||
"If this step fails, you can manually redeem your funds",
|
"If this step fails, you can manually redeem your funds",
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<SwapMoneroRecoveryButton
|
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
|
||||||
swap={swap}
|
|
||||||
size="small"
|
|
||||||
variant="contained"
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -208,10 +204,7 @@ export function StateAlert({
|
||||||
);
|
);
|
||||||
case "Cancel":
|
case "Cancel":
|
||||||
return (
|
return (
|
||||||
<BitcoinPossiblyCancelledAlert
|
<BitcoinPossiblyCancelledAlert timelock={timelock} swap={swap} />
|
||||||
timelock={timelock}
|
|
||||||
swap={swap}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
case "Punish":
|
case "Punish":
|
||||||
return <PunishTimelockExpiredAlert />;
|
return <PunishTimelockExpiredAlert />;
|
||||||
|
|
@ -235,6 +228,7 @@ export function StateAlert({
|
||||||
// 72 is the default cancel timelock in blocks
|
// 72 is the default cancel timelock in blocks
|
||||||
// 4 blocks are around 40 minutes
|
// 4 blocks are around 40 minutes
|
||||||
// If the swap has taken longer than 40 minutes, we consider it unusual
|
// If the swap has taken longer than 40 minutes, we consider it unusual
|
||||||
|
// See: swap-env/src/env.rs
|
||||||
const UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD = 72 - 4;
|
const UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD = 72 - 4;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -246,10 +240,16 @@ export default function SwapStatusAlert({
|
||||||
swap,
|
swap,
|
||||||
onlyShowIfUnusualAmountOfTimeHasPassed,
|
onlyShowIfUnusualAmountOfTimeHasPassed,
|
||||||
}: {
|
}: {
|
||||||
swap: GetSwapInfoResponseExt;
|
swap: GetSwapInfoResponseExt | null;
|
||||||
onlyShowIfUnusualAmountOfTimeHasPassed?: boolean;
|
onlyShowIfUnusualAmountOfTimeHasPassed?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const timelock = useAppSelector(selectSwapTimelock(swap.swap_id));
|
const swapId = swap?.swap_id ?? null;
|
||||||
|
const timelock = useAppSelector(selectSwapTimelock(swapId));
|
||||||
|
const isRunning = useIsSpecificSwapRunning(swapId);
|
||||||
|
|
||||||
|
if (swap == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isGetSwapInfoResponseRunningSwap(swap)) {
|
if (!isGetSwapInfoResponseRunningSwap(swap)) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -263,12 +263,10 @@ export default function SwapStatusAlert({
|
||||||
timelock.type === "None" &&
|
timelock.type === "None" &&
|
||||||
timelock.content.blocks_left > UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD;
|
timelock.content.blocks_left > UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD;
|
||||||
|
|
||||||
if (onlyShowIfUnusualAmountOfTimeHasPassed && hasUnusualAmountOfTimePassed) {
|
if (onlyShowIfUnusualAmountOfTimeHasPassed && !hasUnusualAmountOfTimePassed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRunning = useIsSpecificSwapRunning(swap.swap_id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
key={swap.swap_id}
|
key={swap.swap_id}
|
||||||
|
|
@ -290,8 +288,7 @@ export default function SwapStatusAlert({
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is
|
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running
|
||||||
not running
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
@ -303,7 +300,7 @@ export default function SwapStatusAlert({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StateAlert swap={swap} timelock={timelock} isRunning={isRunning} />
|
<StateAlert swap={swap} timelock={timelock} isRunning={isRunning} />
|
||||||
<TimelockTimeline swap={swap} timelock={timelock} />
|
{timelock && <TimelockTimeline swap={swap} timelock={timelock} />}
|
||||||
</Box>
|
</Box>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ function TimelineSegment({
|
||||||
opacity: isActive ? 1 : 0.3,
|
opacity: isActive ? 1 : 0.3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isActive && (
|
{isActive && durationOfSegment && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
import { Box, Chip, Paper, Tooltip, Typography } from "@mui/material";
|
|
||||||
import { VerifiedUser } from "@mui/icons-material";
|
|
||||||
import { ExtendedMakerStatus } from "models/apiModel";
|
|
||||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
|
||||||
import {
|
|
||||||
MoneroBitcoinExchangeRate,
|
|
||||||
SatsAmount,
|
|
||||||
} from "renderer/components/other/Units";
|
|
||||||
import { getMarkup, satsToBtc, secondsToDays } from "utils/conversionUtils";
|
|
||||||
import { isMakerOutdated, isMakerVersionOutdated } from "utils/multiAddrUtils";
|
|
||||||
import WarningIcon from "@mui/icons-material/Warning";
|
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
import IdentIcon from "renderer/components/icons/IdentIcon";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A chip that displays the markup of the maker's exchange rate compared to the market rate.
|
|
||||||
*/
|
|
||||||
function MakerMarkupChip({ maker }: { maker: ExtendedMakerStatus }) {
|
|
||||||
const marketExchangeRate = useAppSelector((s) => s.rates?.xmrBtcRate);
|
|
||||||
if (marketExchangeRate == null) return null;
|
|
||||||
|
|
||||||
const makerExchangeRate = satsToBtc(maker.price);
|
|
||||||
/** The markup of the exchange rate compared to the market rate in percent */
|
|
||||||
const markup = getMarkup(makerExchangeRate, marketExchangeRate);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip title="The markup this maker charges compared to centralized markets. A lower markup means that you get more Monero for your Bitcoin.">
|
|
||||||
<Chip label={`Markup ${markup.toFixed(2)}%`} />
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MakerInfo({ maker }: { maker: ExtendedMakerStatus }) {
|
|
||||||
const isOutdated = isMakerOutdated(maker);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flex: 1,
|
|
||||||
"& *": {
|
|
||||||
lineBreak: "anywhere",
|
|
||||||
},
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
"This avatar is deterministically derived from the public key of the maker"
|
|
||||||
}
|
|
||||||
arrow
|
|
||||||
>
|
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
||||||
<IdentIcon value={maker.peerId} size={"3rem"} />
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="subtitle1">
|
|
||||||
<TruncatedText limit={16} truncateMiddle>
|
|
||||||
{maker.peerId}
|
|
||||||
</TruncatedText>
|
|
||||||
</Typography>
|
|
||||||
<Typography color="textSecondary" variant="body2">
|
|
||||||
{maker.multiAddr}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
<Typography variant="caption">
|
|
||||||
Exchange rate:{" "}
|
|
||||||
<MoneroBitcoinExchangeRate rate={satsToBtc(maker.price)} />
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption">
|
|
||||||
Minimum amount: <SatsAmount amount={maker.minSwapAmount} />
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption">
|
|
||||||
Maximum amount: <SatsAmount amount={maker.maxSwapAmount} />
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
|
||||||
{maker.testnet && <Chip label="Testnet" />}
|
|
||||||
{maker.uptime && (
|
|
||||||
<Tooltip title="A high uptime (>90%) indicates reliability. Makers with very low uptime may be unreliable and cause swaps to take longer to complete or fail entirely.">
|
|
||||||
<Chip label={`${Math.round(maker.uptime * 100)}% uptime`} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{maker.age && (
|
|
||||||
<Chip
|
|
||||||
label={`Went online ${Math.round(secondsToDays(maker.age))} ${
|
|
||||||
maker.age === 1 ? "day" : "days"
|
|
||||||
} ago`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{maker.recommended === true && (
|
|
||||||
<Tooltip title="This maker has shown to be exceptionally reliable">
|
|
||||||
<Chip label="Recommended" icon={<VerifiedUser />} color="primary" />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{isOutdated && (
|
|
||||||
<Tooltip title="This maker is running an older version of the software. Outdated makers may be unreliable and cause swaps to take longer to complete or fail entirely.">
|
|
||||||
<Chip label="Outdated" icon={<WarningIcon />} color="primary" />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{maker.version && (
|
|
||||||
<Tooltip title="The version of the maker's software">
|
|
||||||
<Chip label={`v${maker.version}`} />
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<MakerMarkupChip maker={maker} />
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
List,
|
|
||||||
ListItemAvatar,
|
|
||||||
ListItemText,
|
|
||||||
} from "@mui/material";
|
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
|
||||||
import SearchIcon from "@mui/icons-material/Search";
|
|
||||||
import { ExtendedMakerStatus } from "models/apiModel";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { setSelectedMaker } from "store/features/makersSlice";
|
|
||||||
import { useAllMakers, useAppDispatch } from "store/hooks";
|
|
||||||
import MakerInfo from "./MakerInfo";
|
|
||||||
import MakerSubmitDialog from "./MakerSubmitDialog";
|
|
||||||
|
|
||||||
import ListItemButton from "@mui/material/ListItemButton";
|
|
||||||
|
|
||||||
type MakerSelectDialogProps = {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function MakerSubmitDialogOpenButton() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItemButton
|
|
||||||
autoFocus
|
|
||||||
onClick={() => {
|
|
||||||
// Prevents background from being clicked and reopening dialog
|
|
||||||
if (!open) {
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MakerSubmitDialog open={open} onClose={() => setOpen(false)} />
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<AddIcon />
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="Add a new maker to public registry" />
|
|
||||||
</ListItemButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MakerListDialog({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
}: MakerSelectDialogProps) {
|
|
||||||
const makers = useAllMakers();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
function handleMakerChange(maker: ExtendedMakerStatus) {
|
|
||||||
dispatch(setSelectedMaker(maker));
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog onClose={onClose} open={open}>
|
|
||||||
<DialogTitle>Select a maker</DialogTitle>
|
|
||||||
<DialogContent sx={{ padding: 0 }} dividers>
|
|
||||||
<List>
|
|
||||||
{makers.map((maker) => (
|
|
||||||
<ListItemButton
|
|
||||||
onClick={() => handleMakerChange(maker)}
|
|
||||||
key={maker.peerId}
|
|
||||||
>
|
|
||||||
<MakerInfo maker={maker} key={maker.peerId} />
|
|
||||||
</ListItemButton>
|
|
||||||
))}
|
|
||||||
<MakerSubmitDialogOpenButton />
|
|
||||||
</List>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={onClose}>Cancel</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
import { Paper, Card, CardContent, IconButton } from "@mui/material";
|
|
||||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useAppSelector } from "store/hooks";
|
|
||||||
import MakerInfo from "./MakerInfo";
|
|
||||||
import MakerListDialog from "./MakerListDialog";
|
|
||||||
|
|
||||||
export default function MakerSelect() {
|
|
||||||
const [selectDialogOpen, setSelectDialogOpen] = useState(false);
|
|
||||||
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
|
||||||
|
|
||||||
if (!selectedMaker) return <>No maker selected</>;
|
|
||||||
|
|
||||||
function handleSelectDialogClose() {
|
|
||||||
setSelectDialogOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSelectDialogOpen() {
|
|
||||||
setSelectDialogOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper variant="outlined" elevation={4}>
|
|
||||||
<MakerListDialog
|
|
||||||
open={selectDialogOpen}
|
|
||||||
onClose={handleSelectDialogClose}
|
|
||||||
/>
|
|
||||||
<Card sx={{ width: "100%" }}>
|
|
||||||
<CardContent sx={{ display: "flex", alignItems: "center" }}>
|
|
||||||
<MakerInfo maker={selectedMaker} />
|
|
||||||
<IconButton onClick={handleSelectDialogOpen} size="small">
|
|
||||||
<ArrowForwardIosIcon />
|
|
||||||
</IconButton>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogActions,
|
|
||||||
DialogContent,
|
|
||||||
DialogContentText,
|
|
||||||
DialogTitle,
|
|
||||||
TextField,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { Multiaddr } from "multiaddr";
|
|
||||||
import { ChangeEvent, useState } from "react";
|
|
||||||
|
|
||||||
type MakerSubmitDialogProps = {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function MakerSubmitDialog({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
}: MakerSubmitDialogProps) {
|
|
||||||
const [multiAddr, setMultiAddr] = useState("");
|
|
||||||
const [peerId, setPeerId] = useState("");
|
|
||||||
|
|
||||||
async function handleMakerSubmit() {
|
|
||||||
if (multiAddr && peerId) {
|
|
||||||
await fetch("https://api.unstoppableswap.net/api/submit-provider", {
|
|
||||||
method: "post",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
multiAddr,
|
|
||||||
peerId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
setMultiAddr("");
|
|
||||||
setPeerId("");
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMultiAddrChange(event: ChangeEvent<HTMLInputElement>) {
|
|
||||||
setMultiAddr(event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePeerIdChange(event: ChangeEvent<HTMLInputElement>) {
|
|
||||||
setPeerId(event.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMultiAddressError(): string | null {
|
|
||||||
try {
|
|
||||||
const multiAddress = new Multiaddr(multiAddr);
|
|
||||||
if (multiAddress.protoNames().includes("p2p")) {
|
|
||||||
return "The multi address should not contain the peer id (/p2p/)";
|
|
||||||
}
|
|
||||||
if (multiAddress.protoNames().find((name) => name.includes("onion"))) {
|
|
||||||
return "It is currently not possible to add a maker that is only reachable via Tor";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
return "Not a valid multi address";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog onClose={onClose} open={open}>
|
|
||||||
<DialogTitle>Submit a maker to the public registry</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<DialogContentText>
|
|
||||||
If the maker is valid and reachable, it will be displayed to all other
|
|
||||||
users to trade with.
|
|
||||||
</DialogContentText>
|
|
||||||
<TextField
|
|
||||||
autoFocus
|
|
||||||
margin="dense"
|
|
||||||
label="Multiaddress"
|
|
||||||
fullWidth
|
|
||||||
helperText={
|
|
||||||
getMultiAddressError() ||
|
|
||||||
"Tells the swap client where the maker can be reached"
|
|
||||||
}
|
|
||||||
value={multiAddr}
|
|
||||||
onChange={handleMultiAddrChange}
|
|
||||||
placeholder="/ip4/182.3.21.93/tcp/9939"
|
|
||||||
error={!!getMultiAddressError()}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
margin="dense"
|
|
||||||
label="Peer ID"
|
|
||||||
fullWidth
|
|
||||||
helperText="Identifies the maker and allows for secure communication"
|
|
||||||
value={peerId}
|
|
||||||
onChange={handlePeerIdChange}
|
|
||||||
placeholder="12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={onClose}>Cancel</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={handleMakerSubmit}
|
|
||||||
disabled={!(multiAddr && peerId && !getMultiAddressError())}
|
|
||||||
color="primary"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -49,7 +49,7 @@ function LinearProgressWithLabel(
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="body2" color="textSecondary">
|
<Typography variant="body2" color="textSecondary">
|
||||||
{props.label || `${Math.round(props.value)}%`}
|
{props.label || `${Math.round(props.value ?? 0)}%`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -84,6 +84,8 @@ export default function UpdaterDialog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInstall() {
|
async function handleInstall() {
|
||||||
|
if (!availableUpdate) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await availableUpdate.downloadAndInstall((event: DownloadEvent) => {
|
await availableUpdate.downloadAndInstall((event: DownloadEvent) => {
|
||||||
if (event.event === "Started") {
|
if (event.event === "Started") {
|
||||||
|
|
@ -92,10 +94,13 @@ export default function UpdaterDialog() {
|
||||||
downloadedBytes: 0,
|
downloadedBytes: 0,
|
||||||
});
|
});
|
||||||
} else if (event.event === "Progress") {
|
} else if (event.event === "Progress") {
|
||||||
setDownloadProgress((prev) => ({
|
setDownloadProgress((prev) => {
|
||||||
...prev,
|
if (!prev) return null;
|
||||||
downloadedBytes: prev.downloadedBytes + event.data.chunkLength,
|
return {
|
||||||
}));
|
contentLength: prev.contentLength,
|
||||||
|
downloadedBytes: prev.downloadedBytes + event.data.chunkLength,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -110,12 +115,13 @@ export default function UpdaterDialog() {
|
||||||
|
|
||||||
const isDownloading = downloadProgress !== null;
|
const isDownloading = downloadProgress !== null;
|
||||||
|
|
||||||
const progress = isDownloading
|
const progress =
|
||||||
? Math.round(
|
isDownloading && downloadProgress.contentLength
|
||||||
(downloadProgress.downloadedBytes / downloadProgress.contentLength) *
|
? Math.round(
|
||||||
100,
|
(downloadProgress.downloadedBytes / downloadProgress.contentLength) *
|
||||||
)
|
100,
|
||||||
: 0;
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Button, Dialog, DialogActions } from "@mui/material";
|
import { Button, Dialog, DialogActions } from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { withdrawBtc } from "renderer/rpc";
|
import { sweepBtc } from "renderer/rpc";
|
||||||
import DialogHeader from "../DialogHeader";
|
import DialogHeader from "../DialogHeader";
|
||||||
import AddressInputPage from "./pages/AddressInputPage";
|
import AddressInputPage from "./pages/AddressInputPage";
|
||||||
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
|
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
|
||||||
|
|
@ -59,7 +59,7 @@ export default function WithdrawDialog({
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={!withdrawAddressValid}
|
disabled={!withdrawAddressValid}
|
||||||
onInvoke={() => withdrawBtc(withdrawAddress)}
|
onInvoke={() => sweepBtc(withdrawAddress)}
|
||||||
onPendingChange={setPending}
|
onPendingChange={setPending}
|
||||||
onSuccess={setWithdrawTxId}
|
onSuccess={setWithdrawTxId}
|
||||||
contextRequirement={isContextWithBitcoinWallet}
|
contextRequirement={isContextWithBitcoinWallet}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ type ModalProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: string;
|
content: string | null;
|
||||||
displayCopyIcon?: boolean;
|
displayCopyIcon?: boolean;
|
||||||
enableQrCode?: boolean;
|
enableQrCode?: boolean;
|
||||||
light?: boolean;
|
light?: boolean;
|
||||||
|
|
@ -72,6 +72,7 @@ export default function ActionableMonospaceTextBox({
|
||||||
const [isRevealed, setIsRevealed] = useState(!spoilerText);
|
const [isRevealed, setIsRevealed] = useState(!spoilerText);
|
||||||
|
|
||||||
const handleCopy = async () => {
|
const handleCopy = async () => {
|
||||||
|
if (!content) return;
|
||||||
await writeText(content);
|
await writeText(content);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
|
@ -160,7 +161,7 @@ export default function ActionableMonospaceTextBox({
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{enableQrCode && (
|
{enableQrCode && content && (
|
||||||
<QRCodeModal
|
<QRCodeModal
|
||||||
open={qrCodeOpen}
|
open={qrCodeOpen}
|
||||||
onClose={() => setQrCodeOpen(false)}
|
onClose={() => setQrCodeOpen(false)}
|
||||||
|
|
|
||||||
52
src-gui/src/renderer/components/other/ErrorBoundary.tsx
Normal file
52
src-gui/src/renderer/components/other/ErrorBoundary.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Vendored from https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/error_boundaries/
|
||||||
|
import React, { Component, ErrorInfo, ReactNode } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
hasError: false,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
public static getDerivedStateFromError(error: Error): State {
|
||||||
|
// Update state so the next render will show the fallback UI.
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error("Uncaught error:", error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Sorry.. there was an error</h1>
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
||||||
|
{this.state.error?.message}
|
||||||
|
</pre>
|
||||||
|
{this.state.error?.stack && (
|
||||||
|
<details style={{ marginTop: '1rem' }}>
|
||||||
|
<summary>Stack trace</summary>
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: '0.875rem' }}>
|
||||||
|
{this.state.error.stack}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
|
||||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
||||||
import TreeItem from "@mui/lab/TreeItem";
|
|
||||||
import TreeView from "@mui/lab/TreeView";
|
|
||||||
import ScrollablePaperTextBox from "./ScrollablePaperTextBox";
|
|
||||||
|
|
||||||
interface JsonTreeViewProps {
|
|
||||||
data: unknown;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function JsonTreeView({ data, label }: JsonTreeViewProps) {
|
|
||||||
const renderTree = (nodes: unknown, parentId: string) => {
|
|
||||||
return Object.keys(nodes).map((key, _) => {
|
|
||||||
const nodeId = `${parentId}.${key}`;
|
|
||||||
if (typeof nodes[key] === "object" && nodes[key] !== null) {
|
|
||||||
return (
|
|
||||||
<TreeItem nodeId={nodeId} label={key} key={nodeId}>
|
|
||||||
{renderTree(nodes[key], nodeId)}
|
|
||||||
</TreeItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<TreeItem
|
|
||||||
nodeId={nodeId}
|
|
||||||
label={`${key}: ${nodes[key]}`}
|
|
||||||
key={nodeId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScrollablePaperTextBox
|
|
||||||
title={label}
|
|
||||||
copyValue={JSON.stringify(data, null, 4)}
|
|
||||||
rows={[
|
|
||||||
<TreeView
|
|
||||||
key={1}
|
|
||||||
defaultCollapseIcon={<ExpandMoreIcon />}
|
|
||||||
defaultExpandIcon={<ChevronRightIcon />}
|
|
||||||
defaultExpanded={["root"]}
|
|
||||||
>
|
|
||||||
<TreeItem nodeId="root" label={label}>
|
|
||||||
{renderTree(data ?? {}, "root")}
|
|
||||||
</TreeItem>
|
|
||||||
</TreeView>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -108,7 +108,7 @@ export default function ScrollablePaperTextBox({
|
||||||
<IconButton onClick={scrollToBottom} size="small">
|
<IconButton onClick={scrollToBottom} size="small">
|
||||||
<KeyboardArrowDownIcon />
|
<KeyboardArrowDownIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
{searchQuery !== undefined && setSearchQuery !== undefined && (
|
{searchQuery !== null && setSearchQuery !== null && (
|
||||||
<ExpandableSearchBox query={searchQuery} setQuery={setSearchQuery} />
|
<ExpandableSearchBox query={searchQuery} setQuery={setSearchQuery} />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default function TruncatedText({
|
||||||
ellipsis?: string;
|
ellipsis?: string;
|
||||||
truncateMiddle?: boolean;
|
truncateMiddle?: boolean;
|
||||||
}) {
|
}) {
|
||||||
let finalChildren = children ?? "";
|
const finalChildren = children ?? "";
|
||||||
|
|
||||||
const truncatedText =
|
const truncatedText =
|
||||||
finalChildren.length > limit
|
finalChildren.length > limit
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export function AmountWithUnit({
|
||||||
unit,
|
unit,
|
||||||
fixedPrecision,
|
fixedPrecision,
|
||||||
exchangeRate,
|
exchangeRate,
|
||||||
parenthesisText = null,
|
parenthesisText,
|
||||||
labelStyles,
|
labelStyles,
|
||||||
amountStyles,
|
amountStyles,
|
||||||
disableTooltip = false,
|
disableTooltip = false,
|
||||||
|
|
@ -18,7 +18,7 @@ export function AmountWithUnit({
|
||||||
unit: string;
|
unit: string;
|
||||||
fixedPrecision: number;
|
fixedPrecision: number;
|
||||||
exchangeRate?: Amount;
|
exchangeRate?: Amount;
|
||||||
parenthesisText?: string;
|
parenthesisText?: string | null;
|
||||||
labelStyles?: SxProps;
|
labelStyles?: SxProps;
|
||||||
amountStyles?: SxProps;
|
amountStyles?: SxProps;
|
||||||
disableTooltip?: boolean;
|
disableTooltip?: boolean;
|
||||||
|
|
@ -142,7 +142,7 @@ export function MoneroBitcoinExchangeRate({
|
||||||
}) {
|
}) {
|
||||||
const marketRate = useAppSelector((state) => state.rates?.xmrBtcRate);
|
const marketRate = useAppSelector((state) => state.rates?.xmrBtcRate);
|
||||||
const markup =
|
const markup =
|
||||||
displayMarkup && marketRate != null
|
displayMarkup && marketRate != null && rate != null
|
||||||
? `${getMarkup(rate, marketRate).toFixed(2)}% markup`
|
? `${getMarkup(rate, marketRate).toFixed(2)}% markup`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
@ -179,7 +179,7 @@ export function MoneroSatsExchangeRate({
|
||||||
rate: Amount;
|
rate: Amount;
|
||||||
displayMarkup?: boolean;
|
displayMarkup?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const btc = satsToBtc(rate);
|
const btc = rate == null ? null : satsToBtc(rate);
|
||||||
|
|
||||||
return <MoneroBitcoinExchangeRate rate={btc} displayMarkup={displayMarkup} />;
|
return <MoneroBitcoinExchangeRate rate={btc} displayMarkup={displayMarkup} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -378,10 +378,6 @@ function MessageBubble({ message }: { message: Message }) {
|
||||||
<Box
|
<Box
|
||||||
sx={(theme) => ({
|
sx={(theme) => ({
|
||||||
padding: 1.5,
|
padding: 1.5,
|
||||||
borderRadius:
|
|
||||||
typeof theme.shape.borderRadius === "number"
|
|
||||||
? theme.shape.borderRadius * 2
|
|
||||||
: 8,
|
|
||||||
maxWidth: "75%",
|
maxWidth: "75%",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
boxShadow: theme.shadows[1],
|
boxShadow: theme.shadows[1],
|
||||||
|
|
|
||||||
|
|
@ -432,7 +432,7 @@ function MoneroNodeUrlSetting() {
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
value={moneroNodeUrl}
|
value={moneroNodeUrl}
|
||||||
onValidatedChange={handleNodeUrlChange}
|
onValidatedChange={(value) => value && handleNodeUrlChange(value)}
|
||||||
placeholder={PLACEHOLDER_MONERO_NODE_URL}
|
placeholder={PLACEHOLDER_MONERO_NODE_URL}
|
||||||
disabled={useMoneroRpcPool}
|
disabled={useMoneroRpcPool}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
@ -675,7 +675,7 @@ function NodeTable({
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
label="Add a new node"
|
label="Add a new node"
|
||||||
value={newNode}
|
value={newNode}
|
||||||
onValidatedChange={setNewNode}
|
onValidatedChange={(value) => setNewNode(value ?? "")}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
fullWidth
|
fullWidth
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
|
|
@ -843,7 +843,9 @@ function RendezvousPointsSetting() {
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
label="Add new rendezvous point"
|
label="Add new rendezvous point"
|
||||||
value={newPoint}
|
value={newPoint}
|
||||||
onValidatedChange={setNewPoint}
|
onValidatedChange={(value) =>
|
||||||
|
setNewPoint(value ?? "")
|
||||||
|
}
|
||||||
placeholder="/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa"
|
placeholder="/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa"
|
||||||
fullWidth
|
fullWidth
|
||||||
isValid={isValidMultiAddressWithPeerId}
|
isValid={isValidMultiAddressWithPeerId}
|
||||||
|
|
|
||||||
|
|
@ -119,9 +119,9 @@ export function SwapMoneroRecoveryButton({
|
||||||
onInvoke={(): Promise<MoneroRecoveryResponse> =>
|
onInvoke={(): Promise<MoneroRecoveryResponse> =>
|
||||||
getMoneroRecoveryKeys(swap.swap_id)
|
getMoneroRecoveryKeys(swap.swap_id)
|
||||||
}
|
}
|
||||||
onSuccess={(keys: MoneroRecoveryResponse) =>
|
onSuccess={(keys: MoneroRecoveryResponse) => {
|
||||||
store.dispatch(rpcSetMoneroRecoveryKeys([swap.swap_id, keys]))
|
store.dispatch(rpcSetMoneroRecoveryKeys([swap.swap_id, keys]));
|
||||||
}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
Display Monero Recovery Keys
|
Display Monero Recovery Keys
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ interface SendAmountInputProps {
|
||||||
currency: string;
|
currency: string;
|
||||||
onCurrencyChange: (currency: string) => void;
|
onCurrencyChange: (currency: string) => void;
|
||||||
fiatCurrency: string;
|
fiatCurrency: string;
|
||||||
xmrPrice: number;
|
xmrPrice: number | null;
|
||||||
showFiatRate: boolean;
|
showFiatRate: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +48,10 @@ export default function SendAmountInput({
|
||||||
return "0.00";
|
return "0.00";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (xmrPrice === null) {
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
const primaryValue = parseFloat(amount);
|
const primaryValue = parseFloat(amount);
|
||||||
if (currency === "XMR") {
|
if (currency === "XMR") {
|
||||||
// Primary is XMR, secondary is USD
|
// Primary is XMR, secondary is USD
|
||||||
|
|
@ -76,7 +80,7 @@ export default function SendAmountInput({
|
||||||
|
|
||||||
if (currency === "XMR") {
|
if (currency === "XMR") {
|
||||||
onAmountChange(Math.max(0, maxAmountXmr).toString());
|
onAmountChange(Math.max(0, maxAmountXmr).toString());
|
||||||
} else {
|
} else if (xmrPrice !== null) {
|
||||||
// Convert to USD for display
|
// Convert to USD for display
|
||||||
const maxAmountUsd = maxAmountXmr * xmrPrice;
|
const maxAmountUsd = maxAmountXmr * xmrPrice;
|
||||||
onAmountChange(Math.max(0, maxAmountUsd).toString());
|
onAmountChange(Math.max(0, maxAmountUsd).toString());
|
||||||
|
|
@ -103,8 +107,9 @@ export default function SendAmountInput({
|
||||||
(currency === "XMR"
|
(currency === "XMR"
|
||||||
? parseFloat(amount) >
|
? parseFloat(amount) >
|
||||||
piconerosToXmr(parseFloat(balance.unlocked_balance))
|
piconerosToXmr(parseFloat(balance.unlocked_balance))
|
||||||
: parseFloat(amount) / xmrPrice >
|
: xmrPrice !== null &&
|
||||||
piconerosToXmr(parseFloat(balance.unlocked_balance)));
|
parseFloat(amount) / xmrPrice >
|
||||||
|
piconerosToXmr(parseFloat(balance.unlocked_balance)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|
@ -203,14 +208,11 @@ export default function SendAmountInput({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography color="text.secondary">Available</Typography>
|
<Typography color="text.secondary">Available</Typography>
|
||||||
<Box sx={{ display: "flex", alignItems: "baseline", gap: 0.5 }}>
|
<Typography color="text.primary">
|
||||||
<Typography color="text.primary">
|
<MoneroAmount
|
||||||
<MoneroAmount
|
amount={piconerosToXmr(parseFloat(balance.unlocked_balance))}
|
||||||
amount={piconerosToXmr(parseFloat(balance.unlocked_balance))}
|
/>
|
||||||
/>
|
</Typography>
|
||||||
</Typography>
|
|
||||||
<Typography color="text.secondary">XMR</Typography>
|
|
||||||
</Box>
|
|
||||||
<Button
|
<Button
|
||||||
variant={isMaxSelected ? "contained" : "secondary"}
|
variant={isMaxSelected ? "contained" : "secondary"}
|
||||||
size="tiny"
|
size="tiny"
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,9 @@ export default function SendSuccessContent({
|
||||||
}) {
|
}) {
|
||||||
const address = successDetails?.address;
|
const address = successDetails?.address;
|
||||||
const amount = successDetails?.amount_sent;
|
const amount = successDetails?.amount_sent;
|
||||||
const explorerUrl = getMoneroTxExplorerUrl(
|
const explorerUrl = successDetails?.tx_hash
|
||||||
successDetails?.tx_hash,
|
? getMoneroTxExplorerUrl(successDetails.tx_hash, isTestnet())
|
||||||
isTestnet(),
|
: null;
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|
@ -79,7 +78,7 @@ export default function SendSuccessContent({
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" color="text.primary">
|
<Typography variant="body1" color="text.primary">
|
||||||
<MonospaceTextBox>
|
<MonospaceTextBox>
|
||||||
{address.slice(0, 8)} ... {address.slice(-8)}
|
{address ? `${address.slice(0, 8)}...${address.slice(-8)}` : "?"}
|
||||||
</MonospaceTextBox>
|
</MonospaceTextBox>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
@ -98,8 +97,13 @@ export default function SendSuccessContent({
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
|
disabled={explorerUrl == null}
|
||||||
endIcon={<ArrowOutwardIcon />}
|
endIcon={<ArrowOutwardIcon />}
|
||||||
onClick={() => open(explorerUrl)}
|
onClick={() => {
|
||||||
|
if (explorerUrl != null) {
|
||||||
|
open(explorerUrl);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
View on Explorer
|
View on Explorer
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,9 @@ export default function SendTransactionContent({
|
||||||
const moneroAmount =
|
const moneroAmount =
|
||||||
currency === "XMR"
|
currency === "XMR"
|
||||||
? parseFloat(sendAmount)
|
? parseFloat(sendAmount)
|
||||||
: parseFloat(sendAmount) / xmrPrice;
|
: xmrPrice !== null
|
||||||
|
? parseFloat(sendAmount) / xmrPrice
|
||||||
|
: null;
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!sendAddress) {
|
if (!sendAddress) {
|
||||||
|
|
@ -103,7 +105,7 @@ export default function SendTransactionContent({
|
||||||
amount: { type: "Sweep" },
|
amount: { type: "Sweep" },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (!sendAmount || sendAmount === "<MAX>") {
|
if (!sendAmount || sendAmount === "<MAX>" || moneroAmount === null) {
|
||||||
throw new Error("Amount is required");
|
throw new Error("Amount is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box, darken, lighten, useTheme } from "@mui/material";
|
import { Box, darken, lighten, useTheme } from "@mui/material";
|
||||||
|
|
||||||
function getColor(colorName: string) {
|
function getColor(colorName: string): string {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
switch (colorName) {
|
switch (colorName) {
|
||||||
case "primary":
|
case "primary":
|
||||||
|
|
@ -11,6 +11,8 @@ function getColor(colorName: string) {
|
||||||
return theme.palette.success.main;
|
return theme.palette.success.main;
|
||||||
case "warning":
|
case "warning":
|
||||||
return theme.palette.warning.main;
|
return theme.palette.warning.main;
|
||||||
|
default:
|
||||||
|
return theme.palette.primary.main;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import dayjs from "dayjs";
|
||||||
import TransactionItem from "./TransactionItem";
|
import TransactionItem from "./TransactionItem";
|
||||||
|
|
||||||
interface TransactionHistoryProps {
|
interface TransactionHistoryProps {
|
||||||
history?: {
|
history: {
|
||||||
transactions: TransactionInfo[];
|
transactions: TransactionInfo[];
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TransactionGroup {
|
interface TransactionGroup {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import { PiconeroAmount } from "../../../other/Units";
|
||||||
import { FiatPiconeroAmount } from "../../../other/Units";
|
import { FiatPiconeroAmount } from "../../../other/Units";
|
||||||
import StateIndicator from "./StateIndicator";
|
import StateIndicator from "./StateIndicator";
|
||||||
import humanizeDuration from "humanize-duration";
|
import humanizeDuration from "humanize-duration";
|
||||||
import { GetMoneroSyncProgressResponse } from "models/tauriModel";
|
import {
|
||||||
|
GetMoneroBalanceResponse,
|
||||||
|
GetMoneroSyncProgressResponse,
|
||||||
|
} from "models/tauriModel";
|
||||||
|
|
||||||
interface TimeEstimationResult {
|
interface TimeEstimationResult {
|
||||||
blocksLeft: number;
|
blocksLeft: number;
|
||||||
|
|
@ -16,7 +19,7 @@ interface TimeEstimationResult {
|
||||||
const AVG_MONERO_BLOCK_SIZE_KB = 130;
|
const AVG_MONERO_BLOCK_SIZE_KB = 130;
|
||||||
|
|
||||||
function useSyncTimeEstimation(
|
function useSyncTimeEstimation(
|
||||||
syncProgress: GetMoneroSyncProgressResponse | undefined,
|
syncProgress: GetMoneroSyncProgressResponse | null,
|
||||||
): TimeEstimationResult | null {
|
): TimeEstimationResult | null {
|
||||||
const poolStatus = useAppSelector((state) => state.pool.status);
|
const poolStatus = useAppSelector((state) => state.pool.status);
|
||||||
const restoreHeight = useAppSelector(
|
const restoreHeight = useAppSelector(
|
||||||
|
|
@ -80,11 +83,8 @@ function useSyncTimeEstimation(
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WalletOverviewProps {
|
interface WalletOverviewProps {
|
||||||
balance?: {
|
balance: GetMoneroBalanceResponse | null;
|
||||||
unlocked_balance: string;
|
syncProgress: GetMoneroSyncProgressResponse | null;
|
||||||
total_balance: string;
|
|
||||||
};
|
|
||||||
syncProgress?: GetMoneroSyncProgressResponse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Component for displaying wallet address and balance
|
// Component for displaying wallet address and balance
|
||||||
|
|
@ -99,10 +99,11 @@ export default function WalletOverview({
|
||||||
const poolStatus = useAppSelector((state) => state.pool.status);
|
const poolStatus = useAppSelector((state) => state.pool.status);
|
||||||
const timeEstimation = useSyncTimeEstimation(syncProgress);
|
const timeEstimation = useSyncTimeEstimation(syncProgress);
|
||||||
|
|
||||||
const pendingBalance =
|
const pendingBalance = balance
|
||||||
parseFloat(balance.total_balance) - parseFloat(balance.unlocked_balance);
|
? parseFloat(balance.total_balance) - parseFloat(balance.unlocked_balance)
|
||||||
|
: null;
|
||||||
|
|
||||||
const isSyncing = syncProgress && syncProgress.progress_percentage < 100;
|
const isSyncing = !!(syncProgress && syncProgress.progress_percentage < 100);
|
||||||
|
|
||||||
// syncProgress.progress_percentage is not good to display
|
// syncProgress.progress_percentage is not good to display
|
||||||
// assuming we have an old wallet, eventually we will always only use the last few cm of the progress bar
|
// assuming we have an old wallet, eventually we will always only use the last few cm of the progress bar
|
||||||
|
|
@ -184,18 +185,18 @@ export default function WalletOverview({
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">
|
||||||
<PiconeroAmount
|
<PiconeroAmount
|
||||||
amount={parseFloat(balance.unlocked_balance)}
|
amount={balance ? parseFloat(balance.unlocked_balance) : null}
|
||||||
fixedPrecision={4}
|
fixedPrecision={4}
|
||||||
disableTooltip
|
disableTooltip
|
||||||
/>
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
<FiatPiconeroAmount
|
<FiatPiconeroAmount
|
||||||
amount={parseFloat(balance.unlocked_balance)}
|
amount={balance ? parseFloat(balance.unlocked_balance) : null}
|
||||||
/>
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{pendingBalance > 0 && (
|
{pendingBalance !== null && pendingBalance > 0 && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,8 @@ export default function WalletPageLoadingState() {
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
||||||
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mb: 2 }}>
|
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mb: 2 }}>
|
||||||
{Array.from({ length: 2 }).map((_) => (
|
{Array.from({ length: 2 }).map((_, i) => (
|
||||||
<Skeleton variant="rounded" sx={{ borderRadius: "100px" }}>
|
<Skeleton key={i} variant="rounded" sx={{ borderRadius: "100px" }}>
|
||||||
<Chip label="Loading..." variant="button" />
|
<Chip label="Loading..." variant="button" />
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Box, LinearProgress, Paper, Typography } from "@mui/material";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id?: string;
|
id?: string | null;
|
||||||
title: ReactNode | null;
|
title: ReactNode | null;
|
||||||
mainContent: ReactNode;
|
mainContent: ReactNode;
|
||||||
additionalContent: ReactNode;
|
additionalContent: ReactNode;
|
||||||
|
|
@ -11,7 +11,7 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function InfoBox({
|
export default function InfoBox({
|
||||||
id = null,
|
id,
|
||||||
title,
|
title,
|
||||||
mainContent,
|
mainContent,
|
||||||
additionalContent,
|
additionalContent,
|
||||||
|
|
@ -21,7 +21,7 @@ export default function InfoBox({
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
id={id}
|
id={id ?? undefined}
|
||||||
sx={{
|
sx={{
|
||||||
padding: 1.5,
|
padding: 1.5,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,8 @@ export default function DepositAndChooseOfferPage({
|
||||||
return (
|
return (
|
||||||
<MakerOfferItem
|
<MakerOfferItem
|
||||||
key={startIndex + index}
|
key={startIndex + index}
|
||||||
quoteWithAddress={quote}
|
quoteWithAddress={quote.quote_with_address}
|
||||||
requestId={quote.request_id}
|
requestId={quote.approval?.request_id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,12 @@ export default function MakerOfferItem({
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
<PromiseInvokeButton
|
<PromiseInvokeButton
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onInvoke={() => resolveApproval(requestId, true as unknown as object)}
|
onInvoke={() => {
|
||||||
|
if (!requestId) {
|
||||||
|
throw new Error("Request ID is required");
|
||||||
|
}
|
||||||
|
return resolveApproval(requestId, true as unknown as object);
|
||||||
|
}}
|
||||||
displayErrorSnackbar
|
displayErrorSnackbar
|
||||||
disabled={!requestId}
|
disabled={!requestId}
|
||||||
tooltipTitle={
|
tooltipTitle={
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,10 @@ import ActionableMonospaceTextBox from "renderer/components/other/ActionableMono
|
||||||
import { getWalletDescriptor } from "renderer/rpc";
|
import { getWalletDescriptor } from "renderer/rpc";
|
||||||
import { ExportBitcoinWalletResponse } from "models/tauriModel";
|
import { ExportBitcoinWalletResponse } from "models/tauriModel";
|
||||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||||
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
|
import {
|
||||||
|
isContextWithBitcoinWallet,
|
||||||
|
hasDescriptorProperty,
|
||||||
|
} from "models/tauriModelExt";
|
||||||
|
|
||||||
const WALLET_DESCRIPTOR_DOCS_URL =
|
const WALLET_DESCRIPTOR_DOCS_URL =
|
||||||
"https://github.com/eigenwallet/core/blob/master/dev-docs/asb/README.md#exporting-the-bitcoin-wallet-descriptor";
|
"https://github.com/eigenwallet/core/blob/master/dev-docs/asb/README.md#exporting-the-bitcoin-wallet-descriptor";
|
||||||
|
|
@ -58,8 +61,12 @@ function WalletDescriptorModal({
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
walletDescriptor: ExportBitcoinWalletResponse;
|
walletDescriptor: ExportBitcoinWalletResponse;
|
||||||
}) {
|
}) {
|
||||||
|
if (!hasDescriptorProperty(walletDescriptor)) {
|
||||||
|
throw new Error("Wallet descriptor does not have descriptor property");
|
||||||
|
}
|
||||||
|
|
||||||
const parsedDescriptor = JSON.parse(
|
const parsedDescriptor = JSON.parse(
|
||||||
walletDescriptor.wallet_descriptor["descriptor"],
|
walletDescriptor.wallet_descriptor.descriptor,
|
||||||
);
|
);
|
||||||
const stringifiedDescriptor = JSON.stringify(parsedDescriptor, null, 4);
|
const stringifiedDescriptor = JSON.stringify(parsedDescriptor, null, 4);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ declare module "@mui/material/styles" {
|
||||||
tint?: string;
|
tint?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaletteColorOptions {
|
interface SimplePaletteColorOptions {
|
||||||
tint?: string;
|
tint?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,16 +61,6 @@ const baseTheme: ThemeOptions = {
|
||||||
backgroundColor: "color-mix(in srgb, #bdbdbd 10%, transparent)",
|
backgroundColor: "color-mix(in srgb, #bdbdbd 10%, transparent)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sizeTiny: {
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: "4px 8px",
|
|
||||||
minHeight: "24px",
|
|
||||||
minWidth: "auto",
|
|
||||||
lineHeight: 1.2,
|
|
||||||
textTransform: "none",
|
|
||||||
borderRadius: "4px",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
variants: [
|
variants: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,13 @@ export async function buyXmr() {
|
||||||
|
|
||||||
address_pool.push(
|
address_pool.push(
|
||||||
{
|
{
|
||||||
address: moneroReceiveAddress,
|
// We need to assert this as being not null even though it can be null
|
||||||
|
//
|
||||||
|
// This is correct because a LabeledMoneroAddress can actually have a null address but
|
||||||
|
// typeshare cannot express that yet (easily)
|
||||||
|
//
|
||||||
|
// TODO: Let typescript do its job here and not assert it
|
||||||
|
address: moneroReceiveAddress!,
|
||||||
percentage: 1 - donationPercentage,
|
percentage: 1 - donationPercentage,
|
||||||
label: "Your wallet",
|
label: "Your wallet",
|
||||||
},
|
},
|
||||||
|
|
@ -222,7 +228,13 @@ export async function buyXmr() {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
address_pool.push({
|
address_pool.push({
|
||||||
address: moneroReceiveAddress,
|
// We need to assert this as being not null even though it can be null
|
||||||
|
//
|
||||||
|
// This is correct because a LabeledMoneroAddress can actually have a null address but
|
||||||
|
// typeshare cannot express that yet (easily)
|
||||||
|
//
|
||||||
|
// TODO: Let typescript do its job here and not assert it
|
||||||
|
address: moneroReceiveAddress!,
|
||||||
percentage: 1,
|
percentage: 1,
|
||||||
label: "Your wallet",
|
label: "Your wallet",
|
||||||
});
|
});
|
||||||
|
|
@ -232,7 +244,9 @@ export async function buyXmr() {
|
||||||
rendezvous_points: PRESET_RENDEZVOUS_POINTS,
|
rendezvous_points: PRESET_RENDEZVOUS_POINTS,
|
||||||
sellers,
|
sellers,
|
||||||
monero_receive_pool: address_pool,
|
monero_receive_pool: address_pool,
|
||||||
bitcoin_change_address: bitcoinChangeAddress,
|
// We convert null to undefined because typescript
|
||||||
|
// expects undefined if the field is optional and does not accept null here
|
||||||
|
bitcoin_change_address: bitcoinChangeAddress ?? undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,7 +298,7 @@ export async function initializeContext() {
|
||||||
});
|
});
|
||||||
logger.info("Initialized context");
|
logger.info("Initialized context");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(error);
|
throw new Error(String(error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,12 +354,12 @@ export async function getSwapInfo(swapId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSwapTimelock(swapId: string) {
|
export async function getSwapTimelock(swapId: string) {
|
||||||
const response = await invoke<
|
const response = await invoke<GetSwapTimelockArgs, GetSwapTimelockResponse>(
|
||||||
GetSwapTimelockArgs,
|
"get_swap_timelock",
|
||||||
GetSwapTimelockResponse
|
{
|
||||||
>("get_swap_timelock", {
|
swap_id: swapId,
|
||||||
swap_id: swapId,
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
timelockChangeEventReceived({
|
timelockChangeEventReceived({
|
||||||
|
|
@ -369,12 +383,12 @@ export async function getAllSwapTimelocks() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function withdrawBtc(address: string): Promise<string> {
|
export async function sweepBtc(address: string): Promise<string> {
|
||||||
const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
|
const response = await invoke<WithdrawBtcArgs, WithdrawBtcResponse>(
|
||||||
"withdraw_btc",
|
"withdraw_btc",
|
||||||
{
|
{
|
||||||
address,
|
address,
|
||||||
amount: null,
|
amount: undefined,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,31 +30,6 @@ const initialState: MakersSlice = {
|
||||||
selectedMaker: null,
|
selectedMaker: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function selectNewSelectedMaker(
|
|
||||||
slice: MakersSlice,
|
|
||||||
peerId?: string,
|
|
||||||
): MakerStatus {
|
|
||||||
const selectedPeerId = peerId || slice.selectedMaker?.peerId;
|
|
||||||
|
|
||||||
// Check if we still have a record of the currently selected provider
|
|
||||||
const currentMaker =
|
|
||||||
slice.registry.makers?.find((prov) => prov.peerId === selectedPeerId) ||
|
|
||||||
slice.rendezvous.makers.find((prov) => prov.peerId === selectedPeerId);
|
|
||||||
|
|
||||||
// If the currently selected provider is not outdated, keep it
|
|
||||||
if (currentMaker != null && !isMakerOutdated(currentMaker)) {
|
|
||||||
return currentMaker;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise we'd prefer to switch to a provider that has the newest version
|
|
||||||
const providers = [
|
|
||||||
...(slice.registry.makers ?? []),
|
|
||||||
...(slice.rendezvous.makers ?? []),
|
|
||||||
];
|
|
||||||
|
|
||||||
return providers.at(0) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makersSlice = createSlice({
|
export const makersSlice = createSlice({
|
||||||
name: "providers",
|
name: "providers",
|
||||||
initialState,
|
initialState,
|
||||||
|
|
@ -83,32 +58,15 @@ export const makersSlice = createSlice({
|
||||||
slice.rendezvous.makers.push(discoveredMakerStatus);
|
slice.rendezvous.makers.push(discoveredMakerStatus);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort the provider list and select a new provider if needed
|
|
||||||
slice.selectedMaker = selectNewSelectedMaker(slice);
|
|
||||||
},
|
},
|
||||||
setRegistryMakers(slice, action: PayloadAction<ExtendedMakerStatus[]>) {
|
setRegistryMakers(slice, action: PayloadAction<ExtendedMakerStatus[]>) {
|
||||||
if (stubTestnetMaker) {
|
if (stubTestnetMaker) {
|
||||||
action.payload.push(stubTestnetMaker);
|
action.payload.push(stubTestnetMaker);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort the provider list and select a new provider if needed
|
|
||||||
slice.selectedMaker = selectNewSelectedMaker(slice);
|
|
||||||
},
|
},
|
||||||
registryConnectionFailed(slice) {
|
registryConnectionFailed(slice) {
|
||||||
slice.registry.connectionFailsCount += 1;
|
slice.registry.connectionFailsCount += 1;
|
||||||
},
|
},
|
||||||
setSelectedMaker(
|
|
||||||
slice,
|
|
||||||
action: PayloadAction<{
|
|
||||||
peerId: string;
|
|
||||||
}>,
|
|
||||||
) {
|
|
||||||
slice.selectedMaker = selectNewSelectedMaker(
|
|
||||||
slice,
|
|
||||||
action.payload.peerId,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -116,7 +74,6 @@ export const {
|
||||||
discoveredMakersByRendezvous,
|
discoveredMakersByRendezvous,
|
||||||
setRegistryMakers,
|
setRegistryMakers,
|
||||||
registryConnectionFailed,
|
registryConnectionFailed,
|
||||||
setSelectedMaker,
|
|
||||||
} = makersSlice.actions;
|
} = makersSlice.actions;
|
||||||
|
|
||||||
export default makersSlice.reducer;
|
export default makersSlice.reducer;
|
||||||
|
|
|
||||||
|
|
@ -89,8 +89,10 @@ export const rpcSlice = createSlice({
|
||||||
slice: RPCSlice,
|
slice: RPCSlice,
|
||||||
action: PayloadAction<TauriTimelockChangeEvent>,
|
action: PayloadAction<TauriTimelockChangeEvent>,
|
||||||
) {
|
) {
|
||||||
slice.state.swapTimelocks[action.payload.swap_id] =
|
if (action.payload.timelock) {
|
||||||
action.payload.timelock;
|
slice.state.swapTimelocks[action.payload.swap_id] =
|
||||||
|
action.payload.timelock;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
rpcSetWithdrawTxId(slice, action: PayloadAction<string>) {
|
rpcSetWithdrawTxId(slice, action: PayloadAction<string>) {
|
||||||
slice.state.withdrawTxId = action.payload;
|
slice.state.withdrawTxId = action.payload;
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@ export const walletSlice = createSlice({
|
||||||
slice.state.lowestCurrentBlock = Math.min(
|
slice.state.lowestCurrentBlock = Math.min(
|
||||||
// We ignore anything below 10 blocks as this may be something like wallet2
|
// We ignore anything below 10 blocks as this may be something like wallet2
|
||||||
// sending a wrong value when it hasn't initialized yet
|
// sending a wrong value when it hasn't initialized yet
|
||||||
slice.state.lowestCurrentBlock < 10 ||
|
slice.state.lowestCurrentBlock === null ||
|
||||||
slice.state.lowestCurrentBlock === null
|
slice.state.lowestCurrentBlock < 10
|
||||||
? Infinity
|
? Infinity
|
||||||
: slice.state.lowestCurrentBlock,
|
: slice.state.lowestCurrentBlock,
|
||||||
action.payload.current_block,
|
action.payload.current_block,
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,11 @@ export function useBitcoinSyncProgress(): TauriBitcoinSyncProgress[] {
|
||||||
const syncingProcesses = pendingProcesses
|
const syncingProcesses = pendingProcesses
|
||||||
.map(([_, c]) => c)
|
.map(([_, c]) => c)
|
||||||
.filter(isBitcoinSyncProgress);
|
.filter(isBitcoinSyncProgress);
|
||||||
return syncingProcesses.map((c) => c.progress.content);
|
return syncingProcesses
|
||||||
|
.map((c) => c.progress.content)
|
||||||
|
.filter(
|
||||||
|
(content): content is TauriBitcoinSyncProgress => content !== undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSyncingBitcoin(): boolean {
|
export function isSyncingBitcoin(): boolean {
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,9 @@ export const selectSwapTimelocks = createSelector(
|
||||||
(rpcState) => rpcState.swapTimelocks,
|
(rpcState) => rpcState.swapTimelocks,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectSwapTimelock = (swapId: string) =>
|
export const selectSwapTimelock = (swapId: string | null) =>
|
||||||
createSelector(
|
createSelector([selectSwapTimelocks], (timelocks) =>
|
||||||
[selectSwapTimelocks],
|
swapId ? (timelocks[swapId] ?? null) : null,
|
||||||
(timelocks) => timelocks[swapId] ?? null,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const selectSwapInfoWithTimelock = (swapId: string) =>
|
export const selectSwapInfoWithTimelock = (swapId: string) =>
|
||||||
|
|
|
||||||
|
|
@ -10,48 +10,47 @@ export function sortApprovalsAndKnownQuotes(
|
||||||
pendingSelectMakerApprovals: PendingSelectMakerApprovalRequest[],
|
pendingSelectMakerApprovals: PendingSelectMakerApprovalRequest[],
|
||||||
known_quotes: QuoteWithAddress[],
|
known_quotes: QuoteWithAddress[],
|
||||||
) {
|
) {
|
||||||
const sortableQuotes = pendingSelectMakerApprovals.map((approval) => {
|
const sortableQuotes: SortableQuoteWithAddress[] =
|
||||||
return {
|
pendingSelectMakerApprovals.map((approval) => {
|
||||||
...approval.request.content.maker,
|
return {
|
||||||
expiration_ts:
|
quote_with_address: approval.request.content.maker,
|
||||||
approval.request_status.state === "Pending"
|
approval:
|
||||||
? approval.request_status.content.expiration_ts
|
approval.request_status.state === "Pending"
|
||||||
: undefined,
|
? {
|
||||||
request_id: approval.request_id,
|
request_id: approval.request_id,
|
||||||
} as SortableQuoteWithAddress;
|
expiration_ts: approval.request_status.content.expiration_ts,
|
||||||
});
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
sortableQuotes.push(
|
sortableQuotes.push(
|
||||||
...known_quotes.map((quote) => ({
|
...known_quotes.map((quote) => ({
|
||||||
...quote,
|
quote_with_address: quote,
|
||||||
request_id: null,
|
approval: null,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
return sortMakerApprovals(sortableQuotes);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sortMakerApprovals(list: SortableQuoteWithAddress[]) {
|
|
||||||
return (
|
return (
|
||||||
_(list)
|
_(sortableQuotes)
|
||||||
.orderBy(
|
.orderBy(
|
||||||
[
|
[
|
||||||
// Prefer makers that have a 'version' attribute
|
// Prefer makers that have a 'version' attribute
|
||||||
// If we don't have a version, we cannot clarify if it's outdated or not
|
// If we don't have a version, we cannot clarify if it's outdated or not
|
||||||
(m) => (m.version ? 0 : 1),
|
(m) => (m.quote_with_address.version ? 0 : 1),
|
||||||
// Prefer makers with a minimum quantity > 0
|
// Prefer makers with a minimum quantity > 0
|
||||||
(m) => ((m.quote.min_quantity ?? 0) > 0 ? 0 : 1),
|
(m) => ((m.quote_with_address.quote.min_quantity ?? 0) > 0 ? 0 : 1),
|
||||||
// Prefer makers that are not outdated
|
// Prefer makers that are not outdated
|
||||||
(m) => (isMakerVersionOutdated(m.version) ? 1 : 0),
|
(m) => (isMakerVersionOutdated(m.quote_with_address.version) ? 1 : 0),
|
||||||
// Prefer approvals over actual quotes
|
// Prefer approvals over actual quotes
|
||||||
(m) => (m.request_id ? 0 : 1),
|
(m) => (m.approval ? 0 : 1),
|
||||||
// Prefer makers with a lower price
|
// Prefer makers with a lower price
|
||||||
(m) => m.quote.price,
|
(m) => m.quote_with_address.quote.price,
|
||||||
],
|
],
|
||||||
["asc", "asc", "asc", "asc", "asc"],
|
["asc", "asc", "asc", "asc", "asc"],
|
||||||
)
|
)
|
||||||
// Remove duplicate makers
|
// Remove duplicate makers
|
||||||
.uniqBy((m) => m.peer_id)
|
.uniqBy((m) => m.quote_with_address.peer_id)
|
||||||
.value()
|
.value()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": false,
|
"strict": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noFallthroughCasesInSwitch": false,
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
|
|
||||||
|
|
@ -532,18 +532,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.28.2"
|
"@babel/runtime" "^7.28.2"
|
||||||
|
|
||||||
"@mui/lab@^7.0.0-beta.13":
|
|
||||||
version "7.0.0-beta.16"
|
|
||||||
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-7.0.0-beta.16.tgz#99045e2840c3f4db0383cdcc477af8c7b60c83a2"
|
|
||||||
integrity sha512-YiyDU84F6ujjaa5xuItuXa40KN1aPC+8PBkP2OAOJGO2MMvdEicuvkEfVSnikH6uLHtKOwGzOeqEqrfaYxcOxw==
|
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.28.2"
|
|
||||||
"@mui/system" "^7.3.1"
|
|
||||||
"@mui/types" "^7.4.5"
|
|
||||||
"@mui/utils" "^7.3.1"
|
|
||||||
clsx "^2.1.1"
|
|
||||||
prop-types "^15.8.1"
|
|
||||||
|
|
||||||
"@mui/material@^7.1.1":
|
"@mui/material@^7.1.1":
|
||||||
version "7.3.1"
|
version "7.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-7.3.1.tgz#bd1bf1344cc7a69b6e459248b544f0ae97945b1d"
|
resolved "https://registry.yarnpkg.com/@mui/material/-/material-7.3.1.tgz#bd1bf1344cc7a69b6e459248b544f0ae97945b1d"
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ impl Amount {
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
pub struct LabeledMoneroAddress {
|
pub struct LabeledMoneroAddress {
|
||||||
// If this is None, we will use an address of the internal Monero wallet
|
// If this is None, we will use an address of the internal Monero wallet
|
||||||
|
// TODO: This should be string | null but typeshare cannot do that yet
|
||||||
#[typeshare(serialized_as = "string")]
|
#[typeshare(serialized_as = "string")]
|
||||||
address: Option<monero::Address>,
|
address: Option<monero::Address>,
|
||||||
#[typeshare(serialized_as = "number")]
|
#[typeshare(serialized_as = "number")]
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,7 @@ pub fn init(
|
||||||
let tracing_file_layer = json_rolling_layer!(
|
let tracing_file_layer = json_rolling_layer!(
|
||||||
&dir,
|
&dir,
|
||||||
"tracing",
|
"tracing",
|
||||||
env_filter_with_all_crates(vec![
|
env_filter_with_all_crates(vec![(crates::OUR_CRATES.to_vec(), LevelFilter::TRACE)]),
|
||||||
(crates::OUR_CRATES.to_vec(), LevelFilter::TRACE)
|
|
||||||
]),
|
|
||||||
24
|
24
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -104,7 +102,10 @@ pub fn init(
|
||||||
let monero_wallet_file_layer = json_rolling_layer!(
|
let monero_wallet_file_layer = json_rolling_layer!(
|
||||||
&dir,
|
&dir,
|
||||||
"tracing-monero-wallet",
|
"tracing-monero-wallet",
|
||||||
env_filter_with_all_crates(vec![(crates::MONERO_WALLET_CRATES.to_vec(), LevelFilter::TRACE)]),
|
env_filter_with_all_crates(vec![(
|
||||||
|
crates::MONERO_WALLET_CRATES.to_vec(),
|
||||||
|
LevelFilter::TRACE
|
||||||
|
)]),
|
||||||
24
|
24
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -145,7 +146,9 @@ pub fn init(
|
||||||
(crates::LIBP2P_CRATES.to_vec(), LevelFilter::INFO),
|
(crates::LIBP2P_CRATES.to_vec(), LevelFilter::INFO),
|
||||||
(crates::TOR_CRATES.to_vec(), LevelFilter::INFO),
|
(crates::TOR_CRATES.to_vec(), LevelFilter::INFO),
|
||||||
])?,
|
])?,
|
||||||
false => env_filter_with_all_crates(vec![(crates::OUR_CRATES.to_vec(), LevelFilter::INFO)])?,
|
false => {
|
||||||
|
env_filter_with_all_crates(vec![(crates::OUR_CRATES.to_vec(), LevelFilter::INFO)])?
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let final_terminal_layer = match format {
|
let final_terminal_layer = match format {
|
||||||
|
|
@ -227,10 +230,7 @@ mod crates {
|
||||||
"unstoppableswap_gui_rs",
|
"unstoppableswap_gui_rs",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const MONERO_WALLET_CRATES: &[&str] = &[
|
pub const MONERO_WALLET_CRATES: &[&str] = &["monero_cpp", "monero_rpc_pool"];
|
||||||
"monero_cpp",
|
|
||||||
"monero_rpc_pool",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A writer that forwards tracing log messages to the tauri guest.
|
/// A writer that forwards tracing log messages to the tauri guest.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue