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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,30 +16,6 @@ import { AppContext, type UseAppStoreType } from './AppContext';
import { GarageContext, type UseGarageStoreType } from './GarageContext';
import { type Origin, type Origins } from '../models/Coordinator.model';
// Refresh delays (ms) according to Order status
const defaultDelay = 5000;
const statusToDelay = [
3000, // 'Waiting for maker bond'
35000, // 'Public'
180000, // 'Paused'
3000, // 'Waiting for taker bond'
999999, // 'Cancelled'
999999, // 'Expired'
8000, // 'Waiting for trade collateral and buyer invoice'
8000, // 'Waiting only for seller trade collateral'
8000, // 'Waiting only for buyer invoice'
10000, // 'Sending fiat - In chatroom'
10000, // 'Fiat sent - In chatroom'
100000, // 'In dispute'
999999, // 'Collaboratively cancelled'
10000, // 'Sending satoshis to buyer'
60000, // 'Successful trade'
30000, // 'Failed lightning network routing'
300000, // 'Wait for dispute resolution'
300000, // 'Maker lost dispute'
300000, // 'Taker lost dispute'
];
export interface CurrentOrderIdProps {
id: number | null;
shortAlias: string | null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,88 +1,163 @@
import { sha256 } from 'js-sha256';
import { type Coordinator, type Garage, Robot, type Order } from '.';
import { Robot, Order, type Federation } from '.';
import { roboidentitiesClient } from '../services/Roboidentities/Web';
import { hexToBase91, validateTokenEntropy } from '../utils';
export interface AuthHeaders {
tokenSHA256: string;
keys: {
pubKey: string;
encPrivKey: string;
};
}
class Slot {
constructor(
token: string,
shortAliases: string[],
robotAttributes: Record<any, any>,
onRobotUpdate: () => void,
onSlotUpdate: () => void,
) {
this.onSlotUpdate = onSlotUpdate;
this.token = token;
this.hashId = sha256(sha256(this.token));
this.nickname = null;
void roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => {
this.nickname = nickname;
onRobotUpdate();
onSlotUpdate();
});
void roboidentitiesClient.generateRobohash(this.hashId, 'small');
void roboidentitiesClient.generateRobohash(this.hashId, 'large');
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
const tokenSHA256 = hexToBase91(sha256(token));
this.robots = shortAliases.reduce((acc: Record<string, Robot>, shortAlias: string) => {
acc[shortAlias] = new Robot(robotAttributes);
acc[shortAlias] = new Robot({
...robotAttributes,
shortAlias,
hasEnoughEntropy,
bitsEntropy,
shannonEntropy,
tokenSHA256,
pubKey: robotAttributes.pubKey,
encPrivKey: robotAttributes.encPrivKey,
});
this.updateSlotFromRobot(acc[shortAlias]);
return acc;
}, {});
this.order = null;
this.activeShortAlias = null;
this.lastShortAlias = null;
this.copiedToken = false;
onRobotUpdate();
this.onSlotUpdate();
}
token: string | null;
hashId: string | null;
nickname: string | null;
robots: Record<string, Robot>;
order: Order | null;
activeShortAlias: string | null;
lastShortAlias: string | null;
activeOrder: Order | null = null;
lastOrder: Order | null = null;
copiedToken: boolean;
onSlotUpdate: () => void;
setCopiedToken = (copied: boolean): void => {
this.copiedToken = copied;
};
// Robots
getRobot = (shortAlias?: string): Robot | null => {
if (shortAlias) {
return this.robots[shortAlias];
} else if (this.activeShortAlias !== null && this.robots[this.activeShortAlias]) {
return this.robots[this.activeShortAlias];
} else if (this.lastShortAlias !== null && this.robots[this.lastShortAlias]) {
return this.robots[this.lastShortAlias];
} else if (this.activeOrder?.id) {
return this.robots[this.activeOrder.shortAlias];
} else if (this.lastOrder?.id && this.robots[this.lastOrder.shortAlias]) {
return this.robots[this.lastOrder.shortAlias];
} else if (Object.values(this.robots).length > 0) {
return Object.values(this.robots)[0];
}
return null;
};
updateRobot = (shortAlias: string, attributes: Record<any, any>): Robot | null => {
this.robots[shortAlias].update(attributes);
if (attributes.lastOrderId) {
this.lastShortAlias = shortAlias;
if (this.activeShortAlias === shortAlias) {
this.activeShortAlias = null;
}
}
if (attributes.activeOrderId) {
this.activeShortAlias = attributes.shortAlias;
}
return this.robots[shortAlias];
fetchRobot = async (federation: Federation): Promise<void> => {
Object.values(this.robots).forEach((robot) => {
void robot.fetch(federation).then((robot) => {
this.updateSlotFromRobot(robot);
});
});
};
syncCoordinator: (coordinator: Coordinator, garage: Garage) => void = (coordinator, garage) => {
updateSlotFromRobot = (robot: Robot | null): void => {
if (robot?.lastOrderId && this.lastOrder?.id !== robot?.lastOrderId) {
this.lastOrder = new Order({ id: robot.lastOrderId, shortAlias: robot.shortAlias });
if (this.activeOrder?.id === robot.lastOrderId) {
this.lastOrder = this.activeOrder;
this.activeOrder = null;
}
}
if (robot?.activeOrderId && this.activeOrder?.id !== robot.activeOrderId) {
this.activeOrder = new Order({
id: robot.activeOrderId,
shortAlias: robot.shortAlias,
});
}
this.onSlotUpdate();
};
// Orders
fetchActiveOrder = async (federation: Federation): Promise<void> => {
void this.activeOrder?.fecth(federation, this);
this.updateSlotFromOrder(this.activeOrder);
};
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();
if (defaultRobot?.token) {
this.robots[coordinator.shortAlias] = new Robot({
this.robots[shortAlias] = new Robot({
shortAlias,
hasEnoughEntropy: defaultRobot.hasEnoughEntropy,
bitsEntropy: defaultRobot.bitsEntropy,
shannonEntropy: defaultRobot.shannonEntropy,
token: defaultRobot.token,
pubKey: defaultRobot.pubKey,
encPrivKey: defaultRobot.encPrivKey,
});
void coordinator.fetchRobot(garage, defaultRobot.token);
void this.robots[shortAlias].fetch(federation);
this.updateSlotFromRobot(this.robots[shortAlias]);
}
};
}

View File

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

View File

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

View File

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