Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

meta.gno

5.01 Kb · 224 lines
  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 + "|" + launchId
  5// First SetMeta caller becomes owner; only owner may update later.
  6package meta
  7
  8import (
  9	"chain"
 10	"chain/runtime"
 11	"strconv"
 12	"strings"
 13
 14	"gno.land/p/nt/avl/v0"
 15)
 16
 17const (
 18	maxDescLen    = 500
 19	maxURILen     = 200
 20	maxHandleLen  = 64
 21	maxPadPathLen = 200
 22	maxLaunchId   = 32
 23)
 24
 25// TokenMeta is presentation data for one launch (not trading state).
 26type TokenMeta struct {
 27	PadPkg      string
 28	LaunchID    string
 29	Owner       address
 30	Description string
 31	ImageURI    string
 32	Website     string
 33	Twitter     string
 34	Telegram    string
 35	Updated     int64
 36}
 37
 38var (
 39	// byKey: pad|id -> *TokenMeta
 40	byKey  avl.Tree
 41	inited bool
 42)
 43
 44func 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 = true
 52	byKey = avl.Tree{}
 53	chain.Emit("Init", "by", cur.Previous().Address().String())
 54}
 55
 56func ensure() {
 57	if !inited {
 58		byKey = avl.Tree{}
 59		inited = true
 60	}
 61}
 62
 63func makeKey(padPkg, launchID string) string {
 64	return padPkg + "|" + launchID
 65}
 66
 67func 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 out
 79}
 80
 81func validateURI(uri string) {
 82	if uri == "" {
 83		return
 84	}
 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}
 89
 90func 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}
 99
100// 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)
118
119	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 = description
128		tm.ImageURI = imageURI
129		tm.Website = website
130		tm.Twitter = twitter
131		tm.Telegram = telegram
132		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}
150
151// GetMeta returns:
152//
153//	owner|description|imageURI|website|twitter|telegram|updated
154//
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}
172
173// 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}
178
179// MetaCount returns number of entries.
180func MetaCount() int {
181	ensure()
182	return byKey.Size()
183}
184
185// 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		return
195	}
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}
203
204func sanitizeMD(s string) string {
205	s = strings.ReplaceAll(s, "`", "'")
206	s = strings.ReplaceAll(s, "<", "")
207	s = strings.ReplaceAll(s, ">", "")
208	return s
209}
210
211func Render(path string) string {
212	path = strings.Trim(path, "/")
213	ensure()
214	if path == "" {
215		return "# gnomemepad 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}
220
221func resetForTest() {
222	byKey = avl.Tree{}
223	inited = false
224}