diff --git a/components/account/AccountBalanceHistoryCard.tsx b/components/account/AccountBalanceHistoryCard.tsx index 34037d2ad18dbb13d48ad6723fc6606ecc79f2a9..b1def201de1fff1c74cbccdf73a2918cce0c5bbd 100644 --- a/components/account/AccountBalanceHistoryCard.tsx +++ b/components/account/AccountBalanceHistoryCard.tsx @@ -2,14 +2,13 @@ import React, { useState, useMemo, MouseEvent } from "react"; import { ArrowDown, ArrowUp } from "lucide-react"; import { Card, CardContent, CardHeader } from "../ui/card"; import Explorer from "@/types/Explorer"; -import Link from "next/link"; -import useBalanceHistory from "@/hooks/api/balanceHistory/useBalanceHistory"; import BalanceHistoryChart from "../balanceHistory/BalanceHistoryChart"; import moment from "moment"; import { useRouter } from "next/router"; import { Loader2 } from "lucide-react"; import NoResult from "../NoResult"; import { Button } from "../ui/button"; +import useAggregatedBalanceHistory from "@/hooks/api/balanceHistory/useAggregatedHistory"; // Define the type for balance operation data type AccountBalanceHistoryCardProps = { @@ -30,42 +29,39 @@ const AccountBalanceHistoryCard: React.FC<AccountBalanceHistoryCardProps> = ({ const accountNameFromRoute = (router.query.accountName as string)?.slice(1); const { - accountBalanceHistory: hiveBalanceHistory, - isAccountBalanceHistoryLoading: hiveBalanceHistoryLoading, - isAccountBalanceHistoryError: hiveBalanceHistoryError, - } = useBalanceHistory( + aggregatedAccountBalanceHistory: hiveBalanceHistory, + isAggregatedAccountBalanceHistoryLoading: hiveBalanceHistoryLoading, + isAggregatedAccountBalanceHistoryError: hiveBalanceHistoryError, + } = useAggregatedBalanceHistory( accountNameFromRoute, "HIVE", - undefined, - undefined, - "desc", - defaultFromDate + "daily", + "asc", + defaultFromDate, ); const { - accountBalanceHistory: vestsBalanceHistory, - isAccountBalanceHistoryLoading: vestsBalanceHistoryLoading, - isAccountBalanceHistoryError: vestsBalanceHistoryError, - } = useBalanceHistory( + aggregatedAccountBalanceHistory: vestsBalanceHistory, + isAggregatedAccountBalanceHistoryLoading: vestsBalanceHistoryLoading, + isAggregatedAccountBalanceHistoryError: vestsBalanceHistoryError, + } = useAggregatedBalanceHistory( accountNameFromRoute, "VESTS", - undefined, - undefined, - "desc", - defaultFromDate + "daily", + "asc", + defaultFromDate, ); const { - accountBalanceHistory: hbdBalanceHistory, - isAccountBalanceHistoryLoading: hbdBalanceHistoryLoading, - isAccountBalanceHistoryError: hbdBalanceHistoryError, - } = useBalanceHistory( + aggregatedAccountBalanceHistory: hbdBalanceHistory, + isAggregatedAccountBalanceHistoryLoading: hbdBalanceHistoryLoading, + isAggregatedAccountBalanceHistoryError: hbdBalanceHistoryError, + } = useAggregatedBalanceHistory( accountNameFromRoute, "HBD", - undefined, - undefined, - "desc", - defaultFromDate + "daily", + "asc", + defaultFromDate, ); const handleBalancesVisibility = () => { @@ -77,90 +73,40 @@ const AccountBalanceHistoryCard: React.FC<AccountBalanceHistoryCardProps> = ({ vestsBalanceHistoryLoading || hbdBalanceHistoryLoading; const hasData = - hiveBalanceHistory?.operations_result?.length > 0 || - vestsBalanceHistory?.operations_result?.length > 0 || - hbdBalanceHistory?.operations_result?.length > 0; + hiveBalanceHistory || + vestsBalanceHistory || + hbdBalanceHistory; const hasError = hiveBalanceHistoryError || vestsBalanceHistoryError || hbdBalanceHistoryError; - const prepareData = ( - operations: { timestamp: string; balance: number }[] - ) => { - if (!operations || operations.length === 0) return []; - - const dailyData = new Map< - string, - { balance: number; balance_change: number } - >(); - - operations.forEach((operation: any) => { - let date; - if (typeof operation.timestamp === "string") { - date = new Date(operation.timestamp); - } else if (typeof operation.timestamp === "number") { - date = new Date(operation.timestamp * 1000); - } else { - return; - } - - if (!isNaN(date.getTime())) { - const dateString = date.toISOString().split("T")[0]; - - let balance_change = parseInt(operation.balance_change, 10); - let balance = parseInt(operation.balance, 10); + const prepareData = ( + operations: { timestamp: string; balance: number }[] + ) => { + if (!operations || operations.length === 0) return []; - if (dailyData.has(dateString)) { - dailyData.get(dateString)!.balance_change += balance_change; - dailyData.get(dateString)!.balance = balance; - } else { - dailyData.set(dateString, { balance, balance_change }); - } - } - }); - - const preparedData = Array.from(dailyData.entries()).map( - ([date, data]) => ({ - timestamp: date, - balance: data.balance, - balance_change: data.balance_change, - }) - ); - - return preparedData; - }; + const aggregatedData = new Map< + string, + { balance: number; balance_change: number } + >(); - // Reverse and prepare data with useMemo - const reversedHiveBalanceHistory = useMemo( - () => - prepareData( - Array.isArray(hiveBalanceHistory?.operations_result) - ? [...hiveBalanceHistory.operations_result].reverse() - : [] - ), - [hiveBalanceHistory?.operations_result] - ); + operations.forEach((operation: any) => { + let balance_change = operation.balance - operation.prev_balance; + let balance = parseInt(operation.balance, 10); - const reversedVestsBalanceHistory = useMemo( - () => - prepareData( - Array.isArray(vestsBalanceHistory?.operations_result) - ? [...vestsBalanceHistory.operations_result].reverse() - : [] - ), - [vestsBalanceHistory?.operations_result] - ); + aggregatedData.set(operation.date, { balance, balance_change }); + }); - const reversedHbdBalanceHistory = useMemo( - () => - prepareData( - Array.isArray(hbdBalanceHistory?.operations_result) - ? [...hbdBalanceHistory.operations_result].reverse() - : [] - ), - [hbdBalanceHistory?.operations_result] - ); + const preparedData = Array.from(aggregatedData.entries()).map( + ([date, data]) => ({ + timestamp: date, + balance: data.balance, + balance_change: data.balance_change, + }) + ); + return preparedData; + }; const handleButtonClick = (e: MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); // Prevents the event from bubbling up @@ -208,9 +154,9 @@ const AccountBalanceHistoryCard: React.FC<AccountBalanceHistoryCardProps> = ({ {!isLoading && !hasData && <NoResult />} {!isLoading && hasData && ( <BalanceHistoryChart - hiveBalanceHistoryData={reversedHiveBalanceHistory} - vestsBalanceHistoryData={reversedVestsBalanceHistory} - hbdBalanceHistoryData={reversedHbdBalanceHistory} + hiveBalanceHistoryData={prepareData(hiveBalanceHistory)} + vestsBalanceHistoryData={prepareData(vestsBalanceHistory)} + hbdBalanceHistoryData={prepareData(hbdBalanceHistory)} quickView={true} className="h-[320px]" /> diff --git a/components/account/AccountPagination.tsx b/components/account/AccountPagination.tsx index c9ce6bccda5676fcfec9d33c6c69ba698de00e40..915ffe5ecfa1edbd68804a78ce3de61f4ea4c566 100644 --- a/components/account/AccountPagination.tsx +++ b/components/account/AccountPagination.tsx @@ -35,12 +35,13 @@ const AccountPagination: React.FC<AccountPaginationProps> = ({ isMirrored={true} /> </div> - <div className="flex items-center mt-2 md:mt-0 md:ml-auto w-full md:w-auto justify-center md:justify-end mb-2"> + <div className="flex items-center mt-2 md:ml-auto w-full md:w-auto justify-center md:justify-end mb-2"> <JumpToPage currentPage={page} onPageChange={setPage} totalCount={operationsCount ?? 1} pageSize={config.standardPaginationSize} + /> </div> </div> diff --git a/components/balanceHistory/BalanceHistoryChart.tsx b/components/balanceHistory/BalanceHistoryChart.tsx index 5c31349ce49c63197b48c3c14f8262f4647d88a5..cb41aecef84b2bd75f93eef8dbb34058d63123e2 100644 --- a/components/balanceHistory/BalanceHistoryChart.tsx +++ b/components/balanceHistory/BalanceHistoryChart.tsx @@ -13,7 +13,8 @@ import { import { formatNumber } from "@/lib/utils"; import { cn } from "@/lib/utils"; import moment from "moment"; -import { ArrowDown, ArrowUp } from "lucide-react"; +import { ArrowDown, ArrowUp, Minus } from "lucide-react"; + interface BalanceHistoryChartProps { hiveBalanceHistoryData?: { @@ -117,6 +118,7 @@ const BalanceHistoryChart: React.FC<BalanceHistoryChartProps> = ({ const balanceChange = payload[0]?.payload.balance_change ?? 0; const isPositiveChange = balanceChange > 0; + const isZeroChange = balanceChange == 0; return ( <div className="bg-theme dark:bg-theme p-2 rounded border border-explorer-light-gray"> @@ -127,7 +129,7 @@ const BalanceHistoryChart: React.FC<BalanceHistoryChartProps> = ({ {isPositiveChange ? ( <ArrowUp className="bg-green-400 p-[1.2px]" size={16}/> - ) : ( + ) : isZeroChange ? <Minus className="bg-black p-[1.2px] mr-1" color={"white"} size={16}/> : ( <ArrowDown className="bg-red-400 p-[1.2px]" size={16}/> )} {` ${formatNumber( diff --git a/hooks/api/balanceHistory/useAggregatedHistory.ts b/hooks/api/balanceHistory/useAggregatedHistory.ts new file mode 100644 index 0000000000000000000000000000000000000000..39c92097af8aae9c82782b74eb121de0fbd61494 --- /dev/null +++ b/hooks/api/balanceHistory/useAggregatedHistory.ts @@ -0,0 +1,54 @@ +import { useQuery } from "@tanstack/react-query"; +import moment from "moment"; + +import fetchingService from "@/services/FetchingService"; + +const useAggregatedBalanceHistory = ( + accountName: string, + coinType: string, + granularity: "daily"|"monthly"|"yearly", + direction: "asc" | "desc", + fromDate?: Date | number | undefined, + toDate?: Date | number | undefined +) => { + const fetchBalanceHist = async () => { + if (fromDate && toDate && moment(fromDate).isAfter(moment(toDate))) { + return []; + } + + return await fetchingService.geAccountAggregatedtBalanceHistory( + accountName, + coinType, + granularity, + direction, + fromDate ? fromDate : undefined, + toDate ? toDate : undefined + ); + }; + + const { + data: aggregatedAccountBalanceHistory, + isLoading: isAggregatedAccountBalanceHistoryLoading, + isError: isAggregatedAccountBalanceHistoryError, + }: any = useQuery({ + queryKey: [ + "get_balance_aggregation", + accountName, + coinType, + direction, + fromDate, + toDate, + ], + queryFn: fetchBalanceHist, + enabled: !!accountName, + refetchOnWindowFocus: false, + }); + + return { + aggregatedAccountBalanceHistory, + isAggregatedAccountBalanceHistoryLoading, + isAggregatedAccountBalanceHistoryError, + }; +}; + +export default useAggregatedBalanceHistory; diff --git a/pages/balanceHistory/[accountName].tsx b/pages/balanceHistory/[accountName].tsx index b7a28088011a9cd6eb0eb03daa9f867052a73f39..58ae8312c9450615d72d785ec304019cc49d0991 100644 --- a/pages/balanceHistory/[accountName].tsx +++ b/pages/balanceHistory/[accountName].tsx @@ -23,6 +23,7 @@ import BalanceHistoryChart from "@/components/balanceHistory/BalanceHistoryChart import ErrorPage from "../ErrorPage"; import NoResult from "@/components/NoResult"; import ScrollTopButton from "@/components/ScrollTopButton"; +import useAggregatedBalanceHistory from "@/hooks/api/balanceHistory/useAggregatedHistory"; // Memoizing the BalanceHistoryChart component to avoid unnecessary re-renders const MemoizedBalanceHistoryChart = React.memo(BalanceHistoryChart); @@ -34,42 +35,25 @@ interface Operation { const prepareData = (operations: Operation[]) => { if (!operations || operations.length === 0) return []; - const dailyData = new Map< + const aggregatedData = new Map< string, { balance: number; balance_change: number } >(); operations.forEach((operation: any) => { - let date; - if (typeof operation.timestamp === "string") { - date = new Date(operation.timestamp); - } else if (typeof operation.timestamp === "number") { - date = new Date(operation.timestamp * 1000); - } else { - return; - } - - if (!isNaN(date.getTime())) { - const dateString = date.toISOString().split("T")[0]; - - let balance_change = parseInt(operation.balance_change, 10); - let balance = parseInt(operation.balance, 10); - - if (dailyData.has(dateString)) { - dailyData.get(dateString)!.balance_change += balance_change; - dailyData.get(dateString)!.balance = balance; - } else { - dailyData.set(dateString, { balance, balance_change }); - } - } - }); + let balance_change = operation.balance - operation.prev_balance; + let balance = parseInt(operation.balance, 10); - const preparedData = Array.from(dailyData.entries()).map(([date, data]) => ({ - timestamp: date, - balance: data.balance, - balance_change: data.balance_change, - })); + aggregatedData.set(operation.date, { balance, balance_change }); + }); + const preparedData = Array.from(aggregatedData.entries()).map( + ([date, data]) => ({ + timestamp: date, + balance: data.balance, + balance_change: data.balance_change, + }) + ); return preparedData; }; @@ -164,26 +148,22 @@ export default function BalanceHistory() { effectiveToBlock ); - // Update chartData to return loading, error, and data const { - accountBalanceHistory: chartData, - isAccountBalanceHistoryLoading: isChartDataLoading, - isAccountBalanceHistoryError: isChartDataError, - } = useBalanceHistory( + aggregatedAccountBalanceHistory: chartData, + isAggregatedAccountBalanceHistoryLoading: isChartDataLoading, + isAggregatedAccountBalanceHistoryError: isChartDataError, + } = useAggregatedBalanceHistory( accountNameFromRoute, paramsState.coinType, - undefined, - 5000, // Default size for chart data - "desc", + "daily", + "asc", effectiveFromBlock, effectiveToBlock ); // Use useMemo to memoize the prepared data so it only recalculates when chartData changes const preparedData = useMemo(() => { - return chartData - ? prepareData(chartData.operations_result?.slice().reverse()) - : []; + return chartData ? prepareData(chartData) : []; }, [chartData]); // This will only recompute when chartData changes let message = ""; @@ -257,67 +237,59 @@ export default function BalanceHistory() { setParams={setParams} /> - {!isAccountBalanceHistoryLoading && - !accountBalanceHistory?.total_operations ? ( - <div> - <NoResult /> - </div> - ) : isAccountBalanceHistoryLoading ? ( + <Card data-testid="account-details"> + {message && ( + <div className="p-4 bg-gray-100 dark:bg-gray-700 rounded-lg mb-4 text-center text-sm text-gray-500"> + {message} + <br /> + </div> + )} + + {isChartDataLoading ? ( + <div className="flex justify-center text-center items-center"> + <Loader2 className="animate-spin mt-1 h-16 w-10 ml-10 dark:text-white" /> + </div> + ) : isChartDataError ? ( + <div className="text-center">Error loading chart data</div> + ) : preparedData.length > 0 ? ( + <MemoizedBalanceHistoryChart + hiveBalanceHistoryData={ + !paramsState.coinType || paramsState.coinType === "HIVE" + ? preparedData + : undefined + } + vestsBalanceHistoryData={ + paramsState.coinType === "VESTS" ? preparedData : undefined + } + hbdBalanceHistoryData={ + paramsState.coinType === "HBD" ? preparedData : undefined + } + quickView={false} + className="h-[450px] mb-10 mr-0 pr-1 pb-6" + /> + ) : ( + <NoResult title="No chart data available" /> + )} + </Card> + + {isAccountBalanceHistoryLoading ? ( <div className="flex justify-center text-center items-center"> <Loader2 className="animate-spin mt-1 h-12 w-12 ml-3" /> </div> + ) : accountBalanceHistory?.total_operations ? ( + <BalanceHistoryTable + operations={convertBalanceHistoryResultsToTableOperations( + accountBalanceHistory + )} + total_operations={accountBalanceHistory.total_operations} + total_pages={accountBalanceHistory.total_pages} + current_page={paramsState.page} + account_name={accountNameFromRoute} + /> ) : ( - <> - <Card data-testid="account-details"> - {message && ( - <div className="p-4 bg-gray-100 dark:bg-gray-700 rounded-lg mb-4 text-center text-sm text-gray-500"> - {message} - <br /> - Results are limited to 5000 records and grouped by day. - <br /> - </div> - )} - - {isChartDataLoading ? ( - <div className="flex justify-center text-center items-center"> - <Loader2 className="animate-spin mt-1 h-16 w-10 ml-10 dark:text-white" /> - </div> - ) : !isChartDataError ? ( - <MemoizedBalanceHistoryChart - hiveBalanceHistoryData={ - !paramsState.coinType || paramsState.coinType === "HIVE" - ? preparedData - : undefined - } - vestsBalanceHistoryData={ - paramsState.coinType === "VESTS" - ? preparedData - : undefined - } - hbdBalanceHistoryData={ - paramsState.coinType === "HBD" - ? preparedData - : undefined - } - quickView={false} - className="h-[450px] mb-10 mr-0 pr-1 pb-6" - /> - ) : ( - <div>Error loading chart data</div> - )} - </Card> - - <BalanceHistoryTable - operations={convertBalanceHistoryResultsToTableOperations( - accountBalanceHistory - )} - total_operations={accountBalanceHistory.total_operations} - total_pages={accountBalanceHistory.total_pages} - current_page={paramsState.page} - account_name={accountNameFromRoute} - /> - </> + <NoResult title="No transaction data available" /> )} + <div className="fixed bottom-[10px] right-0 flex flex-col items-end justify-end px-3 md:px-12"> <ScrollTopButton /> </div> diff --git a/services/FetchingService.ts b/services/FetchingService.ts index 74756016299fa4fdd4e068c00aa547b328de6c8c..5333f481150943eaa5c52e7cdcafdb9bac67a9e7 100644 --- a/services/FetchingService.ts +++ b/services/FetchingService.ts @@ -445,6 +445,26 @@ class FetchingService { "to-block": toBlock, }); } + + + async geAccountAggregatedtBalanceHistory( + accountName: string, + coinType: string, + granularity : "daily"|"monthly"|"yearly", + direction: "asc" | "desc", + fromBlock?: Date | number | undefined, + toBlock?: Date | number | undefined + ): Promise<Hive.AccountAggregatedBalanceHistoryResponse> { + return await this.extendedHiveChain!.restApi["balance-api"].aggregatedHistory({ + accountName, + "coin-type": coinType, + "granularity":granularity, + direction: direction, + "from-block": fromBlock, + "to-block": toBlock, + }); + } + } const fetchingService = new FetchingService(); diff --git a/types/Hive.ts b/types/Hive.ts index 7253bf9afc4181d6ee70a137b8dabbc7c0d676b9..05eeda8d68f2208c13b5166b31a418327a7ed007 100644 --- a/types/Hive.ts +++ b/types/Hive.ts @@ -762,7 +762,7 @@ namespace Hive { balance_change!: number; timestamp!: string; } - export class GetAccountBalanceHistoryParams { + export class AccountBalanceHistoryParams { "accountName": string; "coin-type": string; direction?: Hive.Direction; @@ -776,6 +776,21 @@ namespace Hive { total_pages!: number; operations_result!: AccountBalanceHistory[]; } + export class AccountAggregatedBalanceHistoryParams { + "accountName": string; + "coin-type": string; + "granularity": string; + direction?: Hive.Direction; + "from-block"?: Date | number | undefined; + "to-block"?: Date | number | undefined; + } + export class AccountAggregatedBalanceHistoryResponse { + balance!: number; + prev_balance!: number; + min_balance!: number; + max_balance!: number; + date!:Date; + } export class Delegation { delegator!: string; diff --git a/types/Rest.ts b/types/Rest.ts index f9f8d8298e9fb2081c45ba64ce47ccb011db0154..1663d51ca30d2c0adf5471b100c1cbffee80291c 100644 --- a/types/Rest.ts +++ b/types/Rest.ts @@ -142,7 +142,7 @@ export const extendedRest = { }, "balance-api": { balanceHistory: { - params: Hive.GetAccountBalanceHistoryParams, + params: Hive.AccountBalanceHistoryParams, result: Hive.AccountBalanceHistoryResponse, urlPath: "accounts/{accountName}/balance-history", }, @@ -151,5 +151,10 @@ export const extendedRest = { result: Hive.VestingDelegationsResponse, urlPath: "accounts/{accountName}/delegations", }, + aggregatedHistory: { + params: Hive.AccountAggregatedBalanceHistoryParams, + result: Hive.AccountAggregatedBalanceHistoryResponse, + urlPath: "accounts/{accountName}/aggregated-history", + }, }, };