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

View File

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

View File

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

View File

@ -17,15 +17,13 @@ import {
import RobotAvatar from '../../components/RobotAvatar'; import RobotAvatar from '../../components/RobotAvatar';
import { AppContext, type UseAppStoreType, closeAll } from '../../contexts/AppContext'; import { AppContext, type UseAppStoreType, closeAll } from '../../contexts/AppContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext'; import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
const NavBar = (): JSX.Element => { const NavBar = (): JSX.Element => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const { page, setPage, settings, setSlideDirection, open, setOpen, windowSize, navbarHeight } = const { page, setPage, settings, setSlideDirection, open, setOpen, windowSize, navbarHeight } =
useContext<UseAppStoreType>(AppContext); useContext<UseAppStoreType>(AppContext);
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext); const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@ -50,7 +48,7 @@ const NavBar = (): JSX.Element => {
useEffect(() => { useEffect(() => {
// re-render on orde rand robot updated at for latest orderId in tab // re-render on orde rand robot updated at for latest orderId in tab
}, [robotUpdatedAt]); }, [slotUpdatedAt]);
useEffect(() => { useEffect(() => {
// change tab (page) into the current route // change tab (page) into the current route
@ -77,14 +75,10 @@ const NavBar = (): JSX.Element => {
const slot = garage.getSlot(); const slot = garage.getSlot();
handleSlideDirection(page, newPage); handleSlideDirection(page, newPage);
setPage(newPage); setPage(newPage);
const shortAlias = String(slot?.activeShortAlias);
const activeOrderId = slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId; const shortAlias = slot?.activeOrder?.shortAlias;
const lastOrderId = slot?.getRobot(slot?.lastShortAlias ?? '')?.lastOrderId; const orderId = slot?.activeOrder?.id;
const param = const param = newPage === 'order' ? `${String(shortAlias)}/${String(orderId)}` : '';
newPage === 'order' ? `${shortAlias}/${String(activeOrderId ?? lastOrderId)}` : '';
if (newPage === 'order') {
setCurrentOrderId({ id: activeOrderId ?? lastOrderId, shortAlias });
}
setTimeout(() => { setTimeout(() => {
navigate(`/${newPage}/${param}`); navigate(`/${newPage}/${param}`);
}, theme.transitions.duration.leavingScreen * 3); }, theme.transitions.duration.leavingScreen * 3);
@ -162,7 +156,7 @@ const NavBar = (): JSX.Element => {
sx={tabSx} sx={tabSx}
label={smallBar ? undefined : t('Order')} label={smallBar ? undefined : t('Order')}
value='order' value='order'
disabled={!slot?.getRobot()?.activeOrderId} disabled={!slot?.activeOrder}
icon={<Assignment />} icon={<Assignment />}
iconPosition='start' 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 { useTranslation } from 'react-i18next';
import { Tab, Tabs, Paper, CircularProgress, Grid, Typography, Box } from '@mui/material'; import { Tab, Tabs, Paper, CircularProgress, Grid, Typography, Box } from '@mui/material';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
@ -6,60 +6,59 @@ import { useNavigate, useParams } from 'react-router-dom';
import TradeBox from '../../components/TradeBox'; import TradeBox from '../../components/TradeBox';
import OrderDetails from '../../components/OrderDetails'; 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 { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { WarningDialog } from '../../components/Dialogs'; import { WarningDialog } from '../../components/Dialogs';
import { Order, type Slot } from '../../models';
import { type UseGarageStoreType, GarageContext } from '../../contexts/GarageContext';
const OrderPage = (): JSX.Element => { const OrderPage = (): JSX.Element => {
const { const { windowSize, setOpen, acknowledgedWarning, setAcknowledgedWarning, navbarHeight } =
windowSize, useContext<UseAppStoreType>(AppContext);
open, const { federation } = useContext<UseFederationStoreType>(FederationContext);
setOpen, const { garage } = useContext<UseGarageStoreType>(GarageContext);
acknowledgedWarning,
setAcknowledgedWarning,
settings,
navbarHeight,
hostUrl,
origin,
} = useContext<UseAppStoreType>(AppContext);
const { federation, currentOrder, currentOrderId, setCurrentOrderId } =
useContext<UseFederationStoreType>(FederationContext);
const { badOrder } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const params = useParams(); const params = useParams();
const paramsRef = useRef(params);
const doublePageWidth: number = 50; const doublePageWidth: number = 50;
const maxHeight: number = (windowSize?.height - navbarHeight) * 0.85 - 3; const maxHeight: number = (windowSize?.height - navbarHeight) * 0.85 - 3;
const [tab, setTab] = useState<'order' | 'contract'>('contract'); const [tab, setTab] = useState<'order' | 'contract'>('contract');
const [baseUrl, setBaseUrl] = useState<string>(hostUrl); const [currentOrder, setCurrentOrder] = useState<Order | null>(null);
useEffect(() => { useEffect(() => {
paramsRef.current = params;
const shortAlias = params.shortAlias; const shortAlias = params.shortAlias;
const coordinator = federation.getCoordinator(shortAlias ?? ''); const orderId = Number(params.orderId);
if (coordinator) { const slot = garage.getSlot();
const endpoint = coordinator?.getEndpoint( if (slot?.token) {
settings.network, let order = new Order({ id: orderId, shortAlias });
origin, if (slot.activeOrder?.id === orderId && slot.activeOrder?.shortAlias === shortAlias) {
settings.selfhostedClient, order = slot.activeOrder;
hostUrl, } else if (slot.lastOrder?.id === orderId && slot.lastOrder?.shortAlias === shortAlias) {
); order = slot.lastOrder;
}
if (endpoint) setBaseUrl(`${endpoint?.url}${endpoint?.basePath}`); void order.fecth(federation, slot).then((updatedOrder) => {
updateSlotFromOrder(updatedOrder, slot);
const orderId = Number(params.orderId); });
if (
orderId &&
currentOrderId.id !== orderId &&
currentOrderId.shortAlias !== shortAlias &&
shortAlias
)
setCurrentOrderId({ id: orderId, shortAlias });
if (!acknowledgedWarning) setOpen({ ...closeAll, warning: true });
} }
}, [params, currentOrderId]);
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 { const onClickCoordinator = function (): void {
if (currentOrder?.shortAlias != null) { if (currentOrder?.shortAlias != null) {
@ -87,7 +86,7 @@ const OrderPage = (): JSX.Element => {
); );
const tradeBoxSpace = currentOrder ? ( const tradeBoxSpace = currentOrder ? (
<TradeBox baseUrl={baseUrl} onStartAgain={startAgain} /> <TradeBox onStartAgain={startAgain} currentOrder={currentOrder} />
) : ( ) : (
<></> <></>
); );
@ -95,20 +94,19 @@ const OrderPage = (): JSX.Element => {
return ( return (
<Box> <Box>
<WarningDialog <WarningDialog
open={open.warning} open={!acknowledgedWarning && currentOrder?.status === 0}
onClose={() => { onClose={() => {
setOpen(closeAll);
setAcknowledgedWarning(true); setAcknowledgedWarning(true);
}} }}
longAlias={federation.getCoordinator(params.shortAlias ?? '')?.longAlias} longAlias={federation.getCoordinator(params.shortAlias ?? '')?.longAlias}
/> />
{currentOrder === null && badOrder === undefined && <CircularProgress />} {!currentOrder?.maker_hash_id && <CircularProgress />}
{badOrder !== undefined ? ( {currentOrder?.bad_request && currentOrder.status !== 5 ? (
<Typography align='center' variant='subtitle2' color='secondary'> <Typography align='center' variant='subtitle2' color='secondary'>
{t(badOrder)} {t(currentOrder.bad_request)}
</Typography> </Typography>
) : null} ) : null}
{currentOrder !== null && badOrder === undefined ? ( {currentOrder?.maker_hash_id && (!currentOrder.bad_request || currentOrder.status === 5) ? (
currentOrder.is_participant ? ( currentOrder.is_participant ? (
windowSize.width > doublePageWidth ? ( windowSize.width > doublePageWidth ? (
// DOUBLE PAPER VIEW // DOUBLE PAPER VIEW

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -76,7 +76,11 @@ const makeTheme = function (settings: Settings): Theme {
}; };
const getHostUrl = (network = 'mainnet'): string => { 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:'; let protocol = 'http:';
if (window.NativeRobosats === undefined) { if (window.NativeRobosats === undefined) {
host = getHost(); host = getHost();

View File

@ -8,7 +8,7 @@ import React, {
type ReactNode, type ReactNode,
} from 'react'; } from 'react';
import { type Order, Federation, Settings } from '../models'; import { Federation, Settings } from '../models';
import { federationLottery } from '../utils'; import { federationLottery } from '../utils';
@ -16,30 +16,6 @@ import { AppContext, type UseAppStoreType } from './AppContext';
import { GarageContext, type UseGarageStoreType } from './GarageContext'; import { GarageContext, type UseGarageStoreType } from './GarageContext';
import { type Origin, type Origins } from '../models/Coordinator.model'; 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 { export interface CurrentOrderIdProps {
id: number | null; id: number | null;
shortAlias: string | null; shortAlias: string | null;
@ -52,10 +28,6 @@ export interface FederationContextProviderProps {
export interface UseFederationStoreType { export interface UseFederationStoreType {
federation: Federation; federation: Federation;
sortedCoordinators: string[]; sortedCoordinators: string[];
setDelay: Dispatch<SetStateAction<number>>;
currentOrderId: CurrentOrderIdProps;
setCurrentOrderId: Dispatch<SetStateAction<CurrentOrderIdProps>>;
currentOrder: Order | null;
coordinatorUpdatedAt: string; coordinatorUpdatedAt: string;
federationUpdatedAt: string; federationUpdatedAt: string;
addNewCoordinator: (alias: string, url: string) => void; addNewCoordinator: (alias: string, url: string) => void;
@ -64,10 +36,6 @@ export interface UseFederationStoreType {
export const initialFederationContext: UseFederationStoreType = { export const initialFederationContext: UseFederationStoreType = {
federation: new Federation('onion', new Settings(), ''), federation: new Federation('onion', new Settings(), ''),
sortedCoordinators: [], sortedCoordinators: [],
setDelay: () => {},
currentOrderId: { id: null, shortAlias: null },
setCurrentOrderId: () => {},
currentOrder: null,
coordinatorUpdatedAt: '', coordinatorUpdatedAt: '',
federationUpdatedAt: '', federationUpdatedAt: '',
addNewCoordinator: () => {}, addNewCoordinator: () => {},
@ -80,24 +48,13 @@ export const FederationContextProvider = ({
}: FederationContextProviderProps): JSX.Element => { }: FederationContextProviderProps): JSX.Element => {
const { settings, page, origin, hostUrl, open, torStatus } = const { settings, page, origin, hostUrl, open, torStatus } =
useContext<UseAppStoreType>(AppContext); 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 [federation] = useState(new Federation(origin, settings, hostUrl));
const [sortedCoordinators, setSortedCoordinators] = useState(federationLottery(federation)); const [sortedCoordinators, setSortedCoordinators] = useState(federationLottery(federation));
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>( const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>(
new Date().toISOString(), new Date().toISOString(),
); );
const [federationUpdatedAt, setFederationUpdatedAt] = 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(() => { useEffect(() => {
setMaker((maker) => { setMaker((maker) => {
@ -106,65 +63,15 @@ export const FederationContextProvider = ({
federation.registerHook('onFederationUpdate', () => { federation.registerHook('onFederationUpdate', () => {
setFederationUpdatedAt(new Date().toISOString()); setFederationUpdatedAt(new Date().toISOString());
}); });
federation.registerHook('onCoordinatorUpdate', () => {
setCoordinatorUpdatedAt(new Date().toISOString());
});
}, []); }, []);
useEffect(() => { useEffect(() => {
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) { if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
void federation.updateUrl(origin, settings, hostUrl); void federation.updateUrl(origin, settings, hostUrl);
void federation.update(); void federation.update();
const token = garage.getSlot()?.getRobot()?.token;
if (token) void federation.fetchRobot(garage, token);
} }
}, [settings.network, settings.useProxy, torStatus]); }, [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) => { const addNewCoordinator: (alias: string, url: string) => void = (alias, url) => {
if (!federation.coordinators[alias]) { if (!federation.coordinators[alias]) {
const attributes: Record<any, any> = { const attributes: Record<any, any> = {
@ -188,24 +95,12 @@ export const FederationContextProvider = ({
newCoordinator.update(() => { newCoordinator.update(() => {
setCoordinatorUpdatedAt(new Date().toISOString()); setCoordinatorUpdatedAt(new Date().toISOString());
}); });
garage.syncCoordinator(newCoordinator); garage.syncCoordinator(federation, alias);
setSortedCoordinators(federationLottery(federation)); setSortedCoordinators(federationLottery(federation));
setFederationUpdatedAt(new Date().toISOString()); setFederationUpdatedAt(new Date().toISOString());
} }
}; };
useEffect(() => {
if (currentOrderId.id && currentOrderId.shortAlias) {
setCurrentOrder(null);
setBadOrder(undefined);
clearInterval(timer);
fetchCurrentOrder();
}
return () => {
clearInterval(timer);
};
}, [currentOrderId]);
useEffect(() => { useEffect(() => {
if (page === 'offers') void federation.updateBook(); if (page === 'offers') void federation.updateBook();
}, [page]); }, [page]);
@ -215,7 +110,7 @@ export const FederationContextProvider = ({
const slot = garage.getSlot(); const slot = garage.getSlot();
if (open.profile && slot?.hashId && slot?.token) { 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]); }, [open.profile]);
@ -224,10 +119,6 @@ export const FederationContextProvider = ({
value={{ value={{
federation, federation,
sortedCoordinators, sortedCoordinators,
currentOrderId,
setCurrentOrderId,
currentOrder,
setDelay,
coordinatorUpdatedAt, coordinatorUpdatedAt,
federationUpdatedAt, federationUpdatedAt,
addNewCoordinator, addNewCoordinator,

View File

@ -5,10 +5,14 @@ import React, {
type SetStateAction, type SetStateAction,
useEffect, useEffect,
type ReactNode, type ReactNode,
useContext,
useRef,
} from 'react'; } from 'react';
import { defaultMaker, type Maker, Garage } from '../models'; import { defaultMaker, type Maker, Garage } from '../models';
import { systemClient } from '../services/System'; import { systemClient } from '../services/System';
import { type UseAppStoreType, AppContext } from './AppContext';
import { type UseFederationStoreType, FederationContext } from './FederationContext';
export interface GarageContextProviderProps { export interface GarageContextProviderProps {
children: ReactNode; children: ReactNode;
@ -18,61 +22,138 @@ export interface UseGarageStoreType {
garage: Garage; garage: Garage;
maker: Maker; maker: Maker;
setMaker: Dispatch<SetStateAction<Maker>>; setMaker: Dispatch<SetStateAction<Maker>>;
badOrder?: string; setDelay: Dispatch<SetStateAction<number>>;
setBadOrder: Dispatch<SetStateAction<string | undefined>>; fetchSlotActiveOrder: () => void;
robotUpdatedAt: string; slotUpdatedAt: string;
orderUpdatedAt: string;
} }
export const initialGarageContext: UseGarageStoreType = { export const initialGarageContext: UseGarageStoreType = {
garage: new Garage(), garage: new Garage(),
maker: defaultMaker, maker: defaultMaker,
setMaker: () => {}, setMaker: () => {},
badOrder: undefined, setDelay: () => {},
setBadOrder: () => {}, fetchSlotActiveOrder: () => {},
robotUpdatedAt: '', slotUpdatedAt: '',
orderUpdatedAt: '',
}; };
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 GarageContext = createContext<UseGarageStoreType>(initialGarageContext);
export const GarageContextProvider = ({ children }: GarageContextProviderProps): JSX.Element => { export const GarageContextProvider = ({ children }: GarageContextProviderProps): JSX.Element => {
// All garage data structured // 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 [garage] = useState<Garage>(initialGarageContext.garage);
const [maker, setMaker] = useState<Maker>(initialGarageContext.maker); const [maker, setMaker] = useState<Maker>(initialGarageContext.maker);
const [badOrder, setBadOrder] = useState<string>(); const [slotUpdatedAt, setSlotUpdatedAt] = useState<string>(new Date().toISOString());
const [robotUpdatedAt, setRobotUpdatedAt] = useState<string>(new Date().toISOString()); const [lastOrderCheckAt] = useState<number>(+new Date());
const [orderUpdatedAt, setOrderUpdatedAt] = useState<string>(new Date().toISOString()); const lastOrderCheckAtRef = useRef(lastOrderCheckAt);
const [delay, setDelay] = useState<number>(defaultDelay);
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(() =>
setInterval(() => null, delay),
);
const onRobotUpdated = (): void => { const onSlotUpdated = (): void => {
setRobotUpdatedAt(new Date().toISOString()); setSlotUpdatedAt(new Date().toISOString());
};
const onOrderUpdate = (): void => {
setOrderUpdatedAt(new Date().toISOString());
}; };
useEffect(() => { useEffect(() => {
garage.registerHook('onRobotUpdate', onRobotUpdated); setMaker((maker) => {
garage.registerHook('onOrderUpdate', onOrderUpdate); 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(() => { useEffect(() => {
if (window.NativeRobosats !== undefined && !systemClient.loading) { if (window.NativeRobosats !== undefined && !systemClient.loading) {
garage.loadSlots(); garage.loadSlots();
} }
}, [systemClient.loading]); }, [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 ( return (
<GarageContext.Provider <GarageContext.Provider
value={{ value={{
garage, garage,
maker, maker,
setMaker, setMaker,
badOrder, setDelay,
setBadOrder, fetchSlotActiveOrder,
robotUpdatedAt, slotUpdatedAt,
orderUpdatedAt,
}} }}
> >
{children} {children}

View File

@ -1,16 +1,7 @@
import { import { type LimitList, type PublicOrder, type Settings } from '.';
type Robot,
type LimitList,
type PublicOrder,
type Settings,
type Order,
type Garage,
} from '.';
import { roboidentitiesClient } from '../services/Roboidentities/Web'; import { roboidentitiesClient } from '../services/Roboidentities/Web';
import { apiClient } from '../services/api'; import { apiClient } from '../services/api';
import { validateTokenEntropy } from '../utils';
import { compareUpdateLimit } from './Limit.model'; import { compareUpdateLimit } from './Limit.model';
import { defaultOrder } from './Order.model';
export interface Contact { export interface Contact {
nostr?: string | undefined; nostr?: string | undefined;
@ -180,7 +171,6 @@ export class Coordinator {
public loadingInfo: boolean = false; public loadingInfo: boolean = false;
public limits: LimitList = {}; public limits: LimitList = {};
public loadingLimits: boolean = false; public loadingLimits: boolean = false;
public loadingRobot: string | null;
updateUrl = (origin: Origin, settings: Settings, hostUrl: string): void => { updateUrl = (origin: Origin, settings: Settings, hostUrl: string): void => {
if (settings.selfhostedClient && this.shortAlias !== 'local') { if (settings.selfhostedClient && this.shortAlias !== 'local') {
@ -331,132 +321,6 @@ export class Coordinator {
return { url: String(this[network][origin]), basePath: '' }; 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; export default Coordinator;

View File

@ -1,7 +1,6 @@
import { import {
Coordinator, Coordinator,
type Exchange, type Exchange,
type Garage,
type Origin, type Origin,
type PublicOrder, type PublicOrder,
type Settings, type Settings,
@ -13,7 +12,7 @@ import { getHost } from '../utils';
import { coordinatorDefaultValues } from './Coordinator.model'; import { coordinatorDefaultValues } from './Coordinator.model';
import { updateExchangeInfo } from './Exchange.model'; import { updateExchangeInfo } from './Exchange.model';
type FederationHooks = 'onCoordinatorUpdate' | 'onFederationUpdate'; type FederationHooks = 'onFederationUpdate';
export class Federation { export class Federation {
constructor(origin: Origin, settings: Settings, hostUrl: string) { constructor(origin: Origin, settings: Settings, hostUrl: string) {
@ -36,7 +35,6 @@ export class Federation {
}; };
this.book = []; this.book = [];
this.hooks = { this.hooks = {
onCoordinatorUpdate: [],
onFederationUpdate: [], onFederationUpdate: [],
}; };
@ -97,7 +95,6 @@ export class Federation {
this.book = Object.values(this.coordinators).reduce<PublicOrder[]>((array, coordinator) => { this.book = Object.values(this.coordinators).reduce<PublicOrder[]>((array, coordinator) => {
return [...array, ...coordinator.book]; return [...array, ...coordinator.book];
}, []); }, []);
this.triggerHook('onCoordinatorUpdate');
this.exchange.loadingCoordinators = this.exchange.loadingCoordinators =
this.exchange.loadingCoordinators < 1 ? 0 : this.exchange.loadingCoordinators - 1; this.exchange.loadingCoordinators < 1 ? 0 : this.exchange.loadingCoordinators - 1;
this.loading = this.exchange.loadingCoordinators > 0; this.loading = this.exchange.loadingCoordinators > 0;
@ -140,11 +137,12 @@ export class Federation {
updateBook = async (): Promise<void> => { updateBook = async (): Promise<void> => {
this.loading = true; this.loading = true;
this.book = []; this.book = [];
this.triggerHook('onCoordinatorUpdate'); this.triggerHook('onFederationUpdate');
this.exchange.loadingCoordinators = Object.keys(this.coordinators).length; this.exchange.loadingCoordinators = Object.keys(this.coordinators).length;
for (const coor of Object.values(this.coordinators)) { for (const coor of Object.values(this.coordinators)) {
void coor.updateBook(() => { void coor.updateBook(() => {
this.onCoordinatorSaved(); this.onCoordinatorSaved();
this.triggerHook('onFederationUpdate');
}); });
} }
}; };
@ -154,13 +152,6 @@ export class Federation {
this.triggerHook('onFederationUpdate'); this.triggerHook('onFederationUpdate');
}; };
// Fetchs
fetchRobot = async (garage: Garage, token: string): Promise<void> => {
Object.values(this.coordinators).forEach((coor) => {
void coor.fetchRobot(garage, token);
});
};
// Coordinators // Coordinators
getCoordinator = (shortAlias: string): Coordinator => { getCoordinator = (shortAlias: string): Coordinator => {
return this.coordinators[shortAlias]; return this.coordinators[shortAlias];
@ -169,13 +160,13 @@ export class Federation {
disableCoordinator = (shortAlias: string): void => { disableCoordinator = (shortAlias: string): void => {
this.coordinators[shortAlias].disable(); this.coordinators[shortAlias].disable();
this.updateEnabledCoordinators(); this.updateEnabledCoordinators();
this.triggerHook('onCoordinatorUpdate'); this.triggerHook('onFederationUpdate');
}; };
enableCoordinator = (shortAlias: string): void => { enableCoordinator = (shortAlias: string): void => {
this.coordinators[shortAlias].enable(() => { this.coordinators[shortAlias].enable(() => {
this.updateEnabledCoordinators(); 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 { systemClient } from '../services/System';
import { saveAsJson } from '../utils'; import { saveAsJson } from '../utils';
import Slot from './Slot.model'; import Slot from './Slot.model';
type GarageHooks = 'onRobotUpdate' | 'onOrderUpdate'; type GarageHooks = 'onSlotUpdate';
class Garage { class Garage {
constructor() { constructor() {
@ -11,8 +11,7 @@ class Garage {
this.currentSlot = null; this.currentSlot = null;
this.hooks = { this.hooks = {
onRobotUpdate: [], onSlotUpdate: [],
onOrderUpdate: [],
}; };
this.loadSlots(); this.loadSlots();
@ -29,6 +28,7 @@ class Garage {
}; };
triggerHook = (hookName: GarageHooks): void => { triggerHook = (hookName: GarageHooks): void => {
this.save();
this.hooks[hookName]?.forEach((fn) => { this.hooks[hookName]?.forEach((fn) => {
fn(); fn();
}); });
@ -47,8 +47,7 @@ class Garage {
this.slots = {}; this.slots = {};
this.currentSlot = null; this.currentSlot = null;
systemClient.deleteItem('garage_slots'); systemClient.deleteItem('garage_slots');
this.triggerHook('onRobotUpdate'); this.triggerHook('onSlotUpdate');
this.triggerHook('onOrderUpdate');
}; };
loadSlots = (): void => { loadSlots = (): void => {
@ -64,26 +63,23 @@ class Garage {
Object.keys(rawSlot.robots), Object.keys(rawSlot.robots),
{}, {},
() => { () => {
this.triggerHook('onRobotUpdate'); this.triggerHook('onSlotUpdate');
}, },
); );
Object.keys(rawSlot.robots).forEach((shortAlias) => { this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.lastOrder));
const rawRobot = rawSlot.robots[shortAlias]; this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.activeOrder));
this.updateRobot(rawSlot.token, shortAlias, rawRobot);
});
this.currentSlot = rawSlot?.token; this.currentSlot = rawSlot?.token;
} }
}); });
console.log('Robot Garage was loaded from local storage'); console.log('Robot Garage was loaded from local storage');
this.triggerHook('onRobotUpdate'); this.triggerHook('onSlotUpdate');
this.triggerHook('onOrderUpdate');
} }
}; };
// Slots // Slots
getSlot: (token?: string) => Slot | null = (token) => { getSlot: (token?: string) => Slot | null = (token) => {
const currentToken = token ?? this.currentSlot; const currentToken = token ?? this.currentSlot;
return currentToken ? this.slots[currentToken] ?? null : null; return currentToken ? (this.slots[currentToken] ?? null) : null;
}; };
deleteSlot: (token?: string) => void = (token) => { deleteSlot: (token?: string) => void = (token) => {
@ -92,8 +88,7 @@ class Garage {
Reflect.deleteProperty(this.slots, targetIndex); Reflect.deleteProperty(this.slots, targetIndex);
this.currentSlot = null; this.currentSlot = null;
this.save(); this.save();
this.triggerHook('onRobotUpdate'); this.triggerHook('onSlotUpdate');
this.triggerHook('onOrderUpdate');
} }
}; };
@ -105,7 +100,7 @@ class Garage {
if (attributes) { if (attributes) {
if (attributes.copiedToken !== undefined) slot?.setCopiedToken(attributes.copiedToken); if (attributes.copiedToken !== undefined) slot?.setCopiedToken(attributes.copiedToken);
this.save(); this.save();
this.triggerHook('onRobotUpdate'); this.triggerHook('onSlotUpdate');
} }
return slot; return slot;
}; };
@ -113,7 +108,7 @@ class Garage {
setCurrentSlot: (currentSlot: string) => void = (currentSlot) => { setCurrentSlot: (currentSlot: string) => void = (currentSlot) => {
this.currentSlot = currentSlot; this.currentSlot = currentSlot;
this.save(); this.save();
this.triggerHook('onRobotUpdate'); this.triggerHook('onSlotUpdate');
}; };
getSlotByOrder: (coordinator: string, orderID: number) => Slot | null = ( getSlotByOrder: (coordinator: string, orderID: number) => Slot | null = (
@ -123,7 +118,7 @@ class Garage {
return ( return (
Object.values(this.slots).find((slot) => { Object.values(this.slots).find((slot) => {
const robot = slot.getRobot(coordinator); const robot = slot.getRobot(coordinator);
return slot.activeShortAlias === coordinator && robot?.activeOrderId === orderID; return slot.activeOrder?.shortAlias === coordinator && robot?.activeOrderId === orderID;
}) ?? null }) ?? null
); );
}; };
@ -138,55 +133,29 @@ class Garage {
if (this.getSlot(token) === null) { if (this.getSlot(token) === null) {
this.slots[token] = new Slot(token, shortAliases, attributes, () => { this.slots[token] = new Slot(token, shortAliases, attributes, () => {
this.triggerHook('onRobotUpdate'); this.triggerHook('onSlotUpdate');
}); });
this.save(); this.save();
} }
}; };
updateRobot: (token: string, shortAlias: string, attributes: Record<any, any>) => void = ( fetchRobot = async (federation: Federation, token: string): Promise<void> => {
token,
shortAlias,
attributes,
) => {
if (!token || !shortAlias) return;
const slot = this.getSlot(token); const slot = this.getSlot(token);
if (slot != null) { if (slot != null) {
slot.updateRobot(shortAlias, { token, ...attributes }); await slot.fetchRobot(federation);
this.save(); this.save();
this.triggerHook('onRobotUpdate'); this.triggerHook('onSlotUpdate');
}
};
// 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');
} }
}; };
// Coordinators // Coordinators
syncCoordinator: (coordinator: Coordinator) => void = (coordinator) => { syncCoordinator: (federation: Federation, shortAlias: string) => void = (
federation,
shortAlias,
) => {
Object.values(this.slots).forEach((slot) => { Object.values(this.slots).forEach((slot) => {
slot.syncCoordinator(coordinator, this); slot.syncCoordinator(federation, shortAlias);
}); });
this.save(); this.save();
}; };

View File

@ -1,3 +1,5 @@
import defaultFederation from '../../static/federation.json';
export interface Maker { export interface Maker {
advancedOptions: boolean; advancedOptions: boolean;
coordinator: string; coordinator: string;
@ -23,7 +25,10 @@ export interface Maker {
export const defaultMaker: Maker = { export const defaultMaker: Maker = {
advancedOptions: false, advancedOptions: false,
coordinator: 'exp', coordinator:
Object.keys(defaultFederation)[
Math.floor(Math.random() * Object.keys(defaultFederation).length)
] ?? '',
isExplicit: false, isExplicit: false,
amount: '', amount: '',
paymentMethods: [], paymentMethods: [],
@ -40,6 +45,8 @@ export const defaultMaker: Maker = {
maxAmount: '', maxAmount: '',
badPremiumText: '', badPremiumText: '',
badSatoshisText: '', badSatoshisText: '',
latitude: 0,
longitude: 0,
}; };
export default Maker; 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 { export interface TradeRobotSummary {
is_buyer: boolean; is_buyer: boolean;
sent_fiat: number; sent_fiat: number;
@ -24,168 +49,82 @@ export interface TradeCoordinatorSummary {
trade_revenue_sats: number; trade_revenue_sats: number;
} }
export interface Order { class Order {
id: number; constructor(attributes: object) {
status: number; Object.assign(this, attributes);
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;
}
export const defaultOrder: Order = { id: number = 0;
shortAlias: '', status: number = 0;
id: 0, created_at: Date = new Date();
status: 0, expires_at: Date = new Date();
created_at: new Date(), type: number = 0;
expires_at: new Date(), currency: number = 0;
type: 0, amount: number = 0;
currency: 0, has_range: boolean = false;
amount: 0, min_amount: number = 0;
has_range: false, max_amount: number = 0;
min_amount: 0, payment_method: string = '';
max_amount: 0, is_explicit: boolean = false;
payment_method: '', premium: number = 0;
is_explicit: false, satoshis: number = 0;
premium: 0, maker: number = 0;
satoshis: 0, taker: number = 0;
maker: 0, escrow_duration: number = 0;
taker: 0, total_secs_exp: number = 0;
escrow_duration: 0, penalty: Date | undefined = undefined;
total_secs_exp: 0, is_maker: boolean = false;
penalty: undefined, is_taker: boolean = false;
is_maker: false, is_participant: boolean = false;
is_taker: false, maker_status: 'Active' | 'Seen recently' | 'Inactive' = 'Active';
is_participant: false, taker_status: 'Active' | 'Seen recently' | 'Inactive' = 'Active';
maker_status: 'Active', price_now: number | undefined = undefined;
taker_status: 'Active', satoshis_now: number = 0;
price_now: undefined, latitude: number = 0;
satoshis_now: 0, longitude: number = 0;
latitude: 0, premium_now: number | undefined = undefined;
longitude: 0, premium_percentile: number = 0;
premium_now: undefined, num_similar_orders: number = 0;
premium_percentile: 0, tg_enabled: boolean = false; // deprecated
num_similar_orders: 0, tg_token: string = '';
tg_enabled: false, tg_bot_name: string = '';
tg_token: '', is_buyer: boolean = false;
tg_bot_name: '', is_seller: boolean = false;
is_buyer: false, maker_nick: string = '';
is_seller: false, maker_hash_id: string = '';
maker_nick: '', taker_nick: string = '';
maker_hash_id: '', taker_hash_id: string = '';
taker_nick: '', status_message: string = '';
taker_hash_id: '', is_fiat_sent: boolean = false;
status_message: '', is_disputed: boolean = false;
is_fiat_sent: false, ur_nick: string = '';
is_disputed: false, maker_locked: boolean = false;
ur_nick: '', taker_locked: boolean = false;
maker_locked: false, escrow_locked: boolean = false;
taker_locked: false, trade_satoshis: number = 0;
escrow_locked: false, bond_invoice: string = '';
trade_satoshis: 0, bond_satoshis: number = 0;
bond_invoice: '', escrow_invoice: string = '';
bond_satoshis: 0, escrow_satoshis: number = 0;
escrow_invoice: '', invoice_amount: number = 0;
escrow_satoshis: 0, swap_allowed: boolean = false;
invoice_amount: 0, swap_failure_reason: string = '';
swap_allowed: false, suggested_mining_fee_rate: number = 0;
swap_failure_reason: '', swap_fee_rate: number = 0;
suggested_mining_fee_rate: 0, pending_cancel: boolean = false;
swap_fee_rate: 0, asked_for_cancel: boolean = false;
pending_cancel: false, statement_submitted: boolean = false;
asked_for_cancel: false, retries: number = 0;
statement_submitted: false, next_retry_time: Date = new Date();
retries: 0, failure_reason: string = '';
next_retry_time: new Date(), invoice_expired: boolean = false;
failure_reason: '', public_duration: number = 0;
invoice_expired: false, bond_size: string = '';
public_duration: 0, trade_fee_percent: number = 0;
bond_size: '', bond_size_sats: number = 0;
trade_fee_percent: 0, bond_size_percent: number = 0;
bond_size_sats: 0, chat_last_index: number = 0;
bond_size_percent: 0, maker_summary: TradeRobotSummary = {
chat_last_index: 0,
maker_summary: {
is_buyer: false, is_buyer: false,
sent_fiat: 0, sent_fiat: 0,
received_sats: 0, received_sats: 0,
@ -197,8 +136,9 @@ export const defaultOrder: Order = {
sent_sats: 0, sent_sats: 0,
received_fiat: 0, received_fiat: 0,
trade_fee_sats: 0, trade_fee_sats: 0,
}, };
taker_summary: {
taker_summary: TradeRobotSummary = {
is_buyer: false, is_buyer: false,
sent_fiat: 0, sent_fiat: 0,
received_sats: 0, received_sats: 0,
@ -210,22 +150,118 @@ export const defaultOrder: Order = {
sent_sats: 0, sent_sats: 0,
received_fiat: 0, received_fiat: 0,
trade_fee_sats: 0, trade_fee_sats: 0,
}, };
platform_summary: {
platform_summary: TradeCoordinatorSummary = {
contract_timestamp: new Date(), contract_timestamp: new Date(),
contract_total_time: 0, contract_total_time: 0,
contract_exchange_rate: 0, contract_exchange_rate: 0,
routing_budget_sats: 0, routing_budget_sats: 0,
trade_revenue_sats: 0, trade_revenue_sats: 0,
}, };
expiry_reason: 0,
expiry_message: '', expiry_reason: number = 0;
num_satoshis: 0, expiry_message: string = '';
sent_satoshis: 0, num_satoshis: number = 0;
txid: '', sent_satoshis: number = 0;
tx_queued: false, txid: string = '';
address: '', tx_queued: boolean = false;
network: 'mainnet', 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; export default Order;

View File

@ -1,29 +1,13 @@
import { sha256 } from 'js-sha256'; import { apiClient } from '../services/api';
import { hexToBase91 } from '../utils'; import type Federation from './Federation.model';
import { type AuthHeaders } from './Slot.model';
interface AuthHeaders {
tokenSHA256: string;
keys: {
pubKey: string;
encPrivKey: string;
};
}
class Robot { class Robot {
constructor(attributes?: Record<any, any>) { constructor(attributes?: Record<any, any>) {
if (attributes != null) { Object.assign(this, attributes);
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;
}
} }
public token?: string; public token?: string;
public bitsEntropy?: number;
public shannonEntropy?: number;
public tokenSHA256: string = '';
public pubKey?: string; public pubKey?: string;
public encPrivKey?: string; public encPrivKey?: string;
public stealthInvoices: boolean = true; public stealthInvoices: boolean = true;
@ -37,6 +21,10 @@ class Robot {
public found: boolean = false; public found: boolean = false;
public last_login: string = ''; public last_login: string = '';
public shortAlias: string = ''; public shortAlias: string = '';
public bitsEntropy?: number;
public shannonEntropy?: number;
public tokenSHA256: string = '';
public hasEnoughEntropy: boolean = false;
update = (attributes: Record<string, any>): void => { update = (attributes: Record<string, any>): void => {
Object.assign(this, attributes); 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; export default Robot;

View File

@ -1,88 +1,170 @@
import { sha256 } from 'js-sha256'; 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 { roboidentitiesClient } from '../services/Roboidentities/Web';
import { hexToBase91, validateTokenEntropy } from '../utils';
export interface AuthHeaders {
tokenSHA256: string;
keys: {
pubKey: string;
encPrivKey: string;
};
}
class Slot { class Slot {
constructor( constructor(
token: string, token: string,
shortAliases: string[], shortAliases: string[],
robotAttributes: Record<any, any>, robotAttributes: Record<any, any>,
onRobotUpdate: () => void, onSlotUpdate: () => void,
) { ) {
this.onSlotUpdate = onSlotUpdate;
this.token = token; this.token = token;
this.hashId = sha256(sha256(this.token)); this.hashId = sha256(sha256(this.token));
this.nickname = null; this.nickname = null;
void roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => { void roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => {
this.nickname = nickname; this.nickname = nickname;
onRobotUpdate(); onSlotUpdate();
}); });
void roboidentitiesClient.generateRobohash(this.hashId, 'small'); void roboidentitiesClient.generateRobohash(this.hashId, 'small');
void roboidentitiesClient.generateRobohash(this.hashId, 'large'); 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) => { 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; return acc;
}, {}); }, {});
this.order = null;
this.activeShortAlias = null;
this.lastShortAlias = null;
this.copiedToken = false; this.copiedToken = false;
onRobotUpdate(); this.onSlotUpdate();
} }
token: string | null; token: string | null;
hashId: string | null; hashId: string | null;
nickname: string | null; nickname: string | null;
robots: Record<string, Robot>; robots: Record<string, Robot>;
order: Order | null; activeOrder: Order | null = null;
activeShortAlias: string | null; lastOrder: Order | null = null;
lastShortAlias: string | null;
copiedToken: boolean; copiedToken: boolean;
onSlotUpdate: () => void;
setCopiedToken = (copied: boolean): void => { setCopiedToken = (copied: boolean): void => {
this.copiedToken = copied; this.copiedToken = copied;
}; };
// Robots
getRobot = (shortAlias?: string): Robot | null => { getRobot = (shortAlias?: string): Robot | null => {
if (shortAlias) { if (shortAlias) {
return this.robots[shortAlias]; return this.robots[shortAlias];
} else if (this.activeShortAlias !== null && this.robots[this.activeShortAlias]) { } else if (this.activeOrder?.id) {
return this.robots[this.activeShortAlias]; return this.robots[this.activeOrder.shortAlias];
} else if (this.lastShortAlias !== null && this.robots[this.lastShortAlias]) { } else if (this.lastOrder?.id && this.robots[this.lastOrder.shortAlias]) {
return this.robots[this.lastShortAlias]; return this.robots[this.lastOrder.shortAlias];
} else if (Object.values(this.robots).length > 0) { } else if (Object.values(this.robots).length > 0) {
return Object.values(this.robots)[0]; return Object.values(this.robots)[0];
} }
return null; return null;
}; };
updateRobot = (shortAlias: string, attributes: Record<any, any>): Robot | null => { fetchRobot = async (federation: Federation): Promise<void> => {
this.robots[shortAlias].update(attributes); Object.values(this.robots).forEach((robot) => {
void robot.fetch(federation).then((robot) => {
if (attributes.lastOrderId) { this.updateSlotFromRobot(robot);
this.lastShortAlias = shortAlias; });
if (this.activeShortAlias === shortAlias) { });
this.activeShortAlias = null;
}
}
if (attributes.activeOrderId) {
this.activeShortAlias = attributes.shortAlias;
}
return this.robots[shortAlias];
}; };
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(); const defaultRobot = this.getRobot();
if (defaultRobot?.token) { 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, token: defaultRobot.token,
pubKey: defaultRobot.pubKey, pubKey: defaultRobot.pubKey,
encPrivKey: defaultRobot.encPrivKey, 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 Settings from './Settings.default.basic';
import Coordinator from './Coordinator.model'; import Coordinator from './Coordinator.model';
import Federation from './Federation.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 { Exchange } from './Exchange.model';
export type { Maker } from './Maker.model'; export type { Maker } from './Maker.model';
export type { Order } from './Order.model';
export type { Book, PublicOrder } from './Book.model'; export type { Book, PublicOrder } from './Book.model';
export type { Slot } from './Garage.model';
export type { Language } from './Settings.model'; export type { Language } from './Settings.model';
export type { Favorites } from './Favorites.model'; export type { Favorites } from './Favorites.model';
export type { Contact, Info, Version, Origin } from './Coordinator.model'; export type { Contact, Info, Version, Origin } from './Coordinator.model';

View File

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

View File

@ -19,7 +19,7 @@ const MakerWidget = React.forwardRef(function Component(
ref, ref,
) { ) {
const { fav } = useContext<UseAppStoreType>(AppContext); const { fav } = useContext<UseAppStoreType>(AppContext);
const { coordinatorUpdatedAt } = useContext<UseFederationStoreType>(FederationContext); const { federationUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
const { maker } = useContext<UseGarageStoreType>(GarageContext); const { maker } = useContext<UseGarageStoreType>(GarageContext);
return React.useMemo(() => { return React.useMemo(() => {
return ( return (
@ -27,7 +27,7 @@ const MakerWidget = React.forwardRef(function Component(
<MakerForm /> <MakerForm />
</Paper> </Paper>
); );
}, [maker, fav, coordinatorUpdatedAt]); }, [maker, fav, federationUpdatedAt]);
}); });
export default MakerWidget; 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": { "temple": {
"longAlias": "Temple of Sats", "longAlias": "Temple of Sats",
"shortAlias": "temple", "shortAlias": "temple",