Alchemy第五周 创建动态NFT

准备工作

post image

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

post image

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

post image
post image

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

post image

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

post image
post image

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

初始化项目

post image

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

post image

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

}

}

post image

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

部署合约

post image

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

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

post image

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

post image

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

post image

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

post image
post image

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

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

post image

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

post image

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

post image

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

post image
post image

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

post image

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

post image
post image

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

post image

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

post image
post image

然后你再点击获取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领取地址待更新