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 crossing4// function. Every entry records the caller's address, the message, and the5// block height at which it was signed. Render lists all entries newest-first6// as a Markdown table.7package guestbook89import (10 "strconv"11 "strings"1213 "chain"14 "chain/runtime"1516 "gno.land/p/nt/avl/v0"17)1819// Entry is a single signed guestbook line.20type Entry struct {21 ID int // 1-based sequence number22 Author string // bech32 address of the signer23 Message string // the message left by the signer24 Height int64 // block height at which the entry was signed25}2627const maxMessageLen = 2802829var (30 // entries is keyed by a zero-padded sequence so avl iteration is ordered.31 entries = avl.NewTree()32 count int33)3435// 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 it38// as Sign(cross(cur), "hello"). The cur.IsCurrent() guard is the authentication39// 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 }4445 author := cur.Previous().Address().String()46 e := addEntry(author, message, runtime.ChainHeight())4748 chain.Emit("Signed",49 "id", strconv.Itoa(e.ID),50 "author", e.Author,51 "height", strconv.FormatInt(e.Height, 10),52 )53}5455// addEntry validates the message and appends an entry. Non-crossing internal56// 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 }6667 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 e76}7778// Count returns the total number of signatures. Read-only, non-crossing.79func Count() int {80 return count81}8283// Render lists every entry newest-first as a Markdown table plus a total count.84func Render(path string) string {85 var b strings.Builder8687 b.WriteString("# Guestbook\n\n")8889 if count == 0 {90 b.WriteString("_No one has signed yet. Be the first!_\n")91 return b.String()92 }9394 b.WriteString("**Total signatures:** ")95 b.WriteString(strconv.Itoa(count))96 b.WriteString("\n\n")9798 b.WriteString("| # | Who | Message | Height |\n")99 b.WriteString("|---|-----|---------|--------|\n")100101 // 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 false114 })115116 return b.String()117}118119// seqKey renders a sequence number as a fixed-width, zero-padded string so120// avl's lexicographic key order matches numeric order.121func seqKey(n int) string {122 s := strconv.Itoa(n)123 const width = 16124 if len(s) >= width {125 return s126 }127 return strings.Repeat("0", width-len(s)) + s128}129130// shortAddr abbreviates a bech32 address for compact display.131func shortAddr(addr string) string {132 if len(addr) <= 12 {133 return addr134 }135 return addr[:8] + "…" + addr[len(addr)-4:]136}137138// 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 s146}147Count() int
Render(path string) string
Sign(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, message string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.