走进科学 | 6. 实现代码自动 mint

在前面的章节,我们通过在 etherscan 直接调用合约的方式完成了 mint,或许可以更进一步,我们也可以自己尝试写一段 mint 的代码,本文将介绍如何实现一段 mint 的代码,整个过程需要一些编程基础和知识储备,我们会讲解重要的环节,尽量让大家都能参与其中。为了降低难度,我们把代码放到 Chrome 浏览器的 console(控制台)来执行,这样大家就不用额外搭建代码的运行环境,如果对本文有任何疑问,欢迎大家留言咨询,我们会竭力帮助大家。

好的,在开始编写代码之前,我们需要先做一些准备工作:

1、了解 Infura 和 Ethers 库,学会调用 Infura 的接口;

2、学习如何在 Chrome 浏览器的控制台调用 Infura 接口;

3、代码体验环节需要在代码中使用私钥发起交易,准备一个新钱包地址用于测试体验,而不要用自己经常使用的主钱包地址

Infura

大家都熟知 metamask,天天都在使用小狐狸钱包,但是很多人不知道 Infura,其实 metamask 和 Infura 都是 ConsenSys 旗下的产品,metamask 就是使用的 Infura 提供的接口,Infura 提供了以太坊节点的接口服务;再通俗一点讲,Infura 就是一个可以让你的程序快速接入以太坊的数据源,不需要本地运行以太坊节点。这样就能让程序员更专注于开发,而不是把时间和金钱浪费到以太坊节点搭建和维护上。Infura 的服务有免费和收费的版本,免费版本会限制每天请求的数量,对于学习和一般应用,我们选择使用免费版本就足够了。接下来,我们可以去申请一个 Infura 的账号,整个过程非常简单。

第一步,访问 Infura 网站,https://infura.io/,点击 “SIGN UP” 填入邮箱和密码注册一个新的账号,注册完成之后会在邮箱收到确认邮件。

第二步,注册成功之后,用注册信息登录 Infura,进去之后在面板中点击 “CREATE NEW PROJECT” 创建一个新的项目,PRODUCT 选择 Ethereum,PROJECT NAME 可以任意设置一个名称

创建成功之后,在 ENDPOINTS 这里可以选择支持的网络,本文测试选择使用 Rinkeby 的网络,下面会生成 https 和 wss 两种协议的接口链接,本文我们选择使用 https 的接口链接。

这里我申请的这个接口链接( https://rinkeby.infura.io/v3/0d8d9f7644954cb09d0f63b1672f97f1 )大家也可以用,但这个每天会限制请求人数,用的人多了超出限制就不行了,建议大家都自己去申请一下,一方面方便使用,另外一方面 Infura 也和 metamask 一样,都还是早期,有惊喜也说不定呢。

谷歌浏览器的控制台

第一步,打开谷歌浏览器访问一个网站,比如访问 “https://cn.etherscan.com/” ,在网页空白的地方点鼠标右键,在弹出的菜单中选择 “检查” 或者按 F12 快捷键,在弹出的界面中选择 “Console”

第二步,进入这个界面之后,这里其实提供了一个简易的代码编写环境,比如你要执行一些运算,可以在这里面快速完成(输入代码之后敲回车执行代码)

调用 Infura 接口

当然,这里我们要做的不只是执行一些简单的代码,我们先来完成一个小目标,在这个控制台调用一下 Infura 的接口,去查询一个区块的信息。为了完成这个目标,我们需要使用到 Ethers 库,这个库的作用是帮我们封装了很多方便访问以太坊服务的接口。在这个控制台中如何使用这个库呢?

第一步,我们先在控制台中注入使用这个库,代码如下:

document.write('<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js" type="application/javascript"></script>')

注意,这里在先开网页之后打开的控制台才能成功注入,如果只是开了一个空的新标签页面直接注入是不行的。这段代码的作用是引入了 Ethers 的库,方便在后续代码中引用它。

第二步,注入代码输入回车之后,页面变成了空白,同时可能还会出现这些黄色的警示,但这些并不影响你后续使用,让我们继续输入代码创建一个请求的对象

const rpc = new ethers.providers.JsonRpcProvider("https://rinkeby.infura.io/v3/0d8d9f7644954cb09d0f63b1672f97f1");

注意在这句代码中,我们使用前面申请的 Infura 接口地址创建了了一个 rpc 的请求对象。有了这个请求对象,我们就可以调用查询区块的方法,比如查询 10342503 这个区块的数据(查询因为网络快慢原因,加载速度可能会有差异)。

await rpc.getBlock(10342503);

查询结果如上,我们可以点开查询返回的结果,可以看到详细的区块信息,这当中包含了区块高度,区块 hash,时间戳,交易 hash 列表等信息。

第三步,我们还可以尝试更多的接口,比如查询最新区块高度和查询地址的 ETH 余额。

查询余额的接口返回的是十六进制数据,换算成十进制之后,单位是 wei,要转换成 ether(eth)的单位需要除以 10 的 18 次方。

实现自动 mint 的代码

经过前面内容的学习,我们已经可以在浏览器中调用 Infura 的接口查询一些数据了,那接下来我们分析一段抢跑时代周刊 nft 的代码,该代码出自 @_anishagnihotri,推文地址 https://twitter.com/_anishagnihotri/status/1441072865764429825?s=20,我们基于这段代码,会在这段代码中写上比较详细的注释和讲解其中原理,帮助大家理解整个过程。

// 引入ethers库
const ethers = require("ethers");

// 时代周刊 nft 的合约地址,https://cn.etherscan.com/address/0xDd69da9a83ceDc730bc4d3C56E96D29Acc05eCDE
const address = "0xDd69da9a83ceDc730bc4d3C56E96D29Acc05eCDE";

// 创建一个以太坊节点请求的对象
const rpc = new ethers.providers.JsonRpcProvider("http://localhost:8545");

// 时代周刊 nft 合约的部分 abi,这里提取了 mintTokens 和 balanceOf 方法,前者用于 mint nft,后者用于查询 nft 的余额
const abi = [
  {
    inputs: [{ internalType: "uint8", name: "amount", type: "uint8" }],
    name: "mintTokens",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [{ internalType: "address", name: "owner", type: "address" }],
    name: "balanceOf",
    outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
    stateMutability: "view",
    type: "function",
  },
];

// 构造时代周刊 nft 的合约对象
const times = new ethers.Contract(address, abi, rpc);

// 时代周刊 nft 开始 mint 的时间戳
const startingTimestamp = 1632412800;

// 用于抢跑地址的私钥
const privateKey = "This is privateKey";

// 用私钥创建钱包对象
const wallet = new ethers.Wallet(privateKey);

// 连接钱包
const timesConnected = times.connect(wallet);

(async () => {
  // 监控区块产生的事件
  rpc.on("block", async (blockNumber) => {
    // 获取最新产生区块的时间戳
    const { timestamp } = await rpc.getBlock(blockNumber);
    // 查询地址拥有时代周刊NFT的数量
    const numOwned = (await timesConnected.balanceOf(wallet.address)).toNumber();
    // 输出一条日志信息将区块信息和NFT余额信息记录下来
    console.log(`Block #${blockNumber} balance: ${numOwned} times tokens.`);

    // 关键点
    // 条件一:将最新产生区块的时间戳加上 15 秒,用这个时间去判断是否大于时代周刊开始 mint 的时间
    // 条件二:时代周刊 nft 余额数量不等于 25(时代周刊 nft 一个地址 mint 上限是 25 个)
    // 注意这里两个条件都满足的时候才去发起交易,条件一加上 15 秒是因为以太坊区块产生的速度大概是 15 秒左右产生一个块,
    // 那这样在 mint 开始前的上一个块就把交易发出去,这样抢跑的交易就会提前进入到了交易池,再加上抢跑的交易给了很高的 gas
    // 贿赂旷工,旷工会优先打包这些高 gas 的交易,所以在下一个块产生的时候,正好是 mint 开始之后的时间,而这些高 gas 的抢跑交易
    // 自然就很大概率被旷工打包进 mint 开始之后的第一个块了,条件二这里还加了 nft 余额判断,如果已经 mint 成功就不再发送 mint 交易了,
    // 避免再次发送交易,但这里整个来看还可以做一些优化,可以在发送交易的时候限定 nonce,这样可以更好的避免重复发送交易。
    if (timestamp + 15 >= startingTimestamp && numOwned != 25) {
    // 记录发送抢跑交易的日志
      console.log(`Sending transaction in ${blockNumber}.`);
      // 调用合约的 mintTokens 方法 mint 25 个时代周刊的 nft,第一个是 mint 花费的 eth 2.5 个,时代周刊当时 mint 价格是 0.1eth/个
      // 第二、三个参数是矿工费,在 eip-1559 之后,发送交易需要设置 maxFeePerGas 基础费用(会被燃烧掉)和 maxPriorityFeePerGas 小费(给旷工的奖励)
      // 可以看到这里通过设置很高 gas 的费用去贿赂旷工尽快打包交易
      await timesConnected.mintTokens(25, {
        value: ethers.utils.parseEther("2.5"),
        maxFeePerGas: ethers.utils.parseUnits("500", "gwei"),
        maxPriorityFeePerGas: ethers.utils.parseUnits("500", "gwei"),
      });
    }

    // 当查询到时代周刊 nft 的余额数量是 25 个时候退出抢跑,整个抢跑过程完成。
    if (numOwned == 25) {
      console.log("Successfully exiting");
      process.exit(1);
    }
  });
})();

现实是残酷的,当你还在老老实实等 mint 开始的时间连接钱包的时候,其实科学家们已经把他们的 mint 交易提早 10 多秒就放入交易池等待打包了。为了复现这个过程,我们在测试网部署了一个合约,我们可以尝试用下面这段代码去 mint,整个测试环境还是在浏览器的控制台中完成。

第一步,在控制台中注入 ethers 库,这里我们注入最新版本 5.6.1

document.write('<script src="https://cdn-cors.ethers.io/lib/ethers-5.6.1.umd.min.js" type="application/javascript"></script>')

第二步,创建 infura 的请求对象,测试网合约部署在 ropsten,所以我们这里使用 https://ropsten.infura.io/v3/0d8d9f7644954cb09d0f63b1672f97f1

const rpc = new ethers.providers.JsonRpcProvider("https://ropsten.infura.io/v3/0d8d9f7644954cb09d0f63b1672f97f1");

第三步,从靶场的网站搜索到合约地址,在代码中声明合约地址的变量

const address = "0xDC20938b59078e2550B91090117eF8760E9Ac21D";

第四步,从靶场的网站搜索到 abi,在代码中声明 abi 的变量

const abi = [{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_user","type":"address"},{"indexed":true,"internalType":"uint256","name":"_tokenId","type":"uint256"},{"indexed":false,"internalType":"string","name":"_tokenURI","type":"string"}],"name":"Minted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"allowTransfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"approve","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"enableSale","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxTotalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mintNum","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mintPrice","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_num","type":"uint256"}],"name":"publicMint","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"safeTransferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"_allowTransfer","type":"bool"}],"name":"setAllowTransfer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"_baseURI","type":"string"}],"name":"setBaseURI","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"_enableSale","type":"bool"}],"name":"setEnableSale","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenURI","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"transferFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"_addr","type":"address"},{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]

第五步,创建合约对象,钱包,并连接钱包

// 根据合约地址,abi 和 rpc 创建合约对象
const contract = new ethers.Contract(address, abi, rpc);
// 双引号之间填写钱包私钥,用于学习一定使用一个新地址来学习,切记不要用自己主钱包的私钥。小狐狸地址右边三个点点开,“账户详情”里面可导出私钥。
const privateKey = "xxxxx";
// 根据私钥和 rpc 创建要发起交易钱包的对象
const wallet = new ethers.Wallet(privateKey,rpc);
// 连接钱包
const contractConnected = contract.connect(wallet);

第六步,使用已经连接的合约对象调用合约的 symbol 方法检查是否连通,使用 publicMint 方法发起一笔 mint 交易。

// 调用合约的 symbol 方法,返回的 GC 是 nft 的名字
await contractConnected.symbol();
// 调用合约的 publicMint 方法,mint nft,稍许等待就能看到返回结果,gas 设置 50gwei 已经足够大,测试网 gas 消耗很少

await contractConnected.publicMint(2, {
  value: 0,
  gasLimit: 900000,
  maxFeePerGas: ethers.utils.parseUnits("500", "gwei"),
  maxPriorityFeePerGas: ethers.utils.parseUnits("500", "gwei"),
});

在 publicMint 调用返回交易的信息,其中 hash 是交易的 hash,我们可以拿这个 hash 去区块链浏览器中查询交易的状态。查询地址:https://ropsten.etherscan.io/tx/0x0f3f1378457d7def2e631b1ce338f9e62a84f78ca2c435e940c0bd742f04202f

我们可以看到这笔交易是失败的,因为在写这个文章的时候靶场还未开放,所以不能成功 mint,等待靶场开放期间就能成功 mint 到 nft。

写到这里,我们实现了用代码去 mint nft,我们希望这是一个好的学习方式,大家通过 “走进科学” 系列文章进行学习和试炼场实践进行体验,还可以结合前面时代周刊抢跑的代码,自己写 mint 代码在试炼场进行体验,试炼场一笔交易可以 mint 2 个nft,但我们没有限制 mint 的总数,你可以反复实验。

而这篇文章讲解的内容也只是一个初步尝试和体验,还有很多东西值得进一步关注,比如自动监控区块 mint,多个地址批量 mint,使用 flashbots 等等,如果要更深入学习,我们需要搭建专业的环境和开发工具来帮助我们提升开发和测试效率,感兴趣的小伙伴们可以持续关注我们,后续文章我们会做更深入的讲解。

关于我们

Web3Eye 是一个专注于技术研究和分享的 Web3 加密技术社区,团队拥有多年区块链研发经验和安全技术能力,以帮助更多人安全地进入 Web3 世界,欢迎关注我们的 Twitter 帐号,了解最新动态。

Subscribe to web3eye.eth
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.