mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-20 02:53:00 -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]
|
||||
|
||||
- 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
|
||||
|
||||
- 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",
|
||||
"@fontsource/roboto": "^5.1.0",
|
||||
"@mui/icons-material": "^7.1.1",
|
||||
"@mui/lab": "^7.0.0-beta.13",
|
||||
"@mui/material": "^7.1.1",
|
||||
"@mui/x-date-pickers": "^8.8.0",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
TauriSwapProgressEvent,
|
||||
SendMoneroDetails,
|
||||
ContextStatus,
|
||||
QuoteWithAddress,
|
||||
ExportBitcoinWalletResponse,
|
||||
} from "./tauriModel";
|
||||
import {
|
||||
ContextStatusType,
|
||||
|
|
@ -17,6 +19,16 @@ import {
|
|||
|
||||
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<
|
||||
T extends TauriSwapProgressEventType,
|
||||
> = Extract<TauriSwapProgressEvent, { type: T }>["content"];
|
||||
|
|
@ -354,9 +366,9 @@ export function isPendingPasswordApprovalEvent(
|
|||
* @returns True if funds have been locked, false otherwise
|
||||
*/
|
||||
export function haveFundsBeenLocked(
|
||||
event: TauriSwapProgressEvent | null,
|
||||
event: TauriSwapProgressEvent | null | undefined,
|
||||
): boolean {
|
||||
if (event === null) {
|
||||
if (event === null || event === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -372,7 +384,7 @@ export function haveFundsBeenLocked(
|
|||
}
|
||||
|
||||
export function isContextFullyInitialized(
|
||||
status: ResultContextStatus,
|
||||
status: ResultContextStatus | null,
|
||||
): boolean {
|
||||
if (status == null || status.type === ContextStatusType.Error) {
|
||||
return false;
|
||||
|
|
@ -396,3 +408,20 @@ export function isContextWithMoneroWallet(
|
|||
): boolean {
|
||||
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 PasswordEntryDialog from "./modal/password-entry/PasswordEntryDialog";
|
||||
import ContextErrorDialog from "./modal/context-error/ContextErrorDialog";
|
||||
import ErrorBoundary from "./other/ErrorBoundary";
|
||||
|
||||
declare module "@mui/material/styles" {
|
||||
interface Theme {
|
||||
|
|
@ -47,6 +48,7 @@ export default function App() {
|
|||
console.log("Current theme:", { theme, currentTheme });
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={currentTheme}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
|
|
@ -65,6 +67,7 @@ export default function App() {
|
|||
</LocalizationProvider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -79,13 +82,13 @@ function InnerContent() {
|
|||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<MoneroWalletPage />} />
|
||||
<Route path="/monero-wallet" element={<MoneroWalletPage />} />
|
||||
<Route path="/swap" element={<SwapPage />} />
|
||||
<Route path="/history" element={<HistoryPage />} />
|
||||
<Route path="/bitcoin-wallet" element={<WalletPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route path="/feedback" element={<FeedbackPage />} />
|
||||
<Route path="/" element={<ErrorBoundary><MoneroWalletPage /></ErrorBoundary>} />
|
||||
<Route path="/monero-wallet" element={<ErrorBoundary><MoneroWalletPage /></ErrorBoundary>} />
|
||||
<Route path="/swap" element={<ErrorBoundary><SwapPage /></ErrorBoundary>} />
|
||||
<Route path="/history" element={<ErrorBoundary><HistoryPage /></ErrorBoundary>} />
|
||||
<Route path="/bitcoin-wallet" element={<ErrorBoundary><WalletPage /></ErrorBoundary>} />
|
||||
<Route path="/settings" element={<ErrorBoundary><SettingsPage /></ErrorBoundary>} />
|
||||
<Route path="/feedback" element={<ErrorBoundary><FeedbackPage /></ErrorBoundary>} />
|
||||
</Routes>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ interface PromiseInvokeButtonProps<T> {
|
|||
|
||||
export default function PromiseInvokeButton<T>({
|
||||
disabled = false,
|
||||
onSuccess = null,
|
||||
onSuccess,
|
||||
onInvoke,
|
||||
children,
|
||||
startIcon,
|
||||
|
|
@ -44,7 +44,7 @@ export default function PromiseInvokeButton<T>({
|
|||
isIconButton = false,
|
||||
isChipButton = false,
|
||||
displayErrorSnackbar = false,
|
||||
onPendingChange = null,
|
||||
onPendingChange,
|
||||
contextRequirement = true,
|
||||
tooltipTitle = null,
|
||||
...rest
|
||||
|
|
|
|||
|
|
@ -69,11 +69,7 @@ function BitcoinRedeemedStateAlert({ swap }: { swap: GetSwapInfoResponseExt }) {
|
|||
"If this step fails, you can manually redeem your funds",
|
||||
]}
|
||||
/>
|
||||
<SwapMoneroRecoveryButton
|
||||
swap={swap}
|
||||
size="small"
|
||||
variant="contained"
|
||||
/>
|
||||
<SwapMoneroRecoveryButton swap={swap} size="small" variant="contained" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -208,10 +204,7 @@ export function StateAlert({
|
|||
);
|
||||
case "Cancel":
|
||||
return (
|
||||
<BitcoinPossiblyCancelledAlert
|
||||
timelock={timelock}
|
||||
swap={swap}
|
||||
/>
|
||||
<BitcoinPossiblyCancelledAlert timelock={timelock} swap={swap} />
|
||||
);
|
||||
case "Punish":
|
||||
return <PunishTimelockExpiredAlert />;
|
||||
|
|
@ -235,6 +228,7 @@ export function StateAlert({
|
|||
// 72 is the default cancel timelock in blocks
|
||||
// 4 blocks are around 40 minutes
|
||||
// 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;
|
||||
|
||||
/**
|
||||
|
|
@ -246,10 +240,16 @@ export default function SwapStatusAlert({
|
|||
swap,
|
||||
onlyShowIfUnusualAmountOfTimeHasPassed,
|
||||
}: {
|
||||
swap: GetSwapInfoResponseExt;
|
||||
swap: GetSwapInfoResponseExt | null;
|
||||
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)) {
|
||||
return null;
|
||||
|
|
@ -263,12 +263,10 @@ export default function SwapStatusAlert({
|
|||
timelock.type === "None" &&
|
||||
timelock.content.blocks_left > UNUSUAL_AMOUNT_OF_TIME_HAS_PASSED_THRESHOLD;
|
||||
|
||||
if (onlyShowIfUnusualAmountOfTimeHasPassed && hasUnusualAmountOfTimePassed) {
|
||||
if (onlyShowIfUnusualAmountOfTimeHasPassed && !hasUnusualAmountOfTimePassed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isRunning = useIsSpecificSwapRunning(swap.swap_id);
|
||||
|
||||
return (
|
||||
<Alert
|
||||
key={swap.swap_id}
|
||||
|
|
@ -290,8 +288,7 @@ export default function SwapStatusAlert({
|
|||
)
|
||||
) : (
|
||||
<>
|
||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is
|
||||
not running
|
||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> is not running
|
||||
</>
|
||||
)}
|
||||
</AlertTitle>
|
||||
|
|
@ -303,7 +300,7 @@ export default function SwapStatusAlert({
|
|||
}}
|
||||
>
|
||||
<StateAlert swap={swap} timelock={timelock} isRunning={isRunning} />
|
||||
<TimelockTimeline swap={swap} timelock={timelock} />
|
||||
{timelock && <TimelockTimeline swap={swap} timelock={timelock} />}
|
||||
</Box>
|
||||
</Alert>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ function TimelineSegment({
|
|||
opacity: isActive ? 1 : 0.3,
|
||||
}}
|
||||
>
|
||||
{isActive && (
|
||||
{isActive && durationOfSegment && (
|
||||
<Box
|
||||
sx={{
|
||||
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">
|
||||
{props.label || `${Math.round(props.value)}%`}
|
||||
{props.label || `${Math.round(props.value ?? 0)}%`}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
@ -84,6 +84,8 @@ export default function UpdaterDialog() {
|
|||
}
|
||||
|
||||
async function handleInstall() {
|
||||
if (!availableUpdate) return;
|
||||
|
||||
try {
|
||||
await availableUpdate.downloadAndInstall((event: DownloadEvent) => {
|
||||
if (event.event === "Started") {
|
||||
|
|
@ -92,10 +94,13 @@ export default function UpdaterDialog() {
|
|||
downloadedBytes: 0,
|
||||
});
|
||||
} else if (event.event === "Progress") {
|
||||
setDownloadProgress((prev) => ({
|
||||
...prev,
|
||||
setDownloadProgress((prev) => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
contentLength: prev.contentLength,
|
||||
downloadedBytes: prev.downloadedBytes + event.data.chunkLength,
|
||||
}));
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -110,7 +115,8 @@ export default function UpdaterDialog() {
|
|||
|
||||
const isDownloading = downloadProgress !== null;
|
||||
|
||||
const progress = isDownloading
|
||||
const progress =
|
||||
isDownloading && downloadProgress.contentLength
|
||||
? Math.round(
|
||||
(downloadProgress.downloadedBytes / downloadProgress.contentLength) *
|
||||
100,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Button, Dialog, DialogActions } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { withdrawBtc } from "renderer/rpc";
|
||||
import { sweepBtc } from "renderer/rpc";
|
||||
import DialogHeader from "../DialogHeader";
|
||||
import AddressInputPage from "./pages/AddressInputPage";
|
||||
import BtcTxInMempoolPageContent from "./pages/BitcoinWithdrawTxInMempoolPage";
|
||||
|
|
@ -59,7 +59,7 @@ export default function WithdrawDialog({
|
|||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!withdrawAddressValid}
|
||||
onInvoke={() => withdrawBtc(withdrawAddress)}
|
||||
onInvoke={() => sweepBtc(withdrawAddress)}
|
||||
onPendingChange={setPending}
|
||||
onSuccess={setWithdrawTxId}
|
||||
contextRequirement={isContextWithBitcoinWallet}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ type ModalProps = {
|
|||
};
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
content: string | null;
|
||||
displayCopyIcon?: boolean;
|
||||
enableQrCode?: boolean;
|
||||
light?: boolean;
|
||||
|
|
@ -72,6 +72,7 @@ export default function ActionableMonospaceTextBox({
|
|||
const [isRevealed, setIsRevealed] = useState(!spoilerText);
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!content) return;
|
||||
await writeText(content);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
|
|
@ -160,7 +161,7 @@ export default function ActionableMonospaceTextBox({
|
|||
)}
|
||||
</Box>
|
||||
|
||||
{enableQrCode && (
|
||||
{enableQrCode && content && (
|
||||
<QRCodeModal
|
||||
open={qrCodeOpen}
|
||||
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">
|
||||
<KeyboardArrowDownIcon />
|
||||
</IconButton>
|
||||
{searchQuery !== undefined && setSearchQuery !== undefined && (
|
||||
{searchQuery !== null && setSearchQuery !== null && (
|
||||
<ExpandableSearchBox query={searchQuery} setQuery={setSearchQuery} />
|
||||
)}
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default function TruncatedText({
|
|||
ellipsis?: string;
|
||||
truncateMiddle?: boolean;
|
||||
}) {
|
||||
let finalChildren = children ?? "";
|
||||
const finalChildren = children ?? "";
|
||||
|
||||
const truncatedText =
|
||||
finalChildren.length > limit
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export function AmountWithUnit({
|
|||
unit,
|
||||
fixedPrecision,
|
||||
exchangeRate,
|
||||
parenthesisText = null,
|
||||
parenthesisText,
|
||||
labelStyles,
|
||||
amountStyles,
|
||||
disableTooltip = false,
|
||||
|
|
@ -18,7 +18,7 @@ export function AmountWithUnit({
|
|||
unit: string;
|
||||
fixedPrecision: number;
|
||||
exchangeRate?: Amount;
|
||||
parenthesisText?: string;
|
||||
parenthesisText?: string | null;
|
||||
labelStyles?: SxProps;
|
||||
amountStyles?: SxProps;
|
||||
disableTooltip?: boolean;
|
||||
|
|
@ -142,7 +142,7 @@ export function MoneroBitcoinExchangeRate({
|
|||
}) {
|
||||
const marketRate = useAppSelector((state) => state.rates?.xmrBtcRate);
|
||||
const markup =
|
||||
displayMarkup && marketRate != null
|
||||
displayMarkup && marketRate != null && rate != null
|
||||
? `${getMarkup(rate, marketRate).toFixed(2)}% markup`
|
||||
: null;
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ export function MoneroSatsExchangeRate({
|
|||
rate: Amount;
|
||||
displayMarkup?: boolean;
|
||||
}) {
|
||||
const btc = satsToBtc(rate);
|
||||
const btc = rate == null ? null : satsToBtc(rate);
|
||||
|
||||
return <MoneroBitcoinExchangeRate rate={btc} displayMarkup={displayMarkup} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -378,10 +378,6 @@ function MessageBubble({ message }: { message: Message }) {
|
|||
<Box
|
||||
sx={(theme) => ({
|
||||
padding: 1.5,
|
||||
borderRadius:
|
||||
typeof theme.shape.borderRadius === "number"
|
||||
? theme.shape.borderRadius * 2
|
||||
: 8,
|
||||
maxWidth: "75%",
|
||||
wordBreak: "break-word",
|
||||
boxShadow: theme.shadows[1],
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ function MoneroNodeUrlSetting() {
|
|||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<ValidatedTextField
|
||||
value={moneroNodeUrl}
|
||||
onValidatedChange={handleNodeUrlChange}
|
||||
onValidatedChange={(value) => value && handleNodeUrlChange(value)}
|
||||
placeholder={PLACEHOLDER_MONERO_NODE_URL}
|
||||
disabled={useMoneroRpcPool}
|
||||
fullWidth
|
||||
|
|
@ -675,7 +675,7 @@ function NodeTable({
|
|||
<ValidatedTextField
|
||||
label="Add a new node"
|
||||
value={newNode}
|
||||
onValidatedChange={setNewNode}
|
||||
onValidatedChange={(value) => setNewNode(value ?? "")}
|
||||
placeholder={placeholder}
|
||||
fullWidth
|
||||
isValid={isValid}
|
||||
|
|
@ -843,7 +843,9 @@ function RendezvousPointsSetting() {
|
|||
<ValidatedTextField
|
||||
label="Add new rendezvous point"
|
||||
value={newPoint}
|
||||
onValidatedChange={setNewPoint}
|
||||
onValidatedChange={(value) =>
|
||||
setNewPoint(value ?? "")
|
||||
}
|
||||
placeholder="/dns4/rendezvous.observer/tcp/8888/p2p/12D3KooWMjceGXrYuGuDMGrfmJxALnSDbK4km6s1i1sJEgDTgGQa"
|
||||
fullWidth
|
||||
isValid={isValidMultiAddressWithPeerId}
|
||||
|
|
|
|||
|
|
@ -119,9 +119,9 @@ export function SwapMoneroRecoveryButton({
|
|||
onInvoke={(): Promise<MoneroRecoveryResponse> =>
|
||||
getMoneroRecoveryKeys(swap.swap_id)
|
||||
}
|
||||
onSuccess={(keys: MoneroRecoveryResponse) =>
|
||||
store.dispatch(rpcSetMoneroRecoveryKeys([swap.swap_id, keys]))
|
||||
}
|
||||
onSuccess={(keys: MoneroRecoveryResponse) => {
|
||||
store.dispatch(rpcSetMoneroRecoveryKeys([swap.swap_id, keys]));
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
Display Monero Recovery Keys
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ interface SendAmountInputProps {
|
|||
currency: string;
|
||||
onCurrencyChange: (currency: string) => void;
|
||||
fiatCurrency: string;
|
||||
xmrPrice: number;
|
||||
xmrPrice: number | null;
|
||||
showFiatRate: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
@ -48,6 +48,10 @@ export default function SendAmountInput({
|
|||
return "0.00";
|
||||
}
|
||||
|
||||
if (xmrPrice === null) {
|
||||
return "?";
|
||||
}
|
||||
|
||||
const primaryValue = parseFloat(amount);
|
||||
if (currency === "XMR") {
|
||||
// Primary is XMR, secondary is USD
|
||||
|
|
@ -76,7 +80,7 @@ export default function SendAmountInput({
|
|||
|
||||
if (currency === "XMR") {
|
||||
onAmountChange(Math.max(0, maxAmountXmr).toString());
|
||||
} else {
|
||||
} else if (xmrPrice !== null) {
|
||||
// Convert to USD for display
|
||||
const maxAmountUsd = maxAmountXmr * xmrPrice;
|
||||
onAmountChange(Math.max(0, maxAmountUsd).toString());
|
||||
|
|
@ -103,7 +107,8 @@ export default function SendAmountInput({
|
|||
(currency === "XMR"
|
||||
? parseFloat(amount) >
|
||||
piconerosToXmr(parseFloat(balance.unlocked_balance))
|
||||
: parseFloat(amount) / xmrPrice >
|
||||
: xmrPrice !== null &&
|
||||
parseFloat(amount) / xmrPrice >
|
||||
piconerosToXmr(parseFloat(balance.unlocked_balance)));
|
||||
|
||||
return (
|
||||
|
|
@ -203,14 +208,11 @@ export default function SendAmountInput({
|
|||
}}
|
||||
>
|
||||
<Typography color="text.secondary">Available</Typography>
|
||||
<Box sx={{ display: "flex", alignItems: "baseline", gap: 0.5 }}>
|
||||
<Typography color="text.primary">
|
||||
<MoneroAmount
|
||||
amount={piconerosToXmr(parseFloat(balance.unlocked_balance))}
|
||||
/>
|
||||
</Typography>
|
||||
<Typography color="text.secondary">XMR</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant={isMaxSelected ? "contained" : "secondary"}
|
||||
size="tiny"
|
||||
|
|
|
|||
|
|
@ -20,10 +20,9 @@ export default function SendSuccessContent({
|
|||
}) {
|
||||
const address = successDetails?.address;
|
||||
const amount = successDetails?.amount_sent;
|
||||
const explorerUrl = getMoneroTxExplorerUrl(
|
||||
successDetails?.tx_hash,
|
||||
isTestnet(),
|
||||
);
|
||||
const explorerUrl = successDetails?.tx_hash
|
||||
? getMoneroTxExplorerUrl(successDetails.tx_hash, isTestnet())
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
|
@ -79,7 +78,7 @@ export default function SendSuccessContent({
|
|||
</Typography>
|
||||
<Typography variant="body1" color="text.primary">
|
||||
<MonospaceTextBox>
|
||||
{address.slice(0, 8)} ... {address.slice(-8)}
|
||||
{address ? `${address.slice(0, 8)}...${address.slice(-8)}` : "?"}
|
||||
</MonospaceTextBox>
|
||||
</Typography>
|
||||
</Box>
|
||||
|
|
@ -98,8 +97,13 @@ export default function SendSuccessContent({
|
|||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
disabled={explorerUrl == null}
|
||||
endIcon={<ArrowOutwardIcon />}
|
||||
onClick={() => open(explorerUrl)}
|
||||
onClick={() => {
|
||||
if (explorerUrl != null) {
|
||||
open(explorerUrl);
|
||||
}
|
||||
}}
|
||||
>
|
||||
View on Explorer
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,9 @@ export default function SendTransactionContent({
|
|||
const moneroAmount =
|
||||
currency === "XMR"
|
||||
? parseFloat(sendAmount)
|
||||
: parseFloat(sendAmount) / xmrPrice;
|
||||
: xmrPrice !== null
|
||||
? parseFloat(sendAmount) / xmrPrice
|
||||
: null;
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!sendAddress) {
|
||||
|
|
@ -103,7 +105,7 @@ export default function SendTransactionContent({
|
|||
amount: { type: "Sweep" },
|
||||
});
|
||||
} else {
|
||||
if (!sendAmount || sendAmount === "<MAX>") {
|
||||
if (!sendAmount || sendAmount === "<MAX>" || moneroAmount === null) {
|
||||
throw new Error("Amount is required");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Box, darken, lighten, useTheme } from "@mui/material";
|
||||
|
||||
function getColor(colorName: string) {
|
||||
function getColor(colorName: string): string {
|
||||
const theme = useTheme();
|
||||
switch (colorName) {
|
||||
case "primary":
|
||||
|
|
@ -11,6 +11,8 @@ function getColor(colorName: string) {
|
|||
return theme.palette.success.main;
|
||||
case "warning":
|
||||
return theme.palette.warning.main;
|
||||
default:
|
||||
return theme.palette.primary.main;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import dayjs from "dayjs";
|
|||
import TransactionItem from "./TransactionItem";
|
||||
|
||||
interface TransactionHistoryProps {
|
||||
history?: {
|
||||
history: {
|
||||
transactions: TransactionInfo[];
|
||||
};
|
||||
} | null;
|
||||
}
|
||||
|
||||
interface TransactionGroup {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import { PiconeroAmount } from "../../../other/Units";
|
|||
import { FiatPiconeroAmount } from "../../../other/Units";
|
||||
import StateIndicator from "./StateIndicator";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import { GetMoneroSyncProgressResponse } from "models/tauriModel";
|
||||
import {
|
||||
GetMoneroBalanceResponse,
|
||||
GetMoneroSyncProgressResponse,
|
||||
} from "models/tauriModel";
|
||||
|
||||
interface TimeEstimationResult {
|
||||
blocksLeft: number;
|
||||
|
|
@ -16,7 +19,7 @@ interface TimeEstimationResult {
|
|||
const AVG_MONERO_BLOCK_SIZE_KB = 130;
|
||||
|
||||
function useSyncTimeEstimation(
|
||||
syncProgress: GetMoneroSyncProgressResponse | undefined,
|
||||
syncProgress: GetMoneroSyncProgressResponse | null,
|
||||
): TimeEstimationResult | null {
|
||||
const poolStatus = useAppSelector((state) => state.pool.status);
|
||||
const restoreHeight = useAppSelector(
|
||||
|
|
@ -80,11 +83,8 @@ function useSyncTimeEstimation(
|
|||
}
|
||||
|
||||
interface WalletOverviewProps {
|
||||
balance?: {
|
||||
unlocked_balance: string;
|
||||
total_balance: string;
|
||||
};
|
||||
syncProgress?: GetMoneroSyncProgressResponse;
|
||||
balance: GetMoneroBalanceResponse | null;
|
||||
syncProgress: GetMoneroSyncProgressResponse | null;
|
||||
}
|
||||
|
||||
// Component for displaying wallet address and balance
|
||||
|
|
@ -99,10 +99,11 @@ export default function WalletOverview({
|
|||
const poolStatus = useAppSelector((state) => state.pool.status);
|
||||
const timeEstimation = useSyncTimeEstimation(syncProgress);
|
||||
|
||||
const pendingBalance =
|
||||
parseFloat(balance.total_balance) - parseFloat(balance.unlocked_balance);
|
||||
const pendingBalance = 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
|
||||
// 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 variant="h4">
|
||||
<PiconeroAmount
|
||||
amount={parseFloat(balance.unlocked_balance)}
|
||||
amount={balance ? parseFloat(balance.unlocked_balance) : null}
|
||||
fixedPrecision={4}
|
||||
disableTooltip
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
<FiatPiconeroAmount
|
||||
amount={parseFloat(balance.unlocked_balance)}
|
||||
amount={balance ? parseFloat(balance.unlocked_balance) : null}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
{pendingBalance > 0 && (
|
||||
{pendingBalance !== null && pendingBalance > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ export default function WalletPageLoadingState() {
|
|||
</Skeleton>
|
||||
|
||||
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mb: 2 }}>
|
||||
{Array.from({ length: 2 }).map((_) => (
|
||||
<Skeleton variant="rounded" sx={{ borderRadius: "100px" }}>
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<Skeleton key={i} variant="rounded" sx={{ borderRadius: "100px" }}>
|
||||
<Chip label="Loading..." variant="button" />
|
||||
</Skeleton>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Box, LinearProgress, Paper, Typography } from "@mui/material";
|
|||
import { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
id?: string | null;
|
||||
title: ReactNode | null;
|
||||
mainContent: ReactNode;
|
||||
additionalContent: ReactNode;
|
||||
|
|
@ -11,7 +11,7 @@ type Props = {
|
|||
};
|
||||
|
||||
export default function InfoBox({
|
||||
id = null,
|
||||
id,
|
||||
title,
|
||||
mainContent,
|
||||
additionalContent,
|
||||
|
|
@ -21,7 +21,7 @@ export default function InfoBox({
|
|||
return (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
id={id}
|
||||
id={id ?? undefined}
|
||||
sx={{
|
||||
padding: 1.5,
|
||||
overflow: "hidden",
|
||||
|
|
|
|||
|
|
@ -107,8 +107,8 @@ export default function DepositAndChooseOfferPage({
|
|||
return (
|
||||
<MakerOfferItem
|
||||
key={startIndex + index}
|
||||
quoteWithAddress={quote}
|
||||
requestId={quote.request_id}
|
||||
quoteWithAddress={quote.quote_with_address}
|
||||
requestId={quote.approval?.request_id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,12 @@ export default function MakerOfferItem({
|
|||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
<PromiseInvokeButton
|
||||
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
|
||||
disabled={!requestId}
|
||||
tooltipTitle={
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ import ActionableMonospaceTextBox from "renderer/components/other/ActionableMono
|
|||
import { getWalletDescriptor } from "renderer/rpc";
|
||||
import { ExportBitcoinWalletResponse } from "models/tauriModel";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { isContextWithBitcoinWallet } from "models/tauriModelExt";
|
||||
import {
|
||||
isContextWithBitcoinWallet,
|
||||
hasDescriptorProperty,
|
||||
} from "models/tauriModelExt";
|
||||
|
||||
const WALLET_DESCRIPTOR_DOCS_URL =
|
||||
"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;
|
||||
walletDescriptor: ExportBitcoinWalletResponse;
|
||||
}) {
|
||||
if (!hasDescriptorProperty(walletDescriptor)) {
|
||||
throw new Error("Wallet descriptor does not have descriptor property");
|
||||
}
|
||||
|
||||
const parsedDescriptor = JSON.parse(
|
||||
walletDescriptor.wallet_descriptor["descriptor"],
|
||||
walletDescriptor.wallet_descriptor.descriptor,
|
||||
);
|
||||
const stringifiedDescriptor = JSON.stringify(parsedDescriptor, null, 4);
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ declare module "@mui/material/styles" {
|
|||
tint?: string;
|
||||
}
|
||||
|
||||
interface PaletteColorOptions {
|
||||
interface SimplePaletteColorOptions {
|
||||
tint?: string;
|
||||
}
|
||||
}
|
||||
|
|
@ -61,16 +61,6 @@ const baseTheme: ThemeOptions = {
|
|||
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: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -210,7 +210,13 @@ export async function buyXmr() {
|
|||
|
||||
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,
|
||||
label: "Your wallet",
|
||||
},
|
||||
|
|
@ -222,7 +228,13 @@ export async function buyXmr() {
|
|||
);
|
||||
} else {
|
||||
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,
|
||||
label: "Your wallet",
|
||||
});
|
||||
|
|
@ -232,7 +244,9 @@ export async function buyXmr() {
|
|||
rendezvous_points: PRESET_RENDEZVOUS_POINTS,
|
||||
sellers,
|
||||
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");
|
||||
} 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) {
|
||||
const response = await invoke<
|
||||
GetSwapTimelockArgs,
|
||||
GetSwapTimelockResponse
|
||||
>("get_swap_timelock", {
|
||||
const response = await invoke<GetSwapTimelockArgs, GetSwapTimelockResponse>(
|
||||
"get_swap_timelock",
|
||||
{
|
||||
swap_id: swapId,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
store.dispatch(
|
||||
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>(
|
||||
"withdraw_btc",
|
||||
{
|
||||
address,
|
||||
amount: null,
|
||||
amount: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,31 +30,6 @@ const initialState: MakersSlice = {
|
|||
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({
|
||||
name: "providers",
|
||||
initialState,
|
||||
|
|
@ -83,32 +58,15 @@ export const makersSlice = createSlice({
|
|||
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[]>) {
|
||||
if (stubTestnetMaker) {
|
||||
action.payload.push(stubTestnetMaker);
|
||||
}
|
||||
|
||||
// Sort the provider list and select a new provider if needed
|
||||
slice.selectedMaker = selectNewSelectedMaker(slice);
|
||||
},
|
||||
registryConnectionFailed(slice) {
|
||||
slice.registry.connectionFailsCount += 1;
|
||||
},
|
||||
setSelectedMaker(
|
||||
slice,
|
||||
action: PayloadAction<{
|
||||
peerId: string;
|
||||
}>,
|
||||
) {
|
||||
slice.selectedMaker = selectNewSelectedMaker(
|
||||
slice,
|
||||
action.payload.peerId,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -116,7 +74,6 @@ export const {
|
|||
discoveredMakersByRendezvous,
|
||||
setRegistryMakers,
|
||||
registryConnectionFailed,
|
||||
setSelectedMaker,
|
||||
} = makersSlice.actions;
|
||||
|
||||
export default makersSlice.reducer;
|
||||
|
|
|
|||
|
|
@ -89,8 +89,10 @@ export const rpcSlice = createSlice({
|
|||
slice: RPCSlice,
|
||||
action: PayloadAction<TauriTimelockChangeEvent>,
|
||||
) {
|
||||
if (action.payload.timelock) {
|
||||
slice.state.swapTimelocks[action.payload.swap_id] =
|
||||
action.payload.timelock;
|
||||
}
|
||||
},
|
||||
rpcSetWithdrawTxId(slice, action: PayloadAction<string>) {
|
||||
slice.state.withdrawTxId = action.payload;
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ export const walletSlice = createSlice({
|
|||
slice.state.lowestCurrentBlock = Math.min(
|
||||
// We ignore anything below 10 blocks as this may be something like wallet2
|
||||
// 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
|
||||
: slice.state.lowestCurrentBlock,
|
||||
action.payload.current_block,
|
||||
|
|
|
|||
|
|
@ -253,7 +253,11 @@ export function useBitcoinSyncProgress(): TauriBitcoinSyncProgress[] {
|
|||
const syncingProcesses = pendingProcesses
|
||||
.map(([_, c]) => c)
|
||||
.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 {
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@ export const selectSwapTimelocks = createSelector(
|
|||
(rpcState) => rpcState.swapTimelocks,
|
||||
);
|
||||
|
||||
export const selectSwapTimelock = (swapId: string) =>
|
||||
createSelector(
|
||||
[selectSwapTimelocks],
|
||||
(timelocks) => timelocks[swapId] ?? null,
|
||||
export const selectSwapTimelock = (swapId: string | null) =>
|
||||
createSelector([selectSwapTimelocks], (timelocks) =>
|
||||
swapId ? (timelocks[swapId] ?? null) : null,
|
||||
);
|
||||
|
||||
export const selectSwapInfoWithTimelock = (swapId: string) =>
|
||||
|
|
|
|||
|
|
@ -10,48 +10,47 @@ export function sortApprovalsAndKnownQuotes(
|
|||
pendingSelectMakerApprovals: PendingSelectMakerApprovalRequest[],
|
||||
known_quotes: QuoteWithAddress[],
|
||||
) {
|
||||
const sortableQuotes = pendingSelectMakerApprovals.map((approval) => {
|
||||
const sortableQuotes: SortableQuoteWithAddress[] =
|
||||
pendingSelectMakerApprovals.map((approval) => {
|
||||
return {
|
||||
...approval.request.content.maker,
|
||||
expiration_ts:
|
||||
quote_with_address: approval.request.content.maker,
|
||||
approval:
|
||||
approval.request_status.state === "Pending"
|
||||
? approval.request_status.content.expiration_ts
|
||||
: undefined,
|
||||
? {
|
||||
request_id: approval.request_id,
|
||||
} as SortableQuoteWithAddress;
|
||||
expiration_ts: approval.request_status.content.expiration_ts,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
});
|
||||
|
||||
sortableQuotes.push(
|
||||
...known_quotes.map((quote) => ({
|
||||
...quote,
|
||||
request_id: null,
|
||||
quote_with_address: quote,
|
||||
approval: null,
|
||||
})),
|
||||
);
|
||||
|
||||
return sortMakerApprovals(sortableQuotes);
|
||||
}
|
||||
|
||||
export function sortMakerApprovals(list: SortableQuoteWithAddress[]) {
|
||||
return (
|
||||
_(list)
|
||||
_(sortableQuotes)
|
||||
.orderBy(
|
||||
[
|
||||
// Prefer makers that have a 'version' attribute
|
||||
// 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
|
||||
(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
|
||||
(m) => (isMakerVersionOutdated(m.version) ? 1 : 0),
|
||||
(m) => (isMakerVersionOutdated(m.quote_with_address.version) ? 1 : 0),
|
||||
// Prefer approvals over actual quotes
|
||||
(m) => (m.request_id ? 0 : 1),
|
||||
(m) => (m.approval ? 0 : 1),
|
||||
// Prefer makers with a lower price
|
||||
(m) => m.quote.price,
|
||||
(m) => m.quote_with_address.quote.price,
|
||||
],
|
||||
["asc", "asc", "asc", "asc", "asc"],
|
||||
)
|
||||
// Remove duplicate makers
|
||||
.uniqBy((m) => m.peer_id)
|
||||
.uniqBy((m) => m.quote_with_address.peer_id)
|
||||
.value()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
|
|
|
|||
|
|
@ -532,18 +532,6 @@
|
|||
dependencies:
|
||||
"@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":
|
||||
version "7.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-7.3.1.tgz#bd1bf1344cc7a69b6e459248b544f0ae97945b1d"
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ impl Amount {
|
|||
#[typeshare]
|
||||
pub struct LabeledMoneroAddress {
|
||||
// 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")]
|
||||
address: Option<monero::Address>,
|
||||
#[typeshare(serialized_as = "number")]
|
||||
|
|
|
|||
|
|
@ -78,9 +78,7 @@ pub fn init(
|
|||
let tracing_file_layer = json_rolling_layer!(
|
||||
&dir,
|
||||
"tracing",
|
||||
env_filter_with_all_crates(vec![
|
||||
(crates::OUR_CRATES.to_vec(), LevelFilter::TRACE)
|
||||
]),
|
||||
env_filter_with_all_crates(vec![(crates::OUR_CRATES.to_vec(), LevelFilter::TRACE)]),
|
||||
24
|
||||
);
|
||||
|
||||
|
|
@ -104,7 +102,10 @@ pub fn init(
|
|||
let monero_wallet_file_layer = json_rolling_layer!(
|
||||
&dir,
|
||||
"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
|
||||
);
|
||||
|
||||
|
|
@ -145,7 +146,9 @@ pub fn init(
|
|||
(crates::LIBP2P_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 {
|
||||
|
|
@ -227,10 +230,7 @@ mod crates {
|
|||
"unstoppableswap_gui_rs",
|
||||
];
|
||||
|
||||
pub const MONERO_WALLET_CRATES: &[&str] = &[
|
||||
"monero_cpp",
|
||||
"monero_rpc_pool",
|
||||
];
|
||||
pub const MONERO_WALLET_CRATES: &[&str] = &["monero_cpp", "monero_rpc_pool"];
|
||||
}
|
||||
|
||||
/// A writer that forwards tracing log messages to the tauri guest.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue