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}