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 / OnCreate4// after a successful user action. EOAs cannot self-award trade points.5//6// SetReferrer / CheckIn / AwardPoints / Leaderboard — same as v17// AllowPad / RevokePad — admin allowlist of pad package paths8// OnTrade / OnCreate — called by pad via cross(cur)9// ParamsInfo — extended for UI10package pointsv21112import (13 "chain"14 "chain/runtime"15 "strconv"16 "strings"1718 "gno.land/p/nt/avl/v0"19)2021const (22 PointsReferrerBonus int64 = 5023 PointsRefereeBonus int64 = 2524 PointsCheckIn int64 = 525 CheckInIntervalH int64 = 10026 // Trade / create (v2)27 PointsCreateBonus int64 = 30 // creator on successful Create28 PointsBuyBase int64 = 2 // base points per buy29 PointsSellBase int64 = 1 // base points per sell30 PointsPerGnotBuy int64 = 10 // extra pts per full GNOT bought (volume)31 PointsPerGnotSell int64 = 3 // extra pts per full GNOT sold32 MaxTradePtsPerHeight int64 = 200 // per-user per-height cap (anti-spam)33 UgnotPerGnot int64 = 1_000_00034 LeaderboardMax int = 5035 MaxLeaderboardReturn int = 2036 MaxPadPathLen int = 20037)3839var (40 admin address41 inited bool42 // pointsByAddr: address string -> int6443 pointsByAddr avl.Tree44 // referrerOf: address string -> referrer address string45 referrerOf avl.Tree46 // lastCheckIn: address string -> height int6447 lastCheckIn avl.Tree48 // allowedPads: package path -> true49 allowedPads avl.Tree50 // tradePtsAtHeight: "addr|height" -> int64 points already awarded that height51 tradePtsAtHeight avl.Tree52 // totalTradePts / totalCreatePts counters for ops53 totalTradePts int6454 totalCreatePts int6455)5657func 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 = true66 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}7374func requireInit() {75 if !inited {76 panic("pointsv2: call Init first")77 }78}7980func 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}8990func getPts(addr string) int64 {91 v := pointsByAddr.Get(addr)92 if v == nil {93 return 094 }95 n, ok := v.(int64)96 if !ok {97 return 098 }99 return n100}101102func addPts(addr string, delta int64) {103 if delta == 0 || addr == "" {104 return105 }106 cur := getPts(addr)107 next := cur + delta108 if next < 0 {109 next = 0110 }111 pointsByAddr.Set(addr, next)112}113114// 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}127128// 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}135136// IsPadAllowed reports whether path may award trade points.137func IsPadAllowed(padPkg string) bool {138 requireInit()139 return allowedPads.Has(strings.TrimSpace(padPkg))140}141142// 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 += key151 return false152 })153 return out154}155156func requireAllowedPad(cur realm) string {157 requireInit()158 prev := cur.Previous()159 // Must be a realm call (pad), not EOA160 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 path168}169170func heightCapKey(addr string, h int64) string {171 return addr + "|" + strconv.FormatInt(h, 10)172}173174func 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 - used181 if left < 0 {182 return 0183 }184 return left185}186187func consumeHeightCap(addr string, h, award int64) int64 {188 if award <= 0 {189 return 0190 }191 left := remainingHeightCap(addr, h)192 if left <= 0 {193 return 0194 }195 if award > left {196 award = left197 }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 award205}206207// 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 0214 }215 if volumeUgnot < 0 {216 volumeUgnot = 0217 }218 launchID = strings.TrimSpace(launchID)219 if launchID == "" {220 return 0221 }222223 base := PointsBuyBase224 perG := PointsPerGnotBuy225 if side == 1 {226 base = PointsSellBase227 perG = PointsPerGnotSell228 } else if side != 0 {229 // ignore open / unknown230 return 0231 }232233 gnotFull := volumeUgnot / UgnotPerGnot234 award := base + gnotFull*perG235 if award <= 0 {236 return 0237 }238239 me := trader.String()240 h := runtime.ChainHeight()241 award = consumeHeightCap(me, h, award)242 if award <= 0 {243 return 0244 }245 addPts(me, award)246 totalTradePts += award247 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 award255}256257// 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 0262 }263 launchID = strings.TrimSpace(launchID)264 if launchID == "" {265 return 0266 }267 me := creator.String()268 h := runtime.ChainHeight()269 award := consumeHeightCap(me, h, PointsCreateBonus)270 if award <= 0 {271 return 0272 }273 addPts(me, award)274 totalCreatePts += award275 chain.Emit("OnCreate", "creator", me, "id", launchID, "pts", strconv.FormatInt(award, 10))276 return award277}278279// 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}302303// 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}323324// 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}336337func GetPoints(addr string) int64 {338 requireInit()339 return getPts(strings.TrimSpace(addr))340}341342func 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 s350}351352func Leaderboard(n int) string {353 requireInit()354 if n <= 0 {355 n = 10356 }357 if n > MaxLeaderboardReturn {358 n = MaxLeaderboardReturn359 }360 type row struct {361 addr string362 pts int64363 }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 false369 }370 rows = append(rows, row{addr: key, pts: pts})371 if len(rows) >= LeaderboardMax {372 return true373 }374 return false375 })376 for i := 0; i < len(rows); i++ {377 best := i378 for j := i + 1; j < len(rows); j++ {379 if rows[j].pts > rows[best].pts {380 best = j381 }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 out396}397398func UserCount() int {399 requireInit()400 return pointsByAddr.Size()401}402403func PadCount() int {404 requireInit()405 return allowedPads.Size()406}407408func TransferAdmin(cur realm, newAdmin address) {409 requireAdmin(cur)410 if !newAdmin.IsValid() {411 panic("pointsv2: invalid address")412 }413 old := admin414 admin = newAdmin415 chain.Emit("TransferAdmin", "from", old.String(), "to", newAdmin.String())416}417418func Admin() string {419 requireInit()420 return admin.String()421}422423// ParamsInfo for UI:424//425// referrer|referee|checkIn|interval|createBonus|buyBase|sellBase|ptsPerGnotBuy|ptsPerGnotSell|maxPerHeight|tradePtsTotal|createPtsTotal|v2426func 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}440441func 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 _ = path453 return out454}455456func resetForTest() {457 var zero address458 admin = zero459 inited = false460 pointsByAddr = avl.Tree{}461 referrerOf = avl.Tree{}462 lastCheckIn = avl.Tree{}463 allowedPads = avl.Tree{}464 tradePtsAtHeight = avl.Tree{}465 totalTradePts = 0466 totalCreatePts = 0467}468Admin() string
AllowPad(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, padPkg string)
AwardPoints(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, to string, amount int64)
CheckIn(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int64
GetPoints(addr string) int64
GetReferrer(addr string) string
Init(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string})
IsPadAllowed(padPkg string) bool
Leaderboard(n int) string
ListPads() string
OnCreate(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, creator string, launchID string) int64
OnTrade(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, trader string, launchID string, side int64, volumeUgnot int64) int64
PadCount() int
ParamsInfo() string
Render(path string) string
RevokePad(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, padPkg string)
SetReferrer(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, referrer string)
TransferAdmin(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, newAdmin string)
UserCount() int
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.