Realm
Gno.land Mainnet
gno.land/r/samcrew/memba_bridge_v1
Overview
Network
gnoland-1
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}
429Raw Package Data
Raw JSON data