import React, { useContext, useEffect, 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, ListItemButton, Rating, } from '@mui/material'; import { Inventory, Sell, SmartToy, Percent, PriceChange, Book, Reddit, Key, Bolt, Description, Dns, Email, Equalizer, ExpandMore, GitHub, Language, Send, Tag, Web, VolunteerActivism, Circle, Flag, ApiOutlined, } 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 Coordinator from '../../models/Coordinator.model'; import { type Badges } from '../../models/Coordinator.model'; import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext'; import { verifyCoordinatorToken } from '../../utils/nostr'; 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(false); const [showNostr, setShowNostr] = useState(false); const [client] = window.RobosatsSettings.split('-'); return ( {nostr !== undefined && ( {t('...Opening on Nostr gateway. Pubkey copied!')} {nostr} } open={showNostr} > { setShowNostr(true); setTimeout(() => { if (client === 'mobile') { window.location.href = `nostr:${nostr}`; } else { window.open(`https://njump.me/${nostr}`, '_blank', 'noopener,noreferrer'); } }, 1500); setTimeout(() => { setShowNostr(false); }, 10000); systemClient.copyToClipboard(nostr); }} > )} {pgp && fingerprint && ( )} {email !== undefined && ( )} {telegram !== undefined && ( )} {twitter !== undefined && ( )} {reddit !== undefined && ( )} {website !== undefined && ( )} {matrix !== undefined && ( {t('Matrix channel copied! {{matrix}}', { matrix })} } open={showMatrix} > { setShowMatrix(true); setTimeout(() => { setShowMatrix(false); }, 10000); systemClient.copyToClipboard(matrix); }} > )} {simplex !== undefined && ( )} ); }; interface BadgesProps { badges: Badges | undefined; size_limit: number | undefined; } const BadgesHall = ({ badges, size_limit }: 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 ( {badges?.isFounder === true ? t('Founder: coordinating trades since the testnet federation.') : t('Not a federation founder')} } > {t('Development fund supporter: donates {{percent}}% to make RoboSats better.', { percent: badges?.donatesToDevFund, })} } > = 20 ? undefined : 'grayscale(100%)' }} > {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')} } > {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', )} } > {size_limit > 3000000 ? t('Large limits: the coordinator has large trade limits.') : t('Does not have large trade limits.')} } > 3000000 ? undefined : 'grayscale(100%)' }}> ); }; const CoordinatorDialog = ({ open = false, onClose, shortAlias }: Props): JSX.Element => { const { t } = useTranslation(); const { clientVersion, page, settings, origin } = useContext(AppContext); const { federation } = useContext(FederationContext); const [rating, setRating] = useState([0, 0]); const [averageRating, setAvergeRating] = useState(0); const [expanded, setExpanded] = useState<'summary' | 'stats' | 'policies' | undefined>(undefined); const [coordinator, setCoordinator] = useState( federation.getCoordinator(shortAlias ?? ''), ); const listItemProps = { sx: { maxHeight: '3em', width: '100%' } }; const coordinatorVersion = `v${coordinator?.info?.version?.major ?? '?'}.${ coordinator?.info?.version?.minor ?? '?' }.${coordinator?.info?.version?.patch ?? '?'}`; useEffect(() => { setCoordinator(federation.getCoordinator(shortAlias ?? '')); setRating([0, 0]); setAvergeRating(0); }, [shortAlias]); useEffect(() => { if (open) { const coordinator = federation.getCoordinator(shortAlias ?? ''); if (settings.connection === 'nostr') { federation.roboPool.subscribeRatings( { onevent: (event) => { const verfied = verifyCoordinatorToken(event); const coordinatorPubKey = event.tags.find((t) => t[0] === 'p')?.[1]; if (verfied && coordinatorPubKey === coordinator.nostrHexPubkey) { const eventRating = event.tags.find((t) => t[0] === 'rating')?.[1]; if (eventRating) { setRating((prev) => { const sum = prev[0]; const count = prev[1] + 1; prev = [sum + parseFloat(eventRating), count]; setAvergeRating(sum / count); return prev; }); } } }, oneose: () => {}, }, [coordinator.nostrHexPubkey], ); } coordinator?.loadInfo(); } }, [open]); return ( {String(coordinator?.longAlias)} {String(coordinator?.motto)} {`(${rating[1]})`} {['create'].includes(page) && ( <> {((coordinator?.info?.maker_fee ?? 0) * 100).toFixed(3)}% {((coordinator?.info?.taker_fee ?? 0) * 100).toFixed(3)}% )} {Boolean(coordinator?.info?.notice_severity) && coordinator?.info?.notice_severity !== 'none' && ( {t('Coordinator Notice')}
)} {coordinator?.[settings.network] && ( {`${String( coordinator?.[settings.network][settings.selfhostedClient ? 'onion' : origin], )}`} )} {!coordinator || coordinator?.loadingInfo ? ( ) : coordinator?.info ? ( {Boolean(coordinator?.policies) && ( { setExpanded(expanded === 'policies' ? undefined : 'policies'); }} > }> {t('Policies')} {Object.keys(coordinator?.policies).map((key, index) => ( {index + 1} ))} )} { setExpanded(expanded === 'summary' ? undefined : 'summary'); }} > }> {t('Summary')} {(coordinator?.info?.maker_fee * 100).toFixed(3)}% {(coordinator?.info?.taker_fee * 100).toFixed(3)}% {!coordinator?.info?.swap_enabled ? ( ) : ( <> )} { setExpanded(expanded === 'stats' ? undefined : 'stats'); }} > }> {t('Stats for Nerds')} {coordinator?.info?.lnd_version !== undefined && ( )} {Boolean(coordinator?.info?.cln_version) && ( )} {coordinator?.info?.network === 'testnet' ? ( {`${coordinator?.info?.node_id.slice(0, 12)}... (1ML)`} ) : ( {`${coordinator?.info?.node_id.slice(0, 12)}... (AMBOSS)`} )} {`${coordinator?.info?.robosats_running_commit_hash.slice(0, 12)}...`}
{pn(parseFloat(coordinator?.info?.last_day_volume).toFixed(8))}
{pn(parseFloat(coordinator?.info?.lifetime_volume).toFixed(8))}
) : ( {t('Coordinator offline')} )}
); }; export default CoordinatorDialog;