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