Realm
gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/factory
Overview
Realm Path
gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/factory
Exported Functions
19
State Entries
20
Source Files
2
Total Package Entries
52
Exported Functions
19 exported functions
State
20 state entries
Source Code
FILES
factory.gno
go
1package factory
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime/unsafe"
7 "strconv"
8 "strings"
9
10 "gno.land/p/nt/avl/v0"
11)
12
13const (
14 pkgPath = "gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/factory"
15 nativeDenom = "ugnot"
16 defaultLaunchFee = int64(10_000_000) // 10 GNOT
17 maxNameLen = 64
18 maxImageLen = 200
19 maxSupplyBound = 1_000_000
20 maxRoyaltyBps = 1000
21 maxSymbolLen = 11
22 maxSlugLen = 11
23 minSlugLen = 2
24)
25
26type collection struct {
27 slug string
28 name string
29 symbol string
30 cover string
31 pkg string
32 creator address
33 maxSupply int
34 mintPrice int64
35 minted int
36 royaltyBps int
37}
38
39var (
40 bySlug avl.Tree
41 byPkg avl.Tree
42 pending avl.Tree
43 reserved avl.Tree
44 inited bool
45 admin address
46 self address
47 launchFee int64
48 seenBank int64
49)
50
51func Init(cur realm) {
52 if inited {
53 panic("factory: already initialized")
54 }
55 if !cur.Previous().IsUserCall() {
56 panic("factory: EOA only")
57 }
58 admin = cur.Previous().Address()
59 self = cur.Address()
60 bySlug = avl.Tree{}
61 byPkg = avl.Tree{}
62 pending = avl.Tree{}
63 reserved = avl.Tree{}
64 launchFee = defaultLaunchFee
65 seenBank = bankUgnot()
66 inited = true
67 chain.Emit("Init", "admin", admin.String())
68}
69
70func Address() address {
71 return chain.PackageAddress(pkgPath)
72}
73
74func LaunchFee() int64 {
75 requireInit()
76 return launchFee
77}
78
79func Admin() string {
80 requireInit()
81 return admin.String()
82}
83
84func SetLaunchFee(cur realm, ugnot int64) {
85 requireAdmin(cur)
86 if ugnot < 0 {
87 panic("factory: launch fee")
88 }
89 launchFee = ugnot
90 chain.Emit("SetLaunchFee", "ugnot", strconv.FormatInt(ugnot, 10))
91}
92
93func CreditLaunchFee(cur realm) {
94 requireInit()
95 prev := cur.Previous()
96 if !prev.IsCode() {
97 panic("factory: collection realm")
98 }
99 pkg := prev.PkgPath()
100 if pending.Has(pkg) {
101 panic("factory: already credited")
102 }
103 if byPkg.Has(pkg) {
104 panic("factory: already registered")
105 }
106 now := bankUgnot()
107 if now < seenBank {
108 panic("factory: bank")
109 }
110 if now-seenBank != launchFee {
111 panic("factory: launch fee")
112 }
113 seenBank = now
114 pending.Set(pkg, true)
115 chain.Emit("CreditLaunchFee", "pkg", pkg)
116}
117
118func Register(cur realm, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {
119 requireInit()
120 prev := cur.Previous()
121 if !prev.IsCode() {
122 panic("factory: collection realm")
123 }
124 creatorRlm := prev.Previous()
125 if !creatorRlm.IsUserCall() {
126 panic("factory: EOA creator")
127 }
128 pkg := prev.PkgPath()
129 slug := lastElem(pkg)
130 if reserved.Has(slug) {
131 who := reserved.Get(slug).(address)
132 if creatorRlm.Address() != who {
133 panic("factory: reserved")
134 }
135 reserved.Remove(slug)
136 if pending.Has(pkg) {
137 pending.Remove(pkg)
138 }
139 } else {
140 if !pending.Has(pkg) {
141 panic("factory: launch fee")
142 }
143 pending.Remove(pkg)
144 }
145 register(pkg, creatorRlm.Address(), name, symbol, cover, maxSupply, mintPrice, royaltyBps)
146}
147
148func Reserve(cur realm, slug string) {
149 requireInit()
150 if !cur.Previous().IsUserCall() {
151 panic("factory: EOA only")
152 }
153 slug = strings.ToLower(strings.TrimSpace(slug))
154 if !validSlug(slug) {
155 panic("factory: slug")
156 }
157 if bySlug.Has(slug) || reserved.Has(slug) {
158 panic("factory: slug taken")
159 }
160 takeFactorySend(launchFee)
161 seenBank = bankUgnot()
162 reserved.Set(slug, cur.Previous().Address())
163 chain.Emit("Reserve", "slug", slug)
164}
165
166func CancelReserve(cur realm, slug string) {
167 requireInit()
168 if !cur.Previous().IsUserCall() {
169 panic("factory: EOA only")
170 }
171 slug = strings.ToLower(strings.TrimSpace(slug))
172 if !reserved.Has(slug) {
173 panic("factory: not reserved")
174 }
175 who := reserved.Get(slug).(address)
176 if who != cur.Previous().Address() {
177 panic("factory: not reserver")
178 }
179 if bySlug.Has(slug) {
180 panic("factory: already registered")
181 }
182 reserved.Remove(slug)
183 if launchFee > 0 {
184 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
185 bk.SendCoins(self, who, chain.Coins{{Denom: nativeDenom, Amount: launchFee}})
186 }
187 seenBank = bankUgnot()
188 chain.Emit("CancelReserve", "slug", slug)
189}
190
191func ReservedCreator(slug string) string {
192 requireInit()
193 slug = strings.ToLower(strings.TrimSpace(slug))
194 if !reserved.Has(slug) {
195 return ""
196 }
197 return reserved.Get(slug).(address).String()
198}
199
200func takeFactorySend(want int64) {
201 sent := unsafe.OriginSend()
202 if want == 0 {
203 if len(sent) != 0 {
204 panic("factory: extra denoms")
205 }
206 return
207 }
208 if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {
209 panic("factory: wrong payment")
210 }
211}
212
213func NoteMinted(cur realm, n int) {
214 requireInit()
215 if n < 0 {
216 panic("factory: minted")
217 }
218 prev := cur.Previous()
219 if !prev.IsCode() {
220 panic("factory: collection realm")
221 }
222 c := mustByPkg(prev.PkgPath())
223 c.minted = n
224 byPkg.Set(c.pkg, c)
225 bySlug.Set(c.slug, c)
226}
227
228func NoteRoyalty(cur realm, bps int) {
229 requireInit()
230 if bps < 0 || bps > maxRoyaltyBps {
231 panic("factory: royalty")
232 }
233 prev := cur.Previous()
234 if !prev.IsCode() {
235 panic("factory: collection realm")
236 }
237 c := mustByPkg(prev.PkgPath())
238 c.royaltyBps = bps
239 byPkg.Set(c.pkg, c)
240 bySlug.Set(c.slug, c)
241}
242
243func CollectionOf(slug string) string {
244 requireInit()
245 c := mustBySlug(slug)
246 return c.pkg
247}
248
249func CollectionAddr(slug string) string {
250 requireInit()
251 c := mustBySlug(slug)
252 return chain.PackageAddress(c.pkg).String()
253}
254
255func RoyaltyOf(slug string) int {
256 requireInit()
257 c := mustBySlug(slug)
258 return c.royaltyBps
259}
260
261func ListCollections() string {
262 requireInit()
263 out := ""
264 bySlug.Iterate("", "", func(_ string, value any) bool {
265 c := value.(*collection)
266 if out != "" {
267 out += "\n"
268 }
269 out += c.slug + "|" + c.name + "|" + c.cover + "|" + c.pkg + "|" + strconv.FormatInt(c.mintPrice, 10) + "|" + strconv.Itoa(c.maxSupply) + "|" + strconv.Itoa(c.minted) + "|" + c.creator.String() + "|" + chain.PackageAddress(c.pkg).String() + "|" + strconv.Itoa(c.royaltyBps)
270 return false
271 })
272 return out
273}
274
275func Withdraw(cur realm) {
276 requireAdmin(cur)
277 bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
278 ugnot := bk.GetCoins(self).AmountOf(nativeDenom)
279 if ugnot <= 0 {
280 panic("factory: no funds")
281 }
282 seenBank = 0
283 bk.SendCoins(self, admin, chain.Coins{{Denom: nativeDenom, Amount: ugnot}})
284 chain.Emit("Withdraw", "amount", strconv.FormatInt(ugnot, 10))
285}
286
287func TransferAdmin(cur realm, next address) {
288 requireAdmin(cur)
289 if !next.IsValid() {
290 panic("factory: invalid address")
291 }
292 if next == admin {
293 panic("factory: already admin")
294 }
295 admin = next
296 chain.Emit("TransferAdmin", "addr", next.String())
297}
298
299func Render(_ string) string {
300 if !inited {
301 return "# Bazaar factory\n\nCall Init first.\n"
302 }
303 out := "# Bazaar factory\n\n"
304 out += "- launch fee: " + strconv.FormatInt(launchFee, 10) + " ugnot\n"
305 out += "- collections: " + strconv.Itoa(bySlug.Size()) + "\n\n"
306 lines := ListCollections()
307 if lines == "" {
308 out += "_No collections._\n"
309 } else {
310 out += "```\n" + lines + "\n```\n"
311 }
312 return out
313}
314
315func register(pkg string, creator address, name, symbol, cover string, maxSupply int, mintPrice, royaltyBps int64) {
316 if !strings.HasPrefix(pkg, "gno.land/r/") {
317 panic("factory: pkg")
318 }
319 slug := lastElem(pkg)
320 if !validSlug(slug) {
321 panic("factory: slug")
322 }
323 if !validName(name) {
324 panic("factory: name")
325 }
326 if !validSymbol(symbol) {
327 panic("factory: symbol")
328 }
329 if !validImage(cover) {
330 panic("factory: cover")
331 }
332 if maxSupply < 0 || maxSupply > maxSupplyBound {
333 panic("factory: maxSupply")
334 }
335 if mintPrice < 0 {
336 panic("factory: mintPrice")
337 }
338 if royaltyBps < 0 || royaltyBps > maxRoyaltyBps {
339 panic("factory: royalty")
340 }
341 if bySlug.Has(slug) {
342 panic("factory: slug taken")
343 }
344 if byPkg.Has(pkg) {
345 panic("factory: pkg already registered")
346 }
347 c := &collection{
348 slug: slug,
349 name: name,
350 symbol: symbol,
351 cover: cover,
352 pkg: pkg,
353 creator: creator,
354 maxSupply: maxSupply,
355 mintPrice: mintPrice,
356 royaltyBps: int(royaltyBps),
357 }
358 bySlug.Set(slug, c)
359 byPkg.Set(pkg, c)
360 chain.Emit("Register", "slug", slug, "pkg", pkg, "creator", creator.String())
361}
362
363func requireInit() {
364 if !inited {
365 panic("factory: call Init first")
366 }
367}
368
369func requireAdmin(cur realm) {
370 requireInit()
371 if !cur.Previous().IsUserCall() {
372 panic("factory: EOA only")
373 }
374 if cur.Previous().Address() != admin {
375 panic("factory: not admin")
376 }
377}
378
379func bankUgnot() int64 {
380 return banker.NewReadonlyBanker().GetCoins(Address()).AmountOf(nativeDenom)
381}
382
383func mustBySlug(slug string) *collection {
384 v := bySlug.Get(slug)
385 if v == nil {
386 panic("factory: unknown collection")
387 }
388 return v.(*collection)
389}
390
391func mustByPkg(pkg string) *collection {
392 v := byPkg.Get(pkg)
393 if v == nil {
394 panic("factory: unknown collection")
395 }
396 return v.(*collection)
397}
398
399func lastElem(pkg string) string {
400 i := strings.LastIndex(pkg, "/")
401 if i < 0 || i+1 >= len(pkg) {
402 panic("factory: pkg")
403 }
404 return pkg[i+1:]
405}
406
407func validSlug(s string) bool {
408 if len(s) < minSlugLen || len(s) > maxSlugLen {
409 return false
410 }
411 if s[0] < 'a' || s[0] > 'z' {
412 return false
413 }
414 return asciiSet(s, "abcdefghijklmnopqrstuvwxyz0123456789")
415}
416
417func validSymbol(s string) bool {
418 if len(s) < 1 || len(s) > maxSymbolLen {
419 return false
420 }
421 return asciiSet(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
422}
423
424func validName(name string) bool {
425 if len(name) < 1 || len(name) > maxNameLen {
426 return false
427 }
428 return !strings.ContainsAny(name, "\n\r|")
429}
430
431func validImage(u string) bool {
432 if u == "" {
433 return true
434 }
435 if len(u) > maxImageLen {
436 return false
437 }
438 if strings.ContainsAny(u, " \t\n\r|<>\"'`") {
439 return false
440 }
441 if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
442 return true
443 }
444 const prefix = "/samples/"
445 if !strings.HasPrefix(u, prefix) {
446 return false
447 }
448 rest := u[len(prefix):]
449 if rest == "" {
450 return false
451 }
452 return asciiSet(rest, "abcdefghijklmnopqrstuvwxyz0123456789._-")
453}
454
455func asciiSet(s, allowed string) bool {
456 for i := 0; i < len(s); i++ {
457 if !strings.ContainsRune(allowed, rune(s[i])) {
458 return false
459 }
460 }
461 return true
462}
463Raw Package Data
Raw JSON data