proposals.gno
3.97 Kb · 153 lines
1package daokit
2
3// Handles DAO governance through proposal voting and execution.
4// They contain actions that run when voting requirements are satisfied.
5//
6// Common use cases: member management, treasury operations, rule updates.
7
8import (
9 "chain/runtime"
10 "errors"
11 "time"
12
13 "gno.land/p/nt/avl/v0"
14 "gno.land/p/nt/seqid/v0"
15 "gno.land/p/onbloc/json"
16 "gno.land/p/samcrew/daocond"
17)
18
19type ProposalStatus int
20
21const (
22 ProposalStatusOpen ProposalStatus = iota // Currently accepting votes
23 ProposalStatusPassed // Met conditions, ready for execution
24 ProposalStatusExecuted // Successfully executed
25)
26
27func (s ProposalStatus) String() string {
28 switch s {
29 case ProposalStatusOpen:
30 return "Open"
31 case ProposalStatusPassed:
32 return "Passed"
33 case ProposalStatusExecuted:
34 return "Executed"
35 default:
36 return "Unknown"
37 }
38}
39
40type Proposal struct {
41 ID seqid.ID
42 Title string
43 Description string
44 CreatedAt time.Time
45 CreatedHeight int64
46 ProposerID string
47
48 Action Action // What to execute if passed
49 Condition daocond.Condition // Voting conditions for this proposal
50 Status ProposalStatus
51 ExecutorID string
52 ExecutedAt time.Time
53
54 Ballot daocond.Ballot // All votes cast on this proposal
55}
56
57type ProposalsStore struct {
58 Tree *avl.Tree // int -> Proposal
59 genID seqid.ID
60}
61
62// Data needed to create a new proposal.
63type ProposalRequest struct {
64 Title string
65 Description string
66 Action Action
67}
68
69func NewProposalsStore() *ProposalsStore {
70 return &ProposalsStore{
71 Tree: avl.NewTree(),
72 }
73}
74
75func (p *ProposalsStore) GetProposal(id uint64) *Proposal {
76 value, ok := p.Tree.Get(seqid.ID(id).String())
77 if !ok {
78 return nil
79 }
80 proposal := value.(*Proposal)
81 return proposal
82}
83
84// Returns all proposals that match the given filter function.
85func (p *ProposalsStore) GetProposals(filter func(Proposal) bool) *ProposalsStore {
86 proposals := NewProposalsStore()
87 p.Tree.Iterate("", "", func(key string, value interface{}) bool {
88 prop, ok := value.(*Proposal)
89 if !ok {
90 panic(errors.New("unexpected invalid proposal type"))
91 }
92 prop.UpdateStatus() // XXX: costly and probably insecure
93 if filter(*prop) {
94 proposals.Tree.Set(key, prop)
95 }
96 return false
97 })
98 return proposals
99}
100
101// Updates proposal status based on current voting condition results.
102func (p *Proposal) UpdateStatus() {
103 conditionsAreMet := p.Condition.Eval(p.Ballot)
104 if p.Status == ProposalStatusOpen && conditionsAreMet {
105 p.Status = ProposalStatusPassed
106 }
107}
108
109// Returns all proposals as a JSON string.
110func (p *ProposalsStore) GetProposalsJSON() string {
111 props := make([]*json.Node, 0, p.Tree.Size())
112 // XXX: pagination
113 p.Tree.Iterate("", "", func(key string, value interface{}) bool {
114 prop, ok := value.(*Proposal)
115 if !ok {
116 panic(errors.New("unexpected invalid proposal type"))
117 }
118 prop.UpdateStatus()
119 props = append(props, json.ObjectNode("", map[string]*json.Node{
120 "id": json.NumberNode("", float64(prop.ID)),
121 "title": json.StringNode("", prop.Title),
122 "description": json.StringNode("", prop.Description),
123 "proposer": json.StringNode("", prop.ProposerID),
124 "status": json.StringNode("", prop.Status.String()),
125 "startHeight": json.NumberNode("", float64(prop.CreatedHeight)),
126 "signal": json.NumberNode("", prop.Condition.Signal(prop.Ballot)),
127 }))
128 return false
129 })
130 bz, err := json.Marshal(json.ArrayNode("", props))
131 if err != nil {
132 panic(err)
133 }
134 return string(bz)
135}
136
137func (p *ProposalsStore) newProposal(proposer string, req ProposalRequest, condition daocond.Condition) *Proposal {
138 id := p.genID.Next()
139 proposal := &Proposal{
140 ID: id,
141 Title: req.Title,
142 Description: req.Description,
143 ProposerID: proposer,
144 Status: ProposalStatusOpen,
145 Action: req.Action,
146 Condition: condition,
147 Ballot: daocond.NewBallot(),
148 CreatedAt: time.Now(),
149 CreatedHeight: runtime.ChainHeight(),
150 }
151 p.Tree.Set(id.String(), proposal)
152 return proposal
153}