Examples
Learn by building real-world governance features with the SDK.Sample DAO
These examples use a real DAO on Sepolia testnet:| Contract | Address |
|---|---|
| DAOGovernor | 0xf51C4b7b5AA34052B9C605A6BAf4DB8E844106B0 |
| DAOToken | 0xcfCff2dAad166cd75458E5F4Dd0c1F01c35D8d9C |
| TimelockController | 0xe0E285CaD99C1667101539C09e5b684c0A7BfF18 |
11155111_0xf51C4b7b5AA34052B9C605A6BAf4DB8E844106B0
Project Setup
Create a new Vite + React project:Copy
npm create vite@latest dao-dashboard -- --template react-ts
cd dao-dashboard
npm install
npm install daocafe-sdk @tanstack/react-query
src/main.tsx:
Copy
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import './index.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
refetchOnWindowFocus: false,
},
},
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
DAO Dashboard
A complete dashboard showing DAO info, proposals, and voting power.App Component
Copy
// src/App.tsx
import { DAOHeader } from './components/DAOHeader';
import { ProposalList } from './components/ProposalList';
import { TopHolders } from './components/TopHolders';
const DAO_ID = '11155111_0xf51C4b7b5AA34052B9C605A6BAf4DB8E844106B0';
function App() {
return (
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<DAOHeader daoId={DAO_ID} />
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '20px' }}>
<ProposalList daoId={DAO_ID} />
<TopHolders daoId={DAO_ID} />
</div>
</div>
);
}
export default App;
DAO Header Component
Copy
// src/components/DAOHeader.tsx
import { useDAO } from 'daocafe-sdk';
interface Props {
daoId: string;
}
export function DAOHeader({ daoId }: Props) {
const { data: dao, isLoading } = useDAO(daoId);
if (isLoading) {
return <div>Loading DAO...</div>;
}
if (!dao) {
return <div>DAO not found</div>;
}
return (
<header style={{ marginBottom: '30px' }}>
<h1 style={{ margin: 0 }}>{dao.name}</h1>
<p style={{ color: '#666' }}>
{dao.tokenSymbol} • {dao.proposalCount} proposals
</p>
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '15px',
marginTop: '20px'
}}>
<StatCard label="Total Supply" value={formatNumber(dao.totalSupply)} />
<StatCard label="Voting Delay" value={formatDuration(dao.votingDelay)} />
<StatCard label="Voting Period" value={formatDuration(dao.votingPeriod)} />
<StatCard label="Quorum" value={`${dao.quorumNumerator}%`} />
</div>
</header>
);
}
function StatCard({ label, value }: { label: string; value: string }) {
return (
<div style={{
background: '#f5f5f5',
padding: '15px',
borderRadius: '8px'
}}>
<div style={{ fontSize: '12px', color: '#666' }}>{label}</div>
<div style={{ fontSize: '18px', fontWeight: 'bold' }}>{value}</div>
</div>
);
}
function formatNumber(value: string): string {
const num = BigInt(value) / BigInt(10 ** 18);
return num.toLocaleString();
}
function formatDuration(seconds: string): string {
const num = Number(seconds);
const hours = Math.floor(num / 3600);
const days = Math.floor(hours / 24);
if (days > 0) return `${days}d`;
if (hours > 0) return `${hours}h`;
return `${Math.floor(num / 60)}m`;
}
Proposal List Component
Copy
// src/components/ProposalList.tsx
import { useProposalsByDAO } from 'daocafe-sdk';
import type { Proposal, ProposalState } from 'daocafe-sdk';
interface Props {
daoId: string;
}
export function ProposalList({ daoId }: Props) {
const { data, isLoading } = useProposalsByDAO(daoId, {
limit: 10,
orderBy: 'createdAt',
orderDirection: 'desc',
});
if (isLoading) {
return <div>Loading proposals...</div>;
}
return (
<section>
<h2>Proposals</h2>
{data?.items.length === 0 ? (
<p>No proposals yet</p>
) : (
<ul style={{ listStyle: 'none', padding: 0 }}>
{data?.items.map(proposal => (
<ProposalCard key={proposal.id} proposal={proposal} />
))}
</ul>
)}
</section>
);
}
function ProposalCard({ proposal }: { proposal: Proposal }) {
const forVotes = BigInt(proposal.forVotes);
const againstVotes = BigInt(proposal.againstVotes);
const totalVotes = forVotes + againstVotes;
const forPercent = totalVotes > 0
? Number((forVotes * 100n) / totalVotes)
: 0;
return (
<li style={{
background: '#fff',
border: '1px solid #eee',
borderRadius: '8px',
padding: '15px',
marginBottom: '10px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<h3 style={{ margin: 0, fontSize: '16px' }}>
{proposal.description.slice(0, 60)}...
</h3>
<StateLabel state={proposal.state} />
</div>
<div style={{ marginTop: '10px' }}>
<div style={{
background: '#f0f0f0',
borderRadius: '4px',
overflow: 'hidden',
height: '8px'
}}>
<div style={{
background: '#22c55e',
width: `${forPercent}%`,
height: '100%'
}} />
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between',
fontSize: '12px',
marginTop: '5px'
}}>
<span style={{ color: '#22c55e' }}>For: {formatVotes(proposal.forVotes)}</span>
<span style={{ color: '#ef4444' }}>Against: {formatVotes(proposal.againstVotes)}</span>
</div>
</div>
</li>
);
}
function StateLabel({ state }: { state: ProposalState }) {
const colors: Record<ProposalState, string> = {
PENDING: '#f59e0b',
ACTIVE: '#3b82f6',
CANCELED: '#6b7280',
DEFEATED: '#ef4444',
SUCCEEDED: '#22c55e',
QUEUED: '#8b5cf6',
EXPIRED: '#6b7280',
EXECUTED: '#22c55e',
};
return (
<span style={{
background: colors[state],
color: 'white',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '12px'
}}>
{state}
</span>
);
}
function formatVotes(value: string): string {
const num = Number(BigInt(value) / BigInt(10 ** 18));
return num.toLocaleString();
}
Top Holders Component
Copy
// src/components/TopHolders.tsx
import { useTokenHoldersByDAO } from 'daocafe-sdk';
interface Props {
daoId: string;
}
export function TopHolders({ daoId }: Props) {
const { data, isLoading } = useTokenHoldersByDAO(daoId, {
limit: 10,
orderBy: 'votes',
orderDirection: 'desc',
});
if (isLoading) {
return <div>Loading holders...</div>;
}
return (
<section>
<h2>Top Voters</h2>
<ol style={{ paddingLeft: '20px' }}>
{data?.items.map((holder, index) => (
<li key={holder.id} style={{ marginBottom: '8px' }}>
<code style={{ fontSize: '12px' }}>
{holder.holder.slice(0, 6)}...{holder.holder.slice(-4)}
</code>
<div style={{ fontSize: '14px', fontWeight: 'bold' }}>
{formatVotes(holder.votes)} votes
</div>
</li>
))}
</ol>
</section>
);
}
function formatVotes(value: string): string {
const num = Number(BigInt(value) / BigInt(10 ** 18));
return num.toLocaleString();
}
Active Proposals Widget
A standalone widget showing active proposals across all DAOs.Copy
// src/components/ActiveProposalsWidget.tsx
import { useActiveProposals } from 'daocafe-sdk';
export function ActiveProposalsWidget() {
const { data, isLoading, error } = useActiveProposals({ limit: 5 });
if (isLoading) {
return (
<div style={{ padding: '20px', textAlign: 'center' }}>
Loading active proposals...
</div>
);
}
if (error) {
return (
<div style={{ padding: '20px', color: 'red' }}>
Error: {error.message}
</div>
);
}
if (!data?.items.length) {
return (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
No active proposals at this time
</div>
);
}
return (
<div style={{
background: '#1a1a2e',
color: 'white',
borderRadius: '12px',
padding: '20px'
}}>
<h3 style={{ margin: '0 0 15px 0' }}>
🗳️ Active Proposals ({data.items.length})
</h3>
{data.items.map(proposal => {
const endsIn = getTimeUntil(proposal.voteEnd);
return (
<div
key={proposal.id}
style={{
background: 'rgba(255,255,255,0.1)',
borderRadius: '8px',
padding: '12px',
marginBottom: '10px'
}}
>
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>
{proposal.description.slice(0, 50)}...
</div>
<div style={{ fontSize: '12px', color: '#aaa' }}>
Ends {endsIn}
</div>
</div>
);
})}
</div>
);
}
function getTimeUntil(timestamp: string): string {
const endTime = Number(timestamp) * 1000;
const now = Date.now();
const diff = endTime - now;
if (diff <= 0) return 'soon';
const hours = Math.floor(diff / (1000 * 60 * 60));
const days = Math.floor(hours / 24);
if (days > 0) return `in ${days} day${days > 1 ? 's' : ''}`;
if (hours > 0) return `in ${hours} hour${hours > 1 ? 's' : ''}`;
return 'in less than an hour';
}
Voter Profile
Show a user’s voting history and power across all DAOs.Copy
// src/components/VoterProfile.tsx
import { useVotesByVoter, useTokenHoldingsByAddress } from 'daocafe-sdk';
interface Props {
address: string;
}
export function VoterProfile({ address }: Props) {
const { data: votes } = useVotesByVoter(address, { limit: 20 });
const { data: holdings } = useTokenHoldingsByAddress(address);
const forVotes = votes?.items.filter(v => v.support === 'FOR').length ?? 0;
const againstVotes = votes?.items.filter(v => v.support === 'AGAINST').length ?? 0;
const abstainVotes = votes?.items.filter(v => v.support === 'ABSTAIN').length ?? 0;
return (
<div style={{ padding: '20px' }}>
<h2>Voter Profile</h2>
<p style={{ fontFamily: 'monospace' }}>{address}</p>
<h3>Holdings</h3>
{holdings?.items.length === 0 ? (
<p>No token holdings</p>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={{ textAlign: 'left', padding: '8px', borderBottom: '1px solid #eee' }}>DAO</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Balance</th>
<th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Votes</th>
</tr>
</thead>
<tbody>
{holdings?.items.map(h => (
<tr key={h.id}>
<td style={{ padding: '8px' }}>{h.daoId.slice(0, 20)}...</td>
<td style={{ textAlign: 'right', padding: '8px' }}>{formatTokens(h.balance)}</td>
<td style={{ textAlign: 'right', padding: '8px' }}>{formatTokens(h.votes)}</td>
</tr>
))}
</tbody>
</table>
)}
<h3>Voting History</h3>
<div style={{ display: 'flex', gap: '20px', marginBottom: '15px' }}>
<div>
<span style={{ color: '#22c55e', fontWeight: 'bold' }}>{forVotes}</span> For
</div>
<div>
<span style={{ color: '#ef4444', fontWeight: 'bold' }}>{againstVotes}</span> Against
</div>
<div>
<span style={{ color: '#6b7280', fontWeight: 'bold' }}>{abstainVotes}</span> Abstain
</div>
</div>
{votes?.items.map(vote => (
<div
key={vote.id}
style={{
padding: '10px',
borderLeft: `3px solid ${
vote.support === 'FOR' ? '#22c55e' :
vote.support === 'AGAINST' ? '#ef4444' : '#6b7280'
}`,
marginBottom: '8px',
background: '#f9f9f9'
}}
>
<div style={{ fontWeight: 'bold' }}>{vote.support}</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{formatTokens(vote.weight)} votes
</div>
{vote.reason && (
<div style={{ marginTop: '5px', fontStyle: 'italic' }}>
"{vote.reason}"
</div>
)}
</div>
))}
</div>
);
}
function formatTokens(value: string): string {
const num = Number(BigInt(value) / BigInt(10 ** 18));
return num.toLocaleString();
}
Node.js Script
Fetch and analyze DAO data from the command line:Copy
// scripts/analyze-dao.ts
import { getDAO, getProposalsByDAO, getTokenHoldersByDAO } from 'daocafe-sdk';
const DAO_ID = '11155111_0xf51C4b7b5AA34052B9C605A6BAf4DB8E844106B0';
async function main() {
console.log('Fetching DAO data...\n');
// Get DAO info
const dao = await getDAO(DAO_ID);
if (!dao) {
console.error('DAO not found');
return;
}
console.log(`=== ${dao.name} ===`);
console.log(`Token: ${dao.tokenSymbol}`);
console.log(`Proposals: ${dao.proposalCount}`);
console.log(`Voting Period: ${dao.votingPeriod} seconds`);
console.log();
// Get proposals
const { items: proposals } = await getProposalsByDAO(DAO_ID, { limit: 5 });
console.log('Recent Proposals:');
proposals.forEach((p, i) => {
console.log(` ${i + 1}. [${p.state}] ${p.description.slice(0, 40)}...`);
});
console.log();
// Get top holders
const { items: holders } = await getTokenHoldersByDAO(DAO_ID, {
orderBy: 'votes',
orderDirection: 'desc',
limit: 5,
});
console.log('Top Voters:');
holders.forEach((h, i) => {
const votes = Number(BigInt(h.votes) / BigInt(10 ** 18));
console.log(` ${i + 1}. ${h.holder.slice(0, 10)}... - ${votes.toLocaleString()} votes`);
});
}
main().catch(console.error);
Copy
npx tsx scripts/analyze-dao.ts