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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

bazaarv2.gno

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