This subgraph indexes and tracks governance events for the Rootstock Collective DAO Governor contract on Rootstock testnet.
The subgraph tracks key governance activities including:
- Proposal creation and lifecycle
- Voting activities
- Governance parameter changes
- Contract upgrades and administrative actions
type Proposal @entity {
id: ID!
proposalId: BigInt!
proposer: Bytes!
targets: [Bytes!]!
values: [BigInt!]!
signatures: [String!]!
calldatas: [Bytes!]!
voteStart: BigInt!
voteEnd: BigInt!
description: String!
state: ProposalState!
createdAt: BigInt!
votes: [Vote!]! @derivedFrom(field: "proposal")
forVotes: BigInt!
againstVotes: BigInt!
abstainVotes: BigInt!
}The Proposal entity aggregates all proposal-related data and maintains the current state. Key features:
- Tracks full proposal details including execution data
- Maintains vote tallies
- Links to all votes cast
- Tracks proposal lifecycle states
type Vote @entity {
id: ID!
proposal: Proposal!
voter: Bytes!
support: Int!
weight: BigInt!
reason: String
timestamp: BigInt!
}The Vote entity tracks individual votes cast, enabling:
- Per-address voting history
- Vote weight tracking
- Vote reasoning/rationale storage
- Temporal analysis of voting patterns
The schema is designed for scalability:
- Efficient querying through strategic ID formatting
- Bidirectional relationships between proposals and votes
- Aggregated vote counts at proposal level
- State tracking for proposal lifecycle
{
proposals(first: 5, orderBy: createdAt, orderDirection: desc) {
id
description
state
forVotes
againstVotes
abstainVotes
}
}{
votes(where: { voter: "0x..." }) {
proposal {
id
description
}
support
weight
timestamp
}
}{
proposals(where: { state: Active }) {
id
description
voteEnd
forVotes
againstVotes
}
}- Install dependencies:
npm install- Generate types:
graph codegen- Build subgraph:
graph build- Deploy:
graph deploy https://your-subgraph-service/ your-subgraph-slugThe subgraph handles multiple governance events:
ProposalCreated: Creates new proposal entitiesVoteCast: Records votes and updates proposal vote countsProposalExecuted,ProposalCanceled,ProposalQueued: Updates proposal states
type Proposal @entity {
id: ID!
proposalId: BigInt!
proposer: Bytes!
targets: [Bytes!]!
values: [BigInt!]!
signatures: [String!]!
calldatas: [Bytes!]!
voteStart: BigInt!
voteEnd: BigInt!
description: String!
state: ProposalState!
createdAt: BigInt!
votes: [Vote!]! @derivedFrom(field: "proposal")
forVotes: BigInt!
againstVotes: BigInt!
abstainVotes: BigInt!
}
enum ProposalState {
Pending
Active
Canceled
Defeated
Succeeded
Queued
Expired
Executed
}
type Vote @entity {
id: ID!
proposal: Proposal!
voter: Bytes!
support: Int!
weight: BigInt!
reason: String
timestamp: BigInt!
}export function handleProposalCreated(event: ProposalCreatedEvent): void {
let proposal = new Proposal(event.params.proposalId.toString())
proposal.proposalId = event.params.proposalId
proposal.proposer = event.params.proposer
proposal.targets = changetype<Bytes[]>(event.params.targets)
proposal.values = event.params.values
proposal.signatures = event.params.signatures
proposal.calldatas = event.params.calldatas
proposal.voteStart = event.params.voteStart
proposal.voteEnd = event.params.voteEnd
proposal.description = event.params.description
proposal.state = "Pending"
proposal.createdAt = event.block.timestamp
proposal.forVotes = BigInt.fromI32(0)
proposal.againstVotes = BigInt.fromI32(0)
proposal.abstainVotes = BigInt.fromI32(0)
proposal.save()
}
export function handleVoteCast(event: VoteCastEvent): void {
let voteId = event.params.proposalId.toString()
.concat("-")
.concat(event.params.voter.toHexString())
let vote = new Vote(voteId)
vote.proposal = event.params.proposalId.toString()
vote.voter = event.params.voter
vote.support = event.params.support
vote.weight = event.params.weight
vote.reason = event.params.reason
vote.timestamp = event.block.timestamp
vote.save()
// Update proposal vote counts
let proposal = Proposal.load(event.params.proposalId.toString())
if (proposal) {
if (event.params.support == 0) {
proposal.againstVotes = proposal.againstVotes.plus(event.params.weight)
} else if (event.params.support == 1) {
proposal.forVotes = proposal.forVotes.plus(event.params.weight)
} else if (event.params.support == 2) {
proposal.abstainVotes = proposal.abstainVotes.plus(event.params.weight)
}
proposal.save()
}
}
export function handleProposalExecuted(event: ProposalExecutedEvent): void {
let proposal = Proposal.load(event.params.proposalId.toString())
if (proposal) {
proposal.state = "Executed"
proposal.save()
}
}
export function handleProposalCanceled(event: ProposalCanceledEvent): void {
let proposal = Proposal.load(event.params.proposalId.toString())
if (proposal) {
proposal.state = "Canceled"
proposal.save()
}
}
export function handleProposalQueued(event: ProposalQueuedEvent): void {
let proposal = Proposal.load(event.params.proposalId.toString())
if (proposal) {
proposal.state = "Queued"
proposal.save()
}
}Potential improvements:
- Add delegation tracking
- Implement vote power snapshots
- Add proposal execution tracking
- Add analytics entities for governance metrics
Before deployment, test your subgraph's indexing status:
{
_meta {
block {
number
hash
}
deployment
hasIndexingErrors
}
}Check for indexed proposals:
{
proposalCreateds(first: 5) {
id
proposalId
proposer
blockNumber
}
}Monitor votes:
{
voteCasts(first: 5) {
id
voter
proposalId
support
weight
}
}This subgraph is designed to work seamlessly with frontend applications. The entity structure allows for efficient querying of proposal states and vote tracking, making it ideal for governance dashboards and voting interfaces.
For example, the latest proposal page and proposal we implemented can directly query the subgraph to display:
- Current proposal status
- Vote counts
- User's voting history
- Proposal details
The schema is also extensible, allowing for future additions to track more governance metrics as the DAO evolves.
// Using node-fetch
import fetch from 'node-fetch';
const SUBGRAPH_URL = "https://api.thegraph.com/subgraphs/name/your-username/rootstock-collective-dao";
async function fetchProposals() {
const query = `
{
proposals(first: 5, orderBy: createdAt, orderDirection: desc) {
id
proposalId
description
state
forVotes
againstVotes
abstainVotes
createdAt
}
}
`;
try {
const response = await fetch(SUBGRAPH_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query })
});
const data = await response.json();
console.log('Proposals:', data.data.proposals);
return data.data.proposals;
} catch (error) {
console.error('Error fetching proposals:', error);
}
}
// Using graphql-request (recommended)
import { request, gql } from 'graphql-request';
async function getUserVotes(userAddress: string) {
const query = gql`
query GetUserVotes($voter: String!) {
votes(where: { voter: $voter }) {
proposal {
id
description
}
support
weight
timestamp
}
}
`;
const variables = {
voter: userAddress.toLowerCase()
};
try {
const data = await request(SUBGRAPH_URL, query, variables);
console.log('User votes:', data.votes);
return data.votes;
} catch (error) {
console.error('Error fetching user votes:', error);
}
}
// Usage example
async function main() {
// Fetch recent proposals
const proposals = await fetchProposals();
// Fetch specific user's votes
const userVotes = await getUserVotes('0x123...abc');
}
main();