# Alchemy第五周 创建动态NFT

By [youlaiwuqu空间](https://paragraph.com/@youlaiwuqu) · 2022-09-11

---

准备工作
----

![](https://storage.googleapis.com/papyrus_images/0c1d64d453e484bac1d1a691e295f63096f775ad6120164bfd69698309cac9aa.png)

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

![](https://storage.googleapis.com/papyrus_images/9c52ad4a3e71ca0d9388332f300817e21a37eb97d8921d941f36d70cb1ce388a.png)

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

![](https://storage.googleapis.com/papyrus_images/de5bf5fcffefd3748e96260631ec0273cd46d0f69ba35dc409a766b3421ce388.png)

![](https://storage.googleapis.com/papyrus_images/c9747f69427ee07f90227249eb43d808e4d4102da34dfd92f105018512ee2915.png)

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

![](https://storage.googleapis.com/papyrus_images/7b962b910c6b362dc9bfebb018a96559c30cb084cc3c1dd6705663fe662100d7.png)

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

![](https://storage.googleapis.com/papyrus_images/33969ed90d51334a1b5fa93a730616afb4503e4c86a03866117c6f9e675bbd3c.png)

![](https://storage.googleapis.com/papyrus_images/c4a681478af5d561b4848a52710a58b24ae132e8170c96d5c1efc711f4870409.png)

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

初始化项目
-----

![](https://storage.googleapis.com/papyrus_images/cc86e210bda3b8442576b0e471885de500960cdd224e899e0ab63523ef1648f3.png)

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

![](https://storage.googleapis.com/papyrus_images/829f125994a78b03824d64bc4c728f1ed4c66f623ff2d453baa6ed68231f71b6.png)

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

`}`

`}`

![](https://storage.googleapis.com/papyrus_images/b5793b89841e6c6562466da0c171d1b3397ea7e0610379582a1f69d45843910d.png)

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

部署合约
----

![](https://storage.googleapis.com/papyrus_images/c0f44bab3dbc76dd4935480ca592585f159f63459a0d96fd548bb029b480bf57.png)

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

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

![](https://storage.googleapis.com/papyrus_images/6c5f4da160f0ee287bed169bc2197c277a489eae082735107847cb1bbecf9ff6.png)

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

![](https://storage.googleapis.com/papyrus_images/c536dbdc17b12b9fc0b50f035fed3abd6e82ca97801be79ee0988a4c6b8ba509.png)

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

![](https://storage.googleapis.com/papyrus_images/0c0ab93edb320f4e00a888fe66c59b956053f1f11fa0c80943f4b7e5002ddf9b.png)

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

![](https://storage.googleapis.com/papyrus_images/92cb3bba1686298eefe65e4203cb6e0c70f7dfa84f26dbcd41573c54d17feb7d.png)

![](https://storage.googleapis.com/papyrus_images/d966b963ef6e1e76f33cb814344eb9cfb72b478b1af76a1bf915e82a321c58ed.png)

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

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

![](https://storage.googleapis.com/papyrus_images/7fc98bdfeb02d6e84d2dd72e3a232e37baa3ba623badb15a835685288986bb23.png)

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

![](https://storage.googleapis.com/papyrus_images/27d4a8fcde0c28c89b6ce22ca541314a5b364a96fcf003a5fa4a4771859976bb.png)

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

![](https://storage.googleapis.com/papyrus_images/6657e973c547ce8496576c1a443dd5cb4dac1d2c27532eabe6b0d7743ea9c4f2.png)

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

![](https://storage.googleapis.com/papyrus_images/37fc2e0ee1919cdd35d516e400e3549fa065b57a6ec5b6d2c705753697624c10.png)

![](https://storage.googleapis.com/papyrus_images/9a42950a62ef3eda4bac98c0b697c778e382bc9da33d73cdd394850e225d9a93.png)

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

![](https://storage.googleapis.com/papyrus_images/4525699a80543d7e443ea0030054bc94d30d7311f1e39b39f7dcd0353da80b94.png)

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

![](https://storage.googleapis.com/papyrus_images/51cff7b48a761b29ee9e94583256a4c3768a86f06f134d79323ab278e746840f.png)

![](https://storage.googleapis.com/papyrus_images/6393341d341c20de83d6d26be911a619390c7c9e17b754766a7619075efbfa92.png)

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

![](https://storage.googleapis.com/papyrus_images/aad8b8452f99bec822d02168400ab97583d520e87f9a6f9c986314f3b9c717d1.png)

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

![](https://storage.googleapis.com/papyrus_images/3ccd84bea77c2ea2dee0cf0c632cddcddc340523df4f28261c33802e94671377.png)

![](https://storage.googleapis.com/papyrus_images/45d2cb5c344503e84cdc27e4ad603a03536229ba1bb89ffd1cbb55b5c092f213.png)

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

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

填表
--

填表地址 [https://docs.google.com/forms/d/e/1FAIpQLSdNNLXMYZmIhjcWoT-UedS3AoGpRiPDRaNARUPGXLbX1TVvSg/viewform](https://docs.google.com/forms/d/e/1FAIpQLSdNNLXMYZmIhjcWoT-UedS3AoGpRiPDRaNARUPGXLbX1TVvSg/viewform)

填表最后一个可以填你合约的部署地址 [https://rinkeby.etherscan.io/address/你的合约地址](https://rinkeby.etherscan.io/address/%E4%BD%A0%E7%9A%84%E5%90%88%E7%BA%A6%E5%9C%B0%E5%9D%80)

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

NFT领取地址待更新

---

*Originally published on [youlaiwuqu空间](https://paragraph.com/@youlaiwuqu/alchemy-nft)*
