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主要还是学习业务逻辑或者说玩法。

相关推荐
清 晨18 小时前
开放的数据时代:Web3和个人隐私的未来
web3·去中心化·智能合约·隐私保护
dingzd9521 小时前
数字世界的新秩序:探索Web3的前景
web3·去中心化·智能合约
nina_LeXin2 天前
Mina protocol - 体验教程
web3·区块链·密码学·零知识证明
Roun33 天前
Web3与AI的交汇点:打造未来智能化去中心化应用
人工智能·web3·去中心化
Roun33 天前
科技前沿:Web3与物联网的智能连接
科技·物联网·web3·去中心化
dingzd955 天前
Web3的崛起与智能合约的角色
人工智能·web3·去中心化·创新
Thetoicxdude5 天前
[Day 76] 區塊鏈與人工智能的聯動應用:理論、技術與實踐
人工智能·web3·numpy
清 晨6 天前
区块链之变:揭秘Web3对互联网的改变
人工智能·web3·去中心化·区块链
紫队安全研究6 天前
13. 从Web2到Web3的转型
web3
dingzd956 天前
Web3入门指南:从基础概念到实际应用
人工智能·web3·去中心化·区块链·智能合约