diff --git a/src/locales/en.json b/src/locales/en.json index fe2b39b..b446e9d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -139,9 +139,9 @@ "tag": "Proof", "title": "Verifiable indicators, in real time.", "assets": "Total value of partner assets in the ecosystem", - "members": "Ecosystem members", + "members": "REG holders", "regMarketcap": "REG marketcap", - "votes": "Number of votes on Tally", + "votes": "Number of proposals voted by the DAO", "comingSoon": "Coming soon", "inProgress": "In progress" }, diff --git a/src/locales/fr.json b/src/locales/fr.json index 3cc1309..b9c21a0 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -139,9 +139,9 @@ "tag": "Preuves", "title": "Des indicateurs vérifiables, en temps réel.", "assets": "Valeur totale des actifs partenaires dans l'écosystème", - "members": "Membres de l'écosystème", + "members": "Détenteurs de REG", "regMarketcap": "Marketcap REG", - "votes": "Nombre de votes sur Tally", + "votes": "Nombre de propositions votées par la DAO", "comingSoon": "À venir", "inProgress": "En cours" }, diff --git a/src/views/Home.vue b/src/views/Home.vue index b1be6e7..1b09add 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -6,7 +6,14 @@ import LatestArticles from '../components/blog/LatestArticles.vue' const { t } = useI18n() const COINGECKO_REG_ID = 'realtoken-ecosystem-governance' +const GOVERNOR_ADDRESS = '0x4a5327347f077e72d2aab19f68ba8a7f12ec5d63' +const GNOSIS_RPC_URL = 'https://rpc.gnosischain.com' +const PROPOSAL_CREATED_TOPIC = '0x7d84a6263ae0d98d3329bd7b46bb4e8d6f98cd35a7adb45c274c8b7fd5ebd5e0' +const PROPOSAL_CANCELED_TOPIC = '0x789cf55be980739dad1d0699b93b58e806b51c9d96619bfa8fe0a28abaa7b30c' +const BLOCKSCOUT_API_BASE = 'https://gnosis.blockscout.com/api/v2' const regMarketCapUsd = ref(null) +const tallyVotesCount = ref(null) +const regHoldersCount = ref(null) function formatCompact(value) { if (value == null || typeof value !== 'number') return null @@ -16,16 +23,120 @@ function formatCompact(value) { return value.toLocaleString() } +/** + * Fetch DAO proposal count excluding canceled proposals. + */ +async function fetchGovernorVoteCount() { + const extractProposalId = (log) => { + const data = log?.data + if (typeof data !== 'string' || !data.startsWith('0x') || data.length < 66) return null + return data.slice(0, 66).toLowerCase() + } + + const baseFilter = { + address: GOVERNOR_ADDRESS, + fromBlock: '0x0', + toBlock: 'latest' + } + + const payload = { + jsonrpc: '2.0', + id: 1, + method: 'eth_getLogs' + } + + const [createdRes, canceledRes] = await Promise.all([ + fetch(GNOSIS_RPC_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ ...payload, params: [{ ...baseFilter, topics: [PROPOSAL_CREATED_TOPIC] }] }) + }), + fetch(GNOSIS_RPC_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ ...payload, params: [{ ...baseFilter, topics: [PROPOSAL_CANCELED_TOPIC] }] }) + }) + ]) + + if (!createdRes.ok) throw new Error(`Governor created logs HTTP ${createdRes.status}`) + if (!canceledRes.ok) throw new Error(`Governor canceled logs HTTP ${canceledRes.status}`) + + const createdData = await createdRes.json() + const canceledData = await canceledRes.json() + if (createdData?.error) throw new Error(createdData.error?.message || 'Governor created logs RPC error') + if (canceledData?.error) throw new Error(canceledData.error?.message || 'Governor canceled logs RPC error') + + const createdLogs = Array.isArray(createdData?.result) ? createdData.result : [] + const canceledLogs = Array.isArray(canceledData?.result) ? canceledData.result : [] + + const canceledProposalIds = new Set( + canceledLogs.map(extractProposalId).filter(Boolean) + ) + + const activeProposalCount = createdLogs + .map(extractProposalId) + .filter((proposalId) => proposalId && !canceledProposalIds.has(proposalId)) + .length + + return activeProposalCount +} + +/** + * Resolve REG token contract from CoinGecko platforms map. + */ +function resolveRegTokenAddressFromCoinGecko(data) { + const platforms = data?.platforms || {} + const candidates = [ + platforms['xdai'], + platforms['gnosis-chain'], + platforms['gnosis'], + platforms['ethereum'] + ] + for (const v of candidates) { + if (typeof v === 'string' && /^0x[a-fA-F0-9]{40}$/.test(v)) return v + } + return null +} + +/** + * Read REG holder count from Blockscout indexer (on-chain indexed data). + */ +async function fetchRegHoldersCount(tokenAddress) { + if (!tokenAddress) return null + const res = await fetch(`${BLOCKSCOUT_API_BASE}/tokens/${tokenAddress}/counters`) + if (!res.ok) throw new Error(`Blockscout counters HTTP ${res.status}`) + const data = await res.json() + const raw = data?.token_holders_count + const parsed = Number.parseInt(String(raw), 10) + return Number.isFinite(parsed) ? parsed : null +} + onMounted(async () => { try { - const res = await fetch( + const regRes = await fetch( `https://api.coingecko.com/api/v3/coins/${COINGECKO_REG_ID}?localization=false&tickers=false&community_data=false&developer_data=false` ) - if (!res.ok) return - const data = await res.json() + if (!regRes.ok) return + const data = await regRes.json() const cap = data.market_data?.market_cap?.usd if (typeof cap === 'number') regMarketCapUsd.value = cap - } catch (_) {} + + const regTokenAddress = resolveRegTokenAddressFromCoinGecko(data) + const [votesCount, holdersCount] = await Promise.all([ + fetchGovernorVoteCount().catch((error) => { + console.warn('Unable to load governor vote count:', error) + return null + }), + fetchRegHoldersCount(regTokenAddress).catch((error) => { + console.warn('Unable to load REG holders count:', error) + return null + }) + ]) + if (typeof votesCount === 'number') tallyVotesCount.value = votesCount + if (typeof holdersCount === 'number') regHoldersCount.value = holdersCount + } catch (error) { + console.warn('Unable to load dynamic homepage stats:', error) + } }) const aboutFeatures = computed(() => [ @@ -124,11 +235,13 @@ const steps = computed(() => [ const stats = computed(() => { const regCap = regMarketCapUsd.value != null ? formatCompact(regMarketCapUsd.value) + ' USD' : t('stats.comingSoon') + const votes = typeof tallyVotesCount.value === 'number' ? tallyVotesCount.value.toLocaleString() : '38' + const holders = typeof regHoldersCount.value === 'number' ? regHoldersCount.value.toLocaleString() : '6 200+' return [ - { label: t('stats.assets'), value: '$128M' }, - { label: t('stats.members'), value: '6 200+' }, + { label: t('stats.assets'), value: '+150M' }, + { label: t('stats.members'), value: holders }, { label: t('stats.regMarketcap'), value: regCap }, - { label: t('stats.votes'), value: '38' } + { label: t('stats.votes'), value: votes } ] })