governance API
Proposal system implementing 1M1V (One Member = One Vote) governance with ICRC-7 vote-pass NFTs.
Candid file: governance/src/governance.didStandard: ICRC-7 (for vote-pass NFTs)
Types
ProposalCategory
type ProposalCategory = variant {
Constitutional; // Bylaws, membership rules
Operational; // Day-to-day management
Treasury; // Spending, allocations
SoftwareDevelopment; // Features, infrastructure
};ProposalStatus
type ProposalStatus = variant {
Draft; // In review period (24h before voting)
Active; // Voting period active
Approved; // Passed
Rejected; // Failed
Executed; // Successfully executed
Failed; // Execution failed
};VoteChoice
type VoteChoice = variant {
Yes;
No;
Abstain;
};Proposal
type Proposal = record {
id: nat64;
category: ProposalCategory;
title: text;
description: text;
proposer: principal;
created_at: nat64;
voting_start_at: nat64;
voting_end_at: nat64;
status: ProposalStatus;
yes_votes: nat64;
no_votes: nat64;
abstain_votes: nat64;
quorum_threshold: nat64;
approval_threshold: nat8; // Percentage (0-100)
execution_data: opt blob;
};Vote
type Vote = record {
proposal_id: nat64;
voter: principal;
choice: VoteChoice;
voted_at: nat64;
vote_pass_nft_id: opt nat64;
};Proposal Management
create_proposal
Create a new governance proposal.
"create_proposal": (CreateProposalArgs) -> (variant { Ok: nat64; Err: text });
type CreateProposalArgs = record {
category: ProposalCategory;
title: text;
description: text;
voting_period_hours: nat64; // 24-168 hours
execution_data: opt blob;
};Requirements:
- Caller must be active member
- Title max 200 characters
- Description max 10,000 characters
- Voting period 24-168 hours
TypeScript Example:
const result = await governanceActor.create_proposal({
category: { Treasury: null },
title: 'Allocate funds for community event',
description: 'Proposal to allocate 1000 DOM for Q1 community meetup...',
voting_period_hours: 72n,
execution_data: [],
});
if ('Ok' in result) {
console.log('Created proposal ID:', result.Ok);
}get_proposal (query)
"get_proposal": (proposal_id: nat64) -> (opt Proposal) query;list_proposals (query)
"list_proposals": (filter: opt ProposalFilter, pagination: opt Pagination) -> (vec Proposal) query;
type ProposalFilter = record {
status: opt ProposalStatus;
category: opt ProposalCategory;
proposer: opt principal;
};
type Pagination = record {
offset: nat64;
limit: nat64;
};Voting
cast_vote
Cast a vote on an active proposal.
"cast_vote": (proposal_id: nat64, choice: VoteChoice) -> (variant { Ok; Err: text });Requirements:
- Caller must be active member
- Proposal must be in
Activestatus - Cannot vote twice on same proposal
1M1V Rule: Each vote increments the counter by exactly 1, regardless of token holdings.
Side Effect: Mints a vote-pass NFT for the voter.
has_voted (query)
"has_voted": (proposal_id: nat64, voter: principal) -> (bool) query;get_vote (query)
"get_vote": (proposal_id: nat64, voter: principal) -> (opt Vote) query;Proposal Finalization
finalize
Finalize a proposal after voting ends.
"finalize": (proposal_id: nat64) -> (yes_votes: nat64, no_votes: nat64, abstain_votes: nat64);Actions:
- Calculates final vote counts
- Sets status to
ApprovedorRejected - Burns all vote-pass NFTs for this proposal
execute_proposal
Execute an approved proposal.
"execute_proposal": (proposal_id: nat64) -> (variant { Ok; Err: text });Status transitions: Approved → Executed (success) or Failed (error)
ICRC-7 Vote-Pass NFT Methods
Vote-pass NFTs are Soul-Bound Tokens minted when voting, burned when proposal finalizes.
icrc7_name (query)
"icrc7_name": () -> (text) query;Returns: "Hello World DAO Vote Pass"
icrc7_symbol (query)
"icrc7_symbol": () -> (text) query;Returns: "HWDVP"
icrc7_total_supply (query)
"icrc7_total_supply": () -> (nat64) query;Active vote passes (not yet burned).
icrc7_balance_of (query)
"icrc7_balance_of": (principal) -> (nat64) query;icrc7_tokens_of (query)
"icrc7_tokens_of": (principal) -> (vec nat64) query;icrc7_transfer
Always returns NonTransferable - Vote passes cannot be transferred.
"icrc7_transfer": (from: principal, to: principal, token_id: nat64, memo: opt blob, created_at_time: opt nat64)
-> (variant { Ok: nat64; Err: TransferError });Audit Trail
get_burn_history (query)
Get burned vote passes for a proposal.
"get_burn_history": (proposal_id: nat64) -> (vec BurnRecord) query;
type BurnRecord = record {
token_id: nat64;
proposal_id: nat64;
voter: principal;
burned_at: nat64;
};get_all_burn_history (query)
"get_all_burn_history": (offset: opt nat64, limit: opt nat64) -> (vec BurnRecord) query;Category Statistics
get_proposal_count_by_category (query)
"get_proposal_count_by_category": () -> (vec record { ProposalCategory; nat64 }) query;get_active_proposals_by_category (query)
"get_active_proposals_by_category": (category: ProposalCategory) -> (vec Proposal) query;Threshold Configuration (Controller Only)
set_default_quorum_percentage
"set_default_quorum_percentage": (category: ProposalCategory, percentage: nat8) -> (variant { Ok; Err: text });set_default_approval_percentage
"set_default_approval_percentage": (category: ProposalCategory, percentage: nat8) -> (variant { Ok; Err: text });Default Thresholds:
| Category | Quorum | Approval |
|---|---|---|
| Constitutional | 50% | 67% |
| Operational | 25% | 50% |
| Treasury | 33% | 50% |
| SoftwareDevelopment | 25% | 50% |
Error Messages
| Error | Cause | Resolution |
|---|---|---|
Not a member | Caller not active member | Complete signup |
Already voted | Duplicate vote attempt | Cannot vote twice |
Voting not active | Proposal not in Active status | Wait for voting period |
Proposal not found | Invalid proposal ID | Check proposal exists |
Proposal not approved | Trying to execute non-approved | Proposal must pass first |