Simple NFT Dapp详解

最近跟着敲了一个简单的NFT交互Dapp,项目有点老,但是作为入门项目也ok

环境

node: 16.14.0

模板clone链接:

github clone -b starter_code git@github.com:dappuniversity/millow.git

这个分支是一个空模板,方便我们开发 下载之后 npm i 安装一下相关依赖

要做什么

首先我们这个项目非常简单,只涉及到两个合约。一个是NFT合约 ,一个是交易合约。 我们这个项目中的NFT是房地产,RealEstate。

RealEstate.sol

solidity 复制代码
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract RealEstate is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("Real Estate", "REAL") {}

    function mint(string memory tokenURI) public returns (uint256) {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(msg.sender, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }

    function totalSupply() public view returns (uint256) {
        return _tokenIds.current();
    }
}

这个合约涉及到两个功能,一个是铸造mint ,一个是返回根据当前合约创建的NFT总数,比较简单!

还可以看到当前合约调用了一个叫做@openzeppelin中提供的合约。这个库已经有许多改动,我们还是去官网学习一下吧.

举例:

Counter这个合约被remove,取而代之的是合约内部声明私有变量

Escrow.sol

这个合约功能相对多一点,涉及到不同的用户

solidity 复制代码
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

interface IERC721 {
    function transferFrom(address _from, address _to, uint256 _id) external;
}

contract Escrow {
    address public nftAddress;
    address payable public seller;
    address public inspector;
    address public lender;

    modifier onlySeller() {
        require(msg.sender == seller, "Only seller can call this method");
        _;
    }
    modifier onlyBuyer(uint256 _nftID) {
        require(msg.sender == buyer[_nftID], "Only buyer can call this method");
        _;
    }

    modifier onlyInspector() {
        require(msg.sender == inspector, "Only inspector can call this method");
        _;
    }

    mapping(uint256 => bool) public isListed;
    mapping(uint256 => uint256) public purchasePrice;
    mapping(uint256 => uint256) public escrowAmount;
    mapping(uint256 => address) public buyer;
    mapping(uint256 => bool) public inspectionPassed;
    mapping(uint256 => mapping(address => bool)) public approval;

    constructor(
        address _nftAddress,
        address payable _seller,
        address _inspector,
        address _lender
    ) {
        nftAddress = _nftAddress;
        seller = _seller;
        inspector = _inspector;
        lender = _lender;
    }

    function list(
        uint256 _nftID,
        address _buyer,
        uint256 _purchasePrice,
        uint256 _escrowAmount
    ) public payable onlySeller {
        IERC721(nftAddress).transferFrom(msg.sender, address(this), _nftID);

        isListed[_nftID] = true;
        purchasePrice[_nftID] = _purchasePrice;
        escrowAmount[_nftID] = _escrowAmount;
        buyer[_nftID] = _buyer;
    }

    function depositEarnest(uint256 _nftID) public payable onlyBuyer(_nftID) {
        require(msg.value >= escrowAmount[_nftID]);
    }

    function updateInspectionStatus(
        uint256 _nftID,
        bool _passed
    ) public onlyInspector {
        inspectionPassed[_nftID] = _passed;
    }

    function approveSale(uint256 _nftID) public {
        approval[_nftID][msg.sender] = true;
    }

    receive() external payable {}

    function getBalance() public view returns (uint256) {
        return address(this).balance;
    }

    function finalizeSale(uint256 _nftID) public {
        require(inspectionPassed[_nftID]);
        require(approval[_nftID][buyer[_nftID]]);
        require(approval[_nftID][seller]);
        require(approval[_nftID][lender]);
        require(address(this).balance >= purchasePrice[_nftID]);

        (bool success, ) = payable(seller).call{value: address(this).balance}(
            ""
        );
        require(success);

        IERC721(nftAddress).transferFrom(address(this), buyer[_nftID], _nftID);
    }

    function cancelSale(uint256 _nftID) public {
        if (inspectionPassed[_nftID] == false) {
            payable(buyer[_nftID]).transfer(address(this).balance);
        } else {
            payable(seller).transfer(address(this).balance);
        }
    }
}

合约的调用关系图如下:

代码分解:

  1. 合约声明和接口定义:
solidity 复制代码
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

interface IERC721 {
    function transferFrom(address _from, address _to, uint256 _id) external;
}

这部分声明了智能合约使用的许可证和Solidity编译器版本,并且定义了接口IERC721,接口或者说solidity我其实也不太会,我写前端的嘛 ,,ԾㅂԾ,, 后面再研究下。。

  1. 合约状态变量和构造函数

这部分比较简单,就是初始化变量,以及实例化合约时传入相应的值

  1. 列出NFT进行销售
ini 复制代码
 function list(
        uint256 _nftID,
        address _buyer,
        uint256 _purchasePrice,
        uint256 _escrowAmount
    ) public payable onlySeller {
        IERC721(nftAddress).transferFrom(msg.sender, address(this), _nftID);

        isListed[_nftID] = true;
        purchasePrice[_nftID] = _purchasePrice;
        escrowAmount[_nftID] = _escrowAmount;
        buyer[_nftID] = _buyer;
    }

这些都好理解,除了为什么会在上架时,就传入buy的address,ChatGPT说可能是私人交易,fine 剩下的就是一些功能函数,很简单

合约中出现了这样一句代码,

receive() external payable {}

函数在 Solidity 合约中主要用于接收以太币,使合约能够处理直接发送到它的以太币转账。这对于实现简单的支付和捐赠等功能非常有用

  1. 完成销售:
solidity 复制代码
    function finalizeSale(uint256 _nftID) public {
        require(inspectionPassed[_nftID]);
        require(approval[_nftID][buyer[_nftID]]);
        require(approval[_nftID][seller]);
        require(approval[_nftID][lender]);
        require(address(this).balance >= purchasePrice[_nftID]);

        (bool success, ) = payable(seller).call{value: address(this).balance}(
            ""
        );
        require(success);

        IERC721(nftAddress).transferFrom(address(this), buyer[_nftID], _nftID);
    }

require 就是检查各类条件是否通过,其中有一条代码不太容易理解,我问了chatGPT,是说,

(bool success, ) = payable(seller).call{value: address(this).balance}( "" );

  • payable 允许seller地址接受代币
  • {value: address(this).balance} 获取seller所有的代币余额
  • call执行操作,将余额发送给seller
  • ("") 没什么附加信息

我理解就是交易完成后,把钱全部发送给seller,然后最后一行代码就是把nft转给buyer

部署合约

合约写完后,在scripts文件夹下写一个deploy.js部署脚本,部署写完的合约。在这里我们部署在本地的hardhat创建的本地测试链。

执行

可以看到本地测试网络,将测试网络导入到metaMask中,切换metaMask网络至hardhat

同时终端中也会创建一些虚拟账户给我们,同时也有一些虚拟币

部署脚本deploy.js

js 复制代码
// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
// will compile your contracts, add the Hardhat Runtime Environment's members to the
// global scope, and execute the script.
const hre = require("hardhat");

const tokens = (n) => {
    return ethers.utils.parseUnits(n.toString(), "ether");
};

async function main() {
    const [buyer, seller, inspector, lender] = await ethers.getSigners();
    const RealEstate = await ethers.getContractFactory("RealEstate");
    const realEstate = await RealEstate.deploy();
    realEstate.deployed();

    console.log(`RealEstate deployed to ${realEstate.address}`);
    console.log("Mining 3 properties..");

    for (let i = 0; i < 3; i++) {
        const transaction = await realEstate.connect(seller).mint(`https://ipfs.io/ipfs/QmQVcpsjrA6cr1iJjZAodYwmPekYgbnXGo4DFubJiLc2EB/${i + 1}.json`);
        await transaction.wait();
    }

    const Escrow = await ethers.getContractFactory("Escrow");
    const escrow = await Escrow.deploy(realEstate.address, seller.address, inspector.address, lender.address);
    await escrow.deployed();

    console.log(`Deployed Escrow contract to ${escrow.address}`);

    for (let i = 0; i < 3; i++) {
        const transaction = await realEstate.connect(seller).approve(escrow.address, i + 1);
        await transaction.wait();
    }

    transaction = await escrow.connect(seller).list(1, buyer.address, tokens(20), tokens(10));
    await transaction.wait();

    transaction = await escrow.connect(seller).list(2, buyer.address, tokens(15), tokens(5));
    await transaction.wait();

    transaction = await escrow.connect(seller).list(3, buyer.address, tokens(10), tokens(5));
    await transaction.wait();

    console.log("Finished");
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});

这个合约我们分析一下

const [buyer, seller, inspector, lender] = await ethers.getSigners(); 获取前面提到的默认账户,并且分别指定角色

接下来的三行代码就是用工厂函数获取合约。实例化合约,部署合约 。部署完成之后,mint token 。 mint的时候要传入相应的tokenURI,这个可以理解为一个远程服务器,我们上传文件,然后获取我们上传的文件信息。

这是网址信息pinata

然后就是部署escrow合约,并且将NFT授权给escrow。剩下的就是上架NFT。

前端UI

这是我们的界面

涉及到web3的主要功能点就是:

  1. 连接钱包
  2. 获取上架nft信息
  3. 不同角色执行不同功能

App.js

连接web3网络,我看着它方法还是比较原始

调用内置的provider,获取本地的网络的chainId,后面找点先进点的框架

连接钱包

wagmi有封装好的连接钱包的hooks,所以这些老代码我都不爱学。

获取nft

home.js

执行功能

根据不同的account角色执行不同的功能。

逻辑如下:

fetchDetails里面就是根据不同的nft的id获取当时的信息,比如是否有卖家是否有买家等等。

fetchOwner就是获取当前nft的拥有者,

一旦hasSold状态改变,就重新执行函数

不同的角色有不同的逻辑执行功能,举个栗子:

js 复制代码
    const lendHandler = async () => {
        const signer = await provider.getSigner();

        // Lender approves...
        const transaction = await escrow.connect(signer).approveSale(home.id);
        await transaction.wait();

        // Lender sends funds to contract...
        const lendAmount = (await escrow.purchasePrice(home.id)) - (await escrow.escrowAmount(home.id));
        await signer.sendTransaction({ to: escrow.address, value: lendAmount.toString(), gasLimit: 60000 });

        setHasLended(true);
    };

放贷者就是涉及到交易,所以要

  1. 获取带有签名的交易对象
  2. 执行交易
  3. 获取需要放贷的金额
  4. 然后将需要放贷的数量发送到escrow合约
  5. 任务完成

总结:

这个demo真的很简单,我觉得web3主要还是学习业务逻辑或者说玩法。

相关推荐
Sui_Network3 天前
World Wide Walrus:下一代数据存储协议
大数据·人工智能·web3·去中心化·区块链
元宇宙中心4 天前
Solana 区块链的技术解析及未来展望 #dapp开发#公链搭建
web3·区块链·dapp开发·链游开发·公司开发·公链搭建
贵州晓智信息科技4 天前
Web3D 与 AI 的结合重塑虚拟世界与智能应用
人工智能·web3
Roun35 天前
区块链安全性解析:Web3的去信任化与技术挑战
web3·区块链·隐私保护
web3探路者5 天前
2024年 Web3开发学习路线全指南
学习·web3·区块链·智能合约·软件开发·dapp开发·公司开发
Footprint_Analytics6 天前
除了 TON, 哪些公链在争夺 Telegram 用户?数据表现如何?
游戏·web3·区块链
Roun36 天前
区块链赋能Web3:数据透明与隐私保护的新纪元
人工智能·web3·去中心化·区块链
CertiK6 天前
CertiK安全调研报告:Web3.0桌面钱包的初步安全评估
安全·web3·区块链
Nicolas8936 天前
【隐私计算】隐私计算的应用场景探索(大模型隐私计算、隐私数据存储计算、Web3、隐私物联网等)
大模型·web3·隐私计算·隐私计算应用·隐私数据存储·大模型隐私推理
dingzd956 天前
去中心化治理的实现:Web3的区块链新玩法
web3·去中心化·区块链