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