PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/moul/x/daily/streak/v0

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
v0
Namespace
moul / x / daily / streak
Files
3 (README)(gnomod.toml)
Exported functions
5
Module
gno.land/r/moul/x/daily/streak/v0
gno
0.9

Files (3)

  • README.mdmarkdown
  • gnomod.tomltoml
  • streak.gnogno
streak.gnogno
1// Package streak is an on-chain check-in streak tracker: call CheckIn once per2// "day" to keep a streak alive, miss one and it resets to 1.3//4// There is no wall clock on-chain, so a "day" here is a fixed window of5// BlocksPerDay blocks: day = ChainHeight() / BlocksPerDay. That makes every6// streak decision a pure function of block height — deterministic, replayable,7// and impossible to game by waiting for a favourable timestamp.8//9// State is per-caller and append-only in spirit: an address's best streak is10// never lowered, so breaking a run costs the current streak but not the record.11//12// This is a stateful app with nothing reusable to extract, so it ships as a13// lone realm rather than a p/ library + demo pair.14package streak1516import (17	"sort"18	"strconv"19	"strings"2021	"chain"22	"chain/runtime"2324	"gno.land/p/nt/avl/v0"25)2627// BlocksPerDay is the length of a check-in window, in blocks. Chosen so a28// window is a meaningful stretch of chain activity while keeping tests and29// demos quick to reason about.30const BlocksPerDay = 10003132// maxLeaderboard caps how many entries Render lists.33const maxLeaderboard = 103435// record is one address's streak state.36type record struct {37	current int   // consecutive days up to lastDay38	best    int   // highest current ever reached; never lowered39	lastDay int64 // day index of the most recent check-in40	totalIn int   // total check-ins ever41}4243// users maps address string -> *record.44var users = avl.NewTree()4546// today returns the current day index: block height divided into fixed windows.47func today() int64 { return runtime.ChainHeight() / BlocksPerDay }4849// CheckIn records a check-in for the caller and returns the resulting streak.50//51// Checking in twice in the same window is rejected — the streak only moves when52// the window does. A gap of exactly one window continues the streak; any larger53// gap restarts it at 1.54func CheckIn(cur realm) int {55	if !cur.IsCurrent() {56		panic("spoofed realm")57	}58	prev := cur.Previous()59	if !prev.IsUserCall() {60		panic("only an EOA via MsgCall can check in")61	}62	addr := prev.Address()63	day := today()6465	r := get(addr)66	if r == nil {67		r = &record{}68		users.Set(addr.String(), r)69	} else {70		switch {71		case r.totalIn > 0 && r.lastDay == day:72			panic("already checked in for day " + strconv.FormatInt(day, 10))73		case r.totalIn > 0 && r.lastDay == day-1:74			// consecutive window: the streak continues below75		default:76			// missed at least one window: the run is broken77			r.current = 078		}79	}8081	r.current++82	r.lastDay = day83	r.totalIn++84	if r.current > r.best {85		r.best = r.current86	}8788	chain.Emit("CheckIn",89		"addr", addr.String(),90		"day", strconv.FormatInt(day, 10),91		"streak", strconv.Itoa(r.current),92	)93	return r.current94}9596// get returns the caller's record, or nil.97//98// avl/v0's Get returns a single `any` (not the value, ok pair Go maps use), so99// a missing key surfaces as a nil interface rather than a second return value.100func get(addr address) *record {101	v := users.Get(addr.String())102	if v == nil {103		return nil104	}105	return v.(*record)106}107108// Current returns addr's live streak — 0 once a window has been missed, even109// though the stored value only updates on the next check-in.110func Current(addr address) int {111	r := get(addr)112	if r == nil {113		return 0114	}115	day := today()116	if r.lastDay != day && r.lastDay != day-1 {117		return 0118	}119	return r.current120}121122// Best returns addr's record streak, which is never lowered.123func Best(addr address) int {124	r := get(addr)125	if r == nil {126		return 0127	}128	return r.best129}130131// Day returns the current day index.132func Day() int64 { return today() }133134// entry is a flattened record for rendering.135type entry struct {136	addr    string137	current int138	best    int139	total   int140}141142// byBest ranks entries by best streak descending, then by address so equal143// scores always come out in the same order (Render must be deterministic).144type byBest []entry145146func (e byBest) Len() int      { return len(e) }147func (e byBest) Swap(i, j int) { e[i], e[j] = e[j], e[i] }148func (e byBest) Less(i, j int) bool {149	if e[i].best != e[j].best {150		return e[i].best > e[j].best151	}152	return e[i].addr < e[j].addr153}154155// Render renders the streak board for gnoweb.156//157//	Render("")          / Render("/") -> leaderboard by best streak158//	Render("/<address>")             -> that address's standing159func Render(path string) string {160	var b strings.Builder161	b.WriteString("# Check-in Streaks\n\n")162	b.WriteString("A \"day\" is ")163	b.WriteString(strconv.Itoa(BlocksPerDay))164	b.WriteString(" blocks — there is no clock on-chain, so streaks are decided by block height alone.\n\n")165	b.WriteString("Current day: **")166	b.WriteString(strconv.FormatInt(today(), 10))167	b.WriteString("**\n\n")168169	if q := parseArg(path); q != "" {170		return b.String() + renderOne(q)171	}172173	entries := all()174	if len(entries) == 0 {175		b.WriteString("_Nobody has checked in yet._\n\n")176		b.WriteString("> Call `CheckIn()` to start a streak.\n")177		return b.String()178	}179180	// gno's sort has Sort(Interface) but no Slice(), so ranking goes through an181	// explicit sort.Interface. Ties break on address so the board is stable.182	sort.Sort(byBest(entries))183	if len(entries) > maxLeaderboard {184		entries = entries[:maxLeaderboard]185	}186187	b.WriteString("| # | address | current | best | check-ins |\n|---|---|---|---|---|\n")188	for i, e := range entries {189		b.WriteString("| ")190		b.WriteString(strconv.Itoa(i + 1))191		b.WriteString(" | `")192		b.WriteString(e.addr)193		b.WriteString("` | ")194		b.WriteString(strconv.Itoa(e.current))195		b.WriteString(" | ")196		b.WriteString(strconv.Itoa(e.best))197		b.WriteString(" | ")198		b.WriteString(strconv.Itoa(e.total))199		b.WriteString(" |\n")200	}201	return b.String()202}203204// renderOne renders a single address's standing.205func renderOne(q string) string {206	addr := address(q)207	var b strings.Builder208	b.WriteString("## `")209	b.WriteString(q)210	b.WriteString("`\n\n")211	if !addr.IsValid() {212		b.WriteString("_Not a valid address._\n")213		return b.String()214	}215	r := get(addr)216	if r == nil {217		b.WriteString("_No check-ins yet._\n")218		return b.String()219	}220	b.WriteString("- current streak: **")221	b.WriteString(strconv.Itoa(Current(addr)))222	b.WriteString("**\n- best streak: **")223	b.WriteString(strconv.Itoa(r.best))224	b.WriteString("**\n- total check-ins: **")225	b.WriteString(strconv.Itoa(r.totalIn))226	b.WriteString("**\n- last check-in: day **")227	b.WriteString(strconv.FormatInt(r.lastDay, 10))228	b.WriteString("**\n")229	return b.String()230}231232// all flattens the tree into a slice.233func all() []entry {234	out := []entry{}235	users.Iterate("", "", func(k string, v any) bool {236		r := v.(*record)237		out = append(out, entry{238			addr:    k,239			current: Current(address(k)),240			best:    r.best,241			total:   r.totalIn,242		})243		return false244	})245	return out246}247248// parseArg extracts the first path segment.249func parseArg(path string) string {250	s := strings.TrimSpace(path)251	s = strings.TrimPrefix(s, "/")252	if i := strings.IndexByte(s, '/'); i >= 0 {253		s = s[:i]254	}255	return s256}257

Functions

  • Best(addr string) int

  • 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}) int

  • Current(addr string) int

  • Day() int64

  • Render(path string) string

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

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.