Skip to content

Commit

Permalink
feat: dynamically fetch DAO voting stats (#268)
Browse files Browse the repository at this point in the history
* refactor: replace images

* fetch number of proposals

* fetch voting stats at build time

* improve parsing of proposal titles

* dynamically import ParallaxDaoStats to fix runtime error

* bump version v1.3.10
  • Loading branch information
DiogoSoaress authored Feb 8, 2024
1 parent 6e83e90 commit 1afbb1d
Show file tree
Hide file tree
Showing 20 changed files with 185 additions and 45 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "safe-homepage",
"homepage": "https://github.com/safe-global/safe-homepage",
"version": "1.3.9",
"version": "1.3.10",
"scripts": {
"build": "next build && next export",
"lint": "tsc && next lint",
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
12 changes: 12 additions & 0 deletions src/components/Governance/ParallaxDaoStats/ParallaxDaoStats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ParallaxDaoStatsElement from '@/components/Governance/ParallaxDaoStats/ParallaxDaoStatsElement'
import ParallaxText, { type ParallaxTextProps } from '@/components/common/ParallaxText'

const ParallaxDaoStats = (props: ParallaxTextProps) => {
return (
<ParallaxText {...props}>
<ParallaxDaoStatsElement items={props.items} />
</ParallaxText>
)
}

export default ParallaxDaoStats
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
import Image from 'next/image'
import FrameImage from '@/public/images/Governance/Parallaxes/DaoStats/background.svg'
import ProposalsImage from '@/public/images/Governance/Parallaxes/DaoStats/proposals.png'
import DelegatesImage from '@/public/images/Governance/Parallaxes/DaoStats/delegates.png'
import DelegatorsImage from '@/public/images/Governance/Parallaxes/DaoStats/delegators.png'
import ParallaxWrapper from '@/components/common/ParallaxWrapper'
import css from './styles.module.css'
import { Typography } from '@mui/material'
import { useSafeSnapshot } from '@/hooks/useSafeSnapshot'
import { useVotingDelegation } from '@/hooks/useVotingDelegation'
import type { BaseBlock } from '@/components/Home/types'

const ParallaxDaoStatsElement = ({ items }: Partial<BaseBlock>) => {
const { data: proposals } = useSafeSnapshot()
const [totalDelegates, totalDelegators] = useVotingDelegation()

const ParallaxDaoStatsElement = () => {
return (
<div className={css.parallaxWrapper}>
<FrameImage className={css.baseImage} />
<ParallaxWrapper translateX={0} translateY={0} depth={2} direction={-1}>
<Image src={ProposalsImage} alt="8 proposals" className={css.proposals} />
<div className={`${css.card} ${css.proposals}`}>
<Typography className={css.value}>{proposals?.length || items?.[0].title}</Typography>
<Typography variant="caption" className={css.caption}>
{items?.[0].text}
</Typography>
</div>
</ParallaxWrapper>
<ParallaxWrapper translateX={0} translateY={0} depth={1} direction={-1}>
<Image src={DelegatesImage} alt="2.5K delegates" className={css.delegates} />
<div className={`${css.card} ${css.delegates}`}>
<Typography className={css.value}>{totalDelegates || items?.[1].title}</Typography>
<Typography variant="caption" className={css.caption}>
{items?.[1].text}
</Typography>
</div>
</ParallaxWrapper>
<ParallaxWrapper translateX={0} translateY={0} depth={0} direction={-1}>
<Image src={DelegatorsImage} alt="11.2K delegators" className={css.delegators} />
<div className={`${css.card} ${css.delegators}`}>
<Typography className={css.value}>{totalDelegators || items?.[2].title}</Typography>
<Typography variant="caption" className={css.caption}>
{items?.[2].text}
</Typography>
</div>
</ParallaxWrapper>
</div>
)
Expand Down
13 changes: 3 additions & 10 deletions src/components/Governance/ParallaxDaoStats/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import ParallaxDaoStatsElement from '@/components/Governance/ParallaxDaoStats/ParallaxDaoStatsElement'
import ParallaxText, { type ParallaxTextProps } from '@/components/common/ParallaxText'
import dynamic from 'next/dynamic'

const ParallaxDaoStats = (props: ParallaxTextProps) => {
return (
<ParallaxText {...props}>
<ParallaxDaoStatsElement />
</ParallaxText>
)
}
const Proposals = dynamic(() => import('./ParallaxDaoStats'))

export default ParallaxDaoStats
export default Proposals
50 changes: 48 additions & 2 deletions src/components/Governance/ParallaxDaoStats/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,58 @@
height: 290px;
}

.card {
padding: 14px 24px;
border: 1px solid var(--mui-palette-border-main);
border-radius: 12px;

display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;

background-color: var(--mui-palette-background-default);
}

.proposals {
position: absolute;
bottom: 242px;
left: 60px;
width: 144px;

width: 101px;
height: 124px;
}

.delegates {
position: absolute;
bottom: 150px;
left: 190px;
width: 162px;
height: 118px;
}

.delegators {
position: absolute;
bottom: 0px;
left: 126px;
width: 162px;
height: 118px;
}

.value {
font-size: 57px;
line-height: 72px;
background: linear-gradient(260.13deg, #12ff80 1.24%, #5fddff 102.14%);
background-clip: text;
color: transparent;
text-align: left;
}

.caption {
font-size: 8.6px;
line-height: 17px;
display: flex;
align-items: flex-start;
}

@media (min-width: 600px) {
Expand All @@ -43,18 +76,31 @@
.proposals {
bottom: 333px;
left: 75px;
width: 226px;
width: 141px;
height: 174px;
}

.delegates {
bottom: 207px;
left: 275px;
width: 227px;
height: 164px;
}

.delegators {
bottom: 27px;
left: 160px;
width: 226px;
height: 165px;
}

.value {
font-size: 80px;
line-height: 100px;
}

.caption {
font-size: 12px;
line-height: 24px;
}
}
3 changes: 2 additions & 1 deletion src/components/Governance/Proposals/Proposals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useSafeSnapshot } from '@/hooks/useSafeSnapshot'
import layoutCss from '@/components/common/styles.module.css'
import css from './styles.module.css'

const PROPOSAL_AMOUNT = 4
const PROPOSAL_LINK_BASE_URL = 'https://snapshot.org/#/safe.eth/proposal/'

type SnapshotProposal = {
Expand Down Expand Up @@ -60,7 +61,7 @@ const Proposals = (props: BaseBlock) => {
<HeaderCTA {...props} />

<Stack spacing={3}>
{proposals?.map((proposal) => (
{proposals?.slice(0, PROPOSAL_AMOUNT).map((proposal) => (
<ProposalRow
key={proposal.id}
title={proposal.title}
Expand Down
11 changes: 9 additions & 2 deletions src/components/Governance/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import PageContent from '@/components/common/PageContent'
import governanceContent from '@/content/governance.json'
import VotingDelegationContext from '@/contexts/VotingDelegationContext'
import type { getStaticProps } from '@/pages/governance'
import type { InferGetStaticPropsType } from 'next'

export const Governance = () => {
return <PageContent content={governanceContent} path="governance.json" />
export const Governance = (props: InferGetStaticPropsType<typeof getStaticProps>) => {
return (
<VotingDelegationContext.Provider value={props.votingDelegation}>
<PageContent content={governanceContent} path="governance.json" />
</VotingDelegationContext.Provider>
)
}
1 change: 1 addition & 0 deletions src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const GOOGLE_ANALYTICS_DOMAIN = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_
export const HOTJAR_ID = process.env.NEXT_PUBLIC_HOTJAR_ID || ''
export const HOTJAR_ID_STAGING = process.env.NEXT_PUBLIC_HOTJAR_ID_STAGING || ''
export const HOTJAR_VERSION = process.env.NEXT_PUBLIC_HOTJAR_VERSION || '6'
export const DUNE_API_KEY = process.env.DUNE_API_KEY || ''

// Links
export const WALLET_LINK = 'https://app.safe.global'
Expand Down
16 changes: 15 additions & 1 deletion src/content/governance.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,21 @@
"component": "Governance/ParallaxDaoStats",
"variant": "image-text",
"mobileVariant": "text-image",
"title": "Safe <u>DAO</u> is a <b>decentralised collective</b> stewarding the <i>Safe</i> ecosystem"
"title": "Safe <u>DAO</u> is a <b>decentralised collective</b> stewarding the <i>Safe</i> ecosystem",
"items": [
{
"title": "20",
"text": "Proposals"
},
{
"title": "5.5K",
"text": "Delegates"
},
{
"title": "16K",
"text": "Delegators"
}
]
},
{
"component": "Governance/Proposals",
Expand Down
13 changes: 13 additions & 0 deletions src/contexts/VotingDelegationContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createContext } from 'react'

type VotingDelegation = {
totalDelegates: number | null
totalDelegators: number | null
}

const VotingDelegationContext = createContext<VotingDelegation>({
totalDelegates: null,
totalDelegators: null,
})

export default VotingDelegationContext
10 changes: 1 addition & 9 deletions src/hooks/useSafeSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import useSWRImmutable from 'swr/immutable'

type ShapshotProposalVars = {
space: string
first: number
skip: number
orderBy: 'created'
orderDirection: 'desc' | 'asc'
}
Expand All @@ -26,10 +24,8 @@ const getSnapshot = async (variables: ShapshotProposalVars): Promise<SnapshotPro
const SNAPSHOT_GQL_ENDPOINT = 'https://hub.snapshot.org/graphql'

const query = `
query ($first: Int, $skip: Int, $space: String, $orderBy: String, $orderDirection: OrderDirection) {
query ($space: String, $orderBy: String, $orderDirection: OrderDirection) {
proposals(
first: $first,
skip: $skip,
orderBy: $orderBy,
orderDirection: $orderDirection
where: { space_in: [$space] },
Expand Down Expand Up @@ -62,12 +58,8 @@ const getSnapshot = async (variables: ShapshotProposalVars): Promise<SnapshotPro
}

export const getSafeSnapshot = (space: string): Promise<SnapshotProposal[]> => {
const PROPOSAL_AMOUNT = 4

return getSnapshot({
space,
first: PROPOSAL_AMOUNT,
skip: 0,
orderBy: 'created',
orderDirection: 'desc',
})
Expand Down
12 changes: 5 additions & 7 deletions src/hooks/useSafeStats.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
import { useContext } from 'react'
import { formatValue } from '@/lib/formatValue'
import SafeStatsContext from '@/contexts/SafeStatsContext'
import { DUNE_API_KEY } from '@/config/constants'
import { duneQueryUrlBuilder } from '@/lib/duneQueryUrlBuilder'

const QUERY_ID_TOTAL_TRANSACTIONS = 2093960
const QUERY_ID_TOTAL_ASSETS = 2893829
const QUERY_ID_TOTAL_SAFES_DEPLOYED = 2459401

function totalAssetsEndpoint(queryId: number): string {
return `https://api.dune.com/api/v1/query/${queryId}/results?api_key=${process.env.DUNE_API_KEY}`
}

export const fetchTotalTransactions = async (): Promise<number | null> => {
return fetch(totalAssetsEndpoint(QUERY_ID_TOTAL_TRANSACTIONS))
return fetch(duneQueryUrlBuilder(QUERY_ID_TOTAL_TRANSACTIONS, DUNE_API_KEY))
.then((res) => res.json())
.then((data) => data.result.rows[0].num_txs)
.catch(() => null)
}

export const fetchTotalAssets = async (): Promise<number | null> => {
return fetch(totalAssetsEndpoint(QUERY_ID_TOTAL_ASSETS))
return fetch(duneQueryUrlBuilder(QUERY_ID_TOTAL_ASSETS, DUNE_API_KEY))
.then((res) => res.json())
.then((data) => data.result.rows[0].usd_value)
.catch(() => null)
}

export const fetchTotalSafesDeployed = async (): Promise<number | null> => {
return fetch(totalAssetsEndpoint(QUERY_ID_TOTAL_SAFES_DEPLOYED))
return fetch(duneQueryUrlBuilder(QUERY_ID_TOTAL_SAFES_DEPLOYED, DUNE_API_KEY))
.then((res) => res.json())
.then((data) => data.result.rows[0].num_safes)
.catch(() => null)
Expand Down
23 changes: 23 additions & 0 deletions src/hooks/useVotingDelegation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DUNE_API_KEY } from '@/config/constants'
import VotingDelegationContext from '@/contexts/VotingDelegationContext'
import { duneQueryUrlBuilder } from '@/lib/duneQueryUrlBuilder'
import { formatValue } from '@/lib/formatValue'
import { useContext } from 'react'

const QUERY_ID_TOTAL_DELEGATIONS = 3407074

export const fetchTotalDelegates = async (): Promise<{ delegate_unique: number; delegator_count: number } | null> => {
return fetch(duneQueryUrlBuilder(QUERY_ID_TOTAL_DELEGATIONS, DUNE_API_KEY))
.then((res) => res.json())
.then((data) => data.result.rows[0])
.catch(() => null)
}

export const useVotingDelegation = (): Array<string | null> => {
const { totalDelegates, totalDelegators } = useContext(VotingDelegationContext)

const formattedTotalDelegates = totalDelegates ? formatValue(totalDelegates) : null
const formattedTotalDelegators = totalDelegators ? formatValue(totalDelegators) : null

return [formattedTotalDelegates, formattedTotalDelegators]
}
5 changes: 5 additions & 0 deletions src/lib/__test__/parseSnapshotTitle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const realTitles = [
' [SEP #4] SafeDAO Constitution',
'[SEP #5] Redistributing Unredeemed Tokens From User Airdrop Allocation',
'# [SEP #6] Safe Grants Program (SGP)',
'[SEP #20] [OBRA] Formalizing the Guardian Role onchain with Hats Protocol - Hats Protocol',
]

const badTitles = [
Expand All @@ -31,6 +32,10 @@ describe('parseSnapshotTitle', () => {
'Redistributing Unredeemed Tokens From User Airdrop Allocation',
])
expect(parseSnapshotTitle(realTitles[5])).toEqual(['6', 'Safe Grants Program (SGP)'])
expect(parseSnapshotTitle(realTitles[6])).toEqual([
'20',
'[OBRA] Formalizing the Guardian Role onchain with Hats Protocol - Hats Protocol',
])
})

it('returns 0 and the original title if the both groups are not present in the title', () => {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/duneQueryUrlBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const duneQueryUrlBuilder = (queryId: number, apiKey: string) =>
`https://api.dune.com/api/v1/query/${queryId}/results?api_key=${apiKey}`
2 changes: 1 addition & 1 deletion src/lib/parseSnapshotTitle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Fist group: SEP number
// Second group: SEP title
const snapshotRegex = /SEP #(\d+)\]?[^A-Za-z]*(.*)/
const snapshotRegex = /SEP #(\d+)\]?[^A-Za-z[]*(.*)/

/**
* Parses a snapshot title to extract the numeric identifier and description.
Expand Down
Loading

0 comments on commit 1afbb1d

Please sign in to comment.