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 onOrderClicked = function (id: number, shortAlias: string): void {
if (garage.getSlot().robot.avatarLoaded) {
if (garage.getSlot()?.avatarLoaded) {
setDelay(10000);
navigate(`/order/${shortAlias}/${id}`);
} else {

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ const OrderPage = (): JSX.Element => {
const [tab, setTab] = useState<'order' | 'contract'>('contract');
const [baseUrl, setBaseUrl] = useState<string>(hostUrl);
const [currentOrder, setCurrentOrder] = useState<Order | null>(null);
const [currentOrderId, setCurrentOrderId] = useState<number | null>(null);
useEffect(() => {
const coordinator = federation.getCoordinator(params.shortAlias ?? '');
@ -38,30 +39,41 @@ const OrderPage = (): JSX.Element => {
setBaseUrl(`${url}${basePath}`);
if (currentOrder?.id !== Number(params.orderId)) {
const coordinator = federation.getCoordinator(params.shortAlias ?? '');
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);
});
}
const orderId = Number(params.orderId);
if (orderId && currentOrderId !== orderId) setCurrentOrderId(orderId);
}, [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 {
if (currentOrder?.shortAlias != null) {
setOpen((open) => {
return { ...open, coordinator: shortAlias };
return { ...open, coordinator: currentOrder?.shortAlias };
});
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -91,6 +91,11 @@ const MakerForm = ({
const minRangeAmountMultiple = 1.6;
const amountSafeThresholds = [1.03, 0.98];
useEffect(() => {
const slot = garage.getSlot();
if (slot?.token) federation.fetchRobot(garage, slot?.token);
}, [garage.currentSlot]);
useEffect(() => {
setCurrencyCode(currencyDict[fav.currency === 0 ? 1 : fav.currency]);
if (maker.coordinator != null) {
@ -280,12 +285,19 @@ const MakerForm = ({
};
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 } =
federation
.getCoordinator(maker.coordinator)
?.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) {
setSubmittingRequest(true);
@ -565,7 +577,7 @@ const MakerForm = ({
setOpenDialogs(false);
}}
onClickDone={handleCreateOrder}
hasRobot={garage.getSlot().robot.avatarLoaded}
hasRobot={garage.getSlot()?.avatarLoaded ?? false}
onClickGenerateRobot={onClickGenerateRobot}
/>
<F2fMapDialog

View File

@ -85,7 +85,7 @@ const Notifications = ({
const basePageTitle = t('RoboSats - Simple and Private Bitcoin Exchange');
const moveToOrderPage = function (): void {
navigate(`/order/${String(garage.getOrder()?.id)}`);
navigate(`/order/${String(garage.getSlot()?.order?.id)}`);
setShow(false);
};
@ -106,7 +106,7 @@ const Notifications = ({
const Messages: MessagesProps = {
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',
onClick: moveToOrderPage,
sound: audio.ding,
@ -228,9 +228,9 @@ const Notifications = ({
};
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;
@ -293,8 +293,8 @@ const Notifications = ({
// Notify on order status change
useEffect(() => {
const order = garage.getOrder();
if (order !== null) {
const order = garage.getSlot()?.order;
if (order !== undefined && order !== null) {
if (order.status !== oldOrderStatus) {
handleStatusChange(oldOrderStatus, order.status);
setOldOrderStatus(order.status);

View File

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

View File

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

View File

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

View File

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

View File

@ -40,7 +40,7 @@ export const ChatPrompt = ({
setMessages,
}: ChatPromptProps): JSX.Element => {
const { t } = useTranslation();
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { garage, orderUpdatedAt } = useContext<UseGarageStoreType>(GarageContext);
const [sentButton, setSentButton] = 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 [text, setText] = useState<string>('');
const currencyCode: string = currencies[`${garage.getSlot().order.currency}`];
const currencyCode: string = currencies[`${garage.getSlot()?.order?.currency}`];
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 ({
@ -113,7 +113,7 @@ export const ChatPrompt = ({
setText(t("The buyer has sent the fiat. Click 'Confirm Received' once you receive it."));
}
}
}, [order]);
}, [orderUpdatedAt]);
return (
<Grid

View File

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

View File

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

View File

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

View File

@ -90,9 +90,9 @@ export class Federation {
};
// Fetchs
fetchRobot = async (garage: Garage, slot: number): Promise<void> => {
fetchRobot = async (garage: Garage, token: string): Promise<void> => {
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 { saveAsJson } from '../utils';
export interface Slot {
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,
};
import Slot from './Slot.model';
type GarageHooks = 'onRobotUpdate' | 'onOrderUpdate';
class Garage {
constructor() {
this.slots = [];
const slotsDump: string = systemClient.getItem('garage') ?? '';
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.slots = {};
this.currentSlot = null;
this.hooks = {
onRobotUpdate: [],
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[];
currentSlot: number;
slots: { [token: string]: Slot };
currentSlot: string | null;
hooks: Record<GarageHooks, Array<() => void>>;
// Hooks
registerHook = (hookName: GarageHooks, fn: () => void): void => {
this.hooks[hookName].push(fn);
this.hooks[hookName]?.push(fn);
};
triggerHook = (hookName: GarageHooks): void => {
@ -72,103 +50,97 @@ class Garage {
// Storage
download = (): void => {
saveAsJson(`robotGarage_${new Date().toISOString()}.json`, this.slots);
saveAsJson(`garageSlots_${new Date().toISOString()}.json`, this.slots);
};
save = (): void => {
const saveSlots = this.slots.filter((slot: Slot) => slot !== null);
systemClient.setItem('garage', JSON.stringify(saveSlots));
systemClient.setItem('garageSlots', JSON.stringify(this.slots));
};
delete = (): void => {
this.slots = {};
this.currentSlot = null;
systemClient.deleteItem('garageSlots');
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
};
// Slots
delete = (): void => {
this.slots = [defaultSlot];
systemClient.deleteItem('garage');
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
getSlot: (token?: string) => Slot | null = (token) => {
const currentToken = token ?? this.currentSlot;
return currentToken ? this.slots[currentToken] ?? null : null;
};
deleteSlot: (index?: number) => void = (index) => {
const targetSlot = index ?? this.slots.length - 1;
this.slots.splice(targetSlot, 1);
this.currentSlot = 0;
this.triggerHook('onRobotUpdate');
this.triggerHook('onOrderUpdate');
this.save();
};
getSlot: (index?: number) => Slot = (index = this.currentSlot) => {
if (this.slots[index] === undefined) {
this.slots[index] = defaultSlot;
createSlot: (token: string) => Slot | null = (token) => {
if (token !== null) {
this.slots[token] = new Slot(token);
return this.slots[token];
}
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
updateRobot: (attributes: Record<any, any>, index?: number) => void = (
upsertRobot: (token: string, shortAlias: string, attributes: Record<any, any>) => void = (
token,
shortAlias,
attributes,
index = this.currentSlot,
) => {
const robot = this.getSlot(index).robot;
if (robot != null) {
robot.update(attributes);
if (attributes.lastOrderId !== undefined && attributes.lastOrderId != null) {
this.slots[index].lastOrderId = attributes.lastOrderId;
this.slots[index].lastOrderShortAlias = attributes.shortAlias;
if (attributes.lastOrderId === this.slots[index].activeOrderId) {
this.slots[index].activeOrderId = null;
this.slots[index].activeOrderShortAlias = null;
}
}
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;
}
if (token === null || shortAlias === null) return;
let slot = this.getSlot(token);
if (slot === null && attributes.token) {
slot = this.createSlot(attributes.token);
}
if (slot) {
slot.upsertRobot(shortAlias, attributes);
this.triggerHook('onRobotUpdate');
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
updateOrder: (order: Order, index?: number) => void = (order, index = this.currentSlot) => {
const updatedOrder = this.slots[index].order;
if (updatedOrder !== null && updatedOrder.id === order.id) {
Object.assign(updatedOrder, order);
this.slots[index].order = updatedOrder;
} else {
this.slots[index].order = order;
updateOrder: (order: Order) => void = (order) => {
const slot = this.getSlot();
if (slot) {
const updatedOrder = slot.order ?? null;
if (updatedOrder !== null && updatedOrder.id === order.id) {
Object.assign(updatedOrder, order);
slot.order = updatedOrder;
} else {
slot.order = order;
}
if (slot.order?.is_participant) {
slot.activeShortAlias = order.shortAlias;
}
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 found: boolean = false;
public last_login: string = '';
public copiedToken: boolean = false;
public avatarLoaded: boolean = false;
public shortAlias: string = '';
update = (attributes: Record<string, any>): void => {
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?": "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.",
"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 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)",

View File

@ -387,6 +387,7 @@
"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.",
"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)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"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.",
"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 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)",

View File

@ -387,6 +387,7 @@
"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.",
"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)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap fuera de 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.",
"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 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)",

View File

@ -387,6 +387,7 @@
"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.",
"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)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Échanger hors de LN ",
"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.",
"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 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).",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap da 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.",
"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 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)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ",
"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.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "新しい支払い方法を追加できます。",
"You must fill the form correctly": "フォームに正しく入力する必要があります。",
"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?": "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.",
"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)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"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.",
"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)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"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.",
"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)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"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.",
"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)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Badilisha kutoka LN ",
"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.",
"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 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)",

View File

@ -387,6 +387,7 @@
"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.",
"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)": "You receive approx {{swapSats}} LN Sats (fees might vary)",

View File

@ -387,6 +387,7 @@
"Swap out of LN ": "Swap out of LN ",
"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.",
"You are already maker of an active order": "You are already maker of an active order",
"You can add new methods": "你可以添加新方式",
"You must fill the form correctly": "你必须正确填写表格",
"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?": "交換?",
"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 must fill the form correctly": "你必須正確填寫表格",
"You receive approx {{swapSats}} LN Sats (fees might vary)": "你將接收大約{{swapSats}}閃電聰(費用會造成有所差異)",