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.

efactory.gno

6.74 Kb · 335 lines
  1// Package efactory registers edition collections. It does not share a book
  2// with bazaarv5, and it does not pay perk2.
  3package efactory
  4
  5import (
  6	"chain"
  7	"chain/banker"
  8	"chain/runtime/unsafe"
  9	"strconv"
 10	"strings"
 11
 12	"gno.land/p/nt/avl/v0"
 13)
 14
 15const (
 16	nativeDenom        = "ugnot"
 17	defaultLaunchFee   = int64(1_000_000_000)
 18	defaultProtocolBps = int64(200)
 19	maxProtocolBps     = int64(500)
 20	maxRoyaltyBps      = int64(1000)
 21	maxNameLen         = 64
 22	maxImageLen        = 200
 23	maxSymbolLen       = 11
 24	maxSlugLen         = 11
 25	minSlugLen         = 2
 26)
 27
 28type collection struct {
 29	slug       string
 30	name       string
 31	symbol     string
 32	cover      string
 33	pkg        string
 34	creator    address
 35	minted     int64
 36	royaltyBps int64
 37}
 38
 39type reservation struct {
 40	who  address
 41	paid int64
 42}
 43
 44var (
 45	bySlug      avl.Tree
 46	byPkg       avl.Tree
 47	reserved    avl.Tree
 48	inited      bool
 49	admin       address
 50	self        address
 51	protocolSink address
 52	launchFee   int64
 53	protocolBps int64
 54)
 55
 56func Init(cur realm) {
 57	if inited {
 58		panic("efactory: already")
 59	}
 60	if !cur.Previous().IsUserCall() {
 61		panic("efactory: EOA only")
 62	}
 63	admin = cur.Previous().Address()
 64	protocolSink = admin
 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("EditionFactory", "admin", admin.String())
 73}
 74
 75func SetLaunchFee(cur realm, fee int64) {
 76	requireAdmin(cur)
 77	if fee < 0 {
 78		panic("efactory: fee")
 79	}
 80	launchFee = fee
 81}
 82
 83func SetProtocolBps(cur realm, bps int64) {
 84	requireAdmin(cur)
 85	if bps < 0 || bps > maxProtocolBps {
 86		panic("efactory: bps")
 87	}
 88	protocolBps = bps
 89}
 90
 91func SetProtocolSink(cur realm, sink address) {
 92	requireAdmin(cur)
 93	if !sink.IsValid() || sink == self {
 94		panic("efactory: sink")
 95	}
 96	protocolSink = sink
 97}
 98
 99func ProtocolBps() int64 {
100	requireInit()
101	return protocolBps
102}
103
104func LaunchFee() int64 {
105	requireInit()
106	return launchFee
107}
108
109func ProtocolSink() address {
110	requireInit()
111	return protocolSink
112}
113
114func Reserve(cur realm, slug string) {
115	requireInit()
116	if !cur.Previous().IsUserCall() {
117		panic("efactory: EOA only")
118	}
119	slug = strings.ToLower(strings.TrimSpace(slug))
120	if !validSlug(slug) {
121		panic("efactory: slug")
122	}
123	if bySlug.Has(slug) || reserved.Has(slug) {
124		panic("efactory: slug taken")
125	}
126	caller := cur.Previous().Address()
127	paid := launchFee
128	if caller == admin {
129		paid = 0
130	}
131	takeSend(paid)
132	reserved.Set(slug, &reservation{who: caller, paid: paid})
133	chain.Emit("EditionReserve", "slug", slug, "paid", strconv.FormatInt(paid, 10))
134}
135
136func Register(cur realm, name, symbol, cover string, royaltyBps int64) {
137	requireInit()
138	prev := cur.Previous()
139	if !prev.IsCode() {
140		panic("efactory: collection realm")
141	}
142	creatorRlm := prev.Previous()
143	if !creatorRlm.IsUserCall() {
144		panic("efactory: EOA creator")
145	}
146	if royaltyBps < 0 || royaltyBps > maxRoyaltyBps {
147		panic("efactory: royalty")
148	}
149	if !validName(name) || !validSymbol(symbol) || !validImage(cover) {
150		panic("efactory: meta")
151	}
152	pkg := prev.PkgPath()
153	slug := lastElem(pkg)
154	res := reserved.Get(slug)
155	if res == nil {
156		panic("efactory: reserved")
157	}
158	hold := res.(*reservation)
159	if creatorRlm.Address() != hold.who {
160		panic("efactory: reserved")
161	}
162	reserved.Remove(slug)
163	row := &collection{
164		slug:       slug,
165		name:       name,
166		symbol:     symbol,
167		cover:      cover,
168		pkg:        pkg,
169		creator:    creatorRlm.Address(),
170		royaltyBps: royaltyBps,
171	}
172	byPkg.Set(pkg, row)
173	bySlug.Set(slug, row)
174	chain.Emit("EditionRegister", "slug", slug, "pkg", pkg)
175}
176
177func NoteMinted(cur realm, n int64) {
178	requireInit()
179	if n < 0 {
180		panic("efactory: minted")
181	}
182	prev := cur.Previous()
183	if !prev.IsCode() {
184		panic("efactory: collection realm")
185	}
186	row := mustByPkg(prev.PkgPath())
187	row.minted = n
188	byPkg.Set(row.pkg, row)
189	bySlug.Set(row.slug, row)
190}
191
192func ListEditions() string {
193	requireInit()
194	out := ""
195	bySlug.Iterate("", "", func(_ string, value any) bool {
196		c := value.(*collection)
197		if out != "" {
198			out += "\n"
199		}
200		out += c.slug + "|" + c.name + "|" + c.cover + "|" + c.pkg + "|" +
201			strconv.FormatInt(c.minted, 10) + "|" + c.creator.String() + "|" +
202			chain.PackageAddress(c.pkg).String() + "|" + strconv.FormatInt(c.royaltyBps, 10)
203		return false
204	})
205	return out
206}
207
208func CollectionOf(slug string) string {
209	requireInit()
210	return mustBySlug(slug).pkg
211}
212
213func RoyaltyOf(slug string) int64 {
214	requireInit()
215	return mustBySlug(slug).royaltyBps
216}
217
218func Withdraw(cur realm) {
219	requireAdmin(cur)
220	bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
221	avail := bk.GetCoins(self).AmountOf(nativeDenom) - reservedPaid()
222	if avail <= 0 {
223		panic("efactory: no funds")
224	}
225	bk.SendCoins(self, admin, chain.Coins{{Denom: nativeDenom, Amount: avail}})
226}
227
228func requireInit() {
229	if !inited {
230		panic("efactory: init")
231	}
232}
233
234func requireAdmin(cur realm) {
235	requireInit()
236	if !cur.Previous().IsUserCall() || cur.Previous().Address() != admin {
237		panic("efactory: admin")
238	}
239}
240
241func takeSend(want int64) {
242	sent := unsafe.OriginSend()
243	if want == 0 {
244		if len(sent) != 0 {
245			panic("efactory: extra payment")
246		}
247		return
248	}
249	if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {
250		panic("efactory: wrong payment")
251	}
252}
253
254func reservedPaid() int64 {
255	var n int64
256	reserved.Iterate("", "", func(_ string, value any) bool {
257		n += value.(*reservation).paid
258		return false
259	})
260	return n
261}
262
263func mustBySlug(slug string) *collection {
264	v := bySlug.Get(strings.ToLower(strings.TrimSpace(slug)))
265	if v == nil {
266		panic("efactory: unknown")
267	}
268	return v.(*collection)
269}
270
271func mustByPkg(pkg string) *collection {
272	v := byPkg.Get(pkg)
273	if v == nil {
274		panic("efactory: unknown")
275	}
276	return v.(*collection)
277}
278
279func lastElem(pkg string) string {
280	i := strings.LastIndex(pkg, "/")
281	if i < 0 || i+1 >= len(pkg) {
282		panic("efactory: pkg")
283	}
284	return pkg[i+1:]
285}
286
287func validSlug(s string) bool {
288	if len(s) < minSlugLen || len(s) > maxSlugLen {
289		return false
290	}
291	if s[0] < 'a' || s[0] > 'z' {
292		return false
293	}
294	for i := 0; i < len(s); i++ {
295		c := s[i]
296		if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
297			return false
298		}
299	}
300	return true
301}
302
303func validSymbol(s string) bool {
304	if len(s) < 1 || len(s) > maxSymbolLen {
305		return false
306	}
307	for i := 0; i < len(s); i++ {
308		c := s[i]
309		ok := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
310		if !ok {
311			return false
312		}
313	}
314	return true
315}
316
317func validName(name string) bool {
318	if len(name) < 1 || len(name) > maxNameLen {
319		return false
320	}
321	return !strings.ContainsAny(name, "\n\r|\"\\")
322}
323
324func validImage(u string) bool {
325	if u == "" {
326		return true
327	}
328	if len(u) > maxImageLen || strings.ContainsAny(u, " \t\n\r|<>\"'`") {
329		return false
330	}
331	if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
332		return true
333	}
334	return strings.HasPrefix(u, "/samples/") && len(u) > len("/samples/")
335}