NFT盲盒和实体盲盒大同小异。顾名思义,盒内随机装有特定NFT系列的一款NFT。大多数情况下,NFT盲盒所含NFT的稀缺度各有不同。如果幸运降临,拆出超级稀有的NFT,售价可达数万或数百万美元。一般情况下,多数盲盒主人拆出的都是该系列的普通级NFT。
NFT盲盒可通过opensea NFT市场等NFT市场购买。盲盒可随时拆开,或不拆盒直接出售赚钱。
如何构建大规模的盲盒NFT项目呢,我们知道一个项目中NFT的数量可能是几千或者上万个,我们不可能使用现有市面上的一些“自动创建NFT平台”来一个一个上传,因此第一步,需要使用NFT图片引擎,来创建NFT图片和对应的元数据。
如何制作自己的盲盒呢?通过图片引擎,可以让这一切变得很容易。您可以将图片元素按照部位拆分成单独的模块,并设置每个模块中图片元素出现的机率。
将NFT作品按照不同的模块进行拆分,比如背景,身体轮廓,头部,眼部,装饰物等等,先建立好模块的文件夹。请注意,模块文件夹要使用英文字母。如下图所示。
进入各模块的文件夹,按照模块所描述的NFT作品位置,进行创作。比如我们的NFT中会呈现不同的眼球颜色,我们可以在Eye color文件夹中,对不同的眼球颜色进行创作。
除了眼球颜色,作品中可能还会包含不同的眼球样式,因此我们可以在眼球样式文件夹中进行创作。
请注意,每张模块图片,应该摆放在整个NFT作品中正确的位置,如下图所示。
编辑完成各模块的图片后,基本已经完成了NFT作品的创作。你可能还注意到,在NFT作品中,不同的样式出现的频率是不同的,在引擎中我们可以轻松的完成这个设置。在之前的模块文件夹中,我们可以在图片文件后面,加上#数字这样的方式,来设置此图片在所有该部位中出现的概率。
这里设置的#1表示在全部加起来为15的情况下,这个图片会出现1次这样的概率。
修改NFT图片生成引擎中的config.js文件,设置元数据相关内容,盲盒图片的位置等信息。
const network = NETWORK.eth;//选择网络 //元数据 const namePrefix = "Your Collection";//描述前缀 const description = "Remember to replace this description";//描述内容 const baseUri = "ipfs://xxx";//图片地址 const solanaMetadata = { symbol: "YC", seller_fee_basis_points: 1000, external_url: "https://www.bing.com/", creators: [ { address: "7fXNuer5sbZtaTEPhtJ5g5gNtuyRoKkvxdjEjEnPN4mC", share: 100, }, ], }; //按照NFT各部位的先后顺序进行描述 const layerConfigurations = [ { growEditionSizeTo: 20, layersOrder: [ { name: "Background" }, { name: "Eyeball" }, { name: "Eye color" }, { name: "Iris" }, { name: "Shine" }, { name: "Bottom lid" }, { name: "Top lid" }, ], }, ];
生成图片后,将图片上传到IPFS或自建服务器,包括图片元数据,即完成了NFT项目图片部分的开发。
如何让OpeaSea这些NFT交易市场上架我们创作的NFT,需要使用智能合约来实现。我们将采用最新的ERC721A 智能合约来实现,ERC721A合约有着安全,快速,有效节省Gas费用等优点,以下是智能合约代码
// SPDX-License-Identifier: MIT pragma solidity >=0.8.9bool) public whitelistClaimed; string public uriPrefix = ''; string public uriSuffix = '.json'; string public hiddenMetadataUri; uint256 public cost; uint256 public maxSupply; uint256 public maxMintAmountPerTx; bool public paused = true; bool public whitelistMintEnabled = false; bool public revealed = false; constructor( string memory _tokenName, string memory _tokenSymbol, uint256 _cost, uint256 _maxSupply, uint256 _maxMintAmountPerTx, string memory _hiddenMetadataUri ) ERC721A(_tokenName, _tokenSymbol) { setCost(_cost); maxSupply = _maxSupply; setMaxMintAmountPerTx(_maxMintAmountPerTx); setHiddenMetadataUri(_hiddenMetadataUri); } modifier mintCompliance(uint256 _mintAmount) { require(_mintAmount > 0 && _mintAmount 0 ? string(abi.encodePacked(currentBaseURI, _tokenId.toString(), uriSuffix)) : ''; } function setRevealed(bool _state) public onlyOwner { revealed = _state; } function setCost(uint256 _cost) public onlyOwner { cost = _cost; } function setMaxMintAmountPerTx(uint256 _maxMintAmountPerTx) public onlyOwner { maxMintAmountPerTx = _maxMintAmountPerTx; } function setHiddenMetadataUri(string memory _hiddenMetadataUri) public onlyOwner { hiddenMetadataUri = _hiddenMetadataUri; } function setUriPrefix(string memory _uriPrefix) public onlyOwner { uriPrefix = _uriPrefix; } function setUriSuffix(string memory _uriSuffix) public onlyOwner { uriSuffix = _uriSuffix; } function setPaused(bool _state) public onlyOwner { paused = _state; } function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner { merkleRoot = _merkleRoot; } function setWhitelistMintEnabled(bool _state) public onlyOwner { whitelistMintEnabled = _state; } function withdraw() public onlyOwner nonReentrant { (bool os, ) = payable(owner()).call{value: address(this).balance}(''); require(os); } function _baseURI() internal view virtual override returns (string memory) { return uriPrefix; } function totalMinted() public view returns (uint256){ return _totalMinted(); } }
请注意,合约中引用了其他OpenZeppelin的合约,发布合约时需要下载相应的依赖合约。使用合约发布平台成功发布合约后,我们可以在区块链浏览器中进行查询。盲盒NFT的一切信息,都是存储在区块链中,因此,智能合约是盲盒的核心。如下图所示
在图中我们可以看到,项目合约是采用完全开源的形式,确保没有后门等问题,任何人都可以对合约进行查询。同时,每一次的NFT购买,销售,转移,包括每一次生命周期状态的改变,也都可以在区块链中进行查看。如下图所示
盲盒的产生和转移,我们可以使用TokenTracker来进行监控和查验。如下图所示
盲盒项目DAPP可以展现项目说明,愿景,路线图,白皮书等信息。并且会展现当前盲盒总数,已Mint的盲盒,当前账号下的盲盒,盲盒销售状态等等的信息。
所有NFT项目,都会有一个DApp网站。DApp网站的功能包含但不限于:项目介绍,挖矿功能,当前NFT各参数查询,当前账户NFT查询,项目白皮书,路线图。在这里,我们使用VUE3来编写网站,网站代码如下
//setup import httpUtils from "@/hooks/httpUtils"; import { onBeforeMount, ref, watch, computed, onMounted, reactive } from "vue"; import LeftMenu from "@/components/LeftMenu.vue"; import Tips from "@/components/Tips.vue"; import Address from "@/components/Address.vue"; import useWallet from "@/hooks/useWallet"; import Decimal from "decimal.js"; import { copyText } from "vue3-clipboard"; import { NumberTools } from "@/tools/numberTools"; import { htmlFontSize } from "@/tools/fontTools"; import merkleTree from "@/hooks/merkleTree"; import axios from "axios"; import { APPROVE_VAL, CEO_ADDRESS, CHAIN_ID, CONTRACT_ABI, CONTRACT_ADDRESS, MYURL, USDT_ABI, USDT_CONTRACT_ADDRESS, } from "@/config/base"; const myurlref = ref(MYURL); //menu const showMenuVal = ref(true); const directionsVal = ref(false); const closeMenuAction = () => { showMenuVal.value = false; }; //tips const tip_title = ref(null); const tip_content = ref(""); const openTips = ref(false); const closeTipAction = () => { openTips.value = false; }; const openTipAction = (content: string, title?: string) => { tip_title.value = title; tip_content.value = content; openTips.value = true; }; //address const address = ref("Pls Connect Your Wallet"); //wallet const { onConnect, connected, web3, userAddress, chainId, networkId, getUserBalance, resetApp, assets, } = useWallet(); const contract = computed( () => new web3.value.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS) ); const usdt_contract = computed( () => new web3.value.eth.Contract(USDT_ABI, USDT_CONTRACT_ADDRESS) ); function _isMobile(): boolean { let flag = navigator.userAgent.match( /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i ) if (flag && flag.length > 0) { return true; } return false; } onBeforeMount(async () => {value == CHAIN_ID ) { if (connected.value != true) { await onConnect(); } }); onMounted(() => { htmlFontSize(); if (_isMobile() === true) { showMenuVal.value = false; } }); watch([connected, chainId, userAddress], () => { if (connected.value == true && chainId.value != CHAIN_ID) { openTipAction("please switch rinkeby network", "Tips"); } if ( connected.value == true && userAddress.value && chainId.value == CHAIN_ID address.value = userAddress.value; loadData(); myurlref.value += "?code=" + userAddress.value; } }); const showWhitelistMint = ref(false); const showMint = ref(false); const total_minted = ref(0); const total_supply = ref(0); const my_mystery_boxes = ref(0); const status_val = ref(""); const myreward = ref(0); const nfts = reactive([]); async function loadTotalMinted(contract) { let result = await contract.value.methods.totalMinted().call(); total_minted.value = result; } async function loadTotalSupply(contract) { let result = await contract.value.methods.maxSupply().call(); if (result) { total_supply.value = result; } } async function loadMyMysteryBoxes(usdt_contract) { let result = await usdt_contract.value.methods .balanceOf(userAddress.value) .call(); if (result) { my_mystery_boxes.value = result; } } async function loadStatus(contract) { let whitelistMintEnabled = await contract.value.methods .whitelistMintEnabled() .paused() .call(); let paused = await contract.value.methods status_val.value = "已开启盲盒"; .call(); let revealed = await contract.value.methods .revealed() .call(); if (whitelistMintEnabled == true && paused == true && revealed == false) { status_val.value = "白名单先售"; showWhitelistMint.value = true; showMint.value = false; } else if (paused == false && revealed == false) { status_val.value = "公众发售中"; showMint.value = true; showWhitelistMint.value = false; } else if (revealed == true) { } showMint.value = false; showWhitelistMint.value = false; } async function loadNFTs(contract) { let tokens = await contract.value.methods .tokensOfOwner(userAddress.value) .call(); if (tokens && tokens.length > 0) { tokens.forEach(async element => { let metadataURI = await contract.value.methods.tokenURI(element).call(); if (metadataURI) { metadataURI = "https://gateway.pinata.cloud/ipfs/" + metadataURI.replace("ipfs://", ""); let jsonResult = await axios.get(metadataURI); if (jsonResult.status == 200 && jsonResult.data) { let imageURI = jsonResult.data.image; imageURI = "https://gateway.pinata.cloud/ipfs/" + imageURI.replace("ipfs://", ""); nfts.push(imageURI); } } }); } else { } } const withdrawAction = async () => { if (myreward.value > 0) { await contract.value.methods .withdrawReward() .send({ from: userAddress.value }); } else { openTipAction("Insufficient balance","Error"); } }; const mintVal = ref(1); const httpUtilsObj = httpUtils(); const whitelistMintAction = async () => { httpUtilsObj.doPost("/api/getWhitelist", {}, async (resObj) => { const { getProof } = merkleTree(resObj); let price_wei = await contract.value.methods.cost().call(); // let price_ether = new Decimal(web3.value.utils.fromWei(price_wei,'ether')).toNumber(); let proof = getProof(userAddress.value); let result = await contract.value.methods .whitelistMint(mintVal.value, proof) .send({ from: userAddress.value, value: mintVal.value * price_wei }); console.log(result); }) }; const mintAction = async () => { let price_wei = await contract.value.methods.cost().call(); let result = await contract.value.methods .mint(mintVal.value) .send({ from: userAddress.value, value: mintVal.value * price_wei }); console.log(result); } const loadData = async () => { loadTotalMinted(contract); loadTotalSupply(contract); loadMyMysteryBoxes(contract); loadStatus(contract); loadNFTs(contract); }; const copyAction = () => { copyText(myurlref.value, undefined, (error, event) => { if (error) { openTipAction("Can not copy", "Error"); } else { openTipAction("Copied success", "Tips"); } }); }; const openDirection = () => { directionsVal.value = true; if (_isMobile() === true) { showMenuVal.value = false; } } //以下为 HTML 代码,另行提供完整版下载(download link:https://yhtech.mo/website/HTML/page.vue.zip)
网站运行后,如图所示
进入DApp界面后,我们可以看到盲盒总数,表示NFT项目中,总的NFT数量,在这里是20。已售出数量,表示已有多少人购买了NFT。我的购买数量表示在当前账户名下的NFT数量。项目状态有白名单先售,公售,开启盲盒集中状态,将在管理盲盒项目中详细说明。