app.gno
7.14 Kb · 263 lines
1// Package clubpoll is a Remix Lab "club-poll" app generated from a
2// project configuration. Members join by invitation; members vote in
3// single-choice polls whose electorate is frozen when each poll opens.
4//
5// Authority model: every write derives the actor from cur.Previous(), the
6// immediate caller. An intermediary realm calling on a user's behalf acts as
7// itself and gains none of that user's roles. Block state lives in
8// unexported variables and only inert views leave the realm.
9package clubpoll
10
11import (
12 "strings"
13 "time"
14
15 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/ballots/v1"
16 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/core/v1"
17 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/membership/v1"
18 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/policies/v1"
19)
20
21const (
22 pkgPath = "gno.land/r/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/clubpoll"
23 appTitle = "Club poll"
24 recipeID = "club-poll"
25 recipeVersion = "1.0.0"
26 pageSize = 25
27 optionSep = "|"
28)
29
30// Operation names checked against the policy.
31const (
32 opInvite = "members.invite"
33 opRemove = "members.remove"
34 opSetRole = "members.set_role"
35 opCreate = "poll.create"
36 opOpen = "poll.open"
37 opPolicy = "policy.set"
38)
39
40var (
41 members *membership.Set
42 policy *policies.Policy
43 polls *ballots.Box
44)
45
46// init captures the deployer as the first owner. A realm whose deployer is
47// not a user stays uninitialized and rejects every write; there is no
48// separate initialization call that someone else could front-run.
49func init(cur realm) {
50 deployer := cur.Previous()
51 if deployer.IsUser() && deployer.Address().IsValid() {
52 setup(deployer.Address())
53 }
54}
55
56func setup(owner address) {
57 members = membership.New(membership.Config{
58 JoinMode: membership.JoinInvitation,
59 MaxMembers: 100,
60 }, owner)
61
62 policy = policies.New(members)
63 policy.Define(opInvite, policies.RuleOwner, policies.RuleReviewer)
64 policy.Define(opRemove, policies.RuleOwner, policies.RuleOwner)
65 policy.Define(opSetRole, policies.RuleOwner, policies.RuleOwner)
66 policy.Define(opCreate, policies.RuleOwner, policies.RuleReviewer)
67 policy.Define(opOpen, policies.RuleOwner, policies.RuleReviewer)
68 policy.Define(opPolicy, policies.RuleOwner, policies.RuleOwner)
69
70 polls = ballots.NewBox(ballots.Config{
71 MaxBallots: 100,
72 MaxOptions: 20,
73 MaxElectorate: 100,
74 QuorumPercent: 0,
75 AllowAbstain: false,
76 MinDuration: 60,
77 MaxDuration: 2592000,
78 MaxTitleBytes: 140,
79 MaxOptionBytes: 80,
80 })
81}
82
83// --- Membership ---
84
85// Invite lets addr join with role. Inviting a reviewer or owner also
86// requires the members.set_role permission.
87func Invite(cur realm, addr address, role string) {
88 actor := authorize(cur, opInvite)
89 r, err := core.ParseRole(role)
90 must(err)
91 if r > core.RoleMember {
92 must(policy.Authorize(opSetRole, actor))
93 }
94 must(members.Invite(addr, r))
95}
96
97// RevokeInvite cancels a pending invitation. Revoking an invitation for a
98// reviewer or owner also requires the members.set_role permission.
99func RevokeInvite(cur realm, addr address) {
100 actor := authorize(cur, opInvite)
101 if r, ok := members.InviteRole(addr); ok && r > core.RoleMember {
102 must(policy.Authorize(opSetRole, actor))
103 }
104 must(members.RevokeInvite(addr))
105}
106
107// Join accepts the caller's pending invitation.
108func Join(cur realm) {
109 ready()
110 must(members.Join(cur.Previous().Address()))
111}
112
113// Leave removes the caller from the club. Open polls keep their frozen
114// electorate.
115func Leave(cur realm) {
116 ready()
117 must(members.Leave(cur.Previous().Address()))
118}
119
120// RemoveMember removes addr.
121func RemoveMember(cur realm, addr address) {
122 authorize(cur, opRemove)
123 must(members.Remove(addr))
124}
125
126// SetRole changes addr's role.
127func SetRole(cur realm, addr address, role string) {
128 authorize(cur, opSetRole)
129 r, err := core.ParseRole(role)
130 must(err)
131 must(members.SetRole(addr, r))
132}
133
134// --- Polls ---
135
136// CreatePoll creates a draft poll. options are separated by "|".
137func CreatePoll(cur realm, title string, options string, durationSeconds int64) uint64 {
138 actor := authorize(cur, opCreate)
139 id, err := polls.Create(actor, title, strings.Split(options, optionSep), durationSeconds)
140 must(err)
141 return id
142}
143
144// CancelPoll deletes a draft poll that was never opened, freeing its slot.
145// The poll's creator may cancel it, and so may anyone allowed to open polls.
146func CancelPoll(cur realm, id uint64) {
147 ready()
148 actor := cur.Previous().Address()
149 if creator, ok := polls.Creator(id); ok && creator == actor {
150 must(policy.Authorize(opCreate, actor))
151 } else {
152 must(policy.Authorize(opOpen, actor))
153 }
154 must(polls.CancelDraft(id))
155}
156
157// OpenPoll freezes the poll's options and captures the current members as
158// its electorate.
159func OpenPoll(cur realm, id uint64) {
160 authorize(cur, opOpen)
161 electorate, ok := members.Snapshot(core.RoleMember, polls.Config().MaxElectorate)
162 if !ok {
163 panic(core.Errorf(core.ErrLimitExceeded, "electorate too large").Error())
164 }
165 must(polls.OpenWithElectorate(id, time.Now().Unix(), electorate))
166}
167
168// Vote casts the caller's vote for option index choice.
169func Vote(cur realm, id uint64, choice int) {
170 ready()
171 must(polls.Vote(id, cur.Previous().Address(), choice, time.Now().Unix()))
172}
173
174// Finalize commits a poll's result once its deadline has passed. Anyone may
175// call it; the result depends only on rules frozen at opening.
176func Finalize(cur realm, id uint64) {
177 ready()
178 _, err := polls.Finalize(id, time.Now().Unix())
179 must(err)
180}
181
182// --- Policy ---
183
184// SetRule changes the rule for an operation, within that operation's floor.
185func SetRule(cur realm, op string, rule string) {
186 authorize(cur, opPolicy)
187 r, err := policies.ParseRule(rule)
188 must(err)
189 must(policy.Set(op, r))
190}
191
192// --- Reads ---
193
194// Poll returns an inert summary of poll id.
195func Poll(id uint64) (core.BallotView, bool) {
196 ready()
197 return polls.Ballot(id)
198}
199
200// Polls pages through polls, newest first.
201func Polls(offset, limit int) []core.BallotView {
202 ready()
203 return polls.List(offset, clampLimit(limit))
204}
205
206// Members pages through members ordered by address.
207func Members(offset, limit int) []core.MemberView {
208 ready()
209 return members.List(offset, clampLimit(limit))
210}
211
212// Invites returns up to limit pending invitations (at most 25) starting at
213// offset, with the role each one grants.
214func Invites(offset, limit int) []core.MemberView {
215 ready()
216 return members.Invites(offset, clampLimit(limit))
217}
218
219// RoleOf returns addr's role name.
220func RoleOf(addr address) string {
221 ready()
222 return members.RoleOf(addr).String()
223}
224
225// HasVoted reports whether addr voted in poll id.
226func HasVoted(id uint64, addr address) bool {
227 ready()
228 return polls.HasVoted(id, addr)
229}
230
231// Rules lists every operation's current rule.
232func Rules() []policies.RuleView {
233 ready()
234 return policy.List()
235}
236
237// --- Internals ---
238
239func authorize(cur realm, op string) address {
240 ready()
241 actor := cur.Previous().Address()
242 must(policy.Authorize(op, actor))
243 return actor
244}
245
246func ready() {
247 if members == nil {
248 panic(core.Errorf(core.ErrInvalidState, "app is not initialized").Error())
249 }
250}
251
252func clampLimit(limit int) int {
253 if limit < 1 || limit > pageSize {
254 return pageSize
255 }
256 return limit
257}
258
259func must(err error) {
260 if err != nil {
261 panic(err.Error())
262 }
263}