Solidity – Best Method to List All Tokens of a User

blockchainerc-721etheropenzeppelinsolidity

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?

Best Answer

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!!!!!