在以太坊上搭建NFT的交易市场

NFT的介绍

NFT全称为Non-Fungible Token,非同质化代币,是用于表示数字资产的唯一加密货币令牌,具有不可分割、不可代替、独一无二等特点。在区块链上,数字加密货币分为原生币和代币两大类。原生币如大家熟悉的比特币、以太币等,拥有自己的主链,使用链上的交易来维护账本数据。代币是依附于现有的区块链,使用智能合约来进行账本的记录,如依附于以太坊上而发布的token。代币之中又可分为同质化和非同质化两种。同质化代币即FT(Fungible Token),互相可以替代、可接近无限拆分的token。例如,你手里有一个比特币与我手里的一个比特币,本质上没有任何区别,这就是同质化,就是同质化币[1]。

①NFT非同质化代币的应用:NFT非同质化代币由于其非同质化、不可拆分的特性,使得它可以和现实世界中的一些商品绑定。换言之,其实就是发行在区块链上的数字资产,这个资产可以是游戏道具、数字艺术品、门票等,并且具有唯一性和不可复制性。由于NFT具备天然的收藏属性和便于交易,加密艺术家们可以利用NFT创造出独一无二的数字艺术品[2]。
②NFT非同质化代币的特点:不可分割:NFT非同质化代币是唯一的、且不可分割成更小的单位。任何一枚比特币、以太坊等都可以换成另外一枚 比特币或以太坊,且他们都可以分割成更小的单位,例如0.1btc或者0.01btc。独一无二:NFT非同质化代币是不可互换的资产,它具有独一无二的属性。这意味着,如果NFT资产本身是稀缺的,它也会承载其价值,不可篡改或者复制。透明度:买方可以看到每个过往卖家和投标者的历史,他们付出了什么价格,以及任何数字商品的当下市场状况。

在这之前,有使用Chainlink VRF使用可验证的 RNG 构建的NFT,他创建根据现实世界数据变化的动态 NFT。通过使用去中心化的预言机来获取数据,将随机NFT添加到OpenSea中[2]。

数字化交易市场搭建

入门
建立数字化交易市场需要做的第一件事是编写智能合约。
市场将由两个主要的智能合约组成:
①用于铸造 NFT 的 NFT 合约
②促进 NFT 销售的市场合约
为了编写 NFT,我们可以使用可通过OpenZeppelin获得的ERC721标准。
首先,转到https://remix.ethereum.org/并创建一个名为Marketplace.sol的新文件。
可以通过导入从 Open Zeppelin 开始所需的合约来开始:

// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.3;

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

import "hardhat/console.sol";

创建 NFT 合约

contract NFT is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    address contractAddress;

    constructor(address marketplaceAddress) ERC721("Eat The Blocks NFTs", "ETBNFT") {
        contractAddress = marketplaceAddress;
    }

    function createToken(string memory tokenURI) public returns (uint) {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();

        _mint(msg.sender, newItemId);
        _setTokenURI(newItemId, tokenURI);
        setApprovalForAll(contractAddress, true);
        return newItemId;
    }
}

构造函数接受marketplaceAddress地址的参数,保存值并使其在智能合约中可用。这样当有人想创建合约时,合约可以允许市场合约批准将代币从所有者转移到卖家。该newItemId值是从函数返回的,因为我们将在我们的客户端应用程序中需要它来了解tokenId智能合约生成的动态值。

创建市场合约
市场合约比NFT合约更为复杂,但也是该项目的重要部分。

contract NFTMarket is ReentrancyGuard {
  using Counters for Counters.Counter;
  Counters.Counter private _itemIds;
  Counters.Counter private _itemsSold;

  struct MarketItem {
    uint itemId;
    address nftContract;
    uint256 tokenId;
    address payable seller;
    address payable owner;
    uint256 price;
  }

  mapping(uint256 => MarketItem) private idToMarketItem;

  event MarketItemCreated (
    uint indexed itemId,
    address indexed nftContract,
    uint256 indexed tokenId,
    address seller,
    address owner,
    uint256 price
  );

  function getMarketItem(uint256 marketItemId) public view returns (MarketItem memory) {
    return idToMarketItem[marketItemId];
  }

  function createMarketItem(
    address nftContract,
    uint256 tokenId,
    uint256 price
  ) public payable nonReentrant {
    require(price > 0, "Price must be at least 1 wei");

    _itemIds.increment();
    uint256 itemId = _itemIds.current();
  
    idToMarketItem[itemId] =  MarketItem(
      itemId,
      nftContract,
      tokenId,
      payable(msg.sender),
      payable(address(0)),
      price
    );

    IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);

    emit MarketItemCreated(
      itemId,
      nftContract,
      tokenId,
      msg.sender,
      address(0),
      price
    );
  }

  function createMarketSale(
    address nftContract,
    uint256 itemId
    ) public payable nonReentrant {
    uint price = idToMarketItem[itemId].price;
    uint tokenId = idToMarketItem[itemId].tokenId;
    require(msg.value == price, "Please submit the asking price in order to complete the purchase");

    idToMarketItem[itemId].seller.transfer(msg.value);
    IERC721(nftContract).transferFrom(address(this), msg.sender, tokenId);
    idToMarketItem[itemId].owner = payable(msg.sender);
    _itemsSold.increment();
  }

  function fetchMarketItem(uint itemId) public view returns (MarketItem memory) {
    MarketItem memory item = idToMarketItem[itemId];
    return item;
  }

  function fetchMarketItems() public view returns (MarketItem[] memory) {
    uint itemCount = _itemIds.current();
    uint unsoldItemCount = _itemIds.current() - _itemsSold.current();
    uint currentIndex = 0;

    MarketItem[] memory items = new MarketItem[](unsoldItemCount);
    for (uint i = 0; i < itemCount; i++) {
      if (idToMarketItem[i + 1].owner == address(0)) {
        uint currentId = idToMarketItem[i + 1].itemId;
        MarketItem storage currentItem = idToMarketItem[currentId];
        items[currentIndex] = currentItem;
        currentIndex += 1;
      }
    }
   
    return items;
  }

  function fetchMyNFTs() public view returns (MarketItem[] memory) {
    uint totalItemCount = _itemIds.current();
    uint itemCount = 0;
    uint currentIndex = 0;

    for (uint i = 0; i < totalItemCount; i++) {
      if (idToMarketItem[i + 1].owner == msg.sender) {
        itemCount += 1;
      }
    }

    MarketItem[] memory items = new MarketItem[](itemCount);
    for (uint i = 0; i < totalItemCount; i++) {
      if (idToMarketItem[i + 1].owner == msg.sender) {
        uint currentId = idToMarketItem[i + 1].itemId;
        MarketItem storage currentItem = idToMarketItem[currentId];
        items[currentIndex] = currentItem;
        currentIndex += 1;
      }
    }
   
    return items;
  }
}

继承来自ReentrancyGuard的nonReentrant,它可以应用于函数以确保没有对它们的嵌套(可重入)调用。我们在MarketItem的数据据结构体中储存我们想要在市场上提供的项目的记录。idToMarketltem是一种可以允许我们在IDs和MarketIItem之间创建关键值得映射。createMarketItem得作用是把NFT转移到市场上的交易地址,这样NFT就可以出售了。createMarketSale是作为NFT和Eth之间的介质,使他们可以在交易中进行转换。fetchMarketItems可以帮助你查看所有在市场上出售的NFT。fetchMyNFTs则可以查看你的购买记录。

构建前端
在前端的构造中使用了已有的初始项目。
marketplace_starter

构建用户界面
构建用户界面是本数字化市场的重点项目,包括以下功能:获取NFT、创建NFT并将其出售、允许用户购买NFT以及允许用户查看他们自己的NFT。
loadNFTs函数通过调用fetchMarkItems来返回市场中未出售的NFT。

async function loadNFTs() {
  const provider = new ethers.providers.JsonRpcProvider()
  const tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)
  const marketContract = new ethers.Contract(nftmarketaddress, Market.abi, provider)
  const data = await marketContract.fetchMarketItems()
  
  const items = await Promise.all(data.map(async i => {
    const tokenUri = await tokenContract.tokenURI(i.tokenId)
    const meta = await axios.get(tokenUri)
    let price = web3.utils.fromWei(i.price.toString(), 'ether');
    let item = {
      price,
      tokenId: i.tokenId.toNumber(),
      seller: i.seller,
      owner: i.owner,
      image: meta.data.image,
    }
    return item
  }))
  setNfts(items)
  setLoaded('loaded') 
}

Creating an NFT and placing it for sale用于创造NFT并将其投放市场,包括createToken和createMarketItem来创建新得Token和放置出售的项目。

async function buyNft(nft) {
  const web3Modal = new Web3Modal({
    network: "mainnet",
    cacheProvider: true,
  });
  const connection = await web3Modal.connect()
  const provider = new ethers.providers.Web3Provider(connection)
  const signer = provider.getSigner()
  const contract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
  
  const price = web3.utils.toWei(nft.price.toString(), 'ether');
  
  const transaction = await contract.createMarketSale(nftaddress, nft.tokenId, {
    value: price
  })
  await transaction.wait()
  loadNFTs()
}

关键的一步是允许用户从市场上购买NFT

async function buyNft(nft) {
  const web3Modal = new Web3Modal({
    network: "mainnet",
    cacheProvider: true,
  });
  const connection = await web3Modal.connect()
  const provider = new ethers.providers.Web3Provider(connection)
  const signer = provider.getSigner()
  const contract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
  
  const price = web3.utils.toWei(nft.price.toString(), 'ether');
  
  const transaction = await contract.createMarketSale(nftaddress, nft.tokenId, {
    value: price
  })
  await transaction.wait()
  loadNFTs()
}

Allowing a user to view their own NFTs实现了让用户可以查看他们购买的NFT,从fetchMyNFTs智能合约中调用了该函数。

async function loadNFTs() {
  const web3Modal = new Web3Modal({
    network: "mainnet",
    cacheProvider: true,
  });
  const connection = await web3Modal.connect()
  const provider = new ethers.providers.Web3Provider(connection)
  const signer = provider.getSigner()
    
  const marketContract = new ethers.Contract(nftmarketaddress, Market.abi, signer)
  const tokenContract = new ethers.Contract(nftaddress, NFT.abi, provider)
  const data = await marketContract.fetchMyNFTs()
  
  const items = await Promise.all(data.map(async i => {
    const tokenUri = await tokenContract.tokenURI(i.tokenId)
    const meta = await axios.get(tokenUri)
    let price = web3.utils.fromWei(i.price.toString(), 'ether');
    let item = {
      price,
      tokenId: i.tokenId.toNumber(),
      seller: i.seller,
      owner: i.owner,
      image: meta.data.image,
    }
    return item
  }))

  setNfts(items)
  setLoaded('loaded') 
}

最后就是我们开发交易市场的盈利项目,从交易中收取上市费用,这仅需几行代码就可以完成。首先在Contracts中打开NFTMarkets.sol文件,需要设置使用上市的价格,还需要创建一个变量来存储合约的所有者。
可以在_itemsSold初始化以下代码:

address payable owner;
uint256 listingPrice = 0.01 ether;

然后创建一个constructor来存储地址:

constructor() {
  owner = payable(msg.sender);
}

在createMarketItem功能在,确保上市的费用包含了手续费:

require(price > 0, "Price must be at least 1 wei"); // below this line 
上一篇:BizTalk开发系列(五) 属性字段


下一篇:Android的Spinner控件用法解析