Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}