From 7f61b2d6a6fcb44aa8a1df89e38f92f14d015867 Mon Sep 17 00:00:00 2001 From: Catalin Rosu Date: Sun, 15 Sep 2024 12:53:36 +0300 Subject: [PATCH] Compute the superminority value (#33) * Add initial logic * Use makeRPCCall call * Move endpoint to env file * Avoid multiple calls per second to fetch the superminority. Update comments overall too * Better comments * Make sure the sort works correctly for any size of BigInt values. --- .env.example | 2 + src/components/validators/ValidatorsCards.js | 8 +- src/hooks/useTransactionStats.js | 146 ++++++++++--------- 3 files changed, 85 insertions(+), 71 deletions(-) diff --git a/.env.example b/.env.example index b3c17e007..368e84341 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,5 @@ NEXT_PUBLIC_BUILDER_NEWS_SETTINGS_ID= AIRTABLE_TOKEN= # River RIVER_KEY= +# RPC Endpoint +NEXT_PUBLIC_RPC_ENDPOINT= \ No newline at end of file diff --git a/src/components/validators/ValidatorsCards.js b/src/components/validators/ValidatorsCards.js index dae469919..4a703e0fc 100644 --- a/src/components/validators/ValidatorsCards.js +++ b/src/components/validators/ValidatorsCards.js @@ -9,7 +9,7 @@ import { } from "../../hooks/useTransactionStats"; const ValidatorsCards = ({ visible }) => { - const { validators, availableStats } = useTransactionStats({ + const { validators, availableStats, superminority } = useTransactionStats({ visible, performanceUpdateSeconds: PERF_UPDATE_SEC, sampleHistoryHours: SAMPLE_HISTORY_HOURS, @@ -37,7 +37,11 @@ const ValidatorsCards = ({ visible }) => {
- {availableStats ? 21 : } + {superminority !== null ? ( + + ) : ( + + )}

{t("validators.new.cards.nakamoto-text")} diff --git a/src/hooks/useTransactionStats.js b/src/hooks/useTransactionStats.js index 2497b4791..f706be18b 100644 --- a/src/hooks/useTransactionStats.js +++ b/src/hooks/useTransactionStats.js @@ -1,68 +1,84 @@ 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; - -/** - * Current transaction costs. - * - * @type {string} - */ -export const TRANSACTION_COST = "$0.00025"; +// 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 -/** - * 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} - */ -export const INTERNAL_MAINNET_ENDPOINT = ""; +// RPC Node URL +const rpcNodeURL = process.env.NEXT_PUBLIC_RPC_ENDPOINT; +if (!rpcNodeURL) { + 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: {} }; -const isDevelopment = process?.env?.NODE_ENV === "development" || false; +/** + * 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 ascending order + const sortedValidators = voteAccounts.result.current.sort((a, b) => { + const diff = BigInt(b.activatedStake) - BigInt(a.activatedStake); + if (diff > 0) return 1; + else if (diff < 0) return -1; + else return 0; + }); + + // Calculate total stake from sorted validators + const totalStake = sortedValidators.reduce( + (sum, validator) => sum + BigInt(validator.activatedStake), + BigInt(0), + ); + + // Calculate one-third of the total stake + const oneThirdStake = totalStake / BigInt(3); + let cumulativeStake = BigInt(0); + let superminorityCount = 0; + + // Count superminority + for (const validator of sortedValidators) { + if (cumulativeStake > oneThirdStake) break; + cumulativeStake += BigInt(validator.activatedStake); + superminorityCount++; + } + + return superminorityCount; + } catch (error) { + console.error("Error fetching superminority:", error); + return null; + } +}; /** * 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 = false, + visible, performanceUpdateSeconds, sampleHistoryHours, getLiveTransactionCount = true, @@ -72,21 +88,11 @@ export const useTransactionStats = ({ const [avgTps, setAvgTps] = useState(0); const [totalTransactionCount, setTotalTransactionCount] = useState(0); const [validators, setValidators] = useState(0); + const [superminority, setSuperminority] = useState(null); const getRPCData = useCallback( async (getValidatorNodes, getTransactionCount, abortSignal) => { - if (!visible) { - if (isDevelopment) { - console.debug("not visible, or homepage exit"); - } - return; - } try { - // Do *not* batch these queries. - // Batching has been disabled on the homepage's API endpoint. - const rpcNodeURL = isDevelopment - ? INTERNAL_MAINNET_ENDPOINT - : MAINNET_ENDPOINT; if (rpcNodeURL) { await Promise.all([ (async () => { @@ -96,7 +102,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) { @@ -108,7 +114,6 @@ export const useTransactionStats = ({ }, [], ); - // Use latest sample as average transactions per second. const avgTps = Math.round(short[0]); setAvgTps(avgTps); setAvailableStats(true); @@ -143,17 +148,14 @@ export const useTransactionStats = ({ if (error.name === "AbortError" || error.name === "TypeError") { return; } - - isDevelopment && console.error("error: ", error); + console.error("Error fetching RPC data:", error); } }, - [sampleHistoryHours, visible], + [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( @@ -161,17 +163,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, @@ -186,5 +193,6 @@ export const useTransactionStats = ({ avgTps, totalTransactionCount, validators, + superminority, }; };