← Back to Realms

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}
463

Raw Package Data

Raw JSON data