Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

basic_nft.gno

10.01 Kb · 431 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 := s.balances.Get(addr.String())
 47	if balance == nil {
 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 := s.owners.Get(string(tid))
 57	if owner == nil {
 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 := s.tokenURIs.Get(tid.String())
 67	if uri == nil {
 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 := s.operatorApprovals.Get(key)
101	if approved == nil {
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 := s.tokenApprovals.Get(tid.String())
146	if addr == nil {
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 := s.owners.Get(tid.String())
381	if owner == nil {
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	return s.owners.Has(tid.String())
401}
402
403func (s *basicNFT) beforeTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {
404	// TODO: Implementation
405}
406
407func (s *basicNFT) afterTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {
408	// TODO: Implementation
409}
410
411func (s *basicNFT) checkOnGRC721Received(from, to address, tid TokenID) bool {
412	// TODO: Implementation
413	return true
414}
415
416func (s *basicNFT) RenderHome() (str string) {
417	str += ufmt.Sprintf("# %s ($%s)\n\n", s.name, s.symbol)
418	str += ufmt.Sprintf("* **Total supply**: %d\n", s.TokenCount())
419	str += ufmt.Sprintf("* **Known accounts**: %d\n", s.balances.Size())
420
421	return
422}
423
424// Getter returns an NFTGetter that yields a reader-only view of the NFT.
425// Safe to register with cross-realm aggregators like tokenhub — readers
426// only, no rlm-typed methods, so no cur can be captured via this surface.
427func (n *basicNFT) Getter() NFTGetter {
428	return func() IGRC721Reader {
429		return n
430	}
431}