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