1// Package meta stores off-pad token presentation data (description, image, socials).2// Independent of pad versions — upgrade without redeploying markets.3//4// Key = padPkg + "|" + launchId5// First SetMeta caller becomes owner; only owner may update later.6package meta78import (9 "chain"10 "chain/runtime"11 "strconv"12 "strings"1314 "gno.land/p/nt/avl/v0"15)1617const (18 maxDescLen = 50019 maxURILen = 20020 maxHandleLen = 6421 maxPadPathLen = 20022 maxLaunchId = 3223)2425// TokenMeta is presentation data for one launch (not trading state).26type TokenMeta struct {27 PadPkg string28 LaunchID string29 Owner address30 Description string31 ImageURI string32 Website string33 Twitter string34 Telegram string35 Updated int6436}3738var (39 // byKey: pad|id -> *TokenMeta40 byKey avl.Tree41 inited bool42)4344func Init(cur realm) {45 if inited {46 panic("meta: already initialized")47 }48 if !cur.Previous().IsUserCall() {49 panic("meta: EOA only")50 }51 inited = true52 byKey = avl.Tree{}53 chain.Emit("Init", "by", cur.Previous().Address().String())54}5556func ensure() {57 if !inited {58 byKey = avl.Tree{}59 inited = true60 }61}6263func makeKey(padPkg, launchID string) string {64 return padPkg + "|" + launchID65}6667func sanitizeText(s string, max int) string {68 s = strings.TrimSpace(s)69 if len(s) > max {70 s = s[:max]71 }72 out := ""73 for _, c := range s {74 if c >= 0x20 && c != 0x7f {75 out += string(c)76 }77 }78 return out79}8081func validateURI(uri string) {82 if uri == "" {83 return84 }85 if !(strings.HasPrefix(uri, "https://") || strings.HasPrefix(uri, "http://") || strings.HasPrefix(uri, "ipfs://")) {86 panic("meta: uri must be http(s) or ipfs")87 }88}8990func validatePadPkg(padPkg string) {91 padPkg = strings.TrimSpace(padPkg)92 if padPkg == "" || len(padPkg) > maxPadPathLen {93 panic("meta: invalid pad path")94 }95 if !strings.HasPrefix(padPkg, "gno.land/r/") {96 panic("meta: pad must be gno.land/r/…")97 }98}99100// SetMeta upserts metadata for a launch. First writer owns the entry.101func SetMeta(cur realm, padPkg, launchID, description, imageURI, website, twitter, telegram string) {102 if !cur.Previous().IsUserCall() {103 panic("meta: EOA only")104 }105 ensure()106 validatePadPkg(padPkg)107 launchID = sanitizeText(launchID, maxLaunchId)108 if launchID == "" {109 panic("meta: launch id required")110 }111 description = sanitizeText(description, maxDescLen)112 imageURI = sanitizeText(imageURI, maxURILen)113 website = sanitizeText(website, maxURILen)114 twitter = sanitizeText(twitter, maxHandleLen)115 telegram = sanitizeText(telegram, maxHandleLen)116 validateURI(imageURI)117 validateURI(website)118119 key := makeKey(padPkg, launchID)120 caller := cur.Previous().Address()121 existing := byKey.Get(key)122 if existing != nil {123 tm := existing.(*TokenMeta)124 if tm.Owner != caller {125 panic("meta: not owner")126 }127 tm.Description = description128 tm.ImageURI = imageURI129 tm.Website = website130 tm.Twitter = twitter131 tm.Telegram = telegram132 tm.Updated = runtime.ChainHeight()133 byKey.Set(key, tm)134 } else {135 tm := &TokenMeta{136 PadPkg: padPkg,137 LaunchID: launchID,138 Owner: caller,139 Description: description,140 ImageURI: imageURI,141 Website: website,142 Twitter: twitter,143 Telegram: telegram,144 Updated: runtime.ChainHeight(),145 }146 byKey.Set(key, tm)147 }148 chain.Emit("SetMeta", "key", key, "owner", caller.String())149}150151// GetMeta returns:152//153// owner|description|imageURI|website|twitter|telegram|updated154//155// Empty string if none.156func GetMeta(padPkg, launchID string) string {157 ensure()158 key := makeKey(strings.TrimSpace(padPkg), strings.TrimSpace(launchID))159 v := byKey.Get(key)160 if v == nil {161 return ""162 }163 tm := v.(*TokenMeta)164 return tm.Owner.String() + "|" +165 tm.Description + "|" +166 tm.ImageURI + "|" +167 tm.Website + "|" +168 tm.Twitter + "|" +169 tm.Telegram + "|" +170 strconv.FormatInt(tm.Updated, 10)171}172173// HasMeta reports whether metadata exists.174func HasMeta(padPkg, launchID string) bool {175 ensure()176 return byKey.Has(makeKey(strings.TrimSpace(padPkg), strings.TrimSpace(launchID)))177}178179// MetaCount returns number of entries.180func MetaCount() int {181 ensure()182 return byKey.Size()183}184185// ClearMeta removes caller's owned entry.186func ClearMeta(cur realm, padPkg, launchID string) {187 if !cur.Previous().IsUserCall() {188 panic("meta: EOA only")189 }190 ensure()191 key := makeKey(strings.TrimSpace(padPkg), strings.TrimSpace(launchID))192 v := byKey.Get(key)193 if v == nil {194 return195 }196 tm := v.(*TokenMeta)197 if tm.Owner != cur.Previous().Address() {198 panic("meta: not owner")199 }200 byKey.Remove(key)201 chain.Emit("ClearMeta", "key", key)202}203204func sanitizeMD(s string) string {205 s = strings.ReplaceAll(s, "`", "'")206 s = strings.ReplaceAll(s, "<", "")207 s = strings.ReplaceAll(s, ">", "")208 return s209}210211func Render(path string) string {212 path = strings.Trim(path, "/")213 ensure()214 if path == "" {215 return "# gnomi meta\n\n- Entries: **" + strconv.Itoa(byKey.Size()) + "**\n\n" +216 "SetMeta(padPkg, launchID, description, imageURI, website, twitter, telegram)\n"217 }218 return "> [!WARNING]\n> Path not found: " + sanitizeMD(path) + "\n"219}220221func resetForTest() {222 byKey = avl.Tree{}223 inited = false224}225ClearMeta(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}, padPkg string, launchID string)
GetMeta(padPkg string, launchID string) string
HasMeta(padPkg string, launchID string) bool
Init(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})
MetaCount() int
Render(path string) string
SetMeta(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}, padPkg string, launchID string, description string, imageURI string, website string, twitter string, telegram 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.