diff --git a/package.json b/package.json index 7c189cfc..03f65ce9 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "pnpm": ">=10.4.0" }, "dependencies": { - "@compolabs/spark-orderbook-ts-sdk": "https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.2.tgz", + "@compolabs/spark-orderbook-ts-sdk": "https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.3.tgz", "@compolabs/tradingview-chart": "^1.0.21", "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a66fa9e4..3cf184fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@compolabs/spark-orderbook-ts-sdk': - specifier: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.2.tgz - version: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.2.tgz(@types/react@18.3.18)(fuels@0.97.2(vitest@2.0.5(@types/node@22.13.4)))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.3.tgz + version: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.3.tgz(@types/react@18.3.18)(fuels@0.97.2(vitest@2.0.5(@types/node@22.13.4)))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@compolabs/tradingview-chart': specifier: ^1.0.21 version: 1.0.21 @@ -916,9 +916,9 @@ packages: '@coinbase/wallet-sdk@4.3.0': resolution: {integrity: sha512-T3+SNmiCw4HzDm4we9wCHCxlP0pqCiwKe4sOwPH3YAK2KSKjxPRydKu6UQJrdONFVLG7ujXvbd/6ZqmvJb8rkw==} - '@compolabs/spark-orderbook-ts-sdk@https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.2.tgz': - resolution: {tarball: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.2.tgz} - version: 1.16.2 + '@compolabs/spark-orderbook-ts-sdk@https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.3.tgz': + resolution: {tarball: https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.3.tgz} + version: 1.16.3 engines: {node: '>=18'} peerDependencies: fuels: '>=0.97.1' @@ -7264,7 +7264,7 @@ snapshots: eventemitter3: 5.0.1 preact: 10.26.0 - '@compolabs/spark-orderbook-ts-sdk@https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.2.tgz(@types/react@18.3.18)(fuels@0.97.2(vitest@2.0.5(@types/node@22.13.4)))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@compolabs/spark-orderbook-ts-sdk@https://registry.npmjs.org/@compolabs/spark-orderbook-ts-sdk/-/spark-orderbook-ts-sdk-1.16.3.tgz(@types/react@18.3.18)(fuels@0.97.2(vitest@2.0.5(@types/node@22.13.4)))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@apollo/client': 3.13.1(@types/react@18.3.18)(graphql-ws@5.16.2(graphql@16.10.0))(graphql@16.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) axios: 1.7.9 diff --git a/src/blockchain/FuelNetwork.ts b/src/blockchain/FuelNetwork.ts index 3144783b..871a90f5 100644 --- a/src/blockchain/FuelNetwork.ts +++ b/src/blockchain/FuelNetwork.ts @@ -269,4 +269,8 @@ export class FuelNetwork { getCompetition = async (...params: Parameters) => { return await this.orderbookSdk.getCompetition(...params); }; + + fetchBalancePnl = async (...params: Parameters) => { + return await this.orderbookSdk.getBalancePnlByUser(...params); + }; } diff --git a/src/components/SelectAssets/SelectAssetsInput.tsx b/src/components/SelectAssets/SelectAssetsInput.tsx index 18589c10..275803f2 100644 --- a/src/components/SelectAssets/SelectAssetsInput.tsx +++ b/src/components/SelectAssets/SelectAssetsInput.tsx @@ -24,6 +24,8 @@ export interface AssetBlockData { balance: string; assetId: string; price?: string; + pnl: string; + pnlPrecent: string; } interface IProps extends Omit, "onSelect"> { diff --git a/src/screens/Dashboard/Dashboard.tsx b/src/screens/Dashboard/Dashboard.tsx index 514ebd2e..7d2d7df8 100644 --- a/src/screens/Dashboard/Dashboard.tsx +++ b/src/screens/Dashboard/Dashboard.tsx @@ -7,7 +7,6 @@ import { DashboardPoints } from "@components/Points/DashboardPoints"; import { SmartFlex } from "@components/SmartFlex"; import { media } from "@themes/breakpoints"; -import AssetsDashboard from "@screens/Dashboard/AssetsDashboard"; import InfoDataGraph from "@screens/Dashboard/InfoDataGraph"; import BottomTables from "@screens/SpotScreen/BottomTables"; import StatusBar from "@screens/SpotScreen/StatusBar"; @@ -25,9 +24,9 @@ const Dashboard = observer(() => { - + - + {/**/} diff --git a/src/screens/Dashboard/InfoDataGraph/index.tsx b/src/screens/Dashboard/InfoDataGraph/index.tsx index a784d217..5ebda232 100644 --- a/src/screens/Dashboard/InfoDataGraph/index.tsx +++ b/src/screens/Dashboard/InfoDataGraph/index.tsx @@ -62,9 +62,18 @@ const NoDataTrading = observer(() => { const InfoDataGraph: React.FC = observer(() => { const { dashboardStore } = useStores(); - const data = dashboardStore.activeUserStat - ? generateTradingData(dashboardStore.getChartDataTrading()) - : generateTradingData(dashboardStore.getChartDataPortfolio()); + const data = (() => { + switch (dashboardStore.activeUserStat) { + case 0: + return generateTradingData(dashboardStore.getChartDataTrading()); + case 1: + return []; + case 2: + return generateTradingData(dashboardStore.getChartDataPortfolio()); + default: + throw new Error(`Unexpected activeUserStat: ${dashboardStore.activeUserStat}`); + } + })(); return data.length > 0 ? : ; }); diff --git a/src/screens/Dashboard/MarketDataSection/MarketAttribute.tsx b/src/screens/Dashboard/MarketDataSection/MarketAttribute.tsx index 531e6d5f..7d3d7393 100644 --- a/src/screens/Dashboard/MarketDataSection/MarketAttribute.tsx +++ b/src/screens/Dashboard/MarketDataSection/MarketAttribute.tsx @@ -37,7 +37,7 @@ export const MarketAttribute: React.FC = observer( {isHaveRange && ( <> - {change.value} + {change.value !== "hide" && {change.value}} {change.percentage} diff --git a/src/screens/Dashboard/MarketDataSection/index.tsx b/src/screens/Dashboard/MarketDataSection/index.tsx index e6f209bb..6f05490a 100644 --- a/src/screens/Dashboard/MarketDataSection/index.tsx +++ b/src/screens/Dashboard/MarketDataSection/index.tsx @@ -18,6 +18,17 @@ const marketData = [ }, isShowDetails: true, }, + { + title: "PNL", + value: "$0", + period: "24h", + change: { + value: "none", + percentage: "0", + direction: "up", + }, + isShowDetails: true, + }, { title: "Trading Volume", value: "$0", @@ -36,6 +47,7 @@ export const MarketDataSection: React.FC = observer(() => { const { dashboardStore } = useStores(); const portfolioVolume = dashboardStore.getChartDataPortfolio(); const tradingVolume = dashboardStore.getChartDataTrading(); + const totalBalancePnl = dashboardStore.totalPnl; useEffect(() => { if (dashboardStore.rowSnapshots.length === 0) { setUserStats(structuredClone(marketData)); @@ -47,9 +59,10 @@ export const MarketDataSection: React.FC = observer(() => { const updatedStats = structuredClone(marketData); updatedStats[0].period = dashboardStore.activeFilter.description ?? dashboardStore.activeFilter.title; updatedStats[1].period = dashboardStore.activeFilter.description ?? dashboardStore.activeFilter.title; - + updatedStats[2].period = dashboardStore.activeFilter.description ?? dashboardStore.activeFilter.title; updatedStats[0].value = `$${sumStatsUser?.value?.toFixed(2)}`; - updatedStats[1].value = `$${sumStatsTrading?.toFixed(2) ?? "0.00"}`; + updatedStats[1].value = `$${Number(totalBalancePnl?.pnl ?? 0)?.toFixed(2) ?? "0.00"}`; + updatedStats[2].value = `$${sumStatsTrading?.toFixed(2) ?? "0.00"}`; const calculateChange = (data: DataPoint[]) => { if (data.length === 0) { return { @@ -70,9 +83,19 @@ export const MarketDataSection: React.FC = observer(() => { }; }; updatedStats[0].change = calculateChange(portfolioVolume); - updatedStats[1].change = calculateChange(tradingVolume); + updatedStats[1].change = { + value: "hide", + percentage: Number(totalBalancePnl?.pnlInPercent ?? 0).toFixed(2) + "%", + direction: Number(totalBalancePnl?.pnlInPercent) > 0 ? "up" : "down", + }; + updatedStats[2].change = calculateChange(tradingVolume); setUserStats(updatedStats); - }, [dashboardStore.rowSnapshots, dashboardStore.activeFilter, dashboardStore.tradeEvents]); + }, [ + dashboardStore.rowSnapshots, + dashboardStore.activeFilter, + dashboardStore.tradeEvents, + dashboardStore.balancePnlByUser, + ]); return ; }); diff --git a/src/screens/SpotScreen/BottomTables/BottomTables.tsx b/src/screens/SpotScreen/BottomTables/BottomTables.tsx index 10c9ebeb..3ccd43e8 100644 --- a/src/screens/SpotScreen/BottomTables/BottomTables.tsx +++ b/src/screens/SpotScreen/BottomTables/BottomTables.tsx @@ -4,14 +4,12 @@ import { observer } from "mobx-react"; import { media } from "@themes/breakpoints"; -import { SpotTableImplProps } from "@screens/SpotScreen/BottomTables/SpotTable/SpotTableImpl"; - import SpotTable from "./SpotTable"; -const BottomTables: React.FC = observer(({ isShowBalance }) => { +const BottomTables = observer(() => { return ( - + ); }); diff --git a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTable.tsx b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTable.tsx index 1ebfaa04..b6bfd964 100644 --- a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTable.tsx +++ b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTable.tsx @@ -1,11 +1,11 @@ import React from "react"; -import SpotTableImpl, { SpotTableImplProps } from "./SpotTableImpl"; +import SpotTableImpl from "./SpotTableImpl"; import { SpotTableVMProvider } from "./SpotTableVM"; -const SpotTable: React.FC = ({ isShowBalance }) => ( +const SpotTable = () => ( - + ); diff --git a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableImpl.tsx b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableImpl.tsx index 185ce661..1e25ecdd 100644 --- a/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableImpl.tsx +++ b/src/screens/SpotScreen/BottomTables/SpotTable/SpotTableImpl.tsx @@ -32,6 +32,30 @@ const orderColumnHelper = createColumnHelper(); const tradeColumnHelper = createColumnHelper(); const balanceColumnHelper = createColumnHelper(); +const generatePnl = (pnl: string, theme: Theme, isCoin: boolean = true) => { + const bnPnl = new BN(pnl ?? 0).decimalPlaces(2, BN.ROUND_UP); + const isPositive = bnPnl.isGreaterThan(0); + const isNegative = bnPnl.isLessThan(0); + const sign = isPositive ? "+" : isNegative ? "-" : ""; + const displayValue = bnPnl.abs().toString(); + + const color = bnPnl.isGreaterThan(0) + ? theme.colors.greenLight + : bnPnl.isLessThan(0) + ? theme.colors.redLight + : undefined; + + return isCoin ? ( + + {`${sign}$${displayValue}`} + + ) : ( + + {`(${sign}${displayValue}%)`} + + ); +}; + const ORDER_COLUMNS = (vm: ReturnType, theme: Theme) => [ orderColumnHelper.accessor("timestamp", { header: "Date", @@ -181,6 +205,18 @@ const BALANCE_COLUMNS = ( ); }, }), + balanceColumnHelper.accessor("pnl", { + header: "PnL", + cell: (props) => { + const pnlPrecent = props.row.original.pnlPrecent; + return ( + + {generatePnl(props.getValue(), theme) ?? 0} + {generatePnl(pnlPrecent, theme, false)} + + ); + }, + }), balanceColumnHelper.accessor("contractBalance", { header: () => { return; @@ -210,10 +246,8 @@ const BALANCE_COLUMNS = ( const minNeedLengthPagination = 10; const startPage = 1; // todo: Упростить логику разделить формирование данных и рендер для декстопа и мобилок -export interface SpotTableImplProps { - isShowBalance?: boolean; -} -const SpotTableImpl: React.FC = observer(({ isShowBalance = true }) => { + +const SpotTableImpl = observer(() => { const { accountStore, settingsStore, balanceStore } = useStores(); const [isLoading, setLoading] = useState(null); const vm = useSpotTableVMProvider(); @@ -245,7 +279,7 @@ const SpotTableImpl: React.FC = observer(({ isShowBalance = const TABS = [ { title: "ORDERS", disabled: false, rowCount: openOrdersCount }, { title: "HISTORY", disabled: false, rowCount: historyOrdersCount }, - ...(isShowBalance ? [{ title: "BALANCES", disabled: false, rowCount: balancesInfoList.length }] : []), + { title: "BALANCES", disabled: false, rowCount: balancesInfoList.length }, ]; useEffect(() => { diff --git a/src/stores/BalanceStore.ts b/src/stores/BalanceStore.ts index 87296748..b44bec16 100644 --- a/src/stores/BalanceStore.ts +++ b/src/stores/BalanceStore.ts @@ -17,6 +17,11 @@ import RootStore from "./RootStore"; const UPDATE_INTERVAL = 5 * 1000; +interface PnlFormatted { + pnl: BN; + pnlPrecent: BN; +} + export class BalanceStore { public balances: Map = new Map(); public contractBalances: Map = new Map(); @@ -57,10 +62,27 @@ export class BalanceStore { }; get formattedBalanceInfoList() { - const { oracleStore } = this.rootStore; + const { oracleStore, dashboardStore } = this.rootStore; const bcNetwork = FuelNetwork.getInstance(); const tokens = bcNetwork.getTokenList(); + const pnls = dashboardStore.balancePnlByUser; + const pnlsFormatted: Record = pnls.reduce( + (acc, el) => { + const asset = CONFIG.ALL_MARKETS.find((item) => item.contractId === el.market)?.baseAssetId; + if (!asset) return acc; + + if (!acc[asset]) { + acc[asset] = { pnl: new BN(0), pnlPrecent: new BN(0) }; + } + + acc[asset].pnl = acc[asset].pnl.plus(el.pnlAllTime); + acc[asset].pnlPrecent = acc[asset].pnlPrecent.plus(el.pnlInPersentAllTime); + + return acc; + }, + {} as Record, + ); return tokens.map((token) => { const balance = this.getWalletBalance(token.assetId); @@ -68,6 +90,8 @@ export class BalanceStore { const orderBalance = this.getOrderBalances(token.assetId); const totalBalance = balance.plus(contractBalance); return { + pnl: pnlsFormatted[token.assetId]?.pnl.toFixed(2), + pnlPrecent: pnlsFormatted[token.assetId]?.pnlPrecent.toFixed(2), assetId: token.assetId, asset: token, walletBalance: BN.formatUnits(balance, token.decimals).toString(), diff --git a/src/stores/DashboardStore.ts b/src/stores/DashboardStore.ts index 06bd8571..0ccdc0fe 100644 --- a/src/stores/DashboardStore.ts +++ b/src/stores/DashboardStore.ts @@ -1,6 +1,6 @@ import { makeAutoObservable, reaction } from "mobx"; -import { RowSnapshot, RowTradeEvent } from "@compolabs/spark-orderbook-ts-sdk"; +import { BalancePnlByUserResponse, RowSnapshot, RowTradeEvent } from "@compolabs/spark-orderbook-ts-sdk"; import { filters } from "@screens/Dashboard/const"; import { TradeEvent } from "@screens/Dashboard/InfoDataGraph"; @@ -19,6 +19,10 @@ interface IRecord { timestamp: number; } +type SummedBalancePnl = { + [key: string]: string; +}; + export interface FiltersProps { title: string; value: number; @@ -35,6 +39,7 @@ class DashboardStore { initialized = false; rowSnapshots: RowSnapshot[] = []; tradeEvents: RowTradeEvent[] = []; + balancePnlByUser: BalancePnlByUserResponse[] = []; activeUserStat = 0; activeTime = 0; activeFilter = filters[0]; @@ -71,6 +76,7 @@ class DashboardStore { this.activeTime = this.calculateTime(date, 24); await this.fetchUserScoreSnapshot(); await this.fetchTradeEvent(); + await this.fetchBalancePnl(); }; disconnect = () => { @@ -84,7 +90,37 @@ class DashboardStore { private syncDashboardData = async () => { await this.fetchUserScoreSnapshot(); await this.fetchTradeEvent(); + await this.fetchBalancePnl(); + }; + + getPnlValues = (pnlData: SummedBalancePnl) => { + switch (this.activeFilter.title) { + case "24h": + return { pnl: pnlData.pnl1, pnlInPercent: pnlData.pnlInPersent1 }; + case "7d": + return { pnl: pnlData.pnl7, pnlInPercent: pnlData.pnlInPersent7 }; + case "30d": + return { pnl: pnlData.pnl31, pnlInPercent: pnlData.pnlInPersent31 }; + case "All Time": + return { pnl: pnlData.pnlAllTime, pnlInPercent: pnlData.pnlInPersentAllTime }; + default: + return null; + } }; + get totalPnl() { + const summedValues: SummedBalancePnl | any = {}; + this.balancePnlByUser.forEach((entry) => { + Object.keys(entry).forEach((key) => { + if (key !== "market") { + const currentValue = summedValues[key] || "0"; + const newValue = entry[key as keyof BalancePnlByUserResponse] as string; + summedValues[key] = Number(currentValue) + Number(newValue); + } + }); + }); + + return this.getPnlValues(summedValues); + } getChartDataPortfolio = () => { const result: TradeEvent[] = []; @@ -179,6 +215,22 @@ class DashboardStore { const data = await bcNetwork.getTradeEvent(params); this.tradeEvents = data?.result?.rows ?? []; }; + + private fetchBalancePnl = async () => { + const bcNetwork = FuelNetwork.getInstance(); + const { accountStore } = this.rootStore; + if (!accountStore?.address) return; + const params = { + user: accountStore.address, + }; + const config = { + url: CONFIG.APP.sentioUrl, + apiKey: "TLjw41s3DYbWALbwmvwLDM9vbVEDrD9BP", + }; + bcNetwork.setSentioConfig(config); + const data = await bcNetwork.fetchBalancePnl(params); + this.balancePnlByUser = data?.result?.rows ?? []; + }; } export default DashboardStore;