10

I am writing an NFT smart contract which I am going to test via Hardhat and deploy on RSK.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract MyNFT is ERC721URIStorage {
    uint private _counter;
    address private _owner;

    constructor() ERC721("My NFT", "MNFT") {
      _owner = msg.sender;
    }

    function owner() public view returns (address) {
      return _owner;
    }

    function mintNFT(address recipient, string memory tokenURI)
        public returns (uint256)
    {
        require(msg.sender == owner(), "Only owner is allowed to mint");
        uint newItemId = ++_counter;
        ERC721._mint(recipient, newItemId);
        ERC721URIStorage._setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

Here I have two public functions: owner and mintNFT both returning some values. In my tests I would like to read the return values coming from these two functions. These are the tests I am running on Hardhat:

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("My NFT", () => {
  let deployer;
  let myNFT;

  // deploy NFT before the tests
  before(async () => {
    [deployer] = await ethers.getSigners();
    const MyNFT = await ethers.getContractFactory('MyNFT');
    myNFT = await MyNFT.deploy();
    await myNFT.deployed();
  });

  describe('Receiving a value returned by a view function', () => {
    it('The deployer should be the s/c owner', async  () => {
      const owner = await myNFT.owner();
      expect(owner).to.equal(deployer.address);
    });
  });
  
  describe('Receiving a value returned by a transacting function', () => {
    it('Should return a correct ID of the newly minted item', async () => {
      const newMintItem = {
        id: 1,
        uri: 'ipfs://Qme3QxqsJih5psasse4d2FFLFLwaKx7wHXW3Topk3Q8b14',
      };
      const newItemId = await myNFT.mintNFT(deployer.address, newMintItem.uri);
      expect(newItemId).to.equal(newMintItem.id);
    });
  });
});

In the case of the owner function I get what I expect: It returns my account address, and the first test passes successfully. However, when it comes to the mintNFT function, I don't get what I expect: Instead of the newly created item ID I get something very different and my second test fails.

Why do two very similar tests give me different results? How do I get a return value from a function that sends a transaction? For reference, this is the hardhat.config.js file I'm using:

require("@nomiclabs/hardhat-waffle");

module.exports = {
  solidity: "0.8.4",
  defaultNetwork: 'rskregtest',
  networks: {
    rskregtest: {
      chainId: 33,
      url: 'http://localhost:4444',
    },
  },
};
Ahsan
  • 339
  • 3
  • 11

2 Answers2

11

Values returned from a transaction are not available outside of EVM.

You can either emit an event, or create a public view getter function of the value.

contract MyNFT is ERC721URIStorage {
    // `public` visibility autogenerates view function named `_counter()`
    uint public _counter;
    event NFTMinted(uint256 indexed _id);

    function mintNFT(address recipient, string memory tokenURI)
        public returns (uint256)
    {
        // ...
        emit NFTMinted(newItemId);
        return newItemId;
    }
}
it('Should return a correct ID of the newly minted item', async () => {
    const newMintItem = {
        id: 1,
        uri: 'ipfs://Qme3QxqsJih5psasse4d2FFLFLwaKx7wHXW3Topk3Q8b14',
    };

    // testing the emitted event
    await expect(myNFT.mintNFT(deployer.address, newMintItem.uri))
        .to.emit(myNFT, "NFTMinted")
        .withArgs(newMintItem.id);

    // testing the getter value
    const counter = await myNFT._counter();
    expect(counter).to.equal(newMintItem.id);
});

Docs: https://ethereum-waffle.readthedocs.io/en/latest/matchers.html#emitting-events

Enginer
  • 3,048
  • 1
  • 26
  • 22
Petr Hejda
  • 40,554
  • 8
  • 72
  • 100
11

You can not directly receive a return value coming from a function that you are sending a transaction to off-chain. You can only do so on-chain - i.e. when one SC function invokes another SC function.

However, in this particular case, you can obtain the value through events, as the ERC721 specification includes a Transfer event:

    /// @dev This emits when ownership of any NFT changes by any mechanism.
    ///  This event emits when NFTs are created (`from` == 0) and destroyed
    ///  (`to` == 0). Exception: during contract creation, any number of NFTs
    ///  may be created and assigned without emitting Transfer. At the time of
    ///  any transfer, the approved address for that NFT (if any) is reset to none.
    event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

In Ethers.js, which you are already using in your hardhat tests, instead of smart contract function return value, you get a transaction response object returned on this line:

const txResponse = await myNFT.mintNFT(deployer.address, newMintItem.uri);

Next you need to get a transaction receipt by calling wait on txResponse:

const txReceipt = await txResponse.wait();

In turn, txReceipt contains an events array which includes all events emitted by your transaction. In this case, expect it to include a single Transfer event indicating the first minted item transfer from the zero address to your account address. Get this event by extracting the first element from the events array:

const [transferEvent] = txReceipt.events;

Next take tokenId property from the transferEvent arguments and this will be the Id of your newly minted NFT:

const { tokenId } = transferEvent.args;

The whole test should look as follows:

describe('Receiving a value returned by a transacting function', () => {
    it('Should return a correct ID of the newly minted item', async () => {
      const newMintItem = {
        id: 1,
        uri: 'ipfs://QmY6KX35Rg25rnaffmZzGUFb3raRhtPA5JEFeSSWQA4GHL',
      };
      const txResponse = await myNFT.mintNFT(deployer.address, newMintItem.uri);
      const txReceipt = await txResponse.wait();
      const [transferEvent] = txReceipt.events;
      const { tokenId } = transferEvent.args;
      expect(tokenId).to.equal(newMintItem.id);
    });
  });
Aleks Shenshin
  • 2,117
  • 5
  • 18