mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 20:21:35 +00:00
Merge pull request #1407 from RoboSats/refactor-order-model
Refactor Order/Slot models
This commit is contained in:
commit
a390254e2d
@ -21,8 +21,8 @@ const App = (): JSX.Element => {
|
||||
<Suspense fallback='loading'>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<AppContextProvider>
|
||||
<GarageContextProvider>
|
||||
<FederationContextProvider>
|
||||
<FederationContextProvider>
|
||||
<GarageContextProvider>
|
||||
<CssBaseline />
|
||||
{window.NativeRobosats === undefined && window.RobosatsClient === undefined ? (
|
||||
<HostAlert />
|
||||
@ -30,8 +30,8 @@ const App = (): JSX.Element => {
|
||||
<TorConnectionBadge />
|
||||
)}
|
||||
<Main />
|
||||
</FederationContextProvider>
|
||||
</GarageContextProvider>
|
||||
</GarageContextProvider>
|
||||
</FederationContextProvider>
|
||||
</AppContextProvider>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
|
@ -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');
|
||||
}}
|
||||
|
@ -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={() => {
|
||||
|
@ -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'
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tab, Tabs, Paper, CircularProgress, Grid, Typography, Box } from '@mui/material';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@ -6,60 +6,59 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
import TradeBox from '../../components/TradeBox';
|
||||
import OrderDetails from '../../components/OrderDetails';
|
||||
|
||||
import { AppContext, closeAll, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
|
||||
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { WarningDialog } from '../../components/Dialogs';
|
||||
import { Order, type Slot } from '../../models';
|
||||
import { type UseGarageStoreType, GarageContext } from '../../contexts/GarageContext';
|
||||
|
||||
const OrderPage = (): JSX.Element => {
|
||||
const {
|
||||
windowSize,
|
||||
open,
|
||||
setOpen,
|
||||
acknowledgedWarning,
|
||||
setAcknowledgedWarning,
|
||||
settings,
|
||||
navbarHeight,
|
||||
hostUrl,
|
||||
origin,
|
||||
} = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, currentOrder, currentOrderId, setCurrentOrderId } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const { badOrder } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { windowSize, setOpen, acknowledgedWarning, setAcknowledgedWarning, navbarHeight } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const { garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
const paramsRef = useRef(params);
|
||||
|
||||
const doublePageWidth: number = 50;
|
||||
const maxHeight: number = (windowSize?.height - navbarHeight) * 0.85 - 3;
|
||||
|
||||
const [tab, setTab] = useState<'order' | 'contract'>('contract');
|
||||
const [baseUrl, setBaseUrl] = useState<string>(hostUrl);
|
||||
const [currentOrder, setCurrentOrder] = useState<Order | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
paramsRef.current = params;
|
||||
const shortAlias = params.shortAlias;
|
||||
const coordinator = federation.getCoordinator(shortAlias ?? '');
|
||||
if (coordinator) {
|
||||
const endpoint = coordinator?.getEndpoint(
|
||||
settings.network,
|
||||
origin,
|
||||
settings.selfhostedClient,
|
||||
hostUrl,
|
||||
);
|
||||
|
||||
if (endpoint) setBaseUrl(`${endpoint?.url}${endpoint?.basePath}`);
|
||||
|
||||
const orderId = Number(params.orderId);
|
||||
if (
|
||||
orderId &&
|
||||
currentOrderId.id !== orderId &&
|
||||
currentOrderId.shortAlias !== shortAlias &&
|
||||
shortAlias
|
||||
)
|
||||
setCurrentOrderId({ id: orderId, shortAlias });
|
||||
if (!acknowledgedWarning) setOpen({ ...closeAll, warning: true });
|
||||
const orderId = Number(params.orderId);
|
||||
const slot = garage.getSlot();
|
||||
if (slot?.token) {
|
||||
let order = new Order({ id: orderId, shortAlias });
|
||||
if (slot.activeOrder?.id === orderId && slot.activeOrder?.shortAlias === shortAlias) {
|
||||
order = slot.activeOrder;
|
||||
} else if (slot.lastOrder?.id === orderId && slot.lastOrder?.shortAlias === shortAlias) {
|
||||
order = slot.lastOrder;
|
||||
}
|
||||
void order.fecth(federation, slot).then((updatedOrder) => {
|
||||
updateSlotFromOrder(updatedOrder, slot);
|
||||
});
|
||||
}
|
||||
}, [params, currentOrderId]);
|
||||
|
||||
return () => {
|
||||
setCurrentOrder(null);
|
||||
};
|
||||
}, [params.orderId]);
|
||||
|
||||
const updateSlotFromOrder = (updatedOrder: Order, slot: Slot): void => {
|
||||
if (
|
||||
Number(paramsRef.current.orderId) === updatedOrder.id &&
|
||||
paramsRef.current.shortAlias === updatedOrder.shortAlias
|
||||
) {
|
||||
setCurrentOrder(updatedOrder);
|
||||
slot.updateSlotFromOrder(updatedOrder);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickCoordinator = function (): void {
|
||||
if (currentOrder?.shortAlias != null) {
|
||||
@ -87,7 +86,7 @@ const OrderPage = (): JSX.Element => {
|
||||
);
|
||||
|
||||
const tradeBoxSpace = currentOrder ? (
|
||||
<TradeBox baseUrl={baseUrl} onStartAgain={startAgain} />
|
||||
<TradeBox onStartAgain={startAgain} currentOrder={currentOrder} />
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
@ -95,20 +94,19 @@ const OrderPage = (): JSX.Element => {
|
||||
return (
|
||||
<Box>
|
||||
<WarningDialog
|
||||
open={open.warning}
|
||||
open={!acknowledgedWarning && currentOrder?.status === 0}
|
||||
onClose={() => {
|
||||
setOpen(closeAll);
|
||||
setAcknowledgedWarning(true);
|
||||
}}
|
||||
longAlias={federation.getCoordinator(params.shortAlias ?? '')?.longAlias}
|
||||
/>
|
||||
{currentOrder === null && badOrder === undefined && <CircularProgress />}
|
||||
{badOrder !== undefined ? (
|
||||
{!currentOrder?.maker_hash_id && <CircularProgress />}
|
||||
{currentOrder?.bad_request && currentOrder.status !== 5 ? (
|
||||
<Typography align='center' variant='subtitle2' color='secondary'>
|
||||
{t(badOrder)}
|
||||
{t(currentOrder.bad_request)}
|
||||
</Typography>
|
||||
) : null}
|
||||
{currentOrder !== null && badOrder === undefined ? (
|
||||
{currentOrder?.maker_hash_id && (!currentOrder.bad_request || currentOrder.status === 5) ? (
|
||||
currentOrder.is_participant ? (
|
||||
windowSize.width > doublePageWidth ? (
|
||||
// DOUBLE PAPER VIEW
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -13,11 +13,10 @@ const Routes: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('navigateToPage', (event) => {
|
||||
console.log('navigateToPage', JSON.stringify(event));
|
||||
const orderId: string = event?.detail?.order_id;
|
||||
const coordinator: string = event?.detail?.coordinator;
|
||||
if (orderId && coordinator) {
|
||||
const slot = garage.getSlotByOrder(coordinator, orderId);
|
||||
const slot = garage.getSlotByOrder(coordinator, parseInt(orderId, 10));
|
||||
if (slot?.token) {
|
||||
garage.setCurrentSlot(slot?.token);
|
||||
navigate(`/order/${coordinator}/${orderId}`);
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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}>
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
@ -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 });
|
||||
|
@ -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(() => {
|
||||
|
@ -19,15 +19,14 @@ import {
|
||||
import Countdown from 'react-countdown';
|
||||
|
||||
import currencies from '../../../static/assets/currencies.json';
|
||||
import { apiClient } from '../../services/api';
|
||||
|
||||
import { type Order, type Info } from '../../models';
|
||||
import { ConfirmationDialog } from '../Dialogs';
|
||||
import { LoadingButton } from '@mui/lab';
|
||||
import { computeSats } from '../../utils';
|
||||
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
|
||||
import { type UseAppStoreType, AppContext } from '../../contexts/AppContext';
|
||||
import { type UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface TakeButtonProps {
|
||||
currentOrder: Order;
|
||||
@ -48,9 +47,9 @@ const TakeButton = ({
|
||||
}: TakeButtonProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { settings, origin, hostUrl } = useContext<UseAppStoreType>(AppContext);
|
||||
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { federation, setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const navigate = useNavigate();
|
||||
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
const [takeAmount, setTakeAmount] = useState<string>('');
|
||||
const [badRequest, setBadRequest] = useState<string>('');
|
||||
@ -79,7 +78,7 @@ const TakeButton = ({
|
||||
|
||||
useEffect(() => {
|
||||
setSatoshis(satoshisNow() ?? '');
|
||||
}, [orderUpdatedAt, takeAmount, info]);
|
||||
}, [slotUpdatedAt, takeAmount, info]);
|
||||
|
||||
const currencyCode: string =
|
||||
currentOrder?.currency === 1000 ? 'Sats' : currencies[`${Number(currentOrder?.currency)}`];
|
||||
@ -165,7 +164,7 @@ const TakeButton = ({
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [orderUpdatedAt, takeAmount]);
|
||||
}, [slotUpdatedAt, takeAmount]);
|
||||
|
||||
const onTakeOrderClicked = function (): void {
|
||||
if (currentOrder?.maker_status === 'Inactive') {
|
||||
@ -184,7 +183,7 @@ const TakeButton = ({
|
||||
takeAmount === '' ||
|
||||
takeAmount == null
|
||||
);
|
||||
}, [takeAmount, orderUpdatedAt]);
|
||||
}, [takeAmount, slotUpdatedAt]);
|
||||
|
||||
const takeOrderButton = function (): JSX.Element {
|
||||
if (currentOrder?.has_range) {
|
||||
@ -314,31 +313,20 @@ const TakeButton = ({
|
||||
};
|
||||
|
||||
const takeOrder = function (): void {
|
||||
const robot = garage.getSlot()?.getRobot() ?? null;
|
||||
const slot = garage.getSlot();
|
||||
|
||||
if (currentOrder === null || robot === null) return;
|
||||
if (currentOrder === null || slot === null) return;
|
||||
|
||||
setLoadingTake(true);
|
||||
const { url, basePath } = federation
|
||||
.getCoordinator(currentOrder.shortAlias)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
setCurrentOrderId({ id: null, shortAlias: null });
|
||||
apiClient
|
||||
.post(
|
||||
url + basePath,
|
||||
`/api/order/?order_id=${String(currentOrder?.id)}`,
|
||||
{
|
||||
action: 'take',
|
||||
amount: currentOrder?.currency === 1000 ? takeAmount / 100000000 : takeAmount,
|
||||
},
|
||||
{ tokenSHA256: robot?.tokenSHA256 },
|
||||
)
|
||||
.then((data) => {
|
||||
if (data?.bad_request !== undefined) {
|
||||
setBadRequest(data.bad_request);
|
||||
|
||||
slot
|
||||
.takeOrder(federation, currentOrder, takeAmount)
|
||||
.then((order) => {
|
||||
if (order?.bad_request !== undefined) {
|
||||
setBadRequest(order.bad_request);
|
||||
} else {
|
||||
setCurrentOrderId({ id: currentOrder?.id, shortAlias: currentOrder?.shortAlias });
|
||||
setBadRequest('');
|
||||
navigate(`/order/${order.shortAlias}/${order.id}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
@ -174,8 +174,8 @@ const OrderDetails = ({
|
||||
|
||||
const isBuyer = (order.type === 0 && order.is_maker) || (order.type === 1 && !order.is_maker);
|
||||
const tradeFee = order.is_maker
|
||||
? coordinator.info?.maker_fee ?? 0
|
||||
: coordinator.info?.taker_fee ?? 0;
|
||||
? (coordinator.info?.maker_fee ?? 0)
|
||||
: (coordinator.info?.taker_fee ?? 0);
|
||||
const defaultRoutingBudget = 0.001;
|
||||
const btc_now = order.satoshis_now / 100000000;
|
||||
const rate = Number(order.max_amount ?? order.amount) / btc_now;
|
||||
|
@ -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'> {t('Claim Sats!')} </Typography>
|
||||
)}
|
||||
{slot?.activeShortAlias === coordinator.shortAlias && (
|
||||
{robot?.activeOrderId ? (
|
||||
<Typography color='success'>
|
||||
<b>{t('Active order!')}</b>
|
||||
</Typography>
|
||||
)}
|
||||
{slot?.lastShortAlias === coordinator.shortAlias && (
|
||||
<Typography color='warning'> {t('finished order')}</Typography>
|
||||
) : (
|
||||
robot?.lastOrderId && <Typography color='warning'> {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();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
@ -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(() => {
|
||||
|
@ -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) {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -1,7 +1,5 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Box, Divider, Grid } from '@mui/material';
|
||||
|
||||
import { apiClient } from '../../services/api';
|
||||
import { getWebln, pn } from '../../utils';
|
||||
|
||||
import {
|
||||
@ -102,7 +100,7 @@ const closeAll: OpenDialogProps = {
|
||||
};
|
||||
|
||||
interface TradeBoxProps {
|
||||
baseUrl: string;
|
||||
currentOrder: Order;
|
||||
onStartAgain: () => void;
|
||||
}
|
||||
|
||||
@ -115,10 +113,10 @@ interface Contract {
|
||||
titleIcon: () => JSX.Element;
|
||||
}
|
||||
|
||||
const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
const { garage, orderUpdatedAt, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { settings, hostUrl, origin } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation, setCurrentOrderId } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
const { garage, slotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { settings } = useContext<UseAppStoreType>(AppContext);
|
||||
const { federation } = useContext<UseFederationStoreType>(FederationContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Buttons and Dialogs
|
||||
@ -155,43 +153,30 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
}
|
||||
|
||||
const renewOrder = function (): void {
|
||||
const currentOrder = garage.getSlot()?.order;
|
||||
if (currentOrder) {
|
||||
const body = {
|
||||
type: currentOrder.type,
|
||||
currency: currentOrder.currency,
|
||||
amount: currentOrder.has_range ? null : currentOrder.amount,
|
||||
has_range: currentOrder.has_range,
|
||||
min_amount: currentOrder.min_amount,
|
||||
max_amount: currentOrder.max_amount,
|
||||
payment_method: currentOrder.payment_method,
|
||||
is_explicit: currentOrder.is_explicit,
|
||||
premium: currentOrder.is_explicit ? null : currentOrder.premium,
|
||||
satoshis: currentOrder.is_explicit ? currentOrder.satoshis : null,
|
||||
public_duration: currentOrder.public_duration,
|
||||
escrow_duration: currentOrder.escrow_duration,
|
||||
bond_size: currentOrder.bond_size,
|
||||
latitude: currentOrder.latitude,
|
||||
longitude: currentOrder.longitude,
|
||||
const slot = garage.getSlot();
|
||||
const newOrder = currentOrder;
|
||||
if (newOrder && slot) {
|
||||
const orderAttributes = {
|
||||
type: newOrder.type,
|
||||
currency: newOrder.currency,
|
||||
amount: newOrder.has_range ? null : newOrder.amount,
|
||||
has_range: newOrder.has_range,
|
||||
min_amount: newOrder.min_amount,
|
||||
max_amount: newOrder.max_amount,
|
||||
payment_method: newOrder.payment_method,
|
||||
is_explicit: newOrder.is_explicit,
|
||||
premium: newOrder.is_explicit ? null : newOrder.premium,
|
||||
satoshis: newOrder.is_explicit ? newOrder.satoshis : null,
|
||||
public_duration: newOrder.public_duration,
|
||||
escrow_duration: newOrder.escrow_duration,
|
||||
bond_size: newOrder.bond_size,
|
||||
latitude: newOrder.latitude,
|
||||
longitude: newOrder.longitude,
|
||||
shortAlias: newOrder.shortAlias,
|
||||
};
|
||||
const { url, basePath } = federation
|
||||
.getCoordinator(currentOrder.shortAlias)
|
||||
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
|
||||
apiClient
|
||||
.post(url + basePath, '/api/make/', body, {
|
||||
tokenSHA256: garage.getSlot()?.getRobot()?.tokenSHA256,
|
||||
})
|
||||
.then((data: any) => {
|
||||
if (data.bad_request !== undefined) {
|
||||
setBadOrder(data.bad_request);
|
||||
} else if (data.id !== undefined) {
|
||||
navigate(`/order/${String(currentOrder?.shortAlias)}/${String(data.id)}`);
|
||||
setCurrentOrderId({ id: data.id, shortAlias: currentOrder?.shortAlias });
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setBadOrder('Request error');
|
||||
});
|
||||
void slot.makeOrder(federation, orderAttributes).then((order: Order) => {
|
||||
if (order?.id) navigate(`/order/${String(order?.shortAlias)}/${String(order.id)}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -204,14 +189,11 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
statement,
|
||||
rating,
|
||||
}: SubmitActionProps): void {
|
||||
const robot = garage.getSlot()?.getRobot();
|
||||
const currentOrder = garage.getSlot()?.order;
|
||||
const slot = garage.getSlot();
|
||||
|
||||
void apiClient
|
||||
.post(
|
||||
baseUrl,
|
||||
`/api/order/?order_id=${Number(currentOrder?.id)}`,
|
||||
{
|
||||
if (slot && currentOrder) {
|
||||
currentOrder
|
||||
.submitAction(federation, slot, {
|
||||
action,
|
||||
invoice,
|
||||
routing_budget_ppm,
|
||||
@ -219,29 +201,24 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
mining_fee_rate,
|
||||
statement,
|
||||
rating,
|
||||
},
|
||||
{ tokenSHA256: robot?.tokenSHA256 },
|
||||
)
|
||||
.then((data: Order) => {
|
||||
setOpen(closeAll);
|
||||
setLoadingButtons({ ...noLoadingButtons });
|
||||
if (data.bad_request !== undefined) {
|
||||
setBadOrder(data.bad_request);
|
||||
} else if (data.bad_address !== undefined) {
|
||||
setOnchain({ ...onchain, badAddress: data.bad_address });
|
||||
} else if (data.bad_invoice !== undefined) {
|
||||
setLightning({ ...lightning, badInvoice: data.bad_invoice });
|
||||
} else if (data.bad_statement !== undefined) {
|
||||
setDispute({ ...dispute, badStatement: data.bad_statement });
|
||||
} else {
|
||||
garage.updateOrder(data);
|
||||
setBadOrder(undefined);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setOpen(closeAll);
|
||||
setLoadingButtons({ ...noLoadingButtons });
|
||||
});
|
||||
})
|
||||
.then((data: Order) => {
|
||||
setOpen(closeAll);
|
||||
setLoadingButtons({ ...noLoadingButtons });
|
||||
if (data.bad_address !== undefined) {
|
||||
setOnchain({ ...onchain, badAddress: data.bad_address });
|
||||
} else if (data.bad_invoice !== undefined) {
|
||||
setLightning({ ...lightning, badInvoice: data.bad_invoice });
|
||||
} else if (data.bad_statement !== undefined) {
|
||||
setDispute({ ...dispute, badStatement: data.bad_statement });
|
||||
}
|
||||
slot.updateSlotFromOrder(data);
|
||||
})
|
||||
.catch(() => {
|
||||
setOpen(closeAll);
|
||||
setLoadingButtons({ ...noLoadingButtons });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = function (): void {
|
||||
@ -363,15 +340,14 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
|
||||
// Effect on Order Status change (used for WebLN)
|
||||
useEffect(() => {
|
||||
const currentOrder = garage.getSlot()?.order;
|
||||
if (currentOrder && currentOrder?.status !== lastOrderStatus) {
|
||||
setLastOrderStatus(currentOrder.status);
|
||||
void handleWebln(currentOrder);
|
||||
}
|
||||
}, [orderUpdatedAt]);
|
||||
}, [slotUpdatedAt]);
|
||||
|
||||
const statusToContract = function (): Contract {
|
||||
const order = garage.getSlot()?.order;
|
||||
const order = currentOrder;
|
||||
|
||||
const baseContract: Contract = {
|
||||
title: 'Unknown Order Status',
|
||||
@ -382,7 +358,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
titleIcon: () => <></>,
|
||||
};
|
||||
|
||||
if (!order) return baseContract;
|
||||
if (!currentOrder) return baseContract;
|
||||
|
||||
const status = order.status;
|
||||
const isBuyer = order.is_buyer;
|
||||
@ -573,7 +549,6 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
setOpen({ ...open, confirmDispute: true });
|
||||
}}
|
||||
loadingDispute={loadingButtons.openDispute}
|
||||
baseUrl={baseUrl}
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
/>
|
||||
@ -746,7 +721,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
setOpen(closeAll);
|
||||
}}
|
||||
waitingWebln={waitingWebln}
|
||||
isBuyer={garage.getSlot()?.order?.is_buyer ?? false}
|
||||
isBuyer={currentOrder.is_buyer ?? false}
|
||||
/>
|
||||
<ConfirmDisputeDialog
|
||||
open={open.confirmDispute}
|
||||
@ -769,11 +744,11 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
}}
|
||||
onCollabCancelClick={cancel}
|
||||
loading={loadingButtons.cancel}
|
||||
peerAskedCancel={garage.getSlot()?.order?.pending_cancel ?? false}
|
||||
peerAskedCancel={currentOrder?.pending_cancel ?? false}
|
||||
/>
|
||||
<ConfirmFiatSentDialog
|
||||
open={open.confirmFiatSent}
|
||||
order={garage.getSlot()?.order ?? null}
|
||||
order={currentOrder ?? null}
|
||||
loadingButton={loadingButtons.fiatSent}
|
||||
onClose={() => {
|
||||
setOpen(closeAll);
|
||||
@ -790,14 +765,14 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
/>
|
||||
<ConfirmFiatReceivedDialog
|
||||
open={open.confirmFiatReceived}
|
||||
order={garage.getSlot()?.order ?? null}
|
||||
order={currentOrder ?? null}
|
||||
loadingButton={loadingButtons.fiatReceived}
|
||||
onClose={() => {
|
||||
setOpen(closeAll);
|
||||
}}
|
||||
onConfirmClick={confirmFiatReceived}
|
||||
/>
|
||||
<CollabCancelAlert order={garage.getSlot()?.order ?? null} />
|
||||
<CollabCancelAlert order={currentOrder ?? null} />
|
||||
<Grid
|
||||
container
|
||||
padding={1}
|
||||
@ -808,7 +783,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
>
|
||||
<Grid item>
|
||||
<Title
|
||||
order={garage.getSlot()?.order ?? null}
|
||||
order={currentOrder ?? null}
|
||||
text={contract?.title}
|
||||
color={contract?.titleColor}
|
||||
icon={contract?.titleIcon}
|
||||
@ -822,10 +797,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
{contract?.bondStatus !== 'hide' ? (
|
||||
<Grid item sx={{ width: '100%' }}>
|
||||
<Divider />
|
||||
<BondStatus
|
||||
status={contract?.bondStatus}
|
||||
isMaker={garage.getSlot()?.order?.is_maker ?? false}
|
||||
/>
|
||||
<BondStatus status={contract?.bondStatus} isMaker={currentOrder?.is_maker ?? false} />
|
||||
</Grid>
|
||||
) : (
|
||||
<></>
|
||||
@ -833,7 +805,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
|
||||
|
||||
<Grid item>
|
||||
<CancelButton
|
||||
order={garage.getSlot()?.order ?? null}
|
||||
order={currentOrder ?? null}
|
||||
onClickCancel={cancel}
|
||||
openCancelDialog={() => {
|
||||
setOpen({ ...closeAll, confirmCancel: true });
|
||||
|
@ -76,7 +76,11 @@ const makeTheme = function (settings: Settings): Theme {
|
||||
};
|
||||
|
||||
const getHostUrl = (network = 'mainnet'): string => {
|
||||
let host = defaultFederation.exp[network].onion;
|
||||
const randomAlias =
|
||||
Object.keys(defaultFederation)[
|
||||
Math.floor(Math.random() * Object.keys(defaultFederation).length)
|
||||
];
|
||||
let host = defaultFederation[randomAlias][network].onion;
|
||||
let protocol = 'http:';
|
||||
if (window.NativeRobosats === undefined) {
|
||||
host = getHost();
|
||||
|
@ -8,7 +8,7 @@ import React, {
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
|
||||
import { type Order, Federation, Settings } from '../models';
|
||||
import { Federation, Settings } from '../models';
|
||||
|
||||
import { federationLottery } from '../utils';
|
||||
|
||||
@ -16,30 +16,6 @@ import { AppContext, type UseAppStoreType } from './AppContext';
|
||||
import { GarageContext, type UseGarageStoreType } from './GarageContext';
|
||||
import { type Origin, type Origins } from '../models/Coordinator.model';
|
||||
|
||||
// Refresh delays (ms) according to Order status
|
||||
const defaultDelay = 5000;
|
||||
const statusToDelay = [
|
||||
3000, // 'Waiting for maker bond'
|
||||
35000, // 'Public'
|
||||
180000, // 'Paused'
|
||||
3000, // 'Waiting for taker bond'
|
||||
999999, // 'Cancelled'
|
||||
999999, // 'Expired'
|
||||
8000, // 'Waiting for trade collateral and buyer invoice'
|
||||
8000, // 'Waiting only for seller trade collateral'
|
||||
8000, // 'Waiting only for buyer invoice'
|
||||
10000, // 'Sending fiat - In chatroom'
|
||||
10000, // 'Fiat sent - In chatroom'
|
||||
100000, // 'In dispute'
|
||||
999999, // 'Collaboratively cancelled'
|
||||
10000, // 'Sending satoshis to buyer'
|
||||
60000, // 'Successful trade'
|
||||
30000, // 'Failed lightning network routing'
|
||||
300000, // 'Wait for dispute resolution'
|
||||
300000, // 'Maker lost dispute'
|
||||
300000, // 'Taker lost dispute'
|
||||
];
|
||||
|
||||
export interface CurrentOrderIdProps {
|
||||
id: number | null;
|
||||
shortAlias: string | null;
|
||||
@ -52,10 +28,6 @@ export interface FederationContextProviderProps {
|
||||
export interface UseFederationStoreType {
|
||||
federation: Federation;
|
||||
sortedCoordinators: string[];
|
||||
setDelay: Dispatch<SetStateAction<number>>;
|
||||
currentOrderId: CurrentOrderIdProps;
|
||||
setCurrentOrderId: Dispatch<SetStateAction<CurrentOrderIdProps>>;
|
||||
currentOrder: Order | null;
|
||||
coordinatorUpdatedAt: string;
|
||||
federationUpdatedAt: string;
|
||||
addNewCoordinator: (alias: string, url: string) => void;
|
||||
@ -64,10 +36,6 @@ export interface UseFederationStoreType {
|
||||
export const initialFederationContext: UseFederationStoreType = {
|
||||
federation: new Federation('onion', new Settings(), ''),
|
||||
sortedCoordinators: [],
|
||||
setDelay: () => {},
|
||||
currentOrderId: { id: null, shortAlias: null },
|
||||
setCurrentOrderId: () => {},
|
||||
currentOrder: null,
|
||||
coordinatorUpdatedAt: '',
|
||||
federationUpdatedAt: '',
|
||||
addNewCoordinator: () => {},
|
||||
@ -80,24 +48,13 @@ export const FederationContextProvider = ({
|
||||
}: FederationContextProviderProps): JSX.Element => {
|
||||
const { settings, page, origin, hostUrl, open, torStatus } =
|
||||
useContext<UseAppStoreType>(AppContext);
|
||||
const { setMaker, garage, setBadOrder } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const { setMaker, garage } = useContext<UseGarageStoreType>(GarageContext);
|
||||
const [federation] = useState(new Federation(origin, settings, hostUrl));
|
||||
const [sortedCoordinators, setSortedCoordinators] = useState(federationLottery(federation));
|
||||
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>(
|
||||
new Date().toISOString(),
|
||||
);
|
||||
const [federationUpdatedAt, setFederationUpdatedAt] = useState<string>(new Date().toISOString());
|
||||
const [currentOrderId, setCurrentOrderId] = useState<CurrentOrderIdProps>(
|
||||
initialFederationContext.currentOrderId,
|
||||
);
|
||||
const [currentOrder, setCurrentOrder] = useState<Order | null>(
|
||||
initialFederationContext.currentOrder,
|
||||
);
|
||||
|
||||
const [delay, setDelay] = useState<number>(defaultDelay);
|
||||
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(() =>
|
||||
setInterval(() => null, delay),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setMaker((maker) => {
|
||||
@ -106,65 +63,15 @@ export const FederationContextProvider = ({
|
||||
federation.registerHook('onFederationUpdate', () => {
|
||||
setFederationUpdatedAt(new Date().toISOString());
|
||||
});
|
||||
federation.registerHook('onCoordinatorUpdate', () => {
|
||||
setCoordinatorUpdatedAt(new Date().toISOString());
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
|
||||
void federation.updateUrl(origin, settings, hostUrl);
|
||||
void federation.update();
|
||||
|
||||
const token = garage.getSlot()?.getRobot()?.token;
|
||||
if (token) void federation.fetchRobot(garage, token);
|
||||
}
|
||||
}, [settings.network, settings.useProxy, torStatus]);
|
||||
|
||||
const onOrderReceived = (order: Order): void => {
|
||||
let newDelay = defaultDelay;
|
||||
if (order?.bad_request) {
|
||||
newDelay = 99999999;
|
||||
setBadOrder(order.bad_request);
|
||||
garage.updateOrder(null);
|
||||
setCurrentOrder(null);
|
||||
}
|
||||
if (order?.id) {
|
||||
newDelay =
|
||||
order.status >= 0 && order.status <= 18
|
||||
? page === 'order'
|
||||
? statusToDelay[order.status]
|
||||
: statusToDelay[order.status] * 5 // If user is not looking at "order" tab, refresh less often.
|
||||
: 99999999;
|
||||
garage.updateOrder(order);
|
||||
setCurrentOrder(order);
|
||||
setBadOrder(undefined);
|
||||
}
|
||||
clearInterval(timer);
|
||||
console.log('New Delay:', newDelay);
|
||||
setDelay(newDelay);
|
||||
setTimer(setTimeout(fetchCurrentOrder, newDelay));
|
||||
};
|
||||
|
||||
const fetchCurrentOrder: () => void = () => {
|
||||
const slot = garage?.getSlot();
|
||||
const robot = slot?.getRobot();
|
||||
if (robot && slot?.token && currentOrderId.id && currentOrderId.shortAlias) {
|
||||
const coordinator = federation.getCoordinator(currentOrderId.shortAlias);
|
||||
void coordinator?.fetchOrder(currentOrderId.id, robot, slot?.token).then((order) => {
|
||||
onOrderReceived(order as Order);
|
||||
});
|
||||
} else if (slot?.token && slot?.activeShortAlias && robot?.activeOrderId) {
|
||||
const coordinator = federation.getCoordinator(slot.activeShortAlias);
|
||||
void coordinator?.fetchOrder(robot.activeOrderId, robot, slot.token).then((order) => {
|
||||
onOrderReceived(order as Order);
|
||||
});
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
setTimer(setTimeout(fetchCurrentOrder, defaultDelay));
|
||||
}
|
||||
};
|
||||
|
||||
const addNewCoordinator: (alias: string, url: string) => void = (alias, url) => {
|
||||
if (!federation.coordinators[alias]) {
|
||||
const attributes: Record<any, any> = {
|
||||
@ -188,24 +95,12 @@ export const FederationContextProvider = ({
|
||||
newCoordinator.update(() => {
|
||||
setCoordinatorUpdatedAt(new Date().toISOString());
|
||||
});
|
||||
garage.syncCoordinator(newCoordinator);
|
||||
garage.syncCoordinator(federation, alias);
|
||||
setSortedCoordinators(federationLottery(federation));
|
||||
setFederationUpdatedAt(new Date().toISOString());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentOrderId.id && currentOrderId.shortAlias) {
|
||||
setCurrentOrder(null);
|
||||
setBadOrder(undefined);
|
||||
clearInterval(timer);
|
||||
fetchCurrentOrder();
|
||||
}
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [currentOrderId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (page === 'offers') void federation.updateBook();
|
||||
}, [page]);
|
||||
@ -215,7 +110,7 @@ export const FederationContextProvider = ({
|
||||
const slot = garage.getSlot();
|
||||
|
||||
if (open.profile && slot?.hashId && slot?.token) {
|
||||
void federation.fetchRobot(garage, slot?.token); // refresh/update existing robot
|
||||
void garage.fetchRobot(federation, slot?.token); // refresh/update existing robot
|
||||
}
|
||||
}, [open.profile]);
|
||||
|
||||
@ -224,10 +119,6 @@ export const FederationContextProvider = ({
|
||||
value={{
|
||||
federation,
|
||||
sortedCoordinators,
|
||||
currentOrderId,
|
||||
setCurrentOrderId,
|
||||
currentOrder,
|
||||
setDelay,
|
||||
coordinatorUpdatedAt,
|
||||
federationUpdatedAt,
|
||||
addNewCoordinator,
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { type Coordinator, type Order } from '.';
|
||||
import { type Federation, Order } from '.';
|
||||
import { systemClient } from '../services/System';
|
||||
import { saveAsJson } from '../utils';
|
||||
import Slot from './Slot.model';
|
||||
|
||||
type GarageHooks = 'onRobotUpdate' | 'onOrderUpdate';
|
||||
type GarageHooks = 'onSlotUpdate';
|
||||
|
||||
class Garage {
|
||||
constructor() {
|
||||
@ -11,8 +11,7 @@ class Garage {
|
||||
this.currentSlot = null;
|
||||
|
||||
this.hooks = {
|
||||
onRobotUpdate: [],
|
||||
onOrderUpdate: [],
|
||||
onSlotUpdate: [],
|
||||
};
|
||||
|
||||
this.loadSlots();
|
||||
@ -29,6 +28,7 @@ class Garage {
|
||||
};
|
||||
|
||||
triggerHook = (hookName: GarageHooks): void => {
|
||||
this.save();
|
||||
this.hooks[hookName]?.forEach((fn) => {
|
||||
fn();
|
||||
});
|
||||
@ -47,8 +47,7 @@ class Garage {
|
||||
this.slots = {};
|
||||
this.currentSlot = null;
|
||||
systemClient.deleteItem('garage_slots');
|
||||
this.triggerHook('onRobotUpdate');
|
||||
this.triggerHook('onOrderUpdate');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
};
|
||||
|
||||
loadSlots = (): void => {
|
||||
@ -64,26 +63,23 @@ class Garage {
|
||||
Object.keys(rawSlot.robots),
|
||||
{},
|
||||
() => {
|
||||
this.triggerHook('onRobotUpdate');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
},
|
||||
);
|
||||
Object.keys(rawSlot.robots).forEach((shortAlias) => {
|
||||
const rawRobot = rawSlot.robots[shortAlias];
|
||||
this.updateRobot(rawSlot.token, shortAlias, rawRobot);
|
||||
});
|
||||
this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.lastOrder));
|
||||
this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.activeOrder));
|
||||
this.currentSlot = rawSlot?.token;
|
||||
}
|
||||
});
|
||||
console.log('Robot Garage was loaded from local storage');
|
||||
this.triggerHook('onRobotUpdate');
|
||||
this.triggerHook('onOrderUpdate');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
}
|
||||
};
|
||||
|
||||
// Slots
|
||||
getSlot: (token?: string) => Slot | null = (token) => {
|
||||
const currentToken = token ?? this.currentSlot;
|
||||
return currentToken ? this.slots[currentToken] ?? null : null;
|
||||
return currentToken ? (this.slots[currentToken] ?? null) : null;
|
||||
};
|
||||
|
||||
deleteSlot: (token?: string) => void = (token) => {
|
||||
@ -92,8 +88,7 @@ class Garage {
|
||||
Reflect.deleteProperty(this.slots, targetIndex);
|
||||
this.currentSlot = null;
|
||||
this.save();
|
||||
this.triggerHook('onRobotUpdate');
|
||||
this.triggerHook('onOrderUpdate');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
}
|
||||
};
|
||||
|
||||
@ -105,7 +100,7 @@ class Garage {
|
||||
if (attributes) {
|
||||
if (attributes.copiedToken !== undefined) slot?.setCopiedToken(attributes.copiedToken);
|
||||
this.save();
|
||||
this.triggerHook('onRobotUpdate');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
}
|
||||
return slot;
|
||||
};
|
||||
@ -113,7 +108,7 @@ class Garage {
|
||||
setCurrentSlot: (currentSlot: string) => void = (currentSlot) => {
|
||||
this.currentSlot = currentSlot;
|
||||
this.save();
|
||||
this.triggerHook('onRobotUpdate');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
};
|
||||
|
||||
getSlotByOrder: (coordinator: string, orderID: number) => Slot | null = (
|
||||
@ -123,7 +118,7 @@ class Garage {
|
||||
return (
|
||||
Object.values(this.slots).find((slot) => {
|
||||
const robot = slot.getRobot(coordinator);
|
||||
return slot.activeShortAlias === coordinator && robot?.activeOrderId === orderID;
|
||||
return slot.activeOrder?.shortAlias === coordinator && robot?.activeOrderId === orderID;
|
||||
}) ?? null
|
||||
);
|
||||
};
|
||||
@ -138,55 +133,29 @@ class Garage {
|
||||
|
||||
if (this.getSlot(token) === null) {
|
||||
this.slots[token] = new Slot(token, shortAliases, attributes, () => {
|
||||
this.triggerHook('onRobotUpdate');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
});
|
||||
this.save();
|
||||
}
|
||||
};
|
||||
|
||||
updateRobot: (token: string, shortAlias: string, attributes: Record<any, any>) => void = (
|
||||
token,
|
||||
shortAlias,
|
||||
attributes,
|
||||
) => {
|
||||
if (!token || !shortAlias) return;
|
||||
|
||||
fetchRobot = async (federation: Federation, token: string): Promise<void> => {
|
||||
const slot = this.getSlot(token);
|
||||
|
||||
if (slot != null) {
|
||||
slot.updateRobot(shortAlias, { token, ...attributes });
|
||||
await slot.fetchRobot(federation);
|
||||
this.save();
|
||||
this.triggerHook('onRobotUpdate');
|
||||
}
|
||||
};
|
||||
|
||||
// Orders
|
||||
updateOrder: (order: Order | null) => void = (order) => {
|
||||
const slot = this.getSlot();
|
||||
if (slot != null) {
|
||||
if (order !== null) {
|
||||
const updatedOrder = slot.order ?? null;
|
||||
if (updatedOrder !== null && updatedOrder.id === order.id) {
|
||||
Object.assign(updatedOrder, order);
|
||||
slot.order = updatedOrder;
|
||||
} else {
|
||||
slot.order = order;
|
||||
}
|
||||
if (slot.order?.is_participant) {
|
||||
slot.activeShortAlias = order.shortAlias;
|
||||
}
|
||||
} else {
|
||||
slot.order = null;
|
||||
}
|
||||
this.save();
|
||||
this.triggerHook('onOrderUpdate');
|
||||
this.triggerHook('onSlotUpdate');
|
||||
}
|
||||
};
|
||||
|
||||
// Coordinators
|
||||
syncCoordinator: (coordinator: Coordinator) => void = (coordinator) => {
|
||||
syncCoordinator: (federation: Federation, shortAlias: string) => void = (
|
||||
federation,
|
||||
shortAlias,
|
||||
) => {
|
||||
Object.values(this.slots).forEach((slot) => {
|
||||
slot.syncCoordinator(coordinator, this);
|
||||
slot.syncCoordinator(federation, shortAlias);
|
||||
});
|
||||
this.save();
|
||||
};
|
||||
|
@ -1,3 +1,5 @@
|
||||
import defaultFederation from '../../static/federation.json';
|
||||
|
||||
export interface Maker {
|
||||
advancedOptions: boolean;
|
||||
coordinator: string;
|
||||
@ -23,7 +25,10 @@ export interface Maker {
|
||||
|
||||
export const defaultMaker: Maker = {
|
||||
advancedOptions: false,
|
||||
coordinator: 'exp',
|
||||
coordinator:
|
||||
Object.keys(defaultFederation)[
|
||||
Math.floor(Math.random() * Object.keys(defaultFederation).length)
|
||||
] ?? '',
|
||||
isExplicit: false,
|
||||
amount: '',
|
||||
paymentMethods: [],
|
||||
@ -40,6 +45,8 @@ export const defaultMaker: Maker = {
|
||||
maxAmount: '',
|
||||
badPremiumText: '',
|
||||
badSatoshisText: '',
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
};
|
||||
|
||||
export default Maker;
|
||||
|
@ -1,3 +1,28 @@
|
||||
import { apiClient } from '../services/api';
|
||||
import type Federation from './Federation.model';
|
||||
import type Slot from './Slot.model';
|
||||
|
||||
export interface SubmitActionProps {
|
||||
action:
|
||||
| 'cancel'
|
||||
| 'dispute'
|
||||
| 'pause'
|
||||
| 'confirm'
|
||||
| 'undo_confirm'
|
||||
| 'update_invoice'
|
||||
| 'update_address'
|
||||
| 'submit_statement'
|
||||
| 'rate_platform'
|
||||
| 'take';
|
||||
invoice?: string;
|
||||
routing_budget_ppm?: number;
|
||||
address?: string;
|
||||
mining_fee_rate?: number;
|
||||
statement?: string;
|
||||
rating?: number;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
export interface TradeRobotSummary {
|
||||
is_buyer: boolean;
|
||||
sent_fiat: number;
|
||||
@ -24,168 +49,82 @@ export interface TradeCoordinatorSummary {
|
||||
trade_revenue_sats: number;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: number;
|
||||
status: number;
|
||||
created_at: Date;
|
||||
expires_at: Date;
|
||||
type: number;
|
||||
currency: number;
|
||||
amount: number;
|
||||
has_range: boolean;
|
||||
min_amount: number;
|
||||
max_amount: number;
|
||||
payment_method: string;
|
||||
is_explicit: boolean;
|
||||
premium: number;
|
||||
satoshis: number;
|
||||
maker: number;
|
||||
taker: number;
|
||||
escrow_duration: number;
|
||||
total_secs_exp: number;
|
||||
penalty: Date | undefined;
|
||||
is_maker: boolean;
|
||||
is_taker: boolean;
|
||||
is_participant: boolean;
|
||||
maker_status: 'Active' | 'Seen recently' | 'Inactive';
|
||||
taker_status: 'Active' | 'Seen recently' | 'Inactive';
|
||||
price_now: number | undefined;
|
||||
satoshis_now: number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
premium_now: number | undefined;
|
||||
premium_percentile: number;
|
||||
num_similar_orders: number;
|
||||
tg_enabled: boolean; // deprecated
|
||||
tg_token: string;
|
||||
tg_bot_name: string;
|
||||
is_buyer: boolean;
|
||||
is_seller: boolean;
|
||||
maker_nick: string;
|
||||
maker_hash_id: string;
|
||||
taker_nick: string;
|
||||
taker_hash_id: string;
|
||||
status_message: string;
|
||||
is_fiat_sent: boolean;
|
||||
is_disputed: boolean;
|
||||
ur_nick: string;
|
||||
maker_locked: boolean;
|
||||
taker_locked: boolean;
|
||||
escrow_locked: boolean;
|
||||
trade_satoshis: number;
|
||||
bond_invoice: string;
|
||||
bond_satoshis: number;
|
||||
escrow_invoice: string;
|
||||
escrow_satoshis: number;
|
||||
invoice_amount: number;
|
||||
swap_allowed: boolean;
|
||||
swap_failure_reason: string;
|
||||
suggested_mining_fee_rate: number;
|
||||
swap_fee_rate: number;
|
||||
pending_cancel: boolean;
|
||||
asked_for_cancel: boolean;
|
||||
statement_submitted: boolean;
|
||||
retries: number;
|
||||
next_retry_time: Date;
|
||||
failure_reason: string;
|
||||
invoice_expired: boolean;
|
||||
public_duration: number;
|
||||
bond_size: string;
|
||||
trade_fee_percent: number;
|
||||
bond_size_sats: number;
|
||||
bond_size_percent: number;
|
||||
chat_last_index: number;
|
||||
maker_summary: TradeRobotSummary;
|
||||
taker_summary: TradeRobotSummary;
|
||||
platform_summary: TradeCoordinatorSummary;
|
||||
expiry_reason: number;
|
||||
expiry_message: string;
|
||||
num_satoshis: number;
|
||||
sent_satoshis: number;
|
||||
txid: string;
|
||||
tx_queued: boolean;
|
||||
address: string;
|
||||
network: 'mainnet' | 'testnet';
|
||||
shortAlias: string;
|
||||
bad_request?: string;
|
||||
bad_address?: string;
|
||||
bad_invoice?: string;
|
||||
bad_statement?: string;
|
||||
}
|
||||
class Order {
|
||||
constructor(attributes: object) {
|
||||
Object.assign(this, attributes);
|
||||
}
|
||||
|
||||
export const defaultOrder: Order = {
|
||||
shortAlias: '',
|
||||
id: 0,
|
||||
status: 0,
|
||||
created_at: new Date(),
|
||||
expires_at: new Date(),
|
||||
type: 0,
|
||||
currency: 0,
|
||||
amount: 0,
|
||||
has_range: false,
|
||||
min_amount: 0,
|
||||
max_amount: 0,
|
||||
payment_method: '',
|
||||
is_explicit: false,
|
||||
premium: 0,
|
||||
satoshis: 0,
|
||||
maker: 0,
|
||||
taker: 0,
|
||||
escrow_duration: 0,
|
||||
total_secs_exp: 0,
|
||||
penalty: undefined,
|
||||
is_maker: false,
|
||||
is_taker: false,
|
||||
is_participant: false,
|
||||
maker_status: 'Active',
|
||||
taker_status: 'Active',
|
||||
price_now: undefined,
|
||||
satoshis_now: 0,
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
premium_now: undefined,
|
||||
premium_percentile: 0,
|
||||
num_similar_orders: 0,
|
||||
tg_enabled: false,
|
||||
tg_token: '',
|
||||
tg_bot_name: '',
|
||||
is_buyer: false,
|
||||
is_seller: false,
|
||||
maker_nick: '',
|
||||
maker_hash_id: '',
|
||||
taker_nick: '',
|
||||
taker_hash_id: '',
|
||||
status_message: '',
|
||||
is_fiat_sent: false,
|
||||
is_disputed: false,
|
||||
ur_nick: '',
|
||||
maker_locked: false,
|
||||
taker_locked: false,
|
||||
escrow_locked: false,
|
||||
trade_satoshis: 0,
|
||||
bond_invoice: '',
|
||||
bond_satoshis: 0,
|
||||
escrow_invoice: '',
|
||||
escrow_satoshis: 0,
|
||||
invoice_amount: 0,
|
||||
swap_allowed: false,
|
||||
swap_failure_reason: '',
|
||||
suggested_mining_fee_rate: 0,
|
||||
swap_fee_rate: 0,
|
||||
pending_cancel: false,
|
||||
asked_for_cancel: false,
|
||||
statement_submitted: false,
|
||||
retries: 0,
|
||||
next_retry_time: new Date(),
|
||||
failure_reason: '',
|
||||
invoice_expired: false,
|
||||
public_duration: 0,
|
||||
bond_size: '',
|
||||
trade_fee_percent: 0,
|
||||
bond_size_sats: 0,
|
||||
bond_size_percent: 0,
|
||||
chat_last_index: 0,
|
||||
maker_summary: {
|
||||
id: number = 0;
|
||||
status: number = 0;
|
||||
created_at: Date = new Date();
|
||||
expires_at: Date = new Date();
|
||||
type: number = 0;
|
||||
currency: number = 0;
|
||||
amount: number = 0;
|
||||
has_range: boolean = false;
|
||||
min_amount: number = 0;
|
||||
max_amount: number = 0;
|
||||
payment_method: string = '';
|
||||
is_explicit: boolean = false;
|
||||
premium: number = 0;
|
||||
satoshis: number = 0;
|
||||
maker: number = 0;
|
||||
taker: number = 0;
|
||||
escrow_duration: number = 0;
|
||||
total_secs_exp: number = 0;
|
||||
penalty: Date | undefined = undefined;
|
||||
is_maker: boolean = false;
|
||||
is_taker: boolean = false;
|
||||
is_participant: boolean = false;
|
||||
maker_status: 'Active' | 'Seen recently' | 'Inactive' = 'Active';
|
||||
taker_status: 'Active' | 'Seen recently' | 'Inactive' = 'Active';
|
||||
price_now: number | undefined = undefined;
|
||||
satoshis_now: number = 0;
|
||||
latitude: number = 0;
|
||||
longitude: number = 0;
|
||||
premium_now: number | undefined = undefined;
|
||||
premium_percentile: number = 0;
|
||||
num_similar_orders: number = 0;
|
||||
tg_enabled: boolean = false; // deprecated
|
||||
tg_token: string = '';
|
||||
tg_bot_name: string = '';
|
||||
is_buyer: boolean = false;
|
||||
is_seller: boolean = false;
|
||||
maker_nick: string = '';
|
||||
maker_hash_id: string = '';
|
||||
taker_nick: string = '';
|
||||
taker_hash_id: string = '';
|
||||
status_message: string = '';
|
||||
is_fiat_sent: boolean = false;
|
||||
is_disputed: boolean = false;
|
||||
ur_nick: string = '';
|
||||
maker_locked: boolean = false;
|
||||
taker_locked: boolean = false;
|
||||
escrow_locked: boolean = false;
|
||||
trade_satoshis: number = 0;
|
||||
bond_invoice: string = '';
|
||||
bond_satoshis: number = 0;
|
||||
escrow_invoice: string = '';
|
||||
escrow_satoshis: number = 0;
|
||||
invoice_amount: number = 0;
|
||||
swap_allowed: boolean = false;
|
||||
swap_failure_reason: string = '';
|
||||
suggested_mining_fee_rate: number = 0;
|
||||
swap_fee_rate: number = 0;
|
||||
pending_cancel: boolean = false;
|
||||
asked_for_cancel: boolean = false;
|
||||
statement_submitted: boolean = false;
|
||||
retries: number = 0;
|
||||
next_retry_time: Date = new Date();
|
||||
failure_reason: string = '';
|
||||
invoice_expired: boolean = false;
|
||||
public_duration: number = 0;
|
||||
bond_size: string = '';
|
||||
trade_fee_percent: number = 0;
|
||||
bond_size_sats: number = 0;
|
||||
bond_size_percent: number = 0;
|
||||
chat_last_index: number = 0;
|
||||
maker_summary: TradeRobotSummary = {
|
||||
is_buyer: false,
|
||||
sent_fiat: 0,
|
||||
received_sats: 0,
|
||||
@ -197,8 +136,9 @@ export const defaultOrder: Order = {
|
||||
sent_sats: 0,
|
||||
received_fiat: 0,
|
||||
trade_fee_sats: 0,
|
||||
},
|
||||
taker_summary: {
|
||||
};
|
||||
|
||||
taker_summary: TradeRobotSummary = {
|
||||
is_buyer: false,
|
||||
sent_fiat: 0,
|
||||
received_sats: 0,
|
||||
@ -210,22 +150,118 @@ export const defaultOrder: Order = {
|
||||
sent_sats: 0,
|
||||
received_fiat: 0,
|
||||
trade_fee_sats: 0,
|
||||
},
|
||||
platform_summary: {
|
||||
};
|
||||
|
||||
platform_summary: TradeCoordinatorSummary = {
|
||||
contract_timestamp: new Date(),
|
||||
contract_total_time: 0,
|
||||
contract_exchange_rate: 0,
|
||||
routing_budget_sats: 0,
|
||||
trade_revenue_sats: 0,
|
||||
},
|
||||
expiry_reason: 0,
|
||||
expiry_message: '',
|
||||
num_satoshis: 0,
|
||||
sent_satoshis: 0,
|
||||
txid: '',
|
||||
tx_queued: false,
|
||||
address: '',
|
||||
network: 'mainnet',
|
||||
};
|
||||
};
|
||||
|
||||
expiry_reason: number = 0;
|
||||
expiry_message: string = '';
|
||||
num_satoshis: number = 0;
|
||||
sent_satoshis: number = 0;
|
||||
txid: string = '';
|
||||
tx_queued: boolean = false;
|
||||
address: string = '';
|
||||
network: 'mainnet' | 'testnet' = 'mainnet';
|
||||
shortAlias: string = '';
|
||||
bad_request?: string = '';
|
||||
bad_address?: string = '';
|
||||
bad_invoice?: string = '';
|
||||
bad_statement?: string = '';
|
||||
|
||||
update = (attributes: Record<string, any>): Order => {
|
||||
Object.assign(this, attributes);
|
||||
return this;
|
||||
};
|
||||
|
||||
make: (federation: Federation, slot: Slot) => Promise<this> = async (federation, slot) => {
|
||||
const body = {
|
||||
type: this.type,
|
||||
currency: this.currency,
|
||||
amount: this.has_range ? null : this.amount,
|
||||
has_range: this.has_range,
|
||||
min_amount: this.min_amount,
|
||||
max_amount: this.max_amount,
|
||||
payment_method: this.payment_method,
|
||||
is_explicit: this.is_explicit,
|
||||
premium: this.is_explicit ? null : this.premium,
|
||||
satoshis: this.is_explicit ? this.satoshis : null,
|
||||
public_duration: this.public_duration,
|
||||
escrow_duration: this.escrow_duration,
|
||||
bond_size: this.bond_size,
|
||||
latitude: this.latitude,
|
||||
longitude: this.longitude,
|
||||
};
|
||||
|
||||
if (slot) {
|
||||
const coordinator = federation.getCoordinator(this.shortAlias);
|
||||
const { basePath, url } = coordinator;
|
||||
const data = await apiClient
|
||||
.post(url + basePath, '/api/make/', body, {
|
||||
tokenSHA256: slot?.getRobot()?.tokenSHA256 ?? '',
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
if (data) this.update(data);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
take: (federation: Federation, slot: Slot, takeAmount: string) => Promise<this> = async (
|
||||
federation,
|
||||
slot,
|
||||
takeAmount,
|
||||
) => {
|
||||
return this.submitAction(federation, slot, {
|
||||
action: 'take',
|
||||
amount: this?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount),
|
||||
});
|
||||
};
|
||||
|
||||
submitAction: (federation: Federation, slot: Slot, action: SubmitActionProps) => Promise<this> =
|
||||
async (federation, slot, action) => {
|
||||
if (this.id < 1) return this;
|
||||
|
||||
if (slot) {
|
||||
const coordinator = federation.getCoordinator(this.shortAlias);
|
||||
const { basePath, url } = coordinator;
|
||||
const data = await apiClient
|
||||
.post(url + basePath, `/api/order/?order_id=${Number(this.id)}`, action, {
|
||||
tokenSHA256: slot?.getRobot()?.tokenSHA256 ?? '',
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
if (data) this.update(data);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
fecth: (federation: Federation, slot: Slot) => Promise<this> = async (federation, slot) => {
|
||||
if (this.id < 1) return this;
|
||||
if (!slot) return this;
|
||||
|
||||
const coordinator = federation.getCoordinator(this.shortAlias);
|
||||
const authHeaders = slot.getRobot()?.getAuthHeaders();
|
||||
if (!authHeaders) return this;
|
||||
const { basePath, url } = coordinator;
|
||||
const data = await apiClient
|
||||
.get(url + basePath, `/api/order/?order_id=${this.id}`, authHeaders)
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
|
||||
if (data) this.update(data);
|
||||
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
export default Order;
|
||||
|
@ -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;
|
||||
|
@ -1,88 +1,170 @@
|
||||
import { sha256 } from 'js-sha256';
|
||||
import { type Coordinator, type Garage, Robot, type Order } from '.';
|
||||
import { Robot, Order, type Federation } from '.';
|
||||
import { roboidentitiesClient } from '../services/Roboidentities/Web';
|
||||
import { hexToBase91, validateTokenEntropy } from '../utils';
|
||||
|
||||
export interface AuthHeaders {
|
||||
tokenSHA256: string;
|
||||
keys: {
|
||||
pubKey: string;
|
||||
encPrivKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
class Slot {
|
||||
constructor(
|
||||
token: string,
|
||||
shortAliases: string[],
|
||||
robotAttributes: Record<any, any>,
|
||||
onRobotUpdate: () => void,
|
||||
onSlotUpdate: () => void,
|
||||
) {
|
||||
this.onSlotUpdate = onSlotUpdate;
|
||||
this.token = token;
|
||||
|
||||
this.hashId = sha256(sha256(this.token));
|
||||
this.nickname = null;
|
||||
void roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => {
|
||||
this.nickname = nickname;
|
||||
onRobotUpdate();
|
||||
onSlotUpdate();
|
||||
});
|
||||
void roboidentitiesClient.generateRobohash(this.hashId, 'small');
|
||||
void roboidentitiesClient.generateRobohash(this.hashId, 'large');
|
||||
|
||||
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
|
||||
const tokenSHA256 = hexToBase91(sha256(token));
|
||||
|
||||
this.robots = shortAliases.reduce((acc: Record<string, Robot>, shortAlias: string) => {
|
||||
acc[shortAlias] = new Robot(robotAttributes);
|
||||
acc[shortAlias] = new Robot({
|
||||
...robotAttributes,
|
||||
shortAlias,
|
||||
hasEnoughEntropy,
|
||||
bitsEntropy,
|
||||
shannonEntropy,
|
||||
tokenSHA256,
|
||||
pubKey: robotAttributes.pubKey,
|
||||
encPrivKey: robotAttributes.encPrivKey,
|
||||
});
|
||||
this.updateSlotFromRobot(acc[shortAlias]);
|
||||
return acc;
|
||||
}, {});
|
||||
this.order = null;
|
||||
|
||||
this.activeShortAlias = null;
|
||||
this.lastShortAlias = null;
|
||||
this.copiedToken = false;
|
||||
onRobotUpdate();
|
||||
this.onSlotUpdate();
|
||||
}
|
||||
|
||||
token: string | null;
|
||||
hashId: string | null;
|
||||
nickname: string | null;
|
||||
robots: Record<string, Robot>;
|
||||
order: Order | null;
|
||||
activeShortAlias: string | null;
|
||||
lastShortAlias: string | null;
|
||||
activeOrder: Order | null = null;
|
||||
lastOrder: Order | null = null;
|
||||
copiedToken: boolean;
|
||||
|
||||
onSlotUpdate: () => void;
|
||||
|
||||
setCopiedToken = (copied: boolean): void => {
|
||||
this.copiedToken = copied;
|
||||
};
|
||||
|
||||
// Robots
|
||||
getRobot = (shortAlias?: string): Robot | null => {
|
||||
if (shortAlias) {
|
||||
return this.robots[shortAlias];
|
||||
} else if (this.activeShortAlias !== null && this.robots[this.activeShortAlias]) {
|
||||
return this.robots[this.activeShortAlias];
|
||||
} else if (this.lastShortAlias !== null && this.robots[this.lastShortAlias]) {
|
||||
return this.robots[this.lastShortAlias];
|
||||
} else if (this.activeOrder?.id) {
|
||||
return this.robots[this.activeOrder.shortAlias];
|
||||
} else if (this.lastOrder?.id && this.robots[this.lastOrder.shortAlias]) {
|
||||
return this.robots[this.lastOrder.shortAlias];
|
||||
} else if (Object.values(this.robots).length > 0) {
|
||||
return Object.values(this.robots)[0];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
updateRobot = (shortAlias: string, attributes: Record<any, any>): Robot | null => {
|
||||
this.robots[shortAlias].update(attributes);
|
||||
|
||||
if (attributes.lastOrderId) {
|
||||
this.lastShortAlias = shortAlias;
|
||||
if (this.activeShortAlias === shortAlias) {
|
||||
this.activeShortAlias = null;
|
||||
}
|
||||
}
|
||||
if (attributes.activeOrderId) {
|
||||
this.activeShortAlias = attributes.shortAlias;
|
||||
}
|
||||
|
||||
return this.robots[shortAlias];
|
||||
fetchRobot = async (federation: Federation): Promise<void> => {
|
||||
Object.values(this.robots).forEach((robot) => {
|
||||
void robot.fetch(federation).then((robot) => {
|
||||
this.updateSlotFromRobot(robot);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
syncCoordinator: (coordinator: Coordinator, garage: Garage) => void = (coordinator, garage) => {
|
||||
updateSlotFromRobot = (robot: Robot | null): void => {
|
||||
if (robot?.lastOrderId && this.lastOrder?.id !== robot?.lastOrderId) {
|
||||
this.lastOrder = new Order({ id: robot.lastOrderId, shortAlias: robot.shortAlias });
|
||||
if (this.activeOrder?.id === robot.lastOrderId) {
|
||||
this.lastOrder = this.activeOrder;
|
||||
this.activeOrder = null;
|
||||
}
|
||||
}
|
||||
if (robot?.activeOrderId && this.activeOrder?.id !== robot.activeOrderId) {
|
||||
this.activeOrder = new Order({
|
||||
id: robot.activeOrderId,
|
||||
shortAlias: robot.shortAlias,
|
||||
});
|
||||
}
|
||||
this.onSlotUpdate();
|
||||
};
|
||||
|
||||
// Orders
|
||||
fetchActiveOrder = async (federation: Federation): Promise<void> => {
|
||||
void this.activeOrder?.fecth(federation, this);
|
||||
this.updateSlotFromOrder(this.activeOrder);
|
||||
};
|
||||
|
||||
takeOrder = async (federation: Federation, order: Order, takeAmount: string): Promise<Order> => {
|
||||
await order.take(federation, this, takeAmount);
|
||||
this.updateSlotFromOrder(order);
|
||||
return order;
|
||||
};
|
||||
|
||||
makeOrder = async (federation: Federation, attributes: object): Promise<Order> => {
|
||||
const order = new Order(attributes);
|
||||
await order.make(federation, this);
|
||||
this.lastOrder = this.activeOrder;
|
||||
this.activeOrder = order;
|
||||
this.onSlotUpdate();
|
||||
return this.activeOrder;
|
||||
};
|
||||
|
||||
updateSlotFromOrder: (newOrder: Order | null) => void = (newOrder) => {
|
||||
if (newOrder) {
|
||||
// FIXME: API responses with bad_request should include also order's status
|
||||
if (newOrder?.bad_request?.includes('expired')) newOrder.status = 5;
|
||||
if (newOrder?.bad_request?.includes('collaborativelly')) newOrder.status = 12;
|
||||
if (
|
||||
newOrder.id === this.activeOrder?.id &&
|
||||
newOrder.shortAlias === this.activeOrder?.shortAlias
|
||||
) {
|
||||
this.activeOrder?.update(newOrder);
|
||||
if (this.activeOrder?.bad_request) {
|
||||
this.lastOrder = this.activeOrder;
|
||||
this.activeOrder = null;
|
||||
}
|
||||
this.onSlotUpdate();
|
||||
} else if (newOrder?.is_participant && this.lastOrder?.id !== newOrder.id) {
|
||||
this.activeOrder = newOrder;
|
||||
this.onSlotUpdate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
syncCoordinator: (federation: Federation, shortAlias: string) => void = (
|
||||
federation,
|
||||
shortAlias,
|
||||
) => {
|
||||
const defaultRobot = this.getRobot();
|
||||
if (defaultRobot?.token) {
|
||||
this.robots[coordinator.shortAlias] = new Robot({
|
||||
this.robots[shortAlias] = new Robot({
|
||||
shortAlias,
|
||||
hasEnoughEntropy: defaultRobot.hasEnoughEntropy,
|
||||
bitsEntropy: defaultRobot.bitsEntropy,
|
||||
shannonEntropy: defaultRobot.shannonEntropy,
|
||||
token: defaultRobot.token,
|
||||
pubKey: defaultRobot.pubKey,
|
||||
encPrivKey: defaultRobot.encPrivKey,
|
||||
});
|
||||
void coordinator.fetchRobot(garage, defaultRobot.token);
|
||||
void this.robots[shortAlias].fetch(federation);
|
||||
this.updateSlotFromRobot(this.robots[shortAlias]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -14,20 +14,11 @@ interface FederationWidgetProps {
|
||||
onTouchEnd?: () => void;
|
||||
}
|
||||
|
||||
const FederationWidget = React.forwardRef(function Component(
|
||||
{
|
||||
layout,
|
||||
gridCellSize,
|
||||
style,
|
||||
className,
|
||||
onMouseDown,
|
||||
onMouseUp,
|
||||
onTouchEnd,
|
||||
}: FederationWidgetProps,
|
||||
ref,
|
||||
) {
|
||||
const { federation, coordinatorUpdatedAt } =
|
||||
useContext<UseFederationStoreType>(FederationContext);
|
||||
const FederationWidget = React.forwardRef(function Component({
|
||||
layout,
|
||||
gridCellSize,
|
||||
}: FederationWidgetProps) {
|
||||
const { federation, federationUpdatedAt } = useContext<UseFederationStoreType>(FederationContext);
|
||||
|
||||
return React.useMemo(() => {
|
||||
return (
|
||||
@ -38,7 +29,7 @@ const FederationWidget = React.forwardRef(function Component(
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}, [federation, coordinatorUpdatedAt]);
|
||||
}, [federation, federationUpdatedAt]);
|
||||
});
|
||||
|
||||
export default FederationWidget;
|
||||
|
@ -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;
|
||||
|
@ -1,47 +1,4 @@
|
||||
{
|
||||
"exp": {
|
||||
"longAlias": "Experimental",
|
||||
"shortAlias": "exp",
|
||||
"description": "RoboSats node for development and experimentation. This is the original RoboSats coordinator operated by the RoboSats devs since 2022.",
|
||||
"motto": "Original Robohost. P2P FTW!",
|
||||
"color": "#1976d2",
|
||||
"established": "2022-03-01",
|
||||
"contact": {
|
||||
"email": "robosats@protonmail.com",
|
||||
"telegram": "robosats",
|
||||
"twitter": "robosats",
|
||||
"reddit": "r/robosats",
|
||||
"matrix": "#robosats:matrix.org",
|
||||
"website": "https://learn.robosats.com",
|
||||
"nostr": "npub1p2psats79rypr8lpnl9t5qdekfp700x660qsgw284xvq4s09lqrqqk3m82",
|
||||
"pgp": "/static/federation/pgp/B4AB5F19113D4125DDF217739C4585B561315571.asc",
|
||||
"fingerprint": "B4AB5F19113D4125DDF217739C4585B561315571"
|
||||
},
|
||||
"badges": {
|
||||
"isFounder": true,
|
||||
"donatesToDevFund": 20,
|
||||
"hasGoodOpSec": true,
|
||||
"robotsLove": true,
|
||||
"hasLargeLimits": true
|
||||
},
|
||||
"policies": {
|
||||
"Experimental": "Experimental coordinator used for development. Use at your own risk.",
|
||||
"Dispute Policy": "Evidence in Disputes: In the event of a dispute, users will be asked to provide transaction-related evidence. This could include transaction IDs, screenshots of payment confirmations, or other pertinent transaction records. Personal information or unrelated transaction details should be redacted to maintain privacy.",
|
||||
"Non eligible countries": "USA citizens and residents are not allowed to use the Experimental coordinator. F2F transactions are explicitly blocked at creation time for US locations. If a US citizen or resident violates this rule and is found out to be using the Experimental coordinator during a dispute process, they will be denied further service and the dispute mediation will be terminated."
|
||||
},
|
||||
"mainnet": {
|
||||
"onion": "http://robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion",
|
||||
"clearnet": "https://unsafe.robosats.com",
|
||||
"i2p": "http://r7r4sckft6ptmk4r2jajiuqbowqyxiwsle4iyg4fijtoordc6z7a.b32.i2p"
|
||||
},
|
||||
"testnet": {
|
||||
"onion": "http://robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion",
|
||||
"clearnet": "https://unsafe.testnet.robosats.com",
|
||||
"i2p": ""
|
||||
},
|
||||
"mainnetNodesPubkeys": ["0282eb467bc073833a039940392592bf10cf338a830ba4e392c1667d7697654c7e"],
|
||||
"testnetNodesPubkeys": ["03ecb271b3e2e36f2b91c92c65bab665e5165f8cdfdada1b5f46cfdd3248c87fd6"]
|
||||
},
|
||||
"temple": {
|
||||
"longAlias": "Temple of Sats",
|
||||
"shortAlias": "temple",
|
||||
|
Loading…
Reference in New Issue
Block a user