Search Apps Documentation Source Content File Folder Download Copy Actions Download

basic_nft.gno

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