diff --git a/frontend/src/basic/OrderPage/index.tsx b/frontend/src/basic/OrderPage/index.tsx
index b47a46e5..20f8adf4 100644
--- a/frontend/src/basic/OrderPage/index.tsx
+++ b/frontend/src/basic/OrderPage/index.tsx
@@ -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 && }
{badOrder !== undefined ? (
diff --git a/frontend/src/basic/SettingsPage/index.tsx b/frontend/src/basic/SettingsPage/index.tsx
index b618a2ed..3f2c339c 100644
--- a/frontend/src/basic/SettingsPage/index.tsx
+++ b/frontend/src/basic/SettingsPage/index.tsx
@@ -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(AppContext);
+ const { federation, addNewCoordinator } = useContext(FederationContext);
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
+ const [newAlias, setNewAlias] = useState('');
+ const [newUrl, setNewUrl] = useState('');
+ const [error, setError] = useState();
+ // 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 (
{
+
+
+ {error}
+
+
+
+
+ setNewAlias(e.target.value)}
+ />
+ setNewUrl(e.target.value)}
+ />
+
+
+
);
diff --git a/frontend/src/components/BookTable/BookControl.tsx b/frontend/src/components/BookTable/BookControl.tsx
index ad4518d3..2cafa684 100644
--- a/frontend/src/components/BookTable/BookControl.tsx
+++ b/frontend/src/components/BookTable/BookControl.tsx
@@ -359,7 +359,8 @@ const BookControl = ({
>
{
+ const coordinator = federation.coordinators[params.row.coordinatorShortAlias];
return (
)}
- {pgp !== undefined && (
+ {pgp && fingerprint && (
diff --git a/frontend/src/components/FederationTable/index.tsx b/frontend/src/components/FederationTable/index.tsx
index 34b15228..9e64483b 100644
--- a/frontend/src/components/FederationTable/index.tsx
+++ b/frontend/src/components/FederationTable/index.tsx
@@ -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(FederationContext);
- const { setOpen } = useContext(AppContext);
+ const { setOpen, settings } = useContext(AppContext);
const theme = useTheme();
const [pageSize, setPageSize] = useState(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 (
{
- 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 (
= ({
setCoordinator,
}) => {
const { setOpen } = useContext(AppContext);
- const { federation, sortedCoordinators, coordinatorUpdatedAt } =
- useContext(FederationContext);
+ const { federation, sortedCoordinators } = useContext(FederationContext);
const theme = useTheme();
const { t } = useTranslation();
@@ -41,10 +40,7 @@ const SelectCoordinator: React.FC = ({
setCoordinator(e.target.value);
};
- const coordinator = useMemo(
- () => federation.getCoordinator(coordinatorAlias),
- [coordinatorUpdatedAt],
- );
+ const coordinator = federation.getCoordinator(coordinatorAlias);
return (
@@ -83,7 +79,8 @@ const SelectCoordinator: React.FC = ({
>
-
+
diff --git a/frontend/src/components/RobotAvatar/index.tsx b/frontend/src/components/RobotAvatar/index.tsx
index 33fe8f5b..9f74db0a 100644
--- a/frontend/src/components/RobotAvatar/index.tsx
+++ b/frontend/src/components/RobotAvatar/index.tsx
@@ -70,8 +70,8 @@ const RobotAvatar: React.FC = ({
}, [hashId]);
useEffect(() => {
- if (shortAlias !== undefined) {
- if (window.NativeRobosats === undefined) {
+ if (shortAlias && shortAlias !== '') {
+ if (!window.NativeRobosats) {
setAvatarSrc(
`${hostUrl}/static/federation/avatars/${shortAlias}${small ? '.small' : ''}.webp`,
);
diff --git a/frontend/src/components/SettingsForm/index.tsx b/frontend/src/components/SettingsForm/index.tsx
index 22eaef92..90f96981 100644
--- a/frontend/src/components/SettingsForm/index.tsx
+++ b/frontend/src/components/SettingsForm/index.tsx
@@ -26,6 +26,7 @@ import {
AccountBalance,
AttachMoney,
QrCode,
+ ControlPoint,
} from '@mui/icons-material';
import { systemClient } from '../../services/System';
import { TorIcon } from '../Icons';
diff --git a/frontend/src/contexts/FederationContext.tsx b/frontend/src/contexts/FederationContext.tsx
index b7998111..4946ac7c 100644
--- a/frontend/src/contexts/FederationContext.tsx
+++ b/frontend/src/contexts/FederationContext.tsx
@@ -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(initialFederationContext);
@@ -81,7 +83,7 @@ export const FederationContextProvider = ({
useContext(AppContext);
const { setMaker, garage, setBadOrder } = useContext(GarageContext);
const [federation] = useState(new Federation(origin, settings, hostUrl));
- const sortedCoordinators = useMemo(() => federationLottery(federation), []);
+ const [sortedCoordinators, setSortedCoordinators] = useState(federationLottery(federation));
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState(
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 = {
+ 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}
diff --git a/frontend/src/models/Coordinator.model.ts b/frontend/src/models/Coordinator.model.ts
index c07e771b..6b2f7339 100644
--- a/frontend/src/models/Coordinator.model.ts
+++ b/frontend/src/models/Coordinator.model.ts
@@ -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;
diff --git a/frontend/src/models/Federation.model.ts b/frontend/src/models/Federation.model.ts
index 2d31e6ce..46dfd5ff 100644
--- a/frontend/src/models/Federation.model.ts
+++ b/frontend/src/models/Federation.model.ts
@@ -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, [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 void>>;
+ addCoordinator = (
+ origin: Origin,
+ settings: Settings,
+ hostUrl: string,
+ attributes: Record,
+ ) => {
+ 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);
diff --git a/frontend/src/models/Garage.model.ts b/frontend/src/models/Garage.model.ts
index 47204b91..acfcf40b 100644
--- a/frontend/src/models/Garage.model.ts
+++ b/frontend/src/models/Garage.model.ts
@@ -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;
diff --git a/frontend/src/models/Slot.model.ts b/frontend/src/models/Slot.model.ts
index 76909bf0..cf0a1b40 100644
--- a/frontend/src/models/Slot.model.ts
+++ b/frontend/src/models/Slot.model.ts
@@ -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;