本教程介绍了如何通过 ethers.js 库使用以太币在以太坊区块链上铸造 NFT,以及我们来自第一部分的智能合约:如何创建 NFT。我们还将探讨基本测试。
完成本指南的预计时间:~10 分钟
在本练习中,我们将引导您完成使用OpenZeppelin 库 版本 4以及Ethers.js 以太 坊库而不是 Web3 的替代实现。
我们还将介绍使用Hardhat 和 Waffle 测试您的合约的基础知识。对于本教程,我使用的是 Yarn,但如果您愿意,也可以使用 npm/npx。
最后,我们将使用 TypeScript。这已被很好地记录下来 ,所以我们不会在这里介绍它。
在所有其他方面,本教程与 Web3 版本的工作方式相同,包括 Pinata 和 IPFS 等工具。
快速提醒
提醒一下,“铸造 NFT”是在区块链上发布您的 ERC721 代币的唯一实例的行为。本教程假设您已经在 NFT 教程系列的第一部分中成功将智能合约部署到 Goerli 网络 ,其中包括 .
第 1 步:创建您的 Solidity 合约
OpenZeppelin 是用于安全智能合约开发的库。您只需继承他们对 ERC20 或 ERC721 等流行标准的实现,并根据您的需要扩展行为。我们将把这个文件放在 contracts/MyNFT.sol 中。
复制 // Contract based on https://docs.openzeppelin.com/contracts/4.x/erc721
// SPDX-License-Identifier: MIT
pragma solidity ^ 0.8 . 0 ;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol" ;
import "@openzeppelin/contracts/utils/Counters.sol" ;
contract MyNFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters .Counter private _tokenIds;
constructor () ERC721 ( "MyNFT" , "MNFT" ) {}
function mintNFT (address recipient , string memory tokenURI)
public
returns (uint256)
{
_tokenIds .increment ();
uint256 newItemId = _tokenIds .current ();
_mint (recipient , newItemId);
_setTokenURI (newItemId , tokenURI);
return newItemId;
}
}
第 2 步:创建 Hardhat 任务以部署我们的合约并铸造 NFT
创建包含以下内容的文件 tasks/nft.ts:
复制 import { task , types } from "hardhat/config" ;
import { Contract } from "ethers" ;
import { TransactionResponse } from "@ethersproject/abstract-provider" ;
import { env } from "../lib/env" ;
import { getContract } from "../lib/contract" ;
import { getWallet } from "../lib/wallet" ;
task ( "deploy-contract" , "Deploy NFT contract" ) .setAction ( async (_ , hre) => {
return hre .ethers
.getContractFactory ( "MyNFT" , getWallet ())
.then ((contractFactory) => contractFactory .deploy ())
.then ((result) => {
process . stdout .write ( `Contract address: ${ result .address } ` );
});
});
task ( "mint-nft" , "Mint an NFT" )
.addParam ( "tokenUri" , "Your ERC721 Token URI" , undefined , types .string)
.setAction ( async (tokenUri , hre) => {
return getContract ( "MyNFT" , hre)
.then ((contract : Contract ) => {
return contract .mintNFT ( env ( "ETH_PUBLIC_KEY" ) , tokenUri , {
gasLimit : 500_000 ,
});
})
.then ((tr : TransactionResponse ) => {
process . stdout .write ( `TX hash: ${ tr .hash } ` );
});
});
第 3 步:创建助手
您会注意到我们的任务导入了一些助手。他们来了。
合同.ts
复制 import { Contract , ethers } from "ethers" ;
import { getContractAt } from "@nomiclabs/hardhat-ethers/internal/helpers" ;
import { HardhatRuntimeEnvironment } from "hardhat/types" ;
import { env } from "./env" ;
import { getProvider } from "./provider" ;
export function getContract (
name : string ,
hre : HardhatRuntimeEnvironment
) : Promise < Contract > {
const WALLET = new ethers .Wallet ( env ( "ETH_PRIVATE_KEY" ) , getProvider ());
return getContractAt (hre , name , env ( "NFT_CONTRACT_ADDRESS" ) , WALLET );
}
环境文件
复制 export function env (key : string ) : string {
const value = process .env[key];
if (value === undefined ) {
throw ` ${ key } is undefined` ;
}
return value;
}
供应商.ts
请注意,最终的 getProvider() 函数使用 ropsten 网络。此参数是可选的,如果省略则默认为“homestead”。当然,我们使用的是 Alchemy,但有多种受支持的替代方案 。
复制 import { ethers } from "ethers" ;
export function getProvider () : ethers . providers . Provider {
return ethers .getDefaultProvider ( "ropsten" , {
alchemy : process . env . ALCHEMY_API_KEY ,
});
}
钱包.ts
复制 import { ethers } from "ethers" ;
import { env } from "./env" ;
import { getProvider } from "./provider" ;
export function getWallet () : ethers . Wallet {
return new ethers .Wallet ( env ( "ETH_PRIVATE_KEY" ) , getProvider ());
}
第 4 步:创建测试
在您的测试目录下,创建这些文件。请注意,这些测试并不全面。他们测试了 OpenZeppelin 库提供的一小部分 ERC721 功能,旨在为您提供构建块以创建更强大的测试。
test/MyNFT.spec.ts(单元测试)
复制 import { ethers , waffle } from "hardhat" ;
import { Contract , Wallet } from "ethers" ;
import { expect } from "chai" ;
import { TransactionResponse } from "@ethersproject/abstract-provider" ;
import sinon from "sinon" ;
import { deployTestContract } from "./test-helper" ;
import * as provider from "../lib/provider" ;
describe ( "MyNFT" , () => {
const TOKEN_URI = "http://example.com/ip_records/42" ;
let deployedContract : Contract ;
let wallet : Wallet ;
beforeEach ( async () => {
sinon .stub (provider , "getProvider" ) .returns ( waffle .provider);
[wallet] = waffle . provider .getWallets ();
deployedContract = await deployTestContract ( "MyNFT" );
});
async function mintNftDefault () : Promise < TransactionResponse > {
return deployedContract .mintNFT ( wallet .address , TOKEN_URI );
}
describe ( "mintNft" , async () => {
it ( "emits the Transfer event" , async () => {
await expect ( mintNftDefault ())
. to .emit (deployedContract , "Transfer" )
.withArgs ( ethers . constants .AddressZero , wallet .address , "1" );
});
it ( "returns the new item ID" , async () => {
await expect (
await deployedContract . callStatic .mintNFT ( wallet .address , TOKEN_URI )
). to .eq ( "1" );
});
it ( "increments the item ID" , async () => {
const STARTING_NEW_ITEM_ID = "1" ;
const NEXT_NEW_ITEM_ID = "2" ;
await expect ( mintNftDefault ())
. to .emit (deployedContract , "Transfer" )
.withArgs (
ethers . constants .AddressZero ,
wallet .address ,
STARTING_NEW_ITEM_ID
);
await expect ( mintNftDefault ())
. to .emit (deployedContract , "Transfer" )
.withArgs (
ethers . constants .AddressZero ,
wallet .address ,
NEXT_NEW_ITEM_ID
);
});
it ( "cannot mint to address zero" , async () => {
const TX = deployedContract .mintNFT (
ethers . constants .AddressZero ,
TOKEN_URI
);
await expect ( TX ). to . be .revertedWith ( "ERC721: mint to the zero address" );
});
});
describe ( "balanceOf" , () => {
it ( "gets the count of NFTs for this address" , async () => {
await expect ( await deployedContract .balanceOf ( wallet .address)). to .eq ( "0" );
await mintNftDefault ();
expect ( await deployedContract .balanceOf ( wallet .address)). to .eq ( "1" );
});
});
});
tasks.spec.ts(集成规范)
复制 import { deployTestContract , getTestWallet } from "./test-helper" ;
import { waffle , run } from "hardhat" ;
import { expect } from "chai" ;
import sinon from "sinon" ;
import * as provider from "../lib/provider" ;
describe ( "tasks" , () => {
beforeEach ( async () => {
sinon .stub (provider , "getProvider" ) .returns ( waffle .provider);
const wallet = getTestWallet ();
sinon .stub (process , "env" ) .value ({
ETH_PUBLIC_KEY : wallet .address ,
ETH_PRIVATE_KEY : wallet .privateKey ,
});
});
describe ( "deploy-contract" , () => {
it ( "calls through and returns the transaction object" , async () => {
sinon .stub ( process .stdout , "write" );
await run ( "deploy-contract" );
await expect ( process . stdout .write). to . have . been .calledWith (
"Contract address: 0x610178dA211FEF7D417bC0e6FeD39F05609AD788"
);
});
});
describe ( "mint-nft" , () => {
beforeEach ( async () => {
const deployedContract = await deployTestContract ( "MyNFT" );
process . env . NFT_CONTRACT_ADDRESS = deployedContract .address;
});
it ( "calls through and returns the transaction object" , async () => {
sinon .stub ( process .stdout , "write" );
await run ( "mint-nft" , { tokenUri : "https://example.com/record/4" });
await expect ( process . stdout .write). to . have . been .calledWith (
"TX hash: 0xd1e60d34f92b18796080a7fcbcd8c2b2c009687daec12f8bb325ded6a81f5eed"
);
});
});
});
test-helpers.ts 请注意,这需要导入 NPM 库,包括 sinon、chai 和 sinon-chai。由于使用存根,因此需要调用sinon.restore() 。
复制 import sinon from "sinon" ;
import chai from "chai" ;
import sinonChai from "sinon-chai" ;
import { ethers as hardhatEthers , waffle } from "hardhat" ;
import { Contract , Wallet } from "ethers" ;
chai .use (sinonChai);
afterEach (() => {
sinon .restore ();
});
export function deployTestContract (name : string ) : Promise < Contract > {
return hardhatEthers
.getContractFactory (name , getTestWallet ())
.then ((contractFactory) => contractFactory .deploy ());
}
export function getTestWallet () : Wallet {
return waffle . provider .getWallets ()[ 0 ];
}
第 5 步:配置
这是我们相当简单的hardhat.config.ts 。
复制 import ( "@nomiclabs/hardhat-ethers" );
import ( "@nomiclabs/hardhat-waffle" );
import dotenv from "dotenv" ;
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more
const argv = JSON .parse ( env ( "npm_config_argv" ));
if ( argv .original !== [ "hardhat" , "test" ]) {
require ( 'dotenv' ) .config ();
}
import ( "./tasks/nft" );
import { HardhatUserConfig } from "hardhat/config" ;
const config : HardhatUserConfig = {
solidity : "0.8.6" ,
};
export default config;
请注意,如果我们不运行测试,则只调用 dotenv 的条件。您可能不想在生产环境中运行它,但请放心,如果 .env 文件不存在,dotenv 将默默地忽略它。
运行我们的任务
现在我们已经将这些文件放在适当的位置,我们可以运行 hardhat 来查看我们的任务(为简洁起见,不包括内置任务)。
复制 AVAILABLE TASKS :
deploy - contract Deploy NFT contract
mint - nft Mint an NFT
忘记了任务的论据?没问题。
复制 $ hardhat help deploy - contract
Usage : hardhat [ GLOBAL OPTIONS ] deploy - contract
deploy - contract : Deploy NFT contract
运行我们的测试
为了运行我们的测试,我们运行hardhat test 。
复制 mintNft
✓ calls through and returns the transaction object (60ms)
MyNFT
mintNft
✓ emits the Transfer event (60ms)
✓ returns the new item ID
✓ increments the item ID (57ms)
✓ cannot mint to address zero
balanceOf
✓ gets the count of NFTs for this address
6 passing (2s)
✨ Done in 5.66s.
概括
在本教程中,我们为基于 Solidity 的经过良好测试的 NFT 基础设施奠定了坚实的基础。waffle.provider.getWallets() 提供的钱包链接到一个本地假Hardhat Network 帐户,该帐户方便地预装 了一个 eth 余额,我们可以用它来为我们的测试交易提供资金。