french_boulangerie.gno
9.04 Kb · 351 lines
1package french_boulangerie
2
3import (
4 "chain/runtime"
5 "strings"
6 "strconv"
7)
8
9// ── Types ─────────────────────────────────────────────────
10
11type Member struct {
12 Address address
13 Power int
14 Roles []string
15}
16
17type Vote struct {
18 Voter address
19 Value string // "YES", "NO", "ABSTAIN"
20}
21
22type Proposal struct {
23 ID int
24 Title string
25 Description string
26 Category string
27 Author address
28 Status string // "ACTIVE", "ACCEPTED", "REJECTED", "EXECUTED"
29 Votes []Vote
30 YesVotes int
31 NoVotes int
32 Abstain int
33 TotalPower int
34}
35
36// ── State ─────────────────────────────────────────────────
37
38var (
39 name = "FRENCH BOULANGERIE"
40 description = "This Enterprise DAO is a template for me to test all Entreprise Grade DAOKit Parameters and have red vs blue team experimentation."
41 threshold = 66 // percentage required to pass
42 quorum = 50 // minimum participation % (0 = disabled)
43 members []Member
44 proposals []Proposal
45 nextID = 0
46 allowedCategories []string
47 allowedRoles []string
48)
49
50func init() {
51 members = append(members, Member{Address: address("g187sfsghc9tqayr5rgdmpy2tetnq9ttluxuk79h"), Power: 10, Roles: []string{"admin"}})
52 members = append(members, Member{Address: address("g1747t5m2f08plqjlrjk2q0qld7465hxz8gkx59c"), Power: 8, Roles: []string{"dev"}})
53 members = append(members, Member{Address: address("g1lp8g5p78u9dg6ww4c5e5hdeuw8q9ga056f0adv"), Power: 8, Roles: []string{"member", "finance"}})
54 members = append(members, Member{Address: address("g10sqjf85ljqkvkc3n40dmyrh664ss7sl986v5v7"), Power: 7, Roles: []string{"member", "ops"}})
55 members = append(members, Member{Address: address("g1hykjph3tf3lz74ekspprxcc599dtf7g4hvzkta"), Power: 1, Roles: []string{"member"}})
56 allowedCategories = append(allowedCategories, "governance")
57 allowedCategories = append(allowedCategories, "treasury")
58 allowedCategories = append(allowedCategories, "membership")
59 allowedCategories = append(allowedCategories, "operations")
60 allowedRoles = append(allowedRoles, "admin")
61 allowedRoles = append(allowedRoles, "dev")
62 allowedRoles = append(allowedRoles, "finance")
63 allowedRoles = append(allowedRoles, "ops")
64 allowedRoles = append(allowedRoles, "member")
65}
66
67// ── Queries ───────────────────────────────────────────────
68
69func Render(path string) string {
70 if path == "" {
71 return renderHome()
72 }
73 // Parse proposal ID from path
74 parts := strings.Split(path, "/")
75 if len(parts) >= 1 {
76 id, err := strconv.Atoi(parts[0])
77 if err == nil && id >= 0 && id < len(proposals) {
78 if len(parts) >= 2 && parts[1] == "votes" {
79 return renderVotes(id)
80 }
81 return renderProposal(id)
82 }
83 }
84 return "# Not Found"
85}
86
87func renderHome() string {
88 out := "# " + name + "\n"
89 out += description + "\n\n"
90 out += "Threshold: " + strconv.Itoa(threshold) + "% — Quorum: " + strconv.Itoa(quorum) + "%\n\n"
91 out += "## Members (" + strconv.Itoa(len(members)) + ")\n"
92 for _, m := range members {
93 out += "- " + string(m.Address) + " (roles: " + strings.Join(m.Roles, ", ") + ") — power: " + strconv.Itoa(m.Power) + "\n"
94 }
95 out += "\n## Proposals\n"
96 for i := len(proposals) - 1; i >= 0; i-- {
97 p := proposals[i]
98 out += "### [Prop #" + strconv.Itoa(p.ID) + " - " + p.Title + "](:" + strconv.Itoa(p.ID) + ")\n"
99 out += "Author: " + string(p.Author) + "\n\n"
100 out += "Category: " + p.Category + "\n\n"
101 out += "Status: " + p.Status + "\n\n---\n\n"
102 }
103 if len(proposals) == 0 {
104 out += "No proposals yet.\n"
105 }
106 return out
107}
108
109func renderProposal(id int) string {
110 p := proposals[id]
111 out := "# Prop #" + strconv.Itoa(p.ID) + " - " + p.Title + "\n"
112 out += p.Description + "\n\n"
113 out += "Author: " + string(p.Author) + "\n\n"
114 out += "Category: " + p.Category + "\n\n"
115 out += "Status: " + p.Status + "\n\n"
116 out += "YES: " + strconv.Itoa(p.YesVotes) + " — NO: " + strconv.Itoa(p.NoVotes) + " — ABSTAIN: " + strconv.Itoa(p.Abstain) + "\n"
117 out += "Total Power: " + strconv.Itoa(p.TotalPower) + "/" + strconv.Itoa(totalPower()) + "\n"
118 return out
119}
120
121func renderVotes(id int) string {
122 p := proposals[id]
123 out := "# Proposal #" + strconv.Itoa(p.ID) + " - Vote List\n\n"
124 out += "YES:\n"
125 for _, v := range p.Votes {
126 if v.Value == "YES" {
127 out += "- " + string(v.Voter) + "\n"
128 }
129 }
130 out += "\nNO:\n"
131 for _, v := range p.Votes {
132 if v.Value == "NO" {
133 out += "- " + string(v.Voter) + "\n"
134 }
135 }
136 out += "\nABSTAIN:\n"
137 for _, v := range p.Votes {
138 if v.Value == "ABSTAIN" {
139 out += "- " + string(v.Voter) + "\n"
140 }
141 }
142 return out
143}
144
145// ── Actions ───────────────────────────────────────────────
146
147func Propose(title, desc, category string) int {
148 caller := runtime.OriginCaller()
149 assertMember(caller)
150 assertCategory(category)
151 id := nextID
152 nextID++
153 proposals = append(proposals, Proposal{
154 ID: id,
155 Title: title,
156 Description: desc,
157 Category: category,
158 Author: caller,
159 Status: "ACTIVE",
160 })
161 return id
162}
163
164func VoteOnProposal(id int, vote string) {
165 caller := runtime.OriginCaller()
166 assertMember(caller)
167 if id < 0 || id >= len(proposals) {
168 panic("invalid proposal ID")
169 }
170 p := &proposals[id]
171 if p.Status != "ACTIVE" {
172 panic("proposal is not active")
173 }
174 // Check for duplicate votes
175 for _, v := range p.Votes {
176 if v.Voter == caller {
177 panic("already voted")
178 }
179 }
180 power := getMemberPower(caller)
181 p.Votes = append(p.Votes, Vote{Voter: caller, Value: vote})
182 switch vote {
183 case "YES":
184 p.YesVotes += power
185 case "NO":
186 p.NoVotes += power
187 case "ABSTAIN":
188 p.Abstain += power
189 default:
190 panic("invalid vote: must be YES, NO, or ABSTAIN")
191 }
192 p.TotalPower += power
193 // Check quorum + threshold
194 tpow := totalPower()
195 if tpow > 0 {
196 quorumMet := quorum == 0 || (p.TotalPower * 100 / tpow >= quorum)
197 if quorumMet && p.YesVotes * 100 / tpow >= threshold {
198 p.Status = "ACCEPTED"
199 }
200 if quorumMet && p.NoVotes * 100 / tpow > (100 - threshold) {
201 p.Status = "REJECTED"
202 }
203 }
204}
205
206func ExecuteProposal(id int) {
207 caller := runtime.OriginCaller()
208 assertMember(caller)
209 if id < 0 || id >= len(proposals) {
210 panic("invalid proposal ID")
211 }
212 p := &proposals[id]
213 if p.Status != "ACCEPTED" {
214 panic("proposal must be ACCEPTED to execute")
215 }
216 p.Status = "EXECUTED"
217}
218
219// ── Role Management (admin-only) ──────────────────────────
220
221func AssignRole(target address, role string) {
222 caller := runtime.OriginCaller()
223 assertAdmin(caller)
224 assertRole(role)
225 for i, m := range members {
226 if m.Address == target {
227 // Check role not already assigned
228 for _, r := range m.Roles {
229 if r == role {
230 panic("role already assigned")
231 }
232 }
233 members[i].Roles = append(members[i].Roles, role)
234 return
235 }
236 }
237 panic("target is not a member")
238}
239
240func RemoveRole(target address, role string) {
241 caller := runtime.OriginCaller()
242 assertAdmin(caller)
243 // Prevent removing last admin
244 if role == "admin" {
245 adminCount := 0
246 for _, m := range members {
247 if hasRoleInternal(m, "admin") {
248 adminCount++
249 }
250 }
251 if adminCount <= 1 {
252 panic("cannot remove the last admin")
253 }
254 }
255 for i, m := range members {
256 if m.Address == target {
257 newRoles := []string{}
258 for _, r := range m.Roles {
259 if r != role {
260 newRoles = append(newRoles, r)
261 }
262 }
263 members[i].Roles = newRoles
264 return
265 }
266 }
267 panic("target is not a member")
268}
269
270// ── Helpers ───────────────────────────────────────────────
271
272func assertMember(addr address) {
273 for _, m := range members {
274 if m.Address == addr {
275 return
276 }
277 }
278 panic("not a member")
279}
280
281func assertAdmin(addr address) {
282 for _, m := range members {
283 if m.Address == addr {
284 for _, r := range m.Roles {
285 if r == "admin" {
286 return
287 }
288 }
289 }
290 }
291 panic("admin role required")
292}
293
294func hasRole(addr address, role string) bool {
295 for _, m := range members {
296 if m.Address == addr {
297 return hasRoleInternal(m, role)
298 }
299 }
300 return false
301}
302
303func hasRoleInternal(m Member, role string) bool {
304 for _, r := range m.Roles {
305 if r == role {
306 return true
307 }
308 }
309 return false
310}
311
312func getMemberPower(addr address) int {
313 for _, m := range members {
314 if m.Address == addr {
315 return m.Power
316 }
317 }
318 return 0
319}
320
321func totalPower() int {
322 total := 0
323 for _, m := range members {
324 total += m.Power
325 }
326 return total
327}
328
329func assertCategory(cat string) {
330 for _, c := range allowedCategories {
331 if c == cat {
332 return
333 }
334 }
335 panic("invalid proposal category: " + cat)
336}
337
338func assertRole(role string) {
339 for _, r := range allowedRoles {
340 if r == role {
341 return
342 }
343 }
344 panic("invalid role: " + role)
345}
346
347// ── Config (for Memba integration) ────────────────────────
348
349func GetDAOConfig() string {
350 return name
351}