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数量。项目状态有白名单先售,公售,开启盲盒集中状态,将在管理盲盒项目中详细说明。