Add external coordinator

This commit is contained in:
KoalaSat
2024-05-17 16:36:47 +02:00
parent 997e9aeacc
commit 08e2633181
15 changed files with 227 additions and 66 deletions

View File

@ -39,24 +39,26 @@ const OrderPage = (): JSX.Element => {
useEffect(() => {
const shortAlias = params.shortAlias;
const coordinator = federation.getCoordinator(shortAlias ?? '');
const { url, basePath } = coordinator.getEndpoint(
settings.network,
origin,
settings.selfhostedClient,
hostUrl,
);
if (coordinator) {
const { url, basePath } = coordinator?.getEndpoint(
settings.network,
origin,
settings.selfhostedClient,
hostUrl,
);
setBaseUrl(`${url}${basePath}`);
setBaseUrl(`${url}${basePath}`);
const orderId = Number(params.orderId);
if (
orderId &&
currentOrderId.id !== orderId &&
currentOrderId.shortAlias !== shortAlias &&
shortAlias
)
setCurrentOrderId({ id: orderId, shortAlias });
if (!acknowledgedWarning) setOpen({ ...closeAll, warning: true });
const orderId = Number(params.orderId);
if (
orderId &&
currentOrderId.id !== orderId &&
currentOrderId.shortAlias !== shortAlias &&
shortAlias
)
setCurrentOrderId({ id: orderId, shortAlias });
if (!acknowledgedWarning) setOpen({ ...closeAll, warning: true });
}
}, [params, currentOrderId]);
const onClickCoordinator = function (): void {
@ -98,7 +100,7 @@ const OrderPage = (): JSX.Element => {
setOpen(closeAll);
setAcknowledgedWarning(true);
}}
longAlias={federation.getCoordinator(params.shortAlias ?? '').longAlias}
longAlias={federation.getCoordinator(params.shortAlias ?? '')?.longAlias}
/>
{currentOrder === null && badOrder === undefined && <CircularProgress />}
{badOrder !== undefined ? (

View File

@ -1,12 +1,34 @@
import React, { useContext } from 'react';
import { Grid, Paper } from '@mui/material';
import React, { useContext, useState } from 'react';
import { Button, Grid, List, ListItem, Paper, TextField, Typography } from '@mui/material';
import SettingsForm from '../../components/SettingsForm';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import FederationTable from '../../components/FederationTable';
import { t } from 'i18next';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
const SettingsPage = (): JSX.Element => {
const { windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { federation, addNewCoordinator } = useContext<UseFederationStoreType>(FederationContext);
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
const [newAlias, setNewAlias] = useState<string>('');
const [newUrl, setNewUrl] = useState<string>('');
const [error, setError] = useState<string>();
// Regular expression to match a valid .onion URL
const onionUrlPattern = /^(http:\/\/|https:\/\/)?[a-zA-Z2-7]{16,56}\.onion$/;
const addCoordinator = () => {
if (federation.coordinators[newAlias]) {
setError(t('Alias already exists'));
} else {
if (onionUrlPattern.test(newUrl)) {
addNewCoordinator(newAlias, newUrl);
setNewAlias('');
setNewUrl('');
} else {
setError(t('Invalid URL'));
}
}
};
return (
<Paper
@ -26,6 +48,42 @@ const SettingsPage = (): JSX.Element => {
<Grid item>
<FederationTable maxHeight={18} />
</Grid>
<Grid item>
<Typography align='center' component='h2' variant='subtitle2' color='secondary'>
{error}
</Typography>
</Grid>
<List>
<ListItem>
<TextField
id='outlined-basic'
label={t('Alias')}
variant='outlined'
size='small'
value={newAlias}
onChange={(e) => setNewAlias(e.target.value)}
/>
<TextField
id='outlined-basic'
label={t('URL')}
variant='outlined'
size='small'
value={newUrl}
onChange={(e) => setNewUrl(e.target.value)}
/>
<Button
sx={{ maxHeight: 38 }}
disabled={false}
onClick={addCoordinator}
variant='contained'
color='primary'
size='small'
type='submit'
>
{t('Add')}
</Button>
</ListItem>
</List>
</Grid>
</Paper>
);

View File

@ -359,7 +359,8 @@ const BookControl = ({
>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
<RobotAvatar
shortAlias={coordinator.shortAlias}
shortAlias={coordinator.federated ? coordinator.shortAlias : undefined}
hashId={coordinator.federated ? undefined : coordinator.shortAlias}
style={{ width: '1.55em', height: '1.55em' }}
smooth={true}
small={true}

View File

@ -253,6 +253,7 @@ const BookTable = ({
headerName: t('Host'),
width: width * fontSize,
renderCell: (params: any) => {
const coordinator = federation.coordinators[params.row.coordinatorShortAlias];
return (
<ListItemButton
style={{ cursor: 'pointer' }}
@ -262,7 +263,8 @@ const BookTable = ({
>
<ListItemAvatar sx={{ position: 'relative', left: '-1.54em', bottom: '0.4em' }}>
<RobotAvatar
shortAlias={params.row.coordinatorShortAlias}
shortAlias={coordinator.federated ? params.row.coordinatorShortAlias : undefined}
hashId={coordinator.federated ? undefined : coordinator.mainnet.onion}
style={{ width: '3.215em', height: '3.215em' }}
smooth={true}
small={true}
@ -900,7 +902,6 @@ const BookTable = ({
((federation.exchange.enabledCoordinators - federation.exchange.loadingCoordinators) /
federation.exchange.enabledCoordinators) *
100;
if (!fullscreen) {
return (
<Paper

View File

@ -127,7 +127,7 @@ const ContactButtons = ({
</Grid>
)}
{pgp !== undefined && (
{pgp && fingerprint && (
<Grid item>
<Tooltip
enterTouchDelay={0}
@ -368,7 +368,8 @@ const CoordinatorDialog = ({ open = false, onClose, network, shortAlias }: Props
<Grid container direction='column' alignItems='center' padding={0}>
<Grid item>
<RobotAvatar
shortAlias={coordinator?.shortAlias}
shortAlias={coordinator?.federated ? coordinator?.shortAlias : undefined}
hashId={coordinator?.federated ? undefined : coordinator?.shortAlias}
style={{ width: '7.5em', height: '7.5em' }}
smooth={true}
/>

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState, useContext } from 'react';
import React, { useCallback, useEffect, useState, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, useTheme, Checkbox, CircularProgress, Typography, Grid } from '@mui/material';
import { DataGrid, type GridColDef, type GridValidRowModel } from '@mui/x-data-grid';
@ -21,9 +21,9 @@ const FederationTable = ({
fillContainer = false,
}: FederationTableProps): JSX.Element => {
const { t } = useTranslation();
const { federation, sortedCoordinators, coordinatorUpdatedAt } =
const { federation, sortedCoordinators, coordinatorUpdatedAt, federationUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { setOpen } = useContext<UseAppStoreType>(AppContext);
const { setOpen, settings } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme();
const [pageSize, setPageSize] = useState<number>(0);
@ -43,7 +43,7 @@ const FederationTable = ({
if (useDefaultPageSize) {
setPageSize(defaultPageSize);
}
}, [coordinatorUpdatedAt]);
}, [coordinatorUpdatedAt, federationUpdatedAt]);
const localeText = {
MuiTablePagination: { labelRowsPerPage: t('Coordinators per page:') },
@ -62,6 +62,7 @@ const FederationTable = ({
headerName: t('Coordinator'),
width: width * fontSize,
renderCell: (params: any) => {
const coordinator = federation.coordinators[params.row.shortAlias];
return (
<Grid
container
@ -76,7 +77,8 @@ const FederationTable = ({
>
<Grid item>
<RobotAvatar
shortAlias={params.row.shortAlias}
shortAlias={coordinator.federated ? params.row.shortAlias : undefined}
hashId={coordinator.federated ? undefined : coordinator.mainnet.onion}
style={{ width: '3.215em', height: '3.215em' }}
smooth={true}
small={true}
@ -212,10 +214,13 @@ const FederationTable = ({
}
};
const reorderedCoordinators = sortedCoordinators.reduce((coordinators, key) => {
coordinators[key] = federation.coordinators[key];
return coordinators;
}, {});
const reorderedCoordinators = useMemo(() => {
return sortedCoordinators.reduce((coordinators, key) => {
coordinators[key] = federation.coordinators[key];
return coordinators;
}, {});
}, [settings.network, federationUpdatedAt]);
return (
<Box

View File

@ -26,8 +26,7 @@ const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({
setCoordinator,
}) => {
const { setOpen } = useContext<UseAppStoreType>(AppContext);
const { federation, sortedCoordinators, coordinatorUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const theme = useTheme();
const { t } = useTranslation();
@ -41,10 +40,7 @@ const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({
setCoordinator(e.target.value);
};
const coordinator = useMemo(
() => federation.getCoordinator(coordinatorAlias),
[coordinatorUpdatedAt],
);
const coordinator = federation.getCoordinator(coordinatorAlias);
return (
<Grid item>
@ -83,7 +79,8 @@ const SelectCoordinator: React.FC<SelectCoordinatorProps> = ({
>
<Grid item>
<RobotAvatar
shortAlias={coordinatorAlias}
shortAlias={coordinator?.federated ? coordinator.shortAlias : undefined}
hashId={coordinator?.federated ? undefined : coordinator.shortAlias}
style={{ width: '3em', height: '3em' }}
smooth={true}
flipHorizontally={false}

View File

@ -265,7 +265,12 @@ const OrderDetails = ({
{' '}
<Grid container direction='row' justifyContent='center' alignItems='center'>
<Grid item xs={2}>
<RobotAvatar shortAlias={coordinator.shortAlias} small={true} smooth={true} />
<RobotAvatar
shortAlias={coordinator.federated ? coordinator.shortAlias : undefined}
hashId={coordinator.federated ? undefined : coordinator.shortAlias}
small={true}
smooth={true}
/>
</Grid>
<Grid item xs={4}>
<ListItemText primary={coordinator.longAlias} secondary={t('Order host')} />

View File

@ -70,8 +70,8 @@ const RobotAvatar: React.FC<Props> = ({
}, [hashId]);
useEffect(() => {
if (shortAlias !== undefined) {
if (window.NativeRobosats === undefined) {
if (shortAlias && shortAlias !== '') {
if (!window.NativeRobosats) {
setAvatarSrc(
`${hostUrl}/static/federation/avatars/${shortAlias}${small ? '.small' : ''}.webp`,
);

View File

@ -26,6 +26,7 @@ import {
AccountBalance,
AttachMoney,
QrCode,
ControlPoint,
} from '@mui/icons-material';
import { systemClient } from '../../services/System';
import { TorIcon } from '../Icons';

View File

@ -9,7 +9,7 @@ import React, {
type ReactNode,
} from 'react';
import { type Order, Federation, Settings } from '../models';
import { type Order, Federation, Settings, Coordinator } from '../models';
import { federationLottery } from '../utils';
@ -59,6 +59,7 @@ export interface UseFederationStoreType {
currentOrder: Order | null;
coordinatorUpdatedAt: string;
federationUpdatedAt: string;
addNewCoordinator: (alias: string, url: string) => void;
}
export const initialFederationContext: UseFederationStoreType = {
@ -70,6 +71,7 @@ export const initialFederationContext: UseFederationStoreType = {
currentOrder: null,
coordinatorUpdatedAt: '',
federationUpdatedAt: '',
addNewCoordinator: () => {},
};
export const FederationContext = createContext<UseFederationStoreType>(initialFederationContext);
@ -81,7 +83,7 @@ export const FederationContextProvider = ({
useContext<UseAppStoreType>(AppContext);
const { setMaker, garage, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
const [federation] = useState(new Federation(origin, settings, hostUrl));
const sortedCoordinators = useMemo(() => federationLottery(federation), []);
const [sortedCoordinators, setSortedCoordinators] = useState(federationLottery(federation));
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>(
new Date().toISOString(),
);
@ -164,6 +166,30 @@ export const FederationContextProvider = ({
}
};
const addNewCoordinator: (alias: string, url: string) => void = (alias, url) => {
if (!federation.coordinators[alias]) {
const attributes: Record<any, any> = {
longAlias: alias,
shortAlias: alias,
federated: false,
enabled: true,
};
if (settings.network === 'mainnet') {
attributes.mainnet = url;
} else {
attributes.testnet = url;
}
federation.addCoordinator(origin, settings, hostUrl, attributes);
const newCoordinator = federation.coordinators[alias];
newCoordinator.update(() => {
setCoordinatorUpdatedAt(new Date().toISOString());
});
garage.syncCoordinator(newCoordinator);
setSortedCoordinators(federationLottery(federation));
setFederationUpdatedAt(new Date().toISOString());
}
};
useEffect(() => {
if (currentOrderId.id && currentOrderId.shortAlias) {
setCurrentOrder(null);
@ -200,6 +226,7 @@ export const FederationContextProvider = ({
setDelay,
coordinatorUpdatedAt,
federationUpdatedAt,
addNewCoordinator,
}}
>
{children}

View File

@ -72,6 +72,39 @@ export interface Info {
export type Origin = 'onion' | 'i2p' | 'clearnet';
export const coordinatorDefaultValues = {
longAlias: '',
shortAlias: '',
description: '',
motto: '',
color: '#000',
size_limit: 21 * 100000000,
established: new Date(),
policies: {},
contact: {
email: '',
telegram: '',
simplex: '',
matrix: '',
website: '',
nostr: '',
pgp: '',
fingerprint: '',
},
badges: {
isFounder: false,
donatesToDevFund: 0,
hasGoodOpSec: false,
robotsLove: false,
hasLargeLimits: false,
},
mainnet: undefined,
testnet: undefined,
mainnetNodesPubkeys: '',
testnetNodesPubkeys: '',
federated: true,
};
export interface Origins {
clearnet: Origin | undefined;
onion: Origin | undefined;
@ -102,6 +135,7 @@ export class Coordinator {
this.longAlias = value.longAlias;
this.shortAlias = value.shortAlias;
this.description = value.description;
this.federated = value.federated;
this.motto = value.motto;
this.color = value.color;
this.size_limit = value.badges.isFounder ? 21 * 100000000 : calculateSizeLimit(established);
@ -122,6 +156,7 @@ export class Coordinator {
// These properties are loaded from federation.json
public longAlias: string;
public shortAlias: string;
public federated: boolean;
public enabled?: boolean = true;
public description: string;
public motto: string;

View File

@ -9,39 +9,34 @@ import {
} from '.';
import defaultFederation from '../../static/federation.json';
import { getHost } from '../utils';
import { coordinatorDefaultValues } from './Coordinator.model';
import { updateExchangeInfo } from './Exchange.model';
type FederationHooks = 'onCoordinatorUpdate' | 'onFederationUpdate';
export class Federation {
constructor(origin: Origin, settings: Settings, hostUrl: string) {
this.coordinators = Object.entries(defaultFederation).reduce(
(acc: Record<string, Coordinator>, [key, value]: [string, any]) => {
if (getHost() !== '127.0.0.1:8000' && key === 'local') {
// Do not add `Local Dev` unless it is running on localhost
return acc;
} else {
acc[key] = new Coordinator(value, origin, settings, hostUrl);
return acc;
}
},
{},
);
this.exchange = {
...defaultExchange,
totalCoordinators: Object.keys(this.coordinators).length,
};
this.coordinators = {};
this.exchange = { ...defaultExchange };
this.book = [];
this.hooks = {
onCoordinatorUpdate: [],
onFederationUpdate: [],
};
this.loading = true;
Object.keys(defaultFederation).forEach((key) => {
if (key !== 'local' || getHost() === '127.0.0.1:8000') {
// Do not add `Local Dev` unless it is running on localhost
this.addCoordinator(origin, settings, hostUrl, defaultFederation[key]);
}
});
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
this.loading = true;
const host = getHost();
const url = `${window.location.protocol}//${host}`;
const tesnetHost = Object.values(this.coordinators).find((coor) => {
return Object.values(coor.testnet).includes(url);
});
@ -55,6 +50,22 @@ export class Federation {
public hooks: Record<FederationHooks, Array<() => void>>;
addCoordinator = (
origin: Origin,
settings: Settings,
hostUrl: string,
attributes: Record<any, any>,
) => {
const value = {
...coordinatorDefaultValues,
...attributes,
};
this.coordinators[value.shortAlias] = new Coordinator(value, origin, settings, hostUrl);
this.exchange.totalCoordinators = Object.keys(this.coordinators).length;
this.updateEnabledCoordinators();
this.triggerHook('onFederationUpdate');
};
// Hooks
registerHook = (hookName: FederationHooks, fn: () => void): void => {
this.hooks[hookName].push(fn);

View File

@ -1,4 +1,4 @@
import { type Order } from '.';
import { Coordinator, type Order } from '.';
import { systemClient } from '../services/System';
import { saveAsJson } from '../utils';
import Slot from './Slot.model';
@ -62,12 +62,10 @@ class Garage {
this.slots[rawSlot.token] = new Slot(rawSlot.token, Object.keys(rawSlot.robots), {}, () =>
this.triggerHook('onRobotUpdate'),
);
Object.keys(rawSlot.robots).forEach((shortAlias) => {
const rawRobot = rawSlot.robots[shortAlias];
this.updateRobot(rawSlot.token, shortAlias, rawRobot);
});
this.currentSlot = rawSlot?.token;
}
});
@ -160,6 +158,13 @@ class Garage {
this.triggerHook('onOrderUpdate');
}
};
// Coordinators
syncCoordinator: (coordinator: Coordinator) => void = (coordinator) => {
Object.values(this.slots).forEach((slot) => {
slot.syncCoordinator(coordinator, this);
});
};
}
export default Garage;

View File

@ -1,5 +1,5 @@
import { sha256 } from 'js-sha256';
import { Robot, type Order } from '.';
import { Coordinator, Garage, Robot, type Order } from '.';
import { roboidentitiesClient } from '../services/Roboidentities/Web';
class Slot {
@ -73,6 +73,18 @@ class Slot {
return this.robots[shortAlias];
};
syncCoordinator: (coordinator: Coordinator, garage: Garage) => void = (coordinator, garage) => {
const defaultRobot = this.getRobot();
if (defaultRobot?.token) {
this.robots[coordinator.shortAlias] = new Robot({
token: defaultRobot.token,
pubKey: defaultRobot.pubKey,
encPrivKey: defaultRobot.encPrivKey,
});
coordinator.fetchRobot(garage, defaultRobot.token);
}
};
}
export default Slot;