From ee0c8a052be6007ace5620a5ca21f3e1fd9708e0 Mon Sep 17 00:00:00 2001 From: Martin Marinov Date: Wed, 1 Jul 2026 12:10:27 +0300 Subject: [PATCH] feat(dao): show voting-power distribution - votable sum, accounts, staked-excluded The governance header now surfaces the voting-power breakdown for LightChain: the votable voting-power sum (getTotalVotingPower), the staked LCAI excluded from the quorum base (getPastTotalSupply - getTotalVotingPower), and the ecosystem account count from the explorer index (there is no on-chain holder enumerator). Rendered as a 'Voting power: N LCAI votable across ~K accounts - X LCAI staked (excluded from quorum)' line under the treasury stats. --- app/api/dao-overview/route.ts | 29 ++++++++++++++++++++++++++++- app/build/dao/treasury-bar.tsx | 24 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/app/api/dao-overview/route.ts b/app/api/dao-overview/route.ts index 0510b0a..b664898 100644 --- a/app/api/dao-overview/route.ts +++ b/app/api/dao-overview/route.ts @@ -46,7 +46,22 @@ const NATIVE_QUORUM_ABI = parseAbi([ ]); const NATIVE_VOTES_ABI = parseAbi([ "function getTotalVotingPower(uint256 timepoint) view returns (uint256)", + "function getPastTotalSupply(uint256 timepoint) view returns (uint256)", ]); + +// Distinct-account count. There is no on-chain enumerator for holders (the +// precompile gives votes==balance per account but no list), so the ecosystem +// account count comes from the explorer's index. Best-effort; null on failure. +async function fetchHolderCount(): Promise { + try { + const res = await fetch("https://mainnet.lightscan.app/api/v2/stats", { signal: AbortSignal.timeout(6000) }); + if (!res.ok) return null; + const j = (await res.json()) as { total_addresses?: string }; + return j.total_addresses ? Number(j.total_addresses) : null; + } catch { + return null; + } +} const SCHEDULE_ABI = parseAbi([ "function votingDelay() view returns (uint256)", "function votingPeriod() view returns (uint256)", @@ -126,12 +141,21 @@ async function readLightchainOverview(pub: Pub) { // Evaluate the real quorum + votable base at the latest finalized timepoint. const clock = BigInt(clockRaw as bigint | number); const ref = clock > 0n ? clock - 1n : 0n; - const [quorumWei, votableWei] = await Promise.all([ + const [quorumWei, votableWei, rawSupplyWei, holderCount] = await Promise.all([ pub.readContract({ address: a.governor, abi: NATIVE_QUORUM_ABI, functionName: "quorum", args: [ref] }).catch(() => null), a.ballots ? pub.readContract({ address: a.ballots, abi: NATIVE_VOTES_ABI, functionName: "getTotalVotingPower", args: [ref] }).catch(() => null) : Promise.resolve(null), + a.ballots + ? pub.readContract({ address: a.ballots, abi: NATIVE_VOTES_ABI, functionName: "getPastTotalSupply", args: [ref] }).catch(() => null) + : Promise.resolve(null), + fetchHolderCount(), ]); + // Staked LCAI excluded from the quorum base = raw votes supply - votable supply. + const stakeExcludedWei = + rawSupplyWei != null && votableWei != null && (rawSupplyWei as bigint) > (votableWei as bigint) + ? (rawSupplyWei as bigint) - (votableWei as bigint) + : null; return { treasuryWei: treasuryWei.toString(), feePoolWei: feePoolWei.toString(), @@ -140,7 +164,10 @@ async function readLightchainOverview(pub: Pub) { // The actual quorum threshold + the staked-excluded base it applies to. quorumWei: quorumWei != null ? (quorumWei as bigint).toString() : null, votableSupplyWei: votableWei != null ? (votableWei as bigint).toString() : null, + rawSupplyWei: rawSupplyWei != null ? (rawSupplyWei as bigint).toString() : null, + stakeExcludedWei: stakeExcludedWei != null ? stakeExcludedWei.toString() : null, stakeExcludedFromQuorum: true as boolean, + holderCount: holderCount, }; } diff --git a/app/build/dao/treasury-bar.tsx b/app/build/dao/treasury-bar.tsx index 12c0b81..6760f42 100644 --- a/app/build/dao/treasury-bar.tsx +++ b/app/build/dao/treasury-bar.tsx @@ -26,12 +26,35 @@ interface Overview { quorumWei?: string | null; votableSupplyWei?: string | null; stakeExcludedFromQuorum?: boolean; + // Voting-power distribution: the votable sum, how much staked LCAI is excluded + // from it, and the ecosystem account count (from the explorer index). + rawSupplyWei?: string | null; + stakeExcludedWei?: string | null; + holderCount?: number | null; schedule?: Schedule; error?: string; } const VOTE_SYMBOL: Record = { ethereum: "LCAIB", lightchain: "LCAI" }; +/** LightChain voting-power summary: votable sum, accounts, staked-excluded. */ +function VotingPowerLine({ chain, data }: { chain: DaoChain; data: Overview }) { + if (!data.votableSupplyWei) return null; + const sym = VOTE_SYMBOL[chain]; + const votable = formatLcaiWei(BigInt(data.votableSupplyWei), 0); + const accounts = data.holderCount != null ? `~${data.holderCount.toLocaleString()} accounts` : null; + const excluded = data.stakeExcludedWei && BigInt(data.stakeExcludedWei) > 0n + ? `${formatLcaiWei(BigInt(data.stakeExcludedWei), 0)} ${sym} staked (excluded from quorum)` + : null; + return ( +

+ Voting power: {votable} {sym} votable + {accounts ? <> across {accounts} : null} + {excluded ? <> ยท {excluded} : null} +

+ ); +} + function ScheduleLine({ chain, schedule }: { chain: DaoChain; schedule: Schedule }) { const vote = humanizeDuration(schedule.votingPeriodSeconds); const delay = humanizeDuration(schedule.votingDelaySeconds); @@ -123,6 +146,7 @@ export function TreasuryBar({ chain }: { chain: DaoChain }) { } label="Governor" value={shortAddr(data.governor)} href={`${data.explorer}/address/${data.governor}`} /> + {data.schedule && } );