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

pointsv2.gno

11.07 Kb · 467 lines
  1// Package pointsv2 is referral + check-in + trade/create points for gnomemepad.
  2//
  3// Trade/create awards are pad-only: allowed pad packages call OnTrade / OnCreate
  4// after a successful user action. EOAs cannot self-award trade points.
  5//
  6//	SetReferrer / CheckIn / AwardPoints / Leaderboard — same as v1
  7//	AllowPad / RevokePad — admin allowlist of pad package paths
  8//	OnTrade / OnCreate — called by pad via cross(cur)
  9//	ParamsInfo — extended for UI
 10package pointsv2
 11
 12import (
 13	"chain"
 14	"chain/runtime"
 15	"strconv"
 16	"strings"
 17
 18	"gno.land/p/nt/avl/v0"
 19)
 20
 21const (
 22	PointsReferrerBonus  int64 = 50
 23	PointsRefereeBonus   int64 = 25
 24	PointsCheckIn        int64 = 5
 25	CheckInIntervalH     int64 = 100
 26	// Trade / create (v2)
 27	PointsCreateBonus    int64 = 30  // creator on successful Create
 28	PointsBuyBase        int64 = 2   // base points per buy
 29	PointsSellBase       int64 = 1   // base points per sell
 30	PointsPerGnotBuy     int64 = 10  // extra pts per full GNOT bought (volume)
 31	PointsPerGnotSell    int64 = 3   // extra pts per full GNOT sold
 32	MaxTradePtsPerHeight int64 = 200 // per-user per-height cap (anti-spam)
 33	UgnotPerGnot         int64 = 1_000_000
 34	LeaderboardMax       int   = 50
 35	MaxLeaderboardReturn int   = 20
 36	MaxPadPathLen        int   = 200
 37)
 38
 39var (
 40	admin  address
 41	inited bool
 42	// pointsByAddr: address string -> int64
 43	pointsByAddr avl.Tree
 44	// referrerOf: address string -> referrer address string
 45	referrerOf avl.Tree
 46	// lastCheckIn: address string -> height int64
 47	lastCheckIn avl.Tree
 48	// allowedPads: package path -> true
 49	allowedPads avl.Tree
 50	// tradePtsAtHeight: "addr|height" -> int64 points already awarded that height
 51	tradePtsAtHeight avl.Tree
 52	// totalTradePts / totalCreatePts counters for ops
 53	totalTradePts  int64
 54	totalCreatePts int64
 55)
 56
 57func Init(cur realm) {
 58	if inited {
 59		panic("pointsv2: already initialized")
 60	}
 61	if !cur.Previous().IsUserCall() {
 62		panic("pointsv2: EOA only")
 63	}
 64	admin = cur.Previous().Address()
 65	inited = true
 66	pointsByAddr = avl.Tree{}
 67	referrerOf = avl.Tree{}
 68	lastCheckIn = avl.Tree{}
 69	allowedPads = avl.Tree{}
 70	tradePtsAtHeight = avl.Tree{}
 71	chain.Emit("Init", "admin", admin.String())
 72}
 73
 74func requireInit() {
 75	if !inited {
 76		panic("pointsv2: call Init first")
 77	}
 78}
 79
 80func requireAdmin(cur realm) {
 81	requireInit()
 82	if !cur.Previous().IsUserCall() {
 83		panic("pointsv2: EOA only")
 84	}
 85	if cur.Previous().Address() != admin {
 86		panic("pointsv2: not admin")
 87	}
 88}
 89
 90func getPts(addr string) int64 {
 91	v := pointsByAddr.Get(addr)
 92	if v == nil {
 93		return 0
 94	}
 95	n, ok := v.(int64)
 96	if !ok {
 97		return 0
 98	}
 99	return n
100}
101
102func addPts(addr string, delta int64) {
103	if delta == 0 || addr == "" {
104		return
105	}
106	cur := getPts(addr)
107	next := cur + delta
108	if next < 0 {
109		next = 0
110	}
111	pointsByAddr.Set(addr, next)
112}
113
114// AllowPad registers a pad package path that may call OnTrade / OnCreate.
115func AllowPad(cur realm, padPkg string) {
116	requireAdmin(cur)
117	padPkg = strings.TrimSpace(padPkg)
118	if padPkg == "" || len(padPkg) > MaxPadPathLen {
119		panic("pointsv2: invalid pad path")
120	}
121	if !strings.HasPrefix(padPkg, "gno.land/r/") {
122		panic("pointsv2: pad must be gno.land/r/…")
123	}
124	allowedPads.Set(padPkg, true)
125	chain.Emit("AllowPad", "pad", padPkg)
126}
127
128// RevokePad removes a pad from the allowlist.
129func RevokePad(cur realm, padPkg string) {
130	requireAdmin(cur)
131	padPkg = strings.TrimSpace(padPkg)
132	allowedPads.Remove(padPkg)
133	chain.Emit("RevokePad", "pad", padPkg)
134}
135
136// IsPadAllowed reports whether path may award trade points.
137func IsPadAllowed(padPkg string) bool {
138	requireInit()
139	return allowedPads.Has(strings.TrimSpace(padPkg))
140}
141
142// ListPads returns allowed pad paths, one per line.
143func ListPads() string {
144	requireInit()
145	out := ""
146	allowedPads.Iterate("", "", func(key string, _ any) bool {
147		if out != "" {
148			out += "\n"
149		}
150		out += key
151		return false
152	})
153	return out
154}
155
156func requireAllowedPad(cur realm) string {
157	requireInit()
158	prev := cur.Previous()
159	// Must be a realm call (pad), not EOA
160	if prev.IsUserCall() {
161		panic("pointsv2: pad realm only")
162	}
163	path := prev.PkgPath()
164	if path == "" || !allowedPads.Has(path) {
165		panic("pointsv2: pad not allowed")
166	}
167	return path
168}
169
170func heightCapKey(addr string, h int64) string {
171	return addr + "|" + strconv.FormatInt(h, 10)
172}
173
174func remainingHeightCap(addr string, h int64) int64 {
175	k := heightCapKey(addr, h)
176	used := int64(0)
177	if v := tradePtsAtHeight.Get(k); v != nil {
178		used, _ = v.(int64)
179	}
180	left := MaxTradePtsPerHeight - used
181	if left < 0 {
182		return 0
183	}
184	return left
185}
186
187func consumeHeightCap(addr string, h, award int64) int64 {
188	if award <= 0 {
189		return 0
190	}
191	left := remainingHeightCap(addr, h)
192	if left <= 0 {
193		return 0
194	}
195	if award > left {
196		award = left
197	}
198	k := heightCapKey(addr, h)
199	used := int64(0)
200	if v := tradePtsAtHeight.Get(k); v != nil {
201		used, _ = v.(int64)
202	}
203	tradePtsAtHeight.Set(k, used+award)
204	return award
205}
206
207// OnTrade awards points for a pad buy (side=0) or sell (side=1).
208// volumeUgnot is the GNOT notional of the trade (sent in / received out).
209// Returns points awarded (0 if capped).
210func OnTrade(cur realm, trader address, launchID string, side int64, volumeUgnot int64) int64 {
211	_ = requireAllowedPad(cur)
212	if !trader.IsValid() {
213		return 0
214	}
215	if volumeUgnot < 0 {
216		volumeUgnot = 0
217	}
218	launchID = strings.TrimSpace(launchID)
219	if launchID == "" {
220		return 0
221	}
222
223	base := PointsBuyBase
224	perG := PointsPerGnotBuy
225	if side == 1 {
226		base = PointsSellBase
227		perG = PointsPerGnotSell
228	} else if side != 0 {
229		// ignore open / unknown
230		return 0
231	}
232
233	gnotFull := volumeUgnot / UgnotPerGnot
234	award := base + gnotFull*perG
235	if award <= 0 {
236		return 0
237	}
238
239	me := trader.String()
240	h := runtime.ChainHeight()
241	award = consumeHeightCap(me, h, award)
242	if award <= 0 {
243		return 0
244	}
245	addPts(me, award)
246	totalTradePts += award
247	chain.Emit("OnTrade",
248		"trader", me,
249		"id", launchID,
250		"side", strconv.FormatInt(side, 10),
251		"vol", strconv.FormatInt(volumeUgnot, 10),
252		"pts", strconv.FormatInt(award, 10),
253	)
254	return award
255}
256
257// OnCreate awards create bonus to the launch creator (pad-only).
258func OnCreate(cur realm, creator address, launchID string) int64 {
259	_ = requireAllowedPad(cur)
260	if !creator.IsValid() {
261		return 0
262	}
263	launchID = strings.TrimSpace(launchID)
264	if launchID == "" {
265		return 0
266	}
267	me := creator.String()
268	h := runtime.ChainHeight()
269	award := consumeHeightCap(me, h, PointsCreateBonus)
270	if award <= 0 {
271		return 0
272	}
273	addPts(me, award)
274	totalCreatePts += award
275	chain.Emit("OnCreate", "creator", me, "id", launchID, "pts", strconv.FormatInt(award, 10))
276	return award
277}
278
279// SetReferrer binds caller to a referrer once. Self-referral rejected.
280func SetReferrer(cur realm, referrer address) {
281	requireInit()
282	if !cur.Previous().IsUserCall() {
283		panic("pointsv2: EOA only")
284	}
285	if !referrer.IsValid() {
286		panic("pointsv2: invalid referrer")
287	}
288	me := cur.Previous().Address()
289	if me == referrer {
290		panic("pointsv2: cannot refer self")
291	}
292	meS := me.String()
293	if referrerOf.Has(meS) {
294		panic("pointsv2: referrer already set")
295	}
296	refS := referrer.String()
297	referrerOf.Set(meS, refS)
298	addPts(refS, PointsReferrerBonus)
299	addPts(meS, PointsRefereeBonus)
300	chain.Emit("SetReferrer", "user", meS, "referrer", refS)
301}
302
303// CheckIn grants PointsCheckIn if enough heights passed since last check-in.
304func CheckIn(cur realm) int64 {
305	requireInit()
306	if !cur.Previous().IsUserCall() {
307		panic("pointsv2: EOA only")
308	}
309	me := cur.Previous().Address().String()
310	h := runtime.ChainHeight()
311	last := int64(0)
312	if v := lastCheckIn.Get(me); v != nil {
313		last, _ = v.(int64)
314	}
315	if last > 0 && h-last < CheckInIntervalH {
316		panic("pointsv2: check-in too soon")
317	}
318	lastCheckIn.Set(me, h)
319	addPts(me, PointsCheckIn)
320	chain.Emit("CheckIn", "user", me, "points", strconv.FormatInt(PointsCheckIn, 10))
321	return getPts(me)
322}
323
324// AwardPoints is admin-only (campaigns / corrections).
325func AwardPoints(cur realm, to address, amount int64) {
326	requireAdmin(cur)
327	if !to.IsValid() {
328		panic("pointsv2: invalid to")
329	}
330	if amount == 0 {
331		panic("pointsv2: amount zero")
332	}
333	addPts(to.String(), amount)
334	chain.Emit("Award", "to", to.String(), "amount", strconv.FormatInt(amount, 10))
335}
336
337func GetPoints(addr string) int64 {
338	requireInit()
339	return getPts(strings.TrimSpace(addr))
340}
341
342func GetReferrer(addr string) string {
343	requireInit()
344	v := referrerOf.Get(strings.TrimSpace(addr))
345	if v == nil {
346		return ""
347	}
348	s, _ := v.(string)
349	return s
350}
351
352func Leaderboard(n int) string {
353	requireInit()
354	if n <= 0 {
355		n = 10
356	}
357	if n > MaxLeaderboardReturn {
358		n = MaxLeaderboardReturn
359	}
360	type row struct {
361		addr string
362		pts  int64
363	}
364	rows := make([]row, 0, LeaderboardMax)
365	pointsByAddr.Iterate("", "", func(key string, value any) bool {
366		pts, _ := value.(int64)
367		if pts <= 0 {
368			return false
369		}
370		rows = append(rows, row{addr: key, pts: pts})
371		if len(rows) >= LeaderboardMax {
372			return true
373		}
374		return false
375	})
376	for i := 0; i < len(rows); i++ {
377		best := i
378		for j := i + 1; j < len(rows); j++ {
379			if rows[j].pts > rows[best].pts {
380				best = j
381			}
382		}
383		rows[i], rows[best] = rows[best], rows[i]
384	}
385	if n > len(rows) {
386		n = len(rows)
387	}
388	out := ""
389	for i := 0; i < n; i++ {
390		if out != "" {
391			out += "\n"
392		}
393		out += rows[i].addr + "|" + strconv.FormatInt(rows[i].pts, 10)
394	}
395	return out
396}
397
398func UserCount() int {
399	requireInit()
400	return pointsByAddr.Size()
401}
402
403func PadCount() int {
404	requireInit()
405	return allowedPads.Size()
406}
407
408func TransferAdmin(cur realm, newAdmin address) {
409	requireAdmin(cur)
410	if !newAdmin.IsValid() {
411		panic("pointsv2: invalid address")
412	}
413	old := admin
414	admin = newAdmin
415	chain.Emit("TransferAdmin", "from", old.String(), "to", newAdmin.String())
416}
417
418func Admin() string {
419	requireInit()
420	return admin.String()
421}
422
423// ParamsInfo for UI:
424//
425//	referrer|referee|checkIn|interval|createBonus|buyBase|sellBase|ptsPerGnotBuy|ptsPerGnotSell|maxPerHeight|tradePtsTotal|createPtsTotal|v2
426func ParamsInfo() string {
427	return strconv.FormatInt(PointsReferrerBonus, 10) + "|" +
428		strconv.FormatInt(PointsRefereeBonus, 10) + "|" +
429		strconv.FormatInt(PointsCheckIn, 10) + "|" +
430		strconv.FormatInt(CheckInIntervalH, 10) + "|" +
431		strconv.FormatInt(PointsCreateBonus, 10) + "|" +
432		strconv.FormatInt(PointsBuyBase, 10) + "|" +
433		strconv.FormatInt(PointsSellBase, 10) + "|" +
434		strconv.FormatInt(PointsPerGnotBuy, 10) + "|" +
435		strconv.FormatInt(PointsPerGnotSell, 10) + "|" +
436		strconv.FormatInt(MaxTradePtsPerHeight, 10) + "|" +
437		strconv.FormatInt(totalTradePts, 10) + "|" +
438		strconv.FormatInt(totalCreatePts, 10) + "|v2"
439}
440
441func Render(path string) string {
442	path = strings.Trim(path, "/")
443	if !inited {
444		return "# gnomemepad pointsv2\n\n> Not initialized.\n"
445	}
446	out := "# gnomemepad pointsv2\n\n"
447	out += "- Users: **" + strconv.Itoa(pointsByAddr.Size()) + "**\n"
448	out += "- Allowed pads: **" + strconv.Itoa(allowedPads.Size()) + "**\n"
449	out += "- Trade pts issued: **" + strconv.FormatInt(totalTradePts, 10) + "**\n"
450	out += "- Create pts issued: **" + strconv.FormatInt(totalCreatePts, 10) + "**\n\n"
451	out += "## API\n\n- SetReferrer / CheckIn\n- OnTrade / OnCreate (pad allowlist)\n- AllowPad / Leaderboard\n"
452	_ = path
453	return out
454}
455
456func resetForTest() {
457	var zero address
458	admin = zero
459	inited = false
460	pointsByAddr = avl.Tree{}
461	referrerOf = avl.Tree{}
462	lastCheckIn = avl.Tree{}
463	allowedPads = avl.Tree{}
464	tradePtsAtHeight = avl.Tree{}
465	totalTradePts = 0
466	totalCreatePts = 0
467}