Adapt garage to coordinators

This commit is contained in:
KoalaSat 2023-11-21 18:36:59 +01:00 committed by Reckless_Satoshi
parent e870117a2c
commit e8ec7f989a
42 changed files with 461 additions and 375 deletions

View File

@ -31,7 +31,7 @@ const BookPage = (): JSX.Element => {
const chartWidthEm = width - maxBookTableWidth; const chartWidthEm = width - maxBookTableWidth;
const onOrderClicked = function (id: number, shortAlias: string): void { const onOrderClicked = function (id: number, shortAlias: string): void {
if (garage.getSlot().robot.avatarLoaded) { if (garage.getSlot()?.avatarLoaded) {
setDelay(10000); setDelay(10000);
navigate(`/order/${shortAlias}/${id}`); navigate(`/order/${shortAlias}/${id}`);
} else { } else {

View File

@ -40,22 +40,24 @@ const Main: React.FC = () => {
setAvatarBaseUrl(federation.getCoordinator(sortedCoordinators[0]).getBaseUrl()); setAvatarBaseUrl(federation.getCoordinator(sortedCoordinators[0]).getBaseUrl());
}, [settings.network, settings.selfhostedClient, federation, sortedCoordinators]); }, [settings.network, settings.selfhostedClient, federation, sortedCoordinators]);
const onLoad = () => {
garage.updateSlot({ avatarLoaded: true });
};
return ( return (
<Router> <Router>
<RobotAvatar <RobotAvatar
style={{ display: 'none' }} style={{ display: 'none' }}
nickname={garage.getSlot().robot.nickname} nickname={garage.getSlot()?.getRobot()?.nickname}
baseUrl={federation.getCoordinator(sortedCoordinators[0]).getBaseUrl()} baseUrl={federation.getCoordinator(sortedCoordinators[0]).getBaseUrl()}
onLoad={() => { onLoad={onLoad}
garage.updateRobot({ avatarLoaded: true });
}}
/> />
<Notifications <Notifications
page={page} page={page}
openProfile={() => { openProfile={() => {
setOpen({ ...closeAll, profile: true }); setOpen({ ...closeAll, profile: true });
}} }}
rewards={garage.getSlot().robot.earnedRewards} rewards={garage.getSlot()?.getRobot()?.earnedRewards}
windowWidth={windowSize?.width} windowWidth={windowSize?.width}
/> />
{settings.network === 'testnet' ? ( {settings.network === 'testnet' ? (

View File

@ -51,7 +51,7 @@ const MakerPage = (): JSX.Element => {
]); ]);
const onOrderClicked = function (id: number): void { const onOrderClicked = function (id: number): void {
if (garage.getSlot().robot.avatarLoaded) { if (garage.getSlot()?.avatarLoaded) {
navigate(`/order/${id}`); navigate(`/order/${id}`);
} else { } else {
setOpenNoRobot(true); setOpenNoRobot(true);

View File

@ -42,7 +42,7 @@ const NavBar = (): JSX.Element => {
const tabSx = smallBar const tabSx = smallBar
? { ? {
position: 'relative', position: 'relative',
bottom: garage.getSlot().robot.avatarLoaded ? '0.9em' : '0.13em', bottom: garage.getSlot()?.avatarLoaded ? '0.9em' : '0.13em',
minWidth: '1em', minWidth: '1em',
} }
: { position: 'relative', bottom: '1em', minWidth: '2em' }; : { position: 'relative', bottom: '1em', minWidth: '2em' };
@ -82,8 +82,9 @@ const NavBar = (): JSX.Element => {
setPage(newPage); setPage(newPage);
const param = const param =
newPage === 'order' newPage === 'order'
? `${String(slot.activeOrderShortAlias)}/${String( ? `${String(slot?.activeShortAlias)}/${String(
slot.activeOrderId ?? slot.lastOrderId, slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId ??
slot?.getRobot(slot?.lastShortAlias ?? '')?.lastOrderId,
)}` )}`
: ''; : '';
setTimeout(() => { setTimeout(() => {
@ -96,6 +97,8 @@ const NavBar = (): JSX.Element => {
setOpen(closeAll); setOpen(closeAll);
}, [page, setOpen]); }, [page, setOpen]);
const slot = garage.getSlot();
return ( return (
<Paper <Paper
elevation={6} elevation={6}
@ -118,16 +121,16 @@ const NavBar = (): JSX.Element => {
<Tab <Tab
sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }} sx={{ ...tabSx, minWidth: '2.5em', width: '2.5em', maxWidth: '4em' }}
value='none' value='none'
disabled={garage.getSlot().robot.nickname === null} disabled={slot?.getRobot()?.nickname === null}
onClick={() => { onClick={() => {
setOpen({ ...closeAll, profile: !open.profile }); setOpen({ ...closeAll, profile: !open.profile });
}} }}
icon={ icon={
garage.getSlot().robot.nickname != null && garage.getSlot().robot.avatarLoaded ? ( slot?.getRobot()?.nickname != null && slot?.avatarLoaded ? (
<RobotAvatar <RobotAvatar
style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }} style={{ width: '2.3em', height: '2.3em', position: 'relative', top: '0.2em' }}
avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'} avatarClass={theme.palette.mode === 'dark' ? 'navBarAvatarDark' : 'navBarAvatar'}
nickname={garage.getSlot().robot.nickname} nickname={slot?.getRobot()?.nickname}
baseUrl={hostUrl} baseUrl={hostUrl}
/> />
) : ( ) : (
@ -162,7 +165,9 @@ const NavBar = (): JSX.Element => {
sx={tabSx} sx={tabSx}
label={smallBar ? undefined : t('Order')} label={smallBar ? undefined : t('Order')}
value='order' value='order'
disabled={!garage.getSlot().robot.avatarLoaded || !garage.getSlot().activeOrderId} disabled={
!slot?.avatarLoaded || !slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId
}
icon={<Assignment />} icon={<Assignment />}
iconPosition='start' iconPosition='start'
/> />

View File

@ -26,6 +26,7 @@ const OrderPage = (): JSX.Element => {
const [tab, setTab] = useState<'order' | 'contract'>('contract'); const [tab, setTab] = useState<'order' | 'contract'>('contract');
const [baseUrl, setBaseUrl] = useState<string>(hostUrl); const [baseUrl, setBaseUrl] = useState<string>(hostUrl);
const [currentOrder, setCurrentOrder] = useState<Order | null>(null); const [currentOrder, setCurrentOrder] = useState<Order | null>(null);
const [currentOrderId, setCurrentOrderId] = useState<number | null>(null);
useEffect(() => { useEffect(() => {
const coordinator = federation.getCoordinator(params.shortAlias ?? ''); const coordinator = federation.getCoordinator(params.shortAlias ?? '');
@ -38,30 +39,41 @@ const OrderPage = (): JSX.Element => {
setBaseUrl(`${url}${basePath}`); setBaseUrl(`${url}${basePath}`);
if (currentOrder?.id !== Number(params.orderId)) { const orderId = Number(params.orderId);
const coordinator = federation.getCoordinator(params.shortAlias ?? ''); if (orderId && currentOrderId !== orderId) setCurrentOrderId(orderId);
coordinator
.fetchOrder(Number(params.orderId) ?? null, garage.getSlot().robot)
.then((order) => {
if (order?.bad_request !== undefined) {
setBadOrder(order.bad_request);
} else if (order !== null && order?.id !== null) {
setCurrentOrder(order);
if (order.is_participant) {
garage.updateOrder(order as Order);
}
}
})
.catch((e) => {
console.log(e);
});
}
}, [params]); }, [params]);
useEffect(() => {
setCurrentOrder(null);
if (currentOrderId !== null) {
const coordinator = federation.getCoordinator(params.shortAlias ?? '');
const slot = garage.getSlot();
const robot = slot?.getRobot();
if (robot && slot?.token) {
federation.fetchRobot(garage, slot.token);
coordinator
.fetchOrder(currentOrderId, robot)
.then((order) => {
if (order?.bad_request !== undefined) {
setBadOrder(order.bad_request);
} else if (order !== null && order?.id !== null) {
setCurrentOrder(order);
if (order.is_participant) {
garage.updateOrder(order as Order);
}
}
})
.catch((e) => {
console.log(e);
});
}
}
}, [currentOrderId]);
const onClickCoordinator = function (): void { const onClickCoordinator = function (): void {
if (currentOrder?.shortAlias != null) { if (currentOrder?.shortAlias != null) {
setOpen((open) => { setOpen((open) => {
return { ...open, coordinator: shortAlias }; return { ...open, coordinator: currentOrder?.shortAlias };
}); });
} }
}; };

View File

@ -61,6 +61,9 @@ const Onboarding = ({
}, 1000); }, 1000);
}; };
const slot = garage.getSlot();
const robot = slot?.getRobot();
return ( return (
<Box> <Box>
<Accordion expanded={step === '1'} disableGutters={true}> <Accordion expanded={step === '1'} disableGutters={true}>
@ -122,7 +125,6 @@ const Onboarding = ({
onClick={() => { onClick={() => {
setStep('2'); setStep('2');
getGenerateRobot(inputToken); getGenerateRobot(inputToken);
garage.updateRobot({ nickname: undefined });
}} }}
variant='contained' variant='contained'
size='large' size='large'
@ -149,7 +151,7 @@ const Onboarding = ({
<Grid container direction='column' alignItems='center' spacing={1}> <Grid container direction='column' alignItems='center' spacing={1}>
<Grid item> <Grid item>
<Typography> <Typography>
{garage.getSlot().robot.avatarLoaded && Boolean(garage.getSlot().robot.nickname) ? ( {slot?.avatarLoaded && Boolean(robot?.nickname) ? (
t('This is your trading avatar') t('This is your trading avatar')
) : ( ) : (
<> <>
@ -162,7 +164,7 @@ const Onboarding = ({
<Grid item sx={{ width: '13.5em' }}> <Grid item sx={{ width: '13.5em' }}>
<RobotAvatar <RobotAvatar
nickname={garage.getSlot().robot.nickname} nickname={robot?.nickname}
smooth={true} smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }} style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating' placeholderType='generating'
@ -178,7 +180,7 @@ const Onboarding = ({
/> />
</Grid> </Grid>
{garage.getSlot().robot.avatarLoaded && Boolean(garage.getSlot().robot.nickname) ? ( {slot?.avatarLoaded && Boolean(robot?.nickname) ? (
<Grid item> <Grid item>
<Typography align='center'>{t('Hi! My name is')}</Typography> <Typography align='center'>{t('Hi! My name is')}</Typography>
<Typography component='h5' variant='h5'> <Typography component='h5' variant='h5'>
@ -197,7 +199,7 @@ const Onboarding = ({
width: '1.5em', width: '1.5em',
}} }}
/> />
<b>{garage.getSlot().robot.nickname}</b> <b>{robot.nickname}</b>
<Bolt <Bolt
sx={{ sx={{
color: '#fcba03', color: '#fcba03',
@ -210,13 +212,7 @@ const Onboarding = ({
</Grid> </Grid>
) : null} ) : null}
<Grid item> <Grid item>
<Collapse <Collapse in={!!(slot?.avatarLoaded && Boolean(robot?.nickname))}>
in={
!!(
garage.getSlot().robot.avatarLoaded && Boolean(garage.getSlot().robot.nickname)
)
}
>
<Button <Button
onClick={() => { onClick={() => {
setStep('3'); setStep('3');

View File

@ -22,6 +22,7 @@ import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import { genBase62Token } from '../../utils'; import { genBase62Token } from '../../utils';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext'; import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { UseFederationStoreType, FederationContext } from '../../contexts/FederationContext';
interface RobotProfileProps { interface RobotProfileProps {
robot: Robot; robot: Robot;
@ -45,6 +46,7 @@ const RobotProfile = ({
}: RobotProfileProps): JSX.Element => { }: RobotProfileProps): JSX.Element => {
const { windowSize, hostUrl } = useContext<UseAppStoreType>(AppContext); const { windowSize, hostUrl } = useContext<UseAppStoreType>(AppContext);
const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext); const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const { sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
@ -53,7 +55,9 @@ const RobotProfile = ({
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
useEffect(() => { useEffect(() => {
if (garage.getSlot().robot.nickname != null && garage.getSlot().robot.avatarLoaded) { const slot = garage.getSlot();
const robot = slot?.getRobot(sortedCoordinators[0]);
if (robot?.nickname != null && slot?.avatarLoaded) {
setLoading(false); setLoading(false);
} }
}, [robotUpdatedAt, loading]); }, [robotUpdatedAt, loading]);
@ -64,10 +68,13 @@ const RobotProfile = ({
}; };
const handleChangeSlot = (e: SelectChangeEvent<number | 'loading'>): void => { const handleChangeSlot = (e: SelectChangeEvent<number | 'loading'>): void => {
garage.currentSlot = Number(e.target.value); garage.currentSlot = e.target.value;
setLoading(true); setLoading(true);
}; };
const slot = garage.getSlot();
const robot = slot?.getRobot();
return ( return (
<Grid container direction='column' alignItems='center' spacing={1} padding={1} paddingTop={2}> <Grid container direction='column' alignItems='center' spacing={1} padding={1} paddingTop={2}>
<Grid <Grid
@ -79,7 +86,7 @@ const RobotProfile = ({
sx={{ width: '100%' }} sx={{ width: '100%' }}
> >
<Grid item sx={{ height: '2.3em', position: 'relative' }}> <Grid item sx={{ height: '2.3em', position: 'relative' }}>
{garage.getSlot().robot.avatarLoaded && garage.getSlot().robot.nickname != null ? ( {slot?.avatarLoaded && robot?.nickname != null ? (
<Typography align='center' component='h5' variant='h5'> <Typography align='center' component='h5' variant='h5'>
<div <div
style={{ style={{
@ -98,7 +105,7 @@ const RobotProfile = ({
}} }}
/> />
)} )}
<b>{garage.getSlot().robot.nickname}</b> <b>{robot?.nickname}</b>
{width < 19 ? null : ( {width < 19 ? null : (
<Bolt <Bolt
sx={{ sx={{
@ -120,7 +127,7 @@ const RobotProfile = ({
<Grid item sx={{ width: `13.5em` }}> <Grid item sx={{ width: `13.5em` }}>
<RobotAvatar <RobotAvatar
nickname={garage.getSlot().robot.nickname} nickname={robot?.nickname}
smooth={true} smooth={true}
style={{ maxWidth: '12.5em', maxHeight: '12.5em' }} style={{ maxWidth: '12.5em', maxHeight: '12.5em' }}
placeholderType='generating' placeholderType='generating'
@ -135,7 +142,7 @@ const RobotProfile = ({
tooltipPosition='top' tooltipPosition='top'
baseUrl={hostUrl} baseUrl={hostUrl}
/> />
{garage.getSlot().robot.found && Number(garage.getSlot().lastOrderId) > 0 ? ( {robot?.found && slot?.lastShortAlias ? (
<Typography align='center' variant='h6'> <Typography align='center' variant='h6'>
{t('Welcome back!')} {t('Welcome back!')}
</Typography> </Typography>
@ -144,39 +151,31 @@ const RobotProfile = ({
)} )}
</Grid> </Grid>
{Boolean(garage.getSlot().activeOrderId) && {Boolean(robot?.activeOrderId) &&
garage.getSlot().robot.avatarLoaded && Boolean(slot?.avatarLoaded) &&
Boolean(garage.getSlot().robot.nickname) ? ( Boolean(robot?.nickname) ? (
<Grid item> <Grid item>
<Button <Button
onClick={() => { onClick={() => {
navigate( navigate(
`/order/${String(garage.getSlot().activeOrderShortAlias)}/${String( `/order/${String(slot?.activeShortAlias)}/${String(robot?.activeOrderId)}`,
garage.getSlot().activeOrderId,
)}`,
); );
}} }}
> >
{t('Active order #{{orderID}}', { orderID: garage.getSlot().activeOrderId })} {t('Active order #{{orderID}}', { orderID: robot?.activeOrderId })}
</Button> </Button>
</Grid> </Grid>
) : null} ) : null}
{Boolean(garage.getSlot().lastOrderId) && {Boolean(robot?.lastOrderId) && Boolean(slot?.avatarLoaded) && Boolean(robot?.nickname) ? (
garage.getSlot().robot.avatarLoaded &&
Boolean(garage.getSlot().robot.nickname) ? (
<Grid item container direction='column' alignItems='center'> <Grid item container direction='column' alignItems='center'>
<Grid item> <Grid item>
<Button <Button
onClick={() => { onClick={() => {
navigate( navigate(`/order/${String(slot?.lastShortAlias)}/${String(robot?.lastOrderId)}`);
`/order/${String(garage.getSlot().lastOrderShortAlias)}/${String(
garage.getSlot().lastOrderId,
)}`,
);
}} }}
> >
{t('Last order #{{orderID}}', { orderID: garage.getSlot().lastOrderId })} {t('Last order #{{orderID}}', { orderID: robot?.lastOrderId })}
</Button> </Button>
</Grid> </Grid>
<Grid item> <Grid item>
@ -263,9 +262,9 @@ const RobotProfile = ({
<Typography>{t('Building...')}</Typography> <Typography>{t('Building...')}</Typography>
</MenuItem> </MenuItem>
) : ( ) : (
garage.slots.map((slot: Slot, index: number) => { Object.values(garage.slots).map((slot: Slot, index: number) => {
return ( return (
<MenuItem key={index} value={index}> <MenuItem key={index} value={slot.token}>
<Grid <Grid
container container
direction='row' direction='row'
@ -276,7 +275,7 @@ const RobotProfile = ({
> >
<Grid item> <Grid item>
<RobotAvatar <RobotAvatar
nickname={slot.robot.nickname} nickname={slot?.getRobot()?.nickname}
smooth={true} smooth={true}
style={{ width: '2.6em', height: '2.6em' }} style={{ width: '2.6em', height: '2.6em' }}
placeholderType='loading' placeholderType='loading'
@ -286,7 +285,7 @@ const RobotProfile = ({
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant={windowSize.width < 26 ? 'caption' : undefined}> <Typography variant={windowSize.width < 26 ? 'caption' : undefined}>
{slot.robot.nickname} {slot?.getRobot()?.nickname}
</Typography> </Typography>
</Grid> </Grid>
</Grid> </Grid>
@ -323,7 +322,6 @@ const RobotProfile = ({
color='primary' color='primary'
onClick={() => { onClick={() => {
garage.delete(); garage.delete();
garage.currentSlot = 0;
logoutRobot(); logoutRobot();
setView('welcome'); setView('welcome');
}} }}

View File

@ -38,6 +38,10 @@ const TokenInput = ({
setShowCopied(false); setShowCopied(false);
}, [inputToken]); }, [inputToken]);
useEffect(() => {
setShowCopied(false);
}, [showCopied]);
if (loading) { if (loading) {
return <LinearProgress sx={{ height: '0.8em' }} />; return <LinearProgress sx={{ height: '0.8em' }} />;
} else { } else {
@ -67,14 +71,14 @@ const TokenInput = ({
<Tooltip open={showCopied} title={t('Copied!')}> <Tooltip open={showCopied} title={t('Copied!')}>
<IconButton <IconButton
autoFocus={autoFocusTarget === 'copyButton'} autoFocus={autoFocusTarget === 'copyButton'}
color={garage.getSlot().robot.copiedToken ? 'inherit' : 'primary'} color={garage.getSlot()?.copiedToken ? 'inherit' : 'primary'}
onClick={() => { onClick={() => {
systemClient.copyToClipboard(inputToken); systemClient.copyToClipboard(inputToken);
setShowCopied(true); setShowCopied(true);
setTimeout(() => { setTimeout(() => {
setShowCopied(false); setShowCopied(false);
}, 1000); }, 1000);
garage.updateRobot({ copiedToken: true }); garage.updateSlot({ copiedToken: true }, inputToken);
}} }}
> >
<ContentCopy sx={{ width: '1em', height: '1em' }} /> <ContentCopy sx={{ width: '1em', height: '1em' }} />

View File

@ -30,7 +30,7 @@ interface RobotPageProps {
const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => { const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
const { torStatus, windowSize, settings, page } = useContext<UseAppStoreType>(AppContext); const { torStatus, windowSize, settings, page } = useContext<UseAppStoreType>(AppContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext); const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext); const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
const urlToken = settings.selfhostedClient ? params.token : null; const urlToken = settings.selfhostedClient ? params.token : null;
@ -41,15 +41,15 @@ const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
const [badToken, setBadToken] = useState<string>(''); const [badToken, setBadToken] = useState<string>('');
const [inputToken, setInputToken] = useState<string>(''); const [inputToken, setInputToken] = useState<string>('');
const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>( const [view, setView] = useState<'welcome' | 'onboarding' | 'recovery' | 'profile'>(
garage.getSlot().robot.token !== undefined ? 'profile' : 'welcome', garage.currentSlot !== null ? 'profile' : 'welcome',
); );
useEffect(() => { useEffect(() => {
const token = urlToken ?? garage.getSlot().robot.token; const token = urlToken ?? garage.currentSlot;
if (token !== undefined) { if (token !== undefined && token !== null) {
setInputToken(token); setInputToken(token);
if (window.NativeRobosats === undefined || torStatus === '"Done"') { if (window.NativeRobosats === undefined || torStatus === '"Done"') {
getRecoverRobot(token); getGenerateRobot(token);
setView('profile'); setView('profile');
} }
} }
@ -65,32 +65,18 @@ const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
} }
}, [inputToken]); }, [inputToken]);
const getRecoverRobot = (token: string): void => {
setInputToken(token);
genKey(token)
.then((key) => {
garage.updateRobot({
token,
pubKey: key.publicKeyArmored,
encPrivKey: key.encryptedPrivateKeyArmored,
});
void federation.fetchRobot(garage, garage.currentSlot);
})
.catch((error) => {
console.error('Error:', error);
});
};
const getGenerateRobot = (token: string): void => { const getGenerateRobot = (token: string): void => {
setInputToken(token); setInputToken(token);
genKey(token) genKey(token)
.then((key) => { .then((key) => {
garage.createRobot({ garage.upsertRobot(token, sortedCoordinators[0], {
token, token,
pubKey: key.publicKeyArmored, pubKey: key.publicKeyArmored,
encPrivKey: key.encryptedPrivateKeyArmored, encPrivKey: key.encryptedPrivateKeyArmored,
shortAlias: sortedCoordinators[0],
}); });
void federation.fetchRobot(garage, garage.currentSlot); void federation.fetchRobot(garage, token);
garage.currentSlot = token;
}) })
.catch((error) => { .catch((error) => {
console.error('Error:', error); console.error('Error:', error);
@ -99,7 +85,7 @@ const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
const logoutRobot = (): void => { const logoutRobot = (): void => {
setInputToken(''); setInputToken('');
garage.deleteSlot(garage.currentSlot); garage.deleteSlot();
}; };
if (!(window.NativeRobosats === undefined) && !(torStatus === 'DONE' || torStatus === '"Done"')) { if (!(window.NativeRobosats === undefined) && !(torStatus === 'DONE' || torStatus === '"Done"')) {
@ -194,7 +180,7 @@ const RobotPage = ({ avatarBaseUrl }: RobotPageProps): JSX.Element => {
badToken={badToken} badToken={badToken}
inputToken={inputToken} inputToken={inputToken}
setInputToken={setInputToken} setInputToken={setInputToken}
getRecoverRobot={getRecoverRobot} getRecoverRobot={getGenerateRobot}
/> />
) : null} ) : null}
</Paper> </Paper>

View File

@ -33,7 +33,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
useEffect(() => { useEffect(() => {
setLoading(garage.getSlot().robot.loading); setLoading(!garage.getSlot()?.avatarLoaded);
}, [robotUpdatedAt]); }, [robotUpdatedAt]);
return ( return (
@ -57,7 +57,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
<ListItem className='profileNickname'> <ListItem className='profileNickname'>
<ListItemText secondary={t('Your robot')}> <ListItemText secondary={t('Your robot')}>
<Typography component='h6' variant='h6'> <Typography component='h6' variant='h6'>
{garage.getSlot().robot.nickname !== undefined && ( {garage.getSlot()?.getRobot()?.nickname !== undefined && (
<div style={{ position: 'relative', left: '-7px' }}> <div style={{ position: 'relative', left: '-7px' }}>
<div <div
style={{ style={{
@ -70,7 +70,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
> >
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} /> <BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
<a>{garage.getSlot().robot.nickname}</a> <a>{garage.getSlot()?.getRobot()?.nickname}</a>
<BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} /> <BoltIcon sx={{ color: '#fcba03', height: '28px', width: '24px' }} />
</div> </div>
@ -83,7 +83,7 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
<RobotAvatar <RobotAvatar
avatarClass='profileAvatar' avatarClass='profileAvatar'
style={{ width: 65, height: 65 }} style={{ width: 65, height: 65 }}
nickname={garage.getSlot().robot.nickname} nickname={garage.getSlot()?.getRobot()?.nickname}
baseUrl={baseUrl} baseUrl={baseUrl}
/> />
</ListItemAvatar> </ListItemAvatar>
@ -97,15 +97,10 @@ const ProfileDialog = ({ open = false, baseUrl, onClose }: Props): JSX.Element =
</Typography> </Typography>
{Object.values(federation.coordinators).map((coordinator: Coordinator): JSX.Element => { {Object.values(federation.coordinators).map((coordinator: Coordinator): JSX.Element => {
if (!garage.getSlot().robot?.loading) { if (garage.getSlot()?.avatarLoaded) {
return ( return (
<div key={coordinator.shortAlias}> <div key={coordinator.shortAlias}>
<RobotInfo <RobotInfo coordinator={coordinator} onClose={onClose} />
coordinator={coordinator}
robot={garage.getSlot().robot}
slotIndex={garage.currentSlot}
onClose={onClose}
/>
</div> </div>
); );
} else { } else {

View File

@ -43,7 +43,7 @@ const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): J
sx={{ width: '100%', maxWidth: '550px' }} sx={{ width: '100%', maxWidth: '550px' }}
disabled disabled
label={t('Back it up!')} label={t('Back it up!')}
value={garage.getSlot().robot.token} value={garage.getSlot()?.token}
variant='filled' variant='filled'
size='small' size='small'
InputProps={{ InputProps={{
@ -51,7 +51,7 @@ const StoreTokenDialog = ({ open, onClose, onClickBack, onClickDone }: Props): J
<Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}> <Tooltip disableHoverListener enterTouchDelay={0} title={t('Copied!')}>
<IconButton <IconButton
onClick={() => { onClick={() => {
systemClient.copyToClipboard(garage.getSlot().robot.token); systemClient.copyToClipboard(garage.getSlot()?.token ?? '');
}} }}
> >
<ContentCopy color='primary' /> <ContentCopy color='primary' />

View File

@ -91,6 +91,11 @@ const MakerForm = ({
const minRangeAmountMultiple = 1.6; const minRangeAmountMultiple = 1.6;
const amountSafeThresholds = [1.03, 0.98]; const amountSafeThresholds = [1.03, 0.98];
useEffect(() => {
const slot = garage.getSlot();
if (slot?.token) federation.fetchRobot(garage, slot?.token);
}, [garage.currentSlot]);
useEffect(() => { useEffect(() => {
setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]); setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]);
if (maker.coordinator != null) { if (maker.coordinator != null) {
@ -280,12 +285,19 @@ const MakerForm = ({
}; };
const handleCreateOrder = function (): void { const handleCreateOrder = function (): void {
const slot = garage.getSlot();
if (slot?.activeShortAlias) {
setBadRequest(t('You are already maker of an active order'));
return;
}
const { url, basePath } = const { url, basePath } =
federation federation
.getCoordinator(maker.coordinator) .getCoordinator(maker.coordinator)
?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl) ?? {}; ?.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl) ?? {};
const auth = garage.getSlot().robot.getAuthHeaders(); const auth = slot?.getRobot()?.getAuthHeaders();
if (!disableRequest && maker.coordinator != null && auth !== null) { if (!disableRequest && maker.coordinator != null && auth !== null) {
setSubmittingRequest(true); setSubmittingRequest(true);
@ -565,7 +577,7 @@ const MakerForm = ({
setOpenDialogs(false); setOpenDialogs(false);
}} }}
onClickDone={handleCreateOrder} onClickDone={handleCreateOrder}
hasRobot={garage.getSlot().robot.avatarLoaded} hasRobot={garage.getSlot()?.avatarLoaded ?? false}
onClickGenerateRobot={onClickGenerateRobot} onClickGenerateRobot={onClickGenerateRobot}
/> />
<F2fMapDialog <F2fMapDialog

View File

@ -85,7 +85,7 @@ const Notifications = ({
const basePageTitle = t('RoboSats - Simple and Private Bitcoin Exchange'); const basePageTitle = t('RoboSats - Simple and Private Bitcoin Exchange');
const moveToOrderPage = function (): void { const moveToOrderPage = function (): void {
navigate(`/order/${String(garage.getOrder()?.id)}`); navigate(`/order/${String(garage.getSlot()?.order?.id)}`);
setShow(false); setShow(false);
}; };
@ -106,7 +106,7 @@ const Notifications = ({
const Messages: MessagesProps = { const Messages: MessagesProps = {
bondLocked: { bondLocked: {
title: t(`${garage.getOrder()?.is_maker === true ? 'Maker' : 'Taker'} bond locked`), title: t(`${garage.getSlot()?.order?.is_maker === true ? 'Maker' : 'Taker'} bond locked`),
severity: 'info', severity: 'info',
onClick: moveToOrderPage, onClick: moveToOrderPage,
sound: audio.ding, sound: audio.ding,
@ -228,9 +228,9 @@ const Notifications = ({
}; };
const handleStatusChange = function (oldStatus: number | undefined, status: number): void { const handleStatusChange = function (oldStatus: number | undefined, status: number): void {
const order = garage.getOrder(); const order = garage.getSlot()?.order;
if (order === null) return; if (order === undefined || order === null) return;
let message = emptyNotificationMessage; let message = emptyNotificationMessage;
@ -293,8 +293,8 @@ const Notifications = ({
// Notify on order status change // Notify on order status change
useEffect(() => { useEffect(() => {
const order = garage.getOrder(); const order = garage.getSlot()?.order;
if (order !== null) { if (order !== undefined && order !== null) {
if (order.status !== oldOrderStatus) { if (order.status !== oldOrderStatus) {
handleStatusChange(oldOrderStatus, order.status); handleStatusChange(oldOrderStatus, order.status);
setOldOrderStatus(order.status); setOldOrderStatus(order.status);

View File

@ -314,7 +314,9 @@ const TakeButton = ({
}; };
const takeOrder = function (): void { const takeOrder = function (): void {
if (currentOrder === null) return; const robot = garage.getSlot()?.getRobot() ?? null;
if (currentOrder === null || robot === null) return;
setLoadingTake(true); setLoadingTake(true);
const { url, basePath } = federation const { url, basePath } = federation
@ -328,7 +330,7 @@ const TakeButton = ({
action: 'take', action: 'take',
amount: currentOrder?.currency === 1000 ? takeAmount / 100000000 : takeAmount, amount: currentOrder?.currency === 1000 ? takeAmount / 100000000 : takeAmount,
}, },
{ tokenSHA256: garage.getSlot().robot.tokenSHA256 }, { tokenSHA256: robot?.tokenSHA256 },
) )
.then((data) => { .then((data) => {
setLoadingTake(false); setLoadingTake(false);
@ -370,7 +372,7 @@ const TakeButton = ({
setLoadingTake(true); setLoadingTake(true);
setOpen(closeAll); setOpen(closeAll);
}} }}
hasRobot={garage.getSlot().robot.avatarLoaded} hasRobot={garage.getSlot()?.avatarLoaded ?? false}
onClickGenerateRobot={onClickGenerateRobot} onClickGenerateRobot={onClickGenerateRobot}
/> />
<InactiveMakerDialog /> <InactiveMakerDialog />

View File

@ -23,7 +23,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import { Numbers, Send, EmojiEvents, ExpandMore } from '@mui/icons-material'; import { Numbers, Send, EmojiEvents, ExpandMore } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { type Coordinator, type Robot } from '../../models'; import { type Coordinator } from '../../models';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { EnableTelegramDialog } from '../Dialogs'; import { EnableTelegramDialog } from '../Dialogs';
import { UserNinjaIcon } from '../Icons'; import { UserNinjaIcon } from '../Icons';
@ -33,13 +33,11 @@ import { signCleartextMessage } from '../../pgp';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext'; import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
interface Props { interface Props {
robot: Robot;
slotIndex: number;
coordinator: Coordinator; coordinator: Coordinator;
onClose: () => void; onClose: () => void;
} }
const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }: Props) => { const RobotInfo: React.FC<Props> = ({ coordinator, onClose }: Props) => {
const { garage } = useContext<UseGarageStoreType>(GarageContext); const { garage } = useContext<UseGarageStoreType>(GarageContext);
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
@ -71,7 +69,8 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
const handleWeblnInvoiceClicked = async (e: MouseEvent<HTMLButtonElement, MouseEvent>): void => { const handleWeblnInvoiceClicked = async (e: MouseEvent<HTMLButtonElement, MouseEvent>): void => {
e.preventDefault(); e.preventDefault();
if (robot.earnedRewards > 0) { const robot = garage.getSlot()?.getRobot(coordinator.shortAlias);
if (robot && robot.earnedRewards > 0) {
const webln = await getWebln(); const webln = await getWebln();
const invoice = webln.makeInvoice(robot.earnedRewards).then(() => { const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
if (invoice != null) { if (invoice != null) {
@ -85,12 +84,13 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
setBadInvoice(''); setBadInvoice('');
setShowRewardsSpinner(true); setShowRewardsSpinner(true);
const robot = garage.getSlot(slotIndex).robot; const slot = garage.getSlot();
const robot = slot?.getRobot(coordinator.shortAlias);
if (robot.encPrivKey != null && robot.token != null) { if (robot && slot?.token && robot.encPrivKey != null && robot.token != null) {
void signCleartextMessage(rewardInvoice, robot.encPrivKey, robot.token).then( void signCleartextMessage(rewardInvoice, robot.encPrivKey, robot.token).then(
(signedInvoice) => { (signedInvoice) => {
void coordinator.fetchReward(signedInvoice, garage, slotIndex).then((data) => { void coordinator.fetchReward(signedInvoice, garage, slot?.token).then((data) => {
setBadInvoice(data.bad_invoice ?? ''); setBadInvoice(data.bad_invoice ?? '');
setShowRewardsSpinner(false); setShowRewardsSpinner(false);
setWithdrawn(data.successful_withdrawal); setWithdrawn(data.successful_withdrawal);
@ -103,32 +103,42 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
}; };
const setStealthInvoice = (wantsStealth: boolean): void => { const setStealthInvoice = (wantsStealth: boolean): void => {
void coordinator.fetchStealth(wantsStealth, garage, slotIndex); const slot = garage.getSlot();
if (slot?.token) {
void coordinator.fetchStealth(wantsStealth, garage, slot?.token);
}
}; };
const slot = garage.getSlot();
const robot = slot?.getRobot(coordinator.shortAlias);
return ( return (
<Accordion> <Accordion>
<AccordionSummary expandIcon={<ExpandMore />}> <AccordionSummary expandIcon={<ExpandMore />}>
{`${coordinator.longAlias}:`} {`${coordinator.longAlias}:`}
{garage.getSlot(slotIndex).robot.earnedRewards > 0 && ( {(robot?.earnedRewards ?? 0) > 0 && (
<Typography color='success'>&nbsp;{t('Claim Sats!')} </Typography> <Typography color='success'>&nbsp;{t('Claim Sats!')} </Typography>
)} )}
{(garage.getSlot(slotIndex).robot.activeOrderId ?? 0) > 0 && ( {slot?.activeShortAlias === coordinator.shortAlias && (
<Typography color='success'> <Typography color='success'>
&nbsp;<b>{t('Active order!')}</b> &nbsp;<b>{t('Active order!')}</b>
</Typography> </Typography>
)} )}
{(garage.getSlot(slotIndex).robot.lastOrderId ?? 0) > 0 && {slot?.lastShortAlias === coordinator.shortAlias && (
robot.activeOrderId === undefined && ( <Typography color='warning'>&nbsp;{t('finished order')}</Typography>
<Typography color='warning'>&nbsp;{t('finished order')}</Typography> )}
)}
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<List dense disablePadding={true}> <List dense disablePadding={true}>
{(garage.getSlot(slotIndex).robot.activeOrderId ?? 0) > 0 ? ( {}
{slot?.activeShortAlias && slot?.activeShortAlias === coordinator.shortAlias ? (
<ListItemButton <ListItemButton
onClick={() => { onClick={() => {
navigate(`/order/${coordinator.shortAlias}/${String(robot.activeOrderId)}`); navigate(
`/order/${slot?.activeShortAlias}/${String(
slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId,
)}`,
);
onClose(); onClose();
}} }}
> >
@ -138,14 +148,20 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
</Badge> </Badge>
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={t('One active order #{{orderID}}', { orderID: robot.activeOrderId })} primary={t('One active order #{{orderID}}', {
orderID: slot?.getRobot(slot?.activeShortAlias ?? '')?.activeOrderId,
})}
secondary={t('Your current order')} secondary={t('Your current order')}
/> />
</ListItemButton> </ListItemButton>
) : (garage.getSlot(slotIndex).robot.lastOrderId ?? 0) > 0 ? ( ) : (robot?.lastOrderId ?? 0) > 0 && slot?.lastShortAlias === coordinator.shortAlias ? (
<ListItemButton <ListItemButton
onClick={() => { onClick={() => {
navigate(`/order/${coordinator.shortAlias}/${String(robot.lastOrderId)}`); navigate(
`/order/${slot?.lastShortAlias}/${String(
slot?.getRobot(slot?.lastShortAlias ?? '')?.lastOrderId,
)}`,
);
onClose(); onClose();
}} }}
> >
@ -153,7 +169,9 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
<Numbers color='primary' /> <Numbers color='primary' />
</ListItemIcon> </ListItemIcon>
<ListItemText <ListItemText
primary={t('Your last order #{{orderID}}', { orderID: robot.lastOrderId })} primary={t('Your last order #{{orderID}}', {
orderID: slot?.getRobot(slot?.lastShortAlias ?? '')?.lastOrderId,
})}
secondary={t('Inactive order')} secondary={t('Inactive order')}
/> />
</ListItemButton> </ListItemButton>
@ -176,8 +194,8 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
onClose={() => { onClose={() => {
setOpenEnableTelegram(false); setOpenEnableTelegram(false);
}} }}
tgBotName={robot.tgBotName} tgBotName={robot?.tgBotName ?? ''}
tgToken={robot.tgToken} tgToken={robot?.tgToken ?? ''}
/> />
<ListItem> <ListItem>
@ -186,7 +204,7 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>
{robot.tgEnabled ? ( {robot?.tgEnabled ? (
<Typography color={theme.palette.success.main}> <Typography color={theme.palette.success.main}>
<b>{t('Telegram enabled')}</b> <b>{t('Telegram enabled')}</b>
</Typography> </Typography>
@ -222,9 +240,9 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
label={t('Use stealth invoices')} label={t('Use stealth invoices')}
control={ control={
<Switch <Switch
checked={robot.stealthInvoices} checked={robot?.stealthInvoices}
onChange={() => { onChange={() => {
setStealthInvoice(!robot.stealthInvoices); setStealthInvoice(!robot?.stealthInvoices);
}} }}
/> />
} }
@ -243,12 +261,12 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
<ListItemText secondary={t('Your compensations')}> <ListItemText secondary={t('Your compensations')}>
<Grid container> <Grid container>
<Grid item xs={9}> <Grid item xs={9}>
<Typography>{`${robot.earnedRewards} Sats`}</Typography> <Typography>{`${robot?.earnedRewards} Sats`}</Typography>
</Grid> </Grid>
<Grid item xs={3}> <Grid item xs={3}>
<Button <Button
disabled={robot.earnedRewards === 0} disabled={robot?.earnedRewards === 0}
onClick={() => { onClick={() => {
setOpenClaimRewards(true); setOpenClaimRewards(true);
}} }}
@ -268,7 +286,7 @@ const RobotInfo: React.FC<Props> = ({ robot, slotIndex, coordinator, onClose }:
error={Boolean(badInvoice)} error={Boolean(badInvoice)}
helperText={badInvoice ?? ''} helperText={badInvoice ?? ''}
label={t('Invoice for {{amountSats}} Sats', { label={t('Invoice for {{amountSats}} Sats', {
amountSats: robot.earnedRewards, amountSats: robot?.earnedRewards,
})} })}
size='small' size='small'
value={rewardInvoice} value={rewardInvoice}

View File

@ -47,7 +47,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
}: Props): JSX.Element => { }: Props): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme(); const theme = useTheme();
const { garage } = useContext<UseGarageStoreType>(GarageContext); const { garage, robotUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`)); const [audio] = useState(() => new Audio(`${audioPath}/chat-open.mp3`));
const [connected, setConnected] = useState<boolean>(false); const [connected, setConnected] = useState<boolean>(false);
@ -64,10 +64,10 @@ const EncryptedSocketChat: React.FC<Props> = ({
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
useEffect(() => { useEffect(() => {
if (!connected && garage.getSlot().robot.avatarLoaded) { if (!connected && garage.getSlot()?.avatarLoaded) {
connectWebsocket(); connectWebsocket();
} }
}, [connected, robot]); }, [connected, robotUpdatedAt]);
// Make sure to not keep reconnecting once status is not Chat // Make sure to not keep reconnecting once status is not Chat
useEffect(() => { useEffect(() => {
@ -108,7 +108,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
setConnected(true); setConnected(true);
connection.send({ connection.send({
message: garage.getSlot().robot.pubKey, message: garage.getSlot()?.getRobot()?.pubKey,
nick: userNick, nick: userNick,
}); });
@ -130,10 +130,10 @@ const EncryptedSocketChat: React.FC<Props> = ({
const createJsonFile: () => object = () => { const createJsonFile: () => object = () => {
return { return {
credentials: { credentials: {
own_public_key: garage.getSlot().robot.pubKey, own_public_key: garage.getSlot()?.getRobot()?.pubKey,
peer_public_key: peerPubKey, peer_public_key: peerPubKey,
encrypted_private_key: garage.getSlot().robot.encPrivKey, encrypted_private_key: garage.getSlot()?.getRobot()?.encPrivKey,
passphrase: garage.getSlot().robot.token, passphrase: garage.getSlot()?.token,
}, },
messages, messages,
}; };
@ -141,7 +141,7 @@ const EncryptedSocketChat: React.FC<Props> = ({
const onMessage: (message: any) => void = (message) => { const onMessage: (message: any) => void = (message) => {
const dataFromServer = JSON.parse(message.data); const dataFromServer = JSON.parse(message.data);
const robot = garage.getSlot().robot; const robot = garage.getSlot()?.getRobot();
if (dataFromServer != null && !receivedIndexes.includes(dataFromServer.index)) { if (dataFromServer != null && !receivedIndexes.includes(dataFromServer.index)) {
setReceivedIndexes((prev) => [...prev, dataFromServer.index]); setReceivedIndexes((prev) => [...prev, dataFromServer.index]);
setPeerConnected(dataFromServer.peer_connected); setPeerConnected(dataFromServer.peer_connected);
@ -211,8 +211,8 @@ const EncryptedSocketChat: React.FC<Props> = ({
}; };
const onButtonClicked = (e: React.FormEvent<HTMLFormElement>): void => { const onButtonClicked = (e: React.FormEvent<HTMLFormElement>): void => {
const robot = garage.getSlot().robot; const robot = garage.getSlot()?.getRobot();
if (robot.token !== undefined && value.includes(robot.token)) { if (robot?.token !== undefined && value.includes(robot.token)) {
alert( alert(
`Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`, `Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
); );
@ -263,10 +263,10 @@ const EncryptedSocketChat: React.FC<Props> = ({
}} }}
orderId={Number(orderId)} orderId={Number(orderId)}
messages={messages} messages={messages}
ownPubKey={garage.getSlot().robot.pubKey ?? ''} ownPubKey={garage.getSlot()?.getRobot()?.pubKey ?? ''}
ownEncPrivKey={garage.getSlot().robot.encPrivKey ?? ''} ownEncPrivKey={garage.getSlot()?.getRobot()?.encPrivKey ?? ''}
peerPubKey={peerPubKey ?? 'Not received yet'} peerPubKey={peerPubKey ?? 'Not received yet'}
passphrase={garage.getSlot().robot.token ?? ''} passphrase={garage.getSlot()?.getRobot()?.token ?? ''}
onClickBack={() => { onClickBack={() => {
setAudit(false); setAudit(false);
}} }}

View File

@ -84,12 +84,16 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}, [chatOffset]); }, [chatOffset]);
const loadMessages: () => void = () => { const loadMessages: () => void = () => {
const shortAlias = garage.getSlot()?.activeShortAlias;
if (!shortAlias) return;
const { url, basePath } = federation const { url, basePath } = federation
.getCoordinator(garage.getSlot().activeOrderShortAlias) .getCoordinator(shortAlias)
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); .getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.get(url + basePath, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`, { .get(url + basePath, `/api/chat/?order_id=${orderId}&offset=${lastIndex}`, {
tokenSHA256: garage.getSlot().robot.tokenSHA256, tokenSHA256: garage.getSlot()?.getRobot()?.tokenSHA256 ?? '',
}) })
.then((results: any) => { .then((results: any) => {
if (results != null) { if (results != null) {
@ -106,18 +110,18 @@ const EncryptedTurtleChat: React.FC<Props> = ({
const createJsonFile = (): object => { const createJsonFile = (): object => {
return { return {
credentials: { credentials: {
own_public_key: garage.getSlot().robot.pubKey, own_public_key: garage.getSlot()?.getRobot()?.pubKey,
peer_public_key: peerPubKey, peer_public_key: peerPubKey,
encrypted_private_key: garage.getSlot().robot.encPrivKey, encrypted_private_key: garage.getSlot()?.getRobot()?.encPrivKey,
passphrase: garage.getSlot().robot.token, passphrase: garage.getSlot()?.token,
}, },
messages, messages,
}; };
}; };
const onMessage = (dataFromServer: ServerMessage): void => { const onMessage = (dataFromServer: ServerMessage): void => {
const robot = garage.getSlot().robot; const robot = garage.getSlot();
if (dataFromServer != null) { if (robot && dataFromServer != null) {
// If we receive an encrypted message // If we receive an encrypted message
if (dataFromServer.message.substring(0, 27) === `-----BEGIN PGP MESSAGE-----`) { if (dataFromServer.message.substring(0, 27) === `-----BEGIN PGP MESSAGE-----`) {
void decryptMessage( void decryptMessage(
@ -172,8 +176,8 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}; };
const onButtonClicked = (e: React.FormEvent<HTMLFormElement>): void => { const onButtonClicked = (e: React.FormEvent<HTMLFormElement>): void => {
const robot = garage.getSlot().robot; const robot = garage.getSlot();
if (robot.token !== undefined && value.includes(robot.token)) { if (robot?.token !== undefined && value.includes(robot.token)) {
alert( alert(
`Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`, `Aye! You just sent your own robot robot.token to your peer in chat, that's a catastrophic idea! So bad your message was blocked.`,
); );
@ -182,7 +186,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
// If input string contains '#' send unencrypted and unlogged message // If input string contains '#' send unencrypted and unlogged message
else if (value.substring(0, 1) === '#') { else if (value.substring(0, 1) === '#') {
const { url, basePath } = federation const { url, basePath } = federation
.getCoordinator(garage.getSlot().activeOrderShortAlias ?? '') .getCoordinator(garage.getSlot()?.activeShortAlias ?? '')
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); .getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.post( .post(
@ -193,7 +197,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
order_id: orderId, order_id: orderId,
offset: lastIndex, offset: lastIndex,
}, },
{ tokenSHA256: robot.tokenSHA256 }, { tokenSHA256: robot?.tokenSHA256 },
) )
.then((response) => { .then((response) => {
if (response != null) { if (response != null) {
@ -209,13 +213,13 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}); });
} }
// Else if message is not empty send message // Else if message is not empty send message
else if (value !== '') { else if (value !== '' && robot?.pubKey) {
setWaitingEcho(true); setWaitingEcho(true);
setLastSent(value); setLastSent(value);
encryptMessage(value, robot.pubKey, peerPubKey, robot.encPrivKey, robot.token) encryptMessage(value, robot?.pubKey, peerPubKey ?? '', robot?.encPrivKey, robot?.token)
.then((encryptedMessage) => { .then((encryptedMessage) => {
const { url, basePath } = federation const { url, basePath } = federation
.getCoordinator(garage.getSlot().activeOrderShortAlias ?? '') .getCoordinator(garage.getSlot()?.activeShortAlias ?? '')
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); .getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.post( .post(
@ -226,7 +230,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
order_id: orderId, order_id: orderId,
offset: lastIndex, offset: lastIndex,
}, },
{ tokenSHA256: robot.tokenSHA256 }, { tokenSHA256: robot?.tokenSHA256 },
) )
.then((response) => { .then((response) => {
if (response != null) { if (response != null) {
@ -263,10 +267,10 @@ const EncryptedTurtleChat: React.FC<Props> = ({
}} }}
orderId={Number(orderId)} orderId={Number(orderId)}
messages={messages} messages={messages}
ownPubKey={garage.getSlot().robot.pubKey ?? ''} ownPubKey={garage.getSlot()?.getRobot()?.pubKey ?? ''}
ownEncPrivKey={garage.getSlot().robot.encPrivKey ?? ''} ownEncPrivKey={garage.getSlot()?.getRobot()?.encPrivKey ?? ''}
peerPubKey={peerPubKey ?? 'Not received yet'} peerPubKey={peerPubKey ?? 'Not received yet'}
passphrase={garage.getSlot().robot.token ?? ''} passphrase={garage.getSlot()?.token ?? ''}
onClickBack={() => { onClickBack={() => {
setAudit(false); setAudit(false);
}} }}

View File

@ -40,7 +40,7 @@ export const ChatPrompt = ({
setMessages, setMessages,
}: ChatPromptProps): JSX.Element => { }: ChatPromptProps): JSX.Element => {
const { t } = useTranslation(); const { t } = useTranslation();
const { garage } = useContext<UseGarageStoreType>(GarageContext); const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const [sentButton, setSentButton] = useState<boolean>(false); const [sentButton, setSentButton] = useState<boolean>(false);
const [receivedButton, setReceivedButton] = useState<boolean>(false); const [receivedButton, setReceivedButton] = useState<boolean>(false);
@ -49,9 +49,9 @@ export const ChatPrompt = ({
const [enableDisputeTime, setEnableDisputeTime] = useState<Date>(new Date(order.expires_at)); const [enableDisputeTime, setEnableDisputeTime] = useState<Date>(new Date(order.expires_at));
const [text, setText] = useState<string>(''); const [text, setText] = useState<string>('');
const currencyCode: string = currencies[`${garage.getSlot().order.currency}`]; const currencyCode: string = currencies[`${garage.getSlot()?.order?.currency}`];
const amount: string = pn( const amount: string = pn(
parseFloat(parseFloat(garage.getSlot().order.amount).toFixed(order.currency === 1000 ? 8 : 4)), parseFloat(garage.getSlot()?.order?.amount ?? 0).toFixed(order.currency === 1000 ? 8 : 4),
); );
const disputeCountdownRenderer = function ({ const disputeCountdownRenderer = function ({
@ -113,7 +113,7 @@ export const ChatPrompt = ({
setText(t("The buyer has sent the fiat. Click 'Confirm Received' once you receive it.")); setText(t("The buyer has sent the fiat. Click 'Confirm Received' once you receive it."));
} }
} }
}, [order]); }, [orderUpdatedAt]);
return ( return (
<Grid <Grid

View File

@ -155,8 +155,8 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
} }
const renewOrder = function (): void { const renewOrder = function (): void {
const currentOrder = garage.getSlot().order; const currentOrder = garage.getSlot()?.order;
if (currentOrder !== null) { if (currentOrder) {
const body = { const body = {
type: currentOrder.type, type: currentOrder.type,
currency: currentOrder.currency, currency: currentOrder.currency,
@ -179,7 +179,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl); .getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient apiClient
.post(url + basePath, '/api/make/', body, { .post(url + basePath, '/api/make/', body, {
tokenSHA256: garage.getSlot().robot.tokenSHA256, tokenSHA256: garage.getSlot()?.getRobot()?.tokenSHA256,
}) })
.then((data: any) => { .then((data: any) => {
if (data.bad_request !== undefined) { if (data.bad_request !== undefined) {
@ -203,8 +203,8 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
statement, statement,
rating, rating,
}: SubmitActionProps): void { }: SubmitActionProps): void {
const robot = garage.getSlot().robot; const robot = garage.getSlot()?.getRobot();
const currentOrder = garage.getSlot().order; const currentOrder = garage.getSlot()?.order;
void apiClient void apiClient
.post( .post(
@ -219,7 +219,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
statement, statement,
rating, rating,
}, },
{ tokenSHA256: robot.tokenSHA256 }, { tokenSHA256: robot?.tokenSHA256 },
) )
.then((data: Order) => { .then((data: Order) => {
setOpen(closeAll); setOpen(closeAll);
@ -269,7 +269,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
}; };
const updateInvoice = function (invoice: string): void { const updateInvoice = function (invoice: string): void {
const robot = garage.getSlot().robot; const robot = garage.getSlot()?.getRobot();
if (robot?.encPrivKey != null && robot?.token != null) { if (robot?.encPrivKey != null && robot?.token != null) {
setLoadingButtons({ ...noLoadingButtons, submitInvoice: true }); setLoadingButtons({ ...noLoadingButtons, submitInvoice: true });
@ -284,7 +284,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
}; };
const updateAddress = function (): void { const updateAddress = function (): void {
const robot = garage.getSlot().robot; const robot = garage.getSlot()?.getRobot();
if (robot?.encPrivKey != null && robot?.token != null) { if (robot?.encPrivKey != null && robot?.token != null) {
setLoadingButtons({ ...noLoadingButtons, submitAddress: true }); setLoadingButtons({ ...noLoadingButtons, submitAddress: true });
@ -306,10 +306,10 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
}; };
const submitStatement = function (): void { const submitStatement = function (): void {
const robot = garage.getSlot().robot; const robot = garage.getSlot()?.getRobot();
let statement = dispute.statement; let statement = dispute.statement;
if (dispute.attachLogs) { if (dispute.attachLogs) {
const payload = { statement, messages, token: robot.token }; const payload = { statement, messages, token: robot?.token };
statement = JSON.stringify(payload, null, 2); statement = JSON.stringify(payload, null, 2);
} }
setLoadingButtons({ ...noLoadingButtons, submitStatement: true }); setLoadingButtons({ ...noLoadingButtons, submitStatement: true });
@ -361,15 +361,15 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
// Effect on Order Status change (used for WebLN) // Effect on Order Status change (used for WebLN)
useEffect(() => { useEffect(() => {
const currentOrder = garage.getSlot().order; const currentOrder = garage.getSlot()?.order;
if (currentOrder !== null && currentOrder.status !== lastOrderStatus) { if (currentOrder && currentOrder?.status !== lastOrderStatus) {
setLastOrderStatus(currentOrder.status); setLastOrderStatus(currentOrder.status);
void handleWebln(currentOrder); void handleWebln(currentOrder);
} }
}, [orderUpdatedAt]); }, [orderUpdatedAt]);
const statusToContract = function (): Contract { const statusToContract = function (): Contract {
const order = garage.getSlot().order; const order = garage.getSlot()?.order;
const baseContract: Contract = { const baseContract: Contract = {
title: 'Unknown Order Status', title: 'Unknown Order Status',
@ -380,7 +380,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
titleIcon: () => <></>, titleIcon: () => <></>,
}; };
if (order === null) return baseContract; if (!order) return baseContract;
const status = order.status; const status = order.status;
const isBuyer = order.is_buyer; const isBuyer = order.is_buyer;
@ -747,7 +747,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
setOpen(closeAll); setOpen(closeAll);
}} }}
waitingWebln={waitingWebln} waitingWebln={waitingWebln}
isBuyer={garage.getSlot().order?.is_buyer ?? false} isBuyer={garage.getSlot()?.order?.is_buyer ?? false}
/> />
<ConfirmDisputeDialog <ConfirmDisputeDialog
open={open.confirmDispute} open={open.confirmDispute}
@ -770,11 +770,11 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
}} }}
onCollabCancelClick={cancel} onCollabCancelClick={cancel}
loading={loadingButtons.cancel} loading={loadingButtons.cancel}
peerAskedCancel={garage.getSlot().order?.pending_cancel ?? false} peerAskedCancel={garage.getSlot()?.order?.pending_cancel ?? false}
/> />
<ConfirmFiatSentDialog <ConfirmFiatSentDialog
open={open.confirmFiatSent} open={open.confirmFiatSent}
order={garage.getSlot().order} order={garage.getSlot()?.order ?? null}
loadingButton={loadingButtons.fiatSent} loadingButton={loadingButtons.fiatSent}
onClose={() => { onClose={() => {
setOpen(closeAll); setOpen(closeAll);
@ -791,14 +791,14 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
/> />
<ConfirmFiatReceivedDialog <ConfirmFiatReceivedDialog
open={open.confirmFiatReceived} open={open.confirmFiatReceived}
order={garage.getSlot().order} order={garage.getSlot()?.order ?? null}
loadingButton={loadingButtons.fiatReceived} loadingButton={loadingButtons.fiatReceived}
onClose={() => { onClose={() => {
setOpen(closeAll); setOpen(closeAll);
}} }}
onConfirmClick={confirmFiatReceived} onConfirmClick={confirmFiatReceived}
/> />
<CollabCancelAlert order={garage.getSlot().order} /> <CollabCancelAlert order={garage.getSlot()?.order ?? null} />
<Grid <Grid
container container
padding={1} padding={1}
@ -809,7 +809,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
> >
<Grid item> <Grid item>
<Title <Title
order={garage.getSlot().order} order={garage.getSlot()?.order ?? null}
text={contract?.title} text={contract?.title}
color={contract?.titleColor} color={contract?.titleColor}
icon={contract?.titleIcon} icon={contract?.titleIcon}
@ -825,7 +825,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
<Divider /> <Divider />
<BondStatus <BondStatus
status={contract?.bondStatus} status={contract?.bondStatus}
isMaker={garage.getSlot().order?.is_maker ?? false} isMaker={garage.getSlot()?.order?.is_maker ?? false}
/> />
</Grid> </Grid>
) : ( ) : (
@ -834,7 +834,7 @@ const TradeBox = ({ baseUrl, onStartAgain }: TradeBoxProps): JSX.Element => {
<Grid item> <Grid item>
<CancelButton <CancelButton
order={garage.getSlot().order} order={garage.getSlot()?.order ?? null}
onClickCancel={cancel} onClickCancel={cancel}
openCancelDialog={() => { openCancelDialog={() => {
setOpen({ ...closeAll, confirmCancel: true }); setOpen({ ...closeAll, confirmCancel: true });

View File

@ -67,9 +67,8 @@ export const initialFederationContext: UseFederationStoreType = {
export const FederationContext = createContext<UseFederationStoreType>(initialFederationContext); export const FederationContext = createContext<UseFederationStoreType>(initialFederationContext);
export const useFederationStore = (): UseFederationStoreType => { export const useFederationStore = (): UseFederationStoreType => {
const { settings, page, origin, hostUrl, open, torStatus } = const { settings, origin, hostUrl, open, torStatus } = useContext<UseAppStoreType>(AppContext);
useContext<UseAppStoreType>(AppContext); const { setMaker, garage, setBadOrder, robotUpdatedAt } =
const { setMaker, garage, setBadOrder, robotUpdatedAt, badOrder, orderUpdatedAt } =
useContext<UseGarageStoreType>(GarageContext); useContext<UseGarageStoreType>(GarageContext);
const [federation, setFederation] = useState(initialFederationContext.federation); const [federation, setFederation] = useState(initialFederationContext.federation);
const sortedCoordinators = useMemo(() => { const sortedCoordinators = useMemo(() => {
@ -106,10 +105,11 @@ export const useFederationStore = (): UseFederationStoreType => {
const fetchCurrentOrder = (): void => { const fetchCurrentOrder = (): void => {
const activeSlot = garage.getSlot(); const activeSlot = garage.getSlot();
if (activeSlot.activeOrderShortAlias !== null && activeSlot.activeOrderId !== null) { const robot = activeSlot?.getRobot(activeSlot?.activeShortAlias ?? '');
const coordinator = federation.getCoordinator(activeSlot.activeOrderShortAlias); if (robot && robot?.activeOrderId && activeSlot?.activeShortAlias) {
const coordinator = federation.getCoordinator(activeSlot?.activeShortAlias);
coordinator coordinator
.fetchOrder(activeSlot.activeOrderId, activeSlot.robot) .fetchOrder(robot.activeOrderId, robot)
.then((order) => { .then((order) => {
if (order?.bad_request !== undefined) { if (order?.bad_request !== undefined) {
setBadOrder(order.bad_request); setBadOrder(order.bad_request);
@ -135,18 +135,19 @@ export const useFederationStore = (): UseFederationStoreType => {
}, []); }, []);
useEffect(() => { useEffect(() => {
const robot = garage.getSlot().robot; const slot = garage.getSlot();
const robot = slot?.getRobot();
if (robot !== null) { if (robot && garage.currentSlot) {
if (open.profile && robot?.avatarLoaded) { if (open.profile && slot?.avatarLoaded && slot.token) {
void federation.fetchRobot(garage, garage.currentSlot); // refresh/update existing robot void federation.fetchRobot(garage, slot.token); // refresh/update existing robot
} else if ( } else if (
!robot.avatarLoaded && !Boolean(slot?.avatarLoaded) &&
robot.token !== undefined && robot.token !== undefined &&
robot.encPrivKey !== undefined && robot.encPrivKey !== undefined &&
robot.pubKey !== undefined robot.pubKey !== undefined
) { ) {
void federation.fetchRobot(garage, garage.currentSlot); // create new robot with existing token and keys (on network and coordinator change) void federation.fetchRobot(garage, robot.token); // create new robot with existing token and keys (on network and coordinator change)
} }
} }
}, [open.profile, hostUrl, robotUpdatedAt]); }, [open.profile, hostUrl, robotUpdatedAt]);

View File

@ -157,6 +157,7 @@ export class Coordinator {
}; };
loadBook = (onDataLoad: () => void = () => {}): void => { loadBook = (onDataLoad: () => void = () => {}): void => {
if (!this.enabled) return;
if (this.loadingBook) return; if (this.loadingBook) return;
this.loadingBook = true; this.loadingBook = true;
@ -181,6 +182,7 @@ export class Coordinator {
}; };
loadLimits = (onDataLoad: () => void = () => {}): void => { loadLimits = (onDataLoad: () => void = () => {}): void => {
if (!this.enabled) return;
if (this.loadingLimits) return; if (this.loadingLimits) return;
this.loadingLimits = true; this.loadingLimits = true;
@ -208,6 +210,7 @@ export class Coordinator {
}; };
loadInfo = (onDataLoad: () => void = () => {}): void => { loadInfo = (onDataLoad: () => void = () => {}): void => {
if (!this.enabled) return;
if (this.loadingInfo) return; if (this.loadingInfo) return;
this.loadingInfo = true; this.loadingInfo = true;
@ -263,10 +266,12 @@ export class Coordinator {
} }
}; };
fecthRobot = async (garage: Garage, index: number): Promise<Robot | null> => { fecthRobot = async (garage: Garage, token: string): Promise<Robot | null> => {
const robot = garage?.getSlot(index).robot; if (!this.enabled) return null;
if (robot?.token == null) return null; const robot = garage?.getSlot(token)?.getRobot() ?? null;
if (robot?.token !== token) return null;
const authHeaders = robot.getAuthHeaders(); const authHeaders = robot.getAuthHeaders();
@ -292,29 +297,26 @@ export class Coordinator {
last_login: data.last_login, last_login: data.last_login,
pubKey: data.public_key, pubKey: data.public_key,
encPrivKey: data.encrypted_private_key, encPrivKey: data.encrypted_private_key,
copiedToken: Boolean(data.found),
}; };
}) })
.catch((e) => { .catch((e) => {
console.log(e); console.log(e);
}); });
garage.updateRobot( garage.upsertRobot(token, this.shortAlias, {
{ ...newAttributes,
...newAttributes, tokenSHA256: authHeaders.tokenSHA256,
tokenSHA256: authHeaders.tokenSHA256, loading: false,
loading: false, bitsEntropy,
bitsEntropy, shannonEntropy,
shannonEntropy, shortAlias: this.shortAlias,
shortAlias: this.shortAlias, });
},
index,
);
return garage.getSlot(index).robot; return garage.getSlot(this.shortAlias)?.getRobot() ?? null;
}; };
fetchOrder = async (orderId: number, robot: Robot): Promise<Order | null> => { fetchOrder = async (orderId: number, robot: Robot): Promise<Order | null> => {
if (!this.enabled) return null;
if (!(robot.token != null)) return null; if (!(robot.token != null)) return null;
const authHeaders = robot.getAuthHeaders(); const authHeaders = robot.getAuthHeaders();
@ -340,12 +342,14 @@ export class Coordinator {
fetchReward = async ( fetchReward = async (
signedInvoice: string, signedInvoice: string,
garage: Garage, garage: Garage,
index: number, index: string,
): Promise<null | { ): Promise<null | {
bad_invoice?: string; bad_invoice?: string;
successful_withdrawal?: boolean; successful_withdrawal?: boolean;
}> => { }> => {
const robot = garage.getSlot(index).robot; if (!this.enabled) return null;
const robot = garage.getSlot(index)?.getRobot();
if (!(robot?.token != null) || !(robot.encPrivKey != null)) return null; if (!(robot?.token != null) || !(robot.encPrivKey != null)) return null;
@ -357,17 +361,17 @@ export class Coordinator {
}, },
{ tokenSHA256: robot.tokenSHA256 }, { tokenSHA256: robot.tokenSHA256 },
); );
const newRobot = { garage.upsertRobot(robot?.token, this.shortAlias, {
...robot,
earnedRewards: data?.successful_withdrawal === true ? 0 : robot.earnedRewards, earnedRewards: data?.successful_withdrawal === true ? 0 : robot.earnedRewards,
}; });
garage.updateRobot(newRobot, index);
return data ?? {}; return data ?? {};
}; };
fetchStealth = async (wantsStealth: boolean, garage: Garage, index: number): Promise<null> => { fetchStealth = async (wantsStealth: boolean, garage: Garage, index: string): Promise<null> => {
const robot = garage?.getSlot(index).robot; if (!this.enabled) return null;
const robot = garage?.getSlot(index)?.getRobot();
if (!(robot?.token != null) || !(robot.encPrivKey != null)) return null; if (!(robot?.token != null) || !(robot.encPrivKey != null)) return null;
@ -378,12 +382,9 @@ export class Coordinator {
{ tokenSHA256: robot.tokenSHA256 }, { tokenSHA256: robot.tokenSHA256 },
); );
garage.updateRobot( garage.upsertRobot(robot?.token, this.shortAlias, {
{ stealthInvoices: wantsStealth,
stealthInvoices: wantsStealth, });
},
index,
);
return null; return null;
}; };

View File

@ -90,9 +90,9 @@ export class Federation {
}; };
// Fetchs // Fetchs
fetchRobot = async (garage: Garage, slot: number): Promise<void> => { fetchRobot = async (garage: Garage, token: string): Promise<void> => {
Object.values(this.coordinators).forEach((coor) => { Object.values(this.coordinators).forEach((coor) => {
void coor.fecthRobot(garage, slot); void coor.fecthRobot(garage, token);
}); });
}; };

View File

@ -1,67 +1,45 @@
import { Robot, type Order } from '.'; import { type Order } from '.';
import { systemClient } from '../services/System'; import { systemClient } from '../services/System';
import { saveAsJson } from '../utils'; import { saveAsJson } from '../utils';
export interface Slot { import Slot from './Slot.model';
robot: Robot;
lastOrderId: number | null;
lastOrderShortAlias: string | null;
activeOrderId: number | null;
activeOrderShortAlias: string | null;
order: Order | null;
}
const defaultSlot = {
robot: new Robot(),
lastOrderId: null,
lastOrderShortAlias: null,
activeOrderId: null,
activeOrderShortAlias: null,
order: null,
};
type GarageHooks = 'onRobotUpdate' | 'onOrderUpdate'; type GarageHooks = 'onRobotUpdate' | 'onOrderUpdate';
class Garage { class Garage {
constructor() { constructor() {
this.slots = []; this.slots = {};
const slotsDump: string = systemClient.getItem('garage') ?? ''; this.currentSlot = null;
if (slotsDump !== '') {
const rawSlots = JSON.parse(slotsDump);
this.slots = rawSlots
.filter((raw: any) => raw !== null)
.map((raw: any) => {
const robot = new Robot(raw.robot);
robot.update(raw.robot);
return {
...defaultSlot,
...raw,
robot,
order: null,
};
});
console.log('Robot Garage was loaded from local storage');
}
if (this.slots.length < 1) {
this.slots = [defaultSlot];
}
this.currentSlot = 0;
this.hooks = { this.hooks = {
onRobotUpdate: [], onRobotUpdate: [],
onOrderUpdate: [], onOrderUpdate: [],
}; };
const slotsDump: string = systemClient.getItem('garageSlots') ?? '';
if (slotsDump !== '') {
const rawSlots = JSON.parse(slotsDump);
Object.values(rawSlots).forEach((rawSlot: Record<any, any>) => {
if (rawSlot?.token) {
this.createSlot(rawSlot?.token);
Object.keys(rawSlot.robots).forEach((shortAlias) => {
const rawRobot = rawSlot.robots[shortAlias];
this.upsertRobot(rawRobot.token, shortAlias, rawRobot);
});
this.currentSlot = rawSlot?.token;
}
});
console.log('Robot Garage was loaded from local storage');
}
} }
slots: Slot[]; slots: { [token: string]: Slot };
currentSlot: number; currentSlot: string | null;
hooks: Record<GarageHooks, Array<() => void>>; hooks: Record<GarageHooks, Array<() => void>>;
// Hooks // Hooks
registerHook = (hookName: GarageHooks, fn: () => void): void => { registerHook = (hookName: GarageHooks, fn: () => void): void => {
this.hooks[hookName].push(fn); this.hooks[hookName]?.push(fn);
}; };
triggerHook = (hookName: GarageHooks): void => { triggerHook = (hookName: GarageHooks): void => {
@ -72,103 +50,97 @@ class Garage {
// Storage // Storage
download = (): void => { download = (): void => {
saveAsJson(`robotGarage_${new Date().toISOString()}.json`, this.slots); saveAsJson(`garageSlots_${new Date().toISOString()}.json`, this.slots);
}; };
save = (): void => { save = (): void => {
const saveSlots = this.slots.filter((slot: Slot) => slot !== null); systemClient.setItem('garageSlots', JSON.stringify(this.slots));
systemClient.setItem('garage', JSON.stringify(saveSlots)); };
delete = (): void => {
this.slots = {};
this.currentSlot = null;
systemClient.deleteItem('garageSlots');
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
}; };
// Slots // Slots
delete = (): void => { getSlot: (token?: string) => Slot | null = (token) => {
this.slots = [defaultSlot]; const currentToken = token ?? this.currentSlot;
systemClient.deleteItem('garage'); return currentToken ? this.slots[currentToken] ?? null : null;
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
}; };
deleteSlot: (index?: number) => void = (index) => { createSlot: (token: string) => Slot | null = (token) => {
const targetSlot = index ?? this.slots.length - 1; if (token !== null) {
this.slots.splice(targetSlot, 1); this.slots[token] = new Slot(token);
this.currentSlot = 0; return this.slots[token];
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
this.save();
};
getSlot: (index?: number) => Slot = (index = this.currentSlot) => {
if (this.slots[index] === undefined) {
this.slots[index] = defaultSlot;
} }
return null;
};
return this.slots[index]; deleteSlot: (token?: string) => void = (token) => {
const tagetIndex = token ?? this.currentSlot;
if (tagetIndex) {
delete this.slots[tagetIndex];
this.currentSlot = null;
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
this.save();
}
};
updateSlot: (
attributes: { avatarLoaded?: boolean; copiedToken?: boolean },
token?: string,
) => Slot | null = (attributes, token) => {
const slot = this.getSlot(token);
if (attributes) {
if (attributes.avatarLoaded !== undefined) slot?.setAvatarLoaded(attributes.avatarLoaded);
if (attributes.copiedToken !== undefined) slot?.setCopiedToken(attributes.copiedToken);
this.triggerHook('onRobotUpdate');
}
return slot;
}; };
// Robots // Robots
updateRobot: (attributes: Record<any, any>, index?: number) => void = ( upsertRobot: (token: string, shortAlias: string, attributes: Record<any, any>) => void = (
token,
shortAlias,
attributes, attributes,
index = this.currentSlot,
) => { ) => {
const robot = this.getSlot(index).robot; if (token === null || shortAlias === null) return;
if (robot != null) {
robot.update(attributes); let slot = this.getSlot(token);
if (attributes.lastOrderId !== undefined && attributes.lastOrderId != null) {
this.slots[index].lastOrderId = attributes.lastOrderId; if (slot === null && attributes.token) {
this.slots[index].lastOrderShortAlias = attributes.shortAlias; slot = this.createSlot(attributes.token);
if (attributes.lastOrderId === this.slots[index].activeOrderId) { }
this.slots[index].activeOrderId = null;
this.slots[index].activeOrderShortAlias = null; if (slot) {
} slot.upsertRobot(shortAlias, attributes);
}
if (attributes.activeOrderId !== undefined && attributes.activeOrderId != null) {
this.slots[index].activeOrderId = attributes.activeOrderId;
this.slots[index].activeOrderShortAlias = attributes.shortAlias;
this.slots[index].lastOrderId = null;
this.slots[index].lastOrderShortAlias = null;
}
this.triggerHook('onRobotUpdate'); this.triggerHook('onRobotUpdate');
this.save(); this.save();
} }
}; };
createRobot = (attributes: Record<any, any>): void => {
const newSlot = defaultSlot;
newSlot.robot.update(attributes);
this.slots.push(newSlot);
this.currentSlot = this.slots.length - 1;
this.save();
};
getActiveOrderId: (index?: number) => number | null = (index = this.currentSlot) => {
return this.getSlot(index)?.robot?.activeOrderId ?? null;
};
// Orders // Orders
updateOrder: (order: Order, index?: number) => void = (order, index = this.currentSlot) => { updateOrder: (order: Order) => void = (order) => {
const updatedOrder = this.slots[index].order; const slot = this.getSlot();
if (updatedOrder !== null && updatedOrder.id === order.id) { if (slot) {
Object.assign(updatedOrder, order); const updatedOrder = slot.order ?? null;
this.slots[index].order = updatedOrder; if (updatedOrder !== null && updatedOrder.id === order.id) {
} else { Object.assign(updatedOrder, order);
this.slots[index].order = order; slot.order = updatedOrder;
} else {
slot.order = order;
}
if (slot.order?.is_participant) {
slot.activeShortAlias = order.shortAlias;
}
this.triggerHook('onOrderUpdate');
this.save();
} }
if (this.slots[index].order?.is_participant) {
this.slots[index].activeOrderId = this.slots[index].order?.id ?? null;
this.slots[index].activeOrderShortAlias = this.slots[index].order?.shortAlias ?? null;
}
this.triggerHook('onOrderUpdate');
this.save();
};
deleteOrder: (index?: number) => void = (index = this.currentSlot) => {
this.slots[index].order = null;
this.triggerHook('onOrderUpdate');
this.save();
};
getOrder: (index?: number) => Order | null = (index = this.currentSlot) => {
return this.getSlot(index)?.order;
}; };
} }

View File

@ -37,8 +37,7 @@ class Robot {
public loading: boolean = true; public loading: boolean = true;
public found: boolean = false; public found: boolean = false;
public last_login: string = ''; public last_login: string = '';
public copiedToken: boolean = false; public shortAlias: string = '';
public avatarLoaded: boolean = false;
update = (attributes: Record<string, any>): void => { update = (attributes: Record<string, any>): void => {
Object.assign(this, attributes); Object.assign(this, attributes);

View File

@ -0,0 +1,62 @@
import { Robot, type Order } from '.';
class Slot {
constructor(token: string) {
this.token = token;
this.robots = {};
this.order = null;
this.activeShortAlias = null;
this.lastShortAlias = null;
this.copiedToken = false;
this.avatarLoaded = false;
}
token: string | null;
robots: { [shortAlias: string]: Robot };
order: Order | null;
activeShortAlias: string | null;
lastShortAlias: string | null;
copiedToken: boolean;
avatarLoaded: boolean;
setAvatarLoaded = (avatarLoaded: boolean): void => {
this.avatarLoaded = avatarLoaded;
};
setCopiedToken = (copied: boolean): void => {
this.copiedToken = copied;
};
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 (Object.values(this.robots).length > 0) {
return Object.values(this.robots)[0];
}
return null;
};
upsertRobot = (shortAlias: string, attributes: Record<any, any>): Robot | null => {
if (this.robots[shortAlias] === undefined) this.robots[shortAlias] = new Robot();
this.robots[shortAlias].update(attributes);
if (attributes.lastOrderId !== undefined && attributes.lastOrderId != null) {
this.lastShortAlias = shortAlias;
if (this.activeShortAlias === shortAlias) {
this.activeShortAlias = null;
}
}
if (attributes.activeOrderId !== undefined && attributes.activeOrderId != null) {
this.activeShortAlias = attributes.shortAlias;
}
return this.robots[shortAlias];
};
}
export default Slot;

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Intercanviar?", "Swap?": "Intercanviar?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "Pots afegir nous mètodes", "You can add new methods": "Pots afegir nous mètodes",
"You must fill the form correctly": "Has d'omplir el formulari correctament", "You must fill the form correctly": "Has d'omplir el formulari correctament",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "Reps aprox. {{swapSats}} LN Sats (les taxes poden variar)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "Reps aprox. {{swapSats}} LN Sats (les taxes poden variar)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "You can add new methods", "You can add new methods": "You can add new methods",
"You must fill the form correctly": "You must fill the form correctly", "You must fill the form correctly": "You must fill the form correctly",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "Du kannst neue Methoden hinzufügen", "You can add new methods": "Du kannst neue Methoden hinzufügen",
"You must fill the form correctly": "You must fill the form correctly", "You must fill the form correctly": "You must fill the form correctly",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "You can add new methods", "You can add new methods": "You can add new methods",
"You must fill the form correctly": "You must fill the form correctly", "You must fill the form correctly": "You must fill the form correctly",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap fuera de LN ", "Swap out of LN ": "Swap fuera de LN ",
"Swap?": "¿Swap?", "Swap?": "¿Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "Puedes añadir nuevos métodos", "You can add new methods": "Puedes añadir nuevos métodos",
"You must fill the form correctly": "Rellene correctamente el formulario", "You must fill the form correctly": "Rellene correctamente el formulario",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "Recibes aproximadamente {{swapSats}} LN Sats (la comisión puede variar)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "Recibes aproximadamente {{swapSats}} LN Sats (la comisión puede variar)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "You can add new methods", "You can add new methods": "You can add new methods",
"You must fill the form correctly": "You must fill the form correctly", "You must fill the form correctly": "You must fill the form correctly",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Échanger hors de LN ", "Swap out of LN ": "Échanger hors de LN ",
"Swap?": "Échange?", "Swap?": "Échange?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "Vous pouvez ajouter de nouvelles méthodes", "You can add new methods": "Vous pouvez ajouter de nouvelles méthodes",
"You must fill the form correctly": "Vous devez remplir le formulaire correctement", "You must fill the form correctly": "Vous devez remplir le formulaire correctement",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "Vous recevez environ {{swapSats}} LN Sats (les frais peuvent varier).", "You receive approx {{swapSats}} LN Sats (fees might vary)": "Vous recevez environ {{swapSats}} LN Sats (les frais peuvent varier).",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap da LN ", "Swap out of LN ": "Swap da LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "Puoi aggiungere nuovi metodi", "You can add new methods": "Puoi aggiungere nuovi metodi",
"You must fill the form correctly": "Devi compilare il form correttamente", "You must fill the form correctly": "Devi compilare il form correttamente",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "Riceverai circa {{swapSats}} Sats su LN (le commissioni possono variare)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "Riceverai circa {{swapSats}} Sats su LN (le commissioni possono variare)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "スワップ?", "Swap?": "スワップ?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "新しい支払い方法を追加できます。", "You can add new methods": "新しい支払い方法を追加できます。",
"You must fill the form correctly": "フォームに正しく入力する必要があります。", "You must fill the form correctly": "フォームに正しく入力する必要があります。",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "約{{swapSats}} ライトニングSatsを受け取ります手数料は異なる場合があります", "You receive approx {{swapSats}} LN Sats (fees might vary)": "約{{swapSats}} ライトニングSatsを受け取ります手数料は異なる場合があります",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "You can add new methods", "You can add new methods": "You can add new methods",
"You must fill the form correctly": "You must fill the form correctly", "You must fill the form correctly": "You must fill the form correctly",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "You can add new methods", "You can add new methods": "You can add new methods",
"You must fill the form correctly": "You must fill the form correctly", "You must fill the form correctly": "You must fill the form correctly",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "У Вас уже есть активный ордер",
"You can add new methods": "You can add new methods", "You can add new methods": "You can add new methods",
"You must fill the form correctly": "You must fill the form correctly", "You must fill the form correctly": "You must fill the form correctly",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "You can add new methods", "You can add new methods": "You can add new methods",
"You must fill the form correctly": "You must fill the form correctly", "You must fill the form correctly": "You must fill the form correctly",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Badilisha kutoka LN ", "Swap out of LN ": "Badilisha kutoka LN ",
"Swap?": "Badilisha?", "Swap?": "Badilisha?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "Unaweza kuongeza njia mpya", "You can add new methods": "Unaweza kuongeza njia mpya",
"You must fill the form correctly": "Lazima ujaze fomu kwa usahihi", "You must fill the form correctly": "Lazima ujaze fomu kwa usahihi",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "Unapokea takribani {{swapSats}} LN Sats (ada inaweza kutofautiana)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "Unapokea takribani {{swapSats}} LN Sats (ada inaweza kutofautiana)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "Swap?", "Swap?": "Swap?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "You can add new methods", "You can add new methods": "You can add new methods",
"You must fill the form correctly": "You must fill the form correctly", "You must fill the form correctly": "You must fill the form correctly",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "交换?", "Swap?": "交换?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "你可以添加新方式", "You can add new methods": "你可以添加新方式",
"You must fill the form correctly": "你必须正确填写表格", "You must fill the form correctly": "你必须正确填写表格",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "你将接收大约{{swapSats}}闪电聪(费用会造成有所差异)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "你将接收大约{{swapSats}}闪电聪(费用会造成有所差异)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ", "Swap out of LN ": "Swap out of LN ",
"Swap?": "交換?", "Swap?": "交換?",
"To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.", "To protect your privacy, the exact location you pin will be slightly randomized.": "To protect your privacy, the exact location you pin will be slightly randomized.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "你可以添加新方式", "You can add new methods": "你可以添加新方式",
"You must fill the form correctly": "你必須正確填寫表格", "You must fill the form correctly": "你必須正確填寫表格",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "你將接收大約{{swapSats}}閃電聰(費用會造成有所差異)", "You receive approx {{swapSats}} LN Sats (fees might vary)": "你將接收大約{{swapSats}}閃電聰(費用會造成有所差異)",