2023-04-20 14:52:03 +00:00
|
|
|
import React, { createContext, useEffect, useState } from 'react';
|
2023-02-24 19:17:13 +00:00
|
|
|
import { Page } from '../basic/NavBar';
|
|
|
|
import { OpenDialogs } from '../basic/MainDialogs';
|
|
|
|
|
|
|
|
import {
|
|
|
|
Book,
|
|
|
|
LimitList,
|
|
|
|
Maker,
|
|
|
|
Robot,
|
2023-03-02 11:01:06 +00:00
|
|
|
Garage,
|
2023-02-24 19:17:13 +00:00
|
|
|
Info,
|
|
|
|
Settings,
|
|
|
|
Favorites,
|
|
|
|
defaultMaker,
|
|
|
|
defaultInfo,
|
|
|
|
Coordinator,
|
|
|
|
Order,
|
|
|
|
} from '../models';
|
|
|
|
|
|
|
|
import { apiClient } from '../services/api';
|
2023-05-05 10:12:38 +00:00
|
|
|
import { checkVer, getHost, hexToBase91, validateTokenEntropy } from '../utils';
|
2023-02-24 19:17:13 +00:00
|
|
|
import { sha256 } from 'js-sha256';
|
|
|
|
|
|
|
|
import defaultCoordinators from '../../static/federation.json';
|
2023-04-20 14:52:03 +00:00
|
|
|
import { createTheme, Theme } from '@mui/material/styles';
|
|
|
|
import i18n from '../i18n/Web';
|
2023-05-05 10:12:38 +00:00
|
|
|
import { systemClient } from '../services/System';
|
2023-02-24 19:17:13 +00:00
|
|
|
|
|
|
|
const getWindowSize = function (fontSize: number) {
|
|
|
|
// returns window size in EM units
|
|
|
|
return {
|
|
|
|
width: window.innerWidth / fontSize,
|
|
|
|
height: window.innerHeight / fontSize,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
// 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'
|
2023-03-14 17:23:11 +00:00
|
|
|
60000, // 'Sucessful trade'
|
2023-02-24 19:17:13 +00:00
|
|
|
30000, // 'Failed lightning network routing'
|
|
|
|
300000, // 'Wait for dispute resolution'
|
|
|
|
300000, // 'Maker lost dispute'
|
|
|
|
300000, // 'Taker lost dispute'
|
|
|
|
];
|
|
|
|
|
|
|
|
export interface SlideDirection {
|
|
|
|
in: 'left' | 'right' | undefined;
|
|
|
|
out: 'left' | 'right' | undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface fetchRobotProps {
|
2023-05-05 10:12:38 +00:00
|
|
|
newKeys?: { encPrivKey: string; pubKey: string };
|
|
|
|
newToken?: string;
|
|
|
|
slot?: number;
|
|
|
|
isRefresh?: boolean;
|
2023-02-24 19:17:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE';
|
|
|
|
|
|
|
|
const entryPage: Page | '' =
|
|
|
|
window.NativeRobosats === undefined ? window.location.pathname.split('/')[1] : '';
|
|
|
|
|
2023-04-20 14:52:03 +00:00
|
|
|
export const closeAll = {
|
2023-02-24 19:17:13 +00:00
|
|
|
more: false,
|
|
|
|
learn: false,
|
|
|
|
community: false,
|
|
|
|
info: false,
|
|
|
|
coordinator: false,
|
|
|
|
exchange: false,
|
|
|
|
client: false,
|
|
|
|
update: false,
|
|
|
|
profile: false,
|
|
|
|
};
|
|
|
|
|
2023-04-20 14:52:03 +00:00
|
|
|
const makeTheme = function (settings: Settings) {
|
|
|
|
const theme: Theme = createTheme({
|
|
|
|
palette: {
|
|
|
|
mode: settings.mode,
|
|
|
|
background: {
|
|
|
|
default: settings.mode === 'dark' ? '#070707' : '#fff',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
typography: { fontSize: settings.fontSize },
|
|
|
|
});
|
|
|
|
|
|
|
|
return theme;
|
|
|
|
};
|
|
|
|
|
|
|
|
export const useAppStore = () => {
|
|
|
|
// State provided right at the top level of the app. A chaotic bucket of everything.
|
|
|
|
// Contains app-wide state and functions. Triggers re-renders on the full tree often.
|
|
|
|
|
2023-04-24 14:47:34 +00:00
|
|
|
const [settings, setSettings] = useState<Settings>(() => {
|
|
|
|
return new Settings();
|
|
|
|
});
|
|
|
|
const [theme, setTheme] = useState<Theme>(() => {
|
|
|
|
return makeTheme(settings);
|
|
|
|
});
|
2023-02-24 19:17:13 +00:00
|
|
|
|
2023-04-20 14:52:03 +00:00
|
|
|
useEffect(() => {
|
|
|
|
setTheme(makeTheme(settings));
|
2023-05-05 13:39:38 +00:00
|
|
|
}, [settings.fontSize, settings.mode, settings.lightQRs]);
|
2023-04-20 14:52:03 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
i18n.changeLanguage(settings.language);
|
|
|
|
}, []);
|
2023-02-24 19:17:13 +00:00
|
|
|
|
|
|
|
// All app data structured
|
|
|
|
const [torStatus, setTorStatus] = useState<TorStatus>('NOTINIT');
|
|
|
|
const [book, setBook] = useState<Book>({ orders: [], loading: true });
|
|
|
|
const [limits, setLimits] = useState<{ list: LimitList; loading: boolean }>({
|
|
|
|
list: [],
|
|
|
|
loading: true,
|
|
|
|
});
|
2023-04-24 14:47:34 +00:00
|
|
|
const [garage, setGarage] = useState<Garage>(() => {
|
|
|
|
return new Garage();
|
|
|
|
});
|
|
|
|
const [currentSlot, setCurrentSlot] = useState<number>(() => {
|
|
|
|
return garage.slots.length - 1;
|
|
|
|
});
|
|
|
|
const [robot, setRobot] = useState<Robot>(() => {
|
|
|
|
return new Robot(garage.slots[currentSlot].robot);
|
|
|
|
});
|
2023-02-24 19:17:13 +00:00
|
|
|
const [maker, setMaker] = useState<Maker>(defaultMaker);
|
|
|
|
const [info, setInfo] = useState<Info>(defaultInfo);
|
|
|
|
const [coordinators, setCoordinators] = useState<Coordinator[]>(defaultCoordinators);
|
|
|
|
const [baseUrl, setBaseUrl] = useState<string>('');
|
|
|
|
const [fav, setFav] = useState<Favorites>({ type: null, mode: 'fiat', currency: 0 });
|
|
|
|
|
|
|
|
const [delay, setDelay] = useState<number>(60000);
|
|
|
|
const [timer, setTimer] = useState<NodeJS.Timer | undefined>(setInterval(() => null, delay));
|
|
|
|
const [order, setOrder] = useState<Order | undefined>(undefined);
|
|
|
|
const [badOrder, setBadOrder] = useState<string | undefined>(undefined);
|
|
|
|
|
|
|
|
const [page, setPage] = useState<Page>(entryPage == '' ? 'robot' : entryPage);
|
|
|
|
const [slideDirection, setSlideDirection] = useState<SlideDirection>({
|
|
|
|
in: undefined,
|
|
|
|
out: undefined,
|
|
|
|
});
|
|
|
|
const [currentOrder, setCurrentOrder] = useState<number | undefined>(undefined);
|
|
|
|
|
|
|
|
const navbarHeight = 2.5;
|
|
|
|
const [open, setOpen] = useState<OpenDialogs>(closeAll);
|
|
|
|
|
|
|
|
const [windowSize, setWindowSize] = useState<{ width: number; height: number }>(
|
|
|
|
getWindowSize(theme.typography.fontSize),
|
|
|
|
);
|
|
|
|
|
2023-03-02 11:01:06 +00:00
|
|
|
useEffect(() => {
|
|
|
|
window.addEventListener('torStatus', (event) => {
|
2023-04-20 14:52:03 +00:00
|
|
|
// Trick to improve UX on Android webview: delay the "Connected to TOR" status by 5 secs to avoid long waits on the first request.
|
2023-03-04 12:54:06 +00:00
|
|
|
setTimeout(() => setTorStatus(event?.detail), event?.detail === '"Done"' ? 5000 : 0);
|
2023-03-02 11:01:06 +00:00
|
|
|
});
|
|
|
|
}, []);
|
|
|
|
|
2023-02-24 19:17:13 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (typeof window !== undefined) {
|
|
|
|
window.addEventListener('resize', onResize);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (baseUrl != '') {
|
2023-03-04 18:55:24 +00:00
|
|
|
setBook({ orders: [], loading: true });
|
|
|
|
setLimits({ list: [], loading: true });
|
2023-02-24 19:17:13 +00:00
|
|
|
fetchBook();
|
|
|
|
fetchLimits();
|
|
|
|
}
|
|
|
|
return () => {
|
|
|
|
if (typeof window !== undefined) {
|
|
|
|
window.removeEventListener('resize', onResize);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}, [baseUrl]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
let host = '';
|
2023-03-02 11:01:06 +00:00
|
|
|
let protocol = '';
|
2023-02-24 19:17:13 +00:00
|
|
|
if (window.NativeRobosats === undefined) {
|
|
|
|
host = getHost();
|
2023-03-02 11:01:06 +00:00
|
|
|
protocol = location.protocol;
|
2023-02-24 19:17:13 +00:00
|
|
|
} else {
|
2023-03-02 11:01:06 +00:00
|
|
|
protocol = 'http:';
|
2023-02-24 19:17:13 +00:00
|
|
|
host =
|
|
|
|
settings.network === 'mainnet'
|
|
|
|
? coordinators[0].mainnetOnion
|
|
|
|
: coordinators[0].testnetOnion;
|
|
|
|
}
|
2023-03-02 11:01:06 +00:00
|
|
|
setBaseUrl(`${protocol}//${host}`);
|
2023-02-24 19:17:13 +00:00
|
|
|
}, [settings.network]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setWindowSize(getWindowSize(theme.typography.fontSize));
|
|
|
|
}, [theme.typography.fontSize]);
|
|
|
|
|
|
|
|
const onResize = function () {
|
|
|
|
setWindowSize(getWindowSize(theme.typography.fontSize));
|
|
|
|
};
|
|
|
|
|
|
|
|
const fetchBook = function () {
|
2023-04-28 10:02:29 +00:00
|
|
|
setBook((book) => {
|
|
|
|
return { ...book, loading: true };
|
|
|
|
});
|
2023-02-24 19:17:13 +00:00
|
|
|
apiClient.get(baseUrl, '/api/book/').then((data: any) =>
|
|
|
|
setBook({
|
|
|
|
loading: false,
|
|
|
|
orders: data.not_found ? [] : data,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const fetchLimits = async () => {
|
|
|
|
setLimits({ ...limits, loading: true });
|
|
|
|
const data = apiClient.get(baseUrl, '/api/limits/').then((data) => {
|
|
|
|
setLimits({ list: data ?? [], loading: false });
|
|
|
|
return data;
|
|
|
|
});
|
|
|
|
return await data;
|
|
|
|
};
|
|
|
|
|
|
|
|
const fetchInfo = function () {
|
|
|
|
setInfo({ ...info, loading: true });
|
|
|
|
apiClient.get(baseUrl, '/api/info/').then((data: Info) => {
|
|
|
|
const versionInfo: any = checkVer(data.version.major, data.version.minor, data.version.patch);
|
|
|
|
setInfo({
|
|
|
|
...data,
|
|
|
|
openUpdateClient: versionInfo.updateAvailable,
|
|
|
|
coordinatorVersion: versionInfo.coordinatorVersion,
|
|
|
|
clientVersion: versionInfo.clientVersion,
|
|
|
|
loading: false,
|
|
|
|
});
|
2023-03-04 19:42:17 +00:00
|
|
|
setSettings({ ...settings, network: data.network });
|
2023-02-24 19:17:13 +00:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (open.stats || open.coordinator || info.coordinatorVersion == 'v?.?.?') {
|
2023-04-10 11:42:17 +00:00
|
|
|
if (window.NativeRobosats === undefined || torStatus == '"Done"') {
|
|
|
|
fetchInfo();
|
|
|
|
}
|
2023-02-24 19:17:13 +00:00
|
|
|
}
|
2023-04-10 11:42:17 +00:00
|
|
|
}, [open.stats, open.coordinator]);
|
2023-02-24 19:17:13 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
// Sets Setting network from coordinator API param if accessing via web
|
|
|
|
if (settings.network == undefined && info.network) {
|
|
|
|
setSettings((settings: Settings) => {
|
|
|
|
return { ...settings, network: info.network };
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, [info]);
|
|
|
|
|
|
|
|
// Fetch current order at load and in a loop
|
|
|
|
useEffect(() => {
|
|
|
|
if (currentOrder != undefined && (page == 'order' || (order == badOrder) == undefined)) {
|
|
|
|
fetchOrder();
|
|
|
|
}
|
|
|
|
}, [currentOrder, page]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
clearInterval(timer);
|
|
|
|
setTimer(setInterval(fetchOrder, delay));
|
|
|
|
return () => clearInterval(timer);
|
|
|
|
}, [delay, currentOrder, page, badOrder]);
|
|
|
|
|
|
|
|
const orderReceived = function (data: any) {
|
2023-05-07 17:06:10 +00:00
|
|
|
if (data.bad_request) {
|
2023-02-24 19:17:13 +00:00
|
|
|
setBadOrder(data.bad_request);
|
|
|
|
setDelay(99999999);
|
|
|
|
setOrder(undefined);
|
|
|
|
} else {
|
|
|
|
setDelay(
|
|
|
|
data.status >= 0 && data.status <= 18
|
|
|
|
? page == 'order'
|
|
|
|
? statusToDelay[data.status]
|
|
|
|
: statusToDelay[data.status] * 5
|
|
|
|
: 99999999,
|
|
|
|
);
|
|
|
|
setOrder(data);
|
|
|
|
setBadOrder(undefined);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const fetchOrder = function () {
|
|
|
|
if (currentOrder != undefined) {
|
2023-05-05 10:12:38 +00:00
|
|
|
apiClient
|
2023-05-06 13:42:48 +00:00
|
|
|
.get(baseUrl, '/api/order/?order_id=' + currentOrder, { tokenSHA256: robot.tokenSHA256 })
|
2023-05-05 10:12:38 +00:00
|
|
|
.then(orderReceived);
|
2023-02-24 19:17:13 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const clearOrder = function () {
|
|
|
|
setOrder(undefined);
|
|
|
|
setBadOrder(undefined);
|
|
|
|
};
|
|
|
|
|
|
|
|
const fetchRobot = function ({
|
2023-05-05 10:12:38 +00:00
|
|
|
newToken,
|
|
|
|
newKeys,
|
|
|
|
slot,
|
|
|
|
isRefresh = false,
|
|
|
|
}: fetchRobotProps): void {
|
|
|
|
const token = newToken ?? robot.token ?? '';
|
|
|
|
|
|
|
|
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
|
|
|
|
|
|
|
|
if (!hasEnoughEntropy) {
|
|
|
|
return;
|
2023-02-24 19:17:13 +00:00
|
|
|
}
|
|
|
|
|
2023-05-05 10:12:38 +00:00
|
|
|
const tokenSHA256 = hexToBase91(sha256(token));
|
|
|
|
const targetSlot = slot ?? currentSlot;
|
|
|
|
const encPrivKey = newKeys?.encPrivKey ?? robot.encPrivKey ?? '';
|
|
|
|
const pubKey = newKeys?.pubKey ?? robot.pubKey ?? '';
|
|
|
|
|
2023-05-06 13:42:48 +00:00
|
|
|
// On first authenticated requests, pubkey and privkey are needed in header cookies
|
|
|
|
const auth = {
|
|
|
|
tokenSHA256,
|
|
|
|
keys: {
|
|
|
|
pubKey: pubKey.split('\n').join('\\'),
|
|
|
|
encPrivKey: encPrivKey.split('\n').join('\\'),
|
|
|
|
},
|
|
|
|
};
|
2023-05-05 10:12:38 +00:00
|
|
|
|
|
|
|
if (!isRefresh) {
|
|
|
|
setRobot((robot) => {
|
|
|
|
return {
|
|
|
|
...robot,
|
|
|
|
loading: true,
|
|
|
|
avatarLoaded: false,
|
2023-03-02 11:01:06 +00:00
|
|
|
};
|
2023-05-05 10:12:38 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
apiClient
|
2023-05-06 13:42:48 +00:00
|
|
|
.get(baseUrl, '/api/robot/', auth)
|
2023-05-05 10:12:38 +00:00
|
|
|
.then((data: any) => {
|
|
|
|
const newRobot = {
|
|
|
|
avatarLoaded: isRefresh ? robot.avatarLoaded : false,
|
2023-02-24 19:17:13 +00:00
|
|
|
nickname: data.nickname,
|
2023-03-04 19:42:17 +00:00
|
|
|
token,
|
2023-05-05 10:12:38 +00:00
|
|
|
tokenSHA256,
|
2023-02-24 19:17:13 +00:00
|
|
|
loading: false,
|
|
|
|
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,
|
2023-05-05 10:12:38 +00:00
|
|
|
last_login: data.last_login,
|
|
|
|
bitsEntropy,
|
|
|
|
shannonEntropy,
|
2023-02-24 19:17:13 +00:00
|
|
|
pubKey: data.public_key,
|
|
|
|
encPrivKey: data.encrypted_private_key,
|
2023-03-02 14:20:30 +00:00
|
|
|
copiedToken: !!data.found,
|
2023-03-02 11:01:06 +00:00
|
|
|
};
|
2023-05-05 10:12:38 +00:00
|
|
|
if (currentOrder === undefined) {
|
|
|
|
setCurrentOrder(
|
|
|
|
data.active_order_id
|
|
|
|
? data.active_order_id
|
|
|
|
: data.last_order_id
|
|
|
|
? data.last_order_id
|
|
|
|
: null,
|
|
|
|
);
|
|
|
|
}
|
2023-03-02 11:01:06 +00:00
|
|
|
setRobot(newRobot);
|
|
|
|
garage.updateRobot(newRobot, targetSlot);
|
|
|
|
setCurrentSlot(targetSlot);
|
2023-05-05 10:12:38 +00:00
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
systemClient.deleteCookie('public_key');
|
|
|
|
systemClient.deleteCookie('encrypted_private_key');
|
|
|
|
});
|
2023-02-24 19:17:13 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (baseUrl != '' && page != 'robot') {
|
2023-03-04 18:55:24 +00:00
|
|
|
if (open.profile && robot.avatarLoaded) {
|
2023-05-05 10:12:38 +00:00
|
|
|
fetchRobot({ isRefresh: true }); // refresh/update existing robot
|
2023-03-04 18:55:24 +00:00
|
|
|
} else if (!robot.avatarLoaded && robot.token && robot.encPrivKey && robot.pubKey) {
|
2023-05-05 10:12:38 +00:00
|
|
|
fetchRobot({}); // create new robot with existing token and keys (on network and coordinator change)
|
2023-02-24 19:17:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [open.profile, baseUrl]);
|
|
|
|
|
2023-04-20 14:52:03 +00:00
|
|
|
return {
|
|
|
|
theme,
|
|
|
|
torStatus,
|
|
|
|
settings,
|
|
|
|
setSettings,
|
|
|
|
book,
|
|
|
|
setBook,
|
|
|
|
garage,
|
|
|
|
setGarage,
|
|
|
|
currentSlot,
|
|
|
|
setCurrentSlot,
|
|
|
|
fetchBook,
|
|
|
|
limits,
|
|
|
|
info,
|
|
|
|
setLimits,
|
|
|
|
fetchLimits,
|
|
|
|
maker,
|
|
|
|
setMaker,
|
|
|
|
clearOrder,
|
|
|
|
robot,
|
|
|
|
setRobot,
|
|
|
|
fetchRobot,
|
|
|
|
baseUrl,
|
|
|
|
setBaseUrl,
|
|
|
|
fav,
|
|
|
|
setFav,
|
|
|
|
order,
|
|
|
|
setOrder,
|
|
|
|
badOrder,
|
|
|
|
setBadOrder,
|
|
|
|
setDelay,
|
|
|
|
page,
|
|
|
|
setPage,
|
|
|
|
slideDirection,
|
|
|
|
setSlideDirection,
|
|
|
|
currentOrder,
|
|
|
|
setCurrentOrder,
|
|
|
|
navbarHeight,
|
|
|
|
open,
|
|
|
|
setOpen,
|
|
|
|
windowSize,
|
|
|
|
};
|
2023-02-24 19:17:13 +00:00
|
|
|
};
|
|
|
|
|
2023-04-20 14:52:03 +00:00
|
|
|
export type UseAppStoreType = ReturnType<typeof useAppStore>;
|
|
|
|
|
|
|
|
export const AppContext = createContext<UseAppStoreType | null>(null);
|