17. MerkleTree脚本
这一讲我们写一个利用Merkle Tree白名单铸造NFT的脚本,如果你对Merkle Tree合约不熟悉.
Merkle Tree
Merkle Tree,也叫默克尔树或哈希树,是区块链的底层加密技术,被比特币和以太坊区块链广泛采用。Merkle Tree是一种自下而上构建的加密树,每个叶子是对应数据的哈希,而每个非叶子为它的2个子节点的哈希。

Merkle Tree允许对大型数据结构的内容进行有效和安全的验证(Merkle Proof)。对于有N个叶子结点的Merkle Tree,在已知root根值的情况下,验证某个数据是否有效(属于Merkle Tree叶子结点)只需要log(N)个数据(也叫proof),非常高效。如果数据有误,或者给的proof错误,则无法还原出root根植。下面的例子中,叶子L1的Merkle proof为Hash 0-1和Hash 1:知道这两个值,就能验证L1的值是不是在Merkle Tree的叶子中。

Merkle Tree合约简述
MerkleTree合约利用Merkle Tree验证白名单铸造NFT。我们简单讲下这里用到的两个函数:
构造函数:初始化NFT的名称,代号,和
Merkle Tree的root。mint():利用Merkle Proof验证白名单地址并铸造。参数为白名单地址account,铸造的tokenId,和proof。
MerkleTree.js
MerkleTree.js是构建Merkle Tree和Merkle Proof的Javascript包(Github连接)。你可以用npm安装他:
npm install merkletreejs
这里,我们演示如何生成叶子数据包含4个白名单地址的Merkle Tree。
创建白名单地址数组。
import { MerkleTree } from "merkletreejs"; // 白名单地址 const tokens = [ "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB" ];将数据进行
keccak256哈希(与solidity使用的哈希函数匹配),创建叶子结点。const leaf = tokens.map(x => ethers.keccak256(x))创建
Merkle Tree,哈希函数仍然选择keccak256,可选参数sortPairs: true(constructor函数文档),与Merkle Tree合约处理方式保持一致。const merkletree = new MerkleTree(leaf, ethers.keccak256, { sortPairs: true });获得
Merkle Tree的root。const root = merkletree.getHexRoot()获得第
0个叶子节点的proof。const proof = merkletree.getHexProof(leaf[0]);
Merkle Tree白名单铸造NFT
这里,我们举个例子,利用MerkleTree.js和ethers.js验证白名单并铸造NFT。
生成
Merkle Tree。// 1. 生成merkle tree console.log("\n1. 生成merkle tree") // 白名单地址 const tokens = [ "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB" ]; // leaf, merkletree, proof const leaf = tokens.map(x => ethers.keccak256(x)) const merkletree = new MerkleTree(leaf, ethers.keccak256, { sortPairs: true }); const proof = merkletree.getHexProof(leaf[0]); const root = merkletree.getHexRoot() console.log("Leaf:") console.log(leaf) console.log("\nMerkleTree:") console.log(merkletree.toString()) console.log("\nProof:") console.log(proof) console.log("\nRoot:") console.log(root)
创建provider和wallet
const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; const provider = new ethers.JsonRpcProvider(ALCHEMY_GOERLI_URL); // 利用私钥和provider创建wallet对象 const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b' const wallet = new ethers.Wallet(privateKey, provider)创建合约工厂,为部署合约做准备。
// 3. 创建合约工厂 // NFT的abi const abiNFT = [ "constructor(string memory name, string memory symbol, bytes32 merkleroot)", "function name() view returns (string)", "function symbol() view returns (string)", "function mint(address account, uint256 tokenId, bytes32[] calldata proof) external", "function ownerOf(uint256) view returns (address)", "function balanceOf(address) view returns (uint256)", ]; // 合约字节码,在remix中,你可以在两个地方找到Bytecode // i. 部署面板的Bytecode按钮 // ii. 文件面板artifact文件夹下与合约同名的json文件中 // 里面"object"字段对应的数据就是Bytecode,挺长的,608060起始 // "object": "608060405260646000553480156100... const bytecodeNFT = contractJson.default.object; const factoryNFT = new ethers.ContractFactory(abiNFT, bytecodeNFT, wallet);利用contractFactory部署NFT合约
console.log("\n2. 利用contractFactory部署NFT合约") // 部署合约,填入constructor的参数 const contractNFT = await factoryNFT.deploy("WTF Merkle Tree", "WTF", root) console.log(`合约地址: ${contractNFT.target}`); console.log("等待合约部署上链") await contractNFT.waitForDeployment() console.log("合约已上链")
调用
mint()函数,利用merkle tree验证白名单,并给第0个地址铸造NFT。在mint成功后可以看到NFT余额变为1。console.log("\n3. 调用mint()函数,利用merkle tree验证白名单,给第一个地址铸造NFT") console.log(`NFT名称: ${await contractNFT.name()}`) console.log(`NFT代号: ${await contractNFT.symbol()}`) let tx = await contractNFT.mint(tokens[0], "0", proof) console.log("铸造中,等待交易上链") await tx.wait() console.log(`mint成功,地址${tokens[0]} 的NFT余额: ${await contractNFT.balanceOf(tokens[0])}\n`)
用于生产环境
在生产环境使用Merkle Tree验证白名单发行NFT主要有以下步骤:
- 确定白名单列表。
- 在后端生成白名单列表的
Merkle Tree。 - 部署
NFT合约,并将Merkle Tree的root保存在合约中。 - 用户铸造时,向后端请求地址对应的
proof。 - 用户调用
mint()函数进行铸造NFT。
完整代码
import { ethers } from "ethers";
import { MerkleTree } from "merkletreejs";
import * as contractJson from "./contract.json" assert {type: "json"};
// 1. 生成merkle tree
console.log("\n1. 生成merkle tree")
// 白名单地址
const tokens = [
"0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2",
"0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db",
"0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB"
];
// leaf, merkletree, proof
const leaf = tokens.map(x => ethers.keccak256(x))
const merkletree = new MerkleTree(leaf, ethers.keccak256, { sortPairs: true });
const proof = merkletree.getHexProof(leaf[0]);
const root = merkletree.getHexRoot()
console.log("Leaf:")
console.log(leaf)
console.log("\nMerkleTree:")
console.log(merkletree.toString())
console.log("\nProof:")
console.log(proof)
console.log("\nRoot:")
console.log(root)
// 2. 创建provider和wallet
const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l';
const provider = new ethers.JsonRpcProvider(ALCHEMY_GOERLI_URL);
// 利用私钥和provider创建wallet对象
const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b'
const wallet = new ethers.Wallet(privateKey, provider)
// 3. 创建合约工厂
// NFT的人类可读abi
const abiNFT = [
"constructor(string memory name, string memory symbol, bytes32 merkleroot)",
"function name() view returns (string)",
"function symbol() view returns (string)",
"function mint(address account, uint256 tokenId, bytes32[] calldata proof) external",
"function ownerOf(uint256) view returns (address)",
"function balanceOf(address) view returns (uint256)",
];
// 合约字节码,在remix中,你可以在两个地方找到Bytecode
// i. 部署面板的Bytecode按钮
// ii. 文件面板artifact文件夹下与合约同名的json文件中
// 里面"object"字段对应的数据就是Bytecode,挺长的,608060起始
// "object": "608060405260646000553480156100...
const bytecodeNFT = contractJson.default.object;
const factoryNFT = new ethers.ContractFactory(abiNFT, bytecodeNFT, wallet);
const main = async () => {
// 读取钱包内ETH余额
const balanceETH = await provider.getBalance(wallet)
// 如果钱包ETH足够
if(ethers.formatEther(balanceETH) > 0.002){
// 4. 利用contractFactory部署NFT合约
console.log("\n2. 利用contractFactory部署NFT合约")
// 部署合约,填入constructor的参数
const contractNFT = await factoryNFT.deploy("WTF Merkle Tree", "WTF", root)
console.log(`合约地址: ${contractNFT.target}`);
console.log("等待合约部署上链")
await contractNFT.waitForDeployment()
// 也可以用 contractNFT.deployTransaction.wait()
console.log("合约已上链")
// 5. 调用mint()函数,利用merkle tree验证白名单,给第0个地址铸造NFT
console.log("\n3. 调用mint()函数,利用merkle tree验证白名单,给第一个地址铸造NFT")
console.log(`NFT名称: ${await contractNFT.name()}`)
console.log(`NFT代号: ${await contractNFT.symbol()}`)
let tx = await contractNFT.mint(tokens[0], "0", proof)
console.log("铸造中,等待交易上链")
await tx.wait()
console.log(`mint成功,地址${tokens[0]} 的NFT余额: ${await contractNFT.balanceOf(tokens[0])}\n`)
}else{
// 如果ETH不足
console.log("ETH不足,去水龙头领一些Goerli ETH")
console.log("1. alchemy水龙头: https://goerlifaucet.com/")
console.log("2. paradigm水龙头: https://faucet.paradigm.xyz/")
}
}
main()
