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/tipjar/v0

Realm
Open in gnoweb ↗

Overview

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

Files (3)

  • README.mdmarkdown
  • gnomod.tomltoml
  • tipjar.gnogno
tipjar.gnogno
1// Package tipjar is a public tip jar. Anyone can send real ugnot with an2// optional message via Tip; the realm keeps a running leaderboard of the3// most generous tippers and a feed of the most recent tips, both shown in4// Render. Only the deployer (captured as owner at init time) can Withdraw5// the accumulated balance. State-mutating functions are crossing functions6// (`cur realm`) per the gno 0.9 interrealm convention.7package tipjar89import (10	"sort"11	"strconv"1213	"chain"14	"chain/banker"15	"chain/runtime"16	"chain/runtime/unsafe"1718	"gno.land/p/nt/avl/v0"19)2021const (22	denom         = "ugnot"23	maxRecent     = 1024	maxMessageLen = 14025)2627// tipperStats is the persisted, accumulated record for one tipper.28type tipperStats struct {29	addr        address30	total       int6431	count       int32	lastMessage string33	lastHeight  int6434}3536// tipEntry is one line in the recent-tips feed.37type tipEntry struct {38	from    address39	amount  int6440	message string41	height  int6442}4344var (45	owner address // captured deployer, set once in init4647	tippers avl.Tree // address string -> *tipperStats48	recent  []tipEntry4950	totalReceived int6451	withdrawn     int6452	tipCount      int53)5455func init() {56	owner = unsafe.OriginCaller()57}5859// get returns the stored *tipperStats for addr, creating one if absent.60func get(addr address) *tipperStats {61	key := addr.String()62	if v := tippers.Get(key); v != nil {63		return v.(*tipperStats)64	}65	ts := &tipperStats{addr: addr}66	tippers.Set(key, ts)67	return ts68}6970// Tip credits the caller's OriginSend ugnot to the jar and records it71// against their running total. Only an EOA calling directly via MsgCall may72// tip — see the payment-guard note on IsUserCall vs IsUser.73func Tip(cur realm, message string) string {74	if !cur.IsCurrent() {75		panic("spoofed realm")76	}77	prev := cur.Previous()78	if !prev.IsUserCall() {79		panic("only an EOA via MsgCall can tip")80	}81	if len(message) > maxMessageLen {82		panic("message too long (max " + strconv.Itoa(maxMessageLen) + " chars)")83	}8485	amount := unsafe.OriginSend().AmountOf(denom)86	if amount <= 0 {87		panic("send some ugnot to tip")88	}8990	from := prev.Address()91	height := runtime.ChainHeight()9293	ts := get(from)94	ts.total += amount95	ts.count++96	ts.lastMessage = message97	ts.lastHeight = height9899	totalReceived += amount100	tipCount++101102	recent = append(recent, tipEntry{from: from, amount: amount, message: message, height: height})103	if len(recent) > maxRecent {104		recent = recent[len(recent)-maxRecent:]105	}106107	chain.Emit("Tip", "from", from.String(), "amount", strconv.FormatInt(amount, 10), "message", message)108	return "thanks for the " + strconv.FormatInt(amount, 10) + "ugnot tip!"109}110111// Balance reports the ugnot still held by the jar (received minus withdrawn).112func Balance() int64 {113	return totalReceived - withdrawn114}115116// Withdraw sends amount ugnot from the jar to the owner. Owner-only.117func Withdraw(cur realm, amount int64) {118	if !cur.IsCurrent() {119		panic("spoofed realm")120	}121	if cur.Previous().Address() != owner {122		panic("only the owner can withdraw")123	}124	if amount <= 0 {125		panic("amount must be positive")126	}127	if amount > Balance() {128		panic("amount exceeds available balance")129	}130131	bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)132	bnk.SendCoins(cur.Address(), owner, chain.NewCoins(chain.NewCoin(denom, amount)))133	withdrawn += amount134135	chain.Emit("Withdraw", "amount", strconv.FormatInt(amount, 10))136}137138// byTotal implements sort.Interface, ranking tippers by total tipped139// descending, ties broken by tip count then address for a fully140// deterministic order.141type byTotal struct {142	stats []*tipperStats143}144145func (b byTotal) Len() int      { return len(b.stats) }146func (b byTotal) Swap(i, j int) { b.stats[i], b.stats[j] = b.stats[j], b.stats[i] }147func (b byTotal) Less(i, j int) bool {148	if b.stats[i].total != b.stats[j].total {149		return b.stats[i].total > b.stats[j].total150	}151	if b.stats[i].count != b.stats[j].count {152		return b.stats[i].count > b.stats[j].count153	}154	return b.stats[i].addr.String() < b.stats[j].addr.String()155}156157// leaderboard collects all tippers ranked by total tipped descending.158func leaderboard() []*tipperStats {159	stats := make([]*tipperStats, 0, tippers.Size())160	tippers.Iterate("", "", func(_ string, v any) bool {161		stats = append(stats, v.(*tipperStats))162		return false163	})164	sort.Stable(byTotal{stats: stats})165	return stats166}167168// medal returns the emoji for a given zero-based rank, or "" past the podium.169func medal(rank int) string {170	switch rank {171	case 0:172		return "🥇"173	case 1:174		return "🥈"175	case 2:176		return "🥉"177	default:178		return ""179	}180}181182// display returns a shortened address for table display.183func display(addr address) string {184	s := addr.String()185	if len(s) > 12 {186		return s[:8] + "…" + s[len(s)-4:]187	}188	return s189}190191// Render shows the jar's balance, the tipper leaderboard, and a feed of the192// most recent tips.193func Render(path string) string {194	out := "# 🫙 Tip Jar\n\n"195	out += "Send ugnot with `Tip(message)` to leave a tip and a note. " +196		"The owner can withdraw the accumulated balance with `Withdraw(amount)`.\n\n"197198	out += "**Balance:** " + strconv.FormatInt(Balance(), 10) + "ugnot" +199		"  ·  **Total tips:** " + strconv.Itoa(tipCount) +200		"  ·  **All-time received:** " + strconv.FormatInt(totalReceived, 10) + "ugnot\n\n"201202	stats := leaderboard()203	out += "## Leaderboard\n\n"204	if len(stats) == 0 {205		out += "_No tips yet. Be the first!_\n\n"206	} else {207		out += "| Rank | Tipper | Total tipped | Tips |\n"208		out += "| ---: | :--- | ---: | ---: |\n"209		for i, ts := range stats {210			rankCell := medal(i)211			if rankCell == "" {212				rankCell = strconv.Itoa(i + 1)213			}214			out += "| " + rankCell +215				" | " + display(ts.addr) +216				" | " + strconv.FormatInt(ts.total, 10) + "ugnot" +217				" | " + strconv.Itoa(ts.count) + " |\n"218		}219		out += "\n"220	}221222	out += "## Recent tips\n\n"223	if len(recent) == 0 {224		out += "_Nothing yet._\n"225		return out226	}227	for i := len(recent) - 1; i >= 0; i-- {228		e := recent[i]229		out += "- **" + display(e.from) + "** tipped " + strconv.FormatInt(e.amount, 10) +230			"ugnot at block " + strconv.FormatInt(e.height, 10)231		if e.message != "" {232			out += ": _" + e.message + "_"233		}234		out += "\n"235	}236	return out237}238

Functions

  • Balance() int64

  • Render(path string) string

  • Tip(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}, message string) string

  • Withdraw(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}, amount int64)

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.