import React, { useEffect, useState } from 'react'; import { Page } from '../basic/NavBar'; import { OpenDialogs } from '../basic/MainDialogs'; import { Book, LimitList, Maker, Robot, Garage, Info, Settings, Favorites, defaultMaker, defaultInfo, Coordinator, Order, } from '../models'; import { apiClient } from '../services/api'; import { systemClient } from '../services/System'; import { checkVer, getHost, tokenStrength } from '../utils'; import { sha256 } from 'js-sha256'; import defaultCoordinators from '../../static/federation.json'; import { useTheme } from '@mui/material'; 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' 60000, // 'Sucessful trade' 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 { action?: 'login' | 'generate' | 'refresh'; newKeys?: { encPrivKey: string; pubKey: string } | null; newToken?: string | null; refCode?: string | null; slot?: number | null; setBadRequest?: (state: string) => void; } export type TorStatus = 'NOTINIT' | 'STARTING' | '"Done"' | 'DONE'; export interface AppContextProps { torStatus: TorStatus; federation: Coordinator[]; setFederation: (state: Coordinator[]) => void; settings: Settings; setSettings: (state: Settings) => void; book: Book; info: Info; garage: Garage; setGarage: (state: Garage) => void; currentSlot: number; setCurrentSlot: (state: number) => void; setBook: (state: Book) => void; fetchBook: () => void; limits: { list: LimitList; loading: boolean }; setLimits: (state: { list: LimitList; loading: boolean }) => void; fetchLimits: () => void; maker: Maker; setMaker: (state: Maker) => void; clearOrder: () => void; robot: Robot; setRobot: (state: Robot) => void; focusedCoordinator: number; setFocusedCoordinator: (state: number) => void; baseUrl: string; setBaseUrl: (state: string) => void; fav: Favorites; setFav: (state: Favorites) => void; order: Order | undefined; setOrder: (state: Order | undefined) => void; badOrder: string; setBadOrder: (state: string | undefined) => void; setDelay: (state: number) => void; page: Page; setPage: (state: Page) => void; slideDirection: SlideDirection; setSlideDirection: (state: SlideDirection) => void; currentOrder: number | undefined; setCurrentOrder: (state: number) => void; navbarHeight: number; closeAll: OpenDialogs; open: OpenDialogs; setOpen: (state: OpenDialogs) => void; windowSize: { width: number; height: number }; clientVersion: { semver: Version; short: string; long: string; }; } const entryPage: Page | '' = window.NativeRobosats === undefined ? window.location.pathname.split('/')[1] : ''; const closeAll = { more: false, learn: false, community: false, info: false, coordinator: false, exchange: false, client: false, update: false, profile: false, }; // export const initialState = { // federation: defaultFederation, // setFederation: () => null, // settings: new Settings(), // setSettings: () => null, // book: { orders: [], loading: true }, // setBook: () => null, // fetchBook: () => null, // limits: { // list: [], // loading: true, // }, // setLimits:() => null, // fetchLimits: ()=> null, // maker: defaultMaker, // setMaker: () => null, // clearOrder: () => null, // robot: new Robot(), // setRobot: () => null, // info: defaultExchange, // setExchange: () => null, // focusedCoordinator: 0, // setFocusedCoordinator: () => null, // baseUrl: '', // setBaseUrl: () => null, // fav: { type: null, currency: 0 }, // setFav: () => null, // order: undefined, // setOrder: () => null, // badOrder: '', // setBadOrder: () => null, // setDelay: () => null, // page: entryPage == '' ? 'robot' : entryPage, // setPage: () => null, // slideDirection: { // in: undefined, // out: undefined, // }, // setSlideDirection: () => null, // currentOrder: undefined, // setCurrentOrder: () => null, // navbarHeight: 2.5, // closeAll, // open: closeAll, // setOpen: () => null, // windowSize: getWindowSize(14), // } export interface AppContextProviderProps { children: React.ReactNode; settings: Settings; setSettings: (state: Settings) => void; } export const AppContextProvider = ({ children, settings, setSettings, }: AppContextProviderProps): JSX.Element => { const theme = useTheme(); // All app data structured const [torStatus, setTorStatus] = useState('NOTINIT'); const [book, setBook] = useState({ orders: [], loading: true }); const [limits, setLimits] = useState<{ list: LimitList; loading: boolean }>({ list: [], loading: true, }); const [garage, setGarage] = useState(() => { const initialState = { setGarage }; const newGarage = new Garage(initialState); return newGarage; }); const [currentSlot, setCurrentSlot] = useState(garage.slots.length - 1); const [robot, setRobot] = useState(new Robot(garage.slots[currentSlot].robot)); const [maker, setMaker] = useState(defaultMaker); const [info, setInfo] = useState(defaultInfo); const [coordinators, setCoordinators] = useState(defaultCoordinators); const [baseUrl, setBaseUrl] = useState(''); const [fav, setFav] = useState({ type: null, mode: 'fiat', currency: 0 }); const [delay, setDelay] = useState(60000); const [timer, setTimer] = useState(setInterval(() => null, delay)); const [order, setOrder] = useState(undefined); const [badOrder, setBadOrder] = useState(undefined); const entryPage: Page | '' = window.NativeRobosats === undefined ? window.location.pathname.split('/')[1] : ''; const [page, setPage] = useState(entryPage == '' ? 'robot' : entryPage); const [slideDirection, setSlideDirection] = useState({ in: undefined, out: undefined, }); const [currentOrder, setCurrentOrder] = useState(undefined); const navbarHeight = 2.5; const closeAll = { more: false, learn: false, community: false, info: false, coordinator: false, stats: false, update: false, profile: false, }; const [open, setOpen] = useState(closeAll); const [windowSize, setWindowSize] = useState<{ width: number; height: number }>( getWindowSize(theme.typography.fontSize), ); useEffect(() => { window.addEventListener('torStatus', (event) => { // UX improv: delay the "Conencted" status by 10 secs to avoid long waits for first requests setTimeout(() => setTorStatus(event?.detail), event?.detail === '"Done"' ? 5000 : 0); }); }, []); useEffect(() => { if (typeof window !== undefined) { window.addEventListener('resize', onResize); } if (baseUrl != '') { setBook({ orders: [], loading: true }); setLimits({ list: [], loading: true }); fetchBook(); fetchLimits(); } return () => { if (typeof window !== undefined) { window.removeEventListener('resize', onResize); } }; }, [baseUrl]); useEffect(() => { let host = ''; let protocol = ''; if (window.NativeRobosats === undefined) { host = getHost(); protocol = location.protocol; } else { protocol = 'http:'; host = settings.network === 'mainnet' ? coordinators[0].mainnetOnion : coordinators[0].testnetOnion; } setBaseUrl(`${protocol}//${host}`); }, [settings.network]); useEffect(() => { setWindowSize(getWindowSize(theme.typography.fontSize)); }, [theme.typography.fontSize]); const onResize = function () { setWindowSize(getWindowSize(theme.typography.fontSize)); }; const fetchBook = function () { setBook({ ...book, loading: true }); 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, }); setSettings({ ...settings, network: data.network }); }); }; useEffect(() => { if (open.stats || open.coordinator || info.coordinatorVersion == 'v?.?.?') { if (window.NativeRobosats === undefined || torStatus == '"Done"') { fetchInfo(); } } }, [open.stats, open.coordinator]); 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) { if (data.bad_request != undefined) { 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) { apiClient.get(baseUrl, '/api/order/?order_id=' + currentOrder).then(orderReceived); } }; const clearOrder = function () { setOrder(undefined); setBadOrder(undefined); }; const fetchRobot = function ({ action = 'login', newKeys = null, newToken = null, refCode = null, slot = null, setBadRequest = () => {}, }: fetchRobotProps) { const oldRobot = robot; const targetSlot = slot ?? currentSlot; const token = newToken ?? oldRobot.token; if (action != 'refresh') { setRobot(new Robot()); } setBadRequest(''); const requestBody = {}; if (action == 'login' || action == 'refresh') { requestBody.token_sha256 = sha256(token); } else if (action == 'generate' && token != null) { const strength = tokenStrength(token); requestBody.token_sha256 = sha256(token); requestBody.unique_values = strength.uniqueValues; requestBody.counts = strength.counts; requestBody.length = token.length; requestBody.ref_code = refCode; requestBody.public_key = newKeys?.pubKey ?? oldRobot.pubKey; requestBody.encrypted_private_key = newKeys?.encPrivKey ?? oldRobot.encPrivKey; } apiClient.post(baseUrl, '/api/user/', requestBody).then((data: any) => { let newRobot = robot; setCurrentOrder( data.active_order_id ? data.active_order_id : data.last_order_id ? data.last_order_id : null, ); if (data.bad_request) { setBadRequest(data.bad_request); newRobot = { ...oldRobot, loading: false, nickname: data.nickname ?? oldRobot.nickname, activeOrderId: data.active_order_id ?? null, referralCode: data.referral_code ?? oldRobot.referralCode, earnedRewards: data.earned_rewards ?? oldRobot.earnedRewards, lastOrderId: data.last_order_id ?? oldRobot.lastOrderId, stealthInvoices: data.wants_stealth ?? robot.stealthInvoices, tgEnabled: data.tg_enabled, tgBotName: data.tg_bot_name, tgToken: data.tg_token, found: false, }; } else { newRobot = { ...oldRobot, nickname: data.nickname, token, loading: false, activeOrderId: data.active_order_id ?? null, lastOrderId: data.last_order_id ?? null, referralCode: data.referral_code, 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, bitsEntropy: data.token_bits_entropy, shannonEntropy: data.token_shannon_entropy, pubKey: data.public_key, encPrivKey: data.encrypted_private_key, copiedToken: !!data.found, }; setRobot(newRobot); garage.updateRobot(newRobot, targetSlot); setCurrentSlot(targetSlot); systemClient.setItem('robot_token', token); } }); }; useEffect(() => { if (baseUrl != '' && page != 'robot') { if (open.profile && robot.avatarLoaded) { fetchRobot({ action: 'refresh' }); // refresh/update existing robot } else if (!robot.avatarLoaded && robot.token && robot.encPrivKey && robot.pubKey) { fetchRobot({ action: 'generate' }); // create new robot with existing token and keys (on network and coordinator change) } } }, [open.profile, baseUrl]); return ( {children} ); }; export const AppContext = React.createContext();