ranks.gno
3.99 Kb · 136 lines
1package memba_points_v1
2
3import (
4 "strings"
5
6 "gno.land/p/nt/avl/v0"
7 "gno.land/p/nt/ufmt/v0"
8)
9
10// On-chain leaderboard: a secondary index over accounts, keyed so the tree orders by points.
11// Key = zeroPad(points,19) + ":" + addr → ascending avl order is points-ascending; ReverseIterate
12// yields points-DESCENDING (leaders first). Value = the addr string — points are recovered from the
13// key, so we never store the same *Account pointer in two trees. An account is in `ranks` iff points>0.
14var ranks = avl.NewTree() // rankKey -> addr string
15
16const (
17 // MaxTopN bounds a single TopN/TopNPage response (size + node CPU).
18 MaxTopN = 200
19 // defaultTopN is used when a caller passes count <= 0.
20 defaultTopN = 50
21)
22
23// rankKey orders ascending by points (19-digit zero-pad — maxInt64 is 19 digits), addr for uniqueness.
24func rankKey(points int64, addr string) string { return zeroPad19(points) + ":" + addr }
25
26func zeroPad19(n int64) string {
27 s := itoa64(n)
28 for len(s) < 19 {
29 s = "0" + s
30 }
31 return s
32}
33
34// rankApply maintains `ranks` from Award/Revoke ONLY. old/new are the pre/post points for addr.
35// Remove the stale key, insert the new one; an account dropping to 0 is evicted (never re-inserted).
36func rankApply(addr string, old, new int64) {
37 if old > 0 {
38 ranks.Remove(rankKey(old, addr))
39 }
40 if new > 0 {
41 ranks.Set(rankKey(new, addr), addr)
42 }
43}
44
45// splitRankKey recovers (points, addr) from a stored key.
46func splitRankKey(key string) (int64, string) {
47 i := strings.Index(key, ":")
48 if i < 0 {
49 return 0, key
50 }
51 pts, _ := parseNonNegInt(key[:i])
52 return pts, key[i+1:]
53}
54
55// TopN returns up to min(n, MaxTopN) leaders (points-descending) as JSON:
56// [{"rank":1,"addr":"g1..","points":123,"tier":"Gold"},...]
57func TopN(n int) string { return topNFrom(0, n) }
58
59// TopNPage returns a page of the leaderboard from offset (0-based), up to min(count, MaxTopN).
60func TopNPage(offset, count int) string { return topNFrom(offset, count) }
61
62func topNFrom(offset, count int) string {
63 if count <= 0 {
64 count = defaultTopN
65 }
66 if count > MaxTopN {
67 count = MaxTopN
68 }
69 if offset < 0 {
70 offset = 0
71 }
72 var sb strings.Builder
73 sb.WriteString("[")
74 first := true
75 rank := offset + 1
76 ranks.ReverseIterateByOffset(offset, count, func(key string, _ any) bool {
77 pts, addr := splitRankKey(key)
78 if !first {
79 sb.WriteString(",")
80 }
81 first = false
82 sb.WriteString(ufmt.Sprintf(`{"rank":%d,"addr":"%s","points":%d,"tier":"%s"}`,
83 rank, jsonEscape(addr), pts, jsonEscape(tierFor(pts))))
84 rank++
85 return false
86 })
87 sb.WriteString("]")
88 return sb.String()
89}
90
91// rankAndHolders returns addr's 1-based rank (0 if it holds none) and the total holder count.
92// Exact O(log^2 n): binary-search the ascending index via GetByIndex, then invert to a descending rank.
93func rankAndHolders(addr string, pts int64) (int, int) {
94 holders := ranks.Size()
95 rank := 0
96 if pts > 0 {
97 if idx := rankIndexOf(rankKey(pts, addr)); idx >= 0 {
98 rank = holders - idx // ascending idx (0=lowest) → descending rank (idx=size-1 → rank 1)
99 }
100 }
101 return rank, holders
102}
103
104// rankIndexOf returns the ascending index of an exact key in `ranks`, or -1 if absent.
105func rankIndexOf(key string) int {
106 lo, hi := 0, ranks.Size()-1
107 for lo <= hi {
108 mid := (lo + hi) / 2
109 k, _ := ranks.GetByIndex(mid)
110 if k == key {
111 return mid
112 }
113 if k < key {
114 lo = mid + 1
115 } else {
116 hi = mid - 1
117 }
118 }
119 return -1
120}
121
122// RankOf returns {"addr","points","rank","holders"}; rank/points are 0 if the address holds none.
123func RankOf(addr string) string {
124 pts := GetPoints(addr)
125 rank, holders := rankAndHolders(addr, pts)
126 return ufmt.Sprintf(`{"addr":"%s","points":%d,"rank":%d,"holders":%d}`,
127 jsonEscape(addr), pts, rank, holders)
128}
129
130// ProfileJSON returns points + tier + rank + holders in ONE call (frontend profile, one round-trip).
131func ProfileJSON(addr string) string {
132 pts := GetPoints(addr)
133 rank, holders := rankAndHolders(addr, pts)
134 return ufmt.Sprintf(`{"addr":"%s","points":%d,"tier":"%s","rank":%d,"holders":%d}`,
135 jsonEscape(addr), pts, jsonEscape(tierFor(pts)), rank, holders)
136}