ethers.js 自学笔记-2
(接上文)
什么是 WETH ?
WETH (Wrapped ETH) 是以太坊上的一种代币,是 ETH 的包装版本。主要作用是:
- 标准化:ETH 本身不是 ERC-20 代币,而 WETH 是标准的 ERC-20 代币
- 兼容性:让 ETH 可以在支持 ERC-20 的 DeFi 协议中使用
- 流动性:便于在 DEX 中交易和提供流动性
简单来说,WETH 就是"包装过的 ETH",1 WETH = 1 ETH。
标准的 WETH 地址
看这里
第十五, 十六课 批量转账 批量归结
十五课的脚本有点问题, 现在 goerli 测试网已经停运了, 所以文章里的脚本执行是不通过的, 会报错, 所以我把脚本修改了如下:
1import { ethers } from "ethers"; 2import { providerSepoliaAlchemy as provider, walletSepoliaAlchemy as wallet } from "./0_init.js"; 3 4// 1. 创建HD钱包 5console.log("\n1. 创建HD钱包") 6// 通过助记词生成HD钱包 7const mnemonic = `air organ twist rule prison symptom jazz cheap rather dizzy verb glare jeans orbit weapon universe require tired sing casino business anxiety seminar hunt` 8const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic) 9console.log("主HD钱包地址:", hdNode.address); 10 11// 2. 派生20个子钱包地址 12console.log("\n2. 通过HD钱包派生20个子钱包") 13const numWallet = 20 14// 派生路径:m / purpose' / coin_type' / account' / change / address_index 15// 使用标准BIP44路径派生子钱包 16let addresses = []; 17for (let i = 0; i < numWallet; i++) { 18 // 使用deriveChild方法,从当前HD节点派生子钱包 19 const childNode = hdNode.deriveChild(i); 20 addresses.push(childNode.address); 21 console.log(`子钱包 ${i}: ${childNode.address}`); 22} 23console.log("所有派生地址:", addresses); 24 25// 3. 创建provider和主钱包,用于发送ETH 26console.log("3. 创建provider和主钱包") 27console.log(provider) 28 29// 利用私钥和provider创建wallet对象 30console.log("主钱包地址:", wallet.address); 31 32// 4. 批量转账ETH 33const main = async () => { 34 try { 35 console.log("\n4. 检查主钱包ETH余额") 36 const balance = await provider.getBalance(wallet.address); 37 console.log(`主钱包ETH余额: ${ethers.formatEther(balance)} ETH`); 38 39 // 计算需要的总ETH(20个地址 * 0.0001 ETH + gas费用) 40 const transferAmount = ethers.parseEther("0.0001"); 41 const totalNeeded = transferAmount * BigInt(numWallet) + ethers.parseEther("0.001"); // 额外0.001作为gas 42 43 if (balance < totalNeeded) { 44 console.log("ETH余额不足,无法进行批量转账"); 45 console.log("请使用水龙头获取测试ETH:"); 46 console.log("1. Sepolia水龙头: https://sepoliafaucet.com/"); 47 console.log("2. Alchemy水龙头: https://sepoliafaucet.com/"); 48 return; 49 } 50 51 console.log("\n5. 开始批量转账ETH") 52 const amount = ethers.parseEther("0.0001"); 53 54 for (let i = 0; i < addresses.length; i++) { 55 console.log(`正在转账给地址 ${i + 1}/${numWallet}: ${addresses[i]}`); 56 57 const tx = await wallet.sendTransaction({ 58 to: addresses[i], 59 value: amount 60 }); 61 62 console.log(`交易已发送,哈希: ${tx.hash}`); 63 await tx.wait(); // 等待交易上链 64 console.log(`交易已确认!`); 65 } 66 67 console.log("\n✅ 批量转账完成!"); 68 console.log(`已成功给 ${numWallet} 个地址各转账 0.0001 ETH`); 69 70 } catch (error) { 71 console.error("转账过程中出现错误:", error.message); 72 if (error.message.includes("insufficient funds")) { 73 console.log("ETH余额不足,请检查钱包余额"); 74 } else if (error.message.includes("network")) { 75 console.log("网络连接问题,请检查API Key和网络设置"); 76 } 77 } 78} 79 80main()
执行后控制台打印如下:
1(base) ➜ etherjs-learn git:(main) ✗ node 15_multiTransfer.js 2[[email protected]] injecting env (7) from .env – [tip] encrypt with dotenvx: https://dotenvx.com 3 41. 创建HD钱包 5主HD钱包地址: 0x83E5B09c54C4EB904B9bC842Acab9218c2297d6d 6 72. 通过HD钱包派生20个子钱包 8子钱包 0: 0xe9A893f7646e0f3491a907771Bb1fB8a21718E45 9子钱包 1: 0x01119c09C0678Bfe901054594504af61A5fB029b 10子钱包 2: 0x9C4E2C9dc83d60E046A35Ba0bF6A1CA8859E1369 11子钱包 3: 0xff8A2C9e49059e31F9A9213e03EA7338F35577Cb 12子钱包 4: 0x8f0945F6ca61B95C19D135cCc6E5Dc19570a1459 13子钱包 5: 0xeC89A9BC4f07e47c3C520a3Cd3c0cA09f7CE625F 14子钱包 6: 0x4E7acd43514202328337b3Be3398160908276528 15子钱包 7: 0x51338ee6AEE836C7aA42eb1b26A767836f5cb43d 16子钱包 8: 0x90648f4c96769dC0584a3EBe62Cd952c915aF466 17子钱包 9: 0x9cE13df29Fd5b539e6512c06273514E26F910eC7 18子钱包 10: 0x47E516Fd1b6bf111d9DAdd807fF1fF0255FC9172 19子钱包 11: 0xb65870feF5E0F4A22e55F6186476f5023B7ACd88 20子钱包 12: 0x801C4A0630c4960C4f7EdB756e9c98d28E328d31 21子钱包 13: 0x71B95C72D468AC50bB3CB14958eeF253d9104152 22子钱包 14: 0xcB6B4C9c6fa841d5A19C77ABEE3a1CE07c06d45A 23子钱包 15: 0xFA2E002399392D27B56BBeA4bB969D8dB0968892 24子钱包 16: 0xD937d0D1C396c0f5BeA01B34AAfA3008f150F320 25子钱包 17: 0x65d05786DA5556cb4F3F14547D90Ce04A607426E 26子钱包 18: 0x0A3742AEA34eb8388725F90e8eeB4040D62C6c3F 27子钱包 19: 0xa6B97C11d77dEc3c3DF2135B704163111f6522F9 28所有派生地址: [ 29 '0xe9A893f7646e0f3491a907771Bb1fB8a21718E45', 30 '0x01119c09C0678Bfe901054594504af61A5fB029b', 31 '0x9C4E2C9dc83d60E046A35Ba0bF6A1CA8859E1369', 32 '0xff8A2C9e49059e31F9A9213e03EA7338F35577Cb', 33 '0x8f0945F6ca61B95C19D135cCc6E5Dc19570a1459', 34 '0xeC89A9BC4f07e47c3C520a3Cd3c0cA09f7CE625F', 35 '0x4E7acd43514202328337b3Be3398160908276528', 36 '0x51338ee6AEE836C7aA42eb1b26A767836f5cb43d', 37 '0x90648f4c96769dC0584a3EBe62Cd952c915aF466', 38 '0x9cE13df29Fd5b539e6512c06273514E26F910eC7', 39 '0x47E516Fd1b6bf111d9DAdd807fF1fF0255FC9172', 40 '0xb65870feF5E0F4A22e55F6186476f5023B7ACd88', 41 '0x801C4A0630c4960C4f7EdB756e9c98d28E328d31', 42 '0x71B95C72D468AC50bB3CB14958eeF253d9104152', 43 '0xcB6B4C9c6fa841d5A19C77ABEE3a1CE07c06d45A', 44 '0xFA2E002399392D27B56BBeA4bB969D8dB0968892', 45 '0xD937d0D1C396c0f5BeA01B34AAfA3008f150F320', 46 '0x65d05786DA5556cb4F3F14547D90Ce04A607426E', 47 '0x0A3742AEA34eb8388725F90e8eeB4040D62C6c3F', 48 '0xa6B97C11d77dEc3c3DF2135B704163111f6522F9' 49] 503. 创建provider和主钱包 51JsonRpcProvider {} 52主钱包地址: 0xbd61c00E4B7C2a77Cd660304ddd852379F705ED6 53 544. 检查主钱包ETH余额 55主钱包ETH余额: 0.278647239098476179 ETH 56 575. 开始批量转账ETH 58正在转账给地址 1/20: 0xe9A893f7646e0f3491a907771Bb1fB8a21718E45 59交易已发送,哈希: 0xd7a988109c7b30bae7804f3ae8dd24f60217c177bab649428b265e80c6e04e3c 60交易已确认! 61正在转账给地址 2/20: 0x01119c09C0678Bfe901054594504af61A5fB029b 62交易已发送,哈希: 0xeb9ed5278f923bfcd2622555955531409eebf402a11dc63a617ed2f7fa938500 63交易已确认! 64正在转账给地址 3/20: 0x9C4E2C9dc83d60E046A35Ba0bF6A1CA8859E1369 65交易已发送,哈希: 0x098ccceb6f2a6066c20e9c7b522fea5b2676251ad8da83a6e0e780c36574830a 66交易已确认! 67正在转账给地址 4/20: 0xff8A2C9e49059e31F9A9213e03EA7338F35577Cb 68交易已发送,哈希: 0x3caef30935d62f26e4b8454430c7382838a9ff3a6a028f9c2ee686c66fbee57a 69交易已确认! 70正在转账给地址 5/20: 0x8f0945F6ca61B95C19D135cCc6E5Dc19570a1459 71交易已发送,哈希: 0x7646bbd71e1b9d836213ebfe14499f8002c5f69619feeb725f084fbff73a214b 72交易已确认! 73正在转账给地址 6/20: 0xeC89A9BC4f07e47c3C520a3Cd3c0cA09f7CE625F 74交易已发送,哈希: 0x4c8b423f2ffa77b245b18e82225f2cac2f26349470ffaf698abe56e82182d7c7 75交易已确认! 76正在转账给地址 7/20: 0x4E7acd43514202328337b3Be3398160908276528 77交易已发送,哈希: 0x06b00c8e62a44027651c0575f2e4950b8023d930547fc3fce150037d94640c5e 78交易已确认! 79正在转账给地址 8/20: 0x51338ee6AEE836C7aA42eb1b26A767836f5cb43d 80交易已发送,哈希: 0xb0f4f37c192523ee0bae0ba2b896ad22d2d9007795a0c3cfac1cbdbea903f4a8 81交易已确认! 82正在转账给地址 9/20: 0x90648f4c96769dC0584a3EBe62Cd952c915aF466 83交易已发送,哈希: 0xfffd6a87323a3d6f4002ddd96cfced7f6743e7ad83e04b2abcf1dfb72e00d36d 84交易已确认! 85正在转账给地址 10/20: 0x9cE13df29Fd5b539e6512c06273514E26F910eC7 86交易已发送,哈希: 0xd3565ddfa75cf9e8124121d710de7163868c8658b59443ff103fd15c34ef3b2c 87交易已确认! 88正在转账给地址 11/20: 0x47E516Fd1b6bf111d9DAdd807fF1fF0255FC9172 89交易已发送,哈希: 0x8a106080adb8c68f9b15c1cb7dd3a7e115f57be70c27f00af96717ddc3f0b189 90交易已确认! 91正在转账给地址 12/20: 0xb65870feF5E0F4A22e55F6186476f5023B7ACd88 92交易已发送,哈希: 0x0da78401ea4c33c41abcfa5e48c72e0e6abbde66a699ab4fc28a7633566a0869 93交易已确认! 94正在转账给地址 13/20: 0x801C4A0630c4960C4f7EdB756e9c98d28E328d31 95交易已发送,哈希: 0xec1eb4a883c088b74bcd3a060ea9bf18cfd0db72138fcb12046cbeb0fa2ba588 96交易已确认! 97正在转账给地址 14/20: 0x71B95C72D468AC50bB3CB14958eeF253d9104152 98交易已发送,哈希: 0x52b889f1bb930d8a78aa8f18d34a5f76b459703a3290aec8d80dff9127928981 99交易已确认! 100正在转账给地址 15/20: 0xcB6B4C9c6fa841d5A19C77ABEE3a1CE07c06d45A 101交易已发送,哈希: 0x871f02be2896e9e5b12b3acc35353ba5d8a6ef87911349abe6c450a37f3eb679 102交易已确认! 103正在转账给地址 16/20: 0xFA2E002399392D27B56BBeA4bB969D8dB0968892 104交易已发送,哈希: 0x5277ee66768d4f8b008c63838dec7b9f485314be28da499c3095e35b314b3f5a 105交易已确认! 106正在转账给地址 17/20: 0xD937d0D1C396c0f5BeA01B34AAfA3008f150F320 107交易已发送,哈希: 0x67e714eb5962a4f3dcfc0a216de1870417bf834588b88e3764780686af7c01fd 108交易已确认! 109正在转账给地址 18/20: 0x65d05786DA5556cb4F3F14547D90Ce04A607426E 110交易已发送,哈希: 0x3c8d24baa3a84f0a28edcb1838447c5542393eb67d67c2bf28edf4a7bb77256d 111交易已确认! 112正在转账给地址 19/20: 0x0A3742AEA34eb8388725F90e8eeB4040D62C6c3F 113交易已发送,哈希: 0xcc833f571ccaf2f50aeb74812f72f52756660dfa77c9e6a0167751f328ad1644 114交易已确认! 115正在转账给地址 20/20: 0xa6B97C11d77dEc3c3DF2135B704163111f6522F9 116交易已发送,哈希: 0x28685753bf0833a95356cff571a714824a181680fdc70ad054d31292b569f249 117交易已确认! 118 119✅ 批量转账完成! 120已成功给 20 个地址各转账 0.0001 ETH
以下是自动归结的脚本:
1import { ethers } from "ethers"; 2import { providerSepoliaAlchemy as provider, walletSepoliaAlchemy as wallet } from "./0_init.js"; 3 4// 1. 创建HD钱包 5console.log("\n1. 创建HD钱包") 6// 通过助记词生成HD钱包 7const mnemonic = `air organ twist rule prison symptom jazz cheap rather dizzy verb glare jeans orbit weapon universe require tired sing casino business anxiety seminar hunt` 8const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic) 9console.log("主HD钱包地址:", hdNode.address); 10 11// 2. 派生20个子钱包地址 12console.log("\n2. 通过HD钱包派生20个子钱包") 13const numWallet = 20 14// 派生路径:m / purpose' / coin_type' / account' / change / address_index 15// 使用标准BIP44路径派生子钱包 16let addresses = []; 17let childWallets = []; // 存储子钱包对象 18for (let i = 0; i < numWallet; i++) { 19 // 使用deriveChild方法,从当前HD节点派生子钱包 20 const childNode = hdNode.deriveChild(i); 21 addresses.push(childNode.address); 22 // 创建子钱包对象,用于签名交易 23 const childWallet = new ethers.Wallet(childNode.privateKey, provider); 24 childWallets.push(childWallet); 25 console.log(`子钱包 ${i}: ${childNode.address}`); 26} 27console.log("所有派生地址:", addresses); 28 29// 3. 创建provider和主钱包,用于接收ETH 30console.log("\n3. 创建provider和主钱包") 31console.log("主钱包地址:", wallet.address); 32 33// 4. 批量归集ETH 34const main = async () => { 35 try { 36 console.log("\n4. 检查各子钱包ETH余额") 37 let totalBalance = ethers.parseEther("0"); 38 let walletsWithBalance = []; 39 40 // 检查每个子钱包的余额 41 for (let i = 0; i < addresses.length; i++) { 42 const balance = await provider.getBalance(addresses[i]); 43 console.log(`子钱包 ${i} 余额: ${ethers.formatEther(balance)} ETH`); 44 45 if (balance > ethers.parseEther("0.0001")) { // 只归集有足够余额的钱包 46 walletsWithBalance.push({ 47 index: i, 48 address: addresses[i], 49 wallet: childWallets[i], 50 balance: balance 51 }); 52 totalBalance += balance; 53 } 54 } 55 56 console.log(`\n总可归集余额: ${ethers.formatEther(totalBalance)} ETH`); 57 console.log(`有余额的钱包数量: ${walletsWithBalance.length}`); 58 59 if (walletsWithBalance.length === 0) { 60 console.log("没有找到有足够余额的钱包进行归集"); 61 return; 62 } 63 64 console.log("\n5. 开始批量归集ETH") 65 66 for (let i = 0; i < walletsWithBalance.length; i++) { 67 const walletInfo = walletsWithBalance[i]; 68 const childWallet = walletInfo.wallet; 69 const balance = walletInfo.balance; 70 71 // 计算转账金额(保留一些ETH作为gas费用) 72 const gasEstimate = ethers.parseEther("0.001"); // 预估gas费用 73 const transferAmount = balance - gasEstimate; 74 75 if (transferAmount <= 0) { 76 console.log(`子钱包 ${walletInfo.index} 余额不足支付gas费用,跳过`); 77 continue; 78 } 79 80 console.log(`正在归集子钱包 ${walletInfo.index}: ${walletInfo.address}`); 81 console.log(`转账金额: ${ethers.formatEther(transferAmount)} ETH`); 82 83 try { 84 const tx = await childWallet.sendTransaction({ 85 to: wallet.address, 86 value: transferAmount 87 }); 88 89 console.log(`交易已发送,哈希: ${tx.hash}`); 90 await tx.wait(); // 等待交易上链 91 console.log(`交易已确认!`); 92 93 } catch (error) { 94 console.error(`子钱包 ${walletInfo.index} 归集失败:`, error.message); 95 } 96 } 97 98 // 检查归集后的主钱包余额 99 const finalBalance = await provider.getBalance(wallet.address); 100 console.log(`\n✅ 批量归集完成!`); 101 console.log(`主钱包最终余额: ${ethers.formatEther(finalBalance)} ETH`); 102 103 } catch (error) { 104 console.error("归集过程中出现错误:", error.message); 105 if (error.message.includes("insufficient funds")) { 106 console.log("某些钱包余额不足支付gas费用"); 107 } else if (error.message.includes("network")) { 108 console.log("网络连接问题,请检查API Key和网络设置"); 109 } 110 } 111} 112 113main()
默克尔树
- 文中介绍这个概念我看不太懂, 后来问了 AI 才知道他的作用:
Merkle Tree(默克尔树) 是一种树形数据结构,用于高效地验证大量数据的完整性。
🌳 核心特点:
- 数据完整性验证:快速验证某个数据是否属于一个大数据集
- 零知识证明:证明数据存在而不暴露完整数据集
- 高效性:验证复杂度为 O(log n),而不是 O(n)
这个图的解读
- 由L1 -得出-> Hash 0-0
- Hash 0-0 和 Hash0-1 做一次 hash 得到父结点 Hash 0
- Hash 0 和 Hash 1 做一次 hash 得到根节点 Top Hash
- 由此可知, 只需要知道 Hash 0-1 和 Hash 1 的值, 便可知道 Top Hash的值, 可以判断 L1 是不是属于 Top Hash 这棵哈希树的
默克尔树验证及代码
这节课我复制了WTF 的代码但是老报错, 所以我重新写了一组代码: 运行该文件需要注意的点 :
- 下面的
17_contract.json
文件可以参考这里 - 由于 WTF 的编译代码不能用, 所以我叫 AI 生成了一个 NFTWhitelistContract.sol 文件, 然后AI又很好心的检测到里面有
openzeppelin
外部库, 所以直接又帮我生成一份不依赖外部库的 SimpleERC721Merkle.sol 文件 - 可以将 SimpleERC721Merkle.sol 文件贴到 Remix 上进行编译
- 复制 Remix上编译后的这个路径
artifacts/SimpleERC721Merkle.json
的文件, 粘贴生成17_contract.json
文件
做完以上步骤, 可以运行以下代码:
1import { ethers } from "ethers"; 2import { MerkleTree } from "merkletreejs"; 3import { providerSepoliaInfura as provider, walletSepoliaInfura as wallet } from "./0_init.js"; 4import contractJson from './17_contract.json' assert { type: 'json' }; 5 6console.log("=== 默克尔树NFT铸造系统 ==="); 7 8// 1. 生成默克尔树 9console.log("1. 生成默克尔树"); 10// 白名单地址 11const whitelistAddresses = [ 12 "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", 13 "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", 14 "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", 15 "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB" 16]; 17 18// 生成叶子节点(地址的哈希) 19const leaves = whitelistAddresses.map(x => ethers.keccak256(x)); 20const merkletree = new MerkleTree(leaves, ethers.keccak256, { sortPairs: true }); 21const root = merkletree.getHexRoot(); 22 23console.log("白名单地址:"); 24console.log(whitelistAddresses); 25console.log("默克尔根:", root); 26 27// 2. 验证地址是否在白名单中 28function verifyWhitelist(address) { 29 const leaf = ethers.keccak256(address); 30 const proof = merkletree.getHexProof(leaf); 31 const isValid = merkletree.verify(proof, leaf, root); 32 33 console.log(`验证地址: ${address}`); 34 console.log(`叶子节点: ${leaf}`); 35 console.log(`默克尔证明: ${JSON.stringify(proof)}`); 36 console.log(`是否在白名单中: ${isValid}`); 37 38 return { isValid, proof }; 39} 40 41// 3. 测试验证功能 42console.log("2. 测试白名单验证"); 43const testAddresses = [ 44 "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", // 在白名单中 45 "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", // 在白名单中 46 "0x1234567890123456789012345678901234567890" // 不在白名单中 47]; 48 49testAddresses.forEach(address => { 50 verifyWhitelist(address); 51}); 52 53// 4. 从JSON文件获取合约信息 54const bytecodeNFT = contractJson.data.bytecode.object; 55const abiNFT = contractJson.abi; 56 57console.log("3. 合约信息"); 58console.log("字节码长度:", bytecodeNFT.length); 59console.log("字节码是否以0x开头:", bytecodeNFT.startsWith("0x")); 60console.log("ABI函数数量:", abiNFT.length); 61 62// 5. 主要功能 63const main = async () => { 64 try { 65 console.log("4. 检查钱包余额"); 66 const balanceETH = await provider.getBalance(wallet); 67 console.log("钱包余额:", ethers.formatEther(balanceETH), "ETH"); 68 69 if (bytecodeNFT.length === 0) { 70 console.log("⚠️ 字节码为空,请检查JSON文件"); 71 return; 72 } 73 74 console.log("5. 部署NFT合约"); 75 const factoryNFT = new ethers.ContractFactory(abiNFT, bytecodeNFT, wallet); 76 77 // 部署参数:合约名称、符号、默克尔根 78 const contractName = "MerkleNFT"; 79 const contractSymbol = "MNFT"; 80 81 console.log("部署参数:"); 82 console.log("- 名称:", contractName); 83 console.log("- 符号:", contractSymbol); 84 console.log("- 默克尔根:", root); 85 86 const nftContract = await factoryNFT.deploy(contractName, contractSymbol, root); 87 await nftContract.waitForDeployment(); 88 89 const contractAddress = await nftContract.getAddress(); 90 console.log("✅ 合约部署成功!地址:", contractAddress); 91 92 console.log("6. 铸造NFT"); 93 const tokenId = 1; 94 const mintToAddress = whitelistAddresses[0]; // 使用白名单中的第一个地址 95 96 // 获取该地址的默克尔证明 97 const { isValid, proof } = verifyWhitelist(mintToAddress); 98 99 if (!isValid) { 100 console.log("❌ 地址不在白名单中,无法铸造"); 101 return; 102 } 103 104 console.log("铸造参数:"); 105 console.log("- 接收地址:", mintToAddress); 106 console.log("- Token ID:", tokenId); 107 console.log("- 默克尔证明:", proof); 108 109 const mintTx = await nftContract.mint(mintToAddress, tokenId, proof); 110 await mintTx.wait(); 111 112 console.log("✅ NFT铸造成功!"); 113 114 // 验证铸造结果 115 const owner = await nftContract.ownerOf(tokenId); 116 console.log("Token", tokenId, "的所有者:", owner); 117 118 const balance = await nftContract.balanceOf(mintToAddress); 119 console.log("地址", mintToAddress, "的NFT余额:", balance.toString()); 120 121 // 7. 测试非白名单地址铸造(应该失败) 122 console.log("7. 测试非白名单地址铸造"); 123 const nonWhitelistAddress = "0x1234567890123456789012345678901234567890"; 124 const { proof: invalidProof } = verifyWhitelist(nonWhitelistAddress); 125 126 try { 127 const invalidMintTx = await nftContract.mint(nonWhitelistAddress, 2, invalidProof); 128 await invalidMintTx.wait(); 129 console.log("❌ 非白名单地址铸造成功(不应该发生)"); 130 } catch (error) { 131 console.log("✅ 非白名单地址铸造失败(符合预期)"); 132 console.log("错误信息:", error.message); 133 } 134 135 } catch (error) { 136 console.error("❌ 错误:", error); 137 } 138} 139 140// 8. 离线验证功能(不依赖网络) 141function offlineVerification() { 142 console.log("8. 离线验证功能"); 143 144 // 验证所有白名单地址 145 whitelistAddresses.forEach((address, index) => { 146 const { isValid, proof } = verifyWhitelist(address); 147 console.log(`地址 ${index + 1}: ${isValid ? '✅' : '❌'} ${address}`); 148 }); 149 150 // 验证非白名单地址 151 const nonWhitelistAddress = "0x1234567890123456789012345678901234567890"; 152 const { isValid } = verifyWhitelist(nonWhitelistAddress); 153 console.log(`非白名单地址: ${isValid ? '❌' : '✅'} ${nonWhitelistAddress}`); 154} 155 156// 运行离线验证 157offlineVerification(); 158 159// 运行主程序 160main();
在 zsh上跑了结果如下:
1(base) ➜ etherjs-learn git:(main) ✗ node 17_MerkleTree.js 2(node:33837) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time 3(Use `node --trace-warnings ...` to show where the warning was created) 4[[email protected]] injecting env (7) from .env – [tip] encrypt with dotenvx: https://dotenvx.com 5=== 默克尔树NFT铸造系统 === 6 71. 生成默克尔树 8白名单地址: 9[ 10 '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 11 '0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2', 12 '0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db', 13 '0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB' 14] 15默克尔根: 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 16 172. 测试白名单验证 18 19验证地址: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 20叶子节点: 0x5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229 21默克尔证明: ["0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb","0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"] 22是否在白名单中: true 23 24验证地址: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 25叶子节点: 0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb 26默克尔证明: ["0x5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229","0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"] 27是否在白名单中: true 28 29验证地址: 0x1234567890123456789012345678901234567890 30叶子节点: 0xb6979620706f8c652cfb6bf6e923f5156eadd5abaf4022a0b19d52ada089475f 31默克尔证明: [] 32是否在白名单中: false 33 343. 合约信息 35字节码长度: 19606 36字节码是否以0x开头: false 37file:///Users/ziyouzhiyi/Documents/code/web3/etherjs-learn/17_MerkleTree.js:60 38console.log("ABI函数数量:", abiNFT.length); 39 ^ 40 41TypeError: Cannot read properties of undefined (reading 'length') 42 at file:///Users/ziyouzhiyi/Documents/code/web3/etherjs-learn/17_MerkleTree.js:60:32 43 at ModuleJob.run (node:internal/modules/esm/module_job:222:25) 44 at async ModuleLoader.import (node:internal/modules/esm/loader:316:24) 45 at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:123:5) 46 47Node.js v20.13.1 48(base) ➜ etherjs-learn git:(main) ✗ node 17_MerkleTree.js 49(node:34015) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time 50(Use `node --trace-warnings ...` to show where the warning was created) 51[[email protected]] injecting env (7) from .env – [tip] encrypt with dotenvx: https://dotenvx.com 52=== 默克尔树NFT铸造系统 === 53 541. 生成默克尔树 55白名单地址: 56[ 57 '0x5B38Da6a701c568545dCfcB03FcB875f56beddC4', 58 '0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2', 59 '0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db', 60 '0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB' 61] 62默克尔根: 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 63 642. 测试白名单验证 65 66验证地址: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 67叶子节点: 0x5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229 68默克尔证明: ["0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb","0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"] 69是否在白名单中: true 70 71验证地址: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 72叶子节点: 0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb 73默克尔证明: ["0x5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229","0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"] 74是否在白名单中: true 75 76验证地址: 0x1234567890123456789012345678901234567890 77叶子节点: 0xb6979620706f8c652cfb6bf6e923f5156eadd5abaf4022a0b19d52ada089475f 78默克尔证明: [] 79是否在白名单中: false 80 813. 合约信息 82字节码长度: 19606 83字节码是否以0x开头: false 84ABI函数数量: 21 85 868. 离线验证功能 87 88验证地址: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 89叶子节点: 0x5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229 90默克尔证明: ["0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb","0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"] 91是否在白名单中: true 92地址 1: ✅ 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 93 94验证地址: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 95叶子节点: 0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb 96默克尔证明: ["0x5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229","0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"] 97是否在白名单中: true 98地址 2: ✅ 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 99 100验证地址: 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db 101叶子节点: 0x04a10bfd00977f54cc3450c9b25c9b3a502a089eba0097ba35fc33c4ea5fcb54 102默克尔证明: ["0xdfbe3e504ac4e35541bebad4d0e7574668e16fefa26cd4172f93e18b59ce9486","0x9d997719c0a5b5f6db9b8ac69a988be57cf324cb9fffd51dc2c37544bb520d65"] 103是否在白名单中: true 104地址 3: ✅ 0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db 105 106验证地址: 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB 107叶子节点: 0xdfbe3e504ac4e35541bebad4d0e7574668e16fefa26cd4172f93e18b59ce9486 108默克尔证明: ["0x04a10bfd00977f54cc3450c9b25c9b3a502a089eba0097ba35fc33c4ea5fcb54","0x9d997719c0a5b5f6db9b8ac69a988be57cf324cb9fffd51dc2c37544bb520d65"] 109是否在白名单中: true 110地址 4: ✅ 0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB 111 112验证地址: 0x1234567890123456789012345678901234567890 113叶子节点: 0xb6979620706f8c652cfb6bf6e923f5156eadd5abaf4022a0b19d52ada089475f 114默克尔证明: [] 115是否在白名单中: false 116非白名单地址: ✅ 0x1234567890123456789012345678901234567890 117 1184. 检查钱包余额 119钱包余额: 0.273626490190575529 ETH 120 1215. 部署NFT合约 122部署参数: 123- 名称: MerkleNFT 124- 符号: MNFT 125- 默克尔根: 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 126✅ 合约部署成功!地址: 0xeb18bcCA8872587847F1145Cb15D0cF3Ff9A6BbA 127 1286. 铸造NFT 129 130验证地址: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 131叶子节点: 0x5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229 132默克尔证明: ["0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb","0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"] 133是否在白名单中: true 134铸造参数: 135- 接收地址: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 136- Token ID: 1 137- 默克尔证明: [ 138 '0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb', 139 '0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c' 140] 141✅ NFT铸造成功! 142Token 1 的所有者: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 143地址 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 的NFT余额: 1 144 1457. 测试非白名单地址铸造 146 147验证地址: 0x1234567890123456789012345678901234567890 148叶子节点: 0xb6979620706f8c652cfb6bf6e923f5156eadd5abaf4022a0b19d52ada089475f 149默克尔证明: [] 150是否在白名单中: false 151✅ 非白名单地址铸造失败(符合预期) 152错误信息: execution reverted: "Invalid merkle proof" (action="estimateGas", data="0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000014496e76616c6964206d65726b6c652070726f6f66000000000000000000000000", reason="Invalid merkle proof", transaction={ "data": "0x641ce1400000000000000000000000001234567890123456789012345678901234567890000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", "from": "0xbd61c00E4B7C2a77Cd660304ddd852379F705ED6", "to": "0xeb18bcCA8872587847F1145Cb15D0cF3Ff9A6BbA" }, invocation=null, revert={ "args": [ "Invalid merkle proof" ], "name": "Error", "signature": "Error(string)" }, code=CALL_EXCEPTION, version=6.15.0)
可以看到, 函数 function verifyWhitelist(address)
中的 const isValid = merkletree.verify(proof, leaf, root);
这一步是关键, 这里传入的 3个参数分别为:
- 该地址的验证证明, .getHexProof(ethers.keccak256(addr)) , 该地址必须经keccak256编码后, 再用 merkletree 的getHexProof 16进制的编码
- 叶子节点经过 keccak256 编码的hash值
- 根节点哈希值, 由 .getHexRoot() 生成
数字签名
数字签名对比上面的默克尔树验证, 节省了 Gas 值, 因为纯粹是后端进行验证. 不需要链上的操作. WTF 代码中, 真正数字签名的代码, 只有下面这几行涉及到:
1 2// 钱包地址: 3const account = "0xbd61c00E4B7C2a77Cd660304ddd852379F705ED6" 4const tokenId = "0" 5// 生成签名 6const msgHash = ethers.solidityPackedKeccak256( 7 ['address', 'uint256'], 8 [account, tokenId]) 9 10// 其他代码... 11 12// mint 时做身份检查: 13const tx = await contractNFT.mint(account, tokenId, signature, { value: 0 })
也把我写的 solidity 源码贴出来: 特别留意其中的 mint 函数校验部分
1// SPDX-License-Identifier: MIT 2pragma solidity ^0.8.20; 3 4import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5import "@openzeppelin/contracts/access/Ownable.sol"; 6import "@openzeppelin/contracts/utils/Counters.sol"; 7 8contract SignatureNFT is ERC721, Ownable { 9 using Counters for Counters.Counter; 10 Counters.Counter private _tokenIds; 11 12 uint256 public maxSupply = 1000; 13 uint256 public mintPrice = 0 ether; 14 15 mapping(address => bool) public hasMinted; 16 17 event TokenMinted(address indexed to, uint256 indexed tokenId); 18 19 constructor() ERC721("SignatureNFT", "SNFT") Ownable(msg.sender) {} 20 21 // 铸造函数 - 带签名验证 22 function mint(address account, uint256 tokenId, bytes calldata signature) public payable { 23 require(_tokenIds.current() < maxSupply, "Max supply reached"); 24 require(msg.value >= mintPrice, "Insufficient payment"); 25 require(!hasMinted[account], "Already minted"); 26 require(tokenId == 0, "Only tokenId 0 is allowed"); 27 require(account == msg.sender, "Can only mint to self"); 28 29 // [!!!重点!!!] 验证签名 30 bytes32 messageHash = keccak256(abi.encodePacked(account, tokenId)); 31 bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)); 32 33 (uint8 v, bytes32 r, bytes32 s) = splitSignature(signature); 34 address signer = ecrecover(ethSignedMessageHash, v, r, s); 35 36 require(signer == owner(), "Invalid signature"); 37 38 hasMinted[account] = true; 39 _tokenIds.increment(); 40 uint256 newTokenId = _tokenIds.current(); 41 42 _safeMint(msg.sender, newTokenId); 43 emit TokenMinted(msg.sender, newTokenId); 44 } 45 46 // 分割签名 47 function splitSignature(bytes memory sig) internal pure returns (uint8 v, bytes32 r, bytes32 s) { 48 require(sig.length == 65, "Invalid signature length"); 49 50 assembly { 51 r := mload(add(sig, 32)) 52 s := mload(add(sig, 64)) 53 v := byte(0, mload(add(sig, 96))) 54 } 55 } 56 57 // 批量铸造(仅所有者) 58 function mintBatch(address to, uint256 quantity) public onlyOwner { 59 require(_tokenIds.current() + quantity <= maxSupply, "Exceeds max supply"); 60 61 for (uint256 i = 0; i < quantity; i++) { 62 _tokenIds.increment(); 63 uint256 newTokenId = _tokenIds.current(); 64 _safeMint(to, newTokenId); 65 emit TokenMinted(to, newTokenId); 66 } 67 } 68 69 // 设置铸造价格 70 function setMintPrice(uint256 _price) public onlyOwner { 71 mintPrice = _price; 72 } 73 74 // 设置最大供应量 75 function setMaxSupply(uint256 _maxSupply) public onlyOwner { 76 require(_maxSupply >= _tokenIds.current(), "Cannot reduce below current supply"); 77 maxSupply = _maxSupply; 78 } 79 80 // 提取合约余额 81 function withdraw() public onlyOwner { 82 uint256 balance = address(this).balance; 83 payable(owner()).transfer(balance); 84 } 85 86 // 获取当前铸造的 token 数量 87 function totalSupply() public view returns (uint256) { 88 return _tokenIds.current(); 89 } 90 91 // 获取剩余可铸造数量 92 function remainingSupply() public view returns (uint256) { 93 return maxSupply - _tokenIds.current(); 94 } 95}
而原来的代码我也弃用了, 写了这份新的代码, 因为原来的代码是从部署开始的, 我改造了一下, 部署在 Remix 里即可, 而我们的只关系铸造验证签名的逻辑, 不部署. 在终端运行后如下:
1(base) ➜ etherjs-learn git:(main) ✗ node 18_signature.js 2[[email protected]] injecting env (7) from .env – [tip] encrypt with dotenvx: https://dotenvx.com 3使用合约地址: 0x05a93f8573631eae3f5aa045e8ea027547d57279 4合约所有者: 0xbd61c00E4B7C2a77Cd660304ddd852379F705ED6 5当前钱包地址: 0xbd61c00E4B7C2a77Cd660304ddd852379F705ED6 6铸造地址: 0xbd61c00E4B7C2a77Cd660304ddd852379F705ED6 7Token ID: 0 8消息哈希: 0x38ab276f75354c8585b4547873729bbc4b95984ad74f4eb03942be9a0a8e8777 9签名: 0xf6ff63b010d1a4529bece50317dd70fc5c2eca8e5c4d2c3a543b931e3ec5200b30dc2d1d837cf3a72c1ef3edabb7f86f0d23015020d0a73e651b4c0bd2dee6081c 10恢复的签名者地址: 0xbd61c00E4B7C2a77Cd660304ddd852379F705ED6 11签名者是否匹配: true 12钱包ETH余额: 0.265221514033323046 ETH 13是否已经铸造过: false 14 15开始铸造NFT... 16NFT名称: SignatureNFT 17NFT代号: SNFT 18铸造中,等待交易上链... 19交易哈希: 0x84fb8e3cde000995e365ae12743944b2ddf60b65c81b67aa35e6a1b843b10dfe 20✅ 铸造成功! 21地址 0xbd61c00E4B7C2a77Cd660304ddd852379F705ED6 的NFT余额: 1 22合约总供应量: 1
附上如何获得合约地址的截图:
当然, 也可以在 ether scan 上粘贴自己的钱包代码查找:
监听Mempool
这课的代码主要是这个:
1 provider.on("pending", throttle(async (txHash) => { 2 if (txHash && j <= 100) { 3 // 获取tx详情 4 let tx = await provider.getTransaction(txHash); 5 console.log(`\n[${(new Date).toLocaleTimeString()}] 监听Pending交易 ${j}: ${txHash} \r`); 6 console.log(tx); 7 j++ 8 } 9 }, 1000));
利用provider 进行监听交易, 获取Gas 值高的交易进行打包, WTF 课程里的代码监听的是主网, 现在连接不了, 所以我只能改用 wss 的 sepolia 测试网
他会自动挑选出 gasPrice
值较高的交易: