PatrickAlphaC / hardhat-nft-fcc

102 stars 140 forks source link

TypeError: Cannot read properties of undefined (reading 'stack') #63

Closed Kunal-Khatri-1 closed 1 year ago

Kunal-Khatri-1 commented 2 years ago

I can't wrap my head around why I keep getting this error whenever I run mint.js for RandomIpfsNft: error

I believe I am messing up somewhere in getting the random number from Chainlink in the above image and according to mint.js (attached at last), the random number is requested by the contract, and something weird happens.

When I checked the VRF Subscription I found that the somehow requestNft or i_vrfCoordinator.requestRandomWords is asking for way too much Link for getting the random number: error 2

Please help out with this I am stuck here forever.

RandomIpfsNft Contract:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

error RandomIpfsNft__RangeOutOfBounds();
error RandomIpfsNft__NeedMoreEth();
error RandomIpfsNft__TransferFailed();

contract RandomIpfsNft is VRFConsumerBaseV2, ERC721URIStorage, Ownable {
    // when we mint a NFT, we will trigger a Chainlink VRF call to get us a random number
    // using that number, we will get a random NFT
    // Skyie, Duckie, Quackie
    // make these birds have different rarity
    // Quackie => common
    // Duckie => sort of rare
    // Skyie => super rare

    // users have to pay to mint an NFT
    // the owner of the contract can withdraw the ETH => this will reward the artist for creating an NFT

    // kick off a chainlink VRF request

    // Type Declarations
    enum Specie {
        SKYIE,
        DUCKIE,
        QUACKIE
    }

    VRFCoordinatorV2Interface private immutable i_vrfCoordinator;
    uint64 private immutable i_subscriptionId;
    bytes32 private immutable i_gasLane;
    uint32 private immutable i_callbackGasLimit;
    uint16 private constant REQUEST_CONFIRMATIONS = 3;
    uint32 private constant NUM_WORDS = 1;

    // VRF helpers
    mapping(uint256 => address) private s_requestIdToSender;

    // NFT Variables
    uint256 private s_tokenCounter;
    uint256 internal constant MAX_CHANCE_VALUE = 100;
    // paramterizing instead of hardcoding the URIs
    string[] internal s_birdTokenUris;
    uint256 internal i_mintFee;

    // Events
    event NftRequested(uint256 indexed requestId, address requester);
    event NftMinted(Specie birdSpecie, address minter);

    // constructor will still use ERC721 contructor
    // ERC721URIStorage is extending ERC721
    // ERC721URIStorage has no constructor of its own
    constructor(
        address vrfCoordinatorV2,
        uint64 subscriptionId,
        bytes32 gasLane,
        uint32 callbackGasLimit,
        string[3] memory birdTokenUris,
        uint256 mintFee
    ) VRFConsumerBaseV2(vrfCoordinatorV2) ERC721("Birb", "BIRB") {
        i_vrfCoordinator = VRFCoordinatorV2Interface(vrfCoordinatorV2);
        i_subscriptionId = subscriptionId;
        i_gasLane = gasLane;
        i_callbackGasLimit = callbackGasLimit;
        s_birdTokenUris = birdTokenUris;
        i_mintFee = mintFee;
    }

    function requestNft() public payable returns (uint256 requestId) {
        // pay minimum of mintFee to mint the NFT
        if (msg.value < i_mintFee) {
            revert RandomIpfsNft__NeedMoreEth();
        }
        requestId = i_vrfCoordinator.requestRandomWords(
            i_gasLane,
            i_subscriptionId,
            REQUEST_CONFIRMATIONS,
            i_callbackGasLimit,
            NUM_WORDS
        );

        s_requestIdToSender[requestId] = msg.sender;

        emit NftRequested(requestId, msg.sender);
    }

    // can't do _safeMint(msg.sender, s_tokenCounter) inside fulfillRandomWords
    // this function is called by chainlink nodes => they will own the NFT
    // solution => create a mapping between requestId and whoevers calls requestNft
    // then we can use requestId in this function to get the address of the potential NFT owner
    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
        address nftBirdOwner = s_requestIdToSender[requestId];
        uint256 newTokenId = s_tokenCounter;
        s_tokenCounter = s_tokenCounter + 1;

        // getting a number between 0 and 99
        // 0 - 10 => Skyie
        // 10 - 30 => Duckie
        // 30 - 100 => Quackie
        uint256 moddedRng = randomWords[0] % MAX_CHANCE_VALUE;
        Specie birdSpecie = getBirdFromModdedRng(moddedRng);
        _safeMint(nftBirdOwner, newTokenId);
        // _setTokenURI(uint256 tokenId, string memory _tokenURI)
        // casting the enum back to uint

        // extention in the opnezeppelin code called ERC721URIStorage
        // this version of the ERC-721 has function _setTokenURI
        // NOTE: _setTokenURI isn't the most gas efficient operation.
        // NOTE: We are using it because it does have the most customization
        // we can call setTokenURI and this will automatically update that token's token URI to whatever you set it as
        // function tokenURI(uint256) public view override returns (string memory) {}
        // dont need tokenURI function because _setTokenURI is going to set tokenURI
        _setTokenURI(newTokenId, s_birdTokenUris[uint256(birdSpecie)]);

        emit NftMinted(birdSpecie, nftBirdOwner);
    }

    // Ownalbe comes with onlyOwner modifier
    function withdraw() public onlyOwner {
        uint256 amount = address(this).balance;
        (bool success, ) = payable(msg.sender).call{value: amount}("");
        if (!success) {
            revert RandomIpfsNft__TransferFailed();
        }
    }

    function getBirdFromModdedRng(uint256 moddedRng) public pure returns (Specie) {
        uint256 cumulativeSum = 0;
        uint8[3] memory chanceArray = getChanceArray();
        for (uint256 i = 0; i < chanceArray.length; i++) {
            // Skyie = 0 - 9  (10%)
            // Duckie = 10 - 39  (30%)
            // Quackie = 40 = 99 (60%)
            if (moddedRng >= cumulativeSum && moddedRng < chanceArray[i] + cumulativeSum) {
                return Specie(i);
            }
            cumulativeSum += chanceArray[i];
        }

        revert RandomIpfsNft__RangeOutOfBounds();
    }

    function getChanceArray() public pure returns (uint8[3] memory) {
        return [10, 30, 60];
    }

    function getNftRequestSender(uint256 requestId) public view returns (address) {
        return (s_requestIdToSender[requestId]);
    }

    function getMintFee() public view returns (uint256) {
        return i_mintFee;
    }

    function getBirdTokenUris(uint256 index) public view returns (string memory) {
        return s_birdTokenUris[index];
    }

    function getTokenCounter() public view returns (uint256) {
        return s_tokenCounter;
    }
}

Deploy Script:

const { developmentChains, networkConfig } = require("../helper-hardhat-config")
const { verify } = require("../utils/verify")
const { storeImages, storeTokenUriMetadata } = require("../utils/uploadToPinata")
const { ethers, network } = require("hardhat")

const imagesLocation = "./images/randomNft"

const metaDataTemplate = {
    name: "",
    description: "",
    image: "",
    attributes: [
        {
            trait_type: "cuteness",
            value: "",
        },
    ],
}

let tokenUris = [
    "ipfs://QmfXXASpngdeknPtFBC67foGVWJZ1SqW5ab9YhUPbVCRmV",
    "ipfs://QmbCjVQCzV9ncTDffDYMQzVDcxZGGWJD7hthnLTG4Z1dih",
    "ipfs://QmXccaaMPgzYBBL6Lh9oEaFVKReTAhtLUHb2DCHVEiRQLk",
]

const FUND_AMOUNT = ethers.utils.parseUnits("10")

module.exports = async function ({ getNamedAccounts, deployments }) {
    const { deploy, log } = deployments
    const { deployer } = await getNamedAccounts()
    const chainId = network.config.chainId

    log("-------------------------------")

    // get the IPFS hashes of our images
    // 1. With our own IPFS node => centralized (can be done manually and via scripts. Manually done earlier and for scripts => https://docs.ipfs.io/)
    // 2. Hosting on our own node and some other nodes => Pinata => pay to help pin NFT for you => (trusting Pinata to pin our images and that they are not going to go down)
    //      Pinata is just an IPFS node run by somebody else and we can request to pin our data
    // 3. NFT.Storage => uses fileCoin network at backend to pin our data (filecoin => blockchain dedicated to pinning ipfs data and storing decentralized data)

    if (process.env.UPLOAD_TO_PINATA == "true") {
        tokenUris = await handleTokenUris()
    }

    let VRFCoordiantorV2Address, subscriptionId

    if (developmentChains.includes(network.name)) {
        const VRFCoordinatorV2Mock = await ethers.getContract("VRFCoordinatorV2Mock")
        VRFCoordiantorV2Address = VRFCoordinatorV2Mock.address

        const tx = await VRFCoordinatorV2Mock.createSubscription()
        const txReceipt = await tx.wait(1)

        subscriptionId = txReceipt.events[0].args.subId
        await VRFCoordinatorV2Mock.fundSubscription(subscriptionId, FUND_AMOUNT)
    } else {
        VRFCoordiantorV2Address = networkConfig[chainId].vrfCoordinatorV2
        subscriptionId = networkConfig[chainId].subscriptionId
    }

    log("-------------------------------")

    const args = [
        VRFCoordiantorV2Address,
        subscriptionId,
        networkConfig[chainId].gasLane,
        networkConfig[chainId].callbackGasLimit,
        tokenUris,
        networkConfig[chainId].mintFee,
    ]

    const randomipfsNft = await deploy("RandomIpfsNft", {
        from: deployer,
        args: args,
        log: true,
        waitConfirmations: network.config.blockConfirmations || 1,
    })

    log("-------------------------------")

    if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
        log("Verifying...")
        await verify(randomipfsNft.address, args)
    }
}

async function handleTokenUris() {
    tokenUris = []
    // store the Image in IPFS
    // then store the metadata in IPFS
    const { responses: imageUploadResponses, files } = await storeImages(imagesLocation)
    for (imageUploadResponseIndex in imageUploadResponses) {
        // create metadata
        // upload the metadata
        let tokenUriMetadata = { ...metaDataTemplate }

        // files = ["Skyie.png", "Duckie.png", "Quackie.png"]
        tokenUriMetadata.name = files[imageUploadResponseIndex].replace(".png", "")
        tokenUriMetadata.description = `An adorable ${tokenUriMetadata.name} birb`
        tokenUriMetadata.image = `ipfs://${imageUploadResponses[imageUploadResponseIndex].IpfsHash}`
        console.log(`Uploading ${tokenUriMetadata.name}...`)

        // store the JSON to Pinata / IPFS
        const metadataUploadResponse = await storeTokenUriMetadata(tokenUriMetadata)
        tokenUris.push(`ipfs://${metadataUploadResponse.IpfsHash}`)
    }
    console.log("Token URIs Uploaded! They are: ")
    console.log(tokenUris)
    return tokenUris
}

module.exports.tags = ["all", "randomipfs", "main"]

helper-hardhat-config.js:

const { ethers } = require("hardhat")

const networkConfig = {
    5: {
        name: "goerli",
        vrfCoordinatorV2: "0x2Ca8E0C643bDe4C2E08ab1fA0da3401AdAD7734D",
        gasLane: "0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15",
        subscriptionId: "5947",
        callbackGasLimit: "500000",
        mintFee: ethers.utils.parseEther("0.0001"),
        ethUsdPriceFeed: "0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e",
    },

    31337: {
        name: "hardhat",
        gasLane: "0x79d3d8832d904592c0bf9818b621522c988bb8b0c05cdc3b15aea1b6e8db0c15",
        callbackGasLimit: "700000",
        mintFee: ethers.utils.parseEther("0.01"),
    },
}

const developmentChains = ["hardhat", "localhost"]

module.exports = {
    networkConfig,
    developmentChains,
}

mint.js:

const { ethers, network } = require("hardhat")
const { developmentChains } = require("../helper-hardhat-config")

module.exports = async function ({ getNamedAccounts }) {
    const { deployer } = await getNamedAccounts()

    // BASIC NFT
    const basicNft = await ethers.getContract("BasicNft", deployer)
    const basicMintTx = await basicNft.mintNft()
    await basicMintTx.wait(1)
    console.log(`Basic NFT index 0 has token URI: ${await basicNft.tokenURI(0)}`)

    // DYNAMIC SVG NFT
    const highValue = ethers.utils.parseEther("30000")
    const lowValue = ethers.utils.parseEther("1")

    const dynamicSvgNft = await ethers.getContract("DynamicSvgNft", deployer)
    const dynamicSvgNftMintTx = await dynamicSvgNft.mintNft(highValue, lowValue)
    await dynamicSvgNftMintTx.wait(1)

    console.log(`Dynamic SVG NFT index 1 tokenURI: ${await dynamicSvgNft.tokenURI(1)}`)

    // RANDOM IPFS NFT
    const randomIpfsNft = await ethers.getContract("RandomIpfsNft", deployer)
    console.log(`randomIpfsNft: ${randomIpfsNft.address}`)

    const mintFee = await randomIpfsNft.getMintFee()
    console.log(`mintFee: ${mintFee}`)

    await new Promise(async (resolve, reject) => {
        setTimeout(reject, 30000) // 5 mins

        randomIpfsNft.once("NftMinted", async function () {
            resolve()
            console.log("NftMinted event emitted")
        })

        const randomIpfsNftMintTx = await randomIpfsNft.requestNft({ value: mintFee.toString() })
        console.log(`Nft requested...`)
        const randomIpfsNftMintTxReceipt = await randomIpfsNftMintTx.wait(1)
        console.log("requestNft transaction receipt received...")

        if (developmentChains.includes(network.name)) {
            const requestId = randomIpfsNftMintTxReceipt.events[1].args.requestId.toString()
            const vrfCoordinatorV2Mock = await ethers.getContract("VRFCoordinatorV2Mock", deployer)
            await vrfCoordinatorV2Mock.fulfillRandomWords(requestId, randomIpfsNft.address)
        }
    })

    console.log(`Random IPFS NFT index 0 tokenURI: ${await randomIpfsNft.tokenURI(0)}`)
}

module.exports.tags = ["all", "mint"]
PatrickAlphaC commented 1 year ago

Can you:

  1. Make this a discusson on the full repo? https://github.com/smartcontractkit/full-blockchain-solidity-course-js/
  2. Follow this section for formatting questions? https://www.youtube.com/watch?t=19846&v=gyMwXuJrbJQ&feature=youtu.be