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

quotes.gno

5.01 Kb · 181 lines
  1// Package quotes is a community "Quote of the Moment" board realm.
  2//
  3// Anyone can submit a quote via the exported crossing function AddQuote.
  4// Each quote records its text, the address of the caller who added it, and
  5// the block height at submission time. Render deterministically picks a
  6// "featured" quote by the current block height (a stable index into the
  7// list), shows it as a blockquote, then lists every quote with its author
  8// and id below.
  9package quotes
 10
 11import (
 12	"strconv"
 13	"strings"
 14
 15	"chain"
 16	"chain/runtime"
 17)
 18
 19// quote is a single board entry. Author is stored as a plain string address
 20// (realm values are ephemeral and must never be persisted).
 21type quote struct {
 22	id     int
 23	text   string
 24	author string // bech32 address of the submitter, or "" for seeded quotes
 25	height int64  // block height at submission (0 for seeded quotes)
 26}
 27
 28// board is the persistent realm state. A plain slice is fine here: the list
 29// only ever grows by append and Render walks it in insertion order, which is
 30// deterministic.
 31var board []quote
 32
 33// init seeds the board so a fresh deploy renders nicely.
 34func init() {
 35	seed := []string{
 36		"The unexamined life is not worth living.",
 37		"We suffer more often in imagination than in reality.",
 38		"Waste no more time arguing about what a good person should be. Be one.",
 39		"He who is brave is free.",
 40	}
 41	for _, s := range seed {
 42		board = append(board, quote{
 43			id:     len(board),
 44			text:   s,
 45			author: "", // seeded
 46			height: 0,
 47		})
 48	}
 49}
 50
 51// AddQuote is the exported, state-mutating entry point. It follows the gno 0.9
 52// interrealm crossing convention: the first parameter is `cur realm`. Callers
 53// invoke it as AddQuote(cross(cur), "...") over a MsgCall.
 54func AddQuote(cur realm, text string) {
 55	// Authentication primitive: reject a stale/forged realm value before
 56	// deriving any caller identity from it.
 57	if !cur.IsCurrent() {
 58		panic("spoofed realm: cur is not the live crossing frame")
 59	}
 60
 61	if err := validate(text); err != nil {
 62		panic(err)
 63	}
 64
 65	author := cur.Previous().Address().String()
 66	h := runtime.ChainHeight()
 67
 68	board = append(board, quote{
 69		id:     len(board),
 70		text:   text,
 71		author: author,
 72		height: h,
 73	})
 74
 75	chain.Emit("QuoteAdded",
 76		"id", strconv.Itoa(len(board)-1),
 77		"author", author,
 78		"height", strconv.FormatInt(h, 10),
 79	)
 80}
 81
 82// maxQuoteLen caps quote length to keep storage costs bounded and Render tidy.
 83const maxQuoteLen = 280
 84
 85// validate enforces basic content rules. Returns an error describing the
 86// problem, or nil when the text is acceptable.
 87func validate(text string) error {
 88	if len(text) == 0 {
 89		return errEmpty
 90	}
 91	if len(text) > maxQuoteLen {
 92		return errTooLong
 93	}
 94	return nil
 95}
 96
 97// Sentinel errors for validation. Kept as package-level vars so tests can
 98// compare against them directly.
 99var (
100	errEmpty   = quoteError("quote text must not be empty")
101	errTooLong = quoteError("quote text exceeds " + strconv.Itoa(maxQuoteLen) + " characters")
102)
103
104// quoteError is a trivial string-backed error type (the VM has no host
105// stdlib errors package convenience beyond this).
106type quoteError string
107
108func (e quoteError) Error() string { return string(e) }
109
110// featuredIndex deterministically picks which quote is "of the moment" for a
111// given block height. Pure function of (height, count) so it is fully
112// deterministic and easy to test.
113func featuredIndex(height int64, count int) int {
114	if count <= 0 {
115		return -1
116	}
117	// Guard against a negative height (should not happen on-chain, but keeps
118	// the modulo well-defined).
119	if height < 0 {
120		height = -height
121	}
122	return int(height % int64(count))
123}
124
125// Render produces the Markdown page shown in gnoweb. It is NOT a crossing
126// function (no `cur realm` parameter) — it is a read-only view.
127func Render(path string) string {
128	count := len(board)
129	if count == 0 {
130		return "# Quote of the Moment\n\n_No quotes yet. Be the first to add one._\n"
131	}
132
133	h := runtime.ChainHeight()
134	idx := featuredIndex(h, count)
135	featured := board[idx]
136
137	var b strings.Builder
138	b.WriteString("# Quote of the Moment\n\n")
139
140	// Featured quote as a blockquote.
141	b.WriteString("> ")
142	b.WriteString(featured.text)
143	b.WriteString("\n>\n> — ")
144	b.WriteString(authorLabel(featured))
145	b.WriteString(" (#")
146	b.WriteString(strconv.Itoa(featured.id))
147	b.WriteString(")\n\n")
148
149	b.WriteString("_Featured pick rotates with block height ")
150	b.WriteString(strconv.FormatInt(h, 10))
151	b.WriteString(" — index ")
152	b.WriteString(strconv.Itoa(idx))
153	b.WriteString(" of ")
154	b.WriteString(strconv.Itoa(count))
155	b.WriteString("._\n\n")
156
157	// Full list.
158	b.WriteString("## All quotes (")
159	b.WriteString(strconv.Itoa(count))
160	b.WriteString(")\n\n")
161	for _, q := range board {
162		b.WriteString("- **#")
163		b.WriteString(strconv.Itoa(q.id))
164		b.WriteString("** ")
165		b.WriteString(q.text)
166		b.WriteString(" — _")
167		b.WriteString(authorLabel(q))
168		b.WriteString("_\n")
169	}
170
171	return b.String()
172}
173
174// authorLabel renders a human-friendly author string. Seeded quotes (empty
175// author) show as the house account.
176func authorLabel(q quote) string {
177	if q.author == "" {
178		return "the board"
179	}
180	return q.author
181}