diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 90cee64b..9644bf7c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,8 +21,8 @@ const App = (): JSX.Element => { - - + + {window.NativeRobosats === undefined && window.RobosatsClient === undefined ? ( @@ -30,8 +30,8 @@ const App = (): JSX.Element => { )}
- - + + diff --git a/frontend/src/basic/BookPage/index.tsx b/frontend/src/basic/BookPage/index.tsx index ed74cc24..3b9ebf45 100644 --- a/frontend/src/basic/BookPage/index.tsx +++ b/frontend/src/basic/BookPage/index.tsx @@ -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(AppContext); - const { setDelay, setCurrentOrderId } = useContext(FederationContext); const { garage } = useContext(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 => { > { - navigate(`/order/${id}`); - }} onClickGenerateRobot={() => { navigate('/robot'); }} diff --git a/frontend/src/basic/MakerPage/index.tsx b/frontend/src/basic/MakerPage/index.tsx index 142cf451..67e6d6d7 100644 --- a/frontend/src/basic/MakerPage/index.tsx +++ b/frontend/src/basic/MakerPage/index.tsx @@ -14,8 +14,7 @@ import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageCon const MakerPage = (): JSX.Element => { const { fav, windowSize, navbarHeight } = useContext(AppContext); - const { federation, setDelay, setCurrentOrderId } = - useContext(FederationContext); + const { federation } = useContext(FederationContext); const { garage, maker } = useContext(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 => { }} > { - setCurrentOrderId({ id, shortAlias }); - navigate(`/order/${shortAlias}/${id}`); - }} disableRequest={matches.length > 0 && !showMatches} collapseAll={showMatches} onSubmit={() => { diff --git a/frontend/src/basic/NavBar/NavBar.tsx b/frontend/src/basic/NavBar/NavBar.tsx index 9cabcfd0..ecd8f85c 100644 --- a/frontend/src/basic/NavBar/NavBar.tsx +++ b/frontend/src/basic/NavBar/NavBar.tsx @@ -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(AppContext); - const { garage, robotUpdatedAt } = useContext(GarageContext); - const { setCurrentOrderId } = useContext(FederationContext); + const { garage, slotUpdatedAt } = useContext(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={} iconPosition='start' /> diff --git a/frontend/src/basic/OrderPage/index.tsx b/frontend/src/basic/OrderPage/index.tsx index aa2f3e74..af582549 100644 --- a/frontend/src/basic/OrderPage/index.tsx +++ b/frontend/src/basic/OrderPage/index.tsx @@ -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(AppContext); - const { federation, currentOrder, currentOrderId, setCurrentOrderId } = - useContext(FederationContext); - const { badOrder } = useContext(GarageContext); + const { windowSize, setOpen, acknowledgedWarning, setAcknowledgedWarning, navbarHeight } = + useContext(AppContext); + const { federation } = useContext(FederationContext); + const { garage } = useContext(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(hostUrl); + const [currentOrder, setCurrentOrder] = useState(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 ? ( - + ) : ( <> ); @@ -95,20 +94,19 @@ const OrderPage = (): JSX.Element => { return ( { - setOpen(closeAll); setAcknowledgedWarning(true); }} longAlias={federation.getCoordinator(params.shortAlias ?? '')?.longAlias} /> - {currentOrder === null && badOrder === undefined && } - {badOrder !== undefined ? ( + {!currentOrder?.maker_hash_id && } + {currentOrder?.bad_request && currentOrder.status !== 5 ? ( - {t(badOrder)} + {t(currentOrder.bad_request)} ) : null} - {currentOrder !== null && badOrder === undefined ? ( + {currentOrder?.maker_hash_id && (!currentOrder.bad_request || currentOrder.status === 5) ? ( currentOrder.is_participant ? ( windowSize.width > doublePageWidth ? ( // DOUBLE PAPER VIEW diff --git a/frontend/src/basic/RobotPage/RobotProfile.tsx b/frontend/src/basic/RobotPage/RobotProfile.tsx index 4095d41a..97b0db30 100644 --- a/frontend/src/basic/RobotPage/RobotProfile.tsx +++ b/frontend/src/basic/RobotPage/RobotProfile.tsx @@ -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(AppContext); - const { garage, robotUpdatedAt, orderUpdatedAt } = useContext(GarageContext); - const { setCurrentOrderId } = useContext(FederationContext); + const { garage, slotUpdatedAt } = useContext(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) ? ( {t('Welcome back!')} @@ -156,38 +154,38 @@ const RobotProfile = ({ )} - {loadingCoordinators > 0 && !robot?.activeOrderId ? ( + {loadingCoordinators > 0 && !slot?.activeOrder?.id ? ( {t('Looking for orders!')} ) : null} - {Boolean(robot?.activeOrderId) && Boolean(slot?.hashId) ? ( + {slot?.activeOrder ? ( ) : null} - {Boolean(robot?.lastOrderId) && Boolean(slot?.hashId) ? ( + {!slot?.activeOrder?.id && Boolean(slot?.lastOrder?.id) ? ( @@ -210,10 +208,7 @@ const RobotProfile = ({ ) : null} - {!robot?.activeOrderId && - slot?.hashId && - !robot?.lastOrderId && - loadingCoordinators === 0 ? ( + {!slot?.activeOrder && !slot?.lastOrder && loadingCoordinators === 0 ? ( {t('No existing orders found')} ) : null} diff --git a/frontend/src/basic/RobotPage/index.tsx b/frontend/src/basic/RobotPage/index.tsx index 0902c2aa..d6c39081 100644 --- a/frontend/src/basic/RobotPage/index.tsx +++ b/frontend/src/basic/RobotPage/index.tsx @@ -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) => { diff --git a/frontend/src/basic/SettingsPage/index.tsx b/frontend/src/basic/SettingsPage/index.tsx index a47360e5..af9a8d53 100644 --- a/frontend/src/basic/SettingsPage/index.tsx +++ b/frontend/src/basic/SettingsPage/index.tsx @@ -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(AppContext); const { federation, addNewCoordinator } = useContext(FederationContext); + const { garage } = useContext(GarageContext); const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3; const [newAlias, setNewAlias] = useState(''); const [newUrl, setNewUrl] = useState(''); @@ -26,6 +28,7 @@ const SettingsPage = (): JSX.Element => { fullNewUrl = `http://${newUrl}`; } addNewCoordinator(newAlias, fullNewUrl); + garage.syncCoordinator(federation, newAlias); setNewAlias(''); setNewUrl(''); } else { diff --git a/frontend/src/components/BookTable/index.tsx b/frontend/src/components/BookTable/index.tsx index a42399e3..eb017cce 100644 --- a/frontend/src/components/BookTable/index.tsx +++ b/frontend/src/components/BookTable/index.tsx @@ -88,8 +88,7 @@ const BookTable = ({ onOrderClicked = () => null, }: BookTableProps): JSX.Element => { const { fav, setOpen } = useContext(AppContext); - const { federation, coordinatorUpdatedAt } = - useContext(FederationContext); + const { federation, federationUpdatedAt } = useContext(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 { diff --git a/frontend/src/components/Charts/DepthChart/index.tsx b/frontend/src/components/Charts/DepthChart/index.tsx index 89fdd118..474723a6 100644 --- a/frontend/src/components/Charts/DepthChart/index.tsx +++ b/frontend/src/components/Charts/DepthChart/index.tsx @@ -47,8 +47,7 @@ const DepthChart: React.FC = ({ onOrderClicked = () => null, }) => { const { fav } = useContext(AppContext); - const { federation, coordinatorUpdatedAt, federationUpdatedAt } = - useContext(FederationContext); + const { federation, federationUpdatedAt } = useContext(FederationContext); const { t } = useTranslation(); const theme = useTheme(); const [enrichedOrders, setEnrichedOrders] = useState([]); @@ -81,7 +80,7 @@ const DepthChart: React.FC = ({ }); setEnrichedOrders(enriched); } - }, [coordinatorUpdatedAt, currencyCode]); + }, [federationUpdatedAt, currencyCode]); useEffect(() => { if (enrichedOrders.length > 0) { diff --git a/frontend/src/components/Dialogs/Exchange.tsx b/frontend/src/components/Dialogs/Exchange.tsx index f7be51d3..3ea20200 100644 --- a/frontend/src/components/Dialogs/Exchange.tsx +++ b/frontend/src/components/Dialogs/Exchange.tsx @@ -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(0); useEffect(() => { const loadedCoordinators = federation.exchange.enabledCoordinators - federation.exchange.loadingCoordinators; setLoadingProgress((loadedCoordinators / federation.exchange.enabledCoordinators) * 100); - }, [open, coordinatorUpdatedAt, federationUpdatedAt]); + }, [open, federationUpdatedAt]); return ( diff --git a/frontend/src/components/Dialogs/Profile.tsx b/frontend/src/components/Dialogs/Profile.tsx index 1c2dcc40..192533f6 100644 --- a/frontend/src/components/Dialogs/Profile.tsx +++ b/frontend/src/components/Dialogs/Profile.tsx @@ -27,7 +27,7 @@ interface Props { const ProfileDialog = ({ open = false, onClose }: Props): JSX.Element => { const { federation } = useContext(FederationContext); - const { garage, robotUpdatedAt } = useContext(GarageContext); + const { garage, slotUpdatedAt } = useContext(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 ( { const { t } = useTranslation(); - const { federation, sortedCoordinators, coordinatorUpdatedAt, federationUpdatedAt } = + const { federation, sortedCoordinators, federationUpdatedAt } = useContext(FederationContext); const { setOpen, settings } = useContext(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 = { diff --git a/frontend/src/components/MakerForm/MakerForm.tsx b/frontend/src/components/MakerForm/MakerForm.tsx index 8b7642bc..42b256f8 100644 --- a/frontend/src/components/MakerForm/MakerForm.tsx +++ b/frontend/src/components/MakerForm/MakerForm.tsx @@ -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(AppContext); - const { federation, coordinatorUpdatedAt, federationUpdatedAt } = - useContext(FederationContext); + const { fav, setFav } = useContext(AppContext); + const { federation, federationUpdatedAt } = useContext(FederationContext); const { maker, setMaker, garage } = useContext(GarageContext); const { t } = useTranslation(); const theme = useTheme(); + const navigate = useNavigate(); const [badRequest, setBadRequest] = useState(null); const [amountLimits, setAmountLimits] = useState([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 }); diff --git a/frontend/src/components/Notifications/index.tsx b/frontend/src/components/Notifications/index.tsx index 4e02de53..415177d0 100644 --- a/frontend/src/components/Notifications/index.tsx +++ b/frontend/src/components/Notifications/index.tsx @@ -69,7 +69,7 @@ const Notifications = ({ }: NotificationsProps): JSX.Element => { const { t } = useTranslation(); const navigate = useNavigate(); - const { garage, orderUpdatedAt } = useContext(GarageContext); + const { garage, slotUpdatedAt } = useContext(GarageContext); const [message, setMessage] = useState(emptyNotificationMessage); const [inFocus, setInFocus] = useState(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(() => { diff --git a/frontend/src/components/OrderDetails/TakeButton.tsx b/frontend/src/components/OrderDetails/TakeButton.tsx index e406aaad..00f7081a 100644 --- a/frontend/src/components/OrderDetails/TakeButton.tsx +++ b/frontend/src/components/OrderDetails/TakeButton.tsx @@ -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(AppContext); - const { garage, orderUpdatedAt } = useContext(GarageContext); - const { federation, setCurrentOrderId } = useContext(FederationContext); + const navigate = useNavigate(); + const { garage, slotUpdatedAt } = useContext(GarageContext); + const { federation } = useContext(FederationContext); const [takeAmount, setTakeAmount] = useState(''); const [badRequest, setBadRequest] = useState(''); @@ -79,7 +78,7 @@ const TakeButton = ({ useEffect(() => { setSatoshis(satoshisNow() ?? ''); - }, [orderUpdatedAt, takeAmount, info]); + }, [slotUpdatedAt, takeAmount, info]); const currencyCode: string = currentOrder?.currency === 1000 ? 'Sats' : currencies[`${Number(currentOrder?.currency)}`]; @@ -165,7 +164,7 @@ const TakeButton = ({ } else { return null; } - }, [orderUpdatedAt, takeAmount]); + }, [slotUpdatedAt, takeAmount]); const onTakeOrderClicked = function (): void { if (currentOrder?.maker_status === 'Inactive') { @@ -184,7 +183,7 @@ const TakeButton = ({ takeAmount === '' || takeAmount == null ); - }, [takeAmount, orderUpdatedAt]); + }, [takeAmount, slotUpdatedAt]); const takeOrderButton = function (): JSX.Element { if (currentOrder?.has_range) { @@ -314,31 +313,24 @@ const TakeButton = ({ }; const takeOrder = function (): void { - const robot = garage.getSlot()?.getRobot() ?? null; + const slot = garage.getSlot(); - if (currentOrder === null || robot === null) return; + if (currentOrder === null || slot === null) return; setLoadingTake(true); - const { url, basePath } = federation - .getCoordinator(currentOrder.shortAlias) - .getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); - setCurrentOrderId({ id: null, shortAlias: null }); - apiClient - .post( - url + basePath, - `/api/order/?order_id=${String(currentOrder?.id)}`, - { - action: 'take', - amount: currentOrder?.currency === 1000 ? takeAmount / 100000000 : takeAmount, - }, - { tokenSHA256: robot?.tokenSHA256 }, - ) - .then((data) => { - if (data?.bad_request !== undefined) { - setBadRequest(data.bad_request); + + currentOrder + .submitAction(federation, slot, { + action: 'take', + amount: + currentOrder?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount), + }) + .then((order) => { + if (order?.bad_request !== undefined) { + setBadRequest(order.bad_request); } else { - setCurrentOrderId({ id: currentOrder?.id, shortAlias: currentOrder?.shortAlias }); setBadRequest(''); + navigate(`/order/${order.shortAlias}/${order.id}`); } }) .catch(() => { diff --git a/frontend/src/components/RobotInfo/index.tsx b/frontend/src/components/RobotInfo/index.tsx index 382a222b..03546855 100644 --- a/frontend/src/components/RobotInfo/index.tsx +++ b/frontend/src/components/RobotInfo/index.tsx @@ -41,7 +41,7 @@ interface Props { const RobotInfo: React.FC = ({ coordinator, onClose, disabled }: Props) => { const { garage } = useContext(GarageContext); - const { setCurrentOrderId } = useContext(FederationContext); + const { federation } = useContext(FederationContext); const navigate = useNavigate(); const { t } = useTranslation(); @@ -55,6 +55,8 @@ const RobotInfo: React.FC = ({ coordinator, onClose, disabled }: Props) = const [weblnEnabled, setWeblnEnabled] = useState(false); const [openEnableTelegram, setOpenEnableTelegram] = useState(false); + const robot = garage.getSlot()?.getRobot(coordinator.shortAlias); + const handleWebln = async (): Promise => { void getWebln() .then(() => { @@ -72,7 +74,6 @@ const RobotInfo: React.FC = ({ coordinator, onClose, disabled }: Props) = const handleWeblnInvoiceClicked = async (e: 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 = ({ 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 = ({ 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 ( }> @@ -123,28 +115,21 @@ const RobotInfo: React.FC = ({ coordinator, onClose, disabled }: Props) = {(robot?.earnedRewards ?? 0) > 0 && (  {t('Claim Sats!')} )} - {slot?.activeShortAlias === coordinator.shortAlias && ( + {robot?.activeOrderId ? (  {t('Active order!')} - )} - {slot?.lastShortAlias === coordinator.shortAlias && ( -  {t('finished order')} + ) : ( + robot?.lastOrderId &&  {t('finished order')} )} - {slot?.activeShortAlias === coordinator.shortAlias ? ( + {robot?.activeOrderId ? ( { - 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 = ({ coordinator, onClose, disabled }: Props) = - ) : (robot?.lastOrderId ?? 0) > 0 && slot?.lastShortAlias === coordinator.shortAlias ? ( + ) : robot?.lastOrderId ? ( { - 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 = ({ coordinator, onClose, disabled }: Props) = @@ -253,7 +230,7 @@ const RobotInfo: React.FC = ({ coordinator, onClose, disabled }: Props) = { - setStealthInvoice(!robot?.stealthInvoices); + setStealthInvoice(); }} /> } diff --git a/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx index 2653f10d..0710e94d 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/EncryptedSocketChat/index.tsx @@ -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 = ({ takerHashId, messages, setMessages, - baseUrl, turtleMode, setTurtleMode, }: Props): JSX.Element => { const { t } = useTranslation(); const theme = useTheme(); const { origin, hostUrl, settings } = useContext(AppContext); - const { garage, robotUpdatedAt } = useContext(GarageContext); + const { garage, slotUpdatedAt } = useContext(GarageContext); const { federation } = useContext(FederationContext); const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`)); @@ -78,7 +76,7 @@ const EncryptedSocketChat: React.FC = ({ if (!connected && Boolean(garage.getSlot()?.hashId)) { connectWebsocket(); } - }, [connected, robotUpdatedAt]); + }, [connected, slotUpdatedAt]); // Make sure to not keep reconnecting once status is not Chat useEffect(() => { diff --git a/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx index 43de3a8f..f897a3af 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx @@ -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 = ({ chatOffset, messages, setMessages, - baseUrl, setTurtleMode, turtleMode, }: Props): JSX.Element => { @@ -91,7 +89,7 @@ const EncryptedTurtleChat: React.FC = ({ }, [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 = ({ .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 = ({ // 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 = ({ 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 = ({ 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 = ({ order_id: order.id, offset: lastIndex, }, - { tokenSHA256: robot?.tokenSHA256 }, + { tokenSHA256: slot?.getRobot()?.tokenSHA256 }, ) .then((response) => { if (response != null) { diff --git a/frontend/src/components/TradeBox/EncryptedChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/index.tsx index 3d6b8573..de08a760 100644 --- a/frontend/src/components/TradeBox/EncryptedChat/index.tsx +++ b/frontend/src/components/TradeBox/EncryptedChat/index.tsx @@ -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 = ({ order, chatOffset, - baseUrl, setMessages, messages, status, @@ -49,7 +47,6 @@ const EncryptedChat: React.FC = ({ 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 = ({ takerHashId={order.taker_hash_id} makerHashId={order.maker_hash_id} userNick={order.ur_nick} - baseUrl={baseUrl} turtleMode={turtleMode} setTurtleMode={setTurtleMode} /> diff --git a/frontend/src/components/TradeBox/Prompts/Chat.tsx b/frontend/src/components/TradeBox/Prompts/Chat.tsx index 5bf6958f..78bfa18a 100644 --- a/frontend/src/components/TradeBox/Prompts/Chat.tsx +++ b/frontend/src/components/TradeBox/Prompts/Chat.tsx @@ -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(GarageContext); + const { slotUpdatedAt } = useContext(GarageContext); const [sentButton, setSentButton] = useState(false); const [receivedButton, setReceivedButton] = useState(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 ( diff --git a/frontend/src/components/TradeBox/index.tsx b/frontend/src/components/TradeBox/index.tsx index bdfa70c8..223e8db3 100644 --- a/frontend/src/components/TradeBox/index.tsx +++ b/frontend/src/components/TradeBox/index.tsx @@ -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(GarageContext); - const { settings, hostUrl, origin } = useContext(AppContext); - const { federation, setCurrentOrderId } = useContext(FederationContext); +const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element => { + const { garage, slotUpdatedAt } = useContext(GarageContext); + const { settings } = useContext(AppContext); + const { federation } = useContext(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} /> { }} onCollabCancelClick={cancel} loading={loadingButtons.cancel} - peerAskedCancel={garage.getSlot()?.order?.pending_cancel ?? false} + peerAskedCancel={currentOrder?.pending_cancel ?? false} /> { setOpen(closeAll); @@ -790,14 +765,14 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => { /> { setOpen(closeAll); }} onConfirmClick={confirmFiatReceived} /> - + { > { {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 }); diff --git a/frontend/src/contexts/FederationContext.tsx b/frontend/src/contexts/FederationContext.tsx index b9ce609b..5aa0ab5e 100644 --- a/frontend/src/contexts/FederationContext.tsx +++ b/frontend/src/contexts/FederationContext.tsx @@ -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; diff --git a/frontend/src/contexts/GarageContext.tsx b/frontend/src/contexts/GarageContext.tsx index 3d5289ec..61ee9bb7 100644 --- a/frontend/src/contexts/GarageContext.tsx +++ b/frontend/src/contexts/GarageContext.tsx @@ -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} diff --git a/frontend/src/models/Coordinator.model.ts b/frontend/src/models/Coordinator.model.ts index 09b74fa1..0215a300 100644 --- a/frontend/src/models/Coordinator.model.ts +++ b/frontend/src/models/Coordinator.model.ts @@ -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; diff --git a/frontend/src/models/Federation.model.ts b/frontend/src/models/Federation.model.ts index 295fc90f..b85c87c4 100644 --- a/frontend/src/models/Federation.model.ts +++ b/frontend/src/models/Federation.model.ts @@ -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'); }); }; diff --git a/frontend/src/models/Garage.model.ts b/frontend/src/models/Garage.model.ts index 07645be9..6c9b5c0f 100644 --- a/frontend/src/models/Garage.model.ts +++ b/frontend/src/models/Garage.model.ts @@ -1,9 +1,9 @@ -import { type Coordinator, type Order } from '.'; +import { type Federation, Order } from '.'; import { systemClient } from '../services/System'; import { saveAsJson } from '../utils'; import Slot from './Slot.model'; -type GarageHooks = 'onRobotUpdate' | 'onOrderUpdate'; +type GarageHooks = 'onSlotUpdate'; class Garage { constructor() { @@ -11,8 +11,7 @@ class Garage { this.currentSlot = null; this.hooks = { - onRobotUpdate: [], - onOrderUpdate: [], + onSlotUpdate: [], }; this.loadSlots(); @@ -29,6 +28,7 @@ class Garage { }; triggerHook = (hookName: GarageHooks): void => { + this.save(); this.hooks[hookName]?.forEach((fn) => { fn(); }); @@ -47,8 +47,7 @@ class Garage { this.slots = {}; this.currentSlot = null; systemClient.deleteItem('garage_slots'); - this.triggerHook('onRobotUpdate'); - this.triggerHook('onOrderUpdate'); + this.triggerHook('onSlotUpdate'); }; loadSlots = (): void => { @@ -64,19 +63,16 @@ class Garage { Object.keys(rawSlot.robots), {}, () => { - this.triggerHook('onRobotUpdate'); + this.triggerHook('onSlotUpdate'); }, ); - Object.keys(rawSlot.robots).forEach((shortAlias) => { - const rawRobot = rawSlot.robots[shortAlias]; - this.updateRobot(rawSlot.token, shortAlias, rawRobot); - }); + this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.lastOrder)); + this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.activeOrder)); this.currentSlot = rawSlot?.token; } }); console.log('Robot Garage was loaded from local storage'); - this.triggerHook('onRobotUpdate'); - this.triggerHook('onOrderUpdate'); + this.triggerHook('onSlotUpdate'); } }; @@ -92,8 +88,7 @@ class Garage { Reflect.deleteProperty(this.slots, targetIndex); this.currentSlot = null; this.save(); - this.triggerHook('onRobotUpdate'); - this.triggerHook('onOrderUpdate'); + this.triggerHook('onSlotUpdate'); } }; @@ -138,55 +133,29 @@ class Garage { if (this.getSlot(token) === null) { this.slots[token] = new Slot(token, shortAliases, attributes, () => { - this.triggerHook('onRobotUpdate'); + this.triggerHook('onSlotUpdate'); }); this.save(); } }; - updateRobot: (token: string, shortAlias: string, attributes: Record<any, any>) => void = ( - token, - shortAlias, - attributes, - ) => { - if (!token || !shortAlias) return; - + fetchRobot = async (federation: Federation, token: string): Promise<void> => { const slot = this.getSlot(token); if (slot != null) { - slot.updateRobot(shortAlias, { token, ...attributes }); + await slot.fetchRobot(federation); this.save(); - this.triggerHook('onRobotUpdate'); - } - }; - - // Orders - updateOrder: (order: Order | null) => void = (order) => { - const slot = this.getSlot(); - if (slot != null) { - if (order !== null) { - const updatedOrder = slot.order ?? null; - if (updatedOrder !== null && updatedOrder.id === order.id) { - Object.assign(updatedOrder, order); - slot.order = updatedOrder; - } else { - slot.order = order; - } - if (slot.order?.is_participant) { - slot.activeShortAlias = order.shortAlias; - } - } else { - slot.order = null; - } - this.save(); - this.triggerHook('onOrderUpdate'); + this.triggerHook('onSlotUpdate'); } }; // Coordinators - syncCoordinator: (coordinator: Coordinator) => void = (coordinator) => { + syncCoordinator: (federation: Federation, shortAlias: string) => void = ( + federation, + shortAlias, + ) => { Object.values(this.slots).forEach((slot) => { - slot.syncCoordinator(coordinator, this); + slot.syncCoordinator(federation, shortAlias); }); this.save(); }; diff --git a/frontend/src/models/Order.model.ts b/frontend/src/models/Order.model.ts index 62a82b96..ada55f0f 100644 --- a/frontend/src/models/Order.model.ts +++ b/frontend/src/models/Order.model.ts @@ -1,3 +1,28 @@ +import { apiClient } from '../services/api'; +import type Federation from './Federation.model'; +import type Slot from './Slot.model'; + +export interface SubmitActionProps { + action: + | 'cancel' + | 'dispute' + | 'pause' + | 'confirm' + | 'undo_confirm' + | 'update_invoice' + | 'update_address' + | 'submit_statement' + | 'rate_platform' + | 'take'; + invoice?: string; + routing_budget_ppm?: number; + address?: string; + mining_fee_rate?: number; + statement?: string; + rating?: number; + amount?: number; +} + export interface TradeRobotSummary { is_buyer: boolean; sent_fiat: number; @@ -24,168 +49,82 @@ export interface TradeCoordinatorSummary { trade_revenue_sats: number; } -export interface Order { - id: number; - status: number; - created_at: Date; - expires_at: Date; - type: number; - currency: number; - amount: number; - has_range: boolean; - min_amount: number; - max_amount: number; - payment_method: string; - is_explicit: boolean; - premium: number; - satoshis: number; - maker: number; - taker: number; - escrow_duration: number; - total_secs_exp: number; - penalty: Date | undefined; - is_maker: boolean; - is_taker: boolean; - is_participant: boolean; - maker_status: 'Active' | 'Seen recently' | 'Inactive'; - taker_status: 'Active' | 'Seen recently' | 'Inactive'; - price_now: number | undefined; - satoshis_now: number; - latitude: number; - longitude: number; - premium_now: number | undefined; - premium_percentile: number; - num_similar_orders: number; - tg_enabled: boolean; // deprecated - tg_token: string; - tg_bot_name: string; - is_buyer: boolean; - is_seller: boolean; - maker_nick: string; - maker_hash_id: string; - taker_nick: string; - taker_hash_id: string; - status_message: string; - is_fiat_sent: boolean; - is_disputed: boolean; - ur_nick: string; - maker_locked: boolean; - taker_locked: boolean; - escrow_locked: boolean; - trade_satoshis: number; - bond_invoice: string; - bond_satoshis: number; - escrow_invoice: string; - escrow_satoshis: number; - invoice_amount: number; - swap_allowed: boolean; - swap_failure_reason: string; - suggested_mining_fee_rate: number; - swap_fee_rate: number; - pending_cancel: boolean; - asked_for_cancel: boolean; - statement_submitted: boolean; - retries: number; - next_retry_time: Date; - failure_reason: string; - invoice_expired: boolean; - public_duration: number; - bond_size: string; - trade_fee_percent: number; - bond_size_sats: number; - bond_size_percent: number; - chat_last_index: number; - maker_summary: TradeRobotSummary; - taker_summary: TradeRobotSummary; - platform_summary: TradeCoordinatorSummary; - expiry_reason: number; - expiry_message: string; - num_satoshis: number; - sent_satoshis: number; - txid: string; - tx_queued: boolean; - address: string; - network: 'mainnet' | 'testnet'; - shortAlias: string; - bad_request?: string; - bad_address?: string; - bad_invoice?: string; - bad_statement?: string; -} +class Order { + constructor(attributes: object) { + Object.assign(this, attributes); + } -export const defaultOrder: Order = { - shortAlias: '', - id: 0, - status: 0, - created_at: new Date(), - expires_at: new Date(), - type: 0, - currency: 0, - amount: 0, - has_range: false, - min_amount: 0, - max_amount: 0, - payment_method: '', - is_explicit: false, - premium: 0, - satoshis: 0, - maker: 0, - taker: 0, - escrow_duration: 0, - total_secs_exp: 0, - penalty: undefined, - is_maker: false, - is_taker: false, - is_participant: false, - maker_status: 'Active', - taker_status: 'Active', - price_now: undefined, - satoshis_now: 0, - latitude: 0, - longitude: 0, - premium_now: undefined, - premium_percentile: 0, - num_similar_orders: 0, - tg_enabled: false, - tg_token: '', - tg_bot_name: '', - is_buyer: false, - is_seller: false, - maker_nick: '', - maker_hash_id: '', - taker_nick: '', - taker_hash_id: '', - status_message: '', - is_fiat_sent: false, - is_disputed: false, - ur_nick: '', - maker_locked: false, - taker_locked: false, - escrow_locked: false, - trade_satoshis: 0, - bond_invoice: '', - bond_satoshis: 0, - escrow_invoice: '', - escrow_satoshis: 0, - invoice_amount: 0, - swap_allowed: false, - swap_failure_reason: '', - suggested_mining_fee_rate: 0, - swap_fee_rate: 0, - pending_cancel: false, - asked_for_cancel: false, - statement_submitted: false, - retries: 0, - next_retry_time: new Date(), - failure_reason: '', - invoice_expired: false, - public_duration: 0, - bond_size: '', - trade_fee_percent: 0, - bond_size_sats: 0, - bond_size_percent: 0, - chat_last_index: 0, - maker_summary: { + id: number = 0; + status: number = 0; + created_at: Date = new Date(); + expires_at: Date = new Date(); + type: number = 0; + currency: number = 0; + amount: number = 0; + has_range: boolean = false; + min_amount: number = 0; + max_amount: number = 0; + payment_method: string = ''; + is_explicit: boolean = false; + premium: number = 0; + satoshis: number = 0; + maker: number = 0; + taker: number = 0; + escrow_duration: number = 0; + total_secs_exp: number = 0; + penalty: Date | undefined = undefined; + is_maker: boolean = false; + is_taker: boolean = false; + is_participant: boolean = false; + maker_status: 'Active' | 'Seen recently' | 'Inactive' = 'Active'; + taker_status: 'Active' | 'Seen recently' | 'Inactive' = 'Active'; + price_now: number | undefined = undefined; + satoshis_now: number = 0; + latitude: number = 0; + longitude: number = 0; + premium_now: number | undefined = undefined; + premium_percentile: number = 0; + num_similar_orders: number = 0; + tg_enabled: boolean = false; // deprecated + tg_token: string = ''; + tg_bot_name: string = ''; + is_buyer: boolean = false; + is_seller: boolean = false; + maker_nick: string = ''; + maker_hash_id: string = ''; + taker_nick: string = ''; + taker_hash_id: string = ''; + status_message: string = ''; + is_fiat_sent: boolean = false; + is_disputed: boolean = false; + ur_nick: string = ''; + maker_locked: boolean = false; + taker_locked: boolean = false; + escrow_locked: boolean = false; + trade_satoshis: number = 0; + bond_invoice: string = ''; + bond_satoshis: number = 0; + escrow_invoice: string = ''; + escrow_satoshis: number = 0; + invoice_amount: number = 0; + swap_allowed: boolean = false; + swap_failure_reason: string = ''; + suggested_mining_fee_rate: number = 0; + swap_fee_rate: number = 0; + pending_cancel: boolean = false; + asked_for_cancel: boolean = false; + statement_submitted: boolean = false; + retries: number = 0; + next_retry_time: Date = new Date(); + failure_reason: string = ''; + invoice_expired: boolean = false; + public_duration: number = 0; + bond_size: string = ''; + trade_fee_percent: number = 0; + bond_size_sats: number = 0; + bond_size_percent: number = 0; + chat_last_index: number = 0; + maker_summary: TradeRobotSummary = { is_buyer: false, sent_fiat: 0, received_sats: 0, @@ -197,8 +136,9 @@ export const defaultOrder: Order = { sent_sats: 0, received_fiat: 0, trade_fee_sats: 0, - }, - taker_summary: { + }; + + taker_summary: TradeRobotSummary = { is_buyer: false, sent_fiat: 0, received_sats: 0, @@ -210,22 +150,107 @@ export const defaultOrder: Order = { sent_sats: 0, received_fiat: 0, trade_fee_sats: 0, - }, - platform_summary: { + }; + + platform_summary: TradeCoordinatorSummary = { contract_timestamp: new Date(), contract_total_time: 0, contract_exchange_rate: 0, routing_budget_sats: 0, trade_revenue_sats: 0, - }, - expiry_reason: 0, - expiry_message: '', - num_satoshis: 0, - sent_satoshis: 0, - txid: '', - tx_queued: false, - address: '', - network: 'mainnet', -}; + }; + + expiry_reason: number = 0; + expiry_message: string = ''; + num_satoshis: number = 0; + sent_satoshis: number = 0; + txid: string = ''; + tx_queued: boolean = false; + address: string = ''; + network: 'mainnet' | 'testnet' = 'mainnet'; + shortAlias: string = ''; + bad_request?: string = ''; + bad_address?: string = ''; + bad_invoice?: string = ''; + bad_statement?: string = ''; + + update = (attributes: Record<string, any>): Order => { + Object.assign(this, attributes); + return this; + }; + + make: (federation: Federation, slot: Slot) => Promise<this> = async (federation, slot) => { + const body = { + type: this.type, + currency: this.currency, + amount: this.has_range ? null : this.amount, + has_range: this.has_range, + min_amount: this.min_amount, + max_amount: this.max_amount, + payment_method: this.payment_method, + is_explicit: this.is_explicit, + premium: this.is_explicit ? null : this.premium, + satoshis: this.is_explicit ? this.satoshis : null, + public_duration: this.public_duration, + escrow_duration: this.escrow_duration, + bond_size: this.bond_size, + latitude: this.latitude, + longitude: this.longitude, + }; + + if (slot) { + const coordinator = federation.getCoordinator(this.shortAlias); + const { basePath, url } = coordinator; + const data = await apiClient + .post(url + basePath, '/api/make/', body, { + tokenSHA256: slot?.getRobot()?.tokenSHA256 ?? '', + }) + .catch((e) => { + console.log(e); + }); + if (data) this.update(data); + } + + return this; + }; + + submitAction: (federation: Federation, slot: Slot, action: SubmitActionProps) => Promise<this> = + async (federation, slot, action) => { + if (this.id < 1) return this; + + if (slot) { + const coordinator = federation.getCoordinator(this.shortAlias); + const { basePath, url } = coordinator; + const data = await apiClient + .post(url + basePath, `/api/order/?order_id=${Number(this.id)}`, action, { + tokenSHA256: slot?.getRobot()?.tokenSHA256 ?? '', + }) + .catch((e) => { + console.log(e); + }); + if (data) this.update(data); + } + return this; + }; + + fecth: (federation: Federation, slot: Slot) => Promise<this> = async (federation, slot) => { + if (this.id < 1) return this; + if (!slot) return this; + + const coordinator = federation.getCoordinator(this.shortAlias); + const authHeaders = slot.getRobot()?.getAuthHeaders(); + if (!authHeaders) return this; + const { basePath, url } = coordinator; + const data = await apiClient + .get(url + basePath, `/api/order/?order_id=${this.id}`, authHeaders) + .catch((e) => { + console.log(e); + }); + + if (data) this.update(data); + + return this; + }; +} export default Order; diff --git a/frontend/src/models/Robot.model.ts b/frontend/src/models/Robot.model.ts index ed741f95..a3c6114d 100644 --- a/frontend/src/models/Robot.model.ts +++ b/frontend/src/models/Robot.model.ts @@ -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; diff --git a/frontend/src/models/Slot.model.ts b/frontend/src/models/Slot.model.ts index 4d3fa904..b38ea8c7 100644 --- a/frontend/src/models/Slot.model.ts +++ b/frontend/src/models/Slot.model.ts @@ -1,88 +1,163 @@ import { sha256 } from 'js-sha256'; -import { type Coordinator, type Garage, Robot, type Order } from '.'; +import { Robot, Order, type Federation } from '.'; import { roboidentitiesClient } from '../services/Roboidentities/Web'; +import { hexToBase91, validateTokenEntropy } from '../utils'; + +export interface AuthHeaders { + tokenSHA256: string; + keys: { + pubKey: string; + encPrivKey: string; + }; +} class Slot { constructor( token: string, shortAliases: string[], robotAttributes: Record<any, any>, - onRobotUpdate: () => void, + onSlotUpdate: () => void, ) { + this.onSlotUpdate = onSlotUpdate; this.token = token; this.hashId = sha256(sha256(this.token)); this.nickname = null; void roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => { this.nickname = nickname; - onRobotUpdate(); + onSlotUpdate(); }); void roboidentitiesClient.generateRobohash(this.hashId, 'small'); void roboidentitiesClient.generateRobohash(this.hashId, 'large'); + const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token); + const tokenSHA256 = hexToBase91(sha256(token)); + this.robots = shortAliases.reduce((acc: Record<string, Robot>, shortAlias: string) => { - acc[shortAlias] = new Robot(robotAttributes); + acc[shortAlias] = new Robot({ + ...robotAttributes, + shortAlias, + hasEnoughEntropy, + bitsEntropy, + shannonEntropy, + tokenSHA256, + pubKey: robotAttributes.pubKey, + encPrivKey: robotAttributes.encPrivKey, + }); + this.updateSlotFromRobot(acc[shortAlias]); return acc; }, {}); - this.order = null; - this.activeShortAlias = null; - this.lastShortAlias = null; this.copiedToken = false; - onRobotUpdate(); + this.onSlotUpdate(); } token: string | null; hashId: string | null; nickname: string | null; robots: Record<string, Robot>; - order: Order | null; - activeShortAlias: string | null; - lastShortAlias: string | null; + activeOrder: Order | null = null; + lastOrder: Order | null = null; copiedToken: boolean; + onSlotUpdate: () => void; + setCopiedToken = (copied: boolean): void => { this.copiedToken = copied; }; + // Robots getRobot = (shortAlias?: string): Robot | null => { if (shortAlias) { return this.robots[shortAlias]; - } else if (this.activeShortAlias !== null && this.robots[this.activeShortAlias]) { - return this.robots[this.activeShortAlias]; - } else if (this.lastShortAlias !== null && this.robots[this.lastShortAlias]) { - return this.robots[this.lastShortAlias]; + } else if (this.activeOrder?.id) { + return this.robots[this.activeOrder.shortAlias]; + } else if (this.lastOrder?.id && this.robots[this.lastOrder.shortAlias]) { + return this.robots[this.lastOrder.shortAlias]; } else if (Object.values(this.robots).length > 0) { return Object.values(this.robots)[0]; } return null; }; - updateRobot = (shortAlias: string, attributes: Record<any, any>): Robot | null => { - this.robots[shortAlias].update(attributes); - - if (attributes.lastOrderId) { - this.lastShortAlias = shortAlias; - if (this.activeShortAlias === shortAlias) { - this.activeShortAlias = null; - } - } - if (attributes.activeOrderId) { - this.activeShortAlias = attributes.shortAlias; - } - - return this.robots[shortAlias]; + fetchRobot = async (federation: Federation): Promise<void> => { + Object.values(this.robots).forEach((robot) => { + void robot.fetch(federation).then((robot) => { + this.updateSlotFromRobot(robot); + }); + }); }; - syncCoordinator: (coordinator: Coordinator, garage: Garage) => void = (coordinator, garage) => { + updateSlotFromRobot = (robot: Robot | null): void => { + if (robot?.lastOrderId && this.lastOrder?.id !== robot?.lastOrderId) { + this.lastOrder = new Order({ id: robot.lastOrderId, shortAlias: robot.shortAlias }); + if (this.activeOrder?.id === robot.lastOrderId) { + this.lastOrder = this.activeOrder; + this.activeOrder = null; + } + } + if (robot?.activeOrderId && this.activeOrder?.id !== robot.activeOrderId) { + this.activeOrder = new Order({ + id: robot.activeOrderId, + shortAlias: robot.shortAlias, + }); + } + this.onSlotUpdate(); + }; + + // Orders + fetchActiveOrder = async (federation: Federation): Promise<void> => { + void this.activeOrder?.fecth(federation, this); + this.updateSlotFromOrder(this.activeOrder); + }; + + makeOrder = async (federation: Federation, attributes: object): Promise<Order> => { + const order = new Order(attributes); + await order.make(federation, this); + this.lastOrder = this.activeOrder; + this.activeOrder = order; + this.onSlotUpdate(); + return this.activeOrder; + }; + + updateSlotFromOrder: (newOrder: Order | null) => void = (newOrder) => { + if (newOrder) { + // FIXME: API responses with bad_request should include also order's status + if (newOrder?.bad_request?.includes('expired')) newOrder.status = 5; + if ( + newOrder.id === this.activeOrder?.id && + newOrder.shortAlias === this.activeOrder?.shortAlias + ) { + this.activeOrder?.update(newOrder); + if (this.activeOrder?.bad_request) { + this.lastOrder = this.activeOrder; + this.activeOrder = null; + } + this.onSlotUpdate(); + } else if (newOrder?.is_participant && this.lastOrder?.id !== newOrder.id) { + this.activeOrder = newOrder; + this.onSlotUpdate(); + } + } + }; + + syncCoordinator: (federation: Federation, shortAlias: string) => void = ( + federation, + shortAlias, + ) => { const defaultRobot = this.getRobot(); if (defaultRobot?.token) { - this.robots[coordinator.shortAlias] = new Robot({ + this.robots[shortAlias] = new Robot({ + shortAlias, + hasEnoughEntropy: defaultRobot.hasEnoughEntropy, + bitsEntropy: defaultRobot.bitsEntropy, + shannonEntropy: defaultRobot.shannonEntropy, token: defaultRobot.token, pubKey: defaultRobot.pubKey, encPrivKey: defaultRobot.encPrivKey, }); - void coordinator.fetchRobot(garage, defaultRobot.token); + void this.robots[shortAlias].fetch(federation); + this.updateSlotFromRobot(this.robots[shortAlias]); } }; } diff --git a/frontend/src/models/index.ts b/frontend/src/models/index.ts index b989306c..dbc1b6bd 100644 --- a/frontend/src/models/index.ts +++ b/frontend/src/models/index.ts @@ -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'; diff --git a/frontend/src/pro/Widgets/Federation.tsx b/frontend/src/pro/Widgets/Federation.tsx index 571925ea..422a2831 100644 --- a/frontend/src/pro/Widgets/Federation.tsx +++ b/frontend/src/pro/Widgets/Federation.tsx @@ -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; diff --git a/frontend/src/pro/Widgets/Maker.tsx b/frontend/src/pro/Widgets/Maker.tsx index f3383af4..4372acd8 100644 --- a/frontend/src/pro/Widgets/Maker.tsx +++ b/frontend/src/pro/Widgets/Maker.tsx @@ -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;