Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}