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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

pointsv2.gno

11.06 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 "# gnomi pointsv2\n\n> Not initialized.\n"
445	}
446	out := "# gnomi 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}