合約編寫(xiě)
建議讀者先了解下solidity,這里推薦CryptoZombies,還是比較詳細(xì)的。
ok當(dāng)你大概知道自己在做什么之后,我們就可以開(kāi)始編寫(xiě)智能合約了,首先我們需要一個(gè)編譯器,我是用的web ide remix,當(dāng)然他也有桌面版,使用起來(lái)都是一樣的,web版本的話記得做備份,如果僅靠瀏覽器緩存來(lái)做備份的話,很容易吃虧找不到代碼了等會(huì)。
基本介紹
先看幾個(gè)關(guān)鍵常量
uint public constant MAX_TOKENS = 2000;
uint private constant TOKENS_RESERVED = 4;
//normal whitelist price
uint public white_price = 0.008 ether;
//normal price
uint public price = 0.015 ether;
uint256 public constant MAX_MINT_PER_TX = 4;
bool public isSaleActive = false;
uint256 public totalSupply;
mapping(address => uint256) private mintedPerWallet;
string public baseUri;
string public baseExtension = ".json";
MAX_TOKENS指的是該合約最大能mint的數(shù)量
white_price指的是白名單價(jià)格(如果你的合約有白名單的話),注意這里價(jià)格會(huì)帶上ether關(guān)鍵字后綴,表示每一個(gè)nft的單價(jià)
price指的是普通價(jià)格
MAX_MINT_PER_TX表示一個(gè)賬戶能mint的數(shù)量(如果你的合約有這個(gè)需求的話)
isSaleActive表示當(dāng)前合約是否可以mint的狀態(tài)
mintedPerWallet是一個(gè)map,記錄了每一個(gè)賬戶mint的數(shù)量,對(duì)應(yīng)MAX_MINT_PER_TX.
構(gòu)造方法
constructor() ERC721("the smart contract's name", "SYMBOL") {
baseUri = "ipfs://xxxxxxxxxxxxxxxxx/";
whiteRootOG = xxxxxxxx;
whiteRootNormal = xxxxxxxxx;
// for(uint256 i = 1; i <= TOKENS_RESERVED; ++i) {
// _safeMint(msg.sender, i);
// }
// totalSupply = TOKENS_RESERVED;
}
構(gòu)造方法第一個(gè)參數(shù)為合約名字,baseUri為ipfs的json地址,白名單稍后會(huì)講解,構(gòu)造方法這里有些合約會(huì)直接構(gòu)造出來(lái)幾個(gè)給團(tuán)隊(duì)自己使用,看自己需求。
ipfs
ipfs全名InterPlanetary File System, 是一個(gè)分布式的web,實(shí)現(xiàn)了點(diǎn)到點(diǎn)超媒體協(xié)議,可以讓我們的互聯(lián)網(wǎng)速度更快,更加安全, 并且更加開(kāi)放。 理論上的話未來(lái)可以取代http。如果我們傳上去一個(gè)相同的圖片,得到的ipfs鏈接是一樣的,所以ipfs比http更能確保文件的安全性,而且由于是p2p的形式去下載,所以下載速度相較http也會(huì)快速很多。
ok,簡(jiǎn)單介紹了下ipfs,那么我們?cè)撊绾问褂媚兀?br> ipfs上傳工具目前還是比較多的,我這里建議使用ipfs desktop,像pinata也很方便,但普通用戶都有存儲(chǔ)限制。
首先我們上傳一個(gè)包含圖片的文件夾以后獲取到一個(gè)ipfs的cid地址,然后我們就得生成一個(gè)json去告訴用戶,你的nft的圖片,描述,名字等。
類(lèi)似:
tips:如果你要查看你的ipfs上傳的文件,你可以使用這個(gè)鏈接:https://ipfs.io/ipfs/your-ipfs-cid/
把your-ipfs-cid換成你的文件cid即可。
當(dāng)然一般nft使用場(chǎng)景里會(huì)有很多很多nft,那么這里就需要把生成json文件腳本化比較方便了,其實(shí)就是一個(gè)string字符串寫(xiě)入生成文件,可以用java,python等,這里就不貼了。
然后剛才生成的json文件夾必須取名為metadata,然后這個(gè)metadata文件夾的ipfs cid即是我們合約里要用到的baseUri,當(dāng)然這個(gè)baseUri也是可以動(dòng)態(tài)替換的,這個(gè)后面會(huì)詳解,主要用在一些一開(kāi)始給到用戶的nft是未揭秘,然后解密后的這種場(chǎng)景。
mint
function mint(uint256 _numTokens, bytes32[] calldata whitelist_og_proof, bytes32[] calldata whitelist_normal_proof) external payable {
require(isSaleActive, "The sale is paused.");
require(_numTokens <= MAX_MINT_PER_TX, "You cannot mint that many in one transaction.");
require(mintedPerWallet[msg.sender] + _numTokens <= MAX_MINT_PER_TX, "You cannot mint that many total.");
uint256 curTotalSupply = totalSupply;
require(curTotalSupply + _numTokens <= MAX_TOKENS, "Exceeds total supply.");
require(_numTokens * price <= msg.value, "Insufficient funds.");
for(uint256 i = 1; i <= _numTokens; ++i) {
_safeMint(msg.sender, curTotalSupply + i);
}
mintedPerWallet[msg.sender] += _numTokens;
totalSupply += _numTokens;
}
這里注意函數(shù)后綴加了一個(gè)關(guān)鍵詞payable,意思就是這個(gè)是支付函數(shù)。require方法有點(diǎn)類(lèi)似其他語(yǔ)言中的捕捉異常,如果條件為false的話,則直接報(bào)錯(cuò),錯(cuò)誤信息為后面的string。
那么我們來(lái)簡(jiǎn)單的看下這個(gè)mint函數(shù),忽略白名單相關(guān)形參,第一行示意如果合約現(xiàn)在狀態(tài)isSaleActive為false的話,那么現(xiàn)在無(wú)法交易。
第二行就是控制交易數(shù)量,如果用戶申請(qǐng)了超過(guò)每個(gè)人最大能mint的數(shù)量的話,直接報(bào)錯(cuò)。
第三行是控制每個(gè)人能mint的數(shù)量,會(huì)去map里去讀取每個(gè)人mint的數(shù)量,不能超哥一個(gè)人能mint的最大數(shù)量。
下面就是控制最大數(shù)量了,如果你發(fā)行200個(gè)nft,現(xiàn)在已經(jīng)被mint了199個(gè),這個(gè)時(shí)候你還要去mint2個(gè)話,就會(huì)直接報(bào)錯(cuò)Exceeds total supply。
當(dāng)然以上這些情況都需要根據(jù)你實(shí)際合約的需求去自定義。
然后就是去判斷價(jià)格,這里要注意一點(diǎn),_safeMint方法是可以直接去mint的,msg.sender指的是發(fā)起該交易的用戶的account,所以如果你要去給nft設(shè)置價(jià)格的話,必須在_safeMint前去做一道價(jià)格的關(guān)卡來(lái)控制價(jià)格,如果有白名單等價(jià)格不一樣的話,這里都要去做價(jià)格限制。合約和支付寶、微信支付等不一樣的地方就是他的設(shè)置價(jià)格是在這里進(jìn)行條件判斷設(shè)置的。
提現(xiàn)
function withdrawAll() external payable onlyOwner {
uint256 balance = address(this).balance;
uint256 balanceOne = balance * 0.5;
uint256 balanceTwo = balance * 0.5;
( bool transferOne, bool transferTwo ) = payable(msg.sender1, msg.sender2).call{value: balanceOne, balanceTwo}("");
require(transferOne, "Transfer failed.");
}
這里注意有一個(gè)關(guān)鍵字onlyOwner,意思就是只有創(chuàng)建合約的賬號(hào)可以調(diào)用的方法。這里我們把收益五五分成,分給了msg.sender1和msg.sender2,當(dāng)然這里也可以改成只給一個(gè)正好,這里根據(jù)自己的需求來(lái)自定義即可。
白名單
在一些nft中,會(huì)有一部分用戶的mint價(jià)格和普通用戶的mint價(jià)格不一樣,所以我們要存下這部分用戶的account id,然后如果是這個(gè)用戶群體的話,那么前面控制價(jià)格那里可以針對(duì)這些用戶進(jìn)行不一樣的價(jià)格控制操作。
正常邏輯,我們放一個(gè)數(shù)組來(lái)存放這些account,但是如果智能合約發(fā)布以后,我們要去修改這個(gè)白名單賬號(hào)群體的增刪改查,如果是一個(gè)數(shù)組的話,那就很麻煩,而且批量操作如果寫(xiě)的不當(dāng)?shù)脑?,就?huì)多出很多gas費(fèi),而且數(shù)組的話存放空間也會(huì)變大,完全不適合這種動(dòng)態(tài)化的case,所以我們只能另外尋找方法來(lái)解決這個(gè)case。
區(qū)塊鏈技術(shù)中有一個(gè)概念叫做默克爾樹(shù),也就是Merkle樹(shù)。
默克爾樹(shù)是一種哈希樹(shù),其中每個(gè)葉子節(jié)點(diǎn)都標(biāo)有數(shù)據(jù)塊的加密哈希值,而每個(gè)非葉子節(jié)點(diǎn)都標(biāo)有其子節(jié)點(diǎn)的加密哈希值的標(biāo)簽。大多數(shù)哈希樹(shù)的實(shí)現(xiàn)是二進(jìn)制的(每個(gè)節(jié)點(diǎn)有兩個(gè)子節(jié)點(diǎn)),但它們也可以有更多的子節(jié)點(diǎn)。它允許你驗(yàn)證某些數(shù)據(jù)是否存在于樹(shù)中,而不需要去輪訓(xùn)啊遍歷啊。
我的理解是這樣,有多少數(shù)據(jù)就有多少葉子結(jié)點(diǎn),葉子結(jié)點(diǎn)的數(shù)據(jù)是該數(shù)據(jù)的hash值,兩個(gè)葉子結(jié)點(diǎn)會(huì)生成對(duì)應(yīng)的父節(jié)點(diǎn),然后以此往上推,會(huì)有一個(gè)唯一的根結(jié)點(diǎn),數(shù)據(jù)不同根結(jié)點(diǎn)也會(huì)不同,所以其實(shí)可以根據(jù)根節(jié)點(diǎn)的hash和葉子結(jié)點(diǎn)的hash來(lái)類(lèi)推出這個(gè)葉子結(jié)點(diǎn)是否是該數(shù)據(jù)集中。
ok,原理介紹的差不多了,我們來(lái)簡(jiǎn)單介紹下具體該如何實(shí)操。
合約
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
constructor() ERC721("smart contract's name", "SYMBOL") {
baseUri = "ipfs://xxxxxxxxxx/";
whiteRootOG = 0xad8403ee270f9d5d3aae410de98f923e33c6e9c57df0f1c986119fa61192e14c;
//.,.........
}
function isVerifyMerkleNormal(bytes32[] calldata proof) view public returns (bool) {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
return MerkleProof.verify(proof, whiteRootNormal, leaf);
}
首先你需要import,然后合約的角色是去驗(yàn)證,傳入proof,然后生成leaf對(duì)象,verify方法會(huì)根據(jù)root去做校驗(yàn),這里會(huì)在構(gòu)造方法里先初始化一個(gè)root,后面如果有白名單的增刪改查的話,只要去修改這個(gè)root就行了,如果有修改只需要修改一次gas費(fèi)即可。
前端
前端這里的流程是這樣的,有一個(gè)accounts的數(shù)組,根據(jù)這個(gè)數(shù)組去生成默克爾樹(shù)的roots,如果是部署合約的時(shí)候,直接把這個(gè)roots寫(xiě)進(jìn)合約里就行了,但如果是增刪改查白名單的話,就需要在合約里的write contract方法里去更新這個(gè)roots了。
這里注意,如果非數(shù)組里的賬戶生成的proof為空,前端如果要測(cè)試的話就可以這么來(lái)測(cè)試是否為白名單。
npm i -D merkletreejs keccak256
首先我們要npm install需要的插件。
//生成白名單
const generateWhiteOGProofs = () => {
//buffer化葉子結(jié)點(diǎn)
const leafNodes = whitelistAddressesOG.map(addr => keccak256(addr));
//實(shí)例化默克爾樹(shù)
const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });
setMerkleTreeOG(merkleTree);
//獲取根哈希值
const rootHash = merkleTree.getRoot();
console.log(rootHash);
console.log('Whitelist Merkle Tree og\n', merkleTree.toString());
}
部署
選擇compile 合約文件,也就是編譯檢查,編譯通過(guò)后,我們就可以去做發(fā)布操作了。
我這里選擇的是Injected Provider,用的是Goerli測(cè)試網(wǎng)絡(luò),如果你賬號(hào)里沒(méi)有幣的話,推薦這里,每天可以提取0.5eth。然后deploy按鈕就可以直接付了gas費(fèi)后直接測(cè)試發(fā)布。可以在etherscan里直接view。
這里,合約就部署成功了。
驗(yàn)證合約代碼
由于我們先mint了幾個(gè),所以opensea上直接可以查看了。這里要特別注意,opensea上json數(shù)據(jù)出來(lái)比較慢,如果你的圖片或者視頻比較大的話,也可能會(huì)出現(xiàn)過(guò)了很久也出不來(lái)的情況,在這里建議圖片或者視頻小一點(diǎn)。如果數(shù)據(jù)沒(méi)刷出來(lái),可以在opensea上點(diǎn)擊refresh data按鈕。
目前合約部署成功,但contract的方法并沒(méi)有顯示。所有如果我們要在etherscan上直接讀取合約的一些數(shù)據(jù),或者對(duì)合約進(jìn)行了一些修改操作,比如修改價(jià)格,修改白名單等,就需要對(duì)contract方法進(jìn)行驗(yàn)證。
可以直接在remix上操作,插件里搜索flattener,點(diǎn)擊activate。
然后直接保存sol文件直接在etherscan里保存進(jìn)去進(jìn)行驗(yàn)證即可。
成功以后我們就可以直接在etherscan上read和write contract了。
前端和合約交互
前端和合約的交互的話主要分為兩類(lèi),對(duì)應(yīng)contact里的read和write方法,這里我分別以read里的獲取已經(jīng)mint了多少個(gè)和mint方法去對(duì)應(yīng)read合約方法和write合約方法為例子。
準(zhǔn)備工作
首先我們?cè)趓emix里目錄contracts/artifacts目錄下找到對(duì)應(yīng)的合約名字的json文件,在你的前端項(xiàng)目中新建一個(gè)contract文件夾,將這個(gè)json文件拷貝過(guò)來(lái),并且記錄下你的合約地址進(jìn)行替換,我們需要根據(jù)這個(gè)地址去獲取合約地址對(duì)象。
npm install ethers
npm下載ethers插件。
import { MerkleTree } from 'merkletreejs';
import { keccak256 } from 'ethers/lib/utils';
import { ethers } from 'ethers';
import { message } from 'antd';
import contract from "./../../../../contracts/NFT.json";
//............
const contractAddress = "your contract address";
const abi = contract.abi;
獲取已經(jīng)mint了的數(shù)量
const getTotalSupply = async () => {
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const nftContract = new ethers.Contract(contractAddress, abi, signer);
//獲取總共多少幣了
nftContract.totalSupply().then(c => {
console.log('已經(jīng)mint了: ' + parseInt(c));
setAlreadyMint(parseInt(c));
if (alreaderMint == maxSale) {
setStatus('SoleOut');
}
});
}
} catch (err) {
console.log(err);
}
}
異步方法,獲取到nftContract對(duì)象后,直接可以調(diào)用totalSupply方法即可,totalSupply方法為編寫(xiě)合約的時(shí)候?qū)懙膔ead方法,如果你要在前端查看價(jià)格等其他read方法,道理相同。
mint
const mintNftHandler = async () => {
try {
if (currentAccount == null) {
connectWalletHandler();
return;
} else {
if (minNum != "1" && minNum != "2" && minNum != "3" && minNum != "4") {
// alert("Up to 5 can be minted");
message.open({ content: 'Up to 4 can be minted' });
} else {
if (alreaderMint == maxSale) {
message.open({ content: 'Sold Out' });
}
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const nftContract = new ethers.Contract(contractAddress, abi, signer);
let normalCost = 0;
if (checkIsWhiteListOG()) {
if (isCurrentAccountMinted) {
normalCost = parseInt(minNum) * whitelistOGPriceLast3;
} else {
normalCost = whitelistOGPrice + (parseInt(minNum) - 1) * whitelistOGPriceLast3;
}
} else if (checkIsWhiteListNormal()) {
if (isCurrentAccountMinted) {
normalCost = parseInt(minNum) * whitelistNormalPriceLast3;
} else {
normalCost = whitelistNormalPrice + (parseInt(minNum) - 1) * whitelistNormalPriceLast3;
}
} else {
normalCost = parseInt(minNum) * normalPrice;
}
const errAddress = keccak256(currentAccount);
console.log(merkleTreeOG.toString());
//取得默克爾證明
const hexProofOG = merkleTreeOG.getHexProof(errAddress);
const hexProofNormal = merkleTreeNormal.getHexProof(errAddress);
let nftTxn = await nftContract.mint(minNum, hexProofOG, hexProofNormal,
{ value: ethers.utils.parseEther(normalCost + "") });
message.open({ content: 'Transaction in progress, Please wait...' });
await nftTxn.wait();
message.open({ content: 'mint successful' });
} else {
message.open({ content: 'Ethereum object does not exist' });
}
}
}
} catch (err) {
console.log(err + "");
}
}
注意這是一個(gè)異步方法,首先根據(jù)用戶類(lèi)型去做判斷,需要支付的價(jià)格,然后就直接調(diào)用nftContract.mint方法直接去mint就可以了,這里參數(shù)直接參照合約里的mint方法,加入了一個(gè)value參數(shù)也就是算好的價(jià)格。這里有一個(gè)安全性考慮,如果用戶在前端代碼反編譯了以后去修改了價(jià)格,因?yàn)槲覀兒霞s里是有做價(jià)格保護(hù)的,所以會(huì)直接報(bào)錯(cuò)價(jià)格不夠。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-779872.html
ok,至此,一個(gè)智能合約的基本流程就通了。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-779872.html
到了這里,關(guān)于智能合約Smart Contract技術(shù)詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!