kudos.gno
2.67 Kb · 115 lines
1package kudos
2
3import (
4 "chain"
5 "chain/runtime/unsafe"
6 "sort"
7 "strconv"
8 "strings"
9
10 "gno.land/p/nt/avl/v0"
11)
12
13// counts maps an address (as string) -> number of kudos received.
14var counts avl.Tree
15
16// feed holds recent kudos, newest first, capped at feedCap entries.
17var feed []kudo
18
19const feedCap = 50
20
21type kudo struct {
22 from address
23 to address
24 reason string
25}
26
27// Give records a kudos from the caller to `to` with a reason.
28// A caller cannot give kudos to themselves.
29func Give(cur realm, to address, reason string) {
30 from := unsafe.PreviousRealm().Address()
31 if !to.IsValid() {
32 panic("invalid recipient address")
33 }
34 if from == to {
35 panic("cannot give kudos to yourself")
36 }
37 reason = strings.TrimSpace(reason)
38 if reason == "" {
39 panic("reason must not be empty")
40 }
41
42 key := to.String()
43 n := int64(0)
44 if v, ok := counts.Get(key); ok {
45 n = v.(int64)
46 }
47 counts.Set(key, n+1)
48
49 // prepend newest, cap the feed
50 feed = append([]kudo{{from: from, to: to, reason: reason}}, feed...)
51 if len(feed) > feedCap {
52 feed = feed[:feedCap]
53 }
54
55 chain.Emit("KudosGiven", "from", from.String(), "to", to.String(), "reason", reason)
56}
57
58// entry is a leaderboard row used for deterministic sorting.
59type entry struct {
60 addr string
61 count int64
62}
63
64// byScore sorts by count desc, then address asc for stable ties.
65type byScore []entry
66
67func (s byScore) Len() int { return len(s) }
68func (s byScore) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
69func (s byScore) Less(i, j int) bool {
70 if s[i].count != s[j].count {
71 return s[i].count > s[j].count
72 }
73 return s[i].addr < s[j].addr
74}
75
76func leaderboard() []entry {
77 out := make([]entry, 0, counts.Size())
78 counts.Iterate("", "", func(key string, value any) bool {
79 out = append(out, entry{addr: key, count: value.(int64)})
80 return false
81 })
82 sort.Sort(byScore(out))
83 return out
84}
85
86// Render shows a leaderboard of most-received addresses and a recent feed.
87func Render(path string) string {
88 var b strings.Builder
89 b.WriteString("# Kudos\n\n")
90 b.WriteString("Give props to addresses. `Give(to, reason)` records a kudos.\n\n")
91
92 b.WriteString("## Leaderboard\n\n")
93 lb := leaderboard()
94 if len(lb) == 0 {
95 b.WriteString("_No kudos yet._\n\n")
96 } else {
97 b.WriteString("| # | Address | Kudos |\n")
98 b.WriteString("| --- | --- | --- |\n")
99 for i, e := range lb {
100 b.WriteString("| " + strconv.Itoa(i+1) + " | " + e.addr + " | " + strconv.FormatInt(e.count, 10) + " |\n")
101 }
102 b.WriteString("\n")
103 }
104
105 b.WriteString("## Recent kudos\n\n")
106 if len(feed) == 0 {
107 b.WriteString("_Nothing here yet._\n")
108 } else {
109 for _, k := range feed {
110 b.WriteString("- " + k.from.String() + " → " + k.to.String() + ": " + k.reason + "\n")
111 }
112 }
113
114 return b.String()
115}