Skip to content

Commit

Permalink
Compute the superminority value (#33)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
catalinred authored Sep 15, 2024
1 parent c1413c0 commit 7f61b2d
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 71 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ NEXT_PUBLIC_BUILDER_NEWS_SETTINGS_ID=
AIRTABLE_TOKEN=
# River
RIVER_KEY=
# RPC Endpoint
NEXT_PUBLIC_RPC_ENDPOINT=
8 changes: 6 additions & 2 deletions src/components/validators/ValidatorsCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -37,7 +37,11 @@ const ValidatorsCards = ({ visible }) => {
<div className="col-lg-6 mt-4 mt-lg-0">
<RoundedDepthCard className="px-5 py-8" shadow="bottom">
<div className="h1 text-black">
{availableStats ? 21 : <Loader />}
{superminority !== null ? (
<FormattedNumber value={superminority} />
) : (
<Loader />
)}
</div>
<p className="text-black m-0">
{t("validators.new.cards.nakamoto-text")}
Expand Down
146 changes: 77 additions & 69 deletions src/hooks/useTransactionStats.js
Original file line number Diff line number Diff line change
@@ -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<number>} 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,
Expand All @@ -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 () => {
Expand All @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -143,35 +148,37 @@ 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(
getCurrentValidatorNodes,
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,
Expand All @@ -186,5 +193,6 @@ export const useTransactionStats = ({
avgTps,
totalTransactionCount,
validators,
superminority,
};
};

0 comments on commit 7f61b2d

Please sign in to comment.