Reckless_Satoshi f7b504cea7 Minor lint fixes
2024-01-15 09:33:47 +00:00

826 lines
26 KiB
TypeScript

import React, { useContext, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
Alert,
DialogContent,
Divider,
Grid,
List,
ListItemText,
ListItem,
ListItemIcon,
Typography,
IconButton,
Tooltip,
Link,
Box,
CircularProgress,
Accordion,
AccordionDetails,
AccordionSummary,
AlertTitle,
} from '@mui/material';
import {
Inventory,
Sell,
SmartToy,
Percent,
PriceChange,
Book,
Reddit,
Key,
Bolt,
Description,
Dns,
Email,
Equalizer,
ExpandMore,
GitHub,
Language,
Send,
Tag,
} from '@mui/icons-material';
import LinkIcon from '@mui/icons-material/Link';
import { pn } from '../../utils';
import { type Contact } from '../../models';
import RobotAvatar from '../RobotAvatar';
import {
AmbossIcon,
BitcoinSignIcon,
RoboSatsNoTextIcon,
BadgeFounder,
BadgeDevFund,
BadgePrivacy,
BadgeLoved,
BadgeLimits,
NostrIcon,
SimplexIcon,
XIcon,
} from '../Icons';
import { AppContext } from '../../contexts/AppContext';
import { systemClient } from '../../services/System';
import { type Badges } from '../../models/Coordinator.model';
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
interface Props {
open: boolean;
onClose: () => void;
shortAlias: string | null;
network: 'mainnet' | 'testnet' | undefined;
}
const ContactButtons = ({
nostr,
pgp,
fingerprint,
email,
telegram,
twitter,
matrix,
simplex,
website,
reddit,
}: Contact): JSX.Element => {
const { t } = useTranslation();
const [showMatrix, setShowMatrix] = useState<boolean>(false);
const [showNostr, setShowNostr] = useState<boolean>(false);
return (
<Grid container direction='row' alignItems='center' justifyContent='center'>
{nostr !== undefined && (
<Grid item>
<Tooltip
title={
<div>
<Typography variant='body2'>
{t('...Opening on Nostr gateway. Pubkey copied!')}
</Typography>
<Typography variant='body2'>
<i>{nostr}</i>
</Typography>
</div>
}
open={showNostr}
>
<IconButton
onClick={() => {
setShowNostr(true);
setTimeout(() => window.open(`https://snort.social/p/${nostr}`, '_blank'), 1500);
setTimeout(() => {
setShowNostr(false);
}, 10000);
systemClient.copyToClipboard(nostr);
}}
>
<NostrIcon />
</IconButton>
</Tooltip>
</Grid>
)}
{pgp !== undefined && (
<Grid item>
<Tooltip
enterTouchDelay={0}
enterNextDelay={2000}
title={t('Download PGP Pubkey. Fingerprint: ') + fingerprint.match(/.{1,4}/g).join(' ')}
>
<IconButton component='a' target='_blank' href={pgp} rel='noreferrer'>
<Key />
</IconButton>
</Tooltip>
</Grid>
)}
{email !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Send Email')}>
<IconButton component='a' href={`mailto: ${email}`}>
<Email />
</IconButton>
</Tooltip>
</Grid>
)}
{telegram !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Telegram')}>
<IconButton
component='a'
target='_blank'
href={`https://t.me/${telegram}`}
rel='noreferrer'
>
<Send />
</IconButton>
</Tooltip>
</Grid>
)}
{twitter !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('X')}>
<IconButton
component='a'
target='_blank'
href={`https://x.com/${twitter}`}
rel='noreferrer'
>
<XIcon sx={{ width: '0.8em', height: '0.8em' }} />
</IconButton>
</Tooltip>
</Grid>
)}
{reddit !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Reddit')}>
<IconButton
component='a'
target='_blank'
href={`https://reddit.com/${reddit}`}
rel='noreferrer'
>
<Reddit />
</IconButton>
</Tooltip>
</Grid>
)}
{website !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Website')}>
<IconButton component='a' target='_blank' href={website} rel='noreferrer'>
<Language />
</IconButton>
</Tooltip>
</Grid>
)}
{matrix !== undefined && (
<Grid item>
<Tooltip
title={
<Typography variant='body2'>
{t('Matrix channel copied! {{matrix}}', { matrix })}
</Typography>
}
open={showMatrix}
>
<IconButton
onClick={() => {
setShowMatrix(true);
setTimeout(() => {
setShowMatrix(false);
}, 10000);
systemClient.copyToClipboard(matrix);
}}
>
<Tag />
</IconButton>
</Tooltip>
</Grid>
)}
{simplex !== undefined && (
<Grid item>
<Tooltip enterTouchDelay={0} enterNextDelay={2000} title={t('Simplex')}>
<IconButton component='a' target='_blank' href={`${simplex}`} rel='noreferrer'>
<SimplexIcon sx={{ width: '0.7em', height: '0.7em' }} />
</IconButton>
</Tooltip>
</Grid>
)}
</Grid>
);
};
interface BadgesProps {
badges: Badges | undefined;
}
const BadgesHall = ({ badges }: BadgesProps): JSX.Element => {
const { t } = useTranslation();
const sxProps = {
width: '3em',
height: '3em',
filter: 'drop-shadow(3px 3px 3px RGB(0,0,0,0.3))',
};
const tooltipProps = { enterTouchDelay: 0, enterNextDelay: 2000 };
return (
<Grid container direction='row' alignItems='center' justifyContent='center' spacing={1}>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{badges?.isFounder === true
? t('Founder: coordinating trades since the testnet federation.')
: t('Not a federation founder')}
</Typography>
}
>
<Grid item sx={{ filter: badges?.isFounder !== true ? 'grayscale(100%)' : undefined }}>
<BadgeFounder sx={sxProps} />
</Grid>
</Tooltip>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{t('Development fund supporter: donates {{percent}}% to make RoboSats better.', {
percent: badges?.donatesToDevFund,
})}
</Typography>
}
>
<Grid
item
sx={{ filter: Number(badges?.donatesToDevFund) >= 20 ? undefined : 'grayscale(100%)' }}
>
<BadgeDevFund sx={sxProps} />
</Grid>
</Tooltip>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{badges?.hasGoodOpSec === true
? t(
'Good OpSec: the coordinator follows best practices to protect his and your privacy.',
)
: t('The privacy practices of this coordinator could improve')}
</Typography>
}
>
<Grid item sx={{ filter: badges?.hasGoodOpSec === true ? undefined : 'grayscale(100%)' }}>
<BadgePrivacy sx={sxProps} />
</Grid>
</Tooltip>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{badges?.robotsLove === true
? t('Loved by robots: receives positive comments by robots over the internet.')
: t(
'The coordinator does not seem to receive exceptional love from robots over the internet',
)}
</Typography>
}
>
<Grid item sx={{ filter: badges?.robotsLove === true ? undefined : 'grayscale(100%)' }}>
<BadgeLoved sx={sxProps} />
</Grid>
</Tooltip>
<Tooltip
{...tooltipProps}
title={
<Typography align='center' variant='body2'>
{badges?.hasLargeLimits === true
? t('Large limits: the coordinator has large trade limits.')
: t('Does not have large trade limits.')}
</Typography>
}
>
<Grid item sx={{ filter: badges?.hasLargeLimits === true ? undefined : 'grayscale(100%)' }}>
<BadgeLimits sx={sxProps} />
</Grid>
</Tooltip>
</Grid>
);
};
const CoordinatorDialog = ({ open = false, onClose, network, shortAlias }: Props): JSX.Element => {
const { t } = useTranslation();
const { clientVersion, page, settings, origin } = useContext(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const coordinator = federation.getCoordinator(shortAlias);
const [expanded, setExpanded] = useState<'summary' | 'stats' | 'policies' | undefined>(undefined);
const listItemProps = { sx: { maxHeight: '3em', width: '100%' } };
const coordinatorVersion = `v${coordinator?.info?.version?.major ?? '?'}.${
coordinator?.info?.version?.minor ?? '?'
}.${coordinator?.info?.version?.patch ?? '?'}`;
return (
<Dialog open={open} onClose={onClose}>
<DialogContent>
<Typography align='center' component='h5' variant='h5'>
{String(coordinator?.longAlias)}
</Typography>
<List dense>
<ListItem sx={{ display: 'flex', justifyContent: 'center' }}>
<Grid container direction='column' alignItems='center' padding={0}>
<Grid item>
<RobotAvatar
shortAlias={coordinator?.shortAlias}
style={{ width: '7.5em', height: '7.5em' }}
smooth={true}
/>
</Grid>
<Grid item>
<Typography align='center' variant='body2'>
<i>{String(coordinator?.motto)}</i>
</Typography>
</Grid>
<Grid item>
<ContactButtons {...coordinator?.contact} />
</Grid>
</Grid>
</ListItem>
{['create'].includes(page) && (
<>
<ListItem {...listItemProps}>
<ListItemIcon>
<Percent />
</ListItemIcon>
<Grid container>
<Grid item xs={6}>
<ListItemText secondary={t('Maker fee')}>
{(coordinator?.info?.maker_fee ?? 0 * 100).toFixed(3)}%
</ListItemText>
</Grid>
<Grid item xs={6}>
<ListItemText secondary={t('Taker fee')}>
{(coordinator?.info?.taker_fee ?? 0 * 100).toFixed(3)}%
</ListItemText>
</Grid>
</Grid>
</ListItem>
<ListItem {...listItemProps}>
<ListItemIcon>
<LinkIcon />
</ListItemIcon>
<ListItemText
primary={`${String(coordinator?.info?.current_swap_fee_rate.toPrecision(3))}%`}
secondary={t('Current onchain payout fee')}
/>
</ListItem>
</>
)}
{Boolean(coordinator?.info?.notice_severity) &&
coordinator?.info?.notice_severity !== 'none' && (
<ListItem>
<Alert severity={coordinator?.info?.notice_severity} sx={{ width: '100%' }}>
<AlertTitle>{t('Coordinator Notice')}</AlertTitle>
<div
dangerouslySetInnerHTML={{ __html: coordinator?.info?.notice_message ?? '' }}
/>
</Alert>
</ListItem>
)}
<ListItem>
<BadgesHall badges={coordinator?.badges} />
</ListItem>
<ListItem>
<ListItemIcon>
<Description />
</ListItemIcon>
<ListItemText
primary={coordinator?.description}
primaryTypographyProps={{ sx: { maxWidth: '20em' } }}
secondary={t('Coordinator description')}
/>
</ListItem>
{coordinator?.mainnetNodesPubkeys?.[0] !== undefined && network === 'mainnet' ? (
<ListItem>
<ListItemIcon>
<AmbossIcon />
</ListItemIcon>
<ListItemText secondary={t('Mainnet LN Node')}>
<Link
target='_blank'
href={`https://amboss.space/node/${coordinator?.mainnetNodesPubkeys[0]}`}
rel='noreferrer'
>
{`${coordinator?.mainnetNodesPubkeys?.[0].slice(0, 12)}... (AMBOSS)`}
</Link>
</ListItemText>
</ListItem>
) : (
<></>
)}
{coordinator?.testnetNodesPubkeys?.[0] !== undefined && network === 'testnet' ? (
<ListItem>
<ListItemIcon>
<Dns />
</ListItemIcon>
<ListItemText secondary={t('Testnet LN Node')}>
<Link
target='_blank'
href={`https://1ml.com/testnet/node/${coordinator?.testnetNodesPubkeys[0]}`}
rel='noreferrer'
>
{`${coordinator?.testnetNodesPubkeys[0].slice(0, 12)}... (1ML)`}
</Link>
</ListItemText>
</ListItem>
) : (
<></>
)}
</List>
{coordinator?.loadingInfo ? (
<Box style={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress />
</Box>
) : coordinator?.info ? (
<Box>
{Boolean(coordinator?.policies) && (
<Accordion
expanded={expanded === 'policies'}
onChange={() => {
setExpanded(expanded === 'policies' ? undefined : 'policies');
}}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography>{t('Policies')}</Typography>
</AccordionSummary>
<AccordionDetails sx={{ padding: 0 }}>
<List dense>
{Object.keys(coordinator?.policies).map((key, index) => (
<ListItem key={index} sx={{ maxWidth: '24em' }}>
<ListItemIcon>{index + 1}</ListItemIcon>
<ListItemText primary={key} secondary={coordinator?.policies[key]} />
</ListItem>
))}
</List>
</AccordionDetails>
</Accordion>
)}
<Accordion
expanded={expanded === 'summary'}
onChange={() => {
setExpanded(expanded === 'summary' ? undefined : 'summary');
}}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography>{t('Summary')}</Typography>
</AccordionSummary>
<AccordionDetails sx={{ padding: 0 }}>
<List dense>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Percent />
</ListItemIcon>
<Grid container>
<Grid item xs={6}>
<ListItemText secondary={t('Maker fee')}>
{(coordinator?.info?.maker_fee * 100).toFixed(3)}%
</ListItemText>
</Grid>
<Grid item xs={6}>
<ListItemText secondary={t('Taker fee')}>
{(coordinator?.info?.taker_fee * 100).toFixed(3)}%
</ListItemText>
</Grid>
</Grid>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<LinkIcon />
</ListItemIcon>
<ListItemText
primary={`${coordinator?.info?.current_swap_fee_rate.toPrecision(3)}%`}
secondary={t('Current onchain payout fee')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Inventory />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.num_public_buy_orders}
secondary={t('Public buy orders')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Sell />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.num_public_sell_orders}
secondary={t('Public sell orders')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Book />
</ListItemIcon>
<ListItemText
primary={`${pn(coordinator?.info?.book_liquidity)} Sats`}
secondary={t('Book liquidity')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<SmartToy />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.active_robots_today}
secondary={t('Today active robots')}
/>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<PriceChange />
</ListItemIcon>
<ListItemText
primary={`${coordinator?.info?.last_day_nonkyc_btc_premium}%`}
secondary={t('24h non-KYC bitcoin premium')}
/>
</ListItem>
</List>
</AccordionDetails>
</Accordion>
<Accordion
expanded={expanded === 'stats'}
onChange={() => {
setExpanded(expanded === 'stats' ? undefined : 'stats');
}}
>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography>{t('Stats for Nerds')}</Typography>
</AccordionSummary>
<AccordionDetails>
<List dense>
<ListItem {...listItemProps}>
<ListItemIcon>
<Dns />
</ListItemIcon>
<ListItemText
secondary={t('Coordinator domain')}
primaryTypographyProps={{
style: {
maxWidth: '20em',
wordWrap: 'break-word',
overflowWrap: 'break-word',
},
}}
>
<Link
target='_blank'
href={
coordinator?.[settings.network][
settings.selfhostedClient ? 'onion' : origin
]
}
rel='noreferrer'
>
{`${String(
coordinator?.[settings.network][
settings.selfhostedClient ? 'onion' : origin
],
)}`}
</Link>
</ListItemText>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<RoboSatsNoTextIcon
sx={{
width: '1.4em',
height: '1.4em',
right: '0.2em',
position: 'relative',
}}
/>
</ListItemIcon>
<ListItemText
primary={`${t('Coordinator')} ${coordinatorVersion} - ${t('Client')} ${String(
clientVersion.short,
)}`}
secondary={t('RoboSats version')}
/>
</ListItem>
<Divider />
{coordinator?.info?.lnd_version !== undefined && (
<ListItem {...listItemProps}>
<ListItemIcon>
<Bolt />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.lnd_version}
secondary={t('LND version')}
/>
</ListItem>
)}
{Boolean(coordinator?.info?.cln_version) && (
<ListItem {...listItemProps}>
<ListItemIcon>
<Bolt />
</ListItemIcon>
<ListItemText
primary={coordinator?.info?.cln_version}
secondary={t('CLN version')}
/>
</ListItem>
)}
<Divider />
{coordinator?.info?.network === 'testnet' ? (
<ListItem {...listItemProps}>
<ListItemIcon>
<Dns />
</ListItemIcon>
<ListItemText secondary={`${t('LN Node')}: ${coordinator?.info?.node_alias}`}>
<Link
target='_blank'
href={`https://1ml.com/testnet/node/${coordinator?.info?.node_id}`}
rel='noreferrer'
>
{`${coordinator?.info?.node_id.slice(0, 12)}... (1ML)`}
</Link>
</ListItemText>
</ListItem>
) : (
<ListItem {...listItemProps}>
<ListItemIcon>
<AmbossIcon />
</ListItemIcon>
<ListItemText secondary={coordinator?.info?.node_alias}>
<Link
target='_blank'
href={`https://amboss.space/node/${coordinator?.info?.node_id}`}
rel='noreferrer'
>
{`${coordinator?.info?.node_id.slice(0, 12)}... (AMBOSS)`}
</Link>
</ListItemText>
</ListItem>
)}
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<GitHub />
</ListItemIcon>
<ListItemText secondary={t('Coordinator commit hash')}>
<Link
target='_blank'
href={`https://github.com/Reckless-Satoshi/robosats/tree/${coordinator?.info?.robosats_running_commit_hash}`}
rel='noreferrer'
>
{`${coordinator?.info?.robosats_running_commit_hash.slice(0, 12)}...`}
</Link>
</ListItemText>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Equalizer />
</ListItemIcon>
<ListItemText secondary={t('24h contracted volume')}>
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{pn(parseFloat(coordinator?.info?.last_day_volume).toFixed(8))}
<BitcoinSignIcon
sx={{ width: '1em', height: '1em' }}
color={'text.secondary'}
/>
</div>
</ListItemText>
</ListItem>
<Divider />
<ListItem {...listItemProps}>
<ListItemIcon>
<Equalizer />
</ListItemIcon>
<ListItemText secondary={t('Lifetime contracted volume')}>
<div
style={{
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap',
}}
>
{pn(parseFloat(coordinator?.info?.lifetime_volume).toFixed(8))}
<BitcoinSignIcon
sx={{ width: '1em', height: '1em' }}
color={'text.secondary'}
/>
</div>
</ListItemText>
</ListItem>
</List>
</AccordionDetails>
</Accordion>
</Box>
) : (
<Typography align='center' variant='h6' color='error'>
{t('Coordinator offline')}
</Typography>
)}
</DialogContent>
</Dialog>
);
};
export default CoordinatorDialog;