# 第五周 创建动态NFT

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

### 准备工作

![](/files/4Piu2prk2zkqJHMRhlPP)

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

![](/files/k3bnpMSrIXFLhi5XeufN)

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

![](/files/atbCFiK6x2NGPFWw9IgV)

![](/files/jCUbdAZT3g5Mn0Cd1lC7)

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

![](/files/ADOSE8Qf8mNP6buu7ghU)

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

![](/files/dVpQUL6OmWJyE329UlMQ)

![](/files/Tbpu6oTfazbn8GmszAaE)

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

### 初始化项目

![](/files/OLnMysa1CWuAKR6shwxn)

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

![](/files/jOSa3kzneHBmeXMxUbem)

然后把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;
    }
}
```

![](/files/L0rEkhbD8iQE6IQgQC6x)

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

### 部署合约

![](/files/foG3yKe4AezODCZNuptj)

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

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

![](/files/j8FBfi878Q2uH7Is6waq)

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

![](/files/FblLi1oQ1INwRvp38osb)

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

![](/files/SlvCf878b3vCbb0tkTV3)

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

![](/files/zA3r1TlS0NJUEzEiU1ec)

![](/files/3C0Gt5ZTeMzkDl6ydATf)

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

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

![](/files/r9X6jrrRfqiffVyLGtZd)

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

![](/files/hFfw3mosCIu8LRAUJGrL)

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

![](/files/tA9J094mTG4m5I0WcALn)

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

![](/files/0zNsaBl9o17KnMJgsT1x)

![](/files/SfDAkxx3GmnZZyn2CeyF)

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

![](/files/Mlj7RHD36KdK5PD9Iaoa)

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

![](/files/6pr3XrmJPuU3UhRd7tPr)

![](/files/ymI3tFf3QPRlC8BRVywb)

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

![](/files/e7scoAbkEiQiei32BwM5)

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

![](/files/NEUcK88L2MbnUx8hKqs2)

![](/files/OxDbqUCjJq5ghvEbWytO)

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

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

### 填表

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

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

![](/files/4nowXJ4tSn5CWDlkAchT)

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

NFT领取地址待更新


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://jayjiang.gitbook.io/web3book/alchemy-road-to-web3/di-wu-zhou-chuang-jian-dong-tai-nft.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
