tiers.gno
3.41 Kb · 131 lines
1package memba_points_v1
2
3import (
4 "chain"
5 "strings"
6
7 "gno.land/p/nt/ufmt/v0"
8)
9
10// Tier bands — an on-chain, owner-configurable ladder mapping points → a named status tier.
11// Sorted ascending by MinPoints; the first band MUST have MinPoints == 0 so every holder has a tier.
12// Tiers are canonical ON-CHAIN here (read via TierOf/TierBandsJSON), NOT re-derived client-side.
13
14type Band struct {
15 Name string
16 MinPoints int64
17}
18
19// MaxBands bounds SetTierBands parse cost + TierOf scan cost.
20const MaxBands = 16
21
22// tierBands is the live ladder. Defaults ship at init so tiers are live at launch; the owner tunes
23// them via SetTierBands once the real points distribution is known — the defaults are PLACEHOLDERS,
24// not frozen (SetTierBands replaces the whole ladder).
25var tierBands = defaultBands()
26
27func defaultBands() []Band {
28 return []Band{
29 {"Bronze", 0},
30 {"Silver", 100},
31 {"Gold", 500},
32 {"Platinum", 2000},
33 {"Diamond", 10000},
34 }
35}
36
37// TierOf returns the tier name for an address's current points ("" if no bands are configured). Pure.
38func TierOf(addr string) string { return tierFor(GetPoints(addr)) }
39
40// tierFor returns the highest band whose MinPoints <= points (bands are ascending). O(bands).
41func tierFor(points int64) string {
42 name := ""
43 for _, b := range tierBands {
44 if points >= b.MinPoints {
45 name = b.Name
46 } else {
47 break
48 }
49 }
50 return name
51}
52
53// SetTierBands replaces the tier ladder. Owner only. spec is "Name:Min,Name:Min,..." with strictly
54// ascending Min, the first Min == 0, 1..MaxBands entries, non-empty unique names. Emits TierBandsSet.
55func SetTierBands(cur realm, spec string) {
56 assertOwner(cur)
57 bands := parseBands(spec) // panics on any invalid input
58 tierBands = bands
59 chain.Emit("TierBandsSet", "count", itoa64(int64(len(bands))))
60}
61
62func parseBands(spec string) []Band {
63 if len(spec) == 0 || len(spec) > 512 {
64 panic("tier spec empty or too long")
65 }
66 parts := strings.Split(spec, ",")
67 if len(parts) < 1 || len(parts) > MaxBands {
68 panic("tier band count out of range (1..MaxBands)")
69 }
70 bands := make([]Band, 0, len(parts))
71 seen := make(map[string]bool)
72 var prevMin int64
73 for i, p := range parts {
74 kv := strings.Split(p, ":")
75 if len(kv) != 2 {
76 panic("tier band must be Name:Min")
77 }
78 name := kv[0]
79 if name == "" || seen[name] {
80 panic("tier name empty or duplicate")
81 }
82 min, ok := parseNonNegInt(kv[1])
83 if !ok {
84 panic("tier MinPoints not a non-negative integer")
85 }
86 if i == 0 {
87 if min != 0 {
88 panic("first tier MinPoints must be 0")
89 }
90 } else if min <= prevMin {
91 panic("tier MinPoints must be strictly ascending")
92 }
93 seen[name] = true
94 bands = append(bands, Band{Name: name, MinPoints: min})
95 prevMin = min
96 }
97 return bands
98}
99
100// parseNonNegInt parses a non-negative base-10 int64 (no strconv dep, overflow-guarded).
101func parseNonNegInt(s string) (int64, bool) {
102 if len(s) == 0 || len(s) > 19 {
103 return 0, false
104 }
105 var n int64
106 for i := 0; i < len(s); i++ {
107 c := s[i]
108 if c < '0' || c > '9' {
109 return 0, false
110 }
111 n = n*10 + int64(c-'0')
112 if n < 0 {
113 return 0, false
114 }
115 }
116 return n, true
117}
118
119// TierBandsJSON exposes the ladder: [{"name":"Bronze","minPoints":0},...]
120func TierBandsJSON() string {
121 var sb strings.Builder
122 sb.WriteString("[")
123 for i, b := range tierBands {
124 if i > 0 {
125 sb.WriteString(",")
126 }
127 sb.WriteString(ufmt.Sprintf(`{"name":"%s","minPoints":%d}`, jsonEscape(b.Name), b.MinPoints))
128 }
129 sb.WriteString("]")
130 return sb.String()
131}