Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}