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.

tipjar.gno

5.88 Kb · 224 lines
  1// Package tipjar is a public tip jar. Anyone can send real ugnot with an
  2// optional message via Tip; the realm keeps a running leaderboard of the
  3// most generous tippers and a feed of the most recent tips, both shown in
  4// Render. Only the deployer (captured as owner at init time) can Withdraw
  5// the accumulated balance. State-mutating functions are crossing functions
  6// (`cur realm`) per the gno 0.9 interrealm convention.
  7package tipjar
  8
  9import (
 10	"sort"
 11	"strconv"
 12
 13	"chain"
 14	"chain/banker"
 15	"chain/runtime"
 16	"chain/runtime/unsafe"
 17
 18	"gno.land/p/nym-moultest123/kit/ui/v0"
 19	"gno.land/p/nt/avl/v0"
 20)
 21
 22const (
 23	denom         = "ugnot"
 24	maxRecent     = 10
 25	maxMessageLen = 140
 26)
 27
 28// tipperStats is the persisted, accumulated record for one tipper.
 29type tipperStats struct {
 30	addr        address
 31	total       int64
 32	count       int
 33	lastMessage string
 34	lastHeight  int64
 35}
 36
 37// tipEntry is one line in the recent-tips feed.
 38type tipEntry struct {
 39	from    address
 40	amount  int64
 41	message string
 42	height  int64
 43}
 44
 45var (
 46	owner address // captured deployer, set once in init
 47
 48	tippers avl.Tree // address string -> *tipperStats
 49	recent  []tipEntry
 50
 51	totalReceived int64
 52	withdrawn     int64
 53	tipCount      int
 54)
 55
 56func init() {
 57	owner = unsafe.OriginCaller()
 58}
 59
 60// get returns the stored *tipperStats for addr, creating one if absent.
 61func get(addr address) *tipperStats {
 62	key := addr.String()
 63	if v := tippers.Get(key); v != nil {
 64		return v.(*tipperStats)
 65	}
 66	ts := &tipperStats{addr: addr}
 67	tippers.Set(key, ts)
 68	return ts
 69}
 70
 71// Tip credits the caller's OriginSend ugnot to the jar and records it
 72// against their running total. Only an EOA calling directly via MsgCall may
 73// tip — see the payment-guard note on IsUserCall vs IsUser.
 74func Tip(cur realm, message string) string {
 75	if !cur.IsCurrent() {
 76		panic("spoofed realm")
 77	}
 78	prev := cur.Previous()
 79	if !prev.IsUserCall() {
 80		panic("only an EOA via MsgCall can tip")
 81	}
 82	if len(message) > maxMessageLen {
 83		panic("message too long (max " + strconv.Itoa(maxMessageLen) + " chars)")
 84	}
 85
 86	amount := unsafe.OriginSend().AmountOf(denom)
 87	if amount <= 0 {
 88		panic("send some ugnot to tip")
 89	}
 90
 91	from := prev.Address()
 92	height := runtime.ChainHeight()
 93
 94	ts := get(from)
 95	ts.total += amount
 96	ts.count++
 97	ts.lastMessage = message
 98	ts.lastHeight = height
 99
100	totalReceived += amount
101	tipCount++
102
103	recent = append(recent, tipEntry{from: from, amount: amount, message: message, height: height})
104	if len(recent) > maxRecent {
105		recent = recent[len(recent)-maxRecent:]
106	}
107
108	chain.Emit("Tip", "from", from.String(), "amount", strconv.FormatInt(amount, 10), "message", message)
109	return "thanks for the " + strconv.FormatInt(amount, 10) + "ugnot tip!"
110}
111
112// Balance reports the ugnot still held by the jar (received minus withdrawn).
113func Balance() int64 {
114	return totalReceived - withdrawn
115}
116
117// Withdraw sends amount ugnot from the jar to the owner. Owner-only.
118func Withdraw(cur realm, amount int64) {
119	if !cur.IsCurrent() {
120		panic("spoofed realm")
121	}
122	if cur.Previous().Address() != owner {
123		panic("only the owner can withdraw")
124	}
125	if amount <= 0 {
126		panic("amount must be positive")
127	}
128	if amount > Balance() {
129		panic("amount exceeds available balance")
130	}
131
132	bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
133	bnk.SendCoins(cur.Address(), owner, chain.NewCoins(chain.NewCoin(denom, amount)))
134	withdrawn += amount
135
136	chain.Emit("Withdraw", "amount", strconv.FormatInt(amount, 10))
137}
138
139// byTotal implements sort.Interface, ranking tippers by total tipped
140// descending, ties broken by tip count then address for a fully
141// deterministic order.
142type byTotal struct {
143	stats []*tipperStats
144}
145
146func (b byTotal) Len() int      { return len(b.stats) }
147func (b byTotal) Swap(i, j int) { b.stats[i], b.stats[j] = b.stats[j], b.stats[i] }
148func (b byTotal) Less(i, j int) bool {
149	if b.stats[i].total != b.stats[j].total {
150		return b.stats[i].total > b.stats[j].total
151	}
152	if b.stats[i].count != b.stats[j].count {
153		return b.stats[i].count > b.stats[j].count
154	}
155	return b.stats[i].addr.String() < b.stats[j].addr.String()
156}
157
158// leaderboard collects all tippers ranked by total tipped descending.
159func leaderboard() []*tipperStats {
160	stats := make([]*tipperStats, 0, tippers.Size())
161	tippers.Iterate("", "", func(_ string, v any) bool {
162		stats = append(stats, v.(*tipperStats))
163		return false
164	})
165	sort.Stable(byTotal{stats: stats})
166	return stats
167}
168
169// display returns a shortened address for table display.
170func display(addr address) string {
171	s := addr.String()
172	if len(s) > 12 {
173		return s[:8] + "…" + s[len(s)-4:]
174	}
175	return s
176}
177
178// Render shows the jar's balance, the tipper leaderboard, and a feed of the
179// most recent tips.
180func Render(path string) string {
181	out := "# 🫙 Tip Jar\n\n"
182	out += "Send ugnot with `Tip(message)` to leave a tip and a note. " +
183		"The owner can withdraw the accumulated balance with `Withdraw(amount)`.\n\n"
184
185	out += "**Balance:** " + strconv.FormatInt(Balance(), 10) + "ugnot" +
186		"  ·  **Total tips:** " + strconv.Itoa(tipCount) +
187		"  ·  **All-time received:** " + strconv.FormatInt(totalReceived, 10) + "ugnot\n\n"
188
189	stats := leaderboard()
190	out += "## Leaderboard\n\n"
191	if len(stats) == 0 {
192		out += "_No tips yet. Be the first!_\n\n"
193	} else {
194		out += "| Rank | Tipper | Total tipped | Tips |\n"
195		out += "| ---: | :--- | ---: | ---: |\n"
196		for i, ts := range stats {
197			rankCell := ui.Podium(i)
198			if rankCell == "" {
199				rankCell = strconv.Itoa(i + 1)
200			}
201			out += "| " + rankCell +
202				" | " + display(ts.addr) +
203				" | " + strconv.FormatInt(ts.total, 10) + "ugnot" +
204				" | " + strconv.Itoa(ts.count) + " |\n"
205		}
206		out += "\n"
207	}
208
209	out += "## Recent tips\n\n"
210	if len(recent) == 0 {
211		out += "_Nothing yet._\n"
212		return out
213	}
214	for i := len(recent) - 1; i >= 0; i-- {
215		e := recent[i]
216		out += "- **" + display(e.from) + "** tipped " + strconv.FormatInt(e.amount, 10) +
217			"ugnot at block " + strconv.FormatInt(e.height, 10)
218		if e.message != "" {
219			out += ": _" + e.message + "_"
220		}
221		out += "\n"
222	}
223	return out
224}