Skip to content

Latest commit

 

History

History
257 lines (197 loc) · 6.71 KB

File metadata and controls

257 lines (197 loc) · 6.71 KB

foo721.gno

In this section, we will study foo721, a realm for minting NFTs by using the grc721 package, an implementation of the erc721 standard in Gnolang.

The package path of grc721 is gno.land/p/demo/grc721. You may also find the full code here.

Let's first break down the code by segments.

package foo721


import (
 "std"


 "gno.land/p/demo/grc/grc721"
 "gno.land/p/demo/ufmt"
 "gno.land/r/demo/users"
)

var (
 admin std.Address = "g1en7fv87n8kv8vw52dd5r06gx57rp6sp3ajt9gc"
 foo               = grc721.NewBasicNFT("FooNFT", "FNFT")
)

The code imports libraries, packages, and realms that it needs for implementation. Then, it initializes two variables to define the admin address, set the name of the NFT as FooNFT, its symbol as FNFT.

func assertIsAdmin(address std.Address) {
	if address != admin {
		panic("restricted access")
	}
}

func Mint(to users.AddressOrName, tid grc721.TokenID) {
	caller := std.GetOrigCaller()
	assertIsAdmin(caller)
	err := foo.Mint(to.Resolve(), tid)
	if err != nil {
		panic(err)
	}
}

func Burn(tid grc721.TokenID) {
	caller := std.GetOrigCaller()
	assertIsAdmin(caller)
	err := foo.Burn(tid)
	if err != nil {
		panic(err)
	}
}

The functions contained in the code above ensure that only the admin address has access to minting and burning of tokens.

func init() {
 mintNNFT(admin, 10)
}

func mintNNFT(owner std.Address, n uint64) {
 count := foo.TokenCount()
 for i := count; i < count+n; i++ {
   tid := grc721.TokenID(ufmt.Sprintf("%d", i))
   foo.Mint(owner, tid)
 }
}

The init function above mints 10 NFT tokens to the admin address. You can see from the mint function that it takes the address to receive the tokens and the amount of tokens to mint as arguments.

The logic of minting an NFT is more complex compared to that of grc20, due to the characteristics of NFT as follows:

  • All NFTs are identified by a unique uint256 TokenID value.
  • The ID cannot be modified as long as the contract is functional.
  • A common practice of numbering IDs is to start from 0 and increase it by 1 in sequential order.

For example, If we want to mint NFTs from a contract, we need to know the number of NFTs minted so far from the contract, and specify TokenID which starts minting new NFTs.

Let's assume there's an NFT contract that:

  • 10 NFTs have been minted
  • TokenID starts with 0

And, if we want to mint 10 NFTs, TokenID will be 10~19.

func BalanceOf(user users.AddressOrName) uint64 {
	balance, err := foo.BalanceOf(user.Resolve())
	if err != nil {
		panic(err)
	}

	return balance
}

func OwnerOf(tid grc721.TokenID) std.Address {
	owner, err := foo.OwnerOf(tid)
	if err != nil {
		panic(err)
	}

	return owner
}

func IsApprovedForAll(owner, user users.AddressOrName) bool {
	return foo.IsApprovedForAll(owner.Resolve(), user.Resolve())
}

func GetApproved(tid grc721.TokenID) std.Address {
	addr, err := foo.GetApproved(tid)
	if err != nil {
		panic(err)
	}

	return addr
}

func Approve(user users.AddressOrName, tid grc721.TokenID) {
	err := foo.Approve(user.Resolve(), tid)
	if err != nil {
		panic(err)
	}
}

func SetApprovalForAll(user users.AddressOrName, approved bool) {
	err := foo.SetApprovalForAll(user.Resolve(), approved)
	if err != nil {
		panic(err)
	}
}

func TransferFrom(from, to users.AddressOrName, tid grc721.TokenID) {
	err := foo.TransferFrom(from.Resolve(), to.Resolve(), tid)
	if err != nil {
		panic(err)
	}
}

Other functions are defined in the grc721 specification, each with the following roles.

  • BalanceOf: Returns the number of NFTs owned by an address.
  • OwnerOf: Checks the owner address of a token, specified by its id.
  • IsApprovedForAll: Checks if all tokens of the owner has been approved for the operator.
  • GetApproved: Checks the address of the operator that's been approved of a token, specified by its id.
  • Approve: Approves a token owned by the caller to a user. The token is specified by its id.
  • SetApprovalForAll: Approves all tokens owned by the owner to a user.
  • TransferFrom: Transfers a token from the from address to the to address. The token is specified by its id.

Test Code

package foo721

import (
	"std"
	"testing"

	"gno.land/p/demo/testutils"
	"gno.land/p/demo/grc/grc721"
	"gno.land/r/demo/users"
)

func TestFoo721(t *testing.T) {
	admin := users.AddressOrName("g1en7fv87n8kv8vw52dd5r06gx57rp6sp3ajt9gc")
	tester := users.AddressOrName(testutils.TestAddress("tester"))
	operator := users.AddressOrName(testutils.TestAddress("operator"))
	receiver := users.AddressOrName(testutils.TestAddress("receiver"))

	// check admin balance
	shouldEqual(t, BalanceOf(admin), 10)

	// check nft total supply
	shouldEqual(t, foo.TokenCount(), 10)

	// mint nft to another user
	mintNNFT(tester.Resolve(), 3)
	shouldEqual(t, BalanceOf(tester), 3)
	shouldEqual(t, foo.TokenCount(), 13)

	// check owner of nft
	shouldEqual(t, OwnerOf("7"), admin.Resolve())
	shouldEqual(t, OwnerOf("11"), tester.Resolve())


	// // Approve related 
	shouldEqual(t, IsApprovedForAll(admin, operator), false)

	// admin approves one of the tokens(token_id: 7) under his ownership to operator
	std.TestSetOrigCaller(admin.Resolve())
	Approve(operator, grc721.TokenID("7"))
	shouldEqual(t, GetApproved(grc721.TokenID("7")), operator.Resolve())

	// operator sends token(id: 7) to another address in this case 'receiver'
	std.TestSetOrigCaller(operator.Resolve())
	TransferFrom(admin, receiver, grc721.TokenID("7"))
	shouldEqual(t, OwnerOf("7"), receiver.Resolve())

	/ admin approves all tokens under his ownership to operator
	std.TestSetOrigCaller(admin.Resolve())
	SetApprovalForAll(operator, true)
	shouldEqual(t, IsApprovedForAll(admin, operator), true)

	std.TestSetOrigCaller(operator.Resolve())
	TransferFrom(admin, receiver, grc721.TokenID("1"))

	shouldEqual(t, OwnerOf("1"), receiver.Resolve())
}



// Testing HELPER
func shouldEqual(t *testing.T, got interface{}, expected interface{}) {
	t.Helper()

	if got != expected {
		t.Errorf("expected %v(%T), got %v(%T)", expected, expected, got, got)
	}
}

func shouldErr(t *testing.T, err error) {
	t.Helper()
	if err == nil {
		t.Errorf("expected an error, but got nil.")
	}
}

func shouldNoErr(t *testing.T, err error) {
	t.Helper()
	if err != nil {
		t.Errorf("expected no error, but got err: %s.", err.Error())
	}
}

// TODO: implment shouldPanic & shouldNoPanic to receive func that requires param(s)
func shouldPanic(t *testing.T, f func()) {
	defer func() {
		if r := recover(); r == nil {
			t.Errorf("should have panic")
		}
	}()
	f()
}

func shouldNoPanic(t *testing.T, f func()) {
	defer func() {
		if r := recover(); r != nil {
			t.Errorf("should not have panic")
		}
	}()
	f()
}