From a92ca62bb169c7c48e783055a2ff925be10828c8 Mon Sep 17 00:00:00 2001 From: koalasat Date: Thu, 15 Aug 2024 16:26:04 +0200 Subject: [PATCH 1/2] Refactor Order/Slot models --- frontend/src/App.tsx | 8 +- frontend/src/basic/BookPage/index.tsx | 7 - frontend/src/basic/MakerPage/index.tsx | 9 +- frontend/src/basic/NavBar/NavBar.tsx | 20 +- frontend/src/basic/OrderPage/index.tsx | 90 ++--- frontend/src/basic/RobotPage/RobotProfile.tsx | 31 +- frontend/src/basic/RobotPage/index.tsx | 3 +- frontend/src/basic/SettingsPage/index.tsx | 3 + frontend/src/components/BookTable/index.tsx | 5 +- .../components/Charts/DepthChart/index.tsx | 5 +- frontend/src/components/Dialogs/Exchange.tsx | 4 +- frontend/src/components/Dialogs/Profile.tsx | 4 +- .../src/components/FederationTable/index.tsx | 8 +- .../src/components/MakerForm/MakerForm.tsx | 46 +-- .../src/components/Notifications/index.tsx | 14 +- .../components/OrderDetails/TakeButton.tsx | 48 +-- frontend/src/components/RobotInfo/index.tsx | 59 +-- .../EncryptedSocketChat/index.tsx | 6 +- .../EncryptedTurtleChat/index.tsx | 14 +- .../TradeBox/EncryptedChat/index.tsx | 4 - .../src/components/TradeBox/Prompts/Chat.tsx | 7 +- frontend/src/components/TradeBox/index.tsx | 150 +++---- frontend/src/contexts/FederationContext.tsx | 24 -- frontend/src/contexts/GarageContext.tsx | 127 ++++-- frontend/src/models/Coordinator.model.ts | 138 +------ frontend/src/models/Federation.model.ts | 19 +- frontend/src/models/Garage.model.ts | 69 +--- frontend/src/models/Order.model.ts | 375 ++++++++++-------- frontend/src/models/Robot.model.ts | 107 ++++- frontend/src/models/Slot.model.ts | 139 +++++-- frontend/src/models/index.ts | 8 +- frontend/src/pro/Widgets/Federation.tsx | 21 +- frontend/src/pro/Widgets/Maker.tsx | 4 +- 33 files changed, 756 insertions(+), 820 deletions(-) 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; From 06fc2e8bd745d2f692f5d865bbb88fc471340349 Mon Sep 17 00:00:00 2001 From: koalasat <koalasat@satstralia.com> Date: Tue, 10 Sep 2024 17:55:29 +0200 Subject: [PATCH 2/2] Some fixes --- frontend/src/basic/Routes.tsx | 3 +- .../components/OrderDetails/TakeButton.tsx | 8 +- .../src/components/OrderDetails/index.tsx | 4 +- frontend/src/contexts/AppContext.tsx | 6 +- frontend/src/contexts/FederationContext.tsx | 93 +------------------ frontend/src/models/Garage.model.ts | 8 +- frontend/src/models/Maker.model.ts | 9 +- frontend/src/models/Order.model.ts | 11 +++ frontend/src/models/Slot.model.ts | 7 ++ frontend/static/federation.json | 43 --------- 10 files changed, 44 insertions(+), 148 deletions(-) diff --git a/frontend/src/basic/Routes.tsx b/frontend/src/basic/Routes.tsx index 909a7e45..b9a22110 100644 --- a/frontend/src/basic/Routes.tsx +++ b/frontend/src/basic/Routes.tsx @@ -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}`); diff --git a/frontend/src/components/OrderDetails/TakeButton.tsx b/frontend/src/components/OrderDetails/TakeButton.tsx index 00f7081a..acd5d1fe 100644 --- a/frontend/src/components/OrderDetails/TakeButton.tsx +++ b/frontend/src/components/OrderDetails/TakeButton.tsx @@ -319,12 +319,8 @@ const TakeButton = ({ setLoadingTake(true); - currentOrder - .submitAction(federation, slot, { - action: 'take', - amount: - currentOrder?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount), - }) + slot + .takeOrder(federation, currentOrder, takeAmount) .then((order) => { if (order?.bad_request !== undefined) { setBadRequest(order.bad_request); diff --git a/frontend/src/components/OrderDetails/index.tsx b/frontend/src/components/OrderDetails/index.tsx index 1f4437ec..a368df36 100644 --- a/frontend/src/components/OrderDetails/index.tsx +++ b/frontend/src/components/OrderDetails/index.tsx @@ -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; diff --git a/frontend/src/contexts/AppContext.tsx b/frontend/src/contexts/AppContext.tsx index 26bc3c36..db7e9f13 100644 --- a/frontend/src/contexts/AppContext.tsx +++ b/frontend/src/contexts/AppContext.tsx @@ -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(); diff --git a/frontend/src/contexts/FederationContext.tsx b/frontend/src/contexts/FederationContext.tsx index 5aa0ab5e..da541310 100644 --- a/frontend/src/contexts/FederationContext.tsx +++ b/frontend/src/contexts/FederationContext.tsx @@ -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'; @@ -28,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; @@ -40,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: () => {}, @@ -56,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) => { @@ -82,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> = { @@ -164,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]); @@ -191,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]); @@ -200,10 +119,6 @@ export const FederationContextProvider = ({ value={{ federation, sortedCoordinators, - currentOrderId, - setCurrentOrderId, - currentOrder, - setDelay, coordinatorUpdatedAt, federationUpdatedAt, addNewCoordinator, diff --git a/frontend/src/models/Garage.model.ts b/frontend/src/models/Garage.model.ts index 6c9b5c0f..509a5a51 100644 --- a/frontend/src/models/Garage.model.ts +++ b/frontend/src/models/Garage.model.ts @@ -79,7 +79,7 @@ class Garage { // 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) => { @@ -100,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; }; @@ -108,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 = ( @@ -118,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 ); }; diff --git a/frontend/src/models/Maker.model.ts b/frontend/src/models/Maker.model.ts index 569417cd..f5517e33 100644 --- a/frontend/src/models/Maker.model.ts +++ b/frontend/src/models/Maker.model.ts @@ -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; diff --git a/frontend/src/models/Order.model.ts b/frontend/src/models/Order.model.ts index ada55f0f..5eae7383 100644 --- a/frontend/src/models/Order.model.ts +++ b/frontend/src/models/Order.model.ts @@ -214,6 +214,17 @@ class Order { 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; diff --git a/frontend/src/models/Slot.model.ts b/frontend/src/models/Slot.model.ts index b38ea8c7..1c8990af 100644 --- a/frontend/src/models/Slot.model.ts +++ b/frontend/src/models/Slot.model.ts @@ -111,6 +111,12 @@ class Slot { 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); @@ -124,6 +130,7 @@ class Slot { 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 diff --git a/frontend/static/federation.json b/frontend/static/federation.json index ece01fe6..9bfd3527 100644 --- a/frontend/static/federation.json +++ b/frontend/static/federation.json @@ -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",