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