React Native DApp 開發(fā)全棧實戰(zhàn)·從 0 到 1 系列(鑄造NFT-合約部分)

前言

本文用 Hardhat + OpenZeppelin 5.x,完成一條「可鑄造、可提現(xiàn)、帶版稅」的 ERC-721 代幣主網(wǎng)流水線,分別為智能合約和前端兩部分,本文主要介紹智能合約相關(guān)開發(fā)的內(nèi)容;

前期準(zhǔn)備

編譯合約

合約說明

  • 鑄造 create()(含退款)
  • 提現(xiàn) withdraw()
  • ERC-2981 默認(rèn)版稅 & 單 Token 版稅
  • 權(quán)限控制(Ownable)
// 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 則沿用默認(rèn)
    ) external payable returns (uint256 tokenId) {
        require(msg.value >= price, "Insufficient payment");

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

        // 如果傳了 royaltyBps,則單獨(dú)設(shè)置
        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");
        }
    }

    /* ========== 提現(xiàn) ========== */
    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配置相關(guān)

# 說明:按照順序
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],//參數(shù) 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

測試合約

測試說明主要針對合約核心功能進(jìn)行測試

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);//已經(jīng)部署的合約交互
    });
    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("設(shè)置版稅",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("提現(xiàn)功能",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("提現(xiàn)前",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("提現(xiàn)后",ownerBalAfter)
            console.log("Gas消耗",gasUsed)
            console.log("提現(xiàn)金額",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

總結(jié)

至此,合約部分已全部收尾:
代碼、測試、部署腳本一條龍就緒,并在本地與測試網(wǎng)跑通。
補(bǔ)充一句行規(guī)—— “NFT 合約的測試代碼行數(shù)通常是源碼的 5~10 倍” ;安全與邏輯正確性只能靠測試兜底,而非 Solidity 語法本身。
下一步,我們將把這套經(jīng)過充分驗證的合約無縫接入前端,進(jìn)入真正的用戶交互階段。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容