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