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}