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.

edcol.gno

10.18 Kb · 428 lines
  1// Package edcol is an edition collection. One id is one print run.
  2// List escrows an amount on this realm. Buy can take part of that amount.
  3// The book is efactory, not bazaarv5. Copies do not enter perk2.
  4package edcol
  5
  6import (
  7	"chain"
  8	"chain/banker"
  9	"chain/runtime/unsafe"
 10	"math"
 11	"strconv"
 12	"strings"
 13
 14	"gno.land/p/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/fee/v1"
 15	"gno.land/p/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/grc1155/v0"
 16	"gno.land/p/nt/avl/v0"
 17	"gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/bazaar/efactory"
 18)
 19
 20const (
 21	nativeDenom = "ugnot"
 22	maxSupply   = int64(1_000_000_000)
 23	maxNameLen  = 64
 24	maxImageLen = 200
 25)
 26
 27type edition struct {
 28	name      string
 29	image     string
 30	max       int64
 31	minted    int64
 32	mintPrice int64
 33}
 34
 35type listing struct {
 36	amount int64
 37	unit   int64
 38}
 39
 40var (
 41	inited   bool
 42	creator  address
 43	self     address
 44	name_    string
 45	symbol   string
 46	cover    string
 47	royalty  int64
 48	copies   int64
 49	ledger   *grc1155.Ledger
 50	editions avl.Tree
 51	listings avl.Tree
 52)
 53
 54func Init(cur realm, name, symbol_, cover_ string, royaltyBps int64) {
 55	if inited {
 56		panic("edcol: already")
 57	}
 58	if !cur.IsCurrent() || !cur.Previous().IsUserCall() {
 59		panic("edcol: EOA only")
 60	}
 61	caller := cur.Previous().Address()
 62	if !validName(name) || !validSymbol(symbol_) || !validImage(cover_) {
 63		panic("edcol: meta")
 64	}
 65	creator = caller
 66	self = cur.Address()
 67	name_ = name
 68	symbol = symbol_
 69	cover = cover_
 70	royalty = royaltyBps
 71	copies = 0
 72	ledger = grc1155.New(0, cur)
 73	editions = avl.Tree{}
 74	listings = avl.Tree{}
 75	efactory.Register(cross(cur), name, symbol_, cover_, royaltyBps)
 76	inited = true
 77}
 78
 79// Create opens an edition. Only the creator can call it. max is the copy cap.
 80// mintPrice 0 means only the creator can mint.
 81func Create(cur realm, id, name, image string, max, mintPrice int64) {
 82	caller := requireUser(cur)
 83	if caller != creator {
 84		panic("edcol: creator")
 85	}
 86	if !validID(id) || !validName(name) || !validImage(image) {
 87		panic("edcol: meta")
 88	}
 89	if max <= 0 || max > maxSupply || mintPrice < 0 {
 90		panic("edcol: supply")
 91	}
 92	if editions.Has(id) {
 93		panic("edcol: exists")
 94	}
 95	editions.Set(id, &edition{name: name, image: image, max: max, mintPrice: mintPrice})
 96	chain.Emit("EditionCreate", "id", id, "max", strconv.FormatInt(max, 10))
 97}
 98
 99// Mint adds copies. The creator mints without payment. Anyone else pays mintPrice each.
100func Mint(cur realm, id string, to address, amount int64) {
101	caller := requireUser(cur)
102	ed := mustEdition(id)
103	if !to.IsValid() || to == self || amount <= 0 {
104		panic("edcol: amount")
105	}
106	if amount > ed.max || ed.minted > ed.max-amount {
107		panic("edcol: supply")
108	}
109	if caller == creator {
110		takeExact(0)
111	} else {
112		if ed.mintPrice <= 0 {
113			panic("edcol: creator")
114		}
115		takeExact(mul(ed.mintPrice, amount))
116	}
117	ledger.Mint(0, cur, to, id, amount)
118	ed.minted += amount
119	copies += amount
120	editions.Set(id, ed)
121	efactory.NoteMinted(cross(cur), copies)
122	if caller != creator {
123		bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
124		bk.SendCoins(self, creator, chain.Coins{{Denom: nativeDenom, Amount: mul(ed.mintPrice, amount)}})
125	}
126	chain.Emit("EditionMint", "id", id, "to", to.String(), "amount", strconv.FormatInt(amount, 10))
127}
128
129// List escrows amount of id at a unit price. One open listing per seller and id.
130func List(cur realm, id string, amount, unitPrice int64) {
131	caller := requireUser(cur)
132	mustEdition(id)
133	if amount <= 0 || unitPrice <= 0 {
134		panic("edcol: price")
135	}
136	key := listKey(caller, id)
137	if listings.Has(key) {
138		panic("edcol: listed")
139	}
140	if ledger.Balance(caller, id) < amount {
141		panic("edcol: balance")
142	}
143	ledger.Move(0, cur, caller, self, id, amount)
144	listings.Set(key, &listing{amount: amount, unit: unitPrice})
145	chain.Emit("EditionList", "id", id, "seller", caller.String(), "amount", strconv.FormatInt(amount, 10), "price", strconv.FormatInt(unitPrice, 10))
146}
147
148func Cancel(cur realm, id string) {
149	caller := requireUser(cur)
150	key := listKey(caller, id)
151	row := listings.Get(key)
152	if row == nil {
153		panic("edcol: not listed")
154	}
155	hold := row.(*listing)
156	ledger.Move(0, cur, self, caller, id, hold.amount)
157	listings.Remove(key)
158	chain.Emit("EditionCancel", "id", id, "seller", caller.String())
159}
160
161// Buy takes amount copies from seller's listing. Payment is unit price times amount.
162func Buy(cur realm, seller address, id string, amount int64) {
163	caller := requireUser(cur)
164	if !seller.IsValid() || caller == seller || amount <= 0 {
165		panic("edcol: amount")
166	}
167	key := listKey(seller, id)
168	row := listings.Get(key)
169	if row == nil {
170		panic("edcol: not listed")
171	}
172	hold := row.(*listing)
173	if amount > hold.amount {
174		panic("edcol: amount")
175	}
176	price := mul(hold.unit, amount)
177	takeExact(price)
178	sellerAmt, royal, sinkAmt := split(price)
179	pay(cur, seller, sellerAmt, royal, sinkAmt)
180	ledger.Move(0, cur, self, caller, id, amount)
181	hold.amount -= amount
182	if hold.amount == 0 {
183		listings.Remove(key)
184	} else {
185		listings.Set(key, hold)
186	}
187	chain.Emit("EditionBuy", "id", id, "buyer", caller.String(), "seller", seller.String(), "amount", strconv.FormatInt(amount, 10))
188}
189
190func Transfer(cur realm, to address, id string, amount int64) {
191	caller := requireUser(cur)
192	if !to.IsValid() || to == caller || to == self || amount <= 0 {
193		panic("edcol: amount")
194	}
195	mustEdition(id)
196	if ledger.Balance(caller, id) < amount {
197		panic("edcol: balance")
198	}
199	ledger.Move(0, cur, caller, to, id, amount)
200}
201
202func BalanceOf(addr string, id string) int64 {
203	who, ok := parseAddr(addr)
204	if !ok {
205		return 0
206	}
207	return ledger.Balance(who, id)
208}
209
210func Supply(id string) int64 {
211	if ledger == nil {
212		return 0
213	}
214	return ledger.Supply(id)
215}
216
217func TokenURI(id string) string {
218	ed := mustEdition(id)
219	body := `{"name":` + jsonStr(ed.name) + `,"image":` + jsonStr(ed.image) + `,"edition":` + jsonStr(id) + `}`
220	return "data:application/json;charset=utf-8," + pctURI(body)
221}
222
223func Listing(seller, id string) string {
224	who, ok := parseAddr(seller)
225	if !ok {
226		return ""
227	}
228	row := listings.Get(listKey(who, id))
229	if row == nil {
230		return ""
231	}
232	hold := row.(*listing)
233	return strconv.FormatInt(hold.amount, 10) + "|" + strconv.FormatInt(hold.unit, 10)
234}
235
236func Name() string  { return name_ }
237func Symbol() string { return symbol }
238
239// Editions is id|name|image|max|minted|mintPrice, one edition per line.
240func Editions() string {
241	if !inited {
242		return ""
243	}
244	out := ""
245	editions.Iterate("", "", func(key string, value any) bool {
246		ed := value.(*edition)
247		if out != "" {
248			out += "\n"
249		}
250		out += key + "|" + ed.name + "|" + ed.image + "|" +
251			strconv.FormatInt(ed.max, 10) + "|" +
252			strconv.FormatInt(ed.minted, 10) + "|" +
253			strconv.FormatInt(ed.mintPrice, 10)
254		return false
255	})
256	return out
257}
258
259// OpenListings is seller|id|amount|unitPrice, one listing per line.
260func OpenListings() string {
261	out := ""
262	listings.Iterate("", "", func(key string, value any) bool {
263		hold := value.(*listing)
264		if out != "" {
265			out += "\n"
266		}
267		out += key + "|" + strconv.FormatInt(hold.amount, 10) + "|" + strconv.FormatInt(hold.unit, 10)
268		return false
269	})
270	return out
271}
272
273func requireUser(cur realm) address {
274	if !inited {
275		panic("edcol: init")
276	}
277	if !cur.IsCurrent() {
278		panic("edcol: spoofed realm")
279	}
280	if !cur.Previous().IsUserCall() {
281		panic("edcol: EOA only")
282	}
283	return cur.Previous().Address()
284}
285
286func mustEdition(id string) *edition {
287	v := editions.Get(id)
288	if v == nil {
289		panic("edcol: unknown id")
290	}
291	return v.(*edition)
292}
293
294func listKey(seller address, id string) string {
295	return seller.String() + "|" + id
296}
297
298func split(price int64) (sellerAmt, royal, sinkAmt int64) {
299	proto := fee.ProtocolFee(price, efactory.ProtocolBps())
300	if royalty > 0 {
301		royal = fee.ProtocolFee(price, royalty)
302	}
303	if proto+royal > price {
304		panic("edcol: fee")
305	}
306	sellerAmt = price - proto - royal
307	sinkAmt = proto
308	if sinkAmt > 0 && !efactory.ProtocolSink().IsValid() {
309		panic("edcol: sink")
310	}
311	return
312}
313
314func pay(cur realm, seller address, sellerAmt, royal, sinkAmt int64) {
315	bk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
316	if sellerAmt > 0 {
317		bk.SendCoins(self, seller, chain.Coins{{Denom: nativeDenom, Amount: sellerAmt}})
318	}
319	if royal > 0 {
320		bk.SendCoins(self, creator, chain.Coins{{Denom: nativeDenom, Amount: royal}})
321	}
322	if sinkAmt > 0 {
323		bk.SendCoins(self, efactory.ProtocolSink(), chain.Coins{{Denom: nativeDenom, Amount: sinkAmt}})
324	}
325}
326
327func takeExact(want int64) {
328	sent := unsafe.OriginSend()
329	if want == 0 {
330		if len(sent) != 0 {
331			panic("edcol: extra payment")
332		}
333		return
334	}
335	if len(sent) != 1 || sent[0].Denom != nativeDenom || sent[0].Amount != want {
336		panic("edcol: wrong payment")
337	}
338}
339
340func mul(a, b int64) int64 {
341	if a < 0 || b <= 0 {
342		panic("edcol: amount")
343	}
344	if a == 0 {
345		return 0
346	}
347	if a > math.MaxInt64/b {
348		panic("edcol: overflow")
349	}
350	return a * b
351}
352
353func parseAddr(raw string) (address, bool) {
354	s := strings.TrimSpace(raw)
355	if s == "" {
356		return address(""), false
357	}
358	who := address(s)
359	if !who.IsValid() {
360		return address(""), false
361	}
362	return who, true
363}
364
365func validID(id string) bool {
366	if len(id) < 1 || len(id) > 16 || id[0] < 'a' || id[0] > 'z' {
367		return false
368	}
369	for i := 0; i < len(id); i++ {
370		c := id[i]
371		if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
372			return false
373		}
374	}
375	return true
376}
377
378func validName(name string) bool {
379	if len(name) < 1 || len(name) > maxNameLen {
380		return false
381	}
382	return !strings.ContainsAny(name, "\n\r|\"\\")
383}
384
385func validSymbol(s string) bool {
386	if len(s) < 1 || len(s) > 11 {
387		return false
388	}
389	for i := 0; i < len(s); i++ {
390		c := s[i]
391		if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') {
392			return false
393		}
394	}
395	return true
396}
397
398func validImage(u string) bool {
399	if u == "" {
400		return true
401	}
402	if len(u) > maxImageLen || strings.ContainsAny(u, " \t\n\r|<>\"'`") {
403		return false
404	}
405	if strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "http://") {
406		return true
407	}
408	rest := strings.TrimPrefix(u, "/samples/")
409	return strings.HasPrefix(u, "/samples/") && rest != "" && rest != u
410}
411
412func jsonStr(s string) string {
413	return `"` + s + `"`
414}
415
416func pctURI(s string) string {
417	hex := "0123456789ABCDEF"
418	out := ""
419	for i := 0; i < len(s); i++ {
420		c := s[i]
421		if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~' {
422			out += string(rune(c))
423			continue
424		}
425		out += "%" + string([]byte{hex[c>>4], hex[c&15]})
426	}
427	return out
428}