Add robot garage (#370)

* Add garage model

* Add robot select to profile

* Replace Robot for Garage init

* Add Garage inners, not re-rendering

* Revert

* Collect new phrases and small fixes

* Small fixes

* Fix unencrypted # hack on Turtle chat

* Small fixes and collect phrases
This commit is contained in:
Reckless_Satoshi 2023-03-02 11:01:06 +00:00 committed by GitHub
parent d88c2a5eff
commit c0b8a6d3ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 673 additions and 497 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Grid, ButtonGroup, Dialog, Box } from '@mui/material';
import { useHistory } from 'react-router-dom';
@ -7,49 +7,24 @@ import DepthChart from '../../components/Charts/DepthChart';
import { NoRobotDialog } from '../../components/Dialogs';
import MakerForm from '../../components/MakerForm';
import BookTable from '../../components/BookTable';
import { Page } from '../NavBar';
import { Book, Favorites, LimitList, Maker } from '../../models';
// Icons
import { BarChart, FormatListBulleted } from '@mui/icons-material';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface BookPageProps {
book: Book;
limits: { list: LimitList; loading: boolean };
fetchLimits: () => void;
fav: Favorites;
setFav: (state: Favorites) => void;
onViewOrder: () => void;
fetchBook: () => void;
clearOrder: () => void;
windowSize: { width: number; height: number };
lastDayPremium: number;
maker: Maker;
setMaker: (state: Maker) => void;
hasRobot: boolean;
setPage: (state: Page) => void;
setCurrentOrder: (state: number) => void;
baseUrl: string;
}
const BookPage = ({
lastDayPremium = 0,
limits,
book = { orders: [], loading: true },
fetchBook,
fetchLimits,
clearOrder,
fav,
setFav,
onViewOrder,
maker,
setMaker,
windowSize,
hasRobot = false,
setPage = () => null,
setCurrentOrder = () => null,
baseUrl,
}: BookPageProps): JSX.Element => {
const BookPage = (): JSX.Element => {
const {
robot,
fetchBook,
clearOrder,
windowSize,
setPage,
setCurrentOrder,
baseUrl,
book,
setDelay,
setOrder,
} = useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const history = useHistory();
const [view, setView] = useState<'list' | 'depth'>('list');
@ -62,15 +37,16 @@ const BookPage = ({
const chartWidthEm = width - maxBookTableWidth;
useEffect(() => {
if (book.orders.length < 1) {
fetchBook(true, false);
} else {
fetchBook(false, true);
}
fetchBook();
}, []);
const onViewOrder = function () {
setOrder(undefined);
setDelay(10000);
};
const onOrderClicked = function (id: number) {
if (hasRobot) {
if (robot.avatarLoaded) {
history.push('/order/' + id);
setPage('order');
setCurrentOrder(id);
@ -108,6 +84,7 @@ const BookPage = ({
</ButtonGroup>
);
};
return (
<Grid container direction='column' alignItems='center' spacing={1} sx={{ minWidth: 400 }}>
<NoRobotDialog open={openNoRobot} onClose={() => setOpenNoRobot(false)} setPage={setPage} />
@ -115,21 +92,13 @@ const BookPage = ({
<Dialog open={openMaker} onClose={() => setOpenMaker(false)}>
<Box sx={{ maxWidth: '18em', padding: '0.5em' }}>
<MakerForm
limits={limits}
fetchLimits={fetchLimits}
maker={maker}
setMaker={setMaker}
fav={fav}
setFav={setFav}
setPage={setPage}
hasRobot={hasRobot}
hasRobot={robot.AvatarLoaded}
onOrderCreated={(id) => {
clearOrder();
setCurrentOrder(id);
setPage('order');
history.push('/order/' + id);
}}
baseUrl={baseUrl}
/>
</Box>
</Dialog>

View File

@ -19,38 +19,18 @@ import { AppContextProps, AppContext } from '../contexts/AppContext';
const Main = (): JSX.Element => {
const { t } = useTranslation();
const {
book,
fetchBook,
maker,
setMaker,
clearOrder,
torStatus,
settings,
limits,
fetchLimits,
robot,
setRobot,
fetchRobot,
setOrder,
setDelay,
info,
fav,
setFav,
baseUrl,
order,
page,
setPage,
slideDirection,
setSlideDirection,
currentOrder,
setCurrentOrder,
closeAll,
open,
setOpen,
windowSize,
badOrder,
navbarHeight,
setBadOrder,
} = useContext<AppContextProps>(AppContext);
const Router = window.NativeRobosats === undefined ? BrowserRouter : HashRouter;
@ -101,16 +81,7 @@ const Main = (): JSX.Element => {
appear={slideDirection.in != undefined}
>
<div>
<RobotPage
setPage={setPage}
torStatus={torStatus}
fetchRobot={fetchRobot}
setCurrentOrder={setCurrentOrder}
windowSize={windowSize}
robot={robot}
setRobot={setRobot}
baseUrl={baseUrl}
/>
<RobotPage />
</div>
</Slide>
)}
@ -123,27 +94,7 @@ const Main = (): JSX.Element => {
appear={slideDirection.in != undefined}
>
<div>
<BookPage
book={book}
fetchBook={fetchBook}
onViewOrder={() => {
setOrder(undefined);
setDelay(10000);
}}
limits={limits}
fetchLimits={fetchLimits}
fav={fav}
setFav={setFav}
maker={maker}
setMaker={setMaker}
clearOrder={clearOrder}
lastDayPremium={info.last_day_nonkyc_btc_premium}
windowSize={windowSize}
hasRobot={robot.avatarLoaded}
setPage={setPage}
setCurrentOrder={setCurrentOrder}
baseUrl={baseUrl}
/>
<BookPage />
</div>
</Slide>
</Route>
@ -155,7 +106,7 @@ const Main = (): JSX.Element => {
appear={slideDirection.in != undefined}
>
<div>
<MakerPage hasRobot={robot.avatarLoaded} />
<MakerPage />
</div>
</Slide>
</Route>
@ -169,10 +120,7 @@ const Main = (): JSX.Element => {
appear={slideDirection.in != undefined}
>
<div>
<OrderPage
locationOrderId={props.match.params.orderId}
hasRobot={robot.avatarLoaded}
/>
<OrderPage locationOrderId={props.match.params.orderId} />
</div>
</Slide>
)}
@ -192,19 +140,9 @@ const Main = (): JSX.Element => {
</Switch>
</Box>
<div style={{ alignContent: 'center', display: 'flex' }}>
<NavBar width={windowSize.width} height={navbarHeight} hasRobot={robot.avatarLoaded} />
<NavBar width={windowSize.width} height={navbarHeight} />
</div>
<MainDialogs
open={open}
setOpen={setOpen}
setRobot={setRobot}
setPage={setPage}
setCurrentOrder={setCurrentOrder}
info={info}
robot={robot}
closeAll={closeAll}
baseUrl={baseUrl}
/>
<MainDialogs />
</Router>
);
};

View File

@ -10,13 +10,18 @@ import BookTable from '../../components/BookTable';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface MakerPageProps {
hasRobot: boolean;
}
const MakerPage = ({ hasRobot = false }: MakerPageProps): JSX.Element => {
const { book, fav, maker, clearOrder, windowSize, setCurrentOrder, navbarHeight, setPage } =
useContext<AppContextProps>(AppContext);
const MakerPage = (): JSX.Element => {
const {
robot,
book,
fav,
maker,
clearOrder,
windowSize,
setCurrentOrder,
navbarHeight,
setPage,
} = useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const history = useHistory();
@ -74,7 +79,7 @@ const MakerPage = ({ hasRobot = false }: MakerPageProps): JSX.Element => {
setPage('order');
history.push('/order/' + id);
}}
hasRobot={hasRobot}
hasRobot={robot.avatarLoaded}
disableRequest={matches.length > 0 && !showMatches}
collapseAll={showMatches}
onSubmit={() => setShowMatches(matches.length > 0)}

View File

@ -20,17 +20,16 @@ import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface NavBarProps {
width: number;
height: number;
hasRobot: boolean;
}
const NavBar = ({ width, height, hasRobot = false }: NavBarProps): JSX.Element => {
const NavBar = ({ width, height }: NavBarProps): JSX.Element => {
const {
robot,
page,
settings,
setPage,
setSlideDirection,
open,
robot,
setOpen,
closeAll,
currentOrder,
@ -43,7 +42,7 @@ const NavBar = ({ width, height, hasRobot = false }: NavBarProps): JSX.Element =
const smallBar = width < 50;
const tabSx = smallBar
? { position: 'relative', bottom: robot.nickname ? '1em' : '0em', minWidth: '1em' }
? { position: 'relative', bottom: robot.avatarLoaded ? '0.9em' : '0.13em', minWidth: '1em' }
: { position: 'relative', bottom: '1em', minWidth: '2em' };
const pagesPosition = {
robot: 1,
@ -137,7 +136,7 @@ const NavBar = ({ width, height, hasRobot = false }: NavBarProps): JSX.Element =
sx={tabSx}
label={smallBar ? undefined : t('Order')}
value='order'
disabled={!hasRobot || currentOrder == undefined}
disabled={!robot.avatarLoaded || currentOrder == undefined}
icon={<Assignment />}
iconPosition='start'
/>

View File

@ -6,20 +6,18 @@ import { useHistory } from 'react-router-dom';
import TradeBox from '../../components/TradeBox';
import OrderDetails from '../../components/OrderDetails';
import { Page } from '../NavBar';
import { Order, Settings } from '../../models';
import { apiClient } from '../../services/api';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface OrderPageProps {
hasRobot: boolean;
locationOrderId: number;
}
const OrderPage = ({ hasRobot = false, locationOrderId }: OrderPageProps): JSX.Element => {
const OrderPage = ({ locationOrderId }: OrderPageProps): JSX.Element => {
const {
windowSize,
order,
robot,
settings,
setOrder,
setCurrentOrder,
@ -106,7 +104,7 @@ const OrderPage = ({ hasRobot = false, locationOrderId }: OrderPageProps): JSX.E
setOrder={setOrder}
baseUrl={baseUrl}
setPage={setPage}
hasRobot={hasRobot}
hasRobot={robot.avatarLoaded}
/>
</Paper>
</Grid>
@ -121,6 +119,7 @@ const OrderPage = ({ hasRobot = false, locationOrderId }: OrderPageProps): JSX.E
>
<TradeBox
order={order}
robot={robot}
settings={settings}
setOrder={setOrder}
setBadOrder={setBadOrder}
@ -158,12 +157,13 @@ const OrderPage = ({ hasRobot = false, locationOrderId }: OrderPageProps): JSX.E
setOrder={setOrder}
baseUrl={baseUrl}
setPage={setPage}
hasRobot={hasRobot}
hasRobot={robot.avatarLoaded}
/>
</div>
<div style={{ display: tab == 'contract' ? '' : 'none' }}>
<TradeBox
order={order}
robot={robot}
settings={settings}
setOrder={setOrder}
setBadOrder={setBadOrder}
@ -189,7 +189,7 @@ const OrderPage = ({ hasRobot = false, locationOrderId }: OrderPageProps): JSX.E
setOrder={setOrder}
baseUrl={baseUrl}
setPage={setPage}
hasRobot={hasRobot}
hasRobot={robot.avatarLoaded}
/>
</Paper>
)

View File

@ -49,17 +49,16 @@ const Onboarding = ({
}: OnboardingProps): JSX.Element => {
const { t } = useTranslation();
const history = useHistory();
const theme = useTheme();
const [step, setStep] = useState<'1' | '2' | '3'>('1');
const [generatedToken, setGeneratedToken] = useState<boolean>(false);
const [showMimickProgress, setShowMimickProgress] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const generateToken = () => {
setGeneratedToken(true);
setInputToken(genBase62Token(36));
setShowMimickProgress(true);
setTimeout(() => setShowMimickProgress(false), 1000);
setLoading(true);
setTimeout(() => setLoading(false), 1000);
};
const changePage = function (newPage: Page) {
@ -104,19 +103,16 @@ const Onboarding = ({
</Alert>
</Grid>
<Grid item sx={{ width: '100%' }}>
{showMimickProgress ? (
<LinearProgress sx={{ height: '0.7em' }} />
) : (
<TokenInput
autoFocusTarget='copyButton'
inputToken={inputToken}
setInputToken={setInputToken}
setRobot={setRobot}
badRequest={badRequest}
robot={robot}
onPressEnter={() => null}
/>
)}
<TokenInput
loading={loading}
autoFocusTarget='copyButton'
inputToken={inputToken}
setInputToken={setInputToken}
setRobot={setRobot}
badRequest={badRequest}
robot={robot}
onPressEnter={() => null}
/>
</Grid>
<Grid item>
<Typography>

View File

@ -1,22 +1,37 @@
import React, { useState } from 'react';
import React, { useState, useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { Button, Link, Grid, LinearProgress, Typography, Alert } from '@mui/material';
import { Bolt, Logout, Refresh } from '@mui/icons-material';
import {
Button,
Grid,
LinearProgress,
Typography,
Alert,
Select,
MenuItem,
Box,
useTheme,
Tooltip,
} from '@mui/material';
import { Bolt, Add, DeleteSweep, Logout, Download } from '@mui/icons-material';
import RobotAvatar from '../../components/RobotAvatar';
import TokenInput from './TokenInput';
import { Page } from '../NavBar';
import { Robot } from '../../models';
import { Slot, Robot } from '../../models';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
import { genBase62Token } from '../../utils';
import { LoadingButton } from '@mui/lab';
interface RobotProfileProps {
robot: Robot;
setRobot: (state: Robot) => void;
setView: (state: 'welcome' | 'onboarding' | 'recovery' | 'profile') => void;
getGenerateRobot: (token: string, slot?: number) => void;
inputToken: string;
setCurrentOrder: (state: number) => void;
logoutRobot: () => void;
inputToken: string;
setInputToken: (state: string) => void;
getGenerateRobot: (token: string) => void;
setPage: (state: Page) => void;
baseUrl: string;
badRequest: string;
@ -27,9 +42,9 @@ const RobotProfile = ({
robot,
setRobot,
inputToken,
getGenerateRobot,
setInputToken,
setCurrentOrder,
getGenerateRobot,
logoutRobot,
setPage,
setView,
@ -37,158 +52,287 @@ const RobotProfile = ({
baseUrl,
width,
}: RobotProfileProps): JSX.Element => {
const { currentSlot, garage, setCurrentSlot, windowSize } =
useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const theme = useTheme();
const history = useHistory();
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
if (robot.nickname && robot.avatarLoaded) {
setLoading(false);
}
}, [robot]);
const handleAddRobot = () => {
getGenerateRobot(genBase62Token(36), garage.slots.length);
setLoading(true);
};
const handleChangeSlot = (e) => {
const slot = e.target.value;
getGenerateRobot(garage.slots[slot].robot.token, slot);
setLoading(true);
};
return (
<Grid container direction='column' alignItems='center' spacing={2} padding={2}>
<Grid item sx={{ height: '2.3em', position: 'relative' }}>
{robot.avatarLoaded && robot.nickname ? (
<Typography align='center' component='h5' variant='h5'>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
}}
>
{width < 19 ? null : (
<Bolt
sx={{
color: '#fcba03',
height: '1.5em',
width: '1.5em',
}}
/>
)}
<b>{robot.nickname}</b>
{width < 19 ? null : (
<Bolt
sx={{
color: '#fcba03',
height: '1.5em',
width: '1.5em',
}}
/>
)}
</div>
</Typography>
) : (
<>
<b>{t('Building your robot!')}</b>
<LinearProgress />
</>
)}
</Grid>
<Grid item sx={{ width: `13.5em` }}>
<RobotAvatar
nickname={robot.nickname}
smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating'
imageStyle={{
transform: '',
border: '2px solid #555',
filter: 'drop-shadow(1px 1px 1px #000000)',
height: `12.4em`,
width: `12.4em`,
}}
tooltip={t('This is your trading avatar')}
tooltipPosition='top'
baseUrl={baseUrl}
/>
</Grid>
{robot.found ? (
<Typography align='center' variant='h6'>
{t('Welcome back!')}
</Typography>
) : (
<></>
)}
{robot.activeOrderId ? (
<Grid item>
<Button
onClick={() => {
history.push('/order/' + robot.activeOrderId);
setPage('order');
setCurrentOrder(robot.activeOrderId);
}}
>
{t('Active order #{{orderID}}', { orderID: robot.activeOrderId })}
</Button>
<Grid container direction='column' alignItems='center' spacing={1} padding={1} paddingTop={2}>
<Grid
item
container
direction='column'
alignItems='center'
spacing={1}
sx={{ width: '100%' }}
>
<Grid item sx={{ height: '2.3em', position: 'relative' }}>
{robot.avatarLoaded && robot.nickname ? (
<Typography align='center' component='h5' variant='h5'>
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
}}
>
{width < 19 ? null : (
<Bolt
sx={{
color: '#fcba03',
height: '1.5em',
width: '1.5em',
}}
/>
)}
<b>{robot.nickname}</b>
{width < 19 ? null : (
<Bolt
sx={{
color: '#fcba03',
height: '1.5em',
width: '1.5em',
}}
/>
)}
</div>
</Typography>
) : (
<>
<b>{t('Building your robot!')}</b>
<LinearProgress />
</>
)}
</Grid>
) : null}
{robot.lastOrderId ? (
<Grid item container direction='column' alignItems='center'>
<Grid item sx={{ width: `13.5em` }}>
<RobotAvatar
nickname={robot.nickname}
smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating'
imageStyle={{
transform: '',
border: '2px solid #555',
filter: 'drop-shadow(1px 1px 1px #000000)',
height: `12.4em`,
width: `12.4em`,
}}
tooltip={t('This is your trading avatar')}
tooltipPosition='top'
baseUrl={baseUrl}
/>
{robot.found && !robot.lastOrderId ? (
<Typography align='center' variant='h6'>
{t('Welcome back!')}
</Typography>
) : (
<></>
)}
</Grid>
{robot.activeOrderId && robot.avatarLoaded && robot.nickname ? (
<Grid item>
<Button
onClick={() => {
history.push('/order/' + robot.lastOrderId);
history.push('/order/' + robot.activeOrderId);
setPage('order');
setCurrentOrder(robot.lastOrderId);
setCurrentOrder(robot.activeOrderId);
}}
>
{t('Last order #{{orderID}}', { orderID: robot.lastOrderId })}
{t('Active order #{{orderID}}', { orderID: robot.activeOrderId })}
</Button>
</Grid>
<Grid item>
<Alert severity='warning'>
<Grid container direction='column' alignItems='center'>
<Grid item>
{t(
'Reusing trading identity degrades your privacy against other users, coordinators and observers.',
)}
) : null}
{robot.lastOrderId && robot.avatarLoaded && robot.nickname ? (
<Grid item container direction='column' alignItems='center'>
<Grid item>
<Button
onClick={() => {
history.push('/order/' + robot.lastOrderId);
setPage('order');
setCurrentOrder(robot.lastOrderId);
}}
>
{t('Last order #{{orderID}}', { orderID: robot.lastOrderId })}
</Button>
</Grid>
<Grid item>
<Alert severity='warning'>
<Grid container direction='column' alignItems='center'>
<Grid item>
{t(
'Reusing trading identity degrades your privacy against other users, coordinators and observers.',
)}
</Grid>
<Grid item sx={{ position: 'relative', right: '1em' }}>
<Button color='success' size='small' onClick={handleAddRobot}>
<Add />
{t('Add a new Robot')}
</Button>
</Grid>
</Grid>
<Grid item sx={{ position: 'relative', right: '1em' }}>
<Button
color='inherit'
size='small'
onClick={() => {
logoutRobot();
setView('welcome');
}}
>
<Refresh />
{t('Generate a new Robot')}
</Button>
</Grid>
</Grid>
</Alert>
</Alert>
</Grid>
</Grid>
) : null}
<Grid
item
container
direction='row'
justifyContent='stretch'
alignItems='stretch'
sx={{ width: '100%' }}
>
<Grid
item
xs={2}
sx={{ display: 'flex', justifyContent: 'stretch', alignItems: 'stretch' }}
>
<Tooltip enterTouchDelay={0} enterDelay={300} enterNextDelay={1000} title={t('Logout')}>
<Button
sx={{ minWidth: '2em', width: '100%' }}
color='primary'
variant='outlined'
onClick={() => {
logoutRobot();
setView('welcome');
}}
>
<Logout />
</Button>
</Tooltip>
</Grid>
<Grid item xs={10}>
<TokenInput
inputToken={inputToken}
editable={false}
label={t('Store your token safely')}
setInputToken={setInputToken}
setRobot={setRobot}
badRequest={badRequest}
robot={robot}
onPressEnter={() => null}
/>
</Grid>
</Grid>
) : null}
<Grid item sx={{ width: '100%' }}>
<TokenInput
inputToken={inputToken}
editable={false}
showDownload={true}
label={t('Store your token safely')}
setInputToken={setInputToken}
setRobot={setRobot}
badRequest={badRequest}
robot={robot}
onPressEnter={() => null}
/>
</Grid>
<Grid item>
<Button
size='small'
color='primary'
onClick={() => {
logoutRobot();
setView('welcome');
<Grid item sx={{ width: '100%' }}>
<Box
sx={{
backgroundColor: 'background.paper',
border: '1px solid',
borderRadius: '4px',
borderColor: theme.palette.mode === 'dark' ? '#434343' : '#c4c4c4',
}}
>
<Logout /> <div style={{ width: '0.5em' }} />
{t('Logout Robot')}
</Button>
<Grid container direction='column' alignItems='center' spacing={2} padding={2}>
<Grid item sx={{ width: '100%' }}>
<Typography variant='caption'>{t('Robot Garage')}</Typography>
<Select
fullWidth
required={true}
inputProps={{
style: { textAlign: 'center' },
}}
value={loading ? 'loading' : currentSlot}
onChange={handleChangeSlot}
>
{loading ? (
<MenuItem key={'loading'} value={'loading'}>
<Typography>{t('Building...')}</Typography>
</MenuItem>
) : (
garage.slots.map((slot: Slot, index: number) => {
return (
<MenuItem key={index} value={index}>
<Grid
container
direction='row'
justifyContent='flex-start'
alignItems='center'
style={{ height: '2.8em' }}
spacing={1}
>
<Grid item>
<RobotAvatar
nickname={slot.robot.nickname}
smooth={true}
style={{ width: '2.6em', height: '2.6em' }}
placeholderType='loading'
baseUrl={baseUrl}
/>
</Grid>
<Grid item>
<Typography variant={windowSize.width < 26 ? 'caption' : undefined}>
{slot.robot.nickname}
</Typography>
</Grid>
</Grid>
</MenuItem>
);
})
)}
</Select>
</Grid>
<Grid item container direction='row' alignItems='center' justifyContent='space-evenly'>
<Grid item>
<LoadingButton loading={loading} color='primary' onClick={handleAddRobot}>
<Add /> <div style={{ width: '0.5em' }} />
{t('Add Robot')}
</LoadingButton>
</Grid>
{window.NativeRobosats === undefined ? (
<Grid item>
<Button color='primary' onClick={() => garage.download()}>
<Download />
</Button>
</Grid>
) : null}
<Grid item>
<Button
color='primary'
onClick={() => {
garage.delete();
setCurrentSlot(0);
logoutRobot();
setView('welcome');
}}
>
<DeleteSweep /> <div style={{ width: '0.5em' }} />
{t('Delete Garage')}
</Button>
</Grid>
</Grid>
</Grid>
</Box>
</Grid>
</Grid>
);

View File

@ -1,16 +1,16 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IconButton, TextField, Tooltip, useTheme } from '@mui/material';
import { IconButton, LinearProgress, TextField, Tooltip } from '@mui/material';
import { Robot } from '../../models';
import { Download, ContentCopy } from '@mui/icons-material';
import { ContentCopy } from '@mui/icons-material';
import { systemClient } from '../../services/System';
import { saveAsJson } from '../../utils';
interface TokenInputProps {
robot: Robot;
editable?: boolean;
showDownload?: boolean;
fullWidth?: boolean;
loading?: boolean;
setRobot: (state: Robot) => void;
inputToken: string;
autoFocusTarget?: 'textfield' | 'copyButton' | 'none';
@ -27,12 +27,12 @@ const TokenInput = ({
showCopy = true,
label,
setRobot,
showDownload = false,
fullWidth = true,
onPressEnter,
autoFocusTarget = 'textfield',
inputToken,
badRequest,
loading = false,
setInputToken,
}: TokenInputProps): JSX.Element => {
const { t } = useTranslation();
@ -52,56 +52,49 @@ const TokenInput = ({
};
};
return (
<TextField
error={!!badRequest}
disabled={!editable}
required={true}
label={label || undefined}
value={inputToken}
autoFocus={autoFocusTarget == 'textfield'}
fullWidth={fullWidth}
sx={{ borderColor: 'primary' }}
variant={editable ? 'outlined' : 'filled'}
helperText={badRequest}
size='medium'
onChange={(e) => setInputToken(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
onPressEnter();
}
}}
InputProps={{
startAdornment: showDownload ? (
<Tooltip enterTouchDelay={250} title={t('Download token and PGP credentials')}>
<IconButton
color='primary'
sx={{ position: 'relative', top: label ? '0.4em' : '0em' }}
onClick={() => saveAsJson(robot.nickname + '.json', createJsonFile())}
>
<Download sx={{ width: '1em', height: '1em' }} />
</IconButton>
</Tooltip>
) : null,
endAdornment: showCopy ? (
<Tooltip open={showCopied} title={t('Copied!')}>
<IconButton
autoFocus={autoFocusTarget == 'copyButton'}
color={robot.copiedToken ? 'inherit' : 'primary'}
onClick={() => {
systemClient.copyToClipboard(inputToken);
setShowCopied(true);
setTimeout(() => setShowCopied(false), 1000);
setRobot({ ...robot, copiedToken: true });
}}
>
<ContentCopy sx={{ width: '1em', height: '1em' }} />
</IconButton>
</Tooltip>
) : null,
}}
/>
);
if (loading) {
return <LinearProgress sx={{ height: '0.8em' }} />;
} else {
return (
<TextField
error={!!badRequest}
disabled={!editable}
required={true}
label={label || undefined}
value={inputToken}
autoFocus={autoFocusTarget == 'textfield'}
fullWidth={fullWidth}
sx={{ borderColor: 'primary' }}
variant={editable ? 'outlined' : 'filled'}
helperText={badRequest}
size='medium'
onChange={(e) => setInputToken(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
onPressEnter();
}
}}
InputProps={{
endAdornment: showCopy ? (
<Tooltip open={showCopied} title={t('Copied!')}>
<IconButton
autoFocus={autoFocusTarget == 'copyButton'}
color={robot.copiedToken ? 'inherit' : 'primary'}
onClick={() => {
systemClient.copyToClipboard(inputToken);
setShowCopied(true);
setTimeout(() => setShowCopied(false), 1000);
setRobot({ ...robot, copiedToken: true });
}}
>
<ContentCopy sx={{ width: '1em', height: '1em' }} />
</IconButton>
</Tooltip>
) : null,
}}
/>
);
}
};
export default TokenInput;

View File

@ -12,9 +12,7 @@ import {
} from '@mui/material';
import { useParams } from 'react-router-dom';
import { Page } from '../NavBar';
import { Robot } from '../../models';
import { systemClient } from '../../services/System';
import { apiClient } from '../../services/api';
import Onboarding from './Onboarding';
import Welcome from './Welcome';
@ -25,7 +23,7 @@ import { genKey } from '../../pgp';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
const RobotPage = (): JSX.Element => {
const { setPage, setCurrentOrder, fetchRobot, torStatus, windowSize, robot, setRobot, baseUrl } =
const { robot, setRobot, setPage, setCurrentOrder, fetchRobot, torStatus, windowSize, baseUrl } =
useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
const params = useParams();
@ -49,7 +47,7 @@ const RobotPage = (): JSX.Element => {
}
}, []);
const getGenerateRobot = (token: string) => {
const getGenerateRobot = (token: string, slot?: number) => {
setInputToken(token);
genKey(token).then(function (key) {
fetchRobot({
@ -59,24 +57,16 @@ const RobotPage = (): JSX.Element => {
encPrivKey: key.encryptedPrivateKeyArmored,
},
newToken: token,
slot,
refCode,
setBadRequest,
});
});
};
const deleteRobot = () => {
apiClient.delete(baseUrl, '/api/user');
logoutRobot();
};
const logoutRobot = () => {
setInputToken('');
systemClient.deleteCookie('sessionid');
systemClient.deleteItem('robot_token');
systemClient.deleteItem('pub_key');
systemClient.deleteItem('enc_priv_key');
setTimeout(() => setRobot(new Robot()), 10);
setRobot(new Robot());
};
if (!(window.NativeRobosats === undefined) && !(torStatus == 'DONE' || torStatus == '"Done"')) {
@ -146,7 +136,7 @@ const RobotPage = (): JSX.Element => {
<Onboarding
setView={setView}
robot={robot}
setRobot={setRobot}
setRobot={() => null}
badRequest={badRequest}
inputToken={inputToken}
setInputToken={setInputToken}
@ -160,9 +150,10 @@ const RobotPage = (): JSX.Element => {
<RobotProfile
setView={setView}
robot={robot}
setRobot={setRobot}
setRobot={() => null}
setCurrentOrder={setCurrentOrder}
badRequest={badRequest}
getGenerateRobot={getGenerateRobot}
logoutRobot={logoutRobot}
width={width}
inputToken={inputToken}
@ -177,7 +168,7 @@ const RobotPage = (): JSX.Element => {
<Recovery
setView={setView}
robot={robot}
setRobot={setRobot}
setRobot={() => null}
badRequest={badRequest}
inputToken={inputToken}
setInputToken={setInputToken}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
@ -14,6 +14,7 @@ import {
} from '@mui/material';
import { systemClient } from '../../services/System';
import ContentCopy from '@mui/icons-material/ContentCopy';
import { AppContext, AppContextProps } from '../../contexts/AppContext';
interface Props {
open: boolean;
@ -23,6 +24,7 @@ interface Props {
}
const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): JSX.Element => {
const { robot } = useContext<AppContextProps>(AppContext);
const { t } = useTranslation();
return (
@ -41,17 +43,13 @@ const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): J
sx={{ width: '100%', maxWidth: '550px' }}
disabled
label={t('Back it up!')}
value={systemClient.getItem('robot_token')}
value={robot.token}
variant='filled'
size='small'
InputProps={{
endAdornment: (
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
<IconButton
onClick={() =>
systemClient.copyToClipboard(systemClient.getItem('robot_token'))
}
>
<IconButton onClick={() => systemClient.copyToClipboard(robot.token)}>
<ContentCopy color='primary' />
</IconButton>
</Tooltip>

View File

@ -40,6 +40,7 @@ const RobotAvatar: React.FC<Props> = ({
const { t } = useTranslation();
const theme = useTheme();
const [avatarSrc, setAvatarSrc] = useState<string>();
const [nicknameReady, setNicknameReady] = useState<boolean>(false);
const backgroundData =
placeholderType == 'generating' ? placeholder.generating : placeholder.loading;
@ -55,11 +56,15 @@ const RobotAvatar: React.FC<Props> = ({
if (nickname != undefined) {
if (window.NativeRobosats === undefined) {
setAvatarSrc(baseUrl + '/static/assets/avatars/' + nickname + '.png');
setNicknameReady(true);
} else {
setNicknameReady(true);
apiClient
.fileImageUrl(baseUrl, '/static/assets/avatars/' + nickname + '.png')
.then(setAvatarSrc);
}
} else {
setNicknameReady(false);
}
}, [nickname]);
@ -93,7 +98,7 @@ const RobotAvatar: React.FC<Props> = ({
>
<div className={className}>
<SmoothImage
src={avatarSrc}
src={nicknameReady ? avatarSrc : null}
imageStyles={{
borderRadius: '50%',
border: '0.3px solid #55555',
@ -110,7 +115,7 @@ const RobotAvatar: React.FC<Props> = ({
className={avatarClass}
style={style}
alt={nickname}
src={avatarSrc}
src={nicknameReady ? avatarSrc : null}
imgProps={{
sx: { transform: flipHorizontally ? 'scaleX(-1)' : '' },
style: { transform: flipHorizontally ? 'scaleX(-1)' : '' },

View File

@ -1,11 +1,10 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Tooltip, TextField, Grid, Container, Paper, Typography } from '@mui/material';
import { Button, Tooltip, TextField, Grid, Paper } from '@mui/material';
import { encryptMessage, decryptMessage } from '../../../../pgp';
import { saveAsJson } from '../../../../utils';
import { AuditPGPDialog } from '../../../Dialogs';
import { systemClient } from '../../../../services/System';
import { websocketClient, WebsocketConnection } from '../../../../services/Websocket';
import { Robot } from '../../../../models';
// Icons
import CircularProgress from '@mui/material/CircularProgress';
@ -19,6 +18,7 @@ import ChatBottom from '../ChatBottom';
interface Props {
orderId: number;
status: number;
robot: Robot;
userNick: string;
takerNick: string;
messages: EncryptedChatMessage[];
@ -31,6 +31,7 @@ interface Props {
const EncryptedSocketChat: React.FC<Props> = ({
orderId,
status,
robot,
userNick,
takerNick,
messages,
@ -45,14 +46,10 @@ const EncryptedSocketChat: React.FC<Props> = ({
const audio = new Audio(`/static/assets/sounds/chat-open.mp3`);
const [connected, setConnected] = useState<boolean>(false);
const [peerConnected, setPeerConnected] = useState<boolean>(false);
const [ownPubKey] = useState<string>(
(systemClient.getItem('pub_key') ?? '').split('\\').join('\n'),
);
const [ownEncPrivKey] = useState<string>(
(systemClient.getItem('enc_priv_key') ?? '').split('\\').join('\n'),
);
const [ownPubKey] = useState<string>(robot.pubKey);
const [ownEncPrivKey] = useState<string>(robot.encPrivKey);
const [peerPubKey, setPeerPubKey] = useState<string>();
const [token] = useState<string>(systemClient.getItem('robot_token') || '');
const [token] = useState<string>(robot.token);
const [serverMessages, setServerMessages] = useState<ServerMessage[]>([]);
const [value, setValue] = useState<string>('');
const [connection, setConnection] = useState<WebsocketConnection>();

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, TextField, Grid, Container, Paper } from '@mui/material';
import { Button, TextField, Grid, Paper } from '@mui/material';
import { encryptMessage, decryptMessage } from '../../../../pgp';
import { AuditPGPDialog } from '../../../Dialogs';
import { systemClient } from '../../../../services/System';
import { Robot } from '../../../../models';
// Icons
import CircularProgress from '@mui/material/CircularProgress';
@ -17,6 +17,7 @@ import ChatBottom from '../ChatBottom';
interface Props {
orderId: number;
robot: Robot;
userNick: string;
takerNick: string;
chatOffset: number;
@ -29,6 +30,7 @@ interface Props {
const EncryptedTurtleChat: React.FC<Props> = ({
orderId,
robot,
userNick,
takerNick,
chatOffset,
@ -43,14 +45,10 @@ const EncryptedTurtleChat: React.FC<Props> = ({
const audio = new Audio(`/static/assets/sounds/chat-open.mp3`);
const [peerConnected, setPeerConnected] = useState<boolean>(false);
const [ownPubKey] = useState<string>(
(systemClient.getItem('pub_key') ?? '').split('\\').join('\n'),
);
const [ownEncPrivKey] = useState<string>(
(systemClient.getItem('enc_priv_key') ?? '').split('\\').join('\n'),
);
const [ownPubKey] = useState<string>(robot.pubKey || '');
const [ownEncPrivKey] = useState<string>(robot.encPrivKey || '');
const [peerPubKey, setPeerPubKey] = useState<string>();
const [token] = useState<string>(systemClient.getItem('robot_token') || '');
const [token] = useState<string>(robot.token || '');
const [value, setValue] = useState<string>('');
const [audit, setAudit] = useState<boolean>(false);
const [waitingEcho, setWaitingEcho] = useState<boolean>(false);
@ -171,8 +169,9 @@ const EncryptedTurtleChat: React.FC<Props> = ({
// If input string contains '#' send unencrypted and unlogged message
else if (value.substring(0, 1) == '#') {
apiClient
.post(baseUrl, `/api/chat`, {
.post(baseUrl, `/api/chat/`, {
PGP_message: value,
order_id: orderId,
offset: lastIndex,
})
.then((response) => {

View File

@ -1,9 +1,11 @@
import React, { useState } from 'react';
import { Robot } from '../../../models';
import EncryptedSocketChat from './EncryptedSocketChat';
import EncryptedTurtleChat from './EncryptedTurtleChat';
interface Props {
orderId: number;
status: number;
takerNick: string;
makerNick: string;
userNick: string;
@ -15,6 +17,7 @@ interface Props {
export interface EncryptedChatMessage {
userNick: string;
robot: Robot;
validSignature: boolean;
plainTextMessage: string;
encryptedMessage: string;
@ -32,17 +35,20 @@ export interface ServerMessage {
const EncryptedChat: React.FC<Props> = ({
orderId,
takerNick,
robot,
userNick,
chatOffset,
baseUrl,
setMessages,
messages,
status,
}: Props): JSX.Element => {
const [turtleMode, setTurtleMode] = useState<boolean>(window.ReactNativeWebView !== undefined);
return turtleMode ? (
<EncryptedTurtleChat
messages={messages}
robot={robot}
setMessages={setMessages}
orderId={orderId}
takerNick={takerNick}
@ -54,7 +60,9 @@ const EncryptedChat: React.FC<Props> = ({
/>
) : (
<EncryptedSocketChat
status={status}
messages={messages}
robot={robot}
setMessages={setMessages}
orderId={orderId}
takerNick={takerNick}

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Grid, Typography, Tooltip, Collapse, IconButton } from '@mui/material';
import currencies from '../../../../static/assets/currencies.json';
import { Order } from '../../../models';
import { Order, Robot } from '../../../models';
import { pn } from '../../../utils';
import EncryptedChat, { EncryptedChatMessage } from '../EncryptedChat';
import Countdown, { zeroPad } from 'react-countdown';
@ -11,6 +11,7 @@ import { LoadingButton } from '@mui/lab';
interface ChatPromptProps {
order: Order;
robot: Robot;
onClickConfirmSent: () => void;
loadingSent: boolean;
onClickConfirmReceived: () => void;
@ -24,6 +25,7 @@ interface ChatPromptProps {
export const ChatPrompt = ({
order,
robot,
onClickConfirmSent,
onClickConfirmReceived,
loadingSent,
@ -116,6 +118,7 @@ export const ChatPrompt = ({
<Grid item>
<EncryptedChat
status={order.status}
robot={robot}
chatOffset={order.chat_last_index}
orderId={order.id}
takerNick={order.taker_nick}

View File

@ -93,6 +93,7 @@ const closeAll: OpenDialogProps = {
interface TradeBoxProps {
order: Order;
setOrder: (state: Order) => void;
robot: Robot;
setBadOrder: (state: string | undefined) => void;
onRenewOrder: () => void;
onStartAgain: () => void;
@ -103,6 +104,7 @@ interface TradeBoxProps {
const TradeBox = ({
order,
setOrder,
robot,
settings,
baseUrl,
setBadOrder,
@ -230,7 +232,7 @@ const TradeBox = ({
const submitStatement = function () {
let statement = dispute.statement;
if (dispute.attachLogs) {
const payload = { statement, messages, token: systemClient.getItem('robot_token') };
const payload = { statement, messages, token: robot.token };
statement = JSON.stringify(payload, null, 2);
}
setLoadingButtons({ ...noLoadingButtons, submitStatement: true });
@ -461,6 +463,7 @@ const TradeBox = ({
return (
<ChatPrompt
order={order}
robot={robot}
onClickConfirmSent={confirmFiatSent}
onClickConfirmReceived={() => setOpen({ ...open, confirmFiatReceived: true })}
loadingSent={loadingButtons.fiatSent}

View File

@ -7,6 +7,7 @@ import {
LimitList,
Maker,
Robot,
Garage,
Info,
Settings,
Favorites,
@ -22,7 +23,6 @@ import { sha256 } from 'js-sha256';
import defaultCoordinators from '../../static/federation.json';
import { useTheme } from '@mui/material';
import { systemClient } from '../services/System';
const getWindowSize = function (fontSize: number) {
// returns window size in EM units
@ -61,10 +61,11 @@ export interface SlideDirection {
}
export interface fetchRobotProps {
action?: 'login' | 'generate';
action?: 'login' | 'generate' | 'refresh';
newKeys?: { encPrivKey: string; pubKey: string } | null;
newToken?: string | null;
refCode?: string | null;
slot?: number | null;
setBadRequest?: (state: string) => void;
}
@ -78,6 +79,10 @@ export interface AppContextProps {
setSettings: (state: Settings) => void;
book: Book;
info: Info;
garage: Garage;
setGarage: (state: Garage) => void;
currentSlot: number;
setCurrentSlot: (state: number) => void;
setBook: (state: Book) => void;
fetchBook: () => void;
limits: { list: LimitList; loading: boolean };
@ -200,7 +205,13 @@ export const AppContextProvider = ({
list: [],
loading: true,
});
const [robot, setRobot] = useState<Robot>(new Robot());
const [garage, setGarage] = useState<Garage>(() => {
const initialState = { setGarage };
const newGarage = new Garage(initialState);
return newGarage;
});
const [currentSlot, setCurrentSlot] = useState<number>(garage.slots.length - 1);
const [robot, setRobot] = useState<Robot>(new Robot(garage.slots[currentSlot].robot));
const [maker, setMaker] = useState<Maker>(defaultMaker);
const [info, setInfo] = useState<Info>(defaultInfo);
const [coordinators, setCoordinators] = useState<Coordinator[]>(defaultCoordinators);
@ -219,7 +230,6 @@ export const AppContextProvider = ({
in: undefined,
out: undefined,
});
const [currentOrder, setCurrentOrder] = useState<number | undefined>(undefined);
const navbarHeight = 2.5;
@ -239,6 +249,13 @@ export const AppContextProvider = ({
getWindowSize(theme.typography.fontSize),
);
useEffect(() => {
window.addEventListener('torStatus', (event) => {
// UX improv: delay the "Conencted" status by 10 secs to avoid long waits for first requests
setTimeout(() => setTorStatus(event?.detail), event?.detail === '"Done"' ? 10000 : 0);
});
}, []);
useEffect(() => {
if (typeof window !== undefined) {
window.addEventListener('resize', onResize);
@ -257,15 +274,18 @@ export const AppContextProvider = ({
useEffect(() => {
let host = '';
let protocol = '';
if (window.NativeRobosats === undefined) {
host = getHost();
protocol = location.protocol;
} else {
protocol = 'http:';
host =
settings.network === 'mainnet'
? coordinators[0].mainnetOnion
: coordinators[0].testnetOnion;
}
setBaseUrl(`${location.protocol}//${host}`);
setBaseUrl(`${protocol}//${host}`);
}, [settings.network]);
useEffect(() => {
@ -371,14 +391,19 @@ export const AppContextProvider = ({
newKeys = null,
newToken = null,
refCode = null,
slot = null,
setBadRequest = () => {},
}: fetchRobotProps) {
setRobot({ ...robot, loading: true, avatarLoaded: false });
const oldRobot = robot;
const targetSlot = slot ?? currentSlot;
if (action != 'refresh') {
setRobot(new Robot());
}
setBadRequest('');
let requestBody = {};
if (action == 'login') {
requestBody.token_sha256 = sha256(newToken ?? robot.token);
if (action == 'login' || action == 'refresh') {
requestBody.token_sha256 = sha256(newToken ?? oldRobot.token);
} else if (action == 'generate' && newToken != null) {
const strength = tokenStrength(newToken);
requestBody.token_sha256 = sha256(newToken);
@ -386,11 +411,12 @@ export const AppContextProvider = ({
requestBody.counts = strength.counts;
requestBody.length = newToken.length;
requestBody.ref_code = refCode;
requestBody.public_key = newKeys.pubKey ?? robot.pubkey;
requestBody.encrypted_private_key = newKeys.encPrivKey ?? robot.encPrivKey;
requestBody.public_key = newKeys.pubKey ?? oldRobot.pubkey;
requestBody.encrypted_private_key = newKeys.encPrivKey ?? oldRobot.encPrivKey;
}
apiClient.post(baseUrl, '/api/user/', requestBody).then((data: any) => {
let newRobot = robot;
setCurrentOrder(
data.active_order_id
? data.active_order_id
@ -400,25 +426,25 @@ export const AppContextProvider = ({
);
if (data.bad_request) {
setBadRequest(data.bad_request);
setRobot({
...robot,
newRobot = {
...oldRobot,
loading: false,
nickname: data.nickname ?? robot.nickname,
nickname: data.nickname ?? oldRobot.nickname,
activeOrderId: data.active_order_id ?? null,
referralCode: data.referral_code ?? robot.referralCode,
earnedRewards: data.earned_rewards ?? robot.earnedRewards,
lastOrderId: data.last_order_id ?? robot.lastOrderId,
referralCode: data.referral_code ?? oldRobot.referralCode,
earnedRewards: data.earned_rewards ?? oldRobot.earnedRewards,
lastOrderId: data.last_order_id ?? oldRobot.lastOrderId,
stealthInvoices: data.wants_stealth ?? robot.stealthInvoices,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
found: false,
});
};
} else {
setRobot({
...robot,
newRobot = {
...oldRobot,
nickname: data.nickname,
token: newToken ?? robot.token,
token: newToken ?? oldRobot.token,
loading: false,
activeOrderId: data.active_order_id ?? null,
lastOrderId: data.last_order_id ?? null,
@ -433,11 +459,11 @@ export const AppContextProvider = ({
shannonEntropy: data.token_shannon_entropy,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
copiedToken: data.found ? true : robot.copiedToken,
});
systemClient.setItem('robot_token', newToken ?? robot.token);
systemClient.setItem('pub_key', data.public_key.split('\n').join('\\'));
systemClient.setItem('enc_priv_key', data.encrypted_private_key.split('\n').join('\\'));
copiedToken: data.found ? true : false,
};
setRobot(newRobot);
garage.updateRobot(newRobot, targetSlot);
setCurrentSlot(targetSlot);
}
});
};
@ -445,9 +471,9 @@ export const AppContextProvider = ({
useEffect(() => {
if (baseUrl != '' && page != 'robot') {
if (open.profile || (robot.token && robot.nickname === null)) {
fetchRobot({ action: 'login' }); // fetch existing robot
fetchRobot({ action: 'refresh' }); // refresh/update existing robot
} else if (robot.token && robot.encPrivKey && robot.pubKey) {
fetchRobot({ action: 'login' }); // create new robot with existing token and keys (on network and coordinator change)
fetchRobot({ action: 'refresh' }); // create new robot with existing token and keys (on network and coordinator change)
}
}
}, [open.profile, baseUrl]);
@ -460,8 +486,11 @@ export const AppContextProvider = ({
setSettings,
book,
setBook,
garage,
setGarage,
currentSlot,
setCurrentSlot,
fetchBook,
fetchRobot,
limits,
info,
setLimits,
@ -471,6 +500,7 @@ export const AppContextProvider = ({
clearOrder,
robot,
setRobot,
fetchRobot,
baseUrl,
setBaseUrl,
fav,

View File

@ -0,0 +1,51 @@
import { Robot, Order } from '.';
import { systemClient } from '../services/System';
import { saveAsJson } from '../utils';
export interface Slot {
robot: Robot;
order: Order | null;
}
const emptySlot: Slot = { robot: new Robot(), order: null };
class Garage {
constructor(initialState?: Garage) {
if (initialState?.slots === undefined && systemClient.getItem('garage') != '') {
this.slots = JSON.parse(systemClient.getItem('garage'));
console.log('Robot Garage was loaded from local storage');
} else {
this.slots = [emptySlot];
}
this.setGarage = initialState?.setGarage || (() => {});
}
slots: Slot[] = [emptySlot];
setGarage: (state: Garage) => void = () => {};
save = () => {
systemClient.setItem('garage', JSON.stringify(this.slots));
this.setGarage(new Garage(this));
};
delete = () => {
this.slots = [emptySlot];
systemClient.deleteItem('garage');
this.save();
};
updateRobot: (robot: Robot, index: number) => void = (robot, index) => {
this.slots[index] = { robot, order: null };
this.save();
};
download = () => {
saveAsJson(`robotGarage_${new Date().toISOString()}.json`, this.slots);
};
deleteSlot: (index?: number) => void = (index) => {
const targetSlot = index ?? this.slots.length - 1;
this.slots.splice(targetSlot, 1);
this.save();
};
}
export default Garage;

View File

@ -1,10 +1,10 @@
import { systemClient } from '../services/System';
class Robot {
constructor() {
this.token = systemClient.getItem('robot_token') ?? undefined;
this.pubKey = systemClient.getItem('pub_key') ?? undefined;
this.encPrivKey = systemClient.getItem('enc_priv_key') ?? undefined;
constructor(garageRobot?: Robot) {
if (garageRobot) {
this.token = garageRobot?.token ?? undefined;
this.pubKey = garageRobot?.pubKey ?? undefined;
this.encPrivKey = garageRobot?.encPrivKey ?? undefined;
}
}
public nickname?: string;

View File

@ -39,7 +39,7 @@ class BaseSettings {
: i18n.resolvedLanguage.substring(0, 2);
const networkCookie = systemClient.getItem('settings_network');
this.network = networkCookie !== '' ? networkCookie : undefined;
this.network = networkCookie !== '' ? networkCookie : 'mainnet';
}
public frontend: 'basic' | 'pro' = 'basic';
@ -47,7 +47,7 @@ class BaseSettings {
public fontSize: number = 14;
public language?: Language;
public freezeViewports: boolean = false;
public network: 'mainnet' | 'testnet' | undefined = undefined;
public network: 'mainnet' | 'testnet' = 'mainnet';
public coordinator: Coordinator | undefined = undefined;
public host?: string;
public unsafeClient: boolean = false;

View File

@ -1,6 +1,7 @@
import Robot from './Robot.model';
import Garage from './Garage.model';
import Settings from './Settings.default.basic';
export { Robot, Settings };
export { Robot, Garage, Settings };
export type { LimitList } from './Limit.model';
export type { Limit } from './Limit.model';
@ -9,6 +10,7 @@ export type { Order } from './Order.model';
export type { PublicOrder } from './Book.model';
export type { Book } from './Book.model';
export type { Info } from './Info.model';
export type { Slot } from './Garage.model';
export type { Language } from './Settings.model';
export type { Favorites } from './Favorites.model';
export type { Coordinator } from './Coordinator.model';

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Ordre activa #{{orderID}}",
"Last order #{{orderID}}": "Última ordre #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "La reutilització de la identitat de trading degrada la teva privadesa davant d'altres usuaris, coordinadors i observadors.",
"Generate a new Robot": "Generar un nou Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Guarda el teu token de manera segura",
"Logout Robot": "Tanca sessió Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Descarrega el token i les credencials PGP",
"Copied!": "Copiat!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Genera un token",
@ -144,7 +147,7 @@
"Public sell orders": "Ordres de venta públiques",
"Book liquidity": "Liquiditat en el llibre",
"Today active robots": "Robots actius avui",
"24h non-KYC bitcoin premium": "Prima de bitcoin sense KYC en 24h",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Comissió del creador",
"Taker fee": "Comissió del prenedor",
"Current onchain payout fee": "Cost actual de rebre onchain",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Ulož si svůj token bezpečně",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Zkopirováno!!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Veřejné prodejní nabídky",
"Book liquidity": "Dostupná likvidita",
"Today active robots": "Dnešní aktivní roboti",
"24h non-KYC bitcoin premium": "24h no-KYC bitcoin přirážka",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Poplatek tvůrce",
"Taker fee": "Poplatek příjemce",
"Current onchain payout fee": "Současný poplatek za vyplacení na onchain ",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Verwahre deinen Token sicher",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Kopiert!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Öffentliche Verkaufsangebote",
"Book liquidity": "Marktplatz-Liquidität",
"Today active robots": "Heute aktive Roboter",
"24h non-KYC bitcoin premium": "24h non-KYC Bitcoin-Aufschlag",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Makergebühr",
"Taker fee": "Takergebühr",
"Current onchain payout fee": "Current onchain payout fee",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Store your token safely",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Copied!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Public sell orders",
"Book liquidity": "Book liquidity",
"Today active robots": "Today active robots",
"24h non-KYC bitcoin premium": "24h non-KYC bitcoin premium",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Maker fee",
"Taker fee": "Taker fee",
"Current onchain payout fee": "Current onchain payout fee",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Guarda tu token de forma segura",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "¡Copiado!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Órdenes de venta públicas",
"Book liquidity": "Liquidez en el libro",
"Today active robots": "Robots activos hoy",
"24h non-KYC bitcoin premium": "Prima de bitcoin sin KYC en 24h",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Comisión del creador",
"Taker fee": "Comisión del tomador",
"Current onchain payout fee": "Coste actual de recibir onchain",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Gorde zure tokena era seguru batean",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Kopiatuta!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Salmenta eskaera publikoak",
"Book liquidity": "Liburuaren likidezia",
"Today active robots": "Robot aktiboak gaur",
"24h non-KYC bitcoin premium": "24 orduko ez-KYC prima",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Egile kuota",
"Taker fee": "Hartzaile kuota",
"Current onchain payout fee": "Oraingo onchain jasotze-kuota",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Stockez votre jeton en sécurité",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Copié!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Ordres de vente publics",
"Book liquidity": "Liquidité du livre",
"Today active robots": "Robots actifs aujourd'hui",
"24h non-KYC bitcoin premium": "Prime bitcoin non-KYC 24h",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Frais du createur",
"Taker fee": "Frais du preneur",
"Current onchain payout fee": "Current onchain payout fee",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Custodisci il tuo gettone in modo sicuro",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Copiato!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Ordini di vendita pubblici",
"Book liquidity": "Registro della liquidità",
"Today active robots": "I Robottini attivi oggi",
"24h non-KYC bitcoin premium": "Premio bitcoin non-KYC 24h",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Commissione dell'offerente",
"Taker fee": "Commissione dell'acquirente",
"Current onchain payout fee": "Current onchain payout fee",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Przechowuj swój token bezpiecznie",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Skopiowane!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Zlecenia sprzedaży publicznej",
"Book liquidity": "Płynność księgowa",
"Today active robots": "Dziś aktywne roboty",
"24h non-KYC bitcoin premium": "24h premia bitcoin non-KYC",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Opłata producenta",
"Taker fee": "Opłata takera",
"Current onchain payout fee": "Current onchain payout fee",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Guarde seu token de forma segura",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Copiado!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Ordens de venda públicss",
"Book liquidity": "Liquidez do livro",
"Today active robots": "Robôs ativos hoje",
"24h non-KYC bitcoin premium": "Prêmio de bitcoin não-KYC nas últimas 24 horas",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Taxa do criador",
"Taker fee": "Taxa do tomador",
"Current onchain payout fee": "Taxa de pagamento onchain atual",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Храните Ваш токен в безопасности",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Скопировано!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Ордера на продажу",
"Book liquidity": "Ликвидность книги ордеров",
"Today active robots": "Сегодня активных роботов",
"24h non-KYC bitcoin premium": "Наценка на Биткойн без ЗСК за 24 часа",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Комиссия мейкера",
"Taker fee": "Комиссия тейкера",
"Current onchain payout fee": "Текущая комиссия за выплату ончейн",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "Spara din token säkert",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "Kopierat!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "Publika säljordrar",
"Book liquidity": "Orderbokslikviditet",
"Today active robots": "Aktiva robotar idag",
"24h non-KYC bitcoin premium": "24h non-KYC bitcoin premium",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "Makeravgift",
"Taker fee": "Takeravgift",
"Current onchain payout fee": "Aktuell utbetalningsavgift (on-chain)",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "รักษา token ของคุณไว้ให้ดี",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "คัดลอกแล้ว!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "จำนวนรายการขาย",
"Book liquidity": "สภาพคล่องทางบ้ญชี",
"Today active robots": "จำนวนโรบอทที่ใช้งานในวันนี้",
"24h non-KYC bitcoin premium": "ค่าพรีเมี่ยม 24 ชม.ที่แล้วสำหรับ bitcoin non-KYC",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "ค่าธรรมเนียม Maker",
"Taker fee": "ค่าธรรมเนียม Taker",
"Current onchain payout fee": "ค่าธรรมเนียมการจ่าย On-chain ตอนนี้",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "请安全地存储你的令牌",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "已复制!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "公开出售订单",
"Book liquidity": "账面流动性",
"Today active robots": "今天活跃的机器人",
"24h non-KYC bitcoin premium": "24小时 non-KYC 比特币溢价",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "挂单方费用",
"Taker fee": "吃单方费用",
"Current onchain payout fee": "当前链上支付费用",

View File

@ -12,11 +12,14 @@
"Active order #{{orderID}}": "Active order #{{orderID}}",
"Last order #{{orderID}}": "Last order #{{orderID}}",
"Reusing trading identity degrades your privacy against other users, coordinators and observers.": "Reusing trading identity degrades your privacy against other users, coordinators and observers.",
"Generate a new Robot": "Generate a new Robot",
"Add a new Robot": "Add a new Robot",
"Logout": "Logout",
"Store your token safely": "請安全地存儲你的令牌",
"Logout Robot": "Logout Robot",
"Robot Garage": "Robot Garage",
"Building...": "Building...",
"Add Robot": "Add Robot",
"Delete Garage": "Delete Garage",
"#4": "Phrases in basic/RobotPage/TokenInput.tsx",
"Download token and PGP credentials": "Download token and PGP credentials",
"Copied!": "已複製!",
"#5": "Phrases in basic/RobotPage/Onboarding.tsx",
"1. Generate a token": "1. Generate a token",
@ -144,7 +147,7 @@
"Public sell orders": "公開出售訂單",
"Book liquidity": "賬面流動性",
"Today active robots": "今天活躍的機器人",
"24h non-KYC bitcoin premium": "24小時 non-KYC 比特幣溢價",
"Last 24h mean premium": "Last 24h mean premium",
"Maker fee": "掛單方費用",
"Taker fee": "吃單方費用",
"Current onchain payout fee": "當前鏈上支付費用",