This commit is contained in:
koalasat 2024-08-15 20:02:16 +02:00
parent 166018d86f
commit 9008e98214
No known key found for this signature in database
GPG Key ID: 2F7F61C6146AB157
19 changed files with 330 additions and 354 deletions

View File

@ -21,13 +21,13 @@ const App = (): JSX.Element => {
<Suspense fallback='loading'>
<I18nextProvider i18n={i18n}>
<AppContextProvider>
<GarageContextProvider>
<FederationContextProvider>
<FederationContextProvider>
<GarageContextProvider>
<CssBaseline />
{window.NativeRobosats === undefined ? <HostAlert /> : <TorConnectionBadge />}
<Main />
</FederationContextProvider>
</GarageContextProvider>
</GarageContextProvider>
</FederationContextProvider>
</AppContextProvider>
</I18nextProvider>
</Suspense>

View File

@ -12,12 +12,10 @@ import BookTable from '../../components/BookTable';
import { BarChart, FormatListBulleted, Map } from '@mui/icons-material';
import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import MapChart from '../../components/Charts/MapChart';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
const BookPage = (): JSX.Element => {
const { windowSize } = useContext<UseAppStoreType>(AppContext);
const { setDelay } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const navigate = useNavigate();
@ -32,7 +30,6 @@ const BookPage = (): JSX.Element => {
const onOrderClicked = function (id: number, shortAlias: string): void {
if (garage.getSlot()?.hashId) {
setDelay(10000);
navigate(`/order/${shortAlias}/${id}`);
} else {
setOpenNoRobot(true);

View File

@ -14,7 +14,7 @@ import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageCon
const MakerPage = (): JSX.Element => {
const { fav, windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { federation, setDelay } = useContext<UseFederationStoreType>(FederationContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage, maker } = useContext<UseGarageStoreType>(GarageContext);
const { t } = useTranslation();
const navigate = useNavigate();
@ -53,7 +53,6 @@ const MakerPage = (): JSX.Element => {
const onOrderClicked = function (id: number, shortAlias: string): void {
if (garage.getSlot()?.hashId) {
setDelay(10000);
navigate(`/order/${shortAlias}/${id}`);
} else {
setOpenNoRobot(true);

View File

@ -30,19 +30,13 @@ const OrderPage = (): JSX.Element => {
useEffect(() => {
const shortAlias = params.shortAlias;
const orderId = Number(params.orderId);
const robot = garage.getSlot()?.getRobot();
if (
robot &&
orderId &&
shortAlias &&
currentOrder?.id !== orderId &&
currentOrder?.shortAlias !== shortAlias
) {
const slot = garage.getSlot();
if (slot?.token && currentOrder?.id !== orderId && currentOrder?.shortAlias !== shortAlias) {
const order = new Order({ id: orderId, shortAlias });
void order.fecth(federation, robot).then((updatedOrder) => {
void order.fecth(federation, slot).then((updatedOrder) => {
if (Number(params.orderId) === updatedOrder.id) {
setCurrentOrder(order);
garage.updateOrder(order);
slot.updateSlotFromOrder(order);
}
});
}
@ -51,7 +45,7 @@ const OrderPage = (): JSX.Element => {
return () => {
setCurrentOrder(null);
};
}, []);
}, [params.orderId]);
const onClickCoordinator = function (): void {
if (currentOrder?.shortAlias != null) {

View File

@ -45,7 +45,6 @@ const RobotPage = (): JSX.Element => {
if (token !== undefined && token !== null && page === 'robot') {
setInputToken(token);
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
getGenerateRobot(token);
setView('profile');
}
}
@ -70,7 +69,7 @@ const RobotPage = (): JSX.Element => {
pubKey: key.publicKeyArmored,
encPrivKey: key.encryptedPrivateKeyArmored,
});
void federation.fetchRobot(garage, token);
garage.fetchRobot(federation, token);
garage.setCurrentSlot(token);
})
.catch((error) => {

View File

@ -5,10 +5,12 @@ import { AppContext, type UseAppStoreType } from '../../contexts/AppContext';
import FederationTable from '../../components/FederationTable';
import { t } from 'i18next';
import { FederationContext, type UseFederationStoreType } from '../../contexts/FederationContext';
import { GarageContext, UseGarageStoreType } from '../../contexts/GarageContext';
const SettingsPage = (): JSX.Element => {
const { windowSize, navbarHeight } = useContext<UseAppStoreType>(AppContext);
const { federation, addNewCoordinator } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const maxHeight = (windowSize.height - navbarHeight) * 0.85 - 3;
const [newAlias, setNewAlias] = useState<string>('');
const [newUrl, setNewUrl] = useState<string>('');
@ -26,6 +28,7 @@ const SettingsPage = (): JSX.Element => {
fullNewUrl = `http://${newUrl}`;
}
addNewCoordinator(newAlias, fullNewUrl);
garage.syncCoordinator(federation, newAlias);
setNewAlias('');
setNewUrl('');
} else {

View File

@ -301,12 +301,11 @@ const MakerForm = ({
return;
}
const robot = slot?.getRobot();
const auth = robot?.getAuthHeaders();
const auth = slot?.getAuthHeaders();
if (!disableRequest && maker.coordinator && auth && robot) {
if (!disableRequest && maker.coordinator && auth && slot) {
setSubmittingRequest(true);
const order = new Order({
const orderAttributes = {
type: fav.type === 0 ? 1 : 0,
currency: fav.currency === 0 ? 1 : fav.currency,
amount: makerHasAmountRange ? null : maker.amount,
@ -324,13 +323,12 @@ const MakerForm = ({
latitude: maker.latitude,
longitude: maker.longitude,
shortAlias: maker.coordinator,
});
};
order
.make(federation, robot)
slot
.makeOrder(federation, orderAttributes)
.then((order: Order) => {
if (order.id) {
garage.updateOrder(order);
navigate(`/order/${order.shortAlias}/${order.id}`);
} else if (order?.bad_request) {
setBadRequest(order?.bad_request);

View File

@ -313,31 +313,29 @@ const TakeButton = ({
};
const takeOrder = function (): void {
const robot = garage.getSlot()?.getRobot() ?? null;
const slot = garage.getSlot();
if (currentOrder === null || robot === null) return;
if (currentOrder === null || slot === null) return;
setLoadingTake(true);
if (robot && currentOrder) {
currentOrder
.submitAction(federation, robot, {
action: 'take',
amount:
currentOrder?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount),
})
.then((order) => {
if (order?.bad_request !== undefined) {
setBadRequest(order.bad_request);
} else {
setBadRequest('');
navigate(`/order/${order.shortAlias}/${order.id}`);
}
})
.catch(() => {
setBadRequest('Request error');
});
}
currentOrder
.submitAction(federation, slot, {
action: 'take',
amount:
currentOrder?.currency === 1000 ? Number(takeAmount) / 100000000 : Number(takeAmount),
})
.then((order) => {
if (order?.bad_request !== undefined) {
setBadRequest(order.bad_request);
} else {
setBadRequest('');
navigate(`/order/${order.shortAlias}/${order.id}`);
}
})
.catch(() => {
setBadRequest('Request error');
});
};
return (

View File

@ -31,6 +31,7 @@ import { UserNinjaIcon } from '../Icons';
import { getWebln } from '../../utils';
import { signCleartextMessage } from '../../pgp';
import { GarageContext, type UseGarageStoreType } from '../../contexts/GarageContext';
import { FederationContext, UseFederationStoreType } from '../../contexts/FederationContext';
interface Props {
coordinator: Coordinator;
@ -40,6 +41,7 @@ interface Props {
const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) => {
const { garage } = useContext<UseGarageStoreType>(GarageContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const navigate = useNavigate();
const { t } = useTranslation();
@ -72,7 +74,6 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
const handleWeblnInvoiceClicked = async (e: MouseEvent<HTMLButtonElement, MouseEvent>): void => {
e.preventDefault();
const robot = garage.getSlot()?.getRobot(coordinator.shortAlias);
if (robot != null && robot.earnedRewards > 0) {
const webln = await getWebln();
const invoice = webln.makeInvoice(robot.earnedRewards).then(() => {
@ -87,11 +88,11 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
setBadInvoice('');
setShowRewardsSpinner(true);
if (robot?.token && robot.encPrivKey != null) {
if (robot && robot?.token && robot.encPrivKey != null) {
void signCleartextMessage(rewardInvoice, robot.encPrivKey, robot?.token).then(
(signedInvoice) => {
console.log('Signed message:', signedInvoice);
void coordinator.fetchReward(signedInvoice, garage, robot?.token).then((data) => {
void robot.fetchReward(federation, signedInvoice).then((data) => {
setBadInvoice(data.bad_invoice ?? '');
setShowRewardsSpinner(false);
setWithdrawn(data.successful_withdrawal);
@ -104,9 +105,7 @@ const RobotInfo: React.FC<Props> = ({ coordinator, onClose, disabled }: Props) =
};
const setStealthInvoice = (): void => {
if (robot?.token) {
void coordinator.fetchStealth(!robot?.stealthInvoices, garage, robot?.token);
}
if (robot?.token) void robot.fetchStealth(federation, !robot?.stealthInvoices);
};
return (

View File

@ -98,7 +98,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
.getEndpoint(settings.network, origin, settings.selfhostedClient, hostUrl);
apiClient
.get(url + basePath, `/api/chat/?order_id=${order.id}&offset=${lastIndex}`, {
tokenSHA256: garage.getSlot()?.getRobot()?.tokenSHA256 ?? '',
tokenSHA256: garage.getSlot()?.tokenSHA256 ?? '',
})
.then((results: any) => {
if (results != null) {
@ -207,7 +207,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
order_id: order.id,
offset: lastIndex,
},
{ tokenSHA256: robot?.tokenSHA256 ?? '' },
{ tokenSHA256: slot?.tokenSHA256 ?? '' },
)
.then((response) => {
if (response != null) {
@ -240,7 +240,7 @@ const EncryptedTurtleChat: React.FC<Props> = ({
order_id: order.id,
offset: lastIndex,
},
{ tokenSHA256: robot?.tokenSHA256 },
{ tokenSHA256: slot?.tokenSHA256 },
)
.then((response) => {
if (response != null) {

View File

@ -154,10 +154,9 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
const renewOrder = function (): void {
const slot = garage.getSlot();
const robot = slot?.getRobot();
const newOrder = currentOrder;
if (newOrder && robot) {
const order = new Order({
if (newOrder && slot) {
const orderAttributes = {
type: newOrder.type,
currency: newOrder.currency,
amount: newOrder.has_range ? null : newOrder.amount,
@ -173,11 +172,10 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
bond_size: newOrder.bond_size,
latitude: newOrder.latitude,
longitude: newOrder.longitude,
});
void order.make(federation, robot).then((data: Order) => {
if (data?.id) {
navigate(`/order/${String(newOrder?.shortAlias)}/${String(data.id)}`);
}
shortAlias: newOrder.shortAlias,
};
void slot.makeOrder(federation, orderAttributes).then((order: Order) => {
if (order?.id) navigate(`/order/${String(order?.shortAlias)}/${String(order.id)}`);
});
}
};
@ -191,11 +189,11 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
statement,
rating,
}: SubmitActionProps): void {
const robot = garage.getSlot()?.getRobot();
const slot = garage.getSlot();
if (robot && currentOrder) {
if (slot && currentOrder) {
currentOrder
.submitAction(federation, robot, {
.submitAction(federation, slot, {
action,
invoice,
routing_budget_ppm,
@ -214,7 +212,7 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
} else if (data.bad_statement !== undefined) {
setDispute({ ...dispute, badStatement: data.bad_statement });
}
garage.updateOrder(data);
slot.updateSlotFromOrder(data);
})
.catch(() => {
setOpen(closeAll);

View File

@ -13,33 +13,8 @@ import { type Order, Federation, Settings } from '../models';
import { federationLottery } from '../utils';
import { AppContext, type UseAppStoreType } from './AppContext';
import { GarageContext, type UseGarageStoreType } from './GarageContext';
import { type Origin, type Origins } from '../models/Coordinator.model';
// Refresh delays (ms) according to Order status
const defaultDelay = 5000;
const statusToDelay = [
3000, // 'Waiting for maker bond'
35000, // 'Public'
180000, // 'Paused'
3000, // 'Waiting for taker bond'
999999, // 'Cancelled'
999999, // 'Expired'
8000, // 'Waiting for trade collateral and buyer invoice'
8000, // 'Waiting only for seller trade collateral'
8000, // 'Waiting only for buyer invoice'
10000, // 'Sending fiat - In chatroom'
10000, // 'Fiat sent - In chatroom'
100000, // 'In dispute'
999999, // 'Collaboratively cancelled'
10000, // 'Sending satoshis to buyer'
60000, // 'Sucessful trade'
30000, // 'Failed lightning network routing'
300000, // 'Wait for dispute resolution'
300000, // 'Maker lost dispute'
300000, // 'Taker lost dispute'
];
export interface CurrentOrderIdProps {
id: number | null;
shortAlias: string | null;
@ -52,7 +27,6 @@ export interface FederationContextProviderProps {
export interface UseFederationStoreType {
federation: Federation;
sortedCoordinators: string[];
setDelay: Dispatch<SetStateAction<number>>;
coordinatorUpdatedAt: string;
federationUpdatedAt: string;
addNewCoordinator: (alias: string, url: string) => void;
@ -61,7 +35,6 @@ export interface UseFederationStoreType {
export const initialFederationContext: UseFederationStoreType = {
federation: new Federation('onion', new Settings(), ''),
sortedCoordinators: [],
setDelay: () => {},
coordinatorUpdatedAt: '',
federationUpdatedAt: '',
addNewCoordinator: () => {},
@ -72,9 +45,7 @@ export const FederationContext = createContext<UseFederationStoreType>(initialFe
export const FederationContextProvider = ({
children,
}: FederationContextProviderProps): JSX.Element => {
const { settings, page, origin, hostUrl, open, torStatus } =
useContext<UseAppStoreType>(AppContext);
const { setMaker, garage } = useContext<UseGarageStoreType>(GarageContext);
const { settings, page, origin, hostUrl, torStatus } = useContext<UseAppStoreType>(AppContext);
const [federation] = useState(new Federation(origin, settings, hostUrl));
const [sortedCoordinators, setSortedCoordinators] = useState(federationLottery(federation));
const [coordinatorUpdatedAt, setCoordinatorUpdatedAt] = useState<string>(
@ -82,15 +53,7 @@ export const FederationContextProvider = ({
);
const [federationUpdatedAt, setFederationUpdatedAt] = useState<string>(new Date().toISOString());
const [delay, setDelay] = useState<number>(defaultDelay);
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(() =>
setInterval(() => null, delay),
);
useEffect(() => {
setMaker((maker) => {
return { ...maker, coordinator: sortedCoordinators[0] };
}); // default MakerForm coordinator is decided via sorted lottery
federation.registerHook('onFederationUpdate', () => {
setFederationUpdatedAt(new Date().toISOString());
});
@ -99,59 +62,13 @@ export const FederationContextProvider = ({
});
}, []);
useEffect(() => {
clearInterval(timer);
fetchSlotOrder();
return () => {
clearInterval(timer);
};
}, []);
useEffect(() => {
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
void federation.updateUrl(origin, settings, hostUrl);
void federation.update();
const token = garage.getSlot()?.getRobot()?.token;
if (token) void federation.fetchRobot(garage, token);
}
}, [settings.network, settings.useProxy, torStatus]);
const onOrderReceived = (order: Order): void => {
let newDelay = defaultDelay;
if (order?.bad_request) {
newDelay = 99999999;
garage.updateOrder(order);
}
if (order?.id) {
newDelay =
order.status >= 0 && order.status <= 18
? page === 'order'
? statusToDelay[order.status]
: statusToDelay[order.status] * 5 // If user is not looking at "order" tab, refresh less often.
: 99999999;
}
clearInterval(timer);
console.log('New Delay:', newDelay);
setDelay(newDelay);
setTimer(setTimeout(fetchSlotOrder, newDelay));
};
const fetchSlotOrder: () => void = () => {
const slot = garage?.getSlot();
const robot = slot?.getRobot();
if (slot?.token && slot.activeOrder?.id && robot) {
void slot.activeOrder.fecth(federation, robot).then((order) => {
onOrderReceived(order);
garage.updateOrder(order);
});
} else {
clearInterval(timer);
setTimer(setTimeout(fetchSlotOrder, defaultDelay));
}
};
const addNewCoordinator: (alias: string, url: string) => void = (alias, url) => {
if (!federation.coordinators[alias]) {
const attributes: Record<any, any> = {
@ -175,7 +92,6 @@ export const FederationContextProvider = ({
newCoordinator.update(() => {
setCoordinatorUpdatedAt(new Date().toISOString());
});
garage.syncCoordinator(newCoordinator);
setSortedCoordinators(federationLottery(federation));
setFederationUpdatedAt(new Date().toISOString());
}
@ -185,21 +101,11 @@ export const FederationContextProvider = ({
if (page === 'offers') void federation.updateBook();
}, [page]);
// use effects to fetchRobots on Profile open
useEffect(() => {
const slot = garage.getSlot();
if (open.profile && slot?.hashId && slot?.token) {
void federation.fetchRobot(garage, slot?.token); // refresh/update existing robot
}
}, [open.profile]);
return (
<FederationContext.Provider
value={{
federation,
sortedCoordinators,
setDelay,
coordinatorUpdatedAt,
federationUpdatedAt,
addNewCoordinator,

View File

@ -5,10 +5,13 @@ import React, {
type SetStateAction,
useEffect,
type ReactNode,
useContext,
} from 'react';
import { defaultMaker, type Maker, Garage } from '../models';
import { systemClient } from '../services/System';
import { UseAppStoreType, AppContext } from './AppContext';
import { UseFederationStoreType, FederationContext } from './FederationContext';
export interface GarageContextProviderProps {
children: ReactNode;
@ -18,6 +21,7 @@ export interface UseGarageStoreType {
garage: Garage;
maker: Maker;
setMaker: Dispatch<SetStateAction<Maker>>;
setDelay: Dispatch<SetStateAction<number>>;
robotUpdatedAt: string;
orderUpdatedAt: string;
}
@ -26,18 +30,49 @@ export const initialGarageContext: UseGarageStoreType = {
garage: new Garage(),
maker: defaultMaker,
setMaker: () => {},
setDelay: () => {},
robotUpdatedAt: '',
orderUpdatedAt: '',
};
const defaultDelay = 5000;
// Refresh delays (ms) according to Order status
const statusToDelay = [
3000, // 'Waiting for maker bond'
35000, // 'Public'
180000, // 'Paused'
3000, // 'Waiting for taker bond'
999999, // 'Cancelled'
999999, // 'Expired'
8000, // 'Waiting for trade collateral and buyer invoice'
8000, // 'Waiting only for seller trade collateral'
8000, // 'Waiting only for buyer invoice'
10000, // 'Sending fiat - In chatroom'
10000, // 'Fiat sent - In chatroom'
100000, // 'In dispute'
999999, // 'Collaboratively cancelled'
10000, // 'Sending satoshis to buyer'
60000, // 'Sucessful trade'
30000, // 'Failed lightning network routing'
300000, // 'Wait for dispute resolution'
300000, // 'Maker lost dispute'
300000, // 'Taker lost dispute'
];
export const GarageContext = createContext<UseGarageStoreType>(initialGarageContext);
export const GarageContextProvider = ({ children }: GarageContextProviderProps): JSX.Element => {
// All garage data structured
const { settings, torStatus, open, page } = useContext<UseAppStoreType>(AppContext);
const { federation, sortedCoordinators } = useContext<UseFederationStoreType>(FederationContext);
const [garage] = useState<Garage>(initialGarageContext.garage);
const [maker, setMaker] = useState<Maker>(initialGarageContext.maker);
const [robotUpdatedAt, setRobotUpdatedAt] = useState<string>(new Date().toISOString());
const [orderUpdatedAt, setOrderUpdatedAt] = useState<string>(new Date().toISOString());
const [delay, setDelay] = useState<number>(defaultDelay);
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(() =>
setInterval(() => null, delay),
);
const onRobotUpdated = (): void => {
setRobotUpdatedAt(new Date().toISOString());
@ -48,22 +83,69 @@ export const GarageContextProvider = ({ children }: GarageContextProviderProps):
};
useEffect(() => {
setMaker((maker) => {
return { ...maker, coordinator: sortedCoordinators[0] };
}); // default MakerForm coordinator is decided via sorted lottery
garage.registerHook('onRobotUpdate', onRobotUpdated);
garage.registerHook('onOrderUpdate', onOrderUpdate);
}, []);
useEffect(() => {
clearInterval(timer);
void fetchSlotOrder();
return () => {
clearInterval(timer);
};
}, []);
useEffect(() => {
if (window.NativeRobosats === undefined || torStatus === 'ON' || !settings.useProxy) {
const token = garage.getSlot()?.token;
if (token) void garage.fetchRobot(federation, token);
}
}, [settings.network, settings.useProxy, torStatus]);
useEffect(() => {
if (window.NativeRobosats !== undefined && !systemClient.loading) {
garage.loadSlots();
}
}, [systemClient.loading]);
// use effects to fetchRobots on Profile open
useEffect(() => {
const slot = garage.getSlot();
if (open.profile && slot?.hashId && slot?.token) {
void garage.fetchRobot(federation, slot?.token); // refresh/update existing robot
}
}, [open.profile]);
const fetchSlotOrder: () => Promise<void> = async () => {
let newDelay = defaultDelay;
const slot = garage?.getSlot();
if (slot?.activeOrder?.id) {
const order = await slot.fetchActiveOrder(federation);
console.log(order);
if (order?.bad_request) {
newDelay = 99999999;
} else if (order?.id) {
newDelay = order.status >= 0 && order.status <= 18 ? statusToDelay[order.status] : 99999999;
}
console.log('New Delay:', newDelay);
}
clearInterval(timer);
setDelay(newDelay);
setTimer(setTimeout(fetchSlotOrder, newDelay));
};
return (
<GarageContext.Provider
value={{
garage,
maker,
setMaker,
setDelay,
robotUpdatedAt,
orderUpdatedAt,
}}

View File

@ -172,7 +172,6 @@ export class Coordinator {
public loadingInfo: boolean = false;
public limits: LimitList = {};
public loadingLimits: boolean = false;
public loadingRobot: string | null;
updateUrl = (origin: Origin, settings: Settings, hostUrl: string): void => {
if (settings.selfhostedClient && this.shortAlias !== 'local') {
@ -323,109 +322,6 @@ export class Coordinator {
return { url: String(this[network][origin]), basePath: '' };
}
};
fetchRobot = async (garage: Garage, token: string): Promise<Robot | null> => {
if (!this.enabled || !token || this.loadingRobot === token) return null;
const robot = garage?.getSlot(token)?.getRobot() ?? null;
const authHeaders = robot?.getAuthHeaders();
if (!authHeaders) return null;
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
if (!hasEnoughEntropy) return null;
this.loadingRobot = token;
garage.updateRobot(token, this.shortAlias, { loading: true });
const newAttributes = await apiClient
.get(this.url, `${this.basePath}/api/robot/`, authHeaders)
.then((data: any) => {
return {
nickname: data.nickname,
activeOrderId: data.active_order_id ?? null,
lastOrderId: data.last_order_id ?? null,
earnedRewards: data.earned_rewards ?? 0,
stealthInvoices: data.wants_stealth,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
found: data?.found,
last_login: data.last_login,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
};
})
.catch((e) => {
console.log(e);
})
.finally(() => (this.loadingRobot = null));
garage.updateRobot(token, this.shortAlias, {
...newAttributes,
tokenSHA256: authHeaders.tokenSHA256,
loading: false,
bitsEntropy,
shannonEntropy,
shortAlias: this.shortAlias,
});
return garage.getSlot(this.shortAlias)?.getRobot() ?? null;
};
fetchReward = async (
signedInvoice: string,
garage: Garage,
index: string,
): Promise<null | {
bad_invoice?: string;
successful_withdrawal?: boolean;
}> => {
if (!this.enabled) return null;
const slot = garage.getSlot(index);
const robot = slot?.getRobot();
if (!slot?.token || !robot?.encPrivKey) return null;
const data = await apiClient.post(
this.url,
`${this.basePath}/api/reward/`,
{
invoice: signedInvoice,
},
{ tokenSHA256: robot.tokenSHA256 },
);
garage.updateRobot(slot?.token, this.shortAlias, {
earnedRewards: data?.successful_withdrawal === true ? 0 : robot.earnedRewards,
});
return data ?? {};
};
fetchStealth = async (wantsStealth: boolean, garage: Garage, index: string): Promise<null> => {
if (!this.enabled) return null;
const slot = garage.getSlot(index);
const robot = slot?.getRobot();
if (!(slot?.token != null) || !(robot?.encPrivKey != null)) return null;
await apiClient.post(
this.url,
`${this.basePath}/api/stealth/`,
{ wantsStealth },
{ tokenSHA256: robot.tokenSHA256 },
);
garage.updateRobot(slot?.token, this.shortAlias, {
stealthInvoices: wantsStealth,
});
return null;
};
}
export default Coordinator;

View File

@ -135,13 +135,6 @@ export class Federation {
this.triggerHook('onFederationUpdate');
};
// Fetchs
fetchRobot = async (garage: Garage, token: string): Promise<void> => {
Object.values(this.coordinators).forEach((coor) => {
void coor.fetchRobot(garage, token);
});
};
// Coordinators
getCoordinator = (shortAlias: string): Coordinator => {
return this.coordinators[shortAlias];

View File

@ -1,4 +1,4 @@
import { type Coordinator, type Order } from '.';
import { Federation, Order, type Coordinator } from '.';
import { systemClient } from '../services/System';
import { saveAsJson } from '../utils';
import Slot from './Slot.model';
@ -67,11 +67,8 @@ class Garage {
this.triggerHook('onRobotUpdate');
},
);
Object.keys(rawSlot.robots).forEach((shortAlias) => {
const rawRobot = rawSlot.robots[shortAlias];
this.updateRobot(rawSlot.token, shortAlias, rawRobot);
});
this.updateOrder(rawSlot.token, rawSlot.activeOrder);
this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.lastOrder));
this.slots[rawSlot.token].updateSlotFromOrder(new Order(rawSlot.activeOrder));
this.currentSlot = rawSlot?.token;
}
});
@ -131,36 +128,23 @@ class Garage {
}
};
updateRobot: (token: string, shortAlias: string, attributes: Record<any, any>) => void = (
token,
shortAlias,
attributes,
) => {
if (!token || !shortAlias) return;
fetchRobot = async (federation: Federation, token: string): Promise<void> => {
const slot = this.getSlot(token);
if (slot != null) {
slot.updateRobot(shortAlias, { token, ...attributes });
await slot.fetchRobot(federation);
this.save();
this.triggerHook('onRobotUpdate');
}
};
// Orders
updateOrder: (order: Order | null, token?: string) => void = (order, token) => {
const slot = this.getSlot(token);
if (slot != null) {
slot.updateOrder(order);
this.save();
this.triggerHook('onOrderUpdate');
}
};
// Coordinators
syncCoordinator: (coordinator: Coordinator) => void = (coordinator) => {
syncCoordinator: (federation: Federation, shortAlias: string) => void = (
federation,
shortAlias,
) => {
Object.values(this.slots).forEach((slot) => {
slot.syncCoordinator(coordinator, this);
slot.syncCoordinator(federation, shortAlias);
});
};
}

View File

@ -1,6 +1,7 @@
import { apiClient } from '../services/api';
import type Federation from './Federation.model';
import type Robot from './Robot.model';
import Slot from './Slot.model';
export interface SubmitActionProps {
action:
@ -178,7 +179,7 @@ class Order {
Object.assign(this, attributes);
};
make: (federation: Federation, robot: Robot) => Promise<this> = async (federation, robot) => {
make: (federation: Federation, slot: Slot) => Promise<this> = async (federation, slot) => {
const body = {
type: this.type,
currency: this.currency,
@ -197,42 +198,42 @@ class Order {
longitude: this.longitude,
};
if (robot) {
if (slot) {
const coordinator = federation.getCoordinator(this.shortAlias);
const { basePath, url } = coordinator;
const data = await apiClient.post(url + basePath, '/api/make/', body, {
tokenSHA256: robot.tokenSHA256,
tokenSHA256: slot.tokenSHA256,
});
Object.assign(this, data);
if (data) this.update(data);
}
return this;
};
submitAction: (federation: Federation, robot: Robot, action: SubmitActionProps) => Promise<this> =
async (federation, robot, action) => {
submitAction: (federation: Federation, slot: Slot, action: SubmitActionProps) => Promise<this> =
async (federation, slot, action) => {
if (this.id < 1) return this;
if (robot) {
if (slot) {
const coordinator = federation.getCoordinator(this.shortAlias);
const { basePath, url } = coordinator;
const data = await apiClient.post(
url + basePath,
`/api/order/?order_id=${Number(this.id)}`,
action,
{ tokenSHA256: robot?.tokenSHA256 },
{ tokenSHA256: slot?.tokenSHA256 },
);
Object.assign(this, data);
if (data) this.update(data);
}
return this;
};
fecth: (federation: Federation, robot: Robot) => Promise<this> = async (federation, robot) => {
fecth: (federation: Federation, slot: Slot) => Promise<this> = async (federation, slot) => {
if (this.id < 1) return this;
if (!robot) return this;
if (!slot) return this;
const coordinator = federation.getCoordinator(this.shortAlias);
const authHeaders = robot.getAuthHeaders();
const authHeaders = slot.getAuthHeaders();
if (!authHeaders) return this;
const { basePath, url } = coordinator;
const data = await apiClient.get(
@ -241,7 +242,7 @@ class Order {
authHeaders,
);
Object.assign(this, data);
if (data) this.update(data);
return this;
};

View File

@ -1,29 +1,13 @@
import { sha256 } from 'js-sha256';
import { hexToBase91 } from '../utils';
interface AuthHeaders {
tokenSHA256: string;
keys: {
pubKey: string;
encPrivKey: string;
};
}
import { apiClient } from '../services/api';
import Federation from './Federation.model';
import { AuthHeaders } from './Slot.model';
class Robot {
constructor(attributes?: Record<any, any>) {
if (attributes != null) {
this.token = attributes?.token ?? undefined;
this.tokenSHA256 =
attributes?.tokenSHA256 ?? (this.token != null ? hexToBase91(sha256(this.token)) : '');
this.pubKey = attributes?.pubKey ?? undefined;
this.encPrivKey = attributes?.encPrivKey ?? undefined;
}
Object.assign(this, attributes);
}
public token?: string;
public bitsEntropy?: number;
public shannonEntropy?: number;
public tokenSHA256: string = '';
public pubKey?: string;
public encPrivKey?: string;
public stealthInvoices: boolean = true;
@ -37,6 +21,10 @@ class Robot {
public found: boolean = false;
public last_login: string = '';
public shortAlias: string = '';
public bitsEntropy?: number;
public shannonEntropy?: number;
public tokenSHA256: string = '';
public hasEnoughEntropy: boolean = false;
update = (attributes: Record<string, any>): void => {
Object.assign(this, attributes);
@ -55,6 +43,77 @@ class Robot {
},
};
};
fetch = async (federation: Federation): Promise<Robot | null> => {
const authHeaders = this.getAuthHeaders();
const coordinator = federation.getCoordinator(this.shortAlias);
if (!authHeaders || !coordinator || !this.hasEnoughEntropy) return null;
this.loading = true;
await apiClient
.get(coordinator.url, `${coordinator.basePath}/api/robot/`, authHeaders)
.then((data: any) => {
this.update({
nickname: data.nickname,
activeOrderId: data.active_order_id ?? null,
lastOrderId: data.last_order_id ?? null,
earnedRewards: data.earned_rewards ?? 0,
stealthInvoices: data.wants_stealth,
tgEnabled: data.tg_enabled,
tgBotName: data.tg_bot_name,
tgToken: data.tg_token,
found: data?.found,
last_login: data.last_login,
pubKey: data.public_key,
encPrivKey: data.encrypted_private_key,
});
})
.catch((e) => {
console.log(e);
})
.finally(() => (this.loading = false));
return this;
};
fetchReward = async (
federation: Federation,
signedInvoice: string,
): Promise<null | {
bad_invoice?: string;
successful_withdrawal?: boolean;
}> => {
if (!federation) return null;
const coordinator = federation.getCoordinator(this.shortAlias);
const data = await apiClient.post(
coordinator.url,
`${coordinator.basePath}/api/reward/`,
{
invoice: signedInvoice,
},
{ tokenSHA256: this.tokenSHA256 },
);
this.earnedRewards = data?.successful_withdrawal === true ? 0 : this.earnedRewards;
return data ?? {};
};
fetchStealth = async (federation: Federation, wantsStealth: boolean): Promise<void> => {
if (!federation) return;
const coordinator = federation.getCoordinator(this.shortAlias);
await apiClient.post(
coordinator.url,
`${coordinator.basePath}/api/stealth/`,
{ wantsStealth },
{ tokenSHA256: this.tokenSHA256 },
);
this.stealthInvoices = wantsStealth;
};
}
export default Robot;

View File

@ -1,6 +1,15 @@
import { sha256 } from 'js-sha256';
import { type Coordinator, type Garage, Robot, Order } from '.';
import { type Coordinator, type Garage, Robot, Order, Federation } from '.';
import { roboidentitiesClient } from '../services/Roboidentities/Web';
import { hexToBase91, validateTokenEntropy } from '../utils';
export interface AuthHeaders {
tokenSHA256: string;
keys: {
pubKey: string;
encPrivKey: string;
};
}
class Slot {
constructor(
@ -11,6 +20,14 @@ class Slot {
) {
this.token = token;
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
const tokenSHA256 = hexToBase91(sha256(token));
this.hasEnoughEntropy = hasEnoughEntropy;
this.bitsEntropy = bitsEntropy;
this.shannonEntropy = shannonEntropy;
this.tokenSHA256 = tokenSHA256;
this.hashId = sha256(sha256(this.token));
this.nickname = null;
void roboidentitiesClient.generateRoboname(this.hashId).then((nickname) => {
@ -21,7 +38,15 @@ class Slot {
void roboidentitiesClient.generateRobohash(this.hashId, 'large');
this.robots = shortAliases.reduce((acc: Record<string, Robot>, shortAlias: string) => {
acc[shortAlias] = new Robot(robotAttributes);
acc[shortAlias] = new Robot({
...robotAttributes,
shortAlias,
hasEnoughEntropy,
bitsEntropy,
shannonEntropy,
tokenSHA256,
});
this.updateSlotFromRobot(acc[shortAlias]);
return acc;
}, {});
@ -39,11 +64,31 @@ class Slot {
activeOrder: Order | null;
lastOrder: Order | null;
copiedToken: boolean;
hasEnoughEntropy: boolean;
bitsEntropy: number;
shannonEntropy: number;
tokenSHA256: string = '';
pubKey?: string;
encPrivKey?: string;
setCopiedToken = (copied: boolean): void => {
this.copiedToken = copied;
};
getAuthHeaders = (): AuthHeaders | null => {
const tokenSHA256 = this.tokenSHA256 ?? '';
const encPrivKey = this.encPrivKey ?? '';
const pubKey = this.pubKey ?? '';
return {
tokenSHA256,
keys: {
pubKey: pubKey.split('\n').join('\\'),
encPrivKey: encPrivKey.split('\n').join('\\'),
},
};
};
// Robots
getRobot = (shortAlias?: string): Robot | null => {
if (shortAlias) {
@ -58,48 +103,73 @@ class Slot {
return null;
};
updateRobot = (shortAlias: string, attributes: Record<any, any>): Robot | null => {
this.robots[shortAlias].update(attributes);
fetchRobot = async (federation: Federation) => {
Object.values(this.robots).forEach((robot) => {
void robot.fetch(federation).then((robot) => this.updateSlotFromRobot(robot));
});
};
if (attributes.lastOrderId) {
this.lastOrder = new Order({ id: attributes.lastOrderId, shortAlias: attributes.shortAlias });
if (this.activeOrder?.id === attributes.lastOrderId) {
updateSlotFromRobot = (robot: Robot | null): void => {
if (robot?.lastOrderId) {
this.lastOrder = new Order({ id: robot.lastOrderId, shortAlias: robot.shortAlias });
if (this.activeOrder?.id === robot.lastOrderId) {
this.activeOrder = null;
}
}
if (attributes.activeOrderId && this.activeOrder?.id !== attributes.activeOrderId) {
if (robot?.activeOrderId && this.activeOrder?.id !== robot.activeOrderId) {
this.activeOrder = new Order({
id: attributes.activeOrderId,
shortAlias: attributes.shortAlias,
id: robot.activeOrderId,
shortAlias: robot.shortAlias,
});
}
return this.robots[shortAlias];
};
// Orders
updateOrder: (newOrder: Order | null) => void = (newOrder) => {
fetchActiveOrder = async (federation: Federation) => {
const order = await this.activeOrder?.fecth(federation, this);
if (order) this.updateSlotFromOrder(order);
return order;
};
makeOrder = async (federation: Federation, attributes: object): Promise<Order> => {
const order = new Order(attributes);
await order.make(federation, this);
this.activeOrder = order;
return order;
};
updateSlotFromOrder: (newOrder: Order | null) => void = (newOrder) => {
if (newOrder) {
if (
newOrder.id === this.activeOrder?.id &&
newOrder.shortAlias === this.activeOrder?.shortAlias
) {
this.activeOrder?.update(newOrder);
if (newOrder?.bad_request) {
this.activeOrder = null;
this.lastOrder = newOrder;
} else {
this.activeOrder?.update(newOrder);
}
} else if (newOrder?.is_participant && this.lastOrder?.id !== newOrder.id) {
this.activeOrder = newOrder;
}
}
};
syncCoordinator: (coordinator: Coordinator, garage: Garage) => void = (coordinator, garage) => {
syncCoordinator: (federation: Federation, shortAlias: string) => void = (
federation,
shortAlias,
) => {
const defaultRobot = this.getRobot();
if (defaultRobot?.token) {
this.robots[coordinator.shortAlias] = new Robot({
this.robots[shortAlias] = new Robot({
shortAlias,
token: defaultRobot.token,
pubKey: defaultRobot.pubKey,
encPrivKey: defaultRobot.encPrivKey,
});
void coordinator.fetchRobot(garage, defaultRobot.token);
this.robots[shortAlias].fetch(federation);
this.updateSlotFromRobot(this.robots[shortAlias]);
}
};
}