basic_nft.gno

9.14 Kb ยท 423 lines
  1package grc721
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"math/overflow"
  7	"strconv"
  8
  9	"gno.land/p/nt/avl"
 10	"gno.land/p/nt/ufmt"
 11)
 12
 13type basicNFT struct {
 14	name              string
 15	symbol            string
 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
 23// Returns new basic NFT
 24func NewBasicNFT(name string, symbol string) *basicNFT {
 25	return &basicNFT{
 26		name:   name,
 27		symbol: symbol,
 28
 29		owners:            avl.Tree{},
 30		balances:          avl.Tree{},
 31		tokenApprovals:    avl.Tree{},
 32		tokenURIs:         avl.Tree{},
 33		operatorApprovals: avl.Tree{},
 34	}
 35}
 36
 37func (s *basicNFT) Name() string      { return s.name }
 38func (s *basicNFT) Symbol() string    { return s.symbol }
 39func (s *basicNFT) TokenCount() int64 { return int64(s.owners.Size()) }
 40
 41// BalanceOf returns balance of input address
 42func (s *basicNFT) BalanceOf(addr address) (int64, error) {
 43	if err := isValidAddress(addr); err != nil {
 44		return 0, err
 45	}
 46
 47	balance, found := s.balances.Get(addr.String())
 48	if !found {
 49		return 0, nil
 50	}
 51
 52	return balance.(int64), nil
 53}
 54
 55// OwnerOf returns owner of input token id
 56func (s *basicNFT) OwnerOf(tid TokenID) (address, error) {
 57	owner, found := s.owners.Get(string(tid))
 58	if !found {
 59		return "", ErrInvalidTokenId
 60	}
 61
 62	return owner.(address), nil
 63}
 64
 65// TokenURI returns the URI of input token id
 66func (s *basicNFT) TokenURI(tid TokenID) (string, error) {
 67	uri, found := s.tokenURIs.Get(tid.String())
 68	if !found {
 69		return "", ErrInvalidTokenId
 70	}
 71
 72	return uri.(string), nil
 73}
 74
 75func (s *basicNFT) SetTokenURI(tid TokenID, tURI TokenURI) (bool, error) {
 76	// check for invalid TokenID
 77	if !s.exists(tid) {
 78		return false, ErrInvalidTokenId
 79	}
 80
 81	// check for the right owner
 82	owner, err := s.OwnerOf(tid)
 83	if err != nil {
 84		return false, err
 85	}
 86	caller := runtime.PreviousRealm().Address()
 87	if caller != owner {
 88		return false, ErrCallerIsNotOwner
 89	}
 90	s.tokenURIs.Set(tid.String(), tURI.String())
 91	return true, nil
 92}
 93
 94// IsApprovedForAll returns true if operator is approved for all by the owner.
 95// Otherwise, returns false
 96func (s *basicNFT) IsApprovedForAll(owner, operator address) bool {
 97	key := owner.String() + ":" + operator.String()
 98	approved, found := s.operatorApprovals.Get(key)
 99	if !found {
100		return false
101	}
102
103	return approved.(bool)
104}
105
106// Approve approves the input address for particular token
107func (s *basicNFT) Approve(to address, tid TokenID) error {
108	if err := isValidAddress(to); err != nil {
109		return err
110	}
111
112	owner, err := s.OwnerOf(tid)
113	if err != nil {
114		return err
115	}
116	if owner == to {
117		return ErrApprovalToCurrentOwner
118	}
119
120	caller := runtime.PreviousRealm().Address()
121	if caller != owner && !s.IsApprovedForAll(owner, caller) {
122		return ErrCallerIsNotOwnerOrApproved
123	}
124
125	tidStr := tid.String()
126	s.tokenApprovals.Set(tidStr, to)
127
128	chain.Emit(
129		ApprovalEvent,
130		"slug", s.symbol,
131		"owner", owner.String(),
132		"to", to.String(),
133		"tokenId", tidStr,
134	)
135
136	return nil
137}
138
139// GetApproved return the approved address for token
140func (s *basicNFT) GetApproved(tid TokenID) (address, error) {
141	addr, found := s.tokenApprovals.Get(tid.String())
142	if !found {
143		return zeroAddress, ErrTokenIdNotHasApproved
144	}
145
146	return addr.(address), nil
147}
148
149// SetApprovalForAll can approve the operator to operate on all tokens
150func (s *basicNFT) SetApprovalForAll(operator address, approved bool) error {
151	if err := isValidAddress(operator); err != nil {
152		return ErrInvalidAddress
153	}
154
155	caller := runtime.PreviousRealm().Address()
156	return s.setApprovalForAll(caller, operator, approved)
157}
158
159// Safely transfers `tokenId` token from `from` to `to`, checking that
160// contract recipients are aware of the GRC721 protocol to prevent
161// tokens from being forever locked.
162func (s *basicNFT) SafeTransferFrom(from, to address, tid TokenID) error {
163	caller := runtime.PreviousRealm().Address()
164	if !s.isApprovedOrOwner(caller, tid) {
165		return ErrCallerIsNotOwnerOrApproved
166	}
167
168	err := s.transfer(from, to, tid)
169	if err != nil {
170		return err
171	}
172
173	if !s.checkOnGRC721Received(from, to, tid) {
174		return ErrTransferToNonGRC721Receiver
175	}
176
177	return nil
178}
179
180// Transfers `tokenId` token from `from` to `to`.
181func (s *basicNFT) TransferFrom(from, to address, tid TokenID) error {
182	caller := runtime.PreviousRealm().Address()
183	if !s.isApprovedOrOwner(caller, tid) {
184		return ErrCallerIsNotOwnerOrApproved
185	}
186
187	err := s.transfer(from, to, tid)
188	if err != nil {
189		return err
190	}
191
192	return nil
193}
194
195// Mints `tokenId` and transfers it to `to`.
196func (s *basicNFT) Mint(to address, tid TokenID) error {
197	return s.mint(to, tid)
198}
199
200// Mints `tokenId` and transfers it to `to`. Also checks that
201// contract recipients are using GRC721 protocol
202func (s *basicNFT) SafeMint(to address, tid TokenID) error {
203	err := s.mint(to, tid)
204	if err != nil {
205		return err
206	}
207
208	if !s.checkOnGRC721Received(zeroAddress, to, tid) {
209		return ErrTransferToNonGRC721Receiver
210	}
211
212	return nil
213}
214
215func (s *basicNFT) Burn(tid TokenID) error {
216	owner, err := s.OwnerOf(tid)
217	if err != nil {
218		return err
219	}
220
221	s.beforeTokenTransfer(owner, zeroAddress, tid, 1)
222
223	tidStr := tid.String()
224	s.tokenApprovals.Remove(tidStr)
225	balance, err := s.BalanceOf(owner)
226	if err != nil {
227		return err
228	}
229	balance = overflow.Sub64p(balance, 1)
230
231	ownerStr := owner.String()
232	s.balances.Set(ownerStr, balance)
233	s.owners.Remove(tidStr)
234
235	chain.Emit(
236		BurnEvent,
237		"slug", s.symbol,
238		"from", ownerStr,
239		"tokenId", tidStr,
240	)
241
242	s.afterTokenTransfer(owner, zeroAddress, tid, 1)
243
244	return nil
245}
246
247/* Helper methods */
248
249// Helper for SetApprovalForAll()
250func (s *basicNFT) setApprovalForAll(owner, operator address, approved bool) error {
251	if owner == operator {
252		return ErrApprovalToCurrentOwner
253	}
254
255	key := owner.String() + ":" + operator.String()
256	s.operatorApprovals.Set(key, approved)
257
258	chain.Emit(
259		ApprovalForAllEvent,
260		"slug", s.symbol,
261		"owner", owner.String(),
262		"to", operator.String(),
263		"approved", strconv.FormatBool(approved),
264	)
265
266	return nil
267}
268
269// Helper for TransferFrom() and SafeTransferFrom()
270func (s *basicNFT) transfer(from, to address, tid TokenID) error {
271	if err := isValidAddress(from); err != nil {
272		return ErrInvalidAddress
273	}
274	if err := isValidAddress(to); err != nil {
275		return ErrInvalidAddress
276	}
277
278	if from == to {
279		return ErrCannotTransferToSelf
280	}
281
282	owner, err := s.OwnerOf(tid)
283	if err != nil {
284		return err
285	}
286	if owner != from {
287		return ErrTransferFromIncorrectOwner
288	}
289
290	s.beforeTokenTransfer(from, to, tid, 1)
291
292	// Check that tokenId was not transferred by `beforeTokenTransfer`
293	owner, err = s.OwnerOf(tid)
294	if err != nil {
295		return err
296	}
297	if owner != from {
298		return ErrTransferFromIncorrectOwner
299	}
300
301	tidStr := tid.String()
302	s.tokenApprovals.Remove(tidStr)
303	fromBalance, err := s.BalanceOf(from)
304	if err != nil {
305		return err
306	}
307	toBalance, err := s.BalanceOf(to)
308	if err != nil {
309		return err
310	}
311	fromBalance = overflow.Sub64p(fromBalance, 1)
312	toBalance = overflow.Add64p(toBalance, 1)
313
314	fromStr := from.String()
315	toStr := to.String()
316
317	s.balances.Set(fromStr, fromBalance)
318	s.balances.Set(toStr, toBalance)
319	s.owners.Set(tidStr, to)
320
321	chain.Emit(
322		TransferEvent,
323		"slug", s.symbol,
324		"from", fromStr,
325		"to", toStr,
326		"tokenId", tidStr,
327	)
328
329	s.afterTokenTransfer(from, to, tid, 1)
330
331	return nil
332}
333
334// Helper for Mint() and SafeMint()
335func (s *basicNFT) mint(to address, tid TokenID) error {
336	if err := isValidAddress(to); err != nil {
337		return err
338	}
339
340	if s.exists(tid) {
341		return ErrTokenIdAlreadyExists
342	}
343
344	s.beforeTokenTransfer(zeroAddress, to, tid, 1)
345
346	// Check that tokenId was not minted by `beforeTokenTransfer`
347	if s.exists(tid) {
348		return ErrTokenIdAlreadyExists
349	}
350
351	toBalance, err := s.BalanceOf(to)
352	if err != nil {
353		return err
354	}
355	toBalance = overflow.Add64p(toBalance, 1)
356	toStr := to.String()
357	tidStr := tid.String()
358	s.balances.Set(toStr, toBalance)
359	s.owners.Set(tidStr, to)
360
361	chain.Emit(
362		MintEvent,
363		"slug", s.symbol,
364		"to", toStr,
365		"tokenId", tidStr,
366	)
367
368	s.afterTokenTransfer(zeroAddress, to, tid, 1)
369
370	return nil
371}
372
373func (s *basicNFT) isApprovedOrOwner(addr address, tid TokenID) bool {
374	owner, found := s.owners.Get(tid.String())
375	if !found {
376		return false
377	}
378
379	ownerAddr := owner.(address)
380	if addr == ownerAddr || s.IsApprovedForAll(ownerAddr, addr) {
381		return true
382	}
383
384	approved, err := s.GetApproved(tid)
385	if err != nil {
386		return false
387	}
388
389	return approved == addr
390}
391
392// Checks if token id already exists
393func (s *basicNFT) exists(tid TokenID) bool {
394	_, found := s.owners.Get(tid.String())
395	return found
396}
397
398func (s *basicNFT) beforeTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {
399	// TODO: Implementation
400}
401
402func (s *basicNFT) afterTokenTransfer(from, to address, firstTokenId TokenID, batchSize int64) {
403	// TODO: Implementation
404}
405
406func (s *basicNFT) checkOnGRC721Received(from, to address, tid TokenID) bool {
407	// TODO: Implementation
408	return true
409}
410
411func (s *basicNFT) RenderHome() (str string) {
412	str += ufmt.Sprintf("# %s ($%s)\n\n", s.name, s.symbol)
413	str += ufmt.Sprintf("* **Total supply**: %d\n", s.TokenCount())
414	str += ufmt.Sprintf("* **Known accounts**: %d\n", s.balances.Size())
415
416	return
417}
418
419func (n *basicNFT) Getter() NFTGetter {
420	return func() IGRC721 {
421		return n
422	}
423}