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

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}