← Back to Realms

Realm

gno.land/r/samcrew/memba_bridge_v1

Overview

Realm Path
gno.land/r/samcrew/memba_bridge_v1
Exported Functions
28
State Entries
22
Source Files
4
Total Package Entries
87

Exported Functions

28 exported functions

State

22 state entries

Source Code

FILES
apps.gno
go
1package memba_bridge_v1
2
3import (
4	"chain"
5	"chain/banker"
6	"crypto/sha256"
7	"encoding/hex"
8	"strconv"
9	"strings"
10
11	"gno.land/p/samcrew/daoauth"
12	gov "gno.land/r/samcrew/memba_gov"
13	escrow "gno.land/r/samcrew/escrow_v4"
14	badges "gno.land/r/samcrew/gnobuilders_badges_v2"
15	appstore "gno.land/r/samcrew/memba_appstore_v3"
16	arcade "gno.land/r/samcrew/memba_arcade_leaderboard_v1"
17	channels "gno.land/r/samcrew/memba_dao_channels_v2"
18	feed "gno.land/r/samcrew/memba_feed_v1"
19	feedback "gno.land/r/samcrew/memba_feedback_v2"
20	market "gno.land/r/samcrew/memba_market_config"
21	quest "gno.land/r/samcrew/memba_quest_attestation_v1"
22	reviews "gno.land/r/samcrew/memba_reviews_v2"
23)
24
25// ── Roles: App Store curators, arcade attesters, badge admins, feed
26// moderators. Operational work (curating, attesting, minting, moderating)
27// is done by the holders the DAO appoints here.
28
29func Grant(cur realm, pid uint64, app string, who address) { role(cur, pid, app, who, true) }
30
31func Revoke(cur realm, pid uint64, app string, who address) { role(cur, pid, app, who, false) }
32
33func role(cur realm, pid uint64, app string, who address, grant bool) {
34	approve(cur, pid, rolePlan(app, who, grant))
35	switch {
36	case app == appAppstore && grant:
37		appstore.AddCurator(cross(cur), who)
38	case app == appAppstore:
39		appstore.RemoveCurator(cross(cur), who)
40	case app == appArcade && grant:
41		arcade.AddAttester(cross(cur), who)
42	case app == appArcade:
43		arcade.RemoveAttester(cross(cur), who)
44	case app == appBadges && grant:
45		badges.AddAdmin(cross(cur), who)
46	case app == appBadges:
47		badges.RemoveAdmin(cross(cur), who)
48	case app == appFeed && grant:
49		feed.AddModerator(cross(cur), who)
50	default:
51		feed.RemoveModerator(cross(cur), who)
52	}
53}
54
55func rolePlan(app string, who address, grant bool) plan {
56	if grant {
57		arg(who)
58	} else if canonical(who) == self {
59		// Revoking its own curator grant would end DAO curation until a hand-over round trip.
60		panic("memba_bridge: the bridge keeps its own role")
61	}
62	// A stale grant held by another protected realm can be revoked.
63	if hasRole(app, who) == grant {
64		panic("memba_bridge: role already in that state")
65	}
66	op := "Revoke"
67	if grant {
68		op = "Grant"
69	}
70	args := daoauth.New().Addr(who)
71	if app == appBadges {
72		args.Int(int64(badges.GetAdminCount()))
73	}
74	return newPlan(app, op, args, app, gov.Critical)
75}
76
77func hasRole(app string, who address) bool {
78	switch app {
79	case appAppstore:
80		return appstore.IsCurator(string(who))
81	case appArcade:
82		return arcade.IsAttester(string(who))
83	case appBadges:
84		return badges.IsAdmin(who)
85	case appFeed:
86		return feed.IsModerator(who)
87	}
88	panic("memba_bridge: " + app + " has no role to grant")
89}
90
91// ── App Store ───────────────────────────────────────────────────────────
92
93// Curate runs a curator action on one listing: approve, reject (with a
94// reason), delist, restore or clearflags. Every approval binds the listing's
95// status; approve and restore, which publish, also bind its content, and
96// clearflags binds the flags it clears. Editing or flagging a listing thus
97// cannot void a reject or delist vote, and cannot slip new content past one.
98func Curate(cur realm, pid uint64, op, pkgPath, reason string) {
99	approve(cur, pid, curatePlan(op, pkgPath, reason))
100	switch op {
101	case "approve":
102		appstore.ApproveApp(cross(cur), pkgPath)
103	case "reject":
104		appstore.RejectApp(cross(cur), pkgPath, reason)
105	case "delist":
106		appstore.DelistApp(cross(cur), pkgPath)
107	case "restore":
108		appstore.RestoreApp(cross(cur), pkgPath)
109	default:
110		appstore.ClearFlags(cross(cur), pkgPath)
111	}
112}
113
114func curatePlan(op, pkgPath, reason string) plan {
115	switch {
116	case op != "approve" && op != "reject" && op != "delist" && op != "restore" && op != "clearflags":
117		panic("memba_bridge: unknown curation " + op)
118	case (op == "reject") != (reason != ""):
119		panic("memba_bridge: a reason goes with reject only")
120	}
121	l := appstore.GetGovernanceListing(pkgPath, op == "clearflags")
122	if !l.Exists {
123		panic("memba_bridge: no listing " + pkgPath)
124	}
125	args := daoauth.New().Str(op).Str(pkgPath).Str(reason).Str(l.Status)
126	if op == "approve" || op == "restore" {
127		args.Str(l.ContentHash)
128	}
129	if op == "clearflags" {
130		keys := sha256.Sum256([]byte(l.ClearKeys))
131		args.Int(int64(l.Flags)).Str(hex.EncodeToString(keys[:]))
132	}
133	return newPlan(appAppstore, "Curate", args, appAppstore+"/l/"+pkgPath, gov.Routine)
134}
135
136func SetRegistrationFee(cur realm, pid uint64, fee int64) {
137	approve(cur, pid, registrationFeePlan(fee))
138	appstore.SetRegistrationFee(cross(cur), fee)
139}
140
141func registrationFeePlan(fee int64) plan {
142	if fee < 0 || fee > appstore.MaxRegistrationFee {
143		panic("memba_bridge: the registration fee is 0 to " + strconv.FormatInt(appstore.MaxRegistrationFee, 10) + " ugnot")
144	}
145	return newPlan(appAppstore, "SetRegistrationFee", daoauth.New().Int(fee).Int(appstore.GetRegistrationFee()), appAppstore, gov.Financial)
146}
147
148// SetTreasury points the fees of market_config (which escrow also pays
149// into) or of the App Store at addr.
150func SetTreasury(cur realm, pid uint64, app string, addr address) {
151	approve(cur, pid, treasuryPlan(app, addr))
152	if app == appMarket {
153		market.SetTreasury(cross(cur), addr)
154	} else {
155		appstore.SetTreasury(cross(cur), addr)
156	}
157}
158
159func treasuryPlan(app string, addr address) plan {
160	var current address
161	switch app {
162	case appMarket:
163		current = market.GetTreasury()
164	case appAppstore:
165		current = appstore.GetTreasury()
166	default:
167		panic("memba_bridge: " + app + " has no treasury")
168	}
169	return newPlan(app, "SetTreasury", daoauth.New().Addr(arg(addr)).Str(string(current)), app, gov.Critical)
170}
171
172// ── Market config ───────────────────────────────────────────────────────
173
174func SetFee(cur realm, pid uint64, lane string, bps int64) {
175	approve(cur, pid, feePlan(lane, bps))
176	market.SetFeeBPS(cross(cur), lane, bps)
177}
178
179func feePlan(lane string, bps int64) plan {
180	if lane == "" || bps < 0 || bps > market.MaxFeeBPS {
181		panic("memba_bridge: a fee names its lane and is 0 to " + strconv.FormatInt(market.MaxFeeBPS, 10) + " bps")
182	}
183	return newPlan(appMarket, "SetFee", daoauth.New().Str(lane).Int(bps).Int(int64(market.GetFeeBPS(lane))), appMarket, gov.Financial)
184}
185
186// ── Escrow ──────────────────────────────────────────────────────────────
187
188// ResolveDispute settles one disputed milestone: refund pays the client
189// the whole amount, otherwise the freelancer is paid less the fee. Either
190// way escrow pays out at most the milestone's amount, which is checked. A
191// milestone is disputed at most once (a resolution is final); binding the
192// height of that dispute means no approval can be voted before it exists.
193func ResolveDispute(cur realm, pid uint64, contractID string, milestone int, refund bool) {
194	p, amount := disputePlan(contractID, milestone, refund)
195	approve(cur, pid, p)
196	before := escrowBalance()
197	escrow.ResolveDispute(cross(cur), contractID, milestone, refund)
198	if paid := before - escrowBalance(); paid < 0 || paid > amount {
199		panic("memba_bridge: escrow paid out more than the milestone")
200	}
201}
202
203// disputePlan returns the approval and the milestone's amount.
204func disputePlan(contractID string, milestone int, refund bool) (plan, int64) {
205	c := escrow.GetGovernanceContract(contractID)
206	if !c.Exists || milestone < 0 || milestone >= c.Count {
207		panic("memba_bridge: no such contract milestone")
208	}
209	m := c.Milestones[milestone]
210	if m.Status != "disputed" {
211		panic("memba_bridge: milestone is " + m.Status + ", not disputed")
212	}
213	args := daoauth.New().Str(contractID).Int(int64(milestone)).Bool(refund).
214		Str(m.Status).Str(m.PreDisputeStatus).Int(m.Amount).Int(m.DisputedAt)
215	if !refund {
216		terms := escrow.GetGovernanceFeeTerms()
217		args.Int(terms.EffectiveBPS).Str(string(terms.EffectiveTreasury))
218	}
219	return newPlan(appEscrow, "ResolveDispute", args, appEscrow+"/c/"+contractID+"/m/"+strconv.Itoa(milestone), gov.Financial), m.Amount
220}
221
222func escrowBalance() int64 {
223	// One denom only: junk denoms sent to escrow cost nothing to read past.
224	return banker.NewReadonlyBanker().GetCoin(chain.PackageAddress("gno.land/r/samcrew/"+appEscrow), "ugnot")
225}
226
227// ProposeFeeRecipient stages escrow's fallback fee recipient, used only when
228// market_config's treasury is unusable.
229func ProposeFeeRecipient(cur realm, pid uint64, addr address) {
230	approve(cur, pid, feeRecipientPlan(addr))
231	escrow.ProposeFeeRecipient(cross(cur), addr)
232}
233
234func feeRecipientPlan(addr address) plan {
235	return newPlan(appEscrow, "ProposeFeeRecipient", daoauth.New().Addr(arg(addr)).
236		Str(escrow.GetFeeRecipient()).Str(escrow.GetPendingFeeRecipient()), appEscrow, gov.Critical)
237}
238
239func CancelFeeRecipient(cur realm, pid uint64) {
240	approve(cur, pid, cancelFeeRecipientPlan())
241	escrow.CancelFeeRecipientProposal(cross(cur))
242}
243
244func cancelFeeRecipientPlan() plan {
245	pending := escrow.GetPendingFeeRecipient()
246	if pending == "" {
247		panic("memba_bridge: no fee recipient staged")
248	}
249	return newPlan(appEscrow, "CancelFeeRecipient", daoauth.New().Str(pending), appEscrow, gov.Financial)
250}
251
252// ── Reviews ─────────────────────────────────────────────────────────────
253
254func HideReview(cur realm, pid uint64, id uint64)  { moderate(cur, pid, "HideReview", id) }
255func HideComment(cur realm, pid uint64, id uint64) { moderate(cur, pid, "HideComment", id) }
256
257// Unhide shows an item again and dismisses its flags.
258func Unhide(cur realm, pid uint64, id uint64) { moderate(cur, pid, "Unhide", id) }
259
260// moderate binds the item's kind and visibility; Unhide, which shows the
261// item again, also binds its content, so an author editing it can neither
262// void a hide vote nor slip new text past an unhide vote.
263func moderate(cur realm, pid uint64, op string, id uint64) {
264	approve(cur, pid, moderationPlan(op, id))
265	switch op {
266	case "HideReview":
267		reviews.HideReview(cross(cur), id)
268	case "HideComment":
269		reviews.HideComment(cross(cur), id)
270	default:
271		reviews.Unhide(cross(cur), id)
272	}
273}
274
275func moderationPlan(op string, id uint64) plan {
276	s := reviews.GetModerationState(id)
277	switch {
278	case s.ID == 0:
279		// Ids are predictable: an approval for an item not posted yet would hide whatever takes the id.
280		panic("memba_bridge: no review or comment " + strconv.FormatUint(id, 10))
281	case op == "HideReview" && !s.Review:
282		panic("memba_bridge: item " + strconv.FormatUint(id, 10) + " is a comment, not a review")
283	case op == "HideComment" && s.Review:
284		panic("memba_bridge: item " + strconv.FormatUint(id, 10) + " is a review, not a comment")
285	case op != "Unhide" && (s.Hidden || s.Deleted):
286		panic("memba_bridge: already hidden or deleted")
287	case op == "Unhide" && !s.Hidden && !s.Flagged:
288		panic("memba_bridge: neither hidden nor flagged")
289	}
290	// The author and creation time pin the item itself.
291	args := daoauth.New().Uint(id).Bool(s.Review).Bool(s.Hidden).Bool(s.Deleted).Addr(s.Author).Int(s.CreatedAt)
292	if op == "Unhide" {
293		// The text is bound by its hash and edit height. Unhide also dismisses
294		// flags; the flagged bit is bound, not a count (reviews_v2 exposes none),
295		// so a flag added after the vote to an item already flagged is
296		// dismissed with the others.
297		args.Str(s.BodyHash).Int(s.EditedAt).Bool(s.Flagged)
298	}
299	return newPlan(appReviews, op, args, appReviews+"/i/"+strconv.FormatUint(id, 10), gov.Routine)
300}
301
302// ── Quests ──────────────────────────────────────────────────────────────
303
304// SetQuestSigner rotates the key every quest voucher is checked against.
305func SetQuestSigner(cur realm, pid uint64, pubKeyHex string) {
306	approve(cur, pid, signerPlan(pubKeyHex))
307	quest.SetSigner(cross(cur), pubKeyHex)
308}
309
310func signerPlan(pubKeyHex string) plan {
311	if len(pubKeyHex) != 64 || strings.Trim(pubKeyHex, "0123456789abcdef") != "" || pubKeyHex == quest.GetSigner() {
312		panic("memba_bridge: a new signer is 64 lowercase hex digits")
313	}
314	return newPlan(appQuest, "SetSigner", daoauth.New().Str(pubKeyHex).Str(quest.GetSigner()), appQuest, gov.Critical)
315}
316
317// ── Channels and feedback membership ────────────────────────────────────
318
319// AddMember adds a new member; it never rewrites an existing member's roles.
320func AddMember(cur realm, pid uint64, app string, addr address, roles string) {
321	member(cur, pid, app, "AddMember", addr, roles)
322}
323
324func RemoveMember(cur realm, pid uint64, app string, addr address) {
325	member(cur, pid, app, "RemoveMember", addr, "")
326}
327
328func SetRoles(cur realm, pid uint64, app string, addr address, roles string) {
329	member(cur, pid, app, "SetRoles", addr, roles)
330}
331
332func member(cur realm, pid uint64, app, op string, who address, roles string) {
333	approve(cur, pid, memberPlan(app, op, who, roles))
334	switch {
335	case app == appChannels && op == "AddMember":
336		channels.AddMember(cross(cur), who, roles)
337	case app == appChannels && op == "RemoveMember":
338		channels.RemoveMember(cross(cur), who)
339	case app == appChannels:
340		channels.UpdateMemberRoles(cross(cur), who, roles)
341	case op == "AddMember":
342		feedback.AddMember(cross(cur), who, roles)
343	case op == "RemoveMember":
344		feedback.RemoveMember(cross(cur), who)
345	default:
346		feedback.UpdateMemberRoles(cross(cur), who, roles)
347	}
348}
349
350func memberPlan(app, op string, who address, roles string) plan {
351	if op == "RemoveMember" {
352		canonical(who) // a stale membership of a protected realm can be removed
353	} else {
354		arg(who)
355	}
356	var revision uint64
357	var current string
358	switch app {
359	case appChannels:
360		revision, current = channels.GetMembershipRevision(), channels.GetMemberRoles(who)
361	case appFeedback:
362		revision, current = feedback.GetMembershipRevision(), feedback.GetMemberRoles(who)
363	default:
364		panic("memba_bridge: " + app + " has no members")
365	}
366	if (op == "AddMember") != (current == "") || (op != "RemoveMember" && !validRoles(roles)) {
367		panic("memba_bridge: add a non-member, change a member, roles an ordered subset of admin,dev,ops,member")
368	}
369	return newPlan(app, op, daoauth.New().Addr(who).Str(roles).Uint(revision).Str(current), app, gov.Critical)
370}
371
372// validRoles accepts a non-empty, ordered, duplicate-free subset of
373// admin,dev,ops,member.
374func validRoles(r string) bool {
375	names := []string{"admin", "dev", "ops", "member"}
376	next := 0
377	for _, x := range strings.Split(r, ",") {
378		for next < len(names) && names[next] != x {
379			next++
380		}
381		if next == len(names) {
382			return false
383		}
384		next++
385	}
386	return true
387}
388
389// CreateChannel adds a channel. Channels are permanent (at most 20).
390func CreateChannel(cur realm, pid uint64, app, name, description, ctype string) {
391	approve(cur, pid, channelPlan(app, name, description, ctype))
392	if app == appChannels {
393		channels.CreateChannel(cross(cur), name, description, ctype)
394	} else {
395		feedback.CreateChannel(cross(cur), name, description, ctype)
396	}
397}
398
399func channelPlan(app, name, description, ctype string) plan {
400	if ctype != "text" && ctype != "announcements" && ctype != "readonly" || description == "" || len(description) > 200 || !daoauth.ValidText(description) {
401		panic("memba_bridge: type text, announcements or readonly; description 1-200 printable ASCII characters")
402	}
403	var count int
404	var exists bool
405	switch app {
406	case appChannels:
407		count, exists, _, _, _, _ = channels.GetChannelState(name)
408	case appFeedback:
409		count, exists, _, _, _, _ = feedback.GetChannelState(name)
410	default:
411		panic("memba_bridge: " + app + " has no channels")
412	}
413	if exists {
414		panic("memba_bridge: channel exists")
415	}
416	if !channelName(name) || count >= 20 {
417		panic("memba_bridge: a channel name is 1-50 of a-z, 0-9 or -, and an app holds at most 20 channels")
418	}
419	return newPlan(app, "CreateChannel", daoauth.New().Str(name).Str(description).Str(ctype).Int(int64(count)), app, gov.Critical)
420}
421
422// channelName is the apps' own channel-name rule.
423func channelName(name string) bool {
424	if name == "" || len(name) > 50 {
425		return false
426	}
427	return strings.Trim(name, "abcdefghijklmnopqrstuvwxyz0123456789-") == ""
428}
429

Raw Package Data

Raw JSON data