Search Apps Documentation Source Content File Folder Download Copy Actions Download

guestbook.gno

3.95 Kb · 146 lines
  1// Package guestbook is a public, on-chain guestbook realm for gno.land.
  2//
  3// Anyone can sign the guestbook with a short message via the Sign crossing
  4// function. Every entry records the caller's address, the message, and the
  5// block height at which it was signed. Render lists all entries newest-first
  6// as a Markdown table.
  7package guestbook
  8
  9import (
 10	"strconv"
 11	"strings"
 12
 13	"chain"
 14	"chain/runtime"
 15
 16	"gno.land/p/nt/avl/v0"
 17)
 18
 19// Entry is a single signed guestbook line.
 20type Entry struct {
 21	ID      int    // 1-based sequence number
 22	Author  string // bech32 address of the signer
 23	Message string // the message left by the signer
 24	Height  int64  // block height at which the entry was signed
 25}
 26
 27const maxMessageLen = 280
 28
 29var (
 30	// entries is keyed by a zero-padded sequence so avl iteration is ordered.
 31	entries = avl.NewTree()
 32	count   int
 33)
 34
 35// Sign appends a new entry to the guestbook attributed to the immediate caller.
 36//
 37// It is a crossing function (gno 0.9 interrealm convention): callers invoke it
 38// as Sign(cross(cur), "hello"). The cur.IsCurrent() guard is the authentication
 39// primitive — without it a stale/forged realm value could spoof the author.
 40func Sign(cur realm, message string) {
 41	if !cur.IsCurrent() {
 42		panic("guestbook: spoofed realm; Sign must be called via cross(cur)")
 43	}
 44
 45	author := cur.Previous().Address().String()
 46	e := addEntry(author, message, runtime.ChainHeight())
 47
 48	chain.Emit("Signed",
 49		"id", strconv.Itoa(e.ID),
 50		"author", e.Author,
 51		"height", strconv.FormatInt(e.Height, 10),
 52	)
 53}
 54
 55// addEntry validates the message and appends an entry. Non-crossing internal
 56// helper so the append/validation logic is unit-testable without a realm frame.
 57// Panics (aborting the tx, reverting state) on an invalid message.
 58func addEntry(author, message string, height int64) *Entry {
 59	msg := strings.TrimSpace(message)
 60	if msg == "" {
 61		panic("guestbook: message must not be empty")
 62	}
 63	if len(msg) > maxMessageLen {
 64		panic("guestbook: message too long (max " + strconv.Itoa(maxMessageLen) + " bytes)")
 65	}
 66
 67	count++
 68	e := &Entry{
 69		ID:      count,
 70		Author:  author,
 71		Message: msg,
 72		Height:  height,
 73	}
 74	entries.Set(seqKey(count), e)
 75	return e
 76}
 77
 78// Count returns the total number of signatures. Read-only, non-crossing.
 79func Count() int {
 80	return count
 81}
 82
 83// Render lists every entry newest-first as a Markdown table plus a total count.
 84func Render(path string) string {
 85	var b strings.Builder
 86
 87	b.WriteString("# Guestbook\n\n")
 88
 89	if count == 0 {
 90		b.WriteString("_No one has signed yet. Be the first!_\n")
 91		return b.String()
 92	}
 93
 94	b.WriteString("**Total signatures:** ")
 95	b.WriteString(strconv.Itoa(count))
 96	b.WriteString("\n\n")
 97
 98	b.WriteString("| # | Who | Message | Height |\n")
 99	b.WriteString("|---|-----|---------|--------|\n")
100
101	// avl iterates ascending by key; ReverseIterate gives newest-first.
102	entries.ReverseIterate("", "", func(_ string, v any) bool {
103		e := v.(*Entry)
104		b.WriteString("| ")
105		b.WriteString(strconv.Itoa(e.ID))
106		b.WriteString(" | ")
107		b.WriteString(shortAddr(e.Author))
108		b.WriteString(" | ")
109		b.WriteString(escapeCell(e.Message))
110		b.WriteString(" | ")
111		b.WriteString(strconv.FormatInt(e.Height, 10))
112		b.WriteString(" |\n")
113		return false
114	})
115
116	return b.String()
117}
118
119// seqKey renders a sequence number as a fixed-width, zero-padded string so
120// avl's lexicographic key order matches numeric order.
121func seqKey(n int) string {
122	s := strconv.Itoa(n)
123	const width = 16
124	if len(s) >= width {
125		return s
126	}
127	return strings.Repeat("0", width-len(s)) + s
128}
129
130// shortAddr abbreviates a bech32 address for compact display.
131func shortAddr(addr string) string {
132	if len(addr) <= 12 {
133		return addr
134	}
135	return addr[:8] + "…" + addr[len(addr)-4:]
136}
137
138// escapeCell neutralizes characters that would break a Markdown table cell.
139func escapeCell(s string) string {
140	s = strings.ReplaceAll(s, "\\", "\\\\")
141	s = strings.ReplaceAll(s, "|", "\\|")
142	s = strings.ReplaceAll(s, "\r\n", " ")
143	s = strings.ReplaceAll(s, "\n", " ")
144	s = strings.ReplaceAll(s, "\r", " ")
145	return s
146}