collection.gno
4.54 Kb · 226 lines
1package shop
2
3import (
4 "strconv"
5
6 "chain"
7
8 "gno.land/p/nt/avl/v0"
9 "gno.land/p/nt/grc721/enumerable/v0"
10 "gno.land/p/nt/grc721/metadata/v0"
11 "gno.land/p/nt/grc721/v0"
12 "gno.land/p/nt/seqid/v0"
13)
14
15const (
16 maxImageLen = 200
17 maxTidLen = 64
18 saleCap = 100
19 pageMax = 50
20)
21
22var (
23 token *grc721.Token
24 ledger *grc721.PrivateLedger
25 metaView *metadata.Metadata
26 metaLed *metadata.Ledger
27 enumView *enumerable.Enumerable
28 tokenSeq seqid.ID
29 creator address
30 ready bool
31 admins avl.Tree
32 listings avl.Tree
33 sales [saleCap]sale
34 saleN int
35)
36
37type listing struct {
38 Seller address
39 Price int64
40}
41
42type sale struct {
43 Tid string
44 Seller address
45 Buyer address
46 Price int64
47 Name string
48}
49
50// Init creates the collection once. The caller is the creator and can mint.
51func Init(cur realm, name, symbol string) {
52 caller := requireUser(cur)
53 if ready {
54 panic("market: already init")
55 }
56 token, ledger = grc721.NewToken(name, symbol, tokenSeq.Next(), cur)
57 metaView, metaLed = metadata.NewMetadata(ledger)
58 enumView, _ = enumerable.NewEnumerable(ledger)
59 creator = caller
60 ready = true
61 chain.Emit("Init", "creator", caller.String(), "name", name, "symbol", symbol)
62}
63
64// Grant lets who mint. Only the creator can call it.
65func Grant(cur realm, who address) {
66 caller := requireUser(cur)
67 requireReady()
68 if caller != creator || !who.IsValid() || who == creator {
69 panic("market: admin")
70 }
71 admins.Set(who.String(), true)
72 chain.Emit("Grant", "admin", who.String())
73}
74
75// Revoke removes a minter. Only the creator can call it.
76func Revoke(cur realm, who address) {
77 caller := requireUser(cur)
78 requireReady()
79 if caller != creator {
80 panic("market: admin")
81 }
82 if _, removed := admins.Remove(who.String()); !removed {
83 panic("market: admin")
84 }
85 chain.Emit("Revoke", "admin", who.String())
86}
87
88func canMint(caller address) bool {
89 if caller == creator {
90 return true
91 }
92 return admins.Get(caller.String()) != nil
93}
94
95// Mint issues one token to the caller. image is an https URL.
96func Mint(cur realm, tid, name, image string) {
97 caller := requireUser(cur)
98 requireReady()
99 if !canMint(caller) {
100 panic("market: not minter")
101 }
102 id := parseTid(tid)
103 if name == "" || len(name) > grc721.MaxNameLen || !ascii(name) {
104 panic("market: name")
105 }
106 if !httpsURL(image) {
107 panic("market: image")
108 }
109 if err := ledger.Mint(caller, id); err != nil {
110 panic("market: mint")
111 }
112 if err := metaLed.SetTokenURI(id, image); err != nil {
113 panic("market: tokenURI")
114 }
115 if err := metaLed.SetTokenMetadata(id, metadata.Data{
116 Name: name,
117 Description: name,
118 Image: image,
119 }); err != nil {
120 panic("market: metadata")
121 }
122 chain.Emit("Mint", "tid", tid, "owner", caller.String(), "name", name)
123}
124
125// Transfer moves a token the caller owns.
126func Transfer(cur realm, to address, tid string) {
127 caller := requireUser(cur)
128 requireReady()
129 if !to.IsValid() || to == caller {
130 panic("market: to")
131 }
132 id := parseTid(tid)
133 if err := ledger.TransferFrom(caller, caller, to, id); err != nil {
134 panic("market: transfer")
135 }
136}
137
138func requireReady() {
139 if !ready {
140 panic("market: call Init")
141 }
142}
143
144func requireUser(cur realm) address {
145 prev := cur.Previous()
146 if !prev.IsUserCall() {
147 panic("market: eoa")
148 }
149 return prev.Address()
150}
151
152func parseTid(tid string) grc721.TokenID {
153 if tid == "" || len(tid) > maxTidLen || !ascii(tid) {
154 panic("market: tid")
155 }
156 for i := 0; i < len(tid); i++ {
157 c := tid[i]
158 ok := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-'
159 if !ok {
160 panic("market: tid")
161 }
162 }
163 return grc721.TokenID(tid)
164}
165
166func httpsURL(s string) bool {
167 const prefix = "https://"
168 if len(s) < len(prefix)+1 || len(s) > maxImageLen || !ascii(s) {
169 return false
170 }
171 return s[:len(prefix)] == prefix
172}
173
174func ascii(s string) bool {
175 for i := 0; i < len(s); i++ {
176 if s[i] < 0x20 || s[i] > 0x7e {
177 return false
178 }
179 }
180 return true
181}
182
183func ownerOf(tid grc721.TokenID) address {
184 owner, err := token.OwnerOf(tid)
185 if err != nil {
186 panic("market: owner")
187 }
188 return owner
189}
190
191func metaNameImage(tid grc721.TokenID) (string, string) {
192 data, err := metaView.TokenMetadata(tid)
193 if err != nil {
194 return "", ""
195 }
196 return data.Name, data.Image
197}
198
199func page(offset, limit int) (int, int) {
200 if offset < 0 {
201 offset = 0
202 }
203 if limit < 1 {
204 limit = 1
205 }
206 if limit > pageMax {
207 limit = pageMax
208 }
209 return offset, limit
210}
211
212func quote(s string) string {
213 out := "\""
214 for i := 0; i < len(s); i++ {
215 c := s[i]
216 if c == '\\' || c == '"' {
217 out += "\\"
218 }
219 out += s[i : i+1]
220 }
221 return out + "\""
222}
223
224func i64(n int64) string {
225 return strconv.FormatInt(n, 10)
226}