Merge pull request #1407 from RoboSats/refactor-order-model

Refactor Order/Slot models
This commit is contained in:
KoalaSat 2024-09-10 16:18:12 +00:00 committed by GitHub
commit a390254e2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 794 additions and 962 deletions

View File

@ -21,8 +21,8 @@ const App = (): JSX.Element => {
<Suspense fallback='loading'>
<I18nextProvider i18n={i18n}>
<AppContextProvider>
<GarageContextProvider>
<FederationContextProvider>
<FederationContextProvider>
<GarageContextProvider>
<CssBaseline />
{window.NativeRobosats === undefined && window.RobosatsClient === undefined ? (
<HostAlert />
@ -30,8 +30,8 @@ const App = (): JSX.Element => {
<TorConnectionBadge />
)}
<Main />
</FederationContextProvider>
</GarageContextProvider>
</GarageContextProvider>
</FederationContextProvider>
</AppContextProvider>
</I18nextProvider>
</Suspense>

View File

@ -12,12 +12,10 @@ import BookTable from '../../components/BookTable';
import { BarChart, FormatListBulleted, Map } from '@mui/icons-material';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import MapChart from '../../components/Charts/MapChart';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
const BookPage = (): JSX.Element => {
const { windowSize } = useContext<UseAppStoreType>(AppContext);
const { setDelay, setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const navigate = useNavigate();
@ -32,8 +30,6 @@ const BookPage = (): JSX.Element => {
const onOrderClicked = function (id: number, shortAlias: string): void {
if (garage.getSlot()?.hashId) {
setDelay(10000);
setCurrentOrderId({ id, shortAlias });
navigate(`/order/${shortAlias}/${id}`);
} else {
setOpenNoRobot(true);
@ -102,9 +98,6 @@ const BookPage = (): JSX.Element => {
>
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
<MakerForm
onOrderCreated={(id) => {
navigate(`/order/${id}`);
}}
onClickGenerateRobot={() => {
navigate('/robot');
}}

View File

@ -14,8 +14,7 @@ import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageCon
const MakerPage = (): JSX.Element => {
const { fav, windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { federation, setDelay, setCurrentOrderId } =
useContext<UseFederationStoreType>(FederationContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage, maker } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const navigate = useNavigate();
@ -54,8 +53,6 @@ const MakerPage = (): JSX.Element => {
const onOrderClicked = function (id: number, shortAlias: string): void {
if (garage.getSlot()?.hashId) {
setDelay(10000);
setCurrentOrderId({ id, shortAlias });
navigate(`/order/${shortAlias}/${id}`);
} else {
setOpenNoRobot(true);
@ -105,10 +102,6 @@ const MakerPage = (): JSX.Element => {
}}
>
<MakerForm
onOrderCreated={(shortAlias, id) => {
setCurrentOrderId({ id, shortAlias });
navigate(`/order/${shortAlias}/${id}`);
}}
disableRequest={matches.length > 0 && !showMatches}
collapseAll={showMatches}
onSubmit={() => {

View File

@ -17,15 +17,13 @@ import {
import RobotAvatar from '../../components/RobotAvatar';
import { AppContext, type UseAppStoreType, closeAll } from '../../contexts/AppContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
const NavBar = (): JSX.Element => {
const theme = useTheme();
const { t } = useTranslation();
const { page, setPage, settings, setSlideDirection, open, setOpen, windowSize, navbarHeight } =
useContext<UseAppStoreType>(AppContext);
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const navigate = useNavigate();
const location = useLocation();
@ -50,7 +48,7 @@ const NavBar = (): JSX.Element => {
useEffect(() => {
// re-render on orde rand robot updated at for latest orderId in tab
}, [robotUpdatedAt]);
}, [slotUpdatedAt]);
useEffect(() => {
// change tab (page) into the current route
@ -77,14 +75,10 @@ const NavBar = (): JSX.Element => {
const slot = garage.getSlot();
handleSlideDirection(page, newPage);
setPage(newPage);
const shortAlias = String(slot?.activeShortAlias);
const activeOrderId = slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId;
const lastOrderId = slot?.getRobot(slot?.lastShortAlias ?? '')?.lastOrderId;
const param =
newPage === 'order' ? `${shortAlias}/${String(activeOrderId ?? lastOrderId)}` : '';
if (newPage === 'order') {
setCurrentOrderId({ id: activeOrderId ?? lastOrderId, shortAlias });
}
const shortAlias = slot?.activeOrder?.shortAlias;
const orderId = slot?.activeOrder?.id;
const param = newPage === 'order' ? `${String(shortAlias)}/${String(orderId)}` : '';
setTimeout(() => {
navigate(`/${newPage}/${param}`);
}, theme.transitions.duration.leavingScreen * 3);
@ -162,7 +156,7 @@ const NavBar = (): JSX.Element => {
sx={tabSx}
label={smallBar ? undefined : t('Order')}
value='order'
disabled={!slot?.getRobot()?.activeOrderId}
disabled={!slot?.activeOrder}
icon={<Assignment />}
iconPosition='start'
/>

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Tab, Tabs, Paper, CircularProgress, Grid, Typography, Box } from '@mui/material';
import { useNavigate, useParams } from 'react-router-dom';
@ -6,60 +6,59 @@ import { useNavigate, useParams } from 'react-router-dom';
import TradeBox from '../../components/TradeBox';
import OrderDetails from '../../components/OrderDetails';
import { AppContext, closeAll, type UseAppStoreType } from '../../contexts/AppContext';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { WarningDialog } from '../../components/Dialogs';
import { Order, type Slot } from '../../models';
import { type UseGarageStoreType, GarageContext } from '../../contexts/GarageContext';
const OrderPage = (): JSX.Element => {
const {
windowSize,
open,
setOpen,
acknowledgedWarning,
setAcknowledgedWarning,
settings,
navbarHeight,
hostUrl,
origin,
} = useContext<UseAppStoreType>(AppContext);
const { federation, currentOrder, currentOrderId, setCurrentOrderId } =
useContext<UseFederationStoreType>(FederationContext);
const { badOrder } = useContext<UseGarageStoreType>(GarageContext);
const { windowSize, setOpen, acknowledgedWarning, setAcknowledgedWarning, navbarHeight } =
useContext<UseAppStoreType>(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const navigate = useNavigate();
const params = useParams();
const paramsRef = useRef(params);
const doublePageWidth: number = 50;
const maxHeight: number = (windowSize?.height - navbarHeight) * 0.85 - 3;
const [tab, setTab] = useState<'order' | 'contract'>('contract');
const [baseUrl, setBaseUrl] = useState<string>(hostUrl);
const [currentOrder, setCurrentOrder] = useState<Order | null>(null);
useEffect(() => {
paramsRef.current = params;
const shortAlias = params.shortAlias;
const coordinator = federation.getCoordinator(shortAlias ?? '');
if (coordinator) {
const endpoint = coordinator?.getEndpoint(
settings.network,
origin,
settings.selfhostedClient,
hostUrl,
);
if (endpoint) setBaseUrl(`${endpoint?.url}${endpoint?.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);
const slot = garage.getSlot();
if (slot?.token) {
let order = new Order({ id: orderId, shortAlias });
if (slot.activeOrder?.id === orderId && slot.activeOrder?.shortAlias === shortAlias) {
order = slot.activeOrder;
} else if (slot.lastOrder?.id === orderId && slot.lastOrder?.shortAlias === shortAlias) {
order = slot.lastOrder;
}
void order.fecth(federation, slot).then((updatedOrder) => {
updateSlotFromOrder(updatedOrder, slot);
});
}
}, [params, currentOrderId]);
return () => {
setCurrentOrder(null);
};
}, [params.orderId]);
const updateSlotFromOrder = (updatedOrder: Order, slot: Slot): void => {
if (
Number(paramsRef.current.orderId) === updatedOrder.id &&
paramsRef.current.shortAlias === updatedOrder.shortAlias
) {
setCurrentOrder(updatedOrder);
slot.updateSlotFromOrder(updatedOrder);
}
};
const onClickCoordinator = function (): void {
if (currentOrder?.shortAlias != null) {
@ -87,7 +86,7 @@ const OrderPage = (): JSX.Element => {
);
const tradeBoxSpace = currentOrder ? (
<TradeBox baseUrl={baseUrl} onStartAgain={startAgain} />
<TradeBox onStartAgain={startAgain} currentOrder={currentOrder} />
) : (
<></>
);
@ -95,20 +94,19 @@ const OrderPage = (): JSX.Element => {
return (
<Box>
<WarningDialog
open={open.warning}
open={!acknowledgedWarning && currentOrder?.status === 0}
onClose={() => {
setOpen(closeAll);
setAcknowledgedWarning(true);
}}
longAlias={federation.getCoordinator(params.shortAlias ?? '')?.longAlias}
/>
{currentOrder === null && badOrder === undefined && <CircularProgress />}
{badOrder !== undefined ? (
{!currentOrder?.maker_hash_id && <CircularProgress />}
{currentOrder?.bad_request && currentOrder.status !== 5 ? (
<Typography align='center' variant='subtitle2' color='secondary'>
{t(badOrder)}
{t(currentOrder.bad_request)}
</Typography>
) : null}
{currentOrder !== null && badOrder === undefined ? (
{currentOrder?.maker_hash_id && (!currentOrder.bad_request || currentOrder.status === 5) ? (
currentOrder.is_participant ? (
windowSize.width > doublePageWidth ? (
// DOUBLE PAPER VIEW

View File

@ -22,7 +22,6 @@ import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { genBase62Token } from '../../utils';
import { LoadingButton } from '@mui/lab';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
interface RobotProfileProps {
robot: Robot;
@ -45,8 +44,7 @@ const RobotProfile = ({
width,
}: RobotProfileProps): JSX.Element => {
const { windowSize } = useContext<UseAppStoreType>(AppContext);
const { garage, robotUpdatedAt, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const theme = useTheme();
@ -59,7 +57,7 @@ const RobotProfile = ({
if (slot?.hashId) {
setLoading(false);
}
}, [orderUpdatedAt, robotUpdatedAt, loading]);
}, [slotUpdatedAt, loading]);
const handleAddRobot = (): void => {
getGenerateRobot(genBase62Token(36));
@ -147,7 +145,7 @@ const RobotProfile = ({
tooltip={t('This is your trading avatar')}
tooltipPosition='top'
/>
{robot?.found && Boolean(slot?.lastShortAlias) ? (
{robot?.found && Boolean(slot?.lastOrder?.id) ? (
<Typography align='center' variant='h6'>
{t('Welcome back!')}
</Typography>
@ -156,38 +154,38 @@ const RobotProfile = ({
)}
</Grid>
{loadingCoordinators > 0 && !robot?.activeOrderId ? (
{loadingCoordinators > 0 && !slot?.activeOrder?.id ? (
<Grid>
<b>{t('Looking for orders!')}</b>
<LinearProgress />
</Grid>
) : null}
{Boolean(robot?.activeOrderId) && Boolean(slot?.hashId) ? (
{slot?.activeOrder ? (
<Grid item>
<Button
onClick={() => {
setCurrentOrderId({ id: robot?.activeOrderId, shortAlias: slot?.activeShortAlias });
navigate(
`/order/${String(slot?.activeShortAlias)}/${String(robot?.activeOrderId)}`,
`/order/${String(slot?.activeOrder?.shortAlias)}/${String(slot?.activeOrder?.id)}`,
);
}}
>
{t('Active order #{{orderID}}', { orderID: robot?.activeOrderId })}
{t('Active order #{{orderID}}', { orderID: slot?.activeOrder?.id })}
</Button>
</Grid>
) : null}
{Boolean(robot?.lastOrderId) && Boolean(slot?.hashId) ? (
{!slot?.activeOrder?.id && Boolean(slot?.lastOrder?.id) ? (
<Grid item container direction='column' alignItems='center'>
<Grid item>
<Button
onClick={() => {
setCurrentOrderId({ id: robot?.lastOrderId, shortAlias: slot?.activeShortAlias });
navigate(`/order/${String(slot?.lastShortAlias)}/${String(robot?.lastOrderId)}`);
navigate(
`/order/${String(slot?.lastOrder?.shortAlias)}/${String(slot?.lastOrder?.id)}`,
);
}}
>
{t('Last order #{{orderID}}', { orderID: robot?.lastOrderId })}
{t('Last order #{{orderID}}', { orderID: slot?.lastOrder?.id })}
</Button>
</Grid>
<Grid item>
@ -210,10 +208,7 @@ const RobotProfile = ({
</Grid>
) : null}
{!robot?.activeOrderId &&
slot?.hashId &&
!robot?.lastOrderId &&
loadingCoordinators === 0 ? (
{!slot?.activeOrder && !slot?.lastOrder && loadingCoordinators === 0 ? (
<Grid item>{t('No existing orders found')}</Grid>
) : null}

View File

@ -45,7 +45,6 @@ const RobotPage = (): JSX.Element => {
if (token !== undefined && token !== null && page === 'robot') {
setInputToken(token);
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
getGenerateRobot(token);
setView('profile');
}
}
@ -70,7 +69,7 @@ const RobotPage = (): JSX.Element => {
pubKey: key.publicKeyArmored,
encPrivKey: key.encryptedPrivateKeyArmored,
});
void federation.fetchRobot(garage, token);
void garage.fetchRobot(federation, token);
garage.setCurrentSlot(token);
})
.catch((error) => {

View File

@ -13,11 +13,10 @@ const Routes: React.FC = () => {
useEffect(() => {
window.addEventListener('navigateToPage', (event) => {
console.log('navigateToPage', JSON.stringify(event));
const orderId: string = event?.detail?.order_id;
const coordinator: string = event?.detail?.coordinator;
if (orderId && coordinator) {
const slot = garage.getSlotByOrder(coordinator, orderId);
const slot = garage.getSlotByOrder(coordinator, parseInt(orderId, 10));
if (slot?.token) {
garage.setCurrentSlot(slot?.token);
navigate(`/order/${coordinator}/${orderId}`);

View File

@ -5,10 +5,12 @@ import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import FederationTable from '../../components/FederationTable';
import { t } from 'i18next';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
const SettingsPage = (): JSX.Element => {
const { windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { federation, addNewCoordinator } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
const [newAlias, setNewAlias] = useState<string>('');
const [newUrl, setNewUrl] = useState<string>('');
@ -26,6 +28,7 @@ const SettingsPage = (): JSX.Element => {
fullNewUrl = `http://${newUrl}`;
}
addNewCoordinator(newAlias, fullNewUrl);
garage.syncCoordinator(federation, newAlias);
setNewAlias('');
setNewUrl('');
} else {

View File

@ -88,8 +88,7 @@ const BookTable = ({
onOrderClicked = () => null,
}: BookTableProps): JSX.Element => {
const { fav, setOpen } = useContext<UseAppStoreType>(AppContext);
const { federation, coordinatorUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { federation, federationUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation();
const theme = useTheme();
@ -123,7 +122,7 @@ const BookTable = ({
pageSize: federation.loading && orders.length === 0 ? 0 : defaultPageSize,
page: paginationModel.page,
});
}, [coordinatorUpdatedAt, orders, defaultPageSize]);
}, [federationUpdatedAt, orders, defaultPageSize]);
const localeText = useMemo(() => {
return {

View File

@ -47,8 +47,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
onOrderClicked = () => null,
}) => {
const { fav } = useContext<UseAppStoreType>(AppContext);
const { federation, coordinatorUpdatedAt, federationUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { federation, federationUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation();
const theme = useTheme();
const [enrichedOrders, setEnrichedOrders] = useState<PublicOrder[]>([]);
@ -81,7 +80,7 @@ const DepthChart: React.FC<DepthChartProps> = ({
});
setEnrichedOrders(enriched);
}
}, [coordinatorUpdatedAt, currencyCode]);
}, [federationUpdatedAt, currencyCode]);
useEffect(() => {
if (enrichedOrders.length > 0) {

View File

@ -34,14 +34,14 @@ interface Props {
const ExchangeDialog = ({ open = false, onClose }: Props): JSX.Element => {
const { t } = useTranslation();
const { federation, coordinatorUpdatedAt, federationUpdatedAt } = useContext(FederationContext);
const { federation, federationUpdatedAt } = useContext(FederationContext);
const [loadingProgress, setLoadingProgress] = useState<number>(0);
useEffect(() => {
const loadedCoordinators =
federation.exchange.enabledCoordinators - federation.exchange.loadingCoordinators;
setLoadingProgress((loadedCoordinators / federation.exchange.enabledCoordinators) * 100);
}, [open, coordinatorUpdatedAt, federationUpdatedAt]);
}, [open, federationUpdatedAt]);
return (
<Dialog open={open} onClose={onClose}>

View File

@ -27,7 +27,7 @@ interface Props {
const ProfileDialog = ({ open = false, onClose }: Props): JSX.Element => {
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const slot = garage.getSlot();
@ -42,7 +42,7 @@ const ProfileDialog = ({ open = false, onClose }: Props): JSX.Element => {
setLoadingCoordinators(
Object.values(slot?.robots ?? {}).filter((robot) => robot.loading).length,
);
}, [robotUpdatedAt]);
}, [slotUpdatedAt]);
return (
<Dialog

View File

@ -21,7 +21,7 @@ const FederationTable = ({
fillContainer = false,
}: FederationTableProps): JSX.Element => {
const { t } = useTranslation();
const { federation, sortedCoordinators, coordinatorUpdatedAt, federationUpdatedAt } =
const { federation, sortedCoordinators, federationUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { setOpen, settings } = useContext<UseAppStoreType>(AppContext);
const theme = useTheme();
@ -43,7 +43,7 @@ const FederationTable = ({
if (useDefaultPageSize) {
setPageSize(defaultPageSize);
}
}, [coordinatorUpdatedAt, federationUpdatedAt]);
}, [federationUpdatedAt]);
const localeText = {
MuiTablePagination: { labelRowsPerPage: t('Coordinators per page:') },
@ -111,7 +111,7 @@ const FederationTable = ({
},
};
},
[coordinatorUpdatedAt],
[federationUpdatedAt],
);
const upObj = useCallback(
@ -140,7 +140,7 @@ const FederationTable = ({
},
};
},
[coordinatorUpdatedAt],
[federationUpdatedAt],
);
const columnSpecs = {

View File

@ -24,12 +24,11 @@ import {
IconButton,
} from '@mui/material';
import { type LimitList, defaultMaker } from '../../models';
import { type LimitList, defaultMaker, type Order } from '../../models';
import { LocalizationProvider, MobileTimePicker } from '@mui/x-date-pickers';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { ConfirmationDialog, F2fMapDialog } from '../Dialogs';
import { apiClient } from '../../services/api';
import { FlagWithProps } from '../Icons';
import AutocompletePayments from './AutocompletePayments';
@ -44,6 +43,7 @@ import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import SelectCoordinator from './SelectCoordinator';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { useNavigate } from 'react-router-dom';
interface MakerFormProps {
disableRequest?: boolean;
@ -52,7 +52,6 @@ interface MakerFormProps {
onSubmit?: () => void;
onReset?: () => void;
submitButtonLabel?: string;
onOrderCreated?: (shortAlias: string, id: number) => void;
onClickGenerateRobot?: () => void;
}
@ -63,16 +62,15 @@ const MakerForm = ({
onSubmit = () => {},
onReset = () => {},
submitButtonLabel = 'Create Order',
onOrderCreated = () => null,
onClickGenerateRobot = () => null,
}: MakerFormProps): JSX.Element => {
const { fav, setFav, settings, hostUrl, origin } = useContext<UseAppStoreType>(AppContext);
const { federation, coordinatorUpdatedAt, federationUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const { fav, setFav } = useContext<UseAppStoreType>(AppContext);
const { federation, federationUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
const { maker, setMaker, garage } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
const [badRequest, setBadRequest] = useState<string | null>(null);
const [amountLimits, setAmountLimits] = useState<number[]>([1, 1000]);
@ -92,11 +90,11 @@ const MakerForm = ({
useEffect(() => {
setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]);
}, [coordinatorUpdatedAt]);
}, [federationUpdatedAt]);
useEffect(() => {
updateCoordinatorInfo();
}, [maker.coordinator, coordinatorUpdatedAt]);
}, [maker.coordinator, federationUpdatedAt]);
const updateCoordinatorInfo = (): void => {
if (maker.coordinator != null) {
@ -297,21 +295,14 @@ const MakerForm = ({
const handleCreateOrder = function (): void {
const slot = garage.getSlot();
if (slot?.activeShortAlias) {
if (slot?.activeOrder?.id) {
setBadRequest(t('You are already maker of an active order'));
return;
}
const { url, basePath } =
federation
.getCoordinator(maker.coordinator)
?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl) ?? {};
const auth = slot?.getRobot()?.getAuthHeaders();
if (!disableRequest && maker.coordinator != null && auth !== null) {
if (!disableRequest && maker.coordinator && slot) {
setSubmittingRequest(true);
const body = {
const orderAttributes = {
type: fav.type === 0 ? 1 : 0,
currency: fav.currency === 0 ? 1 : fav.currency,
amount: makerHasAmountRange ? null : maker.amount,
@ -328,15 +319,16 @@ const MakerForm = ({
bond_size: maker.bondSize,
latitude: maker.latitude,
longitude: maker.longitude,
shortAlias: maker.coordinator,
};
apiClient
.post(url, `${basePath}/api/make/`, body, auth)
.then((data: any) => {
setBadRequest(data.bad_request);
if (data.id !== undefined) {
onOrderCreated(maker.coordinator, data.id);
garage.updateOrder(data);
void slot
.makeOrder(federation, orderAttributes)
.then((order: Order) => {
if (order.id) {
navigate(`/order/${order.shortAlias}/${order.id}`);
} else if (order?.bad_request) {
setBadRequest(order?.bad_request);
}
setSubmittingRequest(false);
})
@ -513,7 +505,7 @@ const MakerForm = ({
federation.getCoordinator(maker.coordinator)?.info === undefined ||
federation.getCoordinator(maker.coordinator)?.limits === undefined
);
}, [maker, amountLimits, coordinatorUpdatedAt, fav.type, makerHasAmountRange]);
}, [maker, amountLimits, federationUpdatedAt, fav.type, makerHasAmountRange]);
const clearMaker = function (): void {
setFav({ ...fav, type: null });

View File

@ -69,7 +69,7 @@ const Notifications = ({
}: NotificationsProps): JSX.Element => {
const { t } = useTranslation();
const navigate = useNavigate();
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const [message, setMessage] = useState<NotificationMessage>(emptyNotificationMessage);
const [inFocus, setInFocus] = useState<boolean>(true);
@ -85,7 +85,7 @@ const Notifications = ({
const basePageTitle = t('RoboSats - Simple and Private Bitcoin Exchange');
const moveToOrderPage = function (): void {
navigate(`/order/${String(garage.getSlot()?.order?.id)}`);
navigate(`/order/${String(garage.getSlot()?.activeOrder?.id)}`);
setShow(false);
};
@ -106,7 +106,9 @@ const Notifications = ({
const Messages: MessagesProps = {
bondLocked: {
title: t(`${garage.getSlot()?.order?.is_maker === true ? 'Maker' : 'Taker'} bond locked`),
title: t(
`${garage.getSlot()?.activeOrder?.is_maker === true ? 'Maker' : 'Taker'} bond locked`,
),
severity: 'info',
onClick: moveToOrderPage,
sound: audio.ding,
@ -228,7 +230,7 @@ const Notifications = ({
};
const handleStatusChange = function (oldStatus: number | undefined, status: number): void {
const order = garage.getSlot()?.order;
const order = garage.getSlot()?.activeOrder;
if (order === undefined || order === null) return;
@ -293,7 +295,7 @@ const Notifications = ({
// Notify on order status change
useEffect(() => {
const order = garage.getSlot()?.order;
const order = garage.getSlot()?.activeOrder;
if (order !== undefined && order !== null) {
if (order.status !== oldOrderStatus) {
handleStatusChange(oldOrderStatus, order.status);
@ -305,7 +307,7 @@ const Notifications = ({
setOldChatIndex(order.chat_last_index);
}
}
}, [orderUpdatedAt]);
}, [slotUpdatedAt]);
// Notify on rewards change
useEffect(() => {

View File

@ -19,15 +19,14 @@ import {
import Countdown from 'react-countdown';
import currencies from '../../../static/assets/currencies.json';
import { apiClient } from '../../services/api';
import { type Order, type Info } from '../../models';
import { ConfirmationDialog } from '../Dialogs';
import { LoadingButton } from '@mui/lab';
import { computeSats } from '../../utils';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext';
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
import { useNavigate } from 'react-router-dom';
interface TakeButtonProps {
currentOrder: Order;
@ -48,9 +47,9 @@ const TakeButton = ({
}: TakeButtonProps): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
const { settings, origin, hostUrl } = useContext<UseAppStoreType>(AppContext);
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { federation, setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
const navigate = useNavigate();
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const [takeAmount, setTakeAmount] = useState<string>('');
const [badRequest, setBadRequest] = useState<string>('');
@ -79,7 +78,7 @@ const TakeButton = ({
useEffect(() => {
setSatoshis(satoshisNow() ?? '');
}, [orderUpdatedAt, takeAmount, info]);
}, [slotUpdatedAt, takeAmount, info]);
const currencyCode: string =
currentOrder?.currency === 1000 ? 'Sats' : currencies[`${Number(currentOrder?.currency)}`];
@ -165,7 +164,7 @@ const TakeButton = ({
} else {
return null;
}
}, [orderUpdatedAt, takeAmount]);
}, [slotUpdatedAt, takeAmount]);
const onTakeOrderClicked = function (): void {
if (currentOrder?.maker_status === 'Inactive') {
@ -184,7 +183,7 @@ const TakeButton = ({
takeAmount === '' ||
takeAmount == null
);
}, [takeAmount, orderUpdatedAt]);
}, [takeAmount, slotUpdatedAt]);
const takeOrderButton = function (): JSX.Element {
if (currentOrder?.has_range) {
@ -314,31 +313,20 @@ const TakeButton = ({
};
const takeOrder = function (): void {
const robot = garage.getSlot()?.getRobot() ?? null;
const slot = garage.getSlot();
if (currentOrder === null || robot === null) return;
if (currentOrder === null || slot === null) return;
setLoadingTake(true);
const { url, basePath } = federation
.getCoordinator(currentOrder.shortAlias)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
setCurrentOrderId({ id: null, shortAlias: null });
apiClient
.post(
url + basePath,
`/api/order/?order_id=${String(currentOrder?.id)}`,
{
action: 'take',
amount: currentOrder?.currency === 1000 ? takeAmount / 100000000 : takeAmount,
},
{ tokenSHA256: robot?.tokenSHA256 },
)
.then((data) => {
if (data?.bad_request !== undefined) {
setBadRequest(data.bad_request);
slot
.takeOrder(federation, currentOrder, takeAmount)
.then((order) => {
if (order?.bad_request !== undefined) {
setBadRequest(order.bad_request);
} else {
setCurrentOrderId({ id: currentOrder?.id, shortAlias: currentOrder?.shortAlias });
setBadRequest('');
navigate(`/order/${order.shortAlias}/${order.id}`);
}
})
.catch(() => {

View File

@ -174,8 +174,8 @@ const OrderDetails = ({
const isBuyer = (order.type === 0 && order.is_maker) || (order.type === 1 && !order.is_maker);
const tradeFee = order.is_maker
? coordinator.info?.maker_fee ?? 0
: coordinator.info?.taker_fee ?? 0;
? (coordinator.info?.maker_fee ?? 0)
: (coordinator.info?.taker_fee ?? 0);
const defaultRoutingBudget = 0.001;
const btc_now = order.satoshis_now / 100000000;
const rate = Number(order.max_amount ?? order.amount) / btc_now;

View File

@ -41,7 +41,7 @@ interface Props {
const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) => {
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const navigate = useNavigate();
const { t } = useTranslation();
@ -55,6 +55,8 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
const [weblnEnabled, setWeblnEnabled] = useState<boolean>(false);
const [openEnableTelegram, setOpenEnableTelegram] = useState<boolean>(false);
const robot = garage.getSlot()?.getRobot(coordinator.shortAlias);
const handleWebln = async (): Promise<void> => {
void getWebln()
.then(() => {
@ -72,7 +74,6 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
const handleWeblnInvoiceClicked = async (e: MouseEvent<HTMLButtonElement, MouseEvent>): void => {
e.preventDefault();
const robot = garage.getSlot()?.getRobot(coordinator.shortAlias);
if (robot != null && robot.earnedRewards > 0) {
const webln = await getWebln();
const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
@ -87,14 +88,11 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
setBadInvoice('');
setShowRewardsSpinner(true);
const slot = garage.getSlot();
const robot = slot?.getRobot(coordinator.shortAlias);
if (robot != null && slot?.token != null && robot.encPrivKey != null) {
void signCleartextMessage(rewardInvoice, robot.encPrivKey, slot?.token).then(
if (robot?.token && robot.encPrivKey != null) {
void signCleartextMessage(rewardInvoice, robot.encPrivKey, robot?.token).then(
(signedInvoice) => {
console.log('Signed message:', signedInvoice);
void coordinator.fetchReward(signedInvoice, garage, slot?.token).then((data) => {
void robot.fetchReward(federation, signedInvoice).then((data) => {
setBadInvoice(data.bad_invoice ?? '');
setShowRewardsSpinner(false);
setWithdrawn(data.successful_withdrawal);
@ -106,16 +104,10 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
e.preventDefault();
};
const setStealthInvoice = (wantsStealth: boolean): void => {
const slot = garage.getSlot();
if (slot?.token != null) {
void coordinator.fetchStealth(wantsStealth, garage, slot?.token);
}
const setStealthInvoice = (): void => {
if (robot) void robot.fetchStealth(federation, !robot?.stealthInvoices);
};
const slot = garage.getSlot();
const robot = slot?.getRobot(coordinator.shortAlias);
return (
<Accordion disabled={disabled}>
<AccordionSummary expandIcon={<ExpandMore />}>
@ -123,28 +115,21 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
{(robot?.earnedRewards ?? 0) > 0 && (
<Typography color='success'>&nbsp;{t('Claim Sats!')} </Typography>
)}
{slot?.activeShortAlias === coordinator.shortAlias && (
{robot?.activeOrderId ? (
<Typography color='success'>
&nbsp;<b>{t('Active order!')}</b>
</Typography>
)}
{slot?.lastShortAlias === coordinator.shortAlias && (
<Typography color='warning'>&nbsp;{t('finished order')}</Typography>
) : (
robot?.lastOrderId && <Typography color='warning'>&nbsp;{t('finished order')}</Typography>
)}
</AccordionSummary>
<AccordionDetails>
<List dense disablePadding={true}>
{slot?.activeShortAlias === coordinator.shortAlias ? (
{robot?.activeOrderId ? (
<ListItemButton
onClick={() => {
setCurrentOrderId({
id: slot?.activeShortAlias,
shortAlias: slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId,
});
navigate(
`/order/${String(slot?.activeShortAlias)}/${String(
slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId,
)}`,
`/order/${String(coordinator.shortAlias)}/${String(robot?.activeOrderId)}`,
);
onClose();
}}
@ -156,23 +141,15 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
</ListItemIcon>
<ListItemText
primary={t('One active order #{{orderID}}', {
orderID: slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId,
orderID: String(robot?.activeOrderId),
})}
secondary={t('Your current order')}
/>
</ListItemButton>
) : (robot?.lastOrderId ?? 0) > 0 && slot?.lastShortAlias === coordinator.shortAlias ? (
) : robot?.lastOrderId ? (
<ListItemButton
onClick={() => {
setCurrentOrderId({
id: slot?.activeShortAlias,
shortAlias: slot?.getRobot(slot?.activeShortAlias ?? '')?.lastOrderId,
});
navigate(
`/order/${String(slot?.lastShortAlias)}/${String(
slot?.getRobot(slot?.lastShortAlias ?? '')?.lastOrderId,
)}`,
);
navigate(`/order/${String(coordinator.shortAlias)}/${String(robot?.lastOrderId)}`);
onClose();
}}
>
@ -181,7 +158,7 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
</ListItemIcon>
<ListItemText
primary={t('Your last order #{{orderID}}', {
orderID: slot?.getRobot(slot?.lastShortAlias ?? '')?.lastOrderId,
orderID: robot?.lastOrderId,
})}
secondary={t('Inactive order')}
/>
@ -253,7 +230,7 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
<Switch
checked={robot?.stealthInvoices}
onChange={() => {
setStealthInvoice(!robot?.stealthInvoices);
setStealthInvoice();
}}
/>
}

View File

@ -36,7 +36,6 @@ interface Props {
makerHashId: string;
messages: EncryptedChatMessage[];
setMessages: (messages: EncryptedChatMessage[]) => void;
baseUrl: string;
turtleMode: boolean;
setTurtleMode: (state: boolean) => void;
}
@ -50,14 +49,13 @@ const EncryptedSocketChat: React.FC<Props> = ({
takerHashId,
messages,
setMessages,
baseUrl,
turtleMode,
setTurtleMode,
}: Props): JSX.Element => {
const { t } = useTranslation();
const theme = useTheme();
const { origin, hostUrl, settings } = useContext<UseAppStoreType>(AppContext);
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`));
@ -78,7 +76,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
if (!connected && Boolean(garage.getSlot()?.hashId)) {
connectWebsocket();
}
}, [connected, robotUpdatedAt]);
}, [connected, slotUpdatedAt]);
// Make sure to not keep reconnecting once status is not Chat
useEffect(() => {

View File

@ -30,7 +30,6 @@ interface Props {
chatOffset: number;
messages: EncryptedChatMessage[];
setMessages: (messages: EncryptedChatMessage[]) => void;
baseUrl: string;
turtleMode: boolean;
setTurtleMode: (state: boolean) => void;
}
@ -49,7 +48,6 @@ const EncryptedTurtleChat: React.FC<Props> = ({
chatOffset,
messages,
setMessages,
baseUrl,
setTurtleMode,
turtleMode,
}: Props): JSX.Element => {
@ -91,7 +89,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}, [chatOffset]);
const loadMessages: () => void = () => {
const shortAlias = garage.getSlot()?.activeShortAlias;
const shortAlias = garage.getSlot()?.activeOrder?.shortAlias;
if (!shortAlias) return;
@ -100,7 +98,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient
.get(url + basePath, `/api/chat/?order_id=${order.id}&offset=${lastIndex}`, {
tokenSHA256: garage.getSlot()?.getRobot()?.tokenSHA256 ?? '',
tokenSHA256: garage.getSlot()?.tokenSHA256 ?? '',
})
.then((results: any) => {
if (results != null) {
@ -198,7 +196,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
// If input string contains '#' send unencrypted and unlogged message
else if (value.substring(0, 1) === '#') {
const { url, basePath } = federation
.getCoordinator(garage.getSlot()?.activeShortAlias ?? '')
.getCoordinator(garage.getSlot()?.activeOrder?.shortAlias)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient
.post(
@ -209,7 +207,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
order_id: order.id,
offset: lastIndex,
},
{ tokenSHA256: robot?.tokenSHA256 ?? '' },
{ tokenSHA256: slot?.getRobot()?.tokenSHA256 ?? '' },
)
.then((response) => {
if (response != null) {
@ -231,7 +229,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
encryptMessage(value, robot?.pubKey, peerPubKey ?? '', robot?.encPrivKey, slot?.token)
.then((encryptedMessage) => {
const { url, basePath } = federation
.getCoordinator(garage.getSlot()?.activeShortAlias ?? '')
.getCoordinator(garage.getSlot()?.activeOrder?.shortAlias ?? '')
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient
.post(
@ -242,7 +240,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
order_id: order.id,
offset: lastIndex,
},
{ tokenSHA256: robot?.tokenSHA256 },
{ tokenSHA256: slot?.getRobot()?.tokenSHA256 },
)
.then((response) => {
if (response != null) {

View File

@ -7,7 +7,6 @@ interface Props {
order: Order;
status: number;
chatOffset: number;
baseUrl: string;
messages: EncryptedChatMessage[];
setMessages: (state: EncryptedChatMessage[]) => void;
}
@ -32,7 +31,6 @@ export interface ServerMessage {
const EncryptedChat: React.FC<Props> = ({
order,
chatOffset,
baseUrl,
setMessages,
messages,
status,
@ -49,7 +47,6 @@ const EncryptedChat: React.FC<Props> = ({
makerHashId={order.maker_hash_id}
userNick={order.ur_nick}
chatOffset={chatOffset}
baseUrl={baseUrl}
turtleMode={turtleMode}
setTurtleMode={setTurtleMode}
/>
@ -63,7 +60,6 @@ const EncryptedChat: React.FC<Props> = ({
takerHashId={order.taker_hash_id}
makerHashId={order.maker_hash_id}
userNick={order.ur_nick}
baseUrl={baseUrl}
turtleMode={turtleMode}
setTurtleMode={setTurtleMode}
/>

View File

@ -20,7 +20,6 @@ interface ChatPromptProps {
loadingReceived: boolean;
onClickDispute: () => void;
loadingDispute: boolean;
baseUrl: string;
messages: EncryptedChatMessage[];
setMessages: (state: EncryptedChatMessage[]) => void;
}
@ -35,12 +34,11 @@ export const ChatPrompt = ({
loadingReceived,
onClickDispute,
loadingDispute,
baseUrl,
messages,
setMessages,
}: ChatPromptProps): JSX.Element => {
const { t } = useTranslation();
const { orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const [sentButton, setSentButton] = useState<boolean>(false);
const [receivedButton, setReceivedButton] = useState<boolean>(false);
@ -113,7 +111,7 @@ export const ChatPrompt = ({
setText(t("The buyer has sent the fiat. Click 'Confirm Received' once you receive it."));
}
}
}, [orderUpdatedAt]);
}, [slotUpdatedAt]);
return (
<Grid
@ -135,7 +133,6 @@ export const ChatPrompt = ({
status={order.status}
chatOffset={order.chat_last_index}
order={order}
baseUrl={baseUrl}
messages={messages}
setMessages={setMessages}
/>

View File

@ -1,7 +1,5 @@
import React, { useState, useEffect, useContext } from 'react';
import { Box, Divider, Grid } from '@mui/material';
import { apiClient } from '../../services/api';
import { getWebln, pn } from '../../utils';
import {
@ -102,7 +100,7 @@ const closeAll: OpenDialogProps = {
};
interface TradeBoxProps {
baseUrl: string;
currentOrder: Order;
onStartAgain: () => void;
}
@ -115,10 +113,10 @@ interface Contract {
titleIcon: () => JSX.Element;
}
const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
const { garage, orderUpdatedAt, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
const { settings, hostUrl, origin } = useContext<UseAppStoreType>(AppContext);
const { federation, setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element => {
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { settings } = useContext<UseAppStoreType>(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const navigate = useNavigate();
// Buttons and Dialogs
@ -155,43 +153,30 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
}
const renewOrder = function (): void {
const currentOrder = garage.getSlot()?.order;
if (currentOrder) {
const body = {
type: currentOrder.type,
currency: currentOrder.currency,
amount: currentOrder.has_range ? null : currentOrder.amount,
has_range: currentOrder.has_range,
min_amount: currentOrder.min_amount,
max_amount: currentOrder.max_amount,
payment_method: currentOrder.payment_method,
is_explicit: currentOrder.is_explicit,
premium: currentOrder.is_explicit ? null : currentOrder.premium,
satoshis: currentOrder.is_explicit ? currentOrder.satoshis : null,
public_duration: currentOrder.public_duration,
escrow_duration: currentOrder.escrow_duration,
bond_size: currentOrder.bond_size,
latitude: currentOrder.latitude,
longitude: currentOrder.longitude,
const slot = garage.getSlot();
const newOrder = currentOrder;
if (newOrder && slot) {
const orderAttributes = {
type: newOrder.type,
currency: newOrder.currency,
amount: newOrder.has_range ? null : newOrder.amount,
has_range: newOrder.has_range,
min_amount: newOrder.min_amount,
max_amount: newOrder.max_amount,
payment_method: newOrder.payment_method,
is_explicit: newOrder.is_explicit,
premium: newOrder.is_explicit ? null : newOrder.premium,
satoshis: newOrder.is_explicit ? newOrder.satoshis : null,
public_duration: newOrder.public_duration,
escrow_duration: newOrder.escrow_duration,
bond_size: newOrder.bond_size,
latitude: newOrder.latitude,
longitude: newOrder.longitude,
shortAlias: newOrder.shortAlias,
};
const { url, basePath } = federation
.getCoordinator(currentOrder.shortAlias)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient
.post(url + basePath, '/api/make/', body, {
tokenSHA256: garage.getSlot()?.getRobot()?.tokenSHA256,
})
.then((data: any) => {
if (data.bad_request !== undefined) {
setBadOrder(data.bad_request);
} else if (data.id !== undefined) {
navigate(`/order/${String(currentOrder?.shortAlias)}/${String(data.id)}`);
setCurrentOrderId({ id: data.id, shortAlias: currentOrder?.shortAlias });
}
})
.catch(() => {
setBadOrder('Request error');
});
void slot.makeOrder(federation, orderAttributes).then((order: Order) => {
if (order?.id) navigate(`/order/${String(order?.shortAlias)}/${String(order.id)}`);
});
}
};
@ -204,14 +189,11 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
statement,
rating,
}: SubmitActionProps): void {
const robot = garage.getSlot()?.getRobot();
const currentOrder = garage.getSlot()?.order;
const slot = garage.getSlot();
void apiClient
.post(
baseUrl,
`/api/order/?order_id=${Number(currentOrder?.id)}`,
{
if (slot && currentOrder) {
currentOrder
.submitAction(federation, slot, {
action,
invoice,
routing_budget_ppm,
@ -219,29 +201,24 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
mining_fee_rate,
statement,
rating,
},
{ tokenSHA256: robot?.tokenSHA256 },
)
.then((data: Order) => {
setOpen(closeAll);
setLoadingButtons({ ...noLoadingButtons });
if (data.bad_request !== undefined) {
setBadOrder(data.bad_request);
} else if (data.bad_address !== undefined) {
setOnchain({ ...onchain, badAddress: data.bad_address });
} else if (data.bad_invoice !== undefined) {
setLightning({ ...lightning, badInvoice: data.bad_invoice });
} else if (data.bad_statement !== undefined) {
setDispute({ ...dispute, badStatement: data.bad_statement });
} else {
garage.updateOrder(data);
setBadOrder(undefined);
}
})
.catch(() => {
setOpen(closeAll);
setLoadingButtons({ ...noLoadingButtons });
});
})
.then((data: Order) => {
setOpen(closeAll);
setLoadingButtons({ ...noLoadingButtons });
if (data.bad_address !== undefined) {
setOnchain({ ...onchain, badAddress: data.bad_address });
} else if (data.bad_invoice !== undefined) {
setLightning({ ...lightning, badInvoice: data.bad_invoice });
} else if (data.bad_statement !== undefined) {
setDispute({ ...dispute, badStatement: data.bad_statement });
}
slot.updateSlotFromOrder(data);
})
.catch(() => {
setOpen(closeAll);
setLoadingButtons({ ...noLoadingButtons });
});
}
};
const cancel = function (): void {
@ -363,15 +340,14 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
// Effect on Order Status change (used for WebLN)
useEffect(() => {
const currentOrder = garage.getSlot()?.order;
if (currentOrder && currentOrder?.status !== lastOrderStatus) {
setLastOrderStatus(currentOrder.status);
void handleWebln(currentOrder);
}
}, [orderUpdatedAt]);
}, [slotUpdatedAt]);
const statusToContract = function (): Contract {
const order = garage.getSlot()?.order;
const order = currentOrder;
const baseContract: Contract = {
title: 'Unknown Order Status',
@ -382,7 +358,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
titleIcon: () => <></>,
};
if (!order) return baseContract;
if (!currentOrder) return baseContract;
const status = order.status;
const isBuyer = order.is_buyer;
@ -573,7 +549,6 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
setOpen({ ...open, confirmDispute: true });
}}
loadingDispute={loadingButtons.openDispute}
baseUrl={baseUrl}
messages={messages}
setMessages={setMessages}
/>
@ -746,7 +721,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
setOpen(closeAll);
}}
waitingWebln={waitingWebln}
isBuyer={garage.getSlot()?.order?.is_buyer ?? false}
isBuyer={currentOrder.is_buyer ?? false}
/>
<ConfirmDisputeDialog
open={open.confirmDispute}
@ -769,11 +744,11 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
}}
onCollabCancelClick={cancel}
loading={loadingButtons.cancel}
peerAskedCancel={garage.getSlot()?.order?.pending_cancel ?? false}
peerAskedCancel={currentOrder?.pending_cancel ?? false}
/>
<ConfirmFiatSentDialog
open={open.confirmFiatSent}
order={garage.getSlot()?.order ?? null}
order={currentOrder ?? null}
loadingButton={loadingButtons.fiatSent}
onClose={() => {
setOpen(closeAll);
@ -790,14 +765,14 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
/>
<ConfirmFiatReceivedDialog
open={open.confirmFiatReceived}
order={garage.getSlot()?.order ?? null}
order={currentOrder ?? null}
loadingButton={loadingButtons.fiatReceived}
onClose={() => {
setOpen(closeAll);
}}
onConfirmClick={confirmFiatReceived}
/>
<CollabCancelAlert order={garage.getSlot()?.order ?? null} />
<CollabCancelAlert order={currentOrder ?? null} />
<Grid
container
padding={1}
@ -808,7 +783,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
>
<Grid item>
<Title
order={garage.getSlot()?.order ?? null}
order={currentOrder ?? null}
text={contract?.title}
color={contract?.titleColor}
icon={contract?.titleIcon}
@ -822,10 +797,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
{contract?.bondStatus !== 'hide' ? (
<Grid item sx={{ width: '100%' }}>
<Divider />
<BondStatus
status={contract?.bondStatus}
isMaker={garage.getSlot()?.order?.is_maker ?? false}
/>
<BondStatus status={contract?.bondStatus} isMaker={currentOrder?.is_maker ?? false} />
</Grid>
) : (
<></>
@ -833,7 +805,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
<Grid item>
<CancelButton
order={garage.getSlot()?.order ?? null}
order={currentOrder ?? null}
onClickCancel={cancel}
openCancelDialog={() => {
setOpen({ ...closeAll, confirmCancel: true });

View File

@ -76,7 +76,11 @@ const makeTheme = function (settings: Settings): Theme {
};
const getHostUrl = (network = 'mainnet'): string => {
let host = defaultFederation.exp[network].onion;
const randomAlias =
Object.keys(defaultFederation)[
Math.floor(Math.random() * Object.keys(defaultFederation).length)
];
let host = defaultFederation[randomAlias][network].onion;
let protocol = 'http:';
if (window.NativeRobosats === undefined) {
host = getHost();

View File

@ -8,7 +8,7 @@ import React, {
type ReactNode,
} from 'react';
import { type Order, Federation, Settings } from '../models';
import { Federation, Settings } from '../models';
import { federationLottery } from '../utils';
@ -16,30 +16,6 @@ import { AppContext, type UseAppStoreType } from './AppContext';
import { GarageContext, type UseGarageStoreType } from './GarageContext';
import { type Origin, type Origins } from '../models/Coordinator.model';
// Refresh delays (ms) according to Order status
const defaultDelay = 5000;
const statusToDelay = [
3000, // 'Waiting for maker bond'
35000, // 'Public'
180000, // 'Paused'
3000, // 'Waiting for taker bond'
999999, // 'Cancelled'
999999, // 'Expired'
8000, // 'Waiting for trade collateral and buyer invoice'
8000, // 'Waiting only for seller trade collateral'
8000, // 'Waiting only for buyer invoice'
10000, // 'Sending fiat - In chatroom'
10000, // 'Fiat sent - In chatroom'
100000, // 'In dispute'
999999, // 'Collaboratively cancelled'
10000, // 'Sending satoshis to buyer'
60000, // 'Successful trade'
30000, // 'Failed lightning network routing'
300000, // 'Wait for dispute resolution'
300000, // 'Maker lost dispute'
300000, // 'Taker lost dispute'
];
export interface CurrentOrderIdProps {
id: number | null;
shortAlias: string | null;
@ -52,10 +28,6 @@ export interface FederationContextProviderProps {
export interface UseFederationStoreType {
federation: Federation;
sortedCoordinators: string[];
setDelay: Dispatch<SetStateAction<number>>;
currentOrderId: CurrentOrderIdProps;
setCurrentOrderId: Dispatch<SetStateAction<CurrentOrderIdProps>>;
currentOrder: Order | null;
coordinatorUpdatedAt: string;
federationUpdatedAt: string;
addNewCoordinator: (alias: string, url: string) => void;
@ -64,10 +36,6 @@ export interface UseFederationStoreType {
export const initialFederationContext: UseFederationStoreType = {
federation: new Federation('onion', new Settings(), ''),
sortedCoordinators: [],
setDelay: () => {},
currentOrderId: { id: null, shortAlias: null },
setCurrentOrderId: () => {},
currentOrder: null,
coordinatorUpdatedAt: '',
federationUpdatedAt: '',
addNewCoordinator: () => {},
@ -80,24 +48,13 @@ export const FederationContextProvider = ({
}: FederationContextProviderProps): JSX.Element => {
const { settings, page, origin, hostUrl, open, torStatus } =
useContext<UseAppStoreType>(AppContext);
const { setMaker, garage, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
const { setMaker, garage } = useContext<UseGarageStoreType>(GarageContext);
const [federation] = useState(new Federation(origin, settings, hostUrl));
const [sortedCoordinators, setSortedCoordinators] = useState(federationLottery(federation));
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>(
new Date().toISOString(),
);
const [federationUpdatedAt, setFederationUpdatedAt] = useState<string>(new Date().toISOString());
const [currentOrderId, setCurrentOrderId] = useState<CurrentOrderIdProps>(
initialFederationContext.currentOrderId,
);
const [currentOrder, setCurrentOrder] = useState<Order | null>(
initialFederationContext.currentOrder,
);
const [delay, setDelay] = useState<number>(defaultDelay);
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(() =>
setInterval(() => null, delay),
);
useEffect(() => {
setMaker((maker) => {
@ -106,65 +63,15 @@ export const FederationContextProvider = ({
federation.registerHook('onFederationUpdate', () => {
setFederationUpdatedAt(new Date().toISOString());
});
federation.registerHook('onCoordinatorUpdate', () => {
setCoordinatorUpdatedAt(new Date().toISOString());
});
}, []);
useEffect(() => {
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
void federation.updateUrl(origin, settings, hostUrl);
void federation.update();
const token = garage.getSlot()?.getRobot()?.token;
if (token) void federation.fetchRobot(garage, token);
}
}, [settings.network, settings.useProxy, torStatus]);
const onOrderReceived = (order: Order): void => {
let newDelay = defaultDelay;
if (order?.bad_request) {
newDelay = 99999999;
setBadOrder(order.bad_request);
garage.updateOrder(null);
setCurrentOrder(null);
}
if (order?.id) {
newDelay =
order.status >= 0 && order.status <= 18
? page === 'order'
? statusToDelay[order.status]
: statusToDelay[order.status] * 5 // If user is not looking at "order" tab, refresh less often.
: 99999999;
garage.updateOrder(order);
setCurrentOrder(order);
setBadOrder(undefined);
}
clearInterval(timer);
console.log('New Delay:', newDelay);
setDelay(newDelay);
setTimer(setTimeout(fetchCurrentOrder, newDelay));
};
const fetchCurrentOrder: () => void = () => {
const slot = garage?.getSlot();
const robot = slot?.getRobot();
if (robot && slot?.token && currentOrderId.id && currentOrderId.shortAlias) {
const coordinator = federation.getCoordinator(currentOrderId.shortAlias);
void coordinator?.fetchOrder(currentOrderId.id, robot, slot?.token).then((order) => {
onOrderReceived(order as Order);
});
} else if (slot?.token && slot?.activeShortAlias && robot?.activeOrderId) {
const coordinator = federation.getCoordinator(slot.activeShortAlias);
void coordinator?.fetchOrder(robot.activeOrderId, robot, slot.token).then((order) => {
onOrderReceived(order as Order);
});
} else {
clearInterval(timer);
setTimer(setTimeout(fetchCurrentOrder, defaultDelay));
}
};
const addNewCoordinator: (alias: string, url: string) => void = (alias, url) => {
if (!federation.coordinators[alias]) {
const attributes: Record<any, any> = {
@ -188,24 +95,12 @@ export const FederationContextProvider = ({
newCoordinator.update(() => {
setCoordinatorUpdatedAt(new Date().toISOString());
});
garage.syncCoordinator(newCoordinator);
garage.syncCoordinator(federation, alias);
setSortedCoordinators(federationLottery(federation));
setFederationUpdatedAt(new Date().toISOString());
}
};
useEffect(() => {
if (currentOrderId.id && currentOrderId.shortAlias) {
setCurrentOrder(null);
setBadOrder(undefined);
clearInterval(timer);
fetchCurrentOrder();
}
return () => {
clearInterval(timer);
};
}, [currentOrderId]);
useEffect(() => {
if (page === 'offers') void federation.updateBook();
}, [page]);
@ -215,7 +110,7 @@ export const FederationContextProvider = ({
const slot = garage.getSlot();
if (open.profile && slot?.hashId && slot?.token) {
void federation.fetchRobot(garage, slot?.token); // refresh/update existing robot
void garage.fetchRobot(federation, slot?.token); // refresh/update existing robot
}
}, [open.profile]);
@ -224,10 +119,6 @@ export const FederationContextProvider = ({
value={{
federation,
sortedCoordinators,
currentOrderId,
setCurrentOrderId,
currentOrder,
setDelay,
coordinatorUpdatedAt,
federationUpdatedAt,
addNewCoordinator,

View File

@ -5,10 +5,14 @@ import React, {
type SetStateAction,
useEffect,
type ReactNode,
useContext,
useRef,
} from 'react';
import { defaultMaker, type Maker, Garage } from '../models';
import { systemClient } from '../services/System';
import { type UseAppStoreType, AppContext } from './AppContext';
import { type UseFederationStoreType, FederationContext } from './FederationContext';
export interface GarageContextProviderProps {
children: ReactNode;
@ -18,61 +22,138 @@ export interface UseGarageStoreType {
garage: Garage;
maker: Maker;
setMaker: Dispatch<SetStateAction<Maker>>;
badOrder?: string;
setBadOrder: Dispatch<SetStateAction<string | undefined>>;
robotUpdatedAt: string;
orderUpdatedAt: string;
setDelay: Dispatch<SetStateAction<number>>;
fetchSlotActiveOrder: () => void;
slotUpdatedAt: string;
}
export const initialGarageContext: UseGarageStoreType = {
garage: new Garage(),
maker: defaultMaker,
setMaker: () => {},
badOrder: undefined,
setBadOrder: () => {},
robotUpdatedAt: '',
orderUpdatedAt: '',
setDelay: () => {},
fetchSlotActiveOrder: () => {},
slotUpdatedAt: '',
};
const defaultDelay = 5000;
// Refresh delays (ms) according to Order status
const statusToDelay = [
3000, // 'Waiting for maker bond'
35000, // 'Public'
180000, // 'Paused'
3000, // 'Waiting for taker bond'
999999, // 'Cancelled'
999999, // 'Expired'
8000, // 'Waiting for trade collateral and buyer invoice'
8000, // 'Waiting only for seller trade collateral'
8000, // 'Waiting only for buyer invoice'
10000, // 'Sending fiat - In chatroom'
10000, // 'Fiat sent - In chatroom'
100000, // 'In dispute'
999999, // 'Collaboratively cancelled'
10000, // 'Sending satoshis to buyer'
60000, // 'Sucessful trade'
30000, // 'Failed lightning network routing'
300000, // 'Wait for dispute resolution'
300000, // 'Maker lost dispute'
300000, // 'Taker lost dispute'
];
export const GarageContext = createContext<UseGarageStoreType>(initialGarageContext);
export const GarageContextProvider = ({ children }: GarageContextProviderProps): JSX.Element => {
// All garage data structured
const { settings, torStatus, open, page } = useContext<UseAppStoreType>(AppContext);
const pageRef = useRef(page);
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const [garage] = useState<Garage>(initialGarageContext.garage);
const [maker, setMaker] = useState<Maker>(initialGarageContext.maker);
const [badOrder, setBadOrder] = useState<string>();
const [robotUpdatedAt, setRobotUpdatedAt] = useState<string>(new Date().toISOString());
const [orderUpdatedAt, setOrderUpdatedAt] = useState<string>(new Date().toISOString());
const [slotUpdatedAt, setSlotUpdatedAt] = useState<string>(new Date().toISOString());
const [lastOrderCheckAt] = useState<number>(+new Date());
const lastOrderCheckAtRef = useRef(lastOrderCheckAt);
const [delay, setDelay] = useState<number>(defaultDelay);
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(() =>
setInterval(() => null, delay),
);
const onRobotUpdated = (): void => {
setRobotUpdatedAt(new Date().toISOString());
};
const onOrderUpdate = (): void => {
setOrderUpdatedAt(new Date().toISOString());
const onSlotUpdated = (): void => {
setSlotUpdatedAt(new Date().toISOString());
};
useEffect(() => {
garage.registerHook('onRobotUpdate', onRobotUpdated);
garage.registerHook('onOrderUpdate', onOrderUpdate);
setMaker((maker) => {
return { ...maker, coordinator: sortedCoordinators[0] };
}); // default MakerForm coordinator is decided via sorted lottery
garage.registerHook('onSlotUpdate', onSlotUpdated);
clearInterval(timer);
fetchSlotActiveOrder();
return () => clearTimeout(timer);
}, []);
useEffect(() => {
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
const token = garage.getSlot()?.token;
if (token) void garage.fetchRobot(federation, token);
}
}, [settings.network, settings.useProxy, torStatus]);
useEffect(() => {
if (window.NativeRobosats !== undefined && !systemClient.loading) {
garage.loadSlots();
}
}, [systemClient.loading]);
useEffect(() => {
pageRef.current = page;
}, [page]);
// use effects to fetchRobots on Profile open
useEffect(() => {
const slot = garage.getSlot();
if (open.profile && slot?.hashId && slot?.token) {
void garage.fetchRobot(federation, slot?.token); // refresh/update existing robot
}
}, [open.profile]);
const fetchSlotActiveOrder: () => void = () => {
const slot = garage?.getSlot();
if (slot?.activeOrder?.id) {
let delay =
slot.activeOrder.status >= 0 && slot.activeOrder.status <= 18
? statusToDelay[slot.activeOrder.status]
: defaultDelay;
if (pageRef.current !== 'order') delay = delay * 5;
if (+new Date() - lastOrderCheckAtRef.current >= delay) {
void slot.fetchActiveOrder(federation).finally(() => {
lastOrderCheckAtRef.current = +new Date();
resetInterval();
});
} else {
resetInterval();
}
} else {
resetInterval();
}
};
const resetInterval = (): void => {
clearInterval(timer);
setDelay(defaultDelay);
setTimer(setTimeout(() => fetchSlotActiveOrder(), defaultDelay));
};
return (
<GarageContext.Provider
value={{
garage,
maker,
setMaker,
badOrder,
setBadOrder,
robotUpdatedAt,
orderUpdatedAt,
setDelay,
fetchSlotActiveOrder,
slotUpdatedAt,
}}
>
{children}

View File

@ -1,16 +1,7 @@
import {
type Robot,
type LimitList,
type PublicOrder,
type Settings,
type Order,
type Garage,
} from '.';
import { type LimitList, type PublicOrder, type Settings } from '.';
import { roboidentitiesClient } from '../services/Roboidentities/Web';
import { apiClient } from '../services/api';
import { validateTokenEntropy } from '../utils';
import { compareUpdateLimit } from './Limit.model';
import { defaultOrder } from './Order.model';
export interface Contact {
nostr?: string | undefined;
@ -180,7 +171,6 @@ export class Coordinator {
public loadingInfo: boolean = false;
public limits: LimitList = {};
public loadingLimits: boolean = false;
public loadingRobot: string | null;
updateUrl = (origin: Origin, settings: Settings, hostUrl: string): void => {
if (settings.selfhostedClient && this.shortAlias !== 'local') {
@ -331,132 +321,6 @@ export class Coordinator {
return { url: String(this[network][origin]), basePath: '' };
}
};
fetchRobot = async (garage: Garage, token: string): Promise<Robot | null> => {
if (!this.enabled || !token || this.loadingRobot === token) return null;
const robot = garage?.getSlot(token)?.getRobot() ?? null;
const authHeaders = robot?.getAuthHeaders();
if (!authHeaders) return null;
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
if (!hasEnoughEntropy) return null;
this.loadingRobot = token;
garage.updateRobot(token, this.shortAlias, { loading: true });
const newAttributes = await apiClient
.get(this.url, `${this.basePath}/api/robot/`, authHeaders)
.then((data: any) => {
return {
nickname: data.nickname,
activeOrderId: data.active_order_id ?? null,
lastOrderId: data.last_order_id ?? null,
earnedRewards: data.earned_rewards ?? 0,
stealthInvoices: data.wants_stealth,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
found: data?.found,
last_login: data.last_login,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
};
})
.catch((e) => {
console.log(e);
})
.finally(() => (this.loadingRobot = null));
garage.updateRobot(token, this.shortAlias, {
...newAttributes,
tokenSHA256: authHeaders.tokenSHA256,
loading: false,
bitsEntropy,
shannonEntropy,
shortAlias: this.shortAlias,
});
return garage.getSlot(this.shortAlias)?.getRobot() ?? null;
};
fetchOrder = async (orderId: number, robot: Robot, token: string): Promise<Order | null> => {
if (!this.enabled) return null;
if (!token) return null;
const authHeaders = robot.getAuthHeaders();
if (!authHeaders) return null;
return await apiClient
.get(this.url, `${this.basePath}/api/order/?order_id=${orderId}`, authHeaders)
.then((data) => {
const order: Order = {
...defaultOrder,
...data,
shortAlias: this.shortAlias,
};
return order;
})
.catch((e) => {
console.log(e);
return null;
});
};
fetchReward = async (
signedInvoice: string,
garage: Garage,
index: string,
): Promise<null | {
bad_invoice?: string;
successful_withdrawal?: boolean;
}> => {
if (!this.enabled) return null;
const slot = garage.getSlot(index);
const robot = slot?.getRobot();
if (!slot?.token || !robot?.encPrivKey) return null;
const data = await apiClient.post(
this.url,
`${this.basePath}/api/reward/`,
{
invoice: signedInvoice,
},
{ tokenSHA256: robot.tokenSHA256 },
);
garage.updateRobot(slot?.token, this.shortAlias, {
earnedRewards: data?.successful_withdrawal === true ? 0 : robot.earnedRewards,
});
return data ?? {};
};
fetchStealth = async (wantsStealth: boolean, garage: Garage, index: string): Promise<null> => {
if (!this.enabled) return null;
const slot = garage.getSlot(index);
const robot = slot?.getRobot();
if (!(slot?.token != null) || !(robot?.encPrivKey != null)) return null;
await apiClient.post(
this.url,
`${this.basePath}/api/stealth/`,
{ wantsStealth },
{ tokenSHA256: robot.tokenSHA256 },
);
garage.updateRobot(slot?.token, this.shortAlias, {
stealthInvoices: wantsStealth,
});
return null;
};
}
export default Coordinator;

View File

@ -1,7 +1,6 @@
import {
Coordinator,
type Exchange,
type Garage,
type Origin,
type PublicOrder,
type Settings,
@ -13,7 +12,7 @@ import { getHost } from '../utils';
import { coordinatorDefaultValues } from './Coordinator.model';
import { updateExchangeInfo } from './Exchange.model';
type FederationHooks = 'onCoordinatorUpdate' | 'onFederationUpdate';
type FederationHooks = 'onFederationUpdate';
export class Federation {
constructor(origin: Origin, settings: Settings, hostUrl: string) {
@ -36,7 +35,6 @@ export class Federation {
};
this.book = [];
this.hooks = {
onCoordinatorUpdate: [],
onFederationUpdate: [],
};
@ -97,7 +95,6 @@ export class Federation {
this.book = Object.values(this.coordinators).reduce<PublicOrder[]>((array, coordinator) => {
return [...array, ...coordinator.book];
}, []);
this.triggerHook('onCoordinatorUpdate');
this.exchange.loadingCoordinators =
this.exchange.loadingCoordinators < 1 ? 0 : this.exchange.loadingCoordinators - 1;
this.loading = this.exchange.loadingCoordinators > 0;
@ -140,11 +137,12 @@ export class Federation {
updateBook = async (): Promise<void> => {
this.loading = true;
this.book = [];
this.triggerHook('onCoordinatorUpdate');
this.triggerHook('onFederationUpdate');
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
for (const coor of Object.values(this.coordinators)) {
void coor.updateBook(() => {
this.onCoordinatorSaved();
this.triggerHook('onFederationUpdate');
});
}
};
@ -154,13 +152,6 @@ export class Federation {
this.triggerHook('onFederationUpdate');
};
// Fetchs
fetchRobot = async (garage: Garage, token: string): Promise<void> => {
Object.values(this.coordinators).forEach((coor) => {
void coor.fetchRobot(garage, token);
});
};
// Coordinators
getCoordinator = (shortAlias: string): Coordinator => {
return this.coordinators[shortAlias];
@ -169,13 +160,13 @@ export class Federation {
disableCoordinator = (shortAlias: string): void => {
this.coordinators[shortAlias].disable();
this.updateEnabledCoordinators();
this.triggerHook('onCoordinatorUpdate');
this.triggerHook('onFederationUpdate');
};
enableCoordinator = (shortAlias: string): void => {
this.coordinators[shortAlias].enable(() => {
this.updateEnabledCoordinators();
this.triggerHook('onCoordinatorUpdate');
this.triggerHook('onFederationUpdate');
});
};

View File

@ -1,9 +1,9 @@
import { type Coordinator, type Order } from '.';
import { type Federation, Order } from '.';
import { systemClient } from '../services/System';
import { saveAsJson } from '../utils';
import Slot from './Slot.model';
type GarageHooks = 'onRobotUpdate' | 'onOrderUpdate';
type GarageHooks = 'onSlotUpdate';
class Garage {
constructor() {
@ -11,8 +11,7 @@ class Garage {
this.currentSlot = null;
this.hooks = {
onRobotUpdate: [],
onOrderUpdate: [],
onSlotUpdate: [],
};
this.loadSlots();
@ -29,6 +28,7 @@ class Garage {
};
triggerHook = (hookName: GarageHooks): void => {
this.save();
this.hooks[hookName]?.forEach((fn) => {
fn();
});
@ -47,8 +47,7 @@ class Garage {
this.slots = {};
this.currentSlot = null;
systemClient.deleteItem('garage_slots');
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
this.triggerHook('onSlotUpdate');
};
loadSlots = (): void => {
@ -64,26 +63,23 @@ class Garage {
Object.keys(rawSlot.robots),
{},
() => {
this.triggerHook('onRobotUpdate');
this.triggerHook('onSlotUpdate');
},
);
Object.keys(rawSlot.robots).forEach((shortAlias) => {
const rawRobot = rawSlot.robots[shortAlias];
this.updateRobot(rawSlot.token, shortAlias, rawRobot);
});
this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.lastOrder));
this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.activeOrder));
this.currentSlot = rawSlot?.token;
}
});
console.log('Robot Garage was loaded from local storage');
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
this.triggerHook('onSlotUpdate');
}
};
// Slots
getSlot: (token?: string) => Slot | null = (token) => {
const currentToken = token ?? this.currentSlot;
return currentToken ? this.slots[currentToken] ?? null : null;
return currentToken ? (this.slots[currentToken] ?? null) : null;
};
deleteSlot: (token?: string) => void = (token) => {
@ -92,8 +88,7 @@ class Garage {
Reflect.deleteProperty(this.slots, targetIndex);
this.currentSlot = null;
this.save();
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
this.triggerHook('onSlotUpdate');
}
};
@ -105,7 +100,7 @@ class Garage {
if (attributes) {
if (attributes.copiedToken !== undefined) slot?.setCopiedToken(attributes.copiedToken);
this.save();
this.triggerHook('onRobotUpdate');
this.triggerHook('onSlotUpdate');
}
return slot;
};
@ -113,7 +108,7 @@ class Garage {
setCurrentSlot: (currentSlot: string) => void = (currentSlot) => {
this.currentSlot = currentSlot;
this.save();
this.triggerHook('onRobotUpdate');
this.triggerHook('onSlotUpdate');
};
getSlotByOrder: (coordinator: string, orderID: number) => Slot | null = (
@ -123,7 +118,7 @@ class Garage {
return (
Object.values(this.slots).find((slot) => {
const robot = slot.getRobot(coordinator);
return slot.activeShortAlias === coordinator && robot?.activeOrderId === orderID;
return slot.activeOrder?.shortAlias === coordinator && robot?.activeOrderId === orderID;
}) ?? null
);
};
@ -138,55 +133,29 @@ class Garage {
if (this.getSlot(token) === null) {
this.slots[token] = new Slot(token, shortAliases, attributes, () => {
this.triggerHook('onRobotUpdate');
this.triggerHook('onSlotUpdate');
});
this.save();
}
};
updateRobot: (token: string, shortAlias: string, attributes: Record<any, any>) => void = (
token,
shortAlias,
attributes,
) => {
if (!token || !shortAlias) return;
fetchRobot = async (federation: Federation, token: string): Promise<void> => {
const slot = this.getSlot(token);
if (slot != null) {
slot.updateRobot(shortAlias, { token, ...attributes });
await slot.fetchRobot(federation);
this.save();
this.triggerHook('onRobotUpdate');
}
};
// Orders
updateOrder: (order: Order | null) => void = (order) => {
const slot = this.getSlot();
if (slot != null) {
if (order !== null) {
const updatedOrder = slot.order ?? null;
if (updatedOrder !== null && updatedOrder.id === order.id) {
Object.assign(updatedOrder, order);
slot.order = updatedOrder;
} else {
slot.order = order;
}
if (slot.order?.is_participant) {
slot.activeShortAlias = order.shortAlias;
}
} else {
slot.order = null;
}
this.save();
this.triggerHook('onOrderUpdate');
this.triggerHook('onSlotUpdate');
}
};
// Coordinators
syncCoordinator: (coordinator: Coordinator) => void = (coordinator) => {
syncCoordinator: (federation: Federation, shortAlias: string) => void = (
federation,
shortAlias,
) => {
Object.values(this.slots).forEach((slot) => {
slot.syncCoordinator(coordinator, this);
slot.syncCoordinator(federation, shortAlias);
});
this.save();
};

View File

@ -1,3 +1,5 @@
import defaultFederation from '../../static/federation.json';
export interface Maker {
advancedOptions: boolean;
coordinator: string;
@ -23,7 +25,10 @@ export interface Maker {
export const defaultMaker: Maker = {
advancedOptions: false,
coordinator: 'exp',
coordinator:
Object.keys(defaultFederation)[
Math.floor(Math.random() * Object.keys(defaultFederation).length)
] ?? '',
isExplicit: false,
amount: '',
paymentMethods: [],
@ -40,6 +45,8 @@ export const defaultMaker: Maker = {
maxAmount: '',
badPremiumText: '',
badSatoshisText: '',
latitude: 0,
longitude: 0,
};
export default Maker;

View File

@ -1,3 +1,28 @@
import { apiClient } from '../services/api';
import type Federation from './Federation.model';
import type Slot from './Slot.model';
export interface SubmitActionProps {
action:
| 'cancel'
| 'dispute'
| 'pause'
| 'confirm'
| 'undo_confirm'
| 'update_invoice'
| 'update_address'
| 'submit_statement'
| 'rate_platform'
| 'take';
invoice?: string;
routing_budget_ppm?: number;
address?: string;
mining_fee_rate?: number;
statement?: string;
rating?: number;
amount?: number;
}
export interface TradeRobotSummary {
is_buyer: boolean;
sent_fiat: number;
@ -24,168 +49,82 @@ export interface TradeCoordinatorSummary {
trade_revenue_sats: number;
}
export interface Order {
id: number;
status: number;
created_at: Date;
expires_at: Date;
type: number;
currency: number;
amount: number;
has_range: boolean;
min_amount: number;
max_amount: number;
payment_method: string;
is_explicit: boolean;
premium: number;
satoshis: number;
maker: number;
taker: number;
escrow_duration: number;
total_secs_exp: number;
penalty: Date | undefined;
is_maker: boolean;
is_taker: boolean;
is_participant: boolean;
maker_status: 'Active' | 'Seen recently' | 'Inactive';
taker_status: 'Active' | 'Seen recently' | 'Inactive';
price_now: number | undefined;
satoshis_now: number;
latitude: number;
longitude: number;
premium_now: number | undefined;
premium_percentile: number;
num_similar_orders: number;
tg_enabled: boolean; // deprecated
tg_token: string;
tg_bot_name: string;
is_buyer: boolean;
is_seller: boolean;
maker_nick: string;
maker_hash_id: string;
taker_nick: string;
taker_hash_id: string;
status_message: string;
is_fiat_sent: boolean;
is_disputed: boolean;
ur_nick: string;
maker_locked: boolean;
taker_locked: boolean;
escrow_locked: boolean;
trade_satoshis: number;
bond_invoice: string;
bond_satoshis: number;
escrow_invoice: string;
escrow_satoshis: number;
invoice_amount: number;
swap_allowed: boolean;
swap_failure_reason: string;
suggested_mining_fee_rate: number;
swap_fee_rate: number;
pending_cancel: boolean;
asked_for_cancel: boolean;
statement_submitted: boolean;
retries: number;
next_retry_time: Date;
failure_reason: string;
invoice_expired: boolean;
public_duration: number;
bond_size: string;
trade_fee_percent: number;
bond_size_sats: number;
bond_size_percent: number;
chat_last_index: number;
maker_summary: TradeRobotSummary;
taker_summary: TradeRobotSummary;
platform_summary: TradeCoordinatorSummary;
expiry_reason: number;
expiry_message: string;
num_satoshis: number;
sent_satoshis: number;
txid: string;
tx_queued: boolean;
address: string;
network: 'mainnet' | 'testnet';
shortAlias: string;
bad_request?: string;
bad_address?: string;
bad_invoice?: string;
bad_statement?: string;
}
class Order {
constructor(attributes: object) {
Object.assign(this, attributes);
}
export const defaultOrder: Order = {
shortAlias: '',
id: 0,
status: 0,
created_at: new Date(),
expires_at: new Date(),
type: 0,
currency: 0,
amount: 0,
has_range: false,
min_amount: 0,
max_amount: 0,
payment_method: '',
is_explicit: false,
premium: 0,
satoshis: 0,
maker: 0,
taker: 0,
escrow_duration: 0,
total_secs_exp: 0,
penalty: undefined,
is_maker: false,
is_taker: false,
is_participant: false,
maker_status: 'Active',
taker_status: 'Active',
price_now: undefined,
satoshis_now: 0,
latitude: 0,
longitude: 0,
premium_now: undefined,
premium_percentile: 0,
num_similar_orders: 0,
tg_enabled: false,
tg_token: '',
tg_bot_name: '',
is_buyer: false,
is_seller: false,
maker_nick: '',
maker_hash_id: '',
taker_nick: '',
taker_hash_id: '',
status_message: '',
is_fiat_sent: false,
is_disputed: false,
ur_nick: '',
maker_locked: false,
taker_locked: false,
escrow_locked: false,
trade_satoshis: 0,
bond_invoice: '',
bond_satoshis: 0,
escrow_invoice: '',
escrow_satoshis: 0,
invoice_amount: 0,
swap_allowed: false,
swap_failure_reason: '',
suggested_mining_fee_rate: 0,
swap_fee_rate: 0,
pending_cancel: false,
asked_for_cancel: false,
statement_submitted: false,
retries: 0,
next_retry_time: new Date(),
failure_reason: '',
invoice_expired: false,
public_duration: 0,
bond_size: '',
trade_fee_percent: 0,
bond_size_sats: 0,
bond_size_percent: 0,
chat_last_index: 0,
maker_summary: {
id: number = 0;
status: number = 0;
created_at: Date = new Date();
expires_at: Date = new Date();
type: number = 0;
currency: number = 0;
amount: number = 0;
has_range: boolean = false;
min_amount: number = 0;
max_amount: number = 0;
payment_method: string = '';
is_explicit: boolean = false;
premium: number = 0;
satoshis: number = 0;
maker: number = 0;
taker: number = 0;
escrow_duration: number = 0;
total_secs_exp: number = 0;
penalty: Date | undefined = undefined;
is_maker: boolean = false;
is_taker: boolean = false;
is_participant: boolean = false;
maker_status: 'Active' | 'Seen recently' | 'Inactive' = 'Active';
taker_status: 'Active' | 'Seen recently' | 'Inactive' = 'Active';
price_now: number | undefined = undefined;
satoshis_now: number = 0;
latitude: number = 0;
longitude: number = 0;
premium_now: number | undefined = undefined;
premium_percentile: number = 0;
num_similar_orders: number = 0;
tg_enabled: boolean = false; // deprecated
tg_token: string = '';
tg_bot_name: string = '';
is_buyer: boolean = false;
is_seller: boolean = false;
maker_nick: string = '';
maker_hash_id: string = '';
taker_nick: string = '';
taker_hash_id: string = '';
status_message: string = '';
is_fiat_sent: boolean = false;
is_disputed: boolean = false;
ur_nick: string = '';
maker_locked: boolean = false;
taker_locked: boolean = false;
escrow_locked: boolean = false;
trade_satoshis: number = 0;
bond_invoice: string = '';
bond_satoshis: number = 0;
escrow_invoice: string = '';
escrow_satoshis: number = 0;
invoice_amount: number = 0;
swap_allowed: boolean = false;
swap_failure_reason: string = '';
suggested_mining_fee_rate: number = 0;
swap_fee_rate: number = 0;
pending_cancel: boolean = false;
asked_for_cancel: boolean = false;
statement_submitted: boolean = false;
retries: number = 0;
next_retry_time: Date = new Date();
failure_reason: string = '';
invoice_expired: boolean = false;
public_duration: number = 0;
bond_size: string = '';
trade_fee_percent: number = 0;
bond_size_sats: number = 0;
bond_size_percent: number = 0;
chat_last_index: number = 0;
maker_summary: TradeRobotSummary = {
is_buyer: false,
sent_fiat: 0,
received_sats: 0,
@ -197,8 +136,9 @@ export const defaultOrder: Order = {
sent_sats: 0,
received_fiat: 0,
trade_fee_sats: 0,
},
taker_summary: {
};
taker_summary: TradeRobotSummary = {
is_buyer: false,
sent_fiat: 0,
received_sats: 0,
@ -210,22 +150,118 @@ export const defaultOrder: Order = {
sent_sats: 0,
received_fiat: 0,
trade_fee_sats: 0,
},
platform_summary: {
};
platform_summary: TradeCoordinatorSummary = {
contract_timestamp: new Date(),
contract_total_time: 0,
contract_exchange_rate: 0,
routing_budget_sats: 0,
trade_revenue_sats: 0,
},
expiry_reason: 0,
expiry_message: '',
num_satoshis: 0,
sent_satoshis: 0,
txid: '',
tx_queued: false,
address: '',
network: 'mainnet',
};
};
expiry_reason: number = 0;
expiry_message: string = '';
num_satoshis: number = 0;
sent_satoshis: number = 0;
txid: string = '';
tx_queued: boolean = false;
address: string = '';
network: 'mainnet' | 'testnet' = 'mainnet';
shortAlias: string = '';
bad_request?: string = '';
bad_address?: string = '';
bad_invoice?: string = '';
bad_statement?: string = '';
update = (attributes: Record<string, any>): Order => {
Object.assign(this, attributes);
return this;
};
make: (federation: Federation, slot: Slot) => Promise<this> = async (federation, slot) => {
const body = {
type: this.type,
currency: this.currency,
amount: this.has_range ? null : this.amount,
has_range: this.has_range,
min_amount: this.min_amount,
max_amount: this.max_amount,
payment_method: this.payment_method,
is_explicit: this.is_explicit,
premium: this.is_explicit ? null : this.premium,
satoshis: this.is_explicit ? this.satoshis : null,
public_duration: this.public_duration,
escrow_duration: this.escrow_duration,
bond_size: this.bond_size,
latitude: this.latitude,
longitude: this.longitude,
};
if (slot) {
const coordinator = federation.getCoordinator(this.shortAlias);
const { basePath, url } = coordinator;
const data = await apiClient
.post(url + basePath, '/api/make/', body, {
tokenSHA256: slot?.getRobot()?.tokenSHA256 ?? '',
})
.catch((e) => {
console.log(e);
});
if (data) this.update(data);
}
return this;
};
take: (federation: Federation, slot: Slot, takeAmount: string) => Promise<this> = async (
federation,
slot,
takeAmount,
) => {
return this.submitAction(federation, slot, {
action: 'take',
amount: this?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount),
});
};
submitAction: (federation: Federation, slot: Slot, action: SubmitActionProps) => Promise<this> =
async (federation, slot, action) => {
if (this.id < 1) return this;
if (slot) {
const coordinator = federation.getCoordinator(this.shortAlias);
const { basePath, url } = coordinator;
const data = await apiClient
.post(url + basePath, `/api/order/?order_id=${Number(this.id)}`, action, {
tokenSHA256: slot?.getRobot()?.tokenSHA256 ?? '',
})
.catch((e) => {
console.log(e);
});
if (data) this.update(data);
}
return this;
};
fecth: (federation: Federation, slot: Slot) => Promise<this> = async (federation, slot) => {
if (this.id < 1) return this;
if (!slot) return this;
const coordinator = federation.getCoordinator(this.shortAlias);
const authHeaders = slot.getRobot()?.getAuthHeaders();
if (!authHeaders) return this;
const { basePath, url } = coordinator;
const data = await apiClient
.get(url + basePath, `/api/order/?order_id=${this.id}`, authHeaders)
.catch((e) => {
console.log(e);
});
if (data) this.update(data);
return this;
};
}
export default Order;

View File

@ -1,29 +1,13 @@
import { sha256 } from 'js-sha256';
import { hexToBase91 } from '../utils';
interface AuthHeaders {
tokenSHA256: string;
keys: {
pubKey: string;
encPrivKey: string;
};
}
import { apiClient } from '../services/api';
import type Federation from './Federation.model';
import { type AuthHeaders } from './Slot.model';
class Robot {
constructor(attributes?: Record<any, any>) {
if (attributes != null) {
this.token = attributes?.token ?? undefined;
this.tokenSHA256 =
attributes?.tokenSHA256 ?? (this.token != null ? hexToBase91(sha256(this.token)) : '');
this.pubKey = attributes?.pubKey ?? undefined;
this.encPrivKey = attributes?.encPrivKey ?? undefined;
}
Object.assign(this, attributes);
}
public token?: string;
public bitsEntropy?: number;
public shannonEntropy?: number;
public tokenSHA256: string = '';
public pubKey?: string;
public encPrivKey?: string;
public stealthInvoices: boolean = true;
@ -37,6 +21,10 @@ class Robot {
public found: boolean = false;
public last_login: string = '';
public shortAlias: string = '';
public bitsEntropy?: number;
public shannonEntropy?: number;
public tokenSHA256: string = '';
public hasEnoughEntropy: boolean = false;
update = (attributes: Record<string, any>): void => {
Object.assign(this, attributes);
@ -55,6 +43,85 @@ class Robot {
},
};
};
fetch = async (federation: Federation): Promise<Robot | null> => {
const authHeaders = this.getAuthHeaders();
const coordinator = federation.getCoordinator(this.shortAlias);
if (!authHeaders || !coordinator || !this.hasEnoughEntropy) return null;
this.loading = true;
await apiClient
.get(coordinator.url, `${coordinator.basePath}/api/robot/`, authHeaders)
.then((data: any) => {
this.update({
nickname: data.nickname,
activeOrderId: data.active_order_id ?? null,
lastOrderId: data.last_order_id ?? null,
earnedRewards: data.earned_rewards ?? 0,
stealthInvoices: data.wants_stealth,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
found: data?.found,
last_login: data.last_login,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
});
})
.catch((e) => {
console.log(e);
})
.finally(() => (this.loading = false));
return this;
};
fetchReward = async (
federation: Federation,
signedInvoice: string,
): Promise<null | {
bad_invoice?: string;
successful_withdrawal?: boolean;
}> => {
if (!federation) return null;
const coordinator = federation.getCoordinator(this.shortAlias);
const data = await apiClient
.post(
coordinator.url,
`${coordinator.basePath}/api/reward/`,
{
invoice: signedInvoice,
},
{ tokenSHA256: this.tokenSHA256 },
)
.catch((e) => {
console.log(e);
});
this.earnedRewards = data?.successful_withdrawal === true ? 0 : this.earnedRewards;
return data ?? {};
};
fetchStealth = async (federation: Federation, wantsStealth: boolean): Promise<void> => {
if (!federation) return;
const coordinator = federation.getCoordinator(this.shortAlias);
await apiClient
.post(
coordinator.url,
`${coordinator.basePath}/api/stealth/`,
{ wantsStealth },
{ tokenSHA256: this.tokenSHA256 },
)
.catch((e) => {
console.log(e);
});
this.stealthInvoices = wantsStealth;
};
}
export default Robot;

View File

@ -1,88 +1,170 @@
import { sha256 } from 'js-sha256';
import { type Coordinator, type Garage, Robot, type Order } from '.';
import { Robot, Order, type Federation } from '.';
import { roboidentitiesClient } from '../services/Roboidentities/Web';
import { hexToBase91, validateTokenEntropy } from '../utils';
export interface AuthHeaders {
tokenSHA256: string;
keys: {
pubKey: string;
encPrivKey: string;
};
}
class Slot {
constructor(
token: string,
shortAliases: string[],
robotAttributes: Record<any, any>,
onRobotUpdate: () => void,
onSlotUpdate: () => void,
) {
this.onSlotUpdate = onSlotUpdate;
this.token = token;
this.hashId = sha256(sha256(this.token));
this.nickname = null;
void roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => {
this.nickname = nickname;
onRobotUpdate();
onSlotUpdate();
});
void roboidentitiesClient.generateRobohash(this.hashId, 'small');
void roboidentitiesClient.generateRobohash(this.hashId, 'large');
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
const tokenSHA256 = hexToBase91(sha256(token));
this.robots = shortAliases.reduce((acc: Record<string, Robot>, shortAlias: string) => {
acc[shortAlias] = new Robot(robotAttributes);
acc[shortAlias] = new Robot({
...robotAttributes,
shortAlias,
hasEnoughEntropy,
bitsEntropy,
shannonEntropy,
tokenSHA256,
pubKey: robotAttributes.pubKey,
encPrivKey: robotAttributes.encPrivKey,
});
this.updateSlotFromRobot(acc[shortAlias]);
return acc;
}, {});
this.order = null;
this.activeShortAlias = null;
this.lastShortAlias = null;
this.copiedToken = false;
onRobotUpdate();
this.onSlotUpdate();
}
token: string | null;
hashId: string | null;
nickname: string | null;
robots: Record<string, Robot>;
order: Order | null;
activeShortAlias: string | null;
lastShortAlias: string | null;
activeOrder: Order | null = null;
lastOrder: Order | null = null;
copiedToken: boolean;
onSlotUpdate: () => void;
setCopiedToken = (copied: boolean): void => {
this.copiedToken = copied;
};
// Robots
getRobot = (shortAlias?: string): Robot | null => {
if (shortAlias) {
return this.robots[shortAlias];
} else if (this.activeShortAlias !== null && this.robots[this.activeShortAlias]) {
return this.robots[this.activeShortAlias];
} else if (this.lastShortAlias !== null && this.robots[this.lastShortAlias]) {
return this.robots[this.lastShortAlias];
} else if (this.activeOrder?.id) {
return this.robots[this.activeOrder.shortAlias];
} else if (this.lastOrder?.id && this.robots[this.lastOrder.shortAlias]) {
return this.robots[this.lastOrder.shortAlias];
} else if (Object.values(this.robots).length > 0) {
return Object.values(this.robots)[0];
}
return null;
};
updateRobot = (shortAlias: string, attributes: Record<any, any>): Robot | null => {
this.robots[shortAlias].update(attributes);
if (attributes.lastOrderId) {
this.lastShortAlias = shortAlias;
if (this.activeShortAlias === shortAlias) {
this.activeShortAlias = null;
}
}
if (attributes.activeOrderId) {
this.activeShortAlias = attributes.shortAlias;
}
return this.robots[shortAlias];
fetchRobot = async (federation: Federation): Promise<void> => {
Object.values(this.robots).forEach((robot) => {
void robot.fetch(federation).then((robot) => {
this.updateSlotFromRobot(robot);
});
});
};
syncCoordinator: (coordinator: Coordinator, garage: Garage) => void = (coordinator, garage) => {
updateSlotFromRobot = (robot: Robot | null): void => {
if (robot?.lastOrderId && this.lastOrder?.id !== robot?.lastOrderId) {
this.lastOrder = new Order({ id: robot.lastOrderId, shortAlias: robot.shortAlias });
if (this.activeOrder?.id === robot.lastOrderId) {
this.lastOrder = this.activeOrder;
this.activeOrder = null;
}
}
if (robot?.activeOrderId && this.activeOrder?.id !== robot.activeOrderId) {
this.activeOrder = new Order({
id: robot.activeOrderId,
shortAlias: robot.shortAlias,
});
}
this.onSlotUpdate();
};
// Orders
fetchActiveOrder = async (federation: Federation): Promise<void> => {
void this.activeOrder?.fecth(federation, this);
this.updateSlotFromOrder(this.activeOrder);
};
takeOrder = async (federation: Federation, order: Order, takeAmount: string): Promise<Order> => {
await order.take(federation, this, takeAmount);
this.updateSlotFromOrder(order);
return order;
};
makeOrder = async (federation: Federation, attributes: object): Promise<Order> => {
const order = new Order(attributes);
await order.make(federation, this);
this.lastOrder = this.activeOrder;
this.activeOrder = order;
this.onSlotUpdate();
return this.activeOrder;
};
updateSlotFromOrder: (newOrder: Order | null) => void = (newOrder) => {
if (newOrder) {
// FIXME: API responses with bad_request should include also order's status
if (newOrder?.bad_request?.includes('expired')) newOrder.status = 5;
if (newOrder?.bad_request?.includes('collaborativelly')) newOrder.status = 12;
if (
newOrder.id === this.activeOrder?.id &&
newOrder.shortAlias === this.activeOrder?.shortAlias
) {
this.activeOrder?.update(newOrder);
if (this.activeOrder?.bad_request) {
this.lastOrder = this.activeOrder;
this.activeOrder = null;
}
this.onSlotUpdate();
} else if (newOrder?.is_participant && this.lastOrder?.id !== newOrder.id) {
this.activeOrder = newOrder;
this.onSlotUpdate();
}
}
};
syncCoordinator: (federation: Federation, shortAlias: string) => void = (
federation,
shortAlias,
) => {
const defaultRobot = this.getRobot();
if (defaultRobot?.token) {
this.robots[coordinator.shortAlias] = new Robot({
this.robots[shortAlias] = new Robot({
shortAlias,
hasEnoughEntropy: defaultRobot.hasEnoughEntropy,
bitsEntropy: defaultRobot.bitsEntropy,
shannonEntropy: defaultRobot.shannonEntropy,
token: defaultRobot.token,
pubKey: defaultRobot.pubKey,
encPrivKey: defaultRobot.encPrivKey,
});
void coordinator.fetchRobot(garage, defaultRobot.token);
void this.robots[shortAlias].fetch(federation);
this.updateSlotFromRobot(this.robots[shortAlias]);
}
};
}

View File

@ -3,14 +3,14 @@ import Garage from './Garage.model';
import Settings from './Settings.default.basic';
import Coordinator from './Coordinator.model';
import Federation from './Federation.model';
export { Robot, Garage, Settings, Coordinator, Federation };
import Order from './Order.model';
import Slot from './Slot.model';
export { Robot, Garage, Settings, Coordinator, Federation, Order, Slot };
export type { LimitList, Limit, Limits } from './Limit.model';
export type { LimitList, Limit } from './Limit.model';
export type { Exchange } from './Exchange.model';
export type { Maker } from './Maker.model';
export type { Order } from './Order.model';
export type { Book, PublicOrder } from './Book.model';
export type { Slot } from './Garage.model';
export type { Language } from './Settings.model';
export type { Favorites } from './Favorites.model';
export type { Contact, Info, Version, Origin } from './Coordinator.model';

View File

@ -14,20 +14,11 @@ interface FederationWidgetProps {
onTouchEnd?: () => void;
}
const FederationWidget = React.forwardRef(function Component(
{
layout,
gridCellSize,
style,
className,
onMouseDown,
onMouseUp,
onTouchEnd,
}: FederationWidgetProps,
ref,
) {
const { federation, coordinatorUpdatedAt } =
useContext<UseFederationStoreType>(FederationContext);
const FederationWidget = React.forwardRef(function Component({
layout,
gridCellSize,
}: FederationWidgetProps) {
const { federation, federationUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
return React.useMemo(() => {
return (
@ -38,7 +29,7 @@ const FederationWidget = React.forwardRef(function Component(
/>
</Paper>
);
}, [federation, coordinatorUpdatedAt]);
}, [federation, federationUpdatedAt]);
});
export default FederationWidget;

View File

@ -19,7 +19,7 @@ const MakerWidget = React.forwardRef(function Component(
ref,
) {
const { fav } = useContext<UseAppStoreType>(AppContext);
const { coordinatorUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
const { federationUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
const { maker } = useContext<UseGarageStoreType>(GarageContext);
return React.useMemo(() => {
return (
@ -27,7 +27,7 @@ const MakerWidget = React.forwardRef(function Component(
<MakerForm />
</Paper>
);
}, [maker, fav, coordinatorUpdatedAt]);
}, [maker, fav, federationUpdatedAt]);
});
export default MakerWidget;

View File

@ -1,47 +1,4 @@
{
"exp": {
"longAlias": "Experimental",
"shortAlias": "exp",
"description": "RoboSats node for development and experimentation. This is the original RoboSats coordinator operated by the RoboSats devs since 2022.",
"motto": "Original Robohost. P2P FTW!",
"color": "#1976d2",
"established": "2022-03-01",
"contact": {
"email": "robosats@protonmail.com",
"telegram": "robosats",
"twitter": "robosats",
"reddit": "r/robosats",
"matrix": "#robosats:matrix.org",
"website": "https://learn.robosats.com",
"nostr": "npub1p2psats79rypr8lpnl9t5qdekfp700x660qsgw284xvq4s09lqrqqk3m82",
"pgp": "/static/federation/pgp/B4AB5F19113D4125DDF217739C4585B561315571.asc",
"fingerprint": "B4AB5F19113D4125DDF217739C4585B561315571"
},
"badges": {
"isFounder": true,
"donatesToDevFund": 20,
"hasGoodOpSec": true,
"robotsLove": true,
"hasLargeLimits": true
},
"policies": {
"Experimental": "Experimental coordinator used for development. Use at your own risk.",
"Dispute Policy": "Evidence in Disputes: In the event of a dispute, users will be asked to provide transaction-related evidence. This could include transaction IDs, screenshots of payment confirmations, or other pertinent transaction records. Personal information or unrelated transaction details should be redacted to maintain privacy.",
"Non eligible countries": "USA citizens and residents are not allowed to use the Experimental coordinator. F2F transactions are explicitly blocked at creation time for US locations. If a US citizen or resident violates this rule and is found out to be using the Experimental coordinator during a dispute process, they will be denied further service and the dispute mediation will be terminated."
},
"mainnet": {
"onion": "http://robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion",
"clearnet": "https://unsafe.robosats.com",
"i2p": "http://r7r4sckft6ptmk4r2jajiuqbowqyxiwsle4iyg4fijtoordc6z7a.b32.i2p"
},
"testnet": {
"onion": "http://robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion",
"clearnet": "https://unsafe.testnet.robosats.com",
"i2p": ""
},
"mainnetNodesPubkeys": ["0282eb467bc073833a039940392592bf10cf338a830ba4e392c1667d7697654c7e"],
"testnetNodesPubkeys": ["03ecb271b3e2e36f2b91c92c65bab665e5165f8cdfdada1b5f46cfdd3248c87fd6"]
},
"temple": {
"longAlias": "Temple of Sats",
"shortAlias": "temple",