diff --git a/_deploy/r/gnoswap/gnft/errors.gno b/_deploy/r/gnoswap/gnft/errors.gno index ece3d48d7..6565d9c65 100644 --- a/_deploy/r/gnoswap/gnft/errors.gno +++ b/_deploy/r/gnoswap/gnft/errors.gno @@ -7,7 +7,10 @@ import ( ) var ( - errNoPermission = errors.New("[GNOSWAP-GNFT-001] caller has no permission") + errNoPermission = errors.New("[GNOSWAP-GNFT-001] caller has no permission") + errCannotSetURI = errors.New("[GNOSWAP-GNFT-002] cannot set URI") + errNoTokenForCaller = errors.New("[GNOSWAP-GNFT-003] no token for caller") + errInvalidAddress = errors.New("[GNOSWAP-GNFT-004] invalid addresss") ) func addDetailToError(err error, detail string) string { diff --git a/_deploy/r/gnoswap/gnft/gnft.gno b/_deploy/r/gnoswap/gnft/gnft.gno index 15c1b2690..7fed629a9 100644 --- a/_deploy/r/gnoswap/gnft/gnft.gno +++ b/_deploy/r/gnoswap/gnft/gnft.gno @@ -5,34 +5,55 @@ import ( "std" "time" + "gno.land/p/demo/avl" "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/ownable" "gno.land/p/demo/ufmt" - pusers "gno.land/p/demo/users" - - "gno.land/r/demo/users" - "gno.land/r/gnoswap/v1/common" + "gno.land/r/gnoswap/v1/consts" +) + +var ( + gnft = grc721.NewBasicNFT("GNOSWAP NFT", "GNFT") ) var ( - admin std.Address = "g1q646ctzhvn60v492x8ucvyqnrj2w30cwh6efk5" // deployed position contract - gnft = grc721.NewBasicNFT("GNOSWAP NFT", "GNFT") + owner *ownable.Ownable + tokenList *avl.Tree // addr -> []grc721.TokenID ) -var userMinted = make(map[std.Address][]grc721.TokenID) +func init() { + owner = ownable.NewWithAddress(consts.POSITION_ADDR) // deployed position contract + tokenList = avl.NewTree() +} -// Getters +// Name returns the full name of the NFT +// Returns: +// - string: The name of the NFT collection func Name() string { return gnft.Name() } +// Symbol returns the token symbol of the NFT +// Returns: +// - string: The symbol of the NFT collection func Symbol() string { return gnft.Symbol() } + +// TotalSupply returns the total number of NFTs minted +// Returns: +// - uint64: The total number of tokens that have been minted func TotalSupply() uint64 { return gnft.TokenCount() } +// TokenURI retrieves the metadata URI for a specific token ID +// Parameters: +// - tid: The unique identifier of the token +// +// Returns: +// - string: The metadata URI associated with the token func TokenURI(tid grc721.TokenID) string { uri, err := gnft.TokenURI(tid) if err != nil { @@ -42,103 +63,282 @@ func TokenURI(tid grc721.TokenID) string { return string(uri) } -func BalanceOf(user pusers.AddressOrName) uint64 { - balance, err := gnft.BalanceOf(users.Resolve(user)) +// BalanceOf returns the number of NFTs owned by the specified address. +// Parameters: +// - owner (std.Address): The address to check the NFT balance for. +// +// Returns: +// - uint64: The number of NFTs owned by the address. +// - error: Returns an error if the balance retrieval fails. +func BalanceOf(owner std.Address) (uint64, error) { + balance, err := gnft.BalanceOf(owner) if err != nil { panic(err.Error()) } - - return balance + return balance, nil } -func OwnerOf(tid grc721.TokenID) std.Address { - owner, err := gnft.OwnerOf(tid) +// OwnerOf returns the current owner's address of a specific token ID +// Parameters: +// - tid: The token ID to check ownership of +// +// Returns: +// - std.Address: The address of the token owner +func OwnerOf(tid grc721.TokenID) (std.Address, error) { + ownerAddr, err := gnft.OwnerOf(tid) if err != nil { - panic(err.Error()) + return "", err } - return owner + return ownerAddr, nil } -func IsApprovedForAll(owner, user pusers.AddressOrName) bool { - return gnft.IsApprovedForAll(users.Resolve(owner), users.Resolve(user)) +// SetTokenURI sets the metadata URI using a randomly generated SVG image +// Parameters: +// - tid (grc721.TokenID): The token ID for which the URI will be updated. +// - tURI (grc721.TokenURI): The new metadata URI to associate with the token. +// +// Returns: +// - bool: Returns `true` if the operation is successful. +// - error: Returns an error if the operation fails or the caller is not authorized. +// +// Panics: +// - If the caller is not the token owner, the function panics. +// - If the URI update fails, the function panics with the associated error. +func SetTokenURI(tid grc721.TokenID, tURI grc721.TokenURI) (bool, error) { + assertOnlyNotHalted() + assertCallerIsOwnerOfToken(tid) + + err := setTokenURI(tid, tURI) + if err != nil { + panic(addDetailToError( + errCannotSetURI, + ufmt.Sprintf("token id (%s)", tid), + )) + } + return true, nil } -func GetApproved(tid grc721.TokenID) (std.Address, bool) { - addr, err := gnft.GetApproved(tid) - if err != nil { - return "", false +// SafeTransferFrom securely transfers ownership of a token from one address to another. +// +// This function enforces several checks to ensure the transfer is valid and authorized: +// - Ensures the contract is not halted. +// - Validates the addresses involved in the transfer. +// - Checks that the caller is the token owner or has been approved to transfer the token. +// +// After validation, the function updates the internal token lists by removing the token from the sender's list +// and appending it to the recipient's list. It then calls the underlying transfer logic through `gnft.TransferFrom`. +// +// Parameters: +// - from (std.Address): The current owner's address of the token being transferred. +// - to (std.Address): The recipient's address to receive the token. +// - tid (grc721.TokenID): The ID of the token to be transferred. +// +// Returns: +// - error: Returns `nil` if the transfer is successful; otherwise, it raises an error. +// +// Panics: +// - If the contract is halted. +// - If either `from` or `to` addresses are invalid. +// - If the caller is not the owner or approved operator of the token. +// - If the internal transfer (`gnft.TransferFrom`) fails. +func SafeTransferFrom(from, to std.Address, tid grc721.TokenID) error { + assertOnlyNotHalted() + + assertValidAddr(from) + assertValidAddr(to) + + caller := getPrevAddr() + ownerAddr, _ := OwnerOf(tid) + approved, _ := GetApproved(tid) + if (caller != ownerAddr) && (caller != approved) { + panic(addDetailToError( + errNoPermission, + ufmt.Sprintf("caller (%s) is not the owner or operator of token (%s)", caller, string(tid)), + )) } - return addr, true -} + removeTokenList(from, tid) + appendTokenList(to, tid) -// Setters + checkErr(gnft.TransferFrom(from, to, tid)) + return nil +} -func Approve(user pusers.AddressOrName, tid grc721.TokenID) { - common.IsHalted() +// TransferFrom transfers a token from one address to another +// This function is a direct wrapper around `SafeTransferFrom`, which performs the actual transfer. +// +// Parameters: +// - from (std.Address): The current owner's address of the token being transferred. +// - to (std.Address): The recipient's address to receive the token. +// - tid (grc721.TokenID): The ID of the token to be transferred. +// +// Returns: +// - error: Returns `nil` if the transfer is successful; otherwise, returns an error. +func TransferFrom(from, to std.Address, tid grc721.TokenID) error { + return SafeTransferFrom(from, to, tid) +} - err := gnft.Approve(users.Resolve(user), tid) +// Approve grants permission to transfer a specific token ID to another address. +// +// Parameters: +// - approved (std.Address): The address to grant transfer approval to. +// - tid (grc721.TokenID): The token ID to approve for transfer. +// +// Returns: +// - error: Returns `nil` if the approval is successful, otherwise returns an error. +// +// Panics: +// - If the contract is halted. +// - If the caller is not the token owner. +// - If the `Approve` call fails. +func Approve(approved std.Address, tid grc721.TokenID) error { + assertOnlyNotHalted() + assertCallerIsOwnerOfToken(tid) + + err := gnft.Approve(approved, tid) if err != nil { panic(err.Error()) } + return nil } -func SetApprovalForAll(user pusers.AddressOrName, approved bool) { - common.IsHalted() +// SetApprovalForAll enables or disables approval for a third party (`operator`) to manage all tokens owned by the caller. +// +// Parameters: +// - operator (std.Address): The address to grant or revoke operator permissions for. +// - approved (bool): `true` to enable approval, `false` to revoke approval. +// +// Returns: +// - error: Returns `nil` if the operation is successful, otherwise returns an error. +// +// Panics: +// - If the contract is halted. +// - If the `SetApprovalForAll` operation fails. +func SetApprovalForAll(operator std.Address, approved bool) error { + assertOnlyNotHalted() + checkErr(gnft.SetApprovalForAll(operator, approved)) + return nil +} - err := gnft.SetApprovalForAll(users.Resolve(user), approved) +// GetApproved returns the approved address for a specific token ID. +// +// Parameters: +// - tid (grc721.TokenID): The token ID to check for approval. +// +// Returns: +// - std.Address: The address approved to manage the token. Returns an empty address if no approval exists. +// - error: Returns an error if the lookup fails or the token ID is invalid. +func GetApproved(tid grc721.TokenID) (std.Address, error) { + addr, err := gnft.GetApproved(tid) if err != nil { - panic(err.Error()) + return "", err } + + return addr, nil } -func TransferFrom(from, to pusers.AddressOrName, tid grc721.TokenID) { - common.IsHalted() +// IsApprovedForAll checks if an operator is approved to manage all tokens of an owner. +// +// Parameters: +// - owner (std.Address): The address of the token owner. +// - operator (std.Address): The address to check if it has approval to manage the owner's tokens. +// +// Returns: +// - bool: true if the operator is approved to manage all tokens of the owner, false otherwise. +func IsApprovedForAll(owner, operator std.Address) bool { + return gnft.IsApprovedForAll(owner, operator) +} - err := gnft.TransferFrom(users.Resolve(from), users.Resolve(to), tid) +// SetTokenURIByImageURI generates and sets a new token URI for a specified token ID using a random image URI. +// +// Parameters: +// - tid (grc721.TokenID): The ID of the token for which the URI will be set. +// +// Panics: +// - If the contract is halted. +// - If the caller is not the owner of the token. +// - If the token URI cannot be set. +func SetTokenURIByImageURI(tid grc721.TokenID) { + assertOnlyNotHalted() + assertCallerIsOwnerOfToken(tid) + + tokenURI := genImageURI(generateRandInstance()) + + err := setTokenURI(tid, grc721.TokenURI(tokenURI)) if err != nil { - panic(err.Error()) + panic(addDetailToError( + errCannotSetURI, + ufmt.Sprintf("%s (%s)", err.Error(), string(tid)), + )) } } -// Admin -func Mint(to pusers.AddressOrName, tid grc721.TokenID) grc721.TokenID { - common.IsHalted() - - caller := std.PrevRealm().Addr() - assertIsAdmin(caller) +// SetTokenURILast sets the token URI for the last token owned by the caller using a randomly generated image URI. +// +// This function ensures the contract is active and the caller owns at least one token. +// It retrieves the list of tokens owned by the caller and applies a new token URI to the most recently minted token. +// +// Panics: +// - If the contract is halted. +// - If the caller does not own any tokens (empty token list). +// - If URI generation or assignment fails. +func SetTokenURILast() { + assertOnlyNotHalted() - err := gnft.Mint(users.Resolve(to), tid) - if err != nil { - panic(err.Error()) + caller := getPrevAddr() + tokenListByCaller, _ := getTokenList(caller) + lenTokenListByCaller := len(tokenListByCaller) + if lenTokenListByCaller == 0 { + panic(addDetailToError( + errNoTokenForCaller, + ufmt.Sprintf("caller (%s)", caller), + )) } - userMinted[users.Resolve(to)] = append(userMinted[users.Resolve(to)], tid) + lastTokenId := tokenListByCaller[lenTokenListByCaller-1] + SetTokenURIByImageURI(lastTokenId) +} + +// Mint creates a new NFT and assigns it to the specified address (only callable by owner) +// Parameters: +// - to: The address or username to mint the token to +// - tid: The token ID to assign to the new NFT +// +// Returns: +// - grc721.TokenID: The ID of the newly minted token +func Mint(to std.Address, tid grc721.TokenID) grc721.TokenID { + owner.AssertCallerIsOwner() + assertOnlyNotHalted() + + checkErr(gnft.Mint(to, tid)) + + appendTokenList(to, tid) return tid } +// Burn removes a specific token ID (only callable by owner) +// Parameters: +// - tid: The token ID to burn func Burn(tid grc721.TokenID) { - common.IsHalted() + owner.AssertCallerIsOwner() + assertOnlyNotHalted() - caller := std.PrevRealm().Addr() - assertIsAdmin(caller) - err := gnft.Burn(tid) + ownerAddr, err := OwnerOf(tid) if err != nil { panic(err.Error()) } -} + removeTokenList(ownerAddr, tid) -func SetAdmin(newAdmin pusers.AddressOrName) { - common.IsHalted() - - caller := std.PrevRealm().Addr() - assertIsAdmin(caller) - admin = users.Resolve(newAdmin) + checkErr(gnft.Burn(tid)) } -// Render - +// Render returns the HTML representation of the NFT +// Parameters: +// - path: The path to render +// +// Returns: +// - string: HTML representation of the NFT or 404 if path is invalid func Render(path string) string { switch { case path == "": @@ -148,75 +348,142 @@ func Render(path string) string { } } -// Util -func assertIsAdmin(address std.Address) { - if address != admin { - panic(addDetailToError( - errNoPermission, - ufmt.Sprintf("gnft.gno__assertIsAdmin() || only admin(%s) can call this function, called from %s", admin.String(), address.String()), - )) +// setTokenURI sets the metadata URI for a specific token ID +func setTokenURI(tid grc721.TokenID, tURI grc721.TokenURI) error { + assertOnlyEmptyTokenURI(tid) + _, err := gnft.SetTokenURI(tid, tURI) + if err != nil { + return err } + + prevAddr, prevPkgPath := getPrevAsString() + std.Emit( + "SetTokenURI", + "prevAddr", prevAddr, + "prevRealm", prevPkgPath, + "lpTokenId", "tid", + "tokenURI", "tURI", + ) + + return nil } -func Exists(tid grc721.TokenID) bool { - _, err := gnft.OwnerOf(tid) - if err != nil { - return false +// generateRandInstnace generates a new random instance +// Returns: +// - *rand.Rand: A new random instance +func generateRandInstance() *rand.Rand { + seed1 := uint64(time.Now().Unix()) + TotalSupply() + seed2 := uint64(time.Now().UnixNano()) + TotalSupply() + pcg := rand.NewPCG(seed1, seed2) + return rand.New(pcg) +} + +// getTokenList retrieves the list of nft tokens for an address +// Parameters: +// - addr: The address to check for nft tokens +// +// Returns: +// - []grc721.TokenID: Array of token IDs +// - bool: true if tokens exist for the address, false otherwise +func getTokenList(addr std.Address) ([]grc721.TokenID, bool) { + iTokens, exists := tokenList.Get(addr.String()) + if !exists { + return []grc721.TokenID{}, false } - return true + return iTokens.([]grc721.TokenID), true } -func SetTokenURI(tid grc721.TokenID) { - common.IsHalted() +// mustGetTokenList same as getTokenList but panics if tokens don't exist +// Parameters: +// - addr: The address to check for nft tokens +// +// Returns: +// - []grc721.TokenID: Array of token IDs +func mustGetTokenList(addr std.Address) []grc721.TokenID { + tokens, exists := getTokenList(addr) + if !exists { + panic(ufmt.Sprintf("user %s has no minted nft tokens", addr.String())) + } - // rand instance - seed1 := uint64(time.Now().Unix() + int64(TotalSupply())) - seed2 := uint64(time.Now().UnixNano() + int64(TotalSupply())) - pcg := rand.NewPCG(seed1, seed2) - r := rand.New(pcg) + return tokens +} + +// appendTokenList adds a token ID to the list of nft tokens +// Parameters: +// - addr: The address to append the token for +// - tid: The token ID to append +func appendTokenList(addr std.Address, tid grc721.TokenID) { + prevTokenList, _ := getTokenList(addr) + prevTokenList = append(prevTokenList, tid) + tokenList.Set(addr.String(), prevTokenList) +} + +// removeTokenList removes a token ID from the list of nft tokens +// Parameters: +// - addr: The address to remove the token for +// - tid: The token ID to remove +func removeTokenList(addr std.Address, tid grc721.TokenID) { + prevTokenList, exist := getTokenList(addr) + if !exist { + return + } + + for i, token := range prevTokenList { + if token == tid { + prevTokenList = append(prevTokenList[:i], prevTokenList[i+1:]...) + break + } + } + + tokenList.Set(addr.String(), prevTokenList) +} - tokenURI := genImageURI(r) - ok, _ := gnft.SetTokenURI(tid, grc721.TokenURI(tokenURI)) - if ok { - prevAddr, prevRealm := getPrev() - std.Emit( - "SetTokenURI", - "prevAddr", prevAddr, - "prevRealm", prevRealm, - "lpTokenId", string(tid), - "internal_tokenURI", tokenURI, - ) +// checkErr helper function to panic if an error occurs +// Parameters: +// - err: The error to check +func checkErr(err error) { + if err != nil { + panic(err.Error()) } } -func SetTokenURILast() { +// assertCallerIsOwnerOfToken asserts that the caller is the owner of the token +// Parameters: +// - tid: The token ID to check ownership of +func assertCallerIsOwnerOfToken(tid grc721.TokenID) { + caller := getPrevAddr() + owner, _ := OwnerOf(tid) + if caller != owner { + panic(addDetailToError( + errNoPermission, + ufmt.Sprintf("caller (%s) is not the owner of token (%s)", caller, string(tid)), + )) + } +} + +// assertOnlyNotHalted panics if the contract is halted. +func assertOnlyNotHalted() { common.IsHalted() +} - // rand instance - seed1 := uint64(time.Now().Unix()) - seed2 := uint64(time.Now().UnixNano()) - pcg := rand.NewPCG(seed1, seed2) - r := rand.New(pcg) - - prev := std.PrevRealm() - prevAddr := prev.Addr() - usersNFT := userMinted[prevAddr] - for _, tid := range usersNFT { - tokenURI := genImageURI(r) - - ok, _ := gnft.SetTokenURI(tid, grc721.TokenURI(tokenURI)) - if ok { - prevAddr, prevRealm := getPrev() - std.Emit( - "SetTokenURI", - "prevAddr", prevAddr, - "prevRealm", prevRealm, - "lpTokenId", string(tid), - "internal_tokenURI", tokenURI, - ) - } +// assertValidAddr panics if the address is invalid.s +func assertValidAddr(addr std.Address) { + if !addr.IsValid() { + panic(addDetailToError( + errInvalidAddress, + addr.String(), + )) } +} - delete(userMinted, prevAddr) +// assertOnlyEmptyTokenURI panics if the token URI is not empty. +func assertOnlyEmptyTokenURI(tid grc721.TokenID) { + uri, _ := gnft.TokenURI(tid) + if string(uri) != "" { + panic(addDetailToError( + errCannotSetURI, + ufmt.Sprintf("token id (%s) has already set URI", string(tid)), + )) + } } diff --git a/_deploy/r/gnoswap/gnft/gnft_test.gno b/_deploy/r/gnoswap/gnft/gnft_test.gno new file mode 100644 index 000000000..02f92ebee --- /dev/null +++ b/_deploy/r/gnoswap/gnft/gnft_test.gno @@ -0,0 +1,529 @@ +package gnft + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/grc/grc721" + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + + "gno.land/r/gnoswap/v1/consts" +) + +const ( + errInvalidTokenId = "invalid token id" +) + +var ( + positionAddr = consts.POSITION_ADDR + positionRealm = std.NewCodeRealm(consts.POSITION_PATH) + + addr01 = testutils.TestAddress("addr01") + addr01Realm = std.NewUserRealm(addr01) + + addr02 = testutils.TestAddress("addr02") + addr02Realm = std.NewUserRealm(addr02) +) + +func TestMetadata(t *testing.T) { + tests := []struct { + name string + fn func() string + expected string + }{ + {"Name()", Name, "GNOSWAP NFT"}, + {"Symbol()", Symbol, "GNFT"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uassert.Equal(t, tt.expected, tt.fn()) + }) + } +} + +func TestTotalSupply(t *testing.T) { + tests := []struct { + name string + setup func() + expected uint64 + }{ + { + name: "initial total supply", + expected: uint64(0), + }, + { + name: "total supply after minting", + setup: func() { + std.TestSetRealm(positionRealm) + Mint(addr01, tid(1)) + Mint(addr01, tid(2)) + }, + expected: uint64(2), + }, + { + name: "total supply after burning", + setup: func() { + std.TestSetRealm(positionRealm) + Burn(tid(2)) + }, + expected: uint64(1), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.setup() + } + uassert.Equal(t, tt.expected, TotalSupply()) + }) + } +} + +func TestBalanceOf(t *testing.T) { + tests := []struct { + name string + addr std.Address + expected uint64 + }{ + {"BalanceOf(addr01)", addr01, uint64(1)}, + {"BalanceOf(addr02)", addr02, uint64(0)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + balance, _ := BalanceOf(tt.addr) + uassert.Equal(t, tt.expected, balance) + }) + } +} + +func TestOwnerOf(t *testing.T) { + tests := []struct { + name string + tokenId uint64 + shouldPanic bool + panicMsg string + expected std.Address + }{ + {"OwnerOf(1)", 1, false, "", addr01}, + {"OwnerOf(500)", 500, false, errInvalidTokenId, addr01}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + OwnerOf(tid(tt.tokenId)) + }) + } else { + ownerAddr, err := OwnerOf(tid(tt.tokenId)) + if err != nil { + uassert.Equal(t, tt.panicMsg, err.Error()) + } else { + uassert.Equal(t, tt.expected, ownerAddr) + } + } + }) + } +} + +func TestIsApprovedForAll(t *testing.T) { + tests := []struct { + name string + setup func() + expected bool + }{ + { + name: "IsApprovedForAll(addr01, addr02)", + expected: false, + }, + { + name: "IsApprovedForAll(addr01, addr02) after setting approval", + setup: func() { + std.TestSetRealm(addr01Realm) + SetApprovalForAll((addr02), true) + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.setup() + } + uassert.Equal(t, tt.expected, IsApprovedForAll((addr01), (addr02))) + }) + } +} + +func TestGetApproved(t *testing.T) { + tests := []struct { + name string + setup func() + expectedAddr std.Address + }{ + { + name: "GetApproved(1)", + expectedAddr: std.Address(""), + }, + { + name: "GetApproved(1) after approving", + setup: func() { + std.TestSetRealm(addr01Realm) + Approve(addr02, tid(1)) + }, + expectedAddr: addr02, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.setup() + } + + addr, _ := GetApproved(tid(1)) + uassert.Equal(t, tt.expectedAddr, addr) + }) + } +} + +func TestTransferFrom(t *testing.T) { + resetObject(t) + std.TestSetRealm(positionRealm) + Mint(addr01, tid(1)) + + tests := []struct { + name string + setup func() + callerRealm std.Realm + fromAddr std.Address + toAddr std.Address + tokenIdToTransfer uint64 + shouldPanic bool + panicMsg string + expected std.Address + verifyTokenList func() + }{ + { + name: "transfer non-existent token id", + callerRealm: std.NewUserRealm(addr01), + fromAddr: addr01, + toAddr: addr02, + tokenIdToTransfer: 99, + shouldPanic: true, + panicMsg: "[GNOSWAP-GNFT-001] caller has no permission || caller (g1q646ctzhvn60v492x8ucvyqnrj2w30cwh6efk5) is not the owner or operator of token (99)", + }, + { + name: "transfer token owned by other user without approval", + callerRealm: std.NewUserRealm(addr02), + fromAddr: addr01, + toAddr: addr02, + tokenIdToTransfer: 1, + shouldPanic: true, + panicMsg: "[GNOSWAP-GNFT-001] caller has no permission || caller (g1q646ctzhvn60v492x8ucvyqnrj2w30cwh6efk5) is not the owner or operator of token (1)", + }, + { + name: "transfer token owned by other user with approval", + setup: func() { + std.TestSetRealm(addr01Realm) + Approve((addr02), tid(1)) + }, + callerRealm: std.NewUserRealm(addr02), + fromAddr: addr01, + toAddr: addr02, + tokenIdToTransfer: 1, + verifyTokenList: func() { + uassert.Equal(t, 0, len(mustGetTokenList(addr01))) + uassert.Equal(t, 1, len(mustGetTokenList(addr02))) + }, + }, + { + name: "transfer token owned by caller", + callerRealm: std.NewUserRealm(addr02), + fromAddr: addr02, + toAddr: addr01, + tokenIdToTransfer: 1, + verifyTokenList: func() { + uassert.Equal(t, 1, len(mustGetTokenList(addr01))) + uassert.Equal(t, 0, len(mustGetTokenList(addr02))) + }, + }, + { + name: "transfer from is invalid address", + callerRealm: std.NewUserRealm(addr01), + fromAddr: std.Address(""), + toAddr: addr02, + tokenIdToTransfer: 1, + shouldPanic: true, + panicMsg: "[GNOSWAP-GNFT-004] invalid addresss || ", + }, + { + name: "transfer to is invalid address", + callerRealm: std.NewUserRealm(addr01), + fromAddr: addr01, + toAddr: std.Address("this_is_invalid_address"), + tokenIdToTransfer: 1, + shouldPanic: true, + panicMsg: "[GNOSWAP-GNFT-004] invalid addresss || this_is_invalid_address", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.setup() + } + + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + TransferFrom((tt.fromAddr), (tt.toAddr), tid(tt.tokenIdToTransfer)) + }) + } else { + std.TestSetRealm(tt.callerRealm) + TransferFrom((tt.fromAddr), (tt.toAddr), tid(tt.tokenIdToTransfer)) + tt.verifyTokenList() + } + }) + } +} + +func TestMint(t *testing.T) { + resetObject(t) + + tests := []struct { + name string + callerRealm std.Realm + tokenIdToMint uint64 + addressToMint std.Address + shouldPanic bool + panicMsg string + expected string + verifyTokenList func() + }{ + { + name: "mint without permission", + shouldPanic: true, + panicMsg: "ownable: caller is not owner", + }, + { + name: "mint first nft to addr01", + callerRealm: std.NewCodeRealm(consts.POSITION_PATH), + tokenIdToMint: 1, + addressToMint: addr01, + expected: "1", + verifyTokenList: func() { + uassert.Equal(t, 1, len(mustGetTokenList(addr01))) + }, + }, + { + name: "mint second nft to addr02", + callerRealm: std.NewCodeRealm(consts.POSITION_PATH), + tokenIdToMint: 2, + addressToMint: addr02, + expected: "2", + verifyTokenList: func() { + uassert.Equal(t, 1, len(mustGetTokenList(addr02))) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + Mint((tt.addressToMint), tid(tt.tokenIdToMint)) + }) + + } else { + std.TestSetRealm(tt.callerRealm) + mintedTokenId := Mint((tt.addressToMint), tid(tt.tokenIdToMint)) + uassert.Equal(t, tt.expected, string(mintedTokenId)) + tt.verifyTokenList() + } + }) + } +} + +func TestBurn(t *testing.T) { + tests := []struct { + name string + callerRealm std.Realm + tokenIdToBurn uint64 + shouldPanic bool + panicMsg string + verifyTokenList func() + }{ + { + name: "burn without permission", + tokenIdToBurn: 1, + shouldPanic: true, + panicMsg: "ownable: caller is not owner", + }, + { + name: "burn non-existent token id", + callerRealm: std.NewCodeRealm(consts.POSITION_PATH), + tokenIdToBurn: 99, + shouldPanic: true, + panicMsg: errInvalidTokenId, + }, + { + name: "burn token id(2)", + callerRealm: std.NewCodeRealm(consts.POSITION_PATH), + tokenIdToBurn: 2, + shouldPanic: false, + verifyTokenList: func() { + uassert.Equal(t, 0, len(mustGetTokenList(addr02))) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + std.TestSetRealm(tt.callerRealm) + + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + Burn(tid(tt.tokenIdToBurn)) + }) + } else { + uassert.NotPanics(t, func() { + Burn(tid(tt.tokenIdToBurn)) + }) + tt.verifyTokenList() + } + }) + } +} + +func TestSetTokenURI(t *testing.T) { + tests := []struct { + name string + callerRealm std.Realm + tokenId uint64 + shouldPanic bool + panicMsg string + }{ + { + name: "set token uri without permission", + tokenId: 1, + shouldPanic: true, + panicMsg: `[GNOSWAP-GNFT-001] caller has no permission || caller () is not the owner of token (1)`, + }, + { + name: "set token uri of non-minted token id", + tokenId: 99, + shouldPanic: true, + panicMsg: `[GNOSWAP-GNFT-002] cannot set URI || invalid token id (99)`, + }, + { + name: "set token uri of token id(1)", + callerRealm: addr01Realm, + tokenId: 1, + }, + { + name: "set token uri of token id(1) - twice", + callerRealm: addr01Realm, + tokenId: 1, + shouldPanic: true, + panicMsg: "[GNOSWAP-GNFT-002] cannot set URI || token id (1) has already set URI", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + std.TestSetRealm(tt.callerRealm) + + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + SetTokenURIByImageURI(tid(tt.tokenId)) + }) + } else { + uassert.NotPanics(t, func() { + SetTokenURIByImageURI(tid(tt.tokenId)) + }) + } + }) + } +} + +func TestTokenURI(t *testing.T) { + resetObject(t) + + tests := []struct { + name string + setup func() + tokenId uint64 + shouldPanic bool + panicMsg string + }{ + { + name: "get token uri of non-minted token id", + tokenId: 99, + shouldPanic: true, + panicMsg: errInvalidTokenId, + }, + { + name: "get token uri of minted token but not set token uri", + setup: func() { + std.TestSetRealm(positionRealm) + Mint((addr01), tid(1)) + }, + tokenId: 1, + shouldPanic: true, + panicMsg: errInvalidTokenId, + }, + { + name: "get token uri of minted token after setting token uri", + setup: func() { + std.TestSetRealm(addr01Realm) + SetTokenURIByImageURI(tid(1)) + }, + tokenId: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.setup() + } + + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + TokenURI(tid(tt.tokenId)) + }) + } else { + uassert.NotEmpty(t, TokenURI(tid(tt.tokenId))) + } + }) + } +} + +func TestSetTokenURILast(t *testing.T) { + resetObject(t) + std.TestSetRealm(positionRealm) + Mint(addr01, tid(1)) + Mint(addr01, tid(2)) // last minted + + t.Run("set token uri last", func(t *testing.T) { + std.TestSetRealm(addr01Realm) + SetTokenURILast() + }) + + t.Run("token uri(2)", func(t *testing.T) { + uassert.NotEmpty(t, TokenURI(tid(2))) + }) +} + +func resetObject(t *testing.T) { + t.Helper() + + gnft = grc721.NewBasicNFT("GNOSWAP NFT", "GNFT") + tokenList = avl.NewTree() +} diff --git a/_deploy/r/gnoswap/gnft/gno.mod b/_deploy/r/gnoswap/gnft/gno.mod index f79a9dcee..3af907ab9 100644 --- a/_deploy/r/gnoswap/gnft/gno.mod +++ b/_deploy/r/gnoswap/gnft/gno.mod @@ -1,9 +1 @@ module gno.land/r/gnoswap/v1/gnft - -require ( - gno.land/p/demo/grc/grc721 v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest - gno.land/r/gnoswap/v1/common v0.0.0-latest -) diff --git a/_deploy/r/gnoswap/gnft/tests/svg_generator_test.gno b/_deploy/r/gnoswap/gnft/svg_generator_test.gno similarity index 98% rename from _deploy/r/gnoswap/gnft/tests/svg_generator_test.gno rename to _deploy/r/gnoswap/gnft/svg_generator_test.gno index e8fcce106..4ef09cc7d 100644 --- a/_deploy/r/gnoswap/gnft/tests/svg_generator_test.gno +++ b/_deploy/r/gnoswap/gnft/svg_generator_test.gno @@ -8,7 +8,7 @@ import ( "gno.land/p/demo/uassert" ) -func Test_genImageURI(t *testing.T) { +func TestGenImageURI(t *testing.T) { seed1 := uint64(time.Now().Unix()) seed2 := uint64(time.Now().UnixNano()) pcg := rand.NewPCG(seed1, seed2) diff --git a/_deploy/r/gnoswap/gnft/tests/gnft_test.gno b/_deploy/r/gnoswap/gnft/tests/gnft_test.gno deleted file mode 100644 index 9c90f463d..000000000 --- a/_deploy/r/gnoswap/gnft/tests/gnft_test.gno +++ /dev/null @@ -1,111 +0,0 @@ -package gnft - -import ( - "std" - "testing" - - "gno.land/p/demo/grc/grc721" - "gno.land/p/demo/testutils" - "gno.land/p/demo/uassert" - "gno.land/p/demo/ufmt" - - pusers "gno.land/p/demo/users" - - "gno.land/r/gnoswap/v1/consts" -) - -var ( - positionRealm = std.NewCodeRealm(consts.POSITION_PATH) - - dummyOneAddr = testutils.TestAddress("dummyOne") - dummyOneRealm = std.NewUserRealm(dummyOneAddr) - - dummyTwoAddr = testutils.TestAddress("dummyTwo") - dummyTwoRealm = std.NewUserRealm(dummyTwoAddr) -) - -func TestMint(t *testing.T) { - std.TestSetRealm(positionRealm) - - t.Run("first nft to dummyOne", func(t *testing.T) { - tid := Mint(a2u(dummyOneAddr), tid(1)) - uassert.Equal(t, string(tid), "1") - }) - - t.Run("second nft to dummyOne", func(t *testing.T) { - tid := Mint(a2u(dummyOneAddr), tid(2)) - uassert.Equal(t, string(tid), "2") - }) - - t.Run("third nft to dummyTwo", func(t *testing.T) { - tid := Mint(a2u(dummyTwoAddr), tid(3)) - uassert.Equal(t, string(tid), "3") - }) -} - -func TestSetTokenURILast(t *testing.T) { - t.Run("inital check", func(t *testing.T) { - uassert.Equal(t, len(userMinted), 2) - uassert.Equal(t, len(userMinted[dummyOneAddr]), 2) - uassert.Equal(t, len(userMinted[dummyTwoAddr]), 1) - }) - - t.Run("get token uri (nil should panic)", func(t *testing.T) { - uassert.PanicsWithMessage(t, - "invalid token id", - func() { - TokenURI(tid(1)) - }, - ) - }) - - t.Run("set token uri last", func(t *testing.T) { - std.TestSetRealm(dummyOneRealm) - SetTokenURILast() - }) - - t.Run("get token uri", func(t *testing.T) { - uassert.NotPanics(t, func() { - TokenURI(tid(1)) - }) - }) -} - -func TestTransferFrom(t *testing.T) { - std.TestSetRealm(dummyTwoRealm) - - t.Run("before transfer, check owner", func(t *testing.T) { - uassert.Equal(t, OwnerOf(tid(3)), dummyTwoAddr) - }) - - t.Run("transfer from `two` to `one`", func(t *testing.T) { - TransferFrom(a2u(dummyTwoAddr), a2u(dummyOneAddr), tid(3)) - }) - - t.Run("after transfer, check owner", func(t *testing.T) { - uassert.Equal(t, OwnerOf(tid(3)), dummyOneAddr) - }) - - t.Run("dummyOne can call SetTokenURI", func(t *testing.T) { - std.TestSetRealm(dummyOneRealm) - SetTokenURI(tid(5)) - }) -} - -func TestMetaGetter(t *testing.T) { - t.Run("name", func(t *testing.T) { - uassert.Equal(t, Name(), "GNOSWAP NFT") - }) - - t.Run("symbol", func(t *testing.T) { - uassert.Equal(t, Symbol(), "GNFT") - }) -} - -func a2u(addr std.Address) pusers.AddressOrName { - return pusers.AddressOrName(addr) -} - -func tid(id uint64) grc721.TokenID { - return grc721.TokenID(ufmt.Sprintf("%d", id)) -} diff --git a/_deploy/r/gnoswap/gnft/utils.gno b/_deploy/r/gnoswap/gnft/utils.gno index 6eaed5e81..0e75908cd 100644 --- a/_deploy/r/gnoswap/gnft/utils.gno +++ b/_deploy/r/gnoswap/gnft/utils.gno @@ -3,14 +3,57 @@ package gnft import ( "std" + "gno.land/p/demo/grc/grc721" "gno.land/p/demo/ufmt" + pusers "gno.land/p/demo/users" ) -func isUserCall() bool { - return std.PrevRealm().IsUser() -} - -func getPrev() (string, string) { +// getPrevAsString returns the address and package path of the previous realm. +func getPrevAsString() (string, string) { prev := std.PrevRealm() return prev.Addr().String(), prev.PkgPath() } + +// getPrevAddr returns the address of the previous realm. +func getPrevAddr() std.Address { + return std.PrevRealm().Addr() +} + +// a2u converts std.Address to pusers.AddressOrName. +// pusers is a package that contains the user-related functions. +// +// Input: +// - addr: the address to convert +// +// Output: +// - pusers.AddressOrName: the converted address +func a2u(addr std.Address) pusers.AddressOrName { + return pusers.AddressOrName(addr) +} + +// tid converts uint64 to grc721.TokenID. +// +// Input: +// - id: the uint64 to convert +// +// Output: +// - grc721.TokenID: the converted token ID +func tid(id uint64) grc721.TokenID { + return grc721.TokenID(ufmt.Sprintf("%d", id)) +} + +// Exists checks if a token ID exists. +// +// Input: +// - tid: the token ID to check +// +// Output: +// - bool: true if the token ID exists, false otherwise +func Exists(tid grc721.TokenID) bool { + _, err := gnft.OwnerOf(tid) + if err != nil { + return false + } + + return true +} diff --git a/pool/utils.gno b/pool/utils.gno index 9f3017af0..c731d3618 100644 --- a/pool/utils.gno +++ b/pool/utils.gno @@ -82,7 +82,7 @@ func safeConvertToInt128(value *u256.Uint) *i256.Int { // This function validates that the given `value` is properly initialized and checks whether // it exceeds the maximum value of uint128. If the value exceeds the uint128 range, // it applies a masking operation to truncate the value to fit within the uint128 limit. -//q +// // Parameters: // - value: *u256.Uint, the value to be checked and possibly truncated. //