6

I am using openzeppelin erc721 library to create an NFT and need to list all the tokens of a user. The most obvious way is to iterate through all the tokens in the contract and check if the owner of the token is same as the address I have sent.

function tokensOfOwner(address _owner) external view returns(uint256[] ownerTokens) {
    uint256 tokenCount = balanceOf(_owner);

    if (tokenCount == 0) {
        // Return an empty array
        return new uint256[](0);
    } else {
        uint256[] memory result = new uint256[](tokenCount);
        uint256 totalCats = totalSupply();
        uint256 resultIndex = 0;

        // We count on the fact that all cats have IDs starting at 1 and increasing
        // sequentially up to the totalCat count.
        uint256 catId;

        for (catId = 1; catId <= totalCats; catId++) {
            if (kittyIndexToOwner[catId] == _owner) {
                result[resultIndex] = catId;
                resultIndex++;
            }
        }

        return result;
    }
}

Link to this solution

Another solution that I am thinking of implementing is to have a mapping where I map the user address to an array or another mapping which includes all the tokens the user owns. Upon transferring the token to another address, I would need to remove the token from seller list and add it to buyers list. This solution sounds more reasonable but I dont know how cost efficient it will be. What are the disadvantages of this method?

6
  • 2
    Whatever you do, try to stay away from loops. They are consuming gas. Try to solve these kind of problems off-chain if you can. Commented May 5, 2021 at 19:08
  • take look at this docs.openzeppelin.com/contracts/2.x/api/token/… and see how they implement this method _tokensOfOwner(owner) return all tokens of owner in ERC721 is not a good idea, it is a "NFT" and such a thing you trying to do will only work for a very low number of tokens
    – Majd TL
    Commented May 7, 2021 at 13:06
  • @MajdTL then can you elaborate how properly to list all the NFT, like for marketplace but without the need of centralisation server like opensea? because it is ridiculous when u can't display the list items. Commented Feb 8, 2022 at 0:59
  • 1
    @MiratCanBayrak external view won't use gas, so actually when it's view function, always use loops if possible. Commented Feb 8, 2022 at 1:01
  • If you just want the data offChain to view it and it is not needed to be used by other smart contract then take a look at the Graph. it is a decentralized protocol for indexing and querying data from blockchains. @DellWatson
    – Majd TL
    Commented Feb 8, 2022 at 12:15

3 Answers 3

5
+100

This can be achieved by the following:

Declare storage variables at the contract level

mapping(address => uint256[]) public userOwnedTokens;
mapping(uint256 => int256) public tokenIsAtIndex;

mint(tokenId) {
    // Prior minting logic from OpenZeppelin
    userOwnedTokens[msg.sender].push(tokenId);
    uint256 arrayLength = userOwnedTokens[msg.sender].length;
    tokenIsAtIndex[tokenId] = arrayLength;
}

To get all user tokens without loop, all you have to do is, this is possible as the access specifier for userOwnedTokens is public

contractInstance.methods.userOwnedTokens.call(address) // THIS WILL RETURN AN ARRAY OF TOKEN IDs

In case of transfer of tokens, change tokenIsAtIndex[tokenId] to -1

transfer(from, to, tokenId) {
    // Transfer logic
    uint256 tokenIndex = tokenIsAtIndex[tokenId];
    userOwnedTokens[from][tokenIndex] = 000; // TO DENOTE THAT THE TOKEN HAS BEEN TRANSFERRED, YOU CAN USE ANY OTHER NUMBER
 }

Next time when userOwnedTokens is called you can keep a check wherever the tokenId is 000, those tokens have already been transferred and you can decide not to show it to the user on the frontend.

I hope it helps!!!!!

2
  • Dont really get what is tokenIsAtIndex used for when you just call methods.userOwnedTokens.call(address) tokenIsAtIndex dont have anything with that part?
    – Blissful
    Commented Sep 29, 2021 at 11:36
  • 1
    I don't know why this answer get accepted, definitely wrong when I tried it. userOwnedTokens is on array which userOwnedTokens require index so it will be userOwnedTokens(address, uint256) which make contractInstance.methods.userOwnedTokens.call(address) -> INVALID, from logic itself is already wrong. Commented Feb 8, 2022 at 0:24
4

In the first answer, as you declare "mapping(address => uint256[]) public userOwnedTokens", it expects an address and an uint (position in the array) in the call function. That was my experience.

Based on suggestion made by @dwardu/@hack3r_0m, extend ERC721Enumerable, the following code worked for me, without worries with mappings and indexes:

function getTokenIds(address _owner) public view returns (uint[] memory) {
        uint[] memory _tokensOfOwner = new uint[](ERC721.balanceOf(_owner));
        uint i;

        for (i=0;i<ERC721.balanceOf(_owner);i++){
            _tokensOfOwner[i] = ERC721Enumerable.tokenOfOwnerByIndex(_owner, i);
        }
        return (_tokensOfOwner);
    }
1

As you’re already extending OpenZeppelin’s ERC721 contract, simply extend ERC721Enumerable too, and you may then enumerate user’s tokens by calling tokenOfOwnerByIndex(user, index) for index = 0, 1, 2, …, balanceOf(user) - 1.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.