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.

posting.gno

14.34 Kb · 430 lines
  1package kourt
  2import (
  3	"chain"
  4	"strconv"
  5	"strings"
  6	sanitize "gno.land/p/nt/markdown/sanitize/v0"
  7)
  8const (
  9	ladderT1BpsInit = int64(5)
 10	ladderT2BpsInit = int64(25)
 11	ladderT3BpsInit = int64(100)
 12	postsEntryInit = int64(3)
 13	postsT2Init    = int64(10)
 14	postsT3Init    = int64(30)
 15	ladderTopRatio = int64(10)
 16	passPriceMinCC = int64(100_000)
 17	passPriceBps   = int64(1)
 18	passPriceOfT3 = int64(100)
 19)
 20func mustPostingInvariants() {
 21	if postsT3Init != postsEntryInit*ladderTopRatio {
 22		panic("kourtv2: the top rung must be exactly ladderTopRatio x entry — the ratio is the bound, not a suggestion")
 23	}
 24	if !(0 < ladderT1BpsInit && ladderT1BpsInit < ladderT2BpsInit && ladderT2BpsInit < ladderT3BpsInit) {
 25		panic("kourtv2: the ladder must be strictly increasing")
 26	}
 27	if !(0 < postsEntryInit && postsEntryInit < postsT2Init && postsT2Init < postsT3Init) {
 28		panic("kourtv2: the rates must be strictly increasing")
 29	}
 30	if ladderT3BpsInit < passPriceOfT3*passPriceBps {
 31		panic("kourtv2: t3 must be at least 100x the pass price, or a unit of standing is worth more than the flag lane's burn can cover")
 32	}
 33}
 34func init() { mustPostingInvariants() }
 35type ladder struct {
 36	t1Bps, t2Bps, t3Bps int64
 37	postsEntry, postsT2 int64
 38}
 39var ladderDefault = ladder{
 40	t1Bps: ladderT1BpsInit, t2Bps: ladderT2BpsInit, t3Bps: ladderT3BpsInit,
 41	postsEntry: postsEntryInit, postsT2: postsT2Init,
 42}
 43func (l ladder) postsT3() int64 { return l.postsEntry * ladderTopRatio }
 44func ladderFor(c *Court) ladder {
 45	if c.ladder != nil {
 46		return *c.ladder
 47	}
 48	return ladderDefault
 49}
 50func mustLadder(l ladder) {
 51	if !(0 < l.t1Bps && l.t1Bps < l.t2Bps && l.t2Bps < l.t3Bps && l.t3Bps <= 10_000) {
 52		panic("kourtv2: the ladder must be strictly increasing within 0..10000 bps")
 53	}
 54	if !(0 < l.postsEntry && l.postsEntry < l.postsT2 && l.postsT2 < l.postsT3()) {
 55		panic("kourtv2: the posting rates must be strictly increasing")
 56	}
 57	if l.t3Bps < passPriceOfT3*passPriceBps {
 58		panic("kourtv2: t3 must stay at least 100x the pass price — lowering it alone would make a unit of standing worth more than the flag lane's burn can cover")
 59	}
 60}
 61func SetLadderDefault(cur realm, t1Bps, t2Bps, t3Bps, postsEntry, postsT2 int64) {
 62	if !cur.IsCurrent() {
 63		panic(errStaleRealm)
 64	}
 65	d := ensureGlobalDAO()
 66	if cur.Previous().Address() != d.admin {
 67		panic("kourtv2: only the global DAO admin sets the default ladder")
 68	}
 69	l := ladder{t1Bps, t2Bps, t3Bps, postsEntry, postsT2}
 70	mustLadder(l)
 71	ladderDefault = l
 72	chain.Emit(globalActEvent,
 73		"court", "*",
 74		"claim", "0",
 75		"act", "set-ladder-default:"+ladderKey(l),
 76		"by", cur.Previous().Address().String(),
 77		"height", eventHeight(),
 78	)
 79}
 80func SetCourtLadder(cur realm, courtSlug string, t1Bps, t2Bps, t3Bps, postsEntry, postsT2 int64) {
 81	if !cur.IsCurrent() {
 82		panic(errStaleRealm)
 83	}
 84	who := cur.Previous().Address()
 85	c := mustCourt(courtSlug)
 86	cm := ensureMod(c)
 87	requireActiveMod(cm, who)
 88	l := ladder{t1Bps, t2Bps, t3Bps, postsEntry, postsT2}
 89	mustLadder(l)
 90	if fire, _ := approveAction(cm.pending, "setladder:"+ladderKey(l), who, "",
 91		speechM(cm)); !fire {
 92		return
 93	}
 94	c.ladder = &l
 95	cm.appendCourtLog(who, "setladder:"+ladderKey(l), "")
 96	emitModAct(c.id, 0, "setladder", who)
 97}
 98func ladderKey(l ladder) string {
 99	return strconv.FormatInt(l.t1Bps, 10) + ":" + strconv.FormatInt(l.t2Bps, 10) +
100		":" + strconv.FormatInt(l.t3Bps, 10) + ":" +
101		strconv.FormatInt(l.postsEntry, 10) + ":" + strconv.FormatInt(l.postsT2, 10)
102}
103func ClearCourtLadder(cur realm, courtSlug string) {
104	if !cur.IsCurrent() {
105		panic(errStaleRealm)
106	}
107	who := cur.Previous().Address()
108	c := mustCourt(courtSlug)
109	cm := ensureMod(c)
110	requireMod(cm, who)
111	if c.ladder == nil {
112		panic("kourtv2: this court has no ladder override")
113	}
114	if fire, _ := approveAction(cm.pending, "clearladder:"+c.id, who, "",
115		speechM(cm)); !fire {
116		return
117	}
118	c.ladder = nil
119	cm.appendCourtLog(who, "clearladder", "")
120	emitModAct(c.id, 0, "clearladder", who)
121}
122func boardSupply(c *Court) (int64, bool) {
123	if c.coin.Epoch() < 2 {
124		return 0, false
125	}
126	return c.coin.PastTotal(c.coin.Epoch() - 1), true
127}
128func passPriceFor(c *Court) int64 {
129	sup, sealed := boardSupply(c)
130	if !sealed {
131		return 0
132	}
133	p := sup * passPriceBps / 10_000
134	if p < passPriceMinCC {
135		p = passPriceMinCC
136	}
137	if lid := sup * ladderFor(c).t3Bps / 10_000 / passPriceOfT3; p > lid {
138		p = lid
139	}
140	if p < 0 {
141		p = 0
142	}
143	return p
144}
145func PassPrice(courtSlug string) int64 { return passPriceFor(mustCourt(courtSlug)) }
146func Ladder(courtSlug string) string {
147	c := mustCourt(courtSlug)
148	l := ladderFor(c)
149	inherited := "1"
150	if c.ladder != nil {
151		inherited = "0"
152	}
153	return "t1:" + strconv.FormatInt(l.t1Bps, 10) +
154		";t2:" + strconv.FormatInt(l.t2Bps, 10) +
155		";t3:" + strconv.FormatInt(l.t3Bps, 10) +
156		";entry:" + strconv.FormatInt(l.postsEntry, 10) +
157		";l2:" + strconv.FormatInt(l.postsT2, 10) +
158		";l3:" + strconv.FormatInt(l.postsT3(), 10) +
159		";inherited:" + inherited
160}
161func postLevel(c *Court, who address) int64 {
162	l := ladderFor(c)
163	sup, sealed := boardSupply(c)
164	if !sealed {
165		return 0
166	}
167	var s int64
168	if r := lookupStanding(c, who); r != nil {
169		s = r.score
170	}
171	switch {
172	case s >= sup*l.t3Bps/10_000:
173		return 3
174	case s >= sup*l.t2Bps/10_000:
175		return 2
176	case s >= sup*l.t1Bps/10_000:
177		return 1
178	}
179	if r := lookupStanding(c, who); r != nil && r.passHeld {
180		return 1
181	}
182	return 0
183}
184func postsPerDayAt(l ladder, level int64) int64 {
185	switch level {
186	case 3:
187		return l.postsT3()
188	case 2:
189		return l.postsT2
190	case 1:
191		return l.postsEntry
192	}
193	return 0
194}
195func PostLevel(courtSlug string, who address) int64 {
196	return postLevel(mustCourt(courtSlug), who)
197}
198func PostsPerDay(courtSlug string, who address) int64 {
199	c := mustCourt(courtSlug)
200	return postsPerDayAt(ladderFor(c), postLevel(c, who))
201}
202func BuyCommentPass(cur realm, courtSlug string) int64 {
203	if !cur.IsCurrent() {
204		panic(errStaleRealm)
205	}
206	who := cur.Previous().Address()
207	c := mustCourt(courtSlug)
208	touch(c)
209	if r := lookupStanding(c, who); r != nil && r.passHeld {
210		panic("kourtv2: this address already holds a pass in this court")
211	}
212	price := passPriceFor(c)
213	if price <= 0 {
214		if _, sealed := boardSupply(c); !sealed {
215			panic("kourtv2: this court has not completed a sealed epoch yet; its board is not open")
216		}
217		panic("kourtv2: this court cannot price a pass yet — it has no supply")
218	}
219	mustSpendable(c, who, price)
220	c.coin.Burn(who, price)
221	getStanding(c, who).passHeld = true
222	return price
223}
224func HoldsPass(courtSlug string, who address) bool {
225	r := lookupStanding(mustCourt(courtSlug), who)
226	return r != nil && r.passHeld
227}
228const secsPerDay = int64(86_400)
229func bucketAt(tokens, lastRefill, rate, now int64) (avail, stamp int64) {
230	if rate <= 0 {
231		return 0, lastRefill
232	}
233	if lastRefill == 0 {
234		return rate, now
235	}
236	stamp = lastRefill
237	if gain := (now - lastRefill) * rate / secsPerDay; gain > 0 {
238		tokens = satAdd(tokens, gain)
239		stamp += gain * secsPerDay / rate
240	}
241	if tokens > rate {
242		tokens = rate
243	}
244	if tokens < 0 {
245		tokens = 0
246	}
247	return tokens, stamp
248}
249func refill(c *Court, who address, r *standingRow) int64 {
250	rate := postsPerDayAt(ladderFor(c), postLevel(c, who))
251	r.tokens, r.lastRefill = bucketAt(r.tokens, r.lastRefill, rate, nowTime())
252	return r.tokens
253}
254func mustSpendPost(c *Court, who address) {
255	lvl := postLevel(c, who)
256	if lvl == 0 {
257		if _, sealed := boardSupply(c); !sealed {
258			panic("kourtv2: this court has not completed a sealed epoch yet; its board is not open")
259		}
260		panic("kourtv2: you cannot post in this court yet — earn standing, or buy an entry pass")
261	}
262	r := getStanding(c, who)
263	if refill(c, who, r) < 1 {
264		panic("kourtv2: your posting allowance for now is spent — level " +
265			strconv.FormatInt(lvl, 10) + " allows " +
266			strconv.FormatInt(postsPerDayAt(ladderFor(c), lvl), 10) + " a day")
267	}
268	r.tokens--
269}
270func PostsAvailable(courtSlug string, who address) int64 {
271	c := mustCourt(courtSlug)
272	r := lookupStanding(c, who)
273	if r == nil {
274		return 0
275	}
276	avail, _ := bucketAt(r.tokens, r.lastRefill, postsPerDayAt(ladderFor(c), postLevel(c, who)), nowTime())
277	return avail
278}
279func renderStanding(slug, addrArg string) string {
280	c := mustCourt(slug)
281	who := address(addrArg)
282	if !who.IsValid() {
283		return "## Not found\nThat is not a valid address."
284	}
285	l := ladderFor(c)
286	sup, sealed := boardSupply(c)
287	lvl := postLevel(c, who)
288	var b strings.Builder
289	b.WriteString("# Standing in " + courtNameFor(c) + "\n\n")
290	b.WriteString("`" + sanitize.InlineText(who.String()) + "`\n\n")
291	if !sealed {
292		b.WriteString("This court is too new to have a board. Its thresholds are quoted\n" +
293			"against a sealed epoch and it has not completed one yet; comments open\n" +
294			"as soon as it has.\n\n")
295		b.WriteString(helpLink + "\n")
296		return b.String()
297	}
298	var score, hw int64
299	if r := lookupStanding(c, who); r != nil {
300		score, hw = r.score, r.highWater
301	}
302	frozenAt, frozenUntil := boardFrozenAt(c, who)
303	if frozenUntil != 0 {
304		when := "block " + strconv.FormatInt(frozenUntil, 10)
305		if frozenAt != 0 {
306			when = strconv.FormatInt(frozenAt, 10) + " (" + when + ")"
307		}
308		b.WriteString("> **This court's moderators have frozen you out of its boards**\n" +
309			"> until " + when + ".\n>\n" +
310			"> It reaches comments and upvotes and nothing else: you may still stake,\n" +
311			"> vote, answer, dispute, flag, open claims, withdraw, draw emission, and\n" +
312			"> take every election action. An entry pass will not lift it, and buying\n" +
313			"> one now would burn coin for nothing.\n>\n" +
314			"> The court's [moderation log](/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + slug +
315			"/mod) records who acted and why.\n\n")
316	}
317	b.WriteString("## What you may do\n\n")
318	b.WriteString("- level: **" + strconv.FormatInt(lvl, 10) + "** of 3\n")
319	b.WriteString("- comments: **" + strconv.FormatInt(postsPerDayAt(l, lvl), 10) +
320		" a day**, " + strconv.FormatInt(PostsAvailable(slug, who), 10) + " available now\n")
321	b.WriteString("- per claim: at most " + strconv.Itoa(maxBoardRowsPerAuthor) + " comments\n")
322	if lvl == 0 {
323		if hw > score {
324			b.WriteString("\nA moderator slash took your standing to zero, and with it the\n" +
325				"entry pass if you held one — the burn is not refunded. A successor\n" +
326				"set, or the platform, can restore up to the high-water mark below.\n")
327		}
328		if frozenUntil == 0 {
329			if hw > score {
330				b.WriteString("You can also earn back from zero, or buy a new pass for " +
331					strconv.FormatInt(passPriceFor(c), 10) + " " + qualifiedSymbol(c) + ".\n")
332			} else {
333				b.WriteString("\nYou cannot comment here yet. Earn standing below, or buy an entry\n" +
334					"pass for " + strconv.FormatInt(passPriceFor(c), 10) + " " +
335					qualifiedSymbol(c) + " (burned, one-off).\n")
336			}
337		}
338	}
339	b.WriteString("\n## Standing\n\n")
340	b.WriteString("- earned: **" + strconv.FormatInt(score, 10) + "**\n")
341	b.WriteString("- high-water mark: " + strconv.FormatInt(hw, 10) +
342		" — what a moderator slash can be restored toward\n")
343	b.WriteString("- entry pass: " + yesNo(HoldsPass(slug, who)) + "\n")
344	writeStandingFrom(&b, c, who)
345	b.WriteString("\n## The rungs, in this court\n\n")
346	for _, k := range []struct {
347		name string
348		bps  int64
349		rate int64
350	}{{"1", l.t1Bps, l.postsEntry}, {"2", l.t2Bps, l.postsT2}, {"3", l.t3Bps, l.postsT3()}} {
351		b.WriteString("- level " + k.name + ": " + strconv.FormatInt(sup*k.bps/10_000, 10) +
352			" standing (" + strconv.FormatInt(k.bps, 10) + " bps of supply) → " +
353			strconv.FormatInt(k.rate, 10) + " a day\n")
354	}
355	b.WriteString("\nEach rung is a **share of this court's supply**, not a fixed " +
356		"number, so the bar rises as the court grows. Standing itself is never " +
357		"taken except by a logged moderator slash — but a score that does not " +
358		"move can still fall below a rung it used to clear.\n")
359	b.WriteString("\n## How standing is earned\n\n")
360	b.WriteString("Only where the court adjudicated something, and always in proportion\n" +
361		"to your own capital that the act committed or destroyed:\n\n")
362	b.WriteString("- a quality flag whose slash **settled** — the full bar, two thirds low\n")
363	b.WriteString("- **prevailing in a dispute** — an uphold against real opposition, or an overturn\n")
364	b.WriteString("- authoring a claim the court rated **HIGH** — seeded claims earn nothing\n")
365	b.WriteString("- **winning conviction** on a resolved claim; half where nobody disputed it\n\n")
366	b.WriteString("Never earned: commenting, being upvoted, buying " +
367		qualifiedSymbol(c) + ", or buying the pass.\n")
368	b.WriteString("Standing does not decay. A moderator may slash it for abuse; the\n")
369	b.WriteString("high-water mark above is what a successor set can restore.\n")
370	b.WriteString("\n## The answer record\n\n")
371	b.WriteString("- contested-and-upheld answers: **" +
372		strconv.FormatInt(int64(AnswerRecord(slug, who)), 10) + "**\n")
373	b.WriteString("\n_Adjudicated — moderators cannot change this._\n")
374	return b.String()
375}
376func renderStandingIndex(slug string) string {
377	c := mustCourt(slug)
378	l := ladderFor(c)
379	var b strings.Builder
380	b.WriteString("# Standing in " + courtNameFor(c) + "\n\n")
381	b.WriteString("To see an address's standing, put it on the end of this " +
382		"page's path: `/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt:" + sanitize.InlineText(slug) +
383		"/me/<address>`\n\n")
384	b.WriteString("An entry pass costs " + strconv.FormatInt(passPriceFor(c), 10) +
385		" " + qualifiedSymbol(c) + ", burned, once. Standing is earned and " +
386		"cannot be bought.\n\n")
387	b.WriteString("Standing is measured against this court's coin supply, in " +
388		"basis points — one bp is a hundredth of a percent.\n\n")
389	b.WriteString("- level 1 at " + strconv.FormatInt(l.t1Bps, 10) + " bps of supply → " +
390		strconv.FormatInt(l.postsEntry, 10) + " comments a day\n")
391	b.WriteString("- level 2 at " + strconv.FormatInt(l.t2Bps, 10) + " bps → " +
392		strconv.FormatInt(l.postsT2, 10) + " a day\n")
393	b.WriteString("- level 3 at " + strconv.FormatInt(l.t3Bps, 10) + " bps → " +
394		strconv.FormatInt(l.postsT3(), 10) + " a day\n")
395	return b.String()
396}
397func writeStandingFrom(b *strings.Builder, c *Court, who address) {
398	r := lookupStanding(c, who)
399	if r == nil {
400		return
401	}
402	labels := [standingCatN]string{
403		"quality flags whose slash settled",
404		"disputes you prevailed in",
405		"claims of yours the court rated HIGH",
406		"conviction on resolved claims",
407	}
408	listed := false
409	for i := 0; i < standingCatN; i++ {
410		if r.from[i] == 0 {
411			continue
412		}
413		if !listed {
414			b.WriteString("- from: ")
415			listed = true
416		} else {
417			b.WriteString("; ")
418		}
419		b.WriteString(strconv.FormatInt(r.from[i], 10) + " from " + labels[i])
420	}
421	if listed {
422		b.WriteString("\n")
423	}
424}
425func yesNo(b bool) string {
426	if b {
427		return "held"
428	}
429	return "none"
430}