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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

timecapsule.gno

5.01 Kb · 177 lines
  1// Package timecapsule is a public guestbook where every message is sealed
  2// until a future block height, then permanently unlocked for anyone to read.
  3package timecapsule
  4
  5import (
  6	"strconv"
  7	"strings"
  8
  9	"chain"
 10	"chain/markdown"
 11	"chain/runtime"
 12
 13	"gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/kit/store/v0"
 14	"gno.land/p/nt/markdown/sanitize/v0"
 15)
 16
 17// capsule carries no id field: the id belongs to the store, which hands it
 18// back on lookup and iteration.
 19type capsule struct {
 20	author       address
 21	message      string
 22	createdAt    int64
 23	unlockHeight int64
 24}
 25
 26// capsules assigns the capsule ids. v0 kept its own nextID plus an idKey()
 27// that called strings.Repeat WITHOUT the length guard the other realms had, so
 28// at the 10^12th capsule it did not mis-sort, it panicked: Repeat rejects a
 29// negative count, and the realm would have stopped accepting Leave calls. The
 30// store key is 8 fixed bytes with no width to outgrow.
 31var capsules = store.Named("capsule")
 32
 33const (
 34	maxDelayBlocks int64 = 1_000_000
 35	maxMessageLen  int   = 500
 36)
 37
 38// Leave seals a new message that stays hidden until `delayBlocks` blocks
 39// from now, then becomes publicly readable forever. Returns the capsule id.
 40func Leave(cur realm, message string, delayBlocks int64) int64 {
 41	if !cur.Previous().IsUserCall() {
 42		panic("only direct user calls can leave a capsule")
 43	}
 44
 45	message = strings.TrimSpace(message)
 46	if strings.TrimSpace(markdown.StripBidiAndZeroWidth(message)) == "" {
 47		panic("message must not be empty")
 48	}
 49	if len(message) > maxMessageLen {
 50		panic("message too long")
 51	}
 52	if delayBlocks <= 0 || delayBlocks > maxDelayBlocks {
 53		panic("delayBlocks out of range")
 54	}
 55
 56	now := runtime.ChainHeight()
 57	c := &capsule{
 58		author:       cur.Previous().Address(),
 59		message:      message,
 60		createdAt:    now,
 61		unlockHeight: now + delayBlocks,
 62	}
 63	id := capsules.Add(c)
 64
 65	chain.Emit("CapsuleSealed",
 66		"id", id.String(),
 67		"unlockHeight", strconv.FormatInt(c.unlockHeight, 10),
 68	)
 69	return int64(id)
 70}
 71
 72func Render(path string) string {
 73	switch {
 74	case path == "":
 75		return renderHome()
 76	case path == "sealed":
 77		return renderSealed()
 78	case strings.HasPrefix(path, "capsule/"):
 79		return renderCapsule(strings.TrimPrefix(path, "capsule/"))
 80	default:
 81		return "> [!WARNING]\n> Path not found\n"
 82	}
 83}
 84
 85func renderHome() string {
 86	now := runtime.ChainHeight()
 87
 88	revealed := 0
 89	sealed := 0
 90	capsules.Each(func(_ store.ID, value any) {
 91		if value.(*capsule).unlockHeight <= now {
 92			revealed++
 93		} else {
 94			sealed++
 95		}
 96	})
 97
 98	var out strings.Builder
 99	out.WriteString("# ⏳ Time Capsule Guestbook\n\n")
100	out.WriteString("Leave a message for the future. It stays sealed until its unlock block height, then anyone can read it — call `Leave(message, delayBlocks)`.\n\n")
101	out.WriteString(strconv.Itoa(revealed+sealed) + " capsule(s) total · " +
102		strconv.Itoa(revealed) + " unlocked · " + strconv.Itoa(sealed) + " still sealed")
103	if sealed > 0 {
104		out.WriteString(" (see [sealed](sealed))")
105	}
106	out.WriteString(".\n\n## Unlocked messages\n\n")
107
108	if revealed == 0 {
109		out.WriteString("_None yet. Be the first to leave one that will open later._\n")
110		return out.String()
111	}
112
113	capsules.Each(func(id store.ID, value any) {
114		c := value.(*capsule)
115		if c.unlockHeight <= now {
116			out.WriteString(renderEntry(id, c))
117		}
118	})
119	return out.String()
120}
121
122func renderSealed() string {
123	now := runtime.ChainHeight()
124
125	var out strings.Builder
126	out.WriteString("# Sealed Capsules\n\n")
127
128	found := false
129	capsules.Each(func(id store.ID, value any) {
130		c := value.(*capsule)
131		if c.unlockHeight > now {
132			found = true
133			out.WriteString("- capsule [#" + id.String() + "](capsule/" +
134				id.String() + ") by `" + c.author.String() +
135				"` — unlocks at block " + strconv.FormatInt(c.unlockHeight, 10) +
136				" (" + strconv.FormatInt(c.unlockHeight-now, 10) + " blocks left)\n")
137		}
138	})
139	if !found {
140		out.WriteString("_No sealed capsules right now._\n")
141	}
142	return out.String()
143}
144
145func renderCapsule(idStr string) string {
146	id, ok := store.ParseID(idStr)
147	if !ok {
148		return "> [!WARNING]\n> Invalid capsule id\n"
149	}
150	v, ok := capsules.Get(id)
151	if !ok {
152		return "> [!WARNING]\n> Capsule not found\n"
153	}
154	c := v.(*capsule)
155	now := runtime.ChainHeight()
156
157	var out strings.Builder
158	out.WriteString("# Capsule #" + id.String() + "\n\n")
159	out.WriteString("Sealed by `" + c.author.String() + "` at block " + strconv.FormatInt(c.createdAt, 10) + ".\n\n")
160
161	if c.unlockHeight > now {
162		out.WriteString("\U0001f512 Still sealed. Unlocks at block " + strconv.FormatInt(c.unlockHeight, 10) +
163			" (" + strconv.FormatInt(c.unlockHeight-now, 10) + " blocks left).\n")
164		return out.String()
165	}
166
167	out.WriteString("\U0001f513 Unlocked at block " + strconv.FormatInt(c.unlockHeight, 10) + ":\n\n> " +
168		sanitize.InlineText(c.message) + "\n")
169	return out.String()
170}
171
172func renderEntry(id store.ID, c *capsule) string {
173	return "- **#" + id.String() + "** by `" + c.author.String() +
174		"` (sealed at block " + strconv.FormatInt(c.createdAt, 10) +
175		", opened at block " + strconv.FormatInt(c.unlockHeight, 10) + "):\n  > " +
176		sanitize.InlineText(c.message) + "\n\n"
177}