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

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