React Native DApp 开发全栈实战·从 0 到 1 系列(铸造NFT-合约部分)

  • 木西
  • 发布于 1天前
  • 阅读 205

前言本文用Hardhat+OpenZeppelin5.x,完成一条「可铸造、可提现、带版税」的ERC-721代币主网流水线,分别为智能合约和前端两部分,本文主要介绍智能合约相关开发的内容;前期准备使用hardhat创建项目:这里就不赘述了具体可以作者的另一篇文章《智能合约开发

前言

本文用 Hardhat + OpenZeppelin 5.x,完成一条「可铸造、可提现、带版税」的 ERC-721 代币主网流水线,分别为智能合约和前端两部分,本文主要介绍智能合约相关开发的内容;

前期准备

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/*
  安装:
  npm i @openzeppelin/contracts@5.4
*/
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/common/ERC2981.sol";

contract SimpleClosedNFT is ERC721URIStorage, ERC2981, Ownable {
    uint256 public nextTokenId = 1;
    uint96 public constant FEE_DENOMINATOR = 10_000; // 100% = 10000

    mapping(uint256 => uint256) public mintPrice; // tokenId => price in wei

    constructor(
        string memory _name,
        string memory _symbol,
        address _initialOwner,
        uint96 _defaultRoyalty // 500 = 5%
    ) ERC721(_name, _symbol) Ownable(_initialOwner) {
        _setDefaultRoyalty(_initialOwner, _defaultRoyalty);
    }

    /* ========== 铸造 ========== */
    function create(
        string calldata tokenURI,
        uint256 price,
        uint96 royaltyBps // 可选:单个作品的版税,0 则沿用默认
    ) external payable returns (uint256 tokenId) {
        require(msg.value >= price, "Insufficient payment");

        tokenId = nextTokenId++;
        _safeMint(msg.sender, tokenId);
        _setTokenURI(tokenId, tokenURI);
        mintPrice[tokenId] = price;

        // 如果传了 royaltyBps,则单独设置
        if (royaltyBps > 0) {
            _setTokenRoyalty(tokenId, msg.sender, royaltyBps);
        }

        // 退回多余 ETH
        if (msg.value > price) {
            (bool ok, ) = msg.sender.call{value: msg.value - price}("");
            require(ok, "Refund failed");
        }
    }

    /* ========== 提现 ========== */
    function withdraw() external onlyOwner {
        (bool ok, ) = owner().call{value: address(this).balance}("");
        require(ok, "Withdraw failed");
    }
    /* ========== 支持 721 + 2981 ========== */
    function supportsInterface(bytes4 interfaceId)
    public
    view
    override(ERC721URIStorage, ERC2981)   // ✅ 只写真正声明 override 的合约
    returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

编译指令npx hardhat compile

部署合约

部署说明主要使用hardhat-deploy插件快速部署合约

hardhat.config.json配置相关

# 说明:按照顺序
require("@nomicfoundation/hardhat-ethers");//ethers V6
require('hardhat-deploy');//部署插件
require("hardhat-deploy-ethers");//部署插件解决getContract

部署核心代币

module.exports = async  ({getNamedAccounts,deployments})=>{
    const getNamedAccount = (await getNamedAccounts()).firstAccount;
    const NFTName = "SimpleClosedNFT";
    const NFTSymbol = "SCNFT";
    const DefaultRoyalty = 500;

    const {deploy,log} = deployments;
    const SimpleClosedNFT=await deploy("SimpleClosedNFT",{
        from:getNamedAccount,
        args: [NFTName,NFTSymbol,getNamedAccount,DefaultRoyalty],//参数 string memory _name,string memory _symbol,address _initialOwner,uint96 _defaultRoyalty // 500 = 5%
        log: true,
    })
    // await hre.run("verify:verify", {
    //     address: TokenC.address,
    //     constructorArguments: [TokenName, TokenSymbol],
    //     });
    console.log('SimpleClosedNFT合约地址',SimpleClosedNFT.address)
}
module.exports.tags = ["all", "SimpleClosedNFT"];

部署指令npx hardhat deploy

测试合约

测试说明主要针对合约核心功能进行测试

const {ethers,getNamedAccounts,deployments} = require("hardhat");
const { assert,expect } = require("chai");
describe("SimpleClosedNFT",function(){
    let addr1,addr2;
    let firstAccount//第一个账户
    let secondAccount//第二个账户

    beforeEach(async function(){
        await deployments.fixture(["SimpleClosedNFT"]);
        [addr1,addr2]=await ethers.getSigners();
        firstAccount=(await getNamedAccounts()).firstAccount;
        secondAccount=(await getNamedAccounts()).secondAccount;
        const SimpleClosedNFTDeployment = await deployments.get("SimpleClosedNFT");
        SimpleClosedNFT = await ethers.getContractAt("SimpleClosedNFT",SimpleClosedNFTDeployment.address);//已经部署的合约交互
    });
    describe("SimpleClosedNFT 测试",function(){
        it("读取合约基本信息以及查看账户余额",async function(){
            console.log('代币名',await SimpleClosedNFT.name())
            console.log('代币符号',await SimpleClosedNFT.symbol())
        });
        it("铸造一个nft",async function(){
            const uri = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmZdC1RywVL2mPry9TSP128ZUrvPJCJPtndqMUpv5TNctn";
            const price = ethers.parseEther("0.1");
            const royaltyBps=500;
           await SimpleClosedNFT.connect(addr1).create(uri,price,royaltyBps,{value:price})
           console.log(await SimpleClosedNFT.tokenURI(1))
           console.log("nft的所有者",await SimpleClosedNFT.ownerOf(1))
           const mintPrice=await SimpleClosedNFT.mintPrice(1)
           console.log("nft的价格",ethers.formatEther(mintPrice))

        });
        it("设置版税",async function(){
            const uri = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmZdC1RywVL2mPry9TSP128ZUrvPJCJPtndqMUpv5TNctn";
            const price = ethers.parseEther("0.1");
            const royaltyBps=1000;//10%
            await SimpleClosedNFT.connect(addr1).create(uri, price, royaltyBps, { value: price });
            const info = await SimpleClosedNFT.royaltyInfo(1, 10000);//tokenid,salePrice(销售价格)
            console.log("版税接收地址",info[0])
            console.log("版税金额",info[1])
        });
        it("提现功能",async function(){
             const uri = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmZdC1RywVL2mPry9TSP128ZUrvPJCJPtndqMUpv5TNctn";
            const price = ethers.parseEther("0.1");
            const royaltyBps=1000;//10%
             await SimpleClosedNFT
            .connect(addr1)
            .create(uri, price, 0, {
                value: price,
            });
            const owner = await SimpleClosedNFT.owner(); // 合约 owner 地址
            const ownerBalBefore = await ethers.provider.getBalance(owner);

            console.log("提现前",ownerBalBefore)

            const tx = await SimpleClosedNFT.connect(await ethers.getSigner(owner)).withdraw();
            const receipt = await tx.wait();
            const ownerBalAfter = await ethers.provider.getBalance(owner);
            const withdrawAmount = price; // 本例中只有一笔 0.1 ETH
            const gasUsed = receipt.gasUsed * receipt.gasPrice;
            console.log("提现后",ownerBalAfter)
            console.log("Gas消耗",gasUsed)
            console.log("提现金额",withdrawAmount)
            expect(ownerBalAfter).to.equal(ownerBalBefore + withdrawAmount - gasUsed);

        });
        it("非Owner不能提取",async function(){
           console.log("异常报错",await SimpleClosedNFT.connect(addr2).withdraw())
        });

    });

});

测试指令npx hardhat test ./test/xxx.js

总结

至此,合约部分已全部收尾:
代码、测试、部署脚本一条龙就绪,并在本地与测试网跑通。
补充一句行规—— “NFT 合约的测试代码行数通常是源码的 5~10 倍” ;安全与逻辑正确性只能靠测试兜底,而非 Solidity 语法本身。 下一步,我们将把这套经过充分验证的合约无缝接入前端,进入真正的用户交互阶段。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。