Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compute the superminority value #33

Merged
merged 8 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 () => {
catalinred marked this conversation as resolved.
Show resolved Hide resolved
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.
catalinred marked this conversation as resolved.
Show resolved Hide resolved
* @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,
tigarcia marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -186,5 +193,6 @@ export const useTransactionStats = ({
avgTps,
totalTransactionCount,
validators,
superminority,
};
};
Loading