Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}