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:
Mohan 2025-11-05 18:38:00 +01:00 committed by GitHub
parent 3ce8e360c5
commit 5948a40c8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 312 additions and 648 deletions

View file

@ -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.

View file

@ -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",

View file

@ -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"
);
}

View file

@ -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>
);

View file

@ -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

View file

@ -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>
);

View file

@ -50,7 +50,7 @@ function TimelineSegment({
opacity: isActive ? 1 : 0.3,
}}
>
{isActive && (
{isActive && durationOfSegment && (
<Box
sx={{
position: "absolute",

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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,

View file

@ -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}

View file

@ -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)}

View 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;

View file

@ -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>,
]}
/>
);
}

View file

@ -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>

View file

@ -9,7 +9,7 @@ export default function TruncatedText({
ellipsis?: string;
truncateMiddle?: boolean;
}) {
let finalChildren = children ?? "";
const finalChildren = children ?? "";
const truncatedText =
finalChildren.length > limit

View file

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

View file

@ -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],

View file

@ -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}

View file

@ -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

View file

@ -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"

View file

@ -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>

View file

@ -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");
}

View file

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

View file

@ -5,9 +5,9 @@ import dayjs from "dayjs";
import TransactionItem from "./TransactionItem";
interface TransactionHistoryProps {
history?: {
history: {
transactions: TransactionInfo[];
};
} | null;
}
interface TransactionGroup {

View file

@ -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",

View file

@ -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>
))}

View file

@ -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",

View file

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

View file

@ -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={

View file

@ -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);

View file

@ -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: [
{

View file

@ -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,
},
);

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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 {

View file

@ -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) =>

View file

@ -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()
);
}

View file

@ -15,7 +15,7 @@
"jsx": "react-jsx",
/* Linting */
"strict": false,
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": false,

View file

@ -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"

View file

@ -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")]

View file

@ -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.