'How to receive a value returned by a Solidity smart contract transacting function?
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',
},
},
};
Solution 1:[1]
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
Solution 2:[2]
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);
});
});
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Petr Hejda |
| Solution 2 | Aleks Shenshin |
