Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}