import {
Box,
Button, Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider, FormControlLabel,
Stack,
Typography,
} from “@mui/material”;
import FormatPrice from “…/…/Common/FormatPrice”;
import {FormattedNumber} from “react-intl”;
import React, {useEffect, useState} from “react”;
import {useSnackbar} from “notistack”;
import {useAuthContext} from “…/…/AuthProvider/AuthProvider”;
import {useApiKeyProvider} from “…/…/…/providers/ApiKeyProvider/ApiKeyProvider”;
import {SpotBalanceProps} from “./SpotBalance.props”;
import {useBinanceWsProvider} from “…/…/…/providers/BinanceWsProvider/BinanceWsProvider”;
import {useSelector} from “react-redux”;
import {AppState} from “…/…/…/store/store”;
import {selectSpotBinanceSymbolState} from “…/…/…/store/binanceSymbol”;
import {convertAssets} from “…/…/…/actions/cicap-diary-account”;

const skipAssets = [“BNB”, “DNT”, “DON”, “ETHW”];

const SpotBalance = ({spotBalances}: SpotBalanceProps) => {
const [spotFinResult] = useState(0);
const [convertBalances, setConvertBalances] = useState<SpotBalanceElementPriceChecked[]>([]);
const [showConvertSpotBalanceModal, setShowConvertSpotBalanceModal] = useState(false);
const {diaryToken} = useAuthContext();
const {selectedSingleApiKey} = useApiKeyProvider();
const {enqueueSnackbar} = useSnackbar();
const {spotSubscribe} = useBinanceWsProvider();
const symbols = useSelector(selectSpotBinanceSymbolState);


const handleChangeModal = (asset: string, event: boolean) => {
setConvertBalances(prevState => prevState?.map(b => b.asset === asset ? ({…b, checked: event}) : b));
};

const convertAssetsHandler = () => {
const assets = convertBalances?.filter(a => a.checked === true).map(a => a.asset);

if (assets.length === 0) {
enqueueSnackbar(“Выберите монеты для конвертации.”, {variant: “error”});

return;
}

if (!diaryToken || null === selectedSingleApiKey) return;

convertAssets(selectedSingleApiKey, diaryToken, assets)
.then(data => {
if (!data) return;

if (data.result) {
enqueueSnackbar(“Конвертация завершена успешно.”, {variant: “success”});
}

setShowConvertSpotBalanceModal(false);
});
};

useEffect(() => {
if (symbols.length === 0) return;

convertBalances
.filter(symbol => symbols.includes(${symbol.asset}USDT.toUpperCase()))
.forEach((symbol: SpotBalanceElementPriceChecked) => {
spotSubscribe(${symbol.asset}usdt.toLowerCase());
});

}, [convertBalances, symbols]);

useEffect(() => {
setConvertBalances(
spotBalances
.filter(b => !skipAssets.includes(b.asset))
.map(a => ({…a, checked: false})),
);
}, [spotBalances]);

return <>
<Box sx={{backgroundColor: “#f5f5f5”, borderRadius: “8px”, p: 2, mb: 2}}>
<Typography fontWeight=“bold”>Спотовый счет</Typography>
<Typography color={spotFinResult >= 0 ? theme => theme.palette.green.main : “red”} fontSize=“xx-large” fontWeight=“bold”>
{spotFinResult > 0 ? “+” : “”}{FormatPrice({amount: spotFinResult, currency_iso_code: “USD”})}
</Typography>
<Divider sx={{mb: 2}} />
{
spotBalances?.map((balance) => {
return <Stack direction=“row” justifyContent=“space-between” alignItems=“center” key={balance.asset}>
<Typography>{balance.asset}</Typography>
<Typography>
<FormattedNumber
value={balance.free}
minimumFractionDigits={2}
maximumFractionDigits={10}
/>
</Typography>
</Stack>;
})
}
<Divider sx={{my: 2}} />
<Box sx={{display: “flex”, justifyContent: “center”, fontSize: “small”}} >
<Button
variant=“contained”
sx={{fontSize: “small”}}
onClick={() => {
setShowConvertSpotBalanceModal(true);
}}
>
Конвертировать в BNB
</Button>
</Box>
</Box>

<Dialog
open={showConvertSpotBalanceModal}
onClose={() => {
setShowConvertSpotBalanceModal(false);
}}
fullWidth
maxWidth=“xs”
>
<DialogTitle>Конвертировать в BNB</DialogTitle>
<DialogContent dividers sx={{px: 0}}>
{
convertBalances.length > 0 ? (
convertBalances?.map((balance, index) =>
<SpotBalanceComponent
handleChangeModal={handleChangeModal}
key={balance.asset}
balance={balance}
index={index}
/>
)
) : (
<Typography>Нет монет для конвертации</Typography>
)
}
</DialogContent>
<DialogActions>
<Button
variant=“contained”
onClick={convertAssetsHandler}
>
Конвертировать
</Button>
</DialogActions>
</Dialog>
</>;
};

export default SpotBalance;

type SpotBalanceElementPriceChecked = {
asset: string;
free: number;
locked: number;
checked?: boolean;
lastPrice?: string;
}

type SpotBalanceComponentType = {
balance: SpotBalanceElementPriceChecked;
handleChangeModal: (asset: string, event: boolean) => void;
index: number;
}

const SpotBalanceComponent = ({balance, handleChangeModal, index}:SpotBalanceComponentType) => {
const price = useSelector((state: AppState) => state.binancePrice.spot[${balance.asset}usdt.toLowerCase()]);
console.log(price);

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
handleChangeModal(balance.asset, event.target.checked);
};

return <Stack
direction=“row”
justifyContent=“space-between”
alignItems=“center”
sx={{
backgroundColor: (index - 1) % 2 ? “#f5f5f5” : “#ffffff”,
px: 2,
}}
>
<FormControlLabel
control={
<Checkbox
name=“spot-balance-element-checkbox”
disabled={balance.free === 0}
checked={balance.checked}
onChange={handleChange}
/>
}
label={balance.asset}
/>
<Stack direction=“column” alignItems=“flex-end” justifyContent=“center” spacing={0} py={1}>
<Typography fontSize=“small” fontWeight=“bold” sx={{lineHeight: 1.2}}>
<FormattedNumber
value={balance.free}
minimumFractionDigits={2}
maximumFractionDigits={10}
/>
</Typography>
<Typography fontSize=“small” sx={{lineHeight: 1.2}}>
{
price && +price !== 0
? <FormatPrice amount={+price * balance.free} currency_iso_code=“USD” />
: <></>
}
</Typography>
</Stack>
</Stack>;
};
import {BinanceWsProviderProps} from “./BinanceWsProvider.props”;
import {ReadyState, useBinancePrice} from “…/…/hooks/binancePrices”;
import {createContext, useContext, useEffect, useState} from “react”;

interface BinanceWsProviderContext {
futuresSubscribe: (symbol: string) => void;
futuresUnsubscribe: (symbol: string) => void;
futuresReadyState: number;
spotSubscribe: (symbol: string) => void;
spotUnsubscribe: (symbol: string) => void;
spotReadyState: number;
}

const defaultContextValue: BinanceWsProviderContext = {
futuresSubscribe: (symbol: string) => {},
futuresUnsubscribe: (symbol: string) => {},
futuresReadyState: 0,
spotSubscribe: (symbol: string) => {},
spotUnsubscribe: (symbol: string) => {},
spotReadyState: 0,
};

const Context = createContext(defaultContextValue);

export function useBinanceWsProvider() {
return useContext(Context);
}

const BinanceWsProvider = ({children}: BinanceWsProviderProps) => {
const [futuresSymbols, setFuturesSymbols] = useState<Array<string>>([]);
const [futuresQueue, setFuturesQueue] = useState<Array<string>>([]);
const [spotSymbols, setSpotSymbols] = useState<Array<string>>([]);
const [spotQueue, setSpotQueue] = useState<Array<string>>([]);
const {
futuresSubscribe,
futuresUnsubscribe,
futuresReadyState,
spotSubscribe,
spotUnsubscribe,
spotReadyState,
} = useBinancePrice();

const futuresSymbolSubscribe = (symbol: string) => {
if (futuresSymbols.includes(symbol) || futuresQueue.includes(symbol)) return;

setFuturesQueue(prev => […prev, symbol]);
};

const futuresSymbolUnsubscribe = (symbol: string) => {
if (!futuresSymbols.includes(symbol) && !futuresQueue.includes(symbol)) return;

// TODO: сделать счетчик слушателей, если все отпишутся - отправлять UNSUBSCRIBE
// futuresUnsubscribe(symbol);
setFuturesQueue(prev => prev.filter(item => item !== symbol));
};

const subscribeFromFuturesQueue = () => {
futuresQueue.forEach(symbol => {
futuresSubscribe(symbol);
});
setFuturesSymbols([…futuresSymbols, …futuresQueue]);
setFuturesQueue([]);
};

useEffect(() => {
if (futuresQueue.length > 0 && futuresReadyState === ReadyState.OPEN) {
subscribeFromFuturesQueue();
}
}, [futuresQueue]);

useEffect(() => {
if (futuresReadyState === ReadyState.CLOSED) {
setFuturesQueue([…futuresSymbols, …futuresQueue]);
setFuturesSymbols([]);
}

if (futuresQueue.length > 0 && futuresReadyState === ReadyState.OPEN) {
subscribeFromFuturesQueue();
}
}, [futuresReadyState]);

const spotSymbolSubscribe = (symbol: string) => {
if (spotSymbols.includes(symbol) || spotQueue.includes(symbol)) return;

setSpotQueue(prev => […prev, symbol]);
};

const spotSymbolUnsubscribe = (symbol: string) => {
if (!spotSymbols.includes(symbol) && !spotQueue.includes(symbol)) return;

// TODO: сделать счетчик слушателей, если все отпишутся - отправлять UNSUBSCRIBE
// spotUnsubscribe(symbol);
setSpotQueue(prev => prev.filter(item => item !== symbol));
};

const subscribeFromSpotQueue = () => {
spotQueue.forEach(symbol => {
spotSubscribe(symbol);
});
setSpotSymbols([…spotSymbols, …spotQueue]);
setSpotQueue([]);
};

useEffect(() => {
if (spotQueue.length > 0 && spotReadyState === ReadyState.OPEN) {
subscribeFromSpotQueue();
}
}, [spotQueue]);

useEffect(() => {
if (spotReadyState === ReadyState.CLOSED) {
setSpotQueue([…spotSymbols, …spotQueue]);
setSpotSymbols([]);
}

if (spotQueue.length > 0 && spotReadyState === ReadyState.OPEN) {
subscribeFromSpotQueue();
}
}, [spotReadyState]);

return <Context.Provider
value={{
futuresSubscribe: futuresSymbolSubscribe,
futuresUnsubscribe: futuresSymbolUnsubscribe,
futuresReadyState: futuresReadyState,
spotSubscribe: spotSymbolSubscribe,
spotUnsubscribe: spotSymbolUnsubscribe,
spotReadyState: spotReadyState,
}}
>
{children}
</Context.Provider>;
};

export default BinanceWsProvider;
import {useCallback, useEffect, useReducer, useState} from “react”;
import {useDispatch} from “react-redux”;
import {setFuturesBinancePriceState, setSpotBinancePriceState} from “…/store/binancePriceSlice”;

export enum ReadyState {
UNINSTANTIATED = -1,
CONNECTING = 0,
OPEN = 1,
CLOSING = 2,
CLOSED = 3,
}

export function useBinancePrice() {
const stream = “@ticker”;
const dispatch = useDispatch();
const [futuresWs, setFuturesWs] = useState<WebSocket|null>(null);
const [futuresReadyState, setFuturesReadyState] = useState(0);
const [spotWs, setSpotWs] = useState<WebSocket|null>(null);
const [spotReadyState, setSpotReadyState] = useState(0);

const connectToWs = () => {
if (“undefined” === typeof window) {
setTimeout(“connectToWs”, 1000);

return;
}

setFuturesWs(new WebSocket(“wss://fstream.binance.com/stream”));
setSpotWs(new WebSocket(“wss://stream.binance.com:9443/stream”));
};

const futuresSubscribe = useCallback((symbol: string) => {
if (null === futuresWs || futuresReadyState !== ReadyState.OPEN) return;

futuresWs.send(JSON.stringify({
id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
method: “SUBSCRIBE”,
params: [
${symbol}${stream},
],
}));
}, [futuresReadyState]);

const futuresUnsubscribe = useCallback((symbol: string) => {
if (null === futuresWs || futuresReadyState !== ReadyState.OPEN) return;

futuresWs.send(JSON.stringify({
id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
method: “UNSUBSCRIBE”,
params: [
${symbol}${stream},
],
}));
}, [futuresReadyState]);

useEffect(() => {
if (null === futuresWs) return;

futuresWs.onmessage = (message: MessageEvent) => {
if (!message.data) return;

const data = JSON.parse(message.data);

if (!data?.stream) return;

dispatch(setFuturesBinancePriceState({symbol: data.stream.replace(stream, “”), price: data.data.c}));
};

futuresWs.onopen = () => {
setFuturesReadyState(ReadyState.OPEN);
};

futuresWs.onclose = () => {
setFuturesReadyState(ReadyState.CLOSED);
};
}, [futuresWs]);

const spotSubscribe = useCallback((symbol: string) => {
if (null === spotWs || spotReadyState !== ReadyState.OPEN) return;

spotWs.send(JSON.stringify({
id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
method: “SUBSCRIBE”,
params: [
${symbol}${stream},
],
}));
// console.log(spotWs);
}, [spotReadyState]);

const spotUnsubscribe = useCallback((symbol: string) => {
if (null === spotWs || spotReadyState !== ReadyState.OPEN) return;

spotWs.send(JSON.stringify({
id: ((new Date()).getTime() + Math.random() * 10000).toFixed(0),
method: “UNSUBSCRIBE”,
params: [
${symbol}${stream},
],
}));
// console.log(spotWs);
}, [spotReadyState]);

useEffect(() => {
if (null === spotWs) return;

spotWs.onmessage = (message: MessageEvent) => {
// console.log(“message”, message);
if (!message.data) return;

const data = JSON.parse(message.data);

if (!data?.stream) return;

dispatch(setSpotBinancePriceState({symbol: data.stream.replace(stream, “”), price: data.data.c}));
};

spotWs.onopen = () => {
setSpotReadyState(ReadyState.OPEN);
};

spotWs.onclose = () => {
setSpotReadyState(ReadyState.CLOSED);
};
}, [spotWs]);

useEffect(() => {
connectToWs();
}, []);

return {
futuresSubscribe,
futuresUnsubscribe,
futuresReadyState,
spotSubscribe,
spotUnsubscribe,
spotReadyState,
};
}

import {createSlice} from “@reduxjs/toolkit”;
import {HYDRATE} from “next-redux-wrapper”;

export interface BinancePriceSlice {
futures: {[key: string]: number};
spot: {[key: string]: number};
}

const initialState: BinancePriceSlice = {
futures: {},
spot: {},
};

export const binancePriceSlice = createSlice({
name: “binancePrice”,
initialState,
reducers: {
setFuturesBinancePriceState(state, action) {
state.futures[action.payload.symbol] = action.payload.price;
},
setSpotBinancePriceState(state, action) {
state.spot[action.payload.symbol] = action.payload.price;
},
},
extraReducers: {
[HYDRATE]: (state, action) => {
return {
…state,
…action.payload,
};
},
},
});

export const {
setFuturesBinancePriceState,
setSpotBinancePriceState,
} = binancePriceSlice.actions;

export default binancePriceSlice.reducer;

проанализируй код и функцию

const SpotBalanceComponent = ({balance, handleChangeModal, index}:SpotBalanceComponentType) => {
const price = useSelector((state: AppState) => state.binancePrice.spot[${balance.asset}usdt.toLowerCase()]);
как работает const price и почему он всегда undefined?

ошибка приходит в   useEffect(() => {
    if (null === spotWs) return;

    spotWs.onmessage = (message: MessageEvent) => {
      console.log("message", message);

error
: 
code
: 
2
msg
: 
"Invalid request: request ID must be an unsigned integer"
[[Prototype]]
: 
Object