Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}