返回目錄 / 三步構建大規模NFT盲盒項目

2022-07-22 / 1877

前言

NFT盲盒和实体盲盒大同小异。顾名思义,盒内随机装有特定NFT系列的一款NFT。大多数情况下,NFT盲盒所含NFT的稀缺度各有不同。如果幸运降临,拆出超级稀有的NFT,售价可达数万或数百万美元。一般情况下,多数盲盒主人拆出的都是该系列的普通级NFT。
NFT盲盒可通过opensea NFT市场等NFT市场购买。盲盒可随时拆开,或不拆盒直接出售赚钱。

第一步

如何构建大规模的盲盒NFT项目呢,我们知道一个项目中NFT的数量可能是几千或者上万个,我们不可能使用现有市面上的一些“自动创建NFT平台”来一个一个上传,因此第一步,需要使用NFT图片引擎,来创建NFT图片和对应的元数据。

生产自己的盲盒图片

如何制作自己的盲盒呢?通过图片引擎,可以让这一切变得很容易。您可以将图片元素按照部位拆分成单独的模块,并设置每个模块中图片元素出现的机率。

将NFT作品按照不同的模块进行拆分,比如背景,身体轮廓,头部,眼部,装饰物等等,先建立好模块的文件夹。请注意,模块文件夹要使用英文字母。如下图所示。

image.png

创作各模块包含的图片

进入各模块的文件夹,按照模块所描述的NFT作品位置,进行创作。比如我们的NFT中会呈现不同的眼球颜色,我们可以在Eye color文件夹中,对不同的眼球颜色进行创作。

image.png

除了眼球颜色,作品中可能还会包含不同的眼球样式,因此我们可以在眼球样式文件夹中进行创作。

image.png

请注意,每张模块图片,应该摆放在整个NFT作品中正确的位置,如下图所示。

image.png

编辑

编辑完成各模块的图片后,基本已经完成了NFT作品的创作。你可能还注意到,在NFT作品中,不同的样式出现的频率是不同的,在引擎中我们可以轻松的完成这个设置。在之前的模块文件夹中,我们可以在图片文件后面,加上#数字这样的方式,来设置此图片在所有该部位中出现的概率。

image.png

这里设置的#1表示在全部加起来为15的情况下,这个图片会出现1次这样的概率。

生成NFT

修改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的一切信息,都是存储在区块链中,因此,智能合约是盲盒的核心。如下图所示

image 32.jpeg

在图中我们可以看到,项目合约是采用完全开源的形式,确保没有后门等问题,任何人都可以对合约进行查询。同时,每一次的NFT购买,销售,转移,包括每一次生命周期状态的改变,也都可以在区块链中进行查看。如下图所示

image 33.jpeg

盲盒的产生和转移,我们可以使用TokenTracker来进行监控和查验。如下图所示

image 34.jpeg

第三步

盲盒项目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)


网站运行后,如图所示

image 35.jpeg

进入DApp界面后,我们可以看到盲盒总数,表示NFT项目中,总的NFT数量,在这里是20。已售出数量,表示已有多少人购买了NFT。我的购买数量表示在当前账户名下的NFT数量。项目状态有白名单先售,公售,开启盲盒集中状态,将在管理盲盒项目中详细说明。