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

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}