Refactor Order/Slot models

This commit is contained in:
koalasat 2024-08-15 16:26:04 +02:00
parent 17c9662ab7
commit a92ca62bb1
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
33 changed files with 756 additions and 820 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 ?? '');
if (coordinator) {
const endpoint = coordinator?.getEndpoint(
settings.network,
origin,
settings.selfhostedClient,
hostUrl,
);
if (endpoint) setBaseUrl(`${endpoint?.url}${endpoint?.basePath}`);
const orderId = Number(params.orderId); const orderId = Number(params.orderId);
if ( const slot = garage.getSlot();
orderId && if (slot?.token) {
currentOrderId.id !== orderId && let order = new Order({ id: orderId, shortAlias });
currentOrderId.shortAlias !== shortAlias && if (slot.activeOrder?.id === orderId && slot.activeOrder?.shortAlias === shortAlias) {
shortAlias order = slot.activeOrder;
) } else if (slot.lastOrder?.id === orderId && slot.lastOrder?.shortAlias === shortAlias) {
setCurrentOrderId({ id: orderId, shortAlias }); order = slot.lastOrder;
if (!acknowledgedWarning) setOpen({ ...closeAll, warning: true });
} }
}, [params, currentOrderId]); void order.fecth(federation, slot).then((updatedOrder) => {
updateSlotFromOrder(updatedOrder, slot);
});
}
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

@ -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,24 @@ 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) currentOrder
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); .submitAction(federation, slot, {
setCurrentOrderId({ id: null, shortAlias: null });
apiClient
.post(
url + basePath,
`/api/order/?order_id=${String(currentOrder?.id)}`,
{
action: 'take', action: 'take',
amount: currentOrder?.currency === 1000 ? takeAmount / 100000000 : takeAmount, amount:
}, currentOrder?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount),
{ tokenSHA256: robot?.tokenSHA256 }, })
) .then((order) => {
.then((data) => { if (order?.bad_request !== undefined) {
if (data?.bad_request !== undefined) { setBadRequest(order.bad_request);
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

@ -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,42 +153,29 @@ 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) => { .then((data: Order) => {
setOpen(closeAll); setOpen(closeAll);
setLoadingButtons({ ...noLoadingButtons }); setLoadingButtons({ ...noLoadingButtons });
if (data.bad_request !== undefined) { if (data.bad_address !== undefined) {
setBadOrder(data.bad_request);
} else if (data.bad_address !== undefined) {
setOnchain({ ...onchain, badAddress: data.bad_address }); setOnchain({ ...onchain, badAddress: data.bad_address });
} else if (data.bad_invoice !== undefined) { } else if (data.bad_invoice !== undefined) {
setLightning({ ...lightning, badInvoice: data.bad_invoice }); setLightning({ ...lightning, badInvoice: data.bad_invoice });
} else if (data.bad_statement !== undefined) { } else if (data.bad_statement !== undefined) {
setDispute({ ...dispute, badStatement: data.bad_statement }); setDispute({ ...dispute, badStatement: data.bad_statement });
} else {
garage.updateOrder(data);
setBadOrder(undefined);
} }
slot.updateSlotFromOrder(data);
}) })
.catch(() => { .catch(() => {
setOpen(closeAll); setOpen(closeAll);
setLoadingButtons({ ...noLoadingButtons }); 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

@ -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;

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,19 +63,16 @@ 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');
} }
}; };
@ -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');
} }
}; };
@ -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,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,107 @@ 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: '',
num_satoshis: 0,
sent_satoshis: 0,
txid: '',
tx_queued: false,
address: '',
network: 'mainnet',
}; };
expiry_reason: number = 0;
expiry_message: string = '';
num_satoshis: number = 0;
sent_satoshis: number = 0;
txid: string = '';
tx_queued: boolean = false;
address: string = '';
network: 'mainnet' | 'testnet' = 'mainnet';
shortAlias: string = '';
bad_request?: string = '';
bad_address?: string = '';
bad_invoice?: string = '';
bad_statement?: string = '';
update = (attributes: Record<string, any>): Order => {
Object.assign(this, attributes);
return this;
};
make: (federation: Federation, slot: Slot) => Promise<this> = async (federation, slot) => {
const body = {
type: this.type,
currency: this.currency,
amount: this.has_range ? null : this.amount,
has_range: this.has_range,
min_amount: this.min_amount,
max_amount: this.max_amount,
payment_method: this.payment_method,
is_explicit: this.is_explicit,
premium: this.is_explicit ? null : this.premium,
satoshis: this.is_explicit ? this.satoshis : null,
public_duration: this.public_duration,
escrow_duration: this.escrow_duration,
bond_size: this.bond_size,
latitude: this.latitude,
longitude: this.longitude,
};
if (slot) {
const coordinator = federation.getCoordinator(this.shortAlias);
const { basePath, url } = coordinator;
const data = await apiClient
.post(url + basePath, '/api/make/', body, {
tokenSHA256: slot?.getRobot()?.tokenSHA256 ?? '',
})
.catch((e) => {
console.log(e);
});
if (data) this.update(data);
}
return this;
};
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,163 @@
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);
};
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.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,
style, }: FederationWidgetProps) {
className, const { federation, federationUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
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;