第五周 创建动态NFT

原文地址 :http://docs.alchemy.com/docs/5-connect-apis-to-your-smart-contracts-using-chainlink 这篇教程教大家如何使用Link预言机3的数据来更新你的智能合约,从而创建一个动态NFT。

关注我的推特(https://twitter.com/SoullessL) ,及时获取Alchemy 教程的最新信息。

准备工作

本次操作都在Rinkeby测试网络完成,所以先把metamask切换到以太坊的Rinkeby测试网。

然后进入 https://faucets.chain.link/ 连接你的钱包,点击Send Request获取测试以太坊和link 代币。

获取测试代币结束以后,进入 https://vrf.chain.link/rinkeby 连接你的钱包,点击Create Subscription,来获取一个Link 预言机的订阅,订阅需要消耗少量gas费。

订阅成功以后,可以回到主页 https://vrf.chain.link/ ,可以看到自己的订阅,记住这个ID号,后面会用到。

然后点击ID号进入,点击Add Funds添加一些LInk来给随机数预言机一些link,后面每次调用都需要消耗这边的link。然后点击Confirm来确认。

初始化项目

进入 https://remix.ethereum.org/ 新建工作区name随意

然后把Contracts和Test文件夹里的文件全部删除

然后在Contracts下面新建文件,名字为 bull&bear(也可以是别的),注意名字最好和workspaces不一样,不然可能冲突,然后贴入下面的代码

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

// Chainlink Imports
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
// This import includes functions from both ./KeeperBase.sol and
// ./interfaces/KeeperCompatibleInterface.sol
import "@chainlink/contracts/src/v0.8/KeeperCompatible.sol";

import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";

// Dev imports. This only works on a local dev network
// and will not work on any test or main livenets.
import "hardhat/console.sol";

contract BullBear is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable, VRFConsumerBaseV2, KeeperCompatibleInterface  {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;
    uint public interval;
    uint public lastTimeStamp;

    AggregatorV3Interface public priceFeed;
    int256 public currentPrice;

    // IPFS URIs for the dynamic nft graphics/metadata.
    // NOTE: These connect to my IPFS Companion node.
    // You should upload the contents of the /ipfs folder to your own node for development.
    string[] bullUrisIpfs = [
        "https://ipfs.io/ipfs/QmRXyfi3oNZCubDxiVFre3kLZ8XeGt6pQsnAQRZ7akhSNs?filename=gamer_bull.json",
        "https://ipfs.io/ipfs/QmRJVFeMrtYS2CUVUM2cHJpBV5aX2xurpnsfZxLTTQbiD3?filename=party_bull.json",
        "https://ipfs.io/ipfs/QmdcURmN1kEEtKgnbkVJJ8hrmsSWHpZvLkRgsKKoiWvW9g?filename=simple_bull.json"
    ];
    string[] bearUrisIpfs = [
        "https://ipfs.io/ipfs/Qmdx9Hx7FCDZGExyjLR6vYcnutUR8KhBZBnZfAPHiUommN?filename=beanie_bear.json",
        "https://ipfs.io/ipfs/QmTVLyTSuiKGUEmb88BgXG3qNC8YgpHZiFbjHrXKH3QHEu?filename=coolio_bear.json",
        "https://ipfs.io/ipfs/QmbKhBXVWmwrYsTPFYfroR2N7NAekAMxHUVg2CWks7i9qj?filename=simple_bear.json"
    ];


    // random
    VRFCoordinatorV2Interface COORDINATOR;

    // Your subscription ID.
    uint64 s_subscriptionId;

    // Goerli coordinator. For other networks,
    // see https://docs.chain.link/docs/vrf-contracts/#configurations
    address vrfCoordinator = 0x6168499c0cFfCaCD319c818142124B7A15E857ab;

    // The gas lane to use, which specifies the maximum gas price to bump to.
    // For a list of available gas lanes on each network,
    // see https://docs.chain.link/docs/vrf-contracts/#configurations
    bytes32 keyHash = 0xd89b2bf150e3b9e13446986e571fb9cab24b13cea0a43ea20a6049a85cc807cc;

    // Depends on the number of requested values that you want sent to the
    // fulfillRandomWords() function. Storing each word costs about 20,000 gas,
    // so 100,000 is a safe default for this example contract. Test and adjust
    // this limit based on the network that you select, the size of the request,
    // and the processing of the callback request in the fulfillRandomWords()
    // function.
    uint32 callbackGasLimit = 100000;

    // The default is 3, but you can set this higher.
    uint16 requestConfirmations = 3;

    // For this example, retrieve 2 random values in one request.
    // Cannot exceed VRFCoordinatorV2.MAX_NUM_WORDS.
    uint32 numWords =  2;
    uint256[] public s_randomWords;
    uint256 public s_requestId;



    event TokensUpdated(string marketTrend);

    constructor(uint updateInterval, address _priceFeed, uint64 subscriptionId) ERC721("Bull&Bear", "BBTK") VRFConsumerBaseV2(vrfCoordinator) {
        interval = updateInterval;
        lastTimeStamp = block.timestamp;

        // https://rinkeby.etherscan.io/address/0xECe365B379E1dD183B20fc5f022230C044d51404
        priceFeed = AggregatorV3Interface(_priceFeed);
        currentPrice = getLatestPrice();

        COORDINATOR = VRFCoordinatorV2Interface(vrfCoordinator);
        s_subscriptionId = subscriptionId;
    }

    function safeMint(address to) public {
        // Current counter value will be the minted token's token ID.
        uint256 tokenId = _tokenIdCounter.current();

        // Increment it so next time it's correct when we call .current()
        _tokenIdCounter.increment();

        // Mint the token
        _safeMint(to, tokenId);

        // Default to a bull NFT
        string memory defaultUri = bullUrisIpfs[s_randomWords[0]%3];
        _setTokenURI(tokenId, defaultUri);

        console.log(
            "DONE!!! minted token ",
            tokenId,
            " and assigned token url: ",
            defaultUri
        );
    }

    function checkUpkeep(bytes calldata) external view override returns (bool upkeepNeeded, bytes memory /*performData*/){
        upkeepNeeded = (block.timestamp - lastTimeStamp) > interval;
    }

    function performUpkeep(bytes calldata) external override{
        if((block.timestamp - lastTimeStamp) > interval){
            lastTimeStamp = block.timestamp;
            int latestPrice = getLatestPrice();

            if(latestPrice == currentPrice){
                return;
            }else if(latestPrice < currentPrice){
                updateAllTokenUris("bears");
            }else{
                updateAllTokenUris("bull");
            }

            currentPrice = latestPrice;
        }
    }

    function getLatestPrice() public view returns(int256){
        (,
        int price,
        ,
        ,) = priceFeed.latestRoundData();
        return price;
    }

    function updateAllTokenUris(string memory trend) internal{
        if(compareStrings("bears", trend)){
            for(uint i=0; i< _tokenIdCounter.current(); i++){
                _setTokenURI(i,bearUrisIpfs[s_randomWords[0]%3]);
            }
        }else {
            for(uint i=0; i< _tokenIdCounter.current(); i++){
                _setTokenURI(i,bullUrisIpfs[s_randomWords[0]%3]);
            }
        }

        emit TokensUpdated(trend);
    }

    function setInterval(uint256 newInterval) public onlyOwner{
        interval = newInterval;
    }

    function setPriceFeed(address newFeed) public onlyOwner{
        priceFeed = AggregatorV3Interface(newFeed);
    }

    function compareStrings(string memory a, string memory b) internal pure returns (bool){
        return keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b));
    }

    // The following functions are overrides required by Solidity.
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function _burn(uint256 tokenId)
        internal
        override(ERC721, ERC721URIStorage)
    {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

        // Assumes the subscription is funded sufficiently.
    function requestRandomWords() external onlyOwner {
        // Will revert if subscription is not set and funded.
        s_requestId = COORDINATOR.requestRandomWords(
        keyHash,
        s_subscriptionId,
        requestConfirmations,
        callbackGasLimit,
        numWords
        );
    }

    function fulfillRandomWords(
        uint256, /* requestId */
        uint256[] memory randomWords
    ) internal override {
        s_randomWords = randomWords;
    }
}

然后点击编译,编译完以后会有一个warning,可以忽略

部署合约

部署的参数 ENVIRONMENT 选择 Metamask,账号选择你Metamask里面有测试以太坊的账号,Contract选择我们写的 Bull&Bear 合约

部署的参数UPDATEINTERVAL填写 10,_PRICEFEED 为 Link 上面测试BTC合约的预言机地址 0xECe365B379E1dD183B20fc5f022230C044d51404(合约地址来源https://docs.chain.link/docs/ethereum-addresses/),SUBSCRIPTIONID 为你自己申请的Link预言机订阅号。

部署成功以后,复制你的合约地址。

然后进入第一部分的 https://vrf.chain.link/rinkeby/ ,把你的合约地址添加到Link预言机的订阅里,如果不添加,那么通过Link预言机获取随机数的方法就会执行失败。

然后我们点击 requestRandomWords 方法来获取随机数,然后等Metamask确定交易完成。因为获取随机数然后赋值的时间会比较长,所以交易成功以后也大概需要等2分钟。

然后我们在s_randomWords后面输入0(0是因为我们随机数获取了2个数字,我们这边取第一个数字),点开以后,点击call,我们会得到link预言机给的随机数,我们可以把这个随机数先记录好,等下次重新获取随机数以后对比一下。

然后我们重复上面的操作,点击requestRandomWords方法重新获取随机数。

等交易成功以后再继续等待2分钟左右,我们在s_randomWords后面输入0,再次获取数据,可以看到数字已经改变。随机数更新的速度会比较慢一点,着急的话可以待会再过来看。

然后我们在safemint里面填写自己的ETH地址,给自己mint一个NFT。

等NFT mint成功以后,我们在tokenURI 这边输入0,点击call,来获取我们mint的NFT的元数据信息。

因为我们是通过获取的随机数来mint NFT的,所以这边大家的得到的filename=party_bull.json应该不一样。结果会是我们代码里写的三个数据filename=gamer_bull.json,filename=party_bull.json,filename=simple_bull.json这三个中的一个。

然后我们在setPriceFeed方法里输入Link预言机以太坊价格的合约地址 0x8A753747A1Fa494EC906cE90E9f37563A8AF630e,点击call来把原来的比特币价格的预言机改成以太坊的,因为测试版的预言机价格更新太慢,所以通过这样的取巧操作。

价格更新以后,我们可以点击一下 getLatestPrice方法,可以看到int256后面就是最新的以太坊价格,其中前4位是个位数,后面8位为小数,这是因为以太坊不支持小数所以价格就不包含小数点。而点击currentPrice,价格还是之前btc的价格,因为这时候预言机还没有触发还没有更新价格。(测试网btc价格1天更新一次,eth1小时更新一次)

然后我们在performUpkeep方法的参数里输入“[]”,点击call,手动来触发价格更新方法,这一步方法里也会更新我们的NFT元数据。

然后你再点击获取Token Id 为0的NFT的元数据,可以看到元数据会变成这三个中的一个。因为我们预言机从btc的价格变到了ETH价格,我们代码里设置如果价格降低,元数据就是三个bear中的一个,反之如果预言机价格变高了,则是bull中的一个。

另外附上Link预言机BNB 价格合于地址 0xcf0f51ca2cDAecb464eeE4227f5295F2384F84ED,各位可以用这三个预言机合约多试几次,看看NFT的动态改变。

填表

填表地址 https://alchemyapi.typeform.com/roadtoweekfive

填表最后一个可以填你合约的部署地址 https://rinkeby.etherscan.io/address/你的合约地址

有兴趣的可以自己去 https://testnets.opensea.io/ 看自己mint的nft,刷新几下元数据看看

NFT领取地址待更新

最后更新于