sharedpoll.gno
5.62 Kb · 206 lines
1// Package sharedpoll is the Remix Lab shared-membership composition demo: a
2// poll app whose electorate comes from an existing shared membership realm
3// instead of its own member list.
4//
5// The shared realm is consulted read-only, when a poll opens, to snapshot
6// its members. People join or leave through the shared realm's own
7// interface. This app's administrators (who may create and open polls) are
8// a separate, local set seeded with the deployer.
9package sharedpoll
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/sharedpoll"
23 recipeID = "shared-poll-demo"
24 recipeVersion = "1.0.0"
25 pageSize = 25
26 optionSep = "|"
27 configDigest = "unset"
28)
29
30const (
31 opAdmins = "admins.manage"
32 opCreate = "poll.create"
33 opOpen = "poll.open"
34 opPolicy = "policy.set"
35)
36
37var (
38 admins *membership.Set
39 policy *policies.Policy
40 polls *ballots.Box
41)
42
43func init(cur realm) {
44 deployer := cur.Previous()
45 if deployer.IsUser() && deployer.Address().IsValid() {
46 setup(deployer.Address())
47 }
48}
49
50func setup(owner address) {
51 admins = membership.New(membership.Config{
52 JoinMode: membership.JoinInvitation,
53 MaxMembers: 10,
54 }, owner)
55 policy = policies.New(admins)
56 policy.Define(opAdmins, policies.RuleOwner, policies.RuleOwner)
57 policy.Define(opCreate, policies.RuleOwner, policies.RuleReviewer)
58 policy.Define(opOpen, policies.RuleOwner, policies.RuleReviewer)
59 policy.Define(opPolicy, policies.RuleOwner, policies.RuleOwner)
60 polls = ballots.NewBox(ballots.Config{
61 MaxBallots: 100,
62 MaxOptions: 20,
63 MaxElectorate: 100,
64 QuorumPercent: 0,
65 AllowAbstain: false,
66 MinDuration: 60,
67 MaxDuration: 2592000,
68 MaxTitleBytes: 140,
69 MaxOptionBytes: 80,
70 })
71}
72
73// Descriptor identifies this app's recipe, components and bindings.
74func Descriptor() core.AppDescriptor {
75 return core.AppDescriptor{
76 Recipe: recipeID,
77 RecipeVersion: recipeVersion,
78 Components: []string{
79 "[email protected]",
80 "[email protected]",
81 "[email protected]",
82 "[email protected]",
83 "[email protected]",
84 "[email protected]",
85 },
86 ConfigDigest: configDigest,
87 ViewSchemaVersion: core.ViewSchemaVersion,
88 Bindings: []string{"membership.read/v1=" + sharedPath},
89 }
90}
91
92// AddAdmin grants addr a local administrative role ("reviewer" or "owner").
93func AddAdmin(cur realm, addr address, role string) {
94 authorize(cur, opAdmins)
95 r, err := core.ParseRole(role)
96 must(err)
97 if r != core.RoleReviewer && r != core.RoleOwner {
98 panic(core.Errorf(core.ErrInvalidInput, "admins are reviewers or owners").Error())
99 }
100 must(admins.Add(addr, r))
101}
102
103// RemoveAdmin removes a local administrator.
104func RemoveAdmin(cur realm, addr address) {
105 authorize(cur, opAdmins)
106 must(admins.Remove(addr))
107}
108
109// CreatePoll creates a draft poll. options are separated by "|".
110func CreatePoll(cur realm, title string, options string, durationSeconds int64) uint64 {
111 actor := authorize(cur, opCreate)
112 id, err := polls.Create(actor, title, strings.Split(options, optionSep), durationSeconds)
113 must(err)
114 return id
115}
116
117// CancelPoll deletes a draft poll that was never opened. The poll's creator
118// may cancel it, and so may anyone allowed to open polls.
119func CancelPoll(cur realm, id uint64) {
120 ready()
121 actor := cur.Previous().Address()
122 if creator, ok := polls.Creator(id); ok && creator == actor {
123 must(policy.Authorize(opCreate, actor))
124 } else {
125 must(policy.Authorize(opOpen, actor))
126 }
127 must(polls.CancelDraft(id))
128}
129
130// OpenPoll snapshots the shared realm's current members as the electorate.
131func OpenPoll(cur realm, id uint64) {
132 authorize(cur, opOpen)
133 voters, ok := electorate.Snapshot(core.RoleMember, polls.Config().MaxElectorate)
134 if !ok {
135 panic(core.Errorf(core.ErrLimitExceeded, "electorate too large").Error())
136 }
137 must(polls.OpenWithElectorate(id, time.Now().Unix(), voters))
138}
139
140// Vote casts the caller's vote for option index choice.
141func Vote(cur realm, id uint64, choice int) {
142 ready()
143 must(polls.Vote(id, cur.Previous().Address(), choice, time.Now().Unix()))
144}
145
146// Finalize commits a poll's result once its deadline has passed.
147func Finalize(cur realm, id uint64) {
148 ready()
149 _, err := polls.Finalize(id, time.Now().Unix())
150 must(err)
151}
152
153// SetRule changes the rule for an operation, within its floor.
154func SetRule(cur realm, op string, rule string) {
155 authorize(cur, opPolicy)
156 r, err := policies.ParseRule(rule)
157 must(err)
158 must(policy.Set(op, r))
159}
160
161// Poll returns an inert summary of poll id.
162func Poll(id uint64) (core.BallotView, bool) {
163 ready()
164 return polls.Ballot(id)
165}
166
167// Polls pages through polls, newest first.
168func Polls(offset, limit int) []core.BallotView {
169 ready()
170 if limit < 1 || limit > pageSize {
171 limit = pageSize
172 }
173 return polls.List(offset, limit)
174}
175
176// AdminRole returns addr's local administrative role name ("none" when
177// addr is not an administrator).
178func AdminRole(addr address) string {
179 ready()
180 return admins.RoleOf(addr).String()
181}
182
183// HasVoted reports whether addr voted in poll id.
184func HasVoted(id uint64, addr address) bool {
185 ready()
186 return polls.HasVoted(id, addr)
187}
188
189func authorize(cur realm, op string) address {
190 ready()
191 actor := cur.Previous().Address()
192 must(policy.Authorize(op, actor))
193 return actor
194}
195
196func ready() {
197 if admins == nil {
198 panic(core.Errorf(core.ErrInvalidState, "app is not initialized").Error())
199 }
200}
201
202func must(err error) {
203 if err != nil {
204 panic(err.Error())
205 }
206}