# Alchemy第五周 创建动态NFT **Published by:** [youlaiwuqu空间](https://paragraph.com/@youlaiwuqu/) **Published on:** 2022-09-11 **URL:** https://paragraph.com/@youlaiwuqu/alchemy-nft ## Content 准备工作 本次操作都在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://docs.google.com/forms/d/e/1FAIpQLSdNNLXMYZmIhjcWoT-UedS3AoGpRiPDRaNARUPGXLbX1TVvSg/viewform 填表最后一个可以填你合约的部署地址 https://rinkeby.etherscan.io/address/你的合约地址 有兴趣的可以自己去 https://testnets.opensea.io/ 看自己mint的nft,刷新几下元数据看看 NFT领取地址待更新 ## Publication Information - [youlaiwuqu空间](https://paragraph.com/@youlaiwuqu/): Publication homepage - [All Posts](https://paragraph.com/@youlaiwuqu/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@youlaiwuqu): Subscribe to updates