最近跟着敲了一个简单的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);
}
}
}
合约的调用关系图如下:
代码分解:
- 合约声明和接口定义:
solidity
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
interface IERC721 {
function transferFrom(address _from, address _to, uint256 _id) external;
}
这部分声明了智能合约使用的许可证和Solidity编译器版本,并且定义了接口IERC721,接口
或者说solidity我其实也不太会,我写前端的嘛 ,,ԾㅂԾ,, 后面再研究下。。
- 合约状态变量和构造函数
这部分比较简单,就是初始化变量,以及实例化合约时传入相应的值
- 列出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 合约中主要用于接收以太币,使合约能够处理直接发送到它的以太币转账。这对于实现简单的支付和捐赠等功能非常有用
- 完成销售:
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的主要功能点就是:
- 连接钱包
- 获取上架nft信息
- 不同角色执行不同功能
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);
};
放贷者就是涉及到交易,所以要
- 获取带有签名的交易对象
- 执行交易
- 获取需要放贷的金额
- 然后将需要放贷的数量发送到escrow合约
- 任务完成
总结:
这个demo真的很简单,我觉得web3主要还是学习业务逻辑或者说玩法。