Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

guestbook.gno

3.41 Kb · 105 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.
  7//
  8// v1 renders through [p/moul/kit/ui](/p/moul/kit/ui/v0) instead of the
  9// hand-rolled shortAddr and escapeCell helpers v0 carried. The escaping is the
 10// substantive change: v0 replaced four characters, while ui.Cell also strips
 11// bidi and zero-width characters, which a message can otherwise use to reorder
 12// how the rest of the row reads.
 13package guestbook
 14
 15import (
 16	"strconv"
 17	"strings"
 18
 19	"chain"
 20	"chain/runtime"
 21
 22	"gno.land/p/moul/kit/store/v0"
 23	"gno.land/p/moul/kit/ui/v0"
 24)
 25
 26// Entry is a single signed guestbook line. It carries no ID field: the id
 27// belongs to the store, which hands it back on lookup and iteration.
 28type Entry struct {
 29	Author  string // bech32 address of the signer
 30	Message string // the message left by the signer
 31	Height  int64  // block height at which the entry was signed
 32}
 33
 34const maxMessageLen = 280
 35
 36// entries assigns the sequence numbers. v1 kept its own count plus a seqKey()
 37// that zero-padded to width 16, the widest of the six hand-rolled variants in
 38// this repo and still a ceiling; the store key has none.
 39var entries = store.Named("guestbook: entry")
 40
 41// Sign appends a new entry to the guestbook attributed to the immediate caller.
 42//
 43// It is a crossing function (gno 0.9 interrealm convention): callers invoke it
 44// as Sign(cross(cur), "hello"). The cur.IsCurrent() guard is the authentication
 45// primitive — without it a stale/forged realm value could spoof the author.
 46func Sign(cur realm, message string) {
 47	if !cur.IsCurrent() {
 48		panic("guestbook: spoofed realm; Sign must be called via cross(cur)")
 49	}
 50
 51	author := cur.Previous().Address().String()
 52	id, e := addEntry(author, message, runtime.ChainHeight())
 53
 54	chain.Emit("Signed",
 55		"id", id.String(),
 56		"author", e.Author,
 57		"height", strconv.FormatInt(e.Height, 10),
 58	)
 59}
 60
 61// addEntry validates the message and appends an entry. Non-crossing internal
 62// helper so the append/validation logic is unit-testable without a realm frame.
 63// Panics (aborting the tx, reverting state) on an invalid message.
 64func addEntry(author, message string, height int64) (store.ID, *Entry) {
 65	msg := strings.TrimSpace(message)
 66	if msg == "" {
 67		panic("guestbook: message must not be empty")
 68	}
 69	if len(msg) > maxMessageLen {
 70		panic("guestbook: message too long (max " + strconv.Itoa(maxMessageLen) + " bytes)")
 71	}
 72
 73	e := &Entry{
 74		Author:  author,
 75		Message: msg,
 76		Height:  height,
 77	}
 78	return entries.Add(e), e
 79}
 80
 81// Count returns the total number of signatures. Read-only, non-crossing.
 82func Count() int {
 83	return entries.Len()
 84}
 85
 86// Render lists every entry newest-first as a Markdown table plus a total count.
 87func Render(path string) string {
 88	if entries.Len() == 0 {
 89		return "# Guestbook\n\n" + ui.Empty("No one has signed yet. Be the first!")
 90	}
 91
 92	t := ui.NewTable("#", "Who", "Message", "Height")
 93	// Ids ascend with signing, so reverse iteration is newest-first.
 94	entries.EachReverse(func(id store.ID, v any) {
 95		e := v.(*Entry)
 96		t.Row(
 97			id.String(),
 98			ui.AddrOf(e.Author),
 99			ui.Cell(e.Message),
100			strconv.FormatInt(e.Height, 10),
101		)
102	})
103
104	return "# Guestbook\n\n**Total signatures:** " + strconv.Itoa(entries.Len()) + "\n\n" + t.String()
105}