points.gno
6.40 Kb · 273 lines
1// Package points is a lightweight referral + points ledger for gnomemepad.
2// Independent of pad — does not verify trades on-chain (testnet growth layer).
3//
4// SetReferrer(referrer) — once per user; both get bonus points
5// CheckIn() — daily points (1 per CheckInInterval heights)
6// GetPoints / GetReferrer / Leaderboard
7// AwardPoints — admin only (manual campaigns)
8package points
9
10import (
11 "chain"
12 "chain/runtime"
13 "strconv"
14 "strings"
15
16 "gno.land/p/nt/avl/v0"
17)
18
19const (
20 // Points grants
21 PointsReferrerBonus int64 = 50 // to referrer when someone sets them
22 PointsRefereeBonus int64 = 25 // to new user on first referrer set
23 PointsCheckIn int64 = 5
24 CheckInIntervalH int64 = 100 // ~blocks between check-ins (testnet-friendly)
25 LeaderboardMax int = 50
26 MaxLeaderboardReturn int = 20
27)
28
29var (
30 admin address
31 inited bool
32 // pointsByAddr: address string -> int64
33 pointsByAddr avl.Tree
34 // referrerOf: address string -> referrer address string
35 referrerOf avl.Tree
36 // lastCheckIn: address string -> height int64
37 lastCheckIn avl.Tree
38 // score index for leaderboard: zero-padded score + addr -> addr (desc via reverse iterate)
39 // We keep simple: rebuild top from scan on Leaderboard() for MVP size.
40)
41
42func Init(cur realm) {
43 if inited {
44 panic("points: already initialized")
45 }
46 if !cur.Previous().IsUserCall() {
47 panic("points: EOA only")
48 }
49 admin = cur.Previous().Address()
50 inited = true
51 pointsByAddr = avl.Tree{}
52 referrerOf = avl.Tree{}
53 lastCheckIn = avl.Tree{}
54 chain.Emit("Init", "admin", admin.String())
55}
56
57func requireInit() {
58 if !inited {
59 panic("points: call Init first")
60 }
61}
62
63func requireAdmin(cur realm) {
64 requireInit()
65 if !cur.Previous().IsUserCall() {
66 panic("points: EOA only")
67 }
68 if cur.Previous().Address() != admin {
69 panic("points: not admin")
70 }
71}
72
73func getPts(addr string) int64 {
74 v := pointsByAddr.Get(addr)
75 if v == nil {
76 return 0
77 }
78 n, ok := v.(int64)
79 if !ok {
80 return 0
81 }
82 return n
83}
84
85func addPts(addr string, delta int64) {
86 if delta == 0 || addr == "" {
87 return
88 }
89 cur := getPts(addr)
90 next := cur + delta
91 if next < 0 {
92 next = 0
93 }
94 pointsByAddr.Set(addr, next)
95}
96
97// SetReferrer binds caller to a referrer once. Self-referral rejected.
98func SetReferrer(cur realm, referrer address) {
99 requireInit()
100 if !cur.Previous().IsUserCall() {
101 panic("points: EOA only")
102 }
103 if !referrer.IsValid() {
104 panic("points: invalid referrer")
105 }
106 me := cur.Previous().Address()
107 if me == referrer {
108 panic("points: cannot refer self")
109 }
110 meS := me.String()
111 if referrerOf.Has(meS) {
112 panic("points: referrer already set")
113 }
114 refS := referrer.String()
115 referrerOf.Set(meS, refS)
116 addPts(refS, PointsReferrerBonus)
117 addPts(meS, PointsRefereeBonus)
118 chain.Emit("SetReferrer", "user", meS, "referrer", refS)
119}
120
121// CheckIn grants PointsCheckIn if enough heights passed since last check-in.
122func CheckIn(cur realm) int64 {
123 requireInit()
124 if !cur.Previous().IsUserCall() {
125 panic("points: EOA only")
126 }
127 me := cur.Previous().Address().String()
128 h := runtime.ChainHeight()
129 last := int64(0)
130 if v := lastCheckIn.Get(me); v != nil {
131 last, _ = v.(int64)
132 }
133 if last > 0 && h-last < CheckInIntervalH {
134 panic("points: check-in too soon")
135 }
136 lastCheckIn.Set(me, h)
137 addPts(me, PointsCheckIn)
138 chain.Emit("CheckIn", "user", me, "points", strconv.FormatInt(PointsCheckIn, 10))
139 return getPts(me)
140}
141
142// AwardPoints is admin-only (campaigns / corrections).
143func AwardPoints(cur realm, to address, amount int64) {
144 requireAdmin(cur)
145 if !to.IsValid() {
146 panic("points: invalid to")
147 }
148 if amount == 0 {
149 panic("points: amount zero")
150 }
151 addPts(to.String(), amount)
152 chain.Emit("Award", "to", to.String(), "amount", strconv.FormatInt(amount, 10))
153}
154
155// GetPoints returns points for addr.
156func GetPoints(addr string) int64 {
157 requireInit()
158 return getPts(strings.TrimSpace(addr))
159}
160
161// GetReferrer returns referrer address string or "".
162func GetReferrer(addr string) string {
163 requireInit()
164 v := referrerOf.Get(strings.TrimSpace(addr))
165 if v == nil {
166 return ""
167 }
168 s, _ := v.(string)
169 return s
170}
171
172// Leaderboard returns up to n lines "addr|points" sorted by points desc.
173func Leaderboard(n int) string {
174 requireInit()
175 if n <= 0 {
176 n = 10
177 }
178 if n > MaxLeaderboardReturn {
179 n = MaxLeaderboardReturn
180 }
181 // Collect
182 type row struct {
183 addr string
184 pts int64
185 }
186 rows := make([]row, 0, LeaderboardMax)
187 pointsByAddr.Iterate("", "", func(key string, value any) bool {
188 pts, _ := value.(int64)
189 if pts <= 0 {
190 return false
191 }
192 rows = append(rows, row{addr: key, pts: pts})
193 // soft cap collect
194 if len(rows) >= LeaderboardMax {
195 return true
196 }
197 return false
198 })
199 // Simple selection sort desc (small N)
200 for i := 0; i < len(rows); i++ {
201 best := i
202 for j := i + 1; j < len(rows); j++ {
203 if rows[j].pts > rows[best].pts {
204 best = j
205 }
206 }
207 rows[i], rows[best] = rows[best], rows[i]
208 }
209 if n > len(rows) {
210 n = len(rows)
211 }
212 out := ""
213 for i := 0; i < n; i++ {
214 if out != "" {
215 out += "\n"
216 }
217 out += rows[i].addr + "|" + strconv.FormatInt(rows[i].pts, 10)
218 }
219 return out
220}
221
222// UserCount returns addresses with any points.
223func UserCount() int {
224 requireInit()
225 return pointsByAddr.Size()
226}
227
228// TransferAdmin rotates points admin.
229func TransferAdmin(cur realm, newAdmin address) {
230 requireAdmin(cur)
231 if !newAdmin.IsValid() {
232 panic("points: invalid address")
233 }
234 old := admin
235 admin = newAdmin
236 chain.Emit("TransferAdmin", "from", old.String(), "to", newAdmin.String())
237}
238
239// Admin returns admin address.
240func Admin() string {
241 requireInit()
242 return admin.String()
243}
244
245// ParamsInfo for UI: referrerBonus|refereeBonus|checkIn|interval
246func ParamsInfo() string {
247 return strconv.FormatInt(PointsReferrerBonus, 10) + "|" +
248 strconv.FormatInt(PointsRefereeBonus, 10) + "|" +
249 strconv.FormatInt(PointsCheckIn, 10) + "|" +
250 strconv.FormatInt(CheckInIntervalH, 10)
251}
252
253func Render(path string) string {
254 path = strings.Trim(path, "/")
255 if !inited {
256 return "# gnomemepad points\n\n> Not initialized.\n"
257 }
258 out := "# gnomemepad points\n\n"
259 out += "- Users with points: **" + strconv.Itoa(pointsByAddr.Size()) + "**\n"
260 out += "- Check-in: **" + strconv.FormatInt(PointsCheckIn, 10) + "** pts / " + strconv.FormatInt(CheckInIntervalH, 10) + " heights\n\n"
261 out += "## API\n\n- SetReferrer(addr)\n- CheckIn()\n- GetPoints(addr) / Leaderboard(n)\n"
262 _ = path
263 return out
264}
265
266func resetForTest() {
267 var zero address
268 admin = zero
269 inited = false
270 pointsByAddr = avl.Tree{}
271 referrerOf = avl.Tree{}
272 lastCheckIn = avl.Tree{}
273}