basic_nft.gno
9.14 Kb ยท 423 lines
1package grc721
2
3import (
4 "chain"
5 "chain/runtime"
6 "math/overflow"
7 "strconv"
8
9 "gno.land/p/nt/avl"
10 "gno.land/p/nt/ufmt"
11)
12
13type basicNFT struct {
14 name string
15 symbol string
16 owners avl.Tree // tokenId -> OwnerAddress
17 balances avl.Tree // OwnerAddress -> TokenCount
18 tokenApprovals avl.Tree // TokenId -> ApprovedAddress
19 tokenURIs avl.Tree // TokenId -> URIs
20 operatorApprovals avl.Tree // "OwnerAddress:OperatorAddress" -> bool
21}
22
23// Returns new basic NFT
24func NewBasicNFT(name string, symbol string) *basicNFT {
25 return &basicNFT{
26 name: name,
27 symbol: symbol,
28
29 owners: avl.Tree{},
30 balances: avl.Tree{},
31 tokenApprovals: avl.Tree{},
32 tokenURIs: avl.Tree{},
33 operatorApprovals: avl.Tree{},
34 }
35}
36
37func (s *basicNFT) Name() string { return s.name }
38func (s *basicNFT) Symbol() string { return s.symbol }
39func (s *basicNFT) TokenCount() int64 { return int64(s.owners.Size()) }
40
41// BalanceOf returns balance of input address
42func (s *basicNFT) BalanceOf(addr address) (int64, error) {
43 if err := isValidAddress(addr); err != nil {
44 return 0, err
45 }
46
47 balance, found := s.balances.Get(addr.String())
48 if !found {
49 return 0, nil
50 }
51
52 return balance.(int64), nil
53}
54
55// OwnerOf returns owner of input token id
56func (s *basicNFT) OwnerOf(tid TokenID) (address, error) {
57 owner, found := s.owners.Get(string(tid))
58 if !found {
59 return "", ErrInvalidTokenId
60 }
61
62 return owner.(address), nil
63}
64
65// TokenURI returns the URI of input token id
66func (s *basicNFT) TokenURI(tid TokenID) (string, error) {
67 uri, found := s.tokenURIs.Get(tid.String())
68 if !found {
69 return "", ErrInvalidTokenId
70 }
71
72 return uri.(string), nil
73}
74
75func (s *basicNFT) SetTokenURI(tid TokenID, tURI TokenURI) (bool, error) {
76 // check for invalid TokenID
77 if !s.exists(tid) {
78 return false, ErrInvalidTokenId
79 }
80
81 // check for the right owner
82 owner, err := s.OwnerOf(tid)
83 if err != nil {
84 return false, err
85 }
86 caller := runtime.PreviousRealm().Address()
87 if caller != owner {
88 return false, ErrCallerIsNotOwner
89 }
90 s.tokenURIs.Set(tid.String(), tURI.String())
91 return true, nil
92}
93
94// IsApprovedForAll returns true if operator is approved for all by the owner.
95// Otherwise, returns false
96func (s *basicNFT) IsApprovedForAll(owner, operator address) bool {
97 key := owner.String() + ":" + operator.String()
98 approved, found := s.operatorApprovals.Get(key)
99 if !found {
100 return false
101 }
102
103 return approved.(bool)
104}
105
106// Approve approves the input address for particular token
107func (s *basicNFT) Approve(to address, tid TokenID) error {
108 if err := isValidAddress(to); err != nil {
109 return err
110 }
111
112 owner, err := s.OwnerOf(tid)
113 if err != nil {
114 return err
115 }
116 if owner == to {
117 return ErrApprovalToCurrentOwner
118 }
119
120 caller := runtime.PreviousRealm().Address()
121 if caller != owner && !s.IsApprovedForAll(owner, caller) {
122 return ErrCallerIsNotOwnerOrApproved
123 }
124
125 tidStr := tid.String()
126 s.tokenApprovals.Set(tidStr, to)
127
128 chain.Emit(
129 ApprovalEvent,
130 "slug", s.symbol,
131 "owner", owner.String(),
132 "to", to.String(),
133 "tokenId", tidStr,
134 )
135
136 return nil
137}
138
139// GetApproved return the approved address for token
140func (s *basicNFT) GetApproved(tid TokenID) (address, error) {
141 addr, found := s.tokenApprovals.Get(tid.String())
142 if !found {
143 return zeroAddress, ErrTokenIdNotHasApproved
144 }
145
146 return addr.(address), nil
147}
148
149// SetApprovalForAll can approve the operator to operate on all tokens
150func (s *basicNFT) SetApprovalForAll(operator address, approved bool) error {
151 if err := isValidAddress(operator); err != nil {
152 return ErrInvalidAddress
153 }
154
155 caller := runtime.PreviousRealm().Address()
156 return s.setApprovalForAll(caller, operator, approved)
157}
158
159// Safely transfers `tokenId` token from `from` to `to`, checking that
160// contract recipients are aware of the GRC721 protocol to prevent
161// tokens from being forever locked.
162func (s *basicNFT) SafeTransferFrom(from, to address, tid TokenID) error {
163 caller := runtime.PreviousRealm().Address()
164 if !s.isApprovedOrOwner(caller, tid) {
165 return ErrCallerIsNotOwnerOrApproved
166 }
167
168 err := s.transfer(from, to, tid)
169 if err != nil {
170 return err
171 }
172
173 if !s.checkOnGRC721Received(from, to, tid) {
174 return ErrTransferToNonGRC721Receiver
175 }
176
177 return nil
178}
179
180// Transfers `tokenId` token from `from` to `to`.
181func (s *basicNFT) TransferFrom(from, to address, tid TokenID) error {
182 caller := runtime.PreviousRealm().Address()
183 if !s.isApprovedOrOwner(caller, tid) {
184 return ErrCallerIsNotOwnerOrApproved
185 }
186
187 err := s.transfer(from, to, tid)
188 if err != nil {
189 return err
190 }
191
192 return nil
193}
194
195// Mints `tokenId` and transfers it to `to`.
196func (s *basicNFT) Mint(to address, tid TokenID) error {
197 return s.mint(to, tid)
198}
199
200// Mints `tokenId` and transfers it to `to`. Also checks that
201// contract recipients are using GRC721 protocol
202func (s *basicNFT) SafeMint(to address, tid TokenID) error {
203 err := s.mint(to, tid)
204 if err != nil {
205 return err
206 }
207
208 if !s.checkOnGRC721Received(zeroAddress, to, tid) {
209 return ErrTransferToNonGRC721Receiver
210 }
211
212 return nil
213}
214
215func (s *basicNFT) Burn(tid TokenID) error {
216 owner, err := s.OwnerOf(tid)
217 if err != nil {
218 return err
219 }
220
221 s.beforeTokenTransfer(owner, zeroAddress, tid, 1)
222
223 tidStr := tid.String()
224 s.tokenApprovals.Remove(tidStr)
225 balance, err := s.BalanceOf(owner)
226 if err != nil {
227 return err
228 }
229 balance = overflow.Sub64p(balance, 1)
230
231 ownerStr := owner.String()
232 s.balances.Set(ownerStr, balance)
233 s.owners.Remove(tidStr)
234
235 chain.Emit(
236 BurnEvent,
237 "slug", s.symbol,
238 "from", ownerStr,
239 "tokenId", tidStr,
240 )
241
242 s.afterTokenTransfer(owner, zeroAddress, tid, 1)
243
244 return nil
245}
246
247/* Helper methods */
248
249// Helper for SetApprovalForAll()
250func (s *basicNFT) setApprovalForAll(owner, operator address, approved bool) error {
251 if owner == operator {
252 return ErrApprovalToCurrentOwner
253 }
254
255 key := owner.String() + ":" + operator.String()
256 s.operatorApprovals.Set(key, approved)
257
258 chain.Emit(
259 ApprovalForAllEvent,
260 "slug", s.symbol,
261 "owner", owner.String(),
262 "to", operator.String(),
263 "approved", strconv.FormatBool(approved),
264 )
265
266 return nil
267}
268
269// Helper for TransferFrom() and SafeTransferFrom()
270func (s *basicNFT) transfer(from, to address, tid TokenID) error {
271 if err := isValidAddress(from); err != nil {
272 return ErrInvalidAddress
273 }
274 if err := isValidAddress(to); err != nil {
275 return ErrInvalidAddress
276 }
277
278 if from == to {
279 return ErrCannotTransferToSelf
280 }
281
282 owner, err := s.OwnerOf(tid)
283 if err != nil {
284 return err
285 }
286 if owner != from {
287 return ErrTransferFromIncorrectOwner
288 }
289
290 s.beforeTokenTransfer(from, to, tid, 1)
291
292 // Check that tokenId was not transferred by `beforeTokenTransfer`
293 owner, err = s.OwnerOf(tid)
294 if err != nil {
295 return err
296 }
297 if owner != from {
298 return ErrTransferFromIncorrectOwner
299 }
300
301 tidStr := tid.String()
302 s.tokenApprovals.Remove(tidStr)
303 fromBalance, err := s.BalanceOf(from)
304 if err != nil {
305 return err
306 }
307 toBalance, err := s.BalanceOf(to)
308 if err != nil {
309 return err
310 }
311 fromBalance = overflow.Sub64p(fromBalance, 1)
312 toBalance = overflow.Add64p(toBalance, 1)
313
314 fromStr := from.String()
315 toStr := to.String()
316
317 s.balances.Set(fromStr, fromBalance)
318 s.balances.Set(toStr, toBalance)
319 s.owners.Set(tidStr, to)
320
321 chain.Emit(
322 TransferEvent,
323 "slug", s.symbol,
324 "from", fromStr,
325 "to", toStr,
326 "tokenId", tidStr,
327 )
328
329 s.afterTokenTransfer(from, to, tid, 1)
330
331 return nil
332}
333
334// Helper for Mint() and SafeMint()
335func (s *basicNFT) mint(to address, tid TokenID) error {
336 if err := isValidAddress(to); err != nil {
337 return err
338 }
339
340 if s.exists(tid) {
341 return ErrTokenIdAlreadyExists
342 }
343
344 s.beforeTokenTransfer(zeroAddress, to, tid, 1)
345
346 // Check that tokenId was not minted by `beforeTokenTransfer`
347 if s.exists(tid) {
348 return ErrTokenIdAlreadyExists
349 }
350
351 toBalance, err := s.BalanceOf(to)
352 if err != nil {
353 return err
354 }
355 toBalance = overflow.Add64p(toBalance, 1)
356 toStr := to.String()
357 tidStr := tid.String()
358 s.balances.Set(toStr, toBalance)
359 s.owners.Set(tidStr, to)
360
361 chain.Emit(
362 MintEvent,
363 "slug", s.symbol,
364 "to", toStr,
365 "tokenId", tidStr,
366 )
367
368 s.afterTokenTransfer(zeroAddress, to, tid, 1)
369
370 return nil
371}
372
373func (s *basicNFT) isApprovedOrOwner(addr address, tid TokenID) bool {
374 owner, found := s.owners.Get(tid.String())
375 if !found {
376 return false
377 }
378
379 ownerAddr := owner.(address)
380 if addr == ownerAddr || s.IsApprovedForAll(ownerAddr, addr) {
381 return true
382 }
383
384 approved, err := s.GetApproved(tid)
385 if err != nil {
386 return false
387 }
388
389 return approved == addr
390}
391
392// Checks if token id already exists
393func (s *basicNFT) exists(tid TokenID) bool {
394 _, found := s.owners.Get(tid.String())
395 return found
396}
397
398func (s *basicNFT) beforeTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {
399 // TODO: Implementation
400}
401
402func (s *basicNFT) afterTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {
403 // TODO: Implementation
404}
405
406func (s *basicNFT) checkOnGRC721Received(from, to address, tid TokenID) bool {
407 // TODO: Implementation
408 return true
409}
410
411func (s *basicNFT) RenderHome() (str string) {
412 str += ufmt.Sprintf("# %s ($%s)\n\n", s.name, s.symbol)
413 str += ufmt.Sprintf("* **Total supply**: %d\n", s.TokenCount())
414 str += ufmt.Sprintf("* **Known accounts**: %d\n", s.balances.Size())
415
416 return
417}
418
419func (n *basicNFT) Getter() NFTGetter {
420 return func() IGRC721 {
421 return n
422 }
423}