From ff58c229612a8f51342b328a5feb97185a25cdc6 Mon Sep 17 00:00:00 2001 From: Catalin Rosu Date: Mon, 9 Sep 2024 18:26:02 +0300 Subject: [PATCH] Avoid multiple calls per second to fetch the superminority. Update comments overall too --- src/hooks/useTransactionStats.js | 161 +++++++++++++------------------ 1 file changed, 66 insertions(+), 95 deletions(-) diff --git a/src/hooks/useTransactionStats.js b/src/hooks/useTransactionStats.js index 9c7587c1..61b71f51 100644 --- a/src/hooks/useTransactionStats.js +++ b/src/hooks/useTransactionStats.js @@ -1,67 +1,75 @@ import { useCallback, useEffect, useState } from "react"; import { makeRPCCall } from "../utils/rpcUtils"; -/** - * After how many time the performance updates should be queried. - * - * @type {number} - */ -export const PERF_UPDATE_SEC = 5; - -/** - * How long back should be queried. - * @type {number} - */ -export const SAMPLE_HISTORY_HOURS = 6; +// Constants for performance and sample history +export const PERF_UPDATE_SEC = 5; // Performance update interval in seconds +export const SAMPLE_HISTORY_HOURS = 6; // Hours to query for historical data -/** - * Current transaction costs. - * - * @type {string} - */ -export const TRANSACTION_COST = "$0.00025"; - -/** - * The JSON-RPC Mainnet-Endpoint to query stats from in Preview / Production. - * - * @type {string} - */ -export const MAINNET_ENDPOINT = "https://explorer-api.mainnet-beta.solana.com"; -/** - * The JSON-RPC Endpoint to query stats from in Development. - * - * @type {string} - */ - -const rpcNodeURL = process.env.NEXT_PUBLIC_RPC_ENDPOINT || ""; +// RPC Node URL +const rpcNodeURL = process.env.NEXT_PUBLIC_RPC_ENDPOINT; if (!rpcNodeURL) { - console.warn("Warning: RPC_ENDPOINT is not set."); + console.warn("Warning: NEXT_PUBLIC_RPC_ENDPOINT is not set."); } -// Test for AbortController support. +// Check for AbortController support const isAbortControllerSupported = typeof window !== "undefined" && window.hasOwnProperty("AbortController"); const noOp = () => null; -/** - * Initializes an AbortController if supported. - * - * @returns {AbortController|{abort: (function(): null), signal: {}}} - */ +// Initializes an AbortController if supported const initAbortController = () => isAbortControllerSupported ? new AbortController() : { abort: noOp, signal: {} }; +/** + * Fetches the superminority count. + * + * @returns {Promise} The count of superminority validators. + */ +export const fetchSuperminority = async () => { + try { + const voteAccounts = await makeRPCCall({ + method: "getVoteAccounts", + rpcNodeURL, + }); + + // Sort validators by stake in descending order + const sortedValidators = voteAccounts.result.current.sort((a, b) => + Number(BigInt(b.activatedStake) - BigInt(a.activatedStake)), + ); + + const totalStake = sortedValidators.reduce( + (sum, validator) => sum + BigInt(validator.activatedStake), + BigInt(0), + ); + const oneThirdStake = totalStake / BigInt(3); + + let cumulativeStake = BigInt(0); + let superminorityCount = 0; + + for (const validator of sortedValidators) { + if (cumulativeStake > oneThirdStake) break; + cumulativeStake += BigInt(validator.activatedStake); + superminorityCount++; + } + + return superminorityCount; // Return the count of superminority validators + } catch (error) { + console.error("Error fetching superminority:", error); + return null; // Return null in case of error + } +}; + /** * Hook to return current transaction statistics from the JSON-RPC endpoint. * - * @param {boolean} visible Only fire new queries when visible. - * @param {number} performanceUpdateSeconds Delay before next query. - * @param {number} sampleHistoryHours How many hours (60min.) the query should go back. - * @param {boolean} getLiveTransactionCount - * @param {boolean} getCurrentValidatorNodes - * @returns {{availableStats: boolean, avgTps: number, validators: number, totalTransactionCount: number}} + * @param {boolean} visible Only fire new queries when visible. + * @param {number} performanceUpdateSeconds Delay before next query. + * @param {number} sampleHistoryHours How many hours (60min.) the query should go back. + * @param {boolean} getLiveTransactionCount + * @param {boolean} getCurrentValidatorNodes + * @returns {{availableStats: boolean, avgTps: number, validators: number, totalTransactionCount: number, superminority: number}} */ export const useTransactionStats = ({ visible, @@ -79,9 +87,6 @@ export const useTransactionStats = ({ const getRPCData = useCallback( async (getValidatorNodes, getTransactionCount, abortSignal) => { try { - // Do *not* batch these queries. - // Batching has been disabled on the homepage's API endpoint. - if (rpcNodeURL) { await Promise.all([ (async () => { @@ -91,7 +96,7 @@ export const useTransactionStats = ({ params: [60 * sampleHistoryHours], rpcNodeURL, }); - // Calculate transactions per second for each sample. + // Calculate average transactions per second const short = recentPerformanceSamples.result.reduce( (shortResults, sample) => { if (sample.numTransactions !== 0) { @@ -103,7 +108,6 @@ export const useTransactionStats = ({ }, [], ); - // Use latest sample as average transactions per second. const avgTps = Math.round(short[0]); setAvgTps(avgTps); setAvailableStats(true); @@ -138,15 +142,14 @@ export const useTransactionStats = ({ if (error.name === "AbortError" || error.name === "TypeError") { return; } + console.error("Error fetching RPC data:", error); } }, [sampleHistoryHours], ); - // Load dynamic statistics only when the component is visible. + // Load statistics only when the component is visible useEffect(() => { - // Get average transactions per second & validator node count - // (abort controlled when unmounted, so no state update will happen). const abortController = initAbortController(); if (visible) { getRPCData( @@ -154,17 +157,22 @@ export const useTransactionStats = ({ getLiveTransactionCount, abortController.signal, ); + + // Fetch superminority count + const fetchSuperminorityData = async () => { + const count = await fetchSuperminority(); + setSuperminority(count); + }; + fetchSuperminorityData(); } - // Set an interval to call JSON-RPC periodically. + const interval = setInterval(() => { getRPCData(false, getLiveTransactionCount, abortController.signal); }, performanceUpdateSeconds * 1000); + return () => { abortController.abort(); - // Clear interval to prevent multiple calls to setInterval. - if (interval) { - clearInterval(interval); - } + clearInterval(interval); }; }, [ visible, @@ -174,43 +182,6 @@ export const useTransactionStats = ({ getRPCData, ]); - const fetchSuperminority = async () => { - try { - const voteAccounts = await makeRPCCall({ - method: "getVoteAccounts", - rpcNodeURL, - }); - - // Sort validators by stake in descending order - const sortedValidators = voteAccounts.result.current.sort((a, b) => - Number(BigInt(b.activatedStake) - BigInt(a.activatedStake)), - ); - - const totalStake = sortedValidators.reduce( - (sum, validator) => sum + BigInt(validator.activatedStake), - BigInt(0), - ); - const oneThirdStake = totalStake / BigInt(3); - - let cumulativeStake = BigInt(0); - let superminorityCount = 0; - - for (const validator of sortedValidators) { - if (cumulativeStake > oneThirdStake) break; - cumulativeStake += BigInt(validator.activatedStake); - superminorityCount++; - } - - setSuperminority(superminorityCount); - } catch (error) { - console.error("Error fetching superminority:", error); - } - }; - - if (visible) { - fetchSuperminority(); - } - return { availableStats, avgTps,