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

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

前言本文以OpenZeppelin5.x最新组件为基础,用Hardhat完成「合约→编译→测试→部署」全链路流程。解决openzeppelinV5在0.8.24环境下易出现的编译失败的解决方案;示范了零托管的现场分账逻辑:版税、平台费、卖家收益一次性链上清算,合约不

前言

本文以 OpenZeppelin 5.x 最新组件为基础,用 Hardhat 完成「合约 → 编译 → 测试 → 部署」全链路流程。解决 openzeppelin V5 在 0.8.24 环境下易出现的编译失败的解决方案;示范了零托管的现场分账逻辑:版税、平台费、卖家收益一次性链上清算,合约不留余额,安全又省 Gas。

前期准备

  • 启动本地节点npx hardhat node
  • 想要快速验证合约,可以把合约放在Remix IDE,节省环境配置环节

    合约编译

  • 特别说明针对代码中使用openzeppelin V5会出现编译失败问题
  • 解决方法在hardhat.config.json进行以下设置

    module.exports = {
    solidity:{
    version: "0.8.24",
    settings: {
      evmVersion: "cancun",//处理问题的关键代码
    }
    
    },
    //其他设置......
    }

    核心代码

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

/ OpenZeppelin ^5.4.0 无需 PullPayment / EscrowPayment /

import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/common/ERC2981.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; import "@openzeppelin/contracts/utils/Pausable.sol"; import "@openzeppelin/contracts/access/Ownable.sol";

contract OZMarketplace is ReentrancyGuardTransient, Pausable, Ownable { / ===== 事件 ===== / event Listed(uint256 indexed tokenId, address indexed seller, uint256 price); event Delisted(uint256 indexed tokenId); event Sold(uint256 indexed tokenId, address buyer, address seller, uint256 price);

/* ===== 结构 ===== */
struct Listing {
    address seller;
    uint256 price;
}

/* ===== 常量 & 状态 ===== */
IERC721 public immutable NFT;
uint96  public feeBps = 250;        // 2.5 %
uint96  public constant FEE_DENOMINATOR = 10_000;

mapping(uint256 tokenId => Listing) private _listings;

constructor(address _nft, address initialOwner) Ownable(initialOwner) {
    NFT = IERC721(_nft);
}

/* ===== 管理函数 ===== */
function pause() external onlyOwner {
    _pause();
}

function unpause() external onlyOwner {
    _unpause();
}

function setFeeBps(uint96 _feeBps) external onlyOwner {
    require(_feeBps <= FEE_DENOMINATOR, "fee > 100%");
    feeBps = _feeBps;
}

/* ===== 挂单 ===== */
function list(uint256 tokenId, uint256 price) external whenNotPaused {
    require(price > 0, "Price zero");
    require(NFT.ownerOf(tokenId) == msg.sender, "Not owner");
    require(
        NFT.isApprovedForAll(msg.sender, address(this)) ||
        NFT.getApproved(tokenId) == address(this),
        "Not approved"
    );
    require(_listings[tokenId].seller == address(0), "Listed");

    _listings[tokenId] = Listing(msg.sender, price);
    emit Listed(tokenId, msg.sender, price);
}

/* ===== 撤单 ===== */
function delist(uint256 tokenId) external whenNotPaused {
    Listing memory l = _listings[tokenId];
    require(l.seller == msg.sender, "Not seller");
    delete _listings[tokenId];
    emit Delisted(tokenId);
}

/* ===== 购买(现场转账,无托管) ===== */
function buy(uint256 tokenId)
    external
    payable
    whenNotPaused
    nonReentrant
{
    Listing memory l = _listings[tokenId];
    require(l.seller != address(0), "Not listed");
    require(msg.value == l.price, "Wrong value");

    delete _listings[tokenId];

    /* 版税 & 手续费计算 */
    (address royaltyReceiver, uint256 royaltyAmount) =
        ERC2981(address(NFT)).royaltyInfo(tokenId, l.price);

    uint256 feeAmount = (l.price * feeBps) / FEE_DENOMINATOR;
    uint256 sellerAmount = l.price - royaltyAmount - feeAmount;

    /* 现场转账 —— 合约不留余额 */
    if (royaltyAmount > 0) {
        (bool okRoyalty, ) = royaltyReceiver.call{value: royaltyAmount}("");
        require(okRoyalty, "Royalty fail");
    }
    if (feeAmount > 0) {
        (bool okFee, ) = owner().call{value: feeAmount}("");
        require(okFee, "Fee fail");
    }
    (bool okSeller, ) = l.seller.call{value: sellerAmount}("");
    require(okSeller, "Seller fail");

    NFT.transferFrom(l.seller, msg.sender, tokenId);
    emit Sold(tokenId, msg.sender, l.seller, l.price);
}

/* ===== 查询 ===== */
function getListing(uint256 tokenId)
    external
    view
    returns (address seller, uint256 price)
{
    Listing memory l = _listings[tokenId];
    return (l.seller, l.price);
}

}

**合约编译**:**npx hardat compile**
# 合约测试
**测试流程说明**:
- **完整挂单-购买流程**
-   **NFT 归属:addr1 → addr2**
-   **资金:**

    -   版税 5% → deployer
    -   平台费 2.5% → deployer
    -   剩余 92.5% → addr1

**测试代码**

const {ethers,getNamedAccounts,deployments} = require("hardhat"); const { assert,expect } = require("chai"); describe("OZMarketplace",function(){ let SimpleClosedNFT;//合约 let OZMarketplace;//合约 let addr1; let addr2; let firstAccount//第一个账户 let secondAccount//第二个账户 // let mekadate='ipfs://QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB'; // let mekadate1="ipfs://QmXzbsbjpWpbSGJkgGzmk6r6HLz1nvjpEtjFR6bVhMh3U9" beforeEach(async function(){ await deployments.fixture(["SimpleClosedNFT","OZMarketplace"]); [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);//已经部署的合约交互 const OZMarketplaceDeployment = await deployments.get("OZMarketplace"); OZMarketplace = await ethers.getContractAt("OZMarketplace",OZMarketplaceDeployment.address);//已经部署的合约交互 }) describe("OZMarketplace",function(){ it("把nft挂单-购买流程",async ()=>{

        //铸造一个nft
        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))
       //授权把铸造的nft授权给交易所
       await SimpleClosedNFT.connect(addr1).approve(await OZMarketplace.getAddress(), 1);
       console.log("授权成功",await SimpleClosedNFT.getApproved(1))
       //挂单
       await OZMarketplace.connect(addr1).list(1,price)
         const [seller, pricew] = await OZMarketplace.getListing(1);
         console.log("卖方:",seller)
         console.log("价格:",ethers.formatEther(pricew))
       //购买
         const tx=await OZMarketplace.connect(addr2).buy(1,{value:price})
         const receipt = await tx.wait();
         console.log("购买成功","gas消耗:",receipt.gasUsed , "gas价格:",receipt.gasPrice)
         console.log("nft的所有者",await SimpleClosedNFT.ownerOf(1))
         //校验下架 addr2购买了所欲 下架了
         const [seller1, pricew1] = await OZMarketplace.getListing(1);
         console.log("卖方:",seller1)
         console.log("价格:",ethers.formatEther(pricew1))
        //*  校验资金分账 */
        const gasUsed = receipt.gasUsed * receipt.gasPrice;
        console.log("gas消耗:",gasUsed)
        //版税
        const royaltyAmount = price*500n / 10_000n
        console.log("版税:",ethers.formatEther(royaltyAmount))
        //平台
        const feeAmount = price * 250n / 10_000n;
        console.log("平台:",ethers.formatEther(feeAmount))

        //addr1收入
        const addr1Amount = price - royaltyAmount - feeAmount;
        console.log("addr1收入:",ethers.formatEther(addr1Amount))
    })
})

})

**测试指令**:**npx hardhat test ./test/xxx,js**
# 合约部署
**部署代币**

module.exports = async ({getNamedAccounts,deployments})=>{ const getNamedAccount = (await getNamedAccounts()).firstAccount;

const MyNFT=await deployments.get("SimpleClosedNFT");//获取nft合约

const {deploy,log} = deployments;
const OZMarketplace=await deploy("OZMarketplace",{
    from:getNamedAccount,
    args: [MyNFT.address,getNamedAccount],//参数 nft合约地址,所有者
    log: true,
})
// await hre.run("verify:verify", {
//     address: TokenC.address,
//     constructorArguments: [TokenName, TokenSymbol],
//     });
console.log('OZMarketplace合约地址',OZMarketplace.address)

} module.exports.tags = ["all", "OZMarketplace"];


* **编译指令**:**npx hardat deploy --tags xxx1,xxx2;**
* **参数说明**:**例如 NFT,Exchange) 分别部署nft合约和nft交易的合约**
# 总结
至此NFT交易合约全部完成

1.  **环境踩坑**:用 `"evmVersion": "cancun"` 秒解 OZ V5 与 Solidity 0.8.24 的编译冲突。
1.  **合约设计**:仅用 100 行代码实现挂单、撤单、购买、暂停、手续费及 ERC-2981 版税自动分账,gas 优化到极限。
1.  **全流程测试**:Hardhat 本地网模拟铸造 → 授权 → 挂单 → 购买 → 资金拆分,验证 NFT 所有权与资金流向 100% 正确。
1.  **一键部署**:Hardhat-deploy 脚本化部署,tags 机制可组合部署 NFT 与 Marketplace,后续可直接 `verify` 上链。

现在,你可以把合约地址塞进前端,或者继续拓展:支持 ERC-1155、批量挂单、链下签名订单、实时事件推送。一个生产级的 NFT 市场雏形,正式就绪!
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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