feat(gui): Allow to select from recently used monero addresses (#139)

* feat(gui): Allow user to select from recently used monero addresses in textfield
This commit is contained in:
binarybaron 2024-11-09 12:11:00 +01:00 committed by GitHub
parent 4867d2713f
commit bd3fca7e41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 271 additions and 78 deletions

View file

@ -10,6 +10,14 @@ import SwapPage from "./pages/swap/SwapPage";
import WalletPage from "./pages/wallet/WalletPage";
import GlobalSnackbarProvider from "./snackbar/GlobalSnackbarProvider";
import UpdaterDialog from "./modal/updater/UpdaterDialog";
import { initEventListeners } from "renderer/rpc";
import { useEffect } from "react";
import { fetchProvidersViaHttp, fetchAlertsViaHttp, fetchXmrPrice, fetchBtcPrice, fetchXmrBtcRate } from "renderer/api";
import { store } from "renderer/store/storeRenderer";
import { setAlerts } from "store/features/alertsSlice";
import { setRegistryProviders, registryConnectionFailed } from "store/features/providersSlice";
import { setXmrPrice, setBtcPrice, setXmrBtcRate } from "store/features/ratesSlice";
import logger from "utils/logger";
const useStyles = makeStyles((theme) => ({
innerContent: {
@ -52,6 +60,11 @@ function InnerContent() {
}
export default function App() {
useEffect(() => {
fetchInitialData();
initEventListeners();
}, []);
return (
<ThemeProvider theme={theme}>
<GlobalSnackbarProvider>
@ -65,3 +78,46 @@ export default function App() {
</ThemeProvider>
);
}
async function fetchInitialData() {
try {
const providerList = await fetchProvidersViaHttp();
store.dispatch(setRegistryProviders(providerList));
logger.info(
{ providerList },
"Fetched providers via UnstoppableSwap HTTP API",
);
} catch (e) {
store.dispatch(registryConnectionFailed());
logger.error(e, "Failed to fetch providers via UnstoppableSwap HTTP API");
}
try {
const alerts = await fetchAlertsViaHttp();
store.dispatch(setAlerts(alerts));
logger.info({ alerts }, "Fetched alerts via UnstoppableSwap HTTP API");
} catch (e) {
logger.error(e, "Failed to fetch alerts via UnstoppableSwap HTTP API");
}
try {
const xmrPrice = await fetchXmrPrice();
store.dispatch(setXmrPrice(xmrPrice));
logger.info({ xmrPrice }, "Fetched XMR price");
const btcPrice = await fetchBtcPrice();
store.dispatch(setBtcPrice(btcPrice));
logger.info({ btcPrice }, "Fetched BTC price");
} catch (e) {
logger.error(e, "Error retrieving fiat prices");
}
try {
const xmrBtcRate = await fetchXmrBtcRate();
store.dispatch(setXmrBtcRate(xmrBtcRate));
logger.info({ xmrBtcRate }, "Fetched XMR/BTC rate");
} catch (e) {
logger.error(e, "Error retrieving XMR/BTC rate");
}
}

View file

@ -1,8 +1,18 @@
import { TextField } from "@material-ui/core";
import { Box, Button, Dialog, DialogActions, DialogContent, IconButton, List, ListItem, ListItemText, TextField } from "@material-ui/core";
import { TextFieldProps } from "@material-ui/core/TextField/TextField";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { getMoneroAddresses } from "renderer/rpc";
import { isTestnet } from "store/config";
import { isXmrAddressValid } from "utils/conversionUtils";
import ImportContactsIcon from '@material-ui/icons/ImportContacts';
import TruncatedText from "../other/TruncatedText";
type MoneroAddressTextFieldProps = TextFieldProps & {
address: string;
onAddressChange: (address: string) => void;
onAddressValidityChange: (valid: boolean) => void;
helperText: string;
}
export default function MoneroAddressTextField({
address,
@ -10,30 +20,116 @@ export default function MoneroAddressTextField({
onAddressValidityChange,
helperText,
...props
}: {
address: string;
onAddressChange: (address: string) => void;
onAddressValidityChange: (valid: boolean) => void;
helperText: string;
} & TextFieldProps) {
}: MoneroAddressTextFieldProps) {
const [addresses, setAddresses] = useState<string[]>([]);
const [showDialog, setShowDialog] = useState(false);
// Validation
const placeholder = isTestnet() ? "59McWTPGc745..." : "888tNkZrPN6J...";
const errorText = isXmrAddressValid(address, isTestnet())
? null
: "Not a valid Monero address";
// Effects
useEffect(() => {
onAddressValidityChange(!errorText);
}, [address, onAddressValidityChange, errorText]);
useEffect(() => {
const fetchAddresses = async () => {
const response = await getMoneroAddresses();
setAddresses(response.addresses);
};
fetchAddresses();
}, []);
// Event handlers
const handleClose = () => setShowDialog(false);
const handleAddressSelect = (selectedAddress: string) => {
onAddressChange(selectedAddress);
handleClose();
};
return (
<TextField
value={address}
onChange={(e) => onAddressChange(e.target.value)}
error={!!errorText && address.length > 0}
helperText={address.length > 0 ? errorText || helperText : helperText}
placeholder={placeholder}
variant="outlined"
{...props}
/>
<Box>
<TextField
value={address}
onChange={(e) => onAddressChange(e.target.value)}
error={!!errorText && address.length > 0}
helperText={address.length > 0 ? errorText || helperText : helperText}
placeholder={placeholder}
variant="outlined"
InputProps={{
endAdornment: addresses?.length > 0 && (
<IconButton onClick={() => setShowDialog(true)} size="small">
<ImportContactsIcon />
</IconButton>
)
}}
{...props}
/>
<RecentlyUsedAddressesDialog
open={showDialog}
onClose={handleClose}
addresses={addresses}
onAddressSelect={handleAddressSelect}
/>
</Box>
);
}
interface RecentlyUsedAddressesDialogProps {
open: boolean;
onClose: () => void;
addresses: string[];
onAddressSelect: (address: string) => void;
}
function RecentlyUsedAddressesDialog({
open,
onClose,
addresses,
onAddressSelect
}: RecentlyUsedAddressesDialogProps) {
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
>
<DialogContent>
<List>
{addresses.map((addr) => (
<ListItem
button
key={addr}
onClick={() => onAddressSelect(addr)}
>
<ListItemText
primary={
<Box fontFamily="monospace">
<TruncatedText limit={40} truncateMiddle>
{addr}
</TruncatedText>
</Box>
}
secondary="Recently used as a redeem address"
/>
</ListItem>
))}
</List>
</DialogContent>
<DialogActions>
<Button
onClick={onClose}
variant="contained"
color="primary"
>
Close
</Button>
</DialogActions>
</Dialog>
);
}

View file

@ -2,13 +2,20 @@ export default function TruncatedText({
children,
limit = 6,
ellipsis = "...",
truncateMiddle = false,
}: {
children: string;
limit?: number;
ellipsis?: string;
truncateMiddle?: boolean;
}) {
const truncatedText =
children.length > limit ? children.slice(0, limit) + ellipsis : children;
const truncatedText = children.length > limit
? truncateMiddle
? children.slice(0, Math.floor(limit/2)) +
ellipsis +
children.slice(children.length - Math.floor(limit/2))
: children.slice(0, limit) + ellipsis
: children;
return truncatedText;
return <span>{truncatedText}</span>;
}

View file

@ -28,50 +28,4 @@ root.render(
<App />
</PersistGate>
</Provider>,
);
async function fetchInitialData() {
try {
const providerList = await fetchProvidersViaHttp();
store.dispatch(setRegistryProviders(providerList));
logger.info(
{ providerList },
"Fetched providers via UnstoppableSwap HTTP API",
);
} catch (e) {
store.dispatch(registryConnectionFailed());
logger.error(e, "Failed to fetch providers via UnstoppableSwap HTTP API");
}
try {
const alerts = await fetchAlertsViaHttp();
store.dispatch(setAlerts(alerts));
logger.info({ alerts }, "Fetched alerts via UnstoppableSwap HTTP API");
} catch (e) {
logger.error(e, "Failed to fetch alerts via UnstoppableSwap HTTP API");
}
try {
const xmrPrice = await fetchXmrPrice();
store.dispatch(setXmrPrice(xmrPrice));
logger.info({ xmrPrice }, "Fetched XMR price");
const btcPrice = await fetchBtcPrice();
store.dispatch(setBtcPrice(btcPrice));
logger.info({ btcPrice }, "Fetched BTC price");
} catch (e) {
logger.error(e, "Error retrieving fiat prices");
}
try {
const xmrBtcRate = await fetchXmrBtcRate();
store.dispatch(setXmrBtcRate(xmrBtcRate));
logger.info({ xmrBtcRate }, "Fetched XMR/BTC rate");
} catch (e) {
logger.error(e, "Error retrieving XMR/BTC rate");
}
}
fetchInitialData();
initEventListeners();
);

View file

@ -22,6 +22,7 @@ import {
TauriTimelockChangeEvent,
GetSwapInfoArgs,
ExportBitcoinWalletResponse,
GetMoneroAddressesResponse,
} from "models/tauriModel";
import {
contextStatusEventReceived,
@ -218,4 +219,8 @@ export async function initializeContext() {
export async function getWalletDescriptor() {
return await invokeNoArgs<ExportBitcoinWalletResponse>("get_wallet_descriptor");
}
export async function getMoneroAddresses(): Promise<GetMoneroAddressesResponse> {
return await invokeNoArgs<GetMoneroAddressesResponse>("get_monero_addresses");
}