Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}