1// Package timecapsule is a public guestbook where every message is sealed2// until a future block height, then permanently unlocked for anyone to read.3package timecapsule45import (6 "strconv"7 "strings"89 "chain"10 "chain/runtime"1112 "gno.land/p/nt/avl/v0"13 "gno.land/p/nt/markdown/sanitize/v0"14)1516type capsule struct {17 id int6418 author address19 message string20 createdAt int6421 unlockHeight int6422}2324var (25 capsules avl.Tree // key: zero-padded id -> *capsule26 nextID int6427)2829const (30 maxDelayBlocks int64 = 1_000_00031 maxMessageLen int = 50032)3334// Leave seals a new message that stays hidden until `delayBlocks` blocks35// from now, then becomes publicly readable forever. Returns the capsule id.36func Leave(cur realm, message string, delayBlocks int64) int64 {37 if !cur.Previous().IsUserCall() {38 panic("only direct user calls can leave a capsule")39 }4041 message = strings.TrimSpace(message)42 if message == "" {43 panic("message must not be empty")44 }45 if len(message) > maxMessageLen {46 panic("message too long")47 }48 if delayBlocks <= 0 || delayBlocks > maxDelayBlocks {49 panic("delayBlocks out of range")50 }5152 now := runtime.ChainHeight()53 id := nextID54 nextID++5556 c := &capsule{57 id: id,58 author: cur.Previous().Address(),59 message: message,60 createdAt: now,61 unlockHeight: now + delayBlocks,62 }63 capsules.Set(idKey(id), c)6465 chain.Emit("CapsuleSealed",66 "id", strconv.FormatInt(id, 10),67 "unlockHeight", strconv.FormatInt(c.unlockHeight, 10),68 )69 return id70}7172func idKey(id int64) string {73 s := strconv.FormatInt(id, 10)74 return strings.Repeat("0", 12-len(s)) + s75}7677func Render(path string) string {78 switch {79 case path == "":80 return renderHome()81 case path == "sealed":82 return renderSealed()83 case strings.HasPrefix(path, "capsule/"):84 return renderCapsule(strings.TrimPrefix(path, "capsule/"))85 default:86 return "> [!WARNING]\n> Path not found\n"87 }88}8990func renderHome() string {91 now := runtime.ChainHeight()9293 revealed := 094 sealed := 095 capsules.Iterate("", "", func(_ string, value any) bool {96 if value.(*capsule).unlockHeight <= now {97 revealed++98 } else {99 sealed++100 }101 return false102 })103104 var out strings.Builder105 out.WriteString("# ⏳ Time Capsule Guestbook\n\n")106 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")107 out.WriteString(strconv.Itoa(revealed+sealed) + " capsule(s) total · " +108 strconv.Itoa(revealed) + " unlocked · " + strconv.Itoa(sealed) + " still sealed")109 if sealed > 0 {110 out.WriteString(" (see [sealed](sealed))")111 }112 out.WriteString(".\n\n## Unlocked messages\n\n")113114 if revealed == 0 {115 out.WriteString("_None yet. Be the first to leave one that will open later._\n")116 return out.String()117 }118119 capsules.Iterate("", "", func(_ string, value any) bool {120 c := value.(*capsule)121 if c.unlockHeight <= now {122 out.WriteString(renderEntry(c))123 }124 return false125 })126 return out.String()127}128129func renderSealed() string {130 now := runtime.ChainHeight()131132 var out strings.Builder133 out.WriteString("# Sealed Capsules\n\n")134135 found := false136 capsules.Iterate("", "", func(_ string, value any) bool {137 c := value.(*capsule)138 if c.unlockHeight > now {139 found = true140 out.WriteString("- capsule [#" + strconv.FormatInt(c.id, 10) + "](capsule/" +141 strconv.FormatInt(c.id, 10) + ") by `" + c.author.String() +142 "` — unlocks at block " + strconv.FormatInt(c.unlockHeight, 10) +143 " (" + strconv.FormatInt(c.unlockHeight-now, 10) + " blocks left)\n")144 }145 return false146 })147 if !found {148 out.WriteString("_No sealed capsules right now._\n")149 }150 return out.String()151}152153func renderCapsule(idStr string) string {154 id, err := strconv.ParseInt(idStr, 10, 64)155 if err != nil {156 return "> [!WARNING]\n> Invalid capsule id\n"157 }158 v := capsules.Get(idKey(id))159 if v == nil {160 return "> [!WARNING]\n> Capsule not found\n"161 }162 c := v.(*capsule)163 now := runtime.ChainHeight()164165 var out strings.Builder166 out.WriteString("# Capsule #" + strconv.FormatInt(c.id, 10) + "\n\n")167 out.WriteString("Sealed by `" + c.author.String() + "` at block " + strconv.FormatInt(c.createdAt, 10) + ".\n\n")168169 if c.unlockHeight > now {170 out.WriteString("\U0001f512 Still sealed. Unlocks at block " + strconv.FormatInt(c.unlockHeight, 10) +171 " (" + strconv.FormatInt(c.unlockHeight-now, 10) + " blocks left).\n")172 return out.String()173 }174175 out.WriteString("\U0001f513 Unlocked at block " + strconv.FormatInt(c.unlockHeight, 10) + ":\n\n> " +176 sanitize.InlineText(c.message) + "\n")177 return out.String()178}179180func renderEntry(c *capsule) string {181 return "- **#" + strconv.FormatInt(c.id, 10) + "** by `" + c.author.String() +182 "` (sealed at block " + strconv.FormatInt(c.createdAt, 10) +183 ", opened at block " + strconv.FormatInt(c.unlockHeight, 10) + "):\n > " +184 sanitize.InlineText(c.message) + "\n\n"185}186Leave(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, delayBlocks int64) int64
Render(path string) 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.