Web3.0:构建 NFT 市场(一)

简介: WEB3.0,最近火热的一个关键词,说到WEB3.0,其中少不了 NFT ,为了更好的理解 NFT ,本文通过创建一个 NFT 市场项目来熟悉 NFT 的铸造及市场机制,该项目将基于 Polygon 网络上的 NFT 市场。

WEB3.0,最近火热的一个关键词,说到WEB3.0,其中少不了 NFT ,为了更好的理解 NFT ,本文通过创建一个 NFT 市场项目来熟悉 NFT 的铸造及市场机制,该项目将基于 Polygon 网络上的 NFT 市场。

现在来快速解释一下  Polygon  网络和 NFT 是什么。

基础知识

NFT 代表不可替代代币,如果是加密货币领域的一员,可能听说过这种新的数字资产。今天看到的大多数 NFT 都是存在于区块链上的图像或短 gif。

虽然任何人都可以截取 NFT,但他们没有所有权证明。把它想象成给一幅名画拍照,它没有价值,NFT 的价值来自所有权。区块链上的合约为这些 NFT 分配唯一地址。即使有人从屏幕截图中创建了另一个 NFT,区块链的历史也将证明它不是原创的。

如果使用最大和最流行的可编程加密货币以太坊,创建 NFT 可能会很昂贵(手续费比较昂贵)。

Polygon (正式名称为 Matic)被称为 Layer-2 区块链。它本质上是对以太坊的升级,它降低了价格并提高了交易速度

初始化和配置

首先打开命令终端并输入 npx create-next-app crayon-nft-marketplace,命令将使用 Next.js(一个用于生产的 React 框架)来创建一个带有预配置文件(例如 pages/index.js)的简单应用程序模板。

创建文件夹和一些相应的文件后,就该安装依赖项了:

npm install ethers hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers web3modal  axios --save
npm install @openzeppelin/contracts --save

IPFS 相关的依赖需要单独安装:

npm install ipfs-http-client@56.0.1 --save

在UI方面,使用 Tailwind CSS,安装依赖:

npm install  tailwindcss@latest postcss@latest autoprefixer@latest -D
npx tailwindcss init -p

智能合约

现在开始编写智能合约,是 NFT 的核心部分。项目根目录下执行下面指令:

npx hardhat

选择创建一个空的 hardhat.config.js 文件。

根据 Infura 文档,添加了一个 hardhat 网络,这里将选择 chainId1337 的测试链。

module.exports = {
    solidity: "0.7.3",
    networks: {
        hardhat: {
            chainId: 1337,
        },
    },
};

接下来,继续根据 Infura 提供给 Polygon 主网和 Polygon Mumbai 测试网 URL

module.exports = {
    solidity: "0.7.3",
    networks: {
        hardhat: {
            chainId: 80001,
        },
        mumbai: {
            url: "https://rpc-mumbai.maticvigil.com/",
        },
        mainnet: {
            url: "https://polygon-rpc.com/",
        },
    },
};

项目根目录下创建 .secret 文件,用于存储 Metamask 钱包的私钥,然后在使用的地方导入即可。然后修改文件 hardhat.config.js ,增加私钥导入和修改 solidity  的版本信息,代码如下:

const fs = require("fs");
require("@nomiclabs/hardhat-waffle");
const privateKey = fs.readFileSync(".secret").toString().trim();
module.exports = {
    solidity: "0.8.4",
    networks: {
        hardhat: {
            chainId: 80001,
        },
        mumbai: {
            url: "https://rpc-mumbai.maticvigil.com/",
            accounts: [privateKey],
        },
        mainnet: {
            url: "https://polygon-rpc.com/",
            accounts: [privateKey],
        },
        ropsten: {
            url: `https://ropsten.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161`,
            accounts: [privateKey],
            chainId: 3,
        },
    },
};

创建智能合约

上面完成了合约所需的环境,现在开始创建智能合约。构建 NFT 及 NFT 市场需要两个智能合约,一个用于创建 NFT,另一个用于查看这些 NFT 的交易。在项目根目录下创建文件夹 contracts,在文件夹中创建两个合约文件 CrayonNft.solCrayonNftMarket.sol,合约导入了 OpenZeppelin 文件,可以访问已经创建的合约。

来看下创建NFT的合约  CrayonNft.sol  。

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

合约导入的内容用途介绍如下:

  • ERC721.sol:核心和元数据扩展,具有基本 URI 机制
  • ERC721URIStorage.sol:一种更灵活但更昂贵的元数据存储方式。
  • Counters.sol:一种获取只能递增、递减或重置的计数器的简单方法。对于 ID 生成、合同活动计数等非常有用。

CrayonNft.sol 合约是最简单的一个,合约实现允许用户通过获取文件 URL 并将其推送到存储在区块链上的数组来铸造 NFT,然后在铸造 NFT 时增加这个数组。完整代码如下:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract CrayonNft is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    address contractAddress;
    constructor(address marketplaceAddress) ERC721("Crayon Nft Tokens", "METT") {
        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;
    }
}

现在来看合约 CrayonNftMarket.sol

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

合约导入的内容用途介绍如下:

  • ERC721.sol:核心和元数据扩展,具有基本 URI 机制
  • Counters.sol:一种获取只能递增、递减或重置的计数器的简单方法。对于 ID 生成、合同活动计数等非常有用。
  • ReentrancyGuard.sol:在某些功能期间可以防止重入的修饰符。
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 
import "hardhat/console.sol";
contract CrayonNftMarket is ReentrancyGuard {
    using Counters for Counters.Counter;
    Counters.Counter private _itemIds; 
    Counters.Counter private _itemsSold;
    address payable owner;
    uint256 listingPrice = 0.025 ether;
    constructor() {
        owner = payable(msg.sender);
    }
    // 定义 NFT 销售属性
    struct MarketItem {
        uint itemId;
        address nftContract;
        uint256 tokenId;
        address payable seller;
        address payable owner;
        uint256 price;
        bool sold;
    }
    mapping(uint256 => MarketItem) private idToMarketItem;
    // 市场项目创建触发器
    event MarketItemCreated (
        uint indexed itemId,
        address indexed nftContract,
        uint256 indexed tokenId,
        address seller,
        address owner,
        uint256 price,
        bool sold
    );
    // 返回价格
    function getListingPrice() public view returns (uint256) {
        return listingPrice;
    }
    // 在市场上销售一个NFT
    function createMarketItem(
        address nftContract,
        uint256 tokenId,
        uint256 price
    ) public payable nonReentrant {
        require(price > 0, "Price must be at least 1 wei"); // 防止免费交易
        require(msg.value == listingPrice, "Price must be equal to listing price"); // 交易价格必须和NFT单价相等
        _itemIds.increment();
        uint256 itemId = _itemIds.current();
        idToMarketItem[itemId] =  MarketItem(
            itemId,
            nftContract,
            tokenId,
            payable(msg.sender),
            payable(address(0)),
            price,
            false
        );
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId); // 将NFT的所有权转让给合同
        emit MarketItemCreated(
            itemId,
            nftContract,
            tokenId,
            msg.sender,
            address(0),
            price,
            false
        );
    }
    // 创建销售市场项目转让所有权和资金
    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); // 将价值转移给卖方
        idToMarketItem[itemId].owner = payable(msg.sender);
        idToMarketItem[itemId].sold = true;
        _itemsSold.increment();
        console.log("Nft Contract Address:",nftContract);
        console.log("tokenId:",tokenId);
        payable(owner).transfer(listingPrice);
    }
    // 返回市场上所有未售出的商品
    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 =  i + 1;
                MarketItem storage currentItem = idToMarketItem[currentId]; 
                items[currentIndex] = currentItem;
                currentIndex += 1;
            } 
        }
        return items;
    }
    // 获取用户购买的NFT
    function fetchMyNFTs() public view returns (MarketItem[] memory) {
        uint totalItemCount = _itemIds.current();
        uint itemCount = 0;
        uint currentIndex = 0;
        for (uint i = 0; i < totalItemCount; i++) {
            for (uint j = 0; j < totalItemCount; j++) {
                itemCount += 1;
            }
        }
        MarketItem[] memory items = new MarketItem[](itemCount);
        for (uint256 i = 0; i < totalItemCount; i++) {
            if (idToMarketItem[i + 1].owner == msg.sender) {
                uint currentId =  i + 1;
                MarketItem storage currentItem = idToMarketItem[currentId];
                items[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return items;
    }
    // 获取卖家制作的NFT
    function fetchItemsCreated() 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].seller == msg.sender) {
                itemCount += 1;
            }
        }
        MarketItem[] memory items = new MarketItem[](itemCount); 
        for (uint i = 0; i < totalItemCount; i++) {
            if (idToMarketItem[i + 1].seller == msg.sender) {
                uint currentId = i + 1;
                MarketItem storage currentItem = idToMarketItem[currentId];
                items[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return items;
    }
}

合约 CrayonNftMarket.sol  定义了正在出售的 NFT、转移资金和所有权、防止多次请求、防止零价值交易,并允许用户查看自己的收藏。

测试智能合约

上面完成了两个智能合约,现在开始编写测试代码,并进行测试。在项目根目录下创建 test 文件夹,并在文件夹中创建文件 test.js,测试代码主要是模拟构建 NFT,并将其投放市场进行交易。测试代码需要依赖框架 ChaiHardhat

首先测试模拟部署NFT市场合约,代码逻辑是先找到相应的合约,部署它,然后获取部署后的地址,代码如下:

// 模拟部署NFT市场合约
const nftMarket = await ethers.getContractFactory("CrayonNftMarket");
const market = await nftMarket.deploy();
await market.deployed(); // 等待市场合约部署
const marketAddress = market.address; // 获取市场合约部署地址

每个成功部署的智能合约都有自己的地址。接下来模拟部署 NFT 合约,和上面的逻辑一样,代码如下:

// 模拟部署NFT合约
const nftContract = await ethers.getContractFactory("CrayonNft");
const nft = await nftContract.deploy(marketAddress);
await nft.deployed();
const nftContractAddress = nft.address; // 获取NFT合约部署地址

上面的代码逻辑将创建NFT的合约部署到在运行的NFT市场,并等待返回 NFT 地址。随着测试NFT市场合约和 创建 NFT 合约的部署,将获取发布到交易市场价格将其转换为以太币(一个完整的以太坊代币),而不是使用 ethers.js 的 Gwei(在实际部署中,将使用 matic 而不是 ethers 作为价格单位)。

const auctionPrice = ethers.utils.parseUnits("100", "ether");

测试代码中还将创建两个 NFT,并将其推送到交易市场,代码如下:

// 创建两个NFT Token
await nft.createToken("https://resources.crayon.dev/nfts/logo.jpg");
await nft.createToken("https://resources.crayon.dev/nfts/share.jpg");
// 将两个NFT 推送到市场进行交易
await market.createMarketItem(nftContractAddress, 1, auctionPrice, {
    value: listingPrice,
});
await market.createMarketItem(nftContractAddress, 2, auctionPrice, {
    value: listingPrice,
});

下面的代码将获取 NFT 地址,将它们放入正确的数组位置,然后获取交易价格。交易价格以以太币为单位。

// 查询并返回未售出的NFT
let arrayItems = await market.fetchMarketItems();
arrayItems = await Promise.all(
    arrayItems.map(async (i) => {
        const tokenUri = await nft.tokenURI(i.tokenId);
        return {
            price: i.price.toString(),
            tokenId: i.tokenId.toString(),
            seller: i.seller,
            owner: i.owner,
            tokenUri,
        };
    })
);
console.log("未销售的NFT: ", arrayItems);

测试完整代码如下:

require("chai");
const { ethers } = require("hardhat");
describe("NFTMarket 测试", function () {
    it("创建NFT并投入市场", async () => {
        // 模拟部署NFT市场合约
        const nftMarket = await ethers.getContractFactory("CrayonNftMarket");
        const market = await nftMarket.deploy();
        await market.deployed(); // 等待市场合约部署
        const marketAddress = market.address; // 获取市场合约部署地址
        // 模拟部署NFT合约
        const nftContract = await ethers.getContractFactory("CrayonNft");
        const nft = await nftContract.deploy(marketAddress);
        await nft.deployed();
        const nftContractAddress = nft.address; // 获取NFT合约部署地址
        let listingPrice = await market.getListingPrice(); // 获取市场价格
        listingPrice = listingPrice.toString();
        // 将发布到市场的交易价格转为 ETH ,而不是 Gwei
        const auctionPrice = ethers.utils.parseUnits("100", "ether");
        // 创建两个NFT Token
        await nft.createToken("https://resources.crayon.dev/nfts/logo.jpg");
        await nft.createToken("https://resources.crayon.dev/nfts/share.jpg");
        // 将两个NFT 推送到市场进行交易
        await market.createMarketItem(nftContractAddress, 1, auctionPrice, {
            value: listingPrice,
        });
        await market.createMarketItem(nftContractAddress, 2, auctionPrice, {
            value: listingPrice,
        });
        /*
            在现实世界的中,用户将通过Metamask等数字钱包与合约进行交互。
            在测试环境中,将使用由Hardhat提供的本地地址进行交互
        */
        const [_, buyerAddress] = await ethers.getSigners();
        // 执行Token(即NFT)销售给另一个用户
        await market
            .connect(buyerAddress)
            .createMarketSale(nftContractAddress, 1, { value: auctionPrice });
        // 查询并返回未售出的NFT
        let arrayItems = await market.fetchMarketItems();
        arrayItems = await Promise.all(
            arrayItems.map(async (i) => {
                const tokenUri = await nft.tokenURI(i.tokenId);
                return {
                    price: i.price.toString(),
                    tokenId: i.tokenId.toString(),
                    seller: i.seller,
                    owner: i.owner,
                    tokenUri,
                };
            })
        );
        console.log("未销售的NFT: ", arrayItems);
    });
});

接下来执行测试命令:

npx hardhat test

即可看到测试结果,不出意外是直接通过。


相关文章
|
1月前
|
移动开发 开发者 HTML5
构建响应式Web界面:Flexbox与Grid的实战应用
【10月更文挑战第22天】随着互联网的普及,用户对Web界面的要求越来越高,不仅需要美观,还要具备良好的响应性和兼容性。为了满足这些需求,Web开发者需要掌握一些高级的布局技术。Flexbox和Grid是现代Web布局的两大法宝,它们分别由CSS3和HTML5引入,能够帮助开发者构建出更加灵活和易于维护的响应式Web界面。本文将深入探讨Flexbox和Grid的实战应用,并通过具体实例来展示它们在构建响应式Web界面中的强大能力。
39 3
|
3天前
|
监控 前端开发 JavaScript
使用 MERN 堆栈构建可扩展 Web 应用程序的最佳实践
使用 MERN 堆栈构建可扩展 Web 应用程序的最佳实践
17 6
|
18天前
|
SQL 安全 前端开发
PHP与现代Web开发:构建高效的网络应用
【10月更文挑战第37天】在数字化时代,PHP作为一门强大的服务器端脚本语言,持续影响着Web开发的面貌。本文将深入探讨PHP在现代Web开发中的角色,包括其核心优势、面临的挑战以及如何利用PHP构建高效、安全的网络应用。通过具体代码示例和最佳实践的分享,旨在为开发者提供实用指南,帮助他们在不断变化的技术环境中保持竞争力。
|
20天前
|
PHP 开发者
深入浅出PHP:构建你的第一个Web应用
【10月更文挑战第35天】在数字时代的浪潮中,掌握编程技能已成为通往未来的钥匙。本文将带你从零开始,一步步走进PHP的世界,解锁创建动态网页的魔法。通过浅显易懂的语言和实际代码示例,我们将共同打造一个简单但功能强大的Web应用。无论你是编程新手还是希望扩展技能的老手,这篇文章都将是你的理想选择。让我们一起探索PHP的魅力,开启你的编程之旅!
|
23天前
|
缓存 前端开发 JavaScript
构建高性能与用户体验并重的现代Web应用
构建高性能与用户体验并重的现代Web应用
39 5
|
21天前
|
开发框架 前端开发 JavaScript
利用Python和Flask构建轻量级Web应用的实战指南
利用Python和Flask构建轻量级Web应用的实战指南
59 2
|
1月前
|
存储 消息中间件 缓存
构建互联网高性能WEB系统经验总结
构建互联网高性能WEB系统经验总结
|
24天前
|
监控 前端开发 JavaScript
探索微前端架构:构建可扩展的现代Web应用
【10月更文挑战第29天】本文探讨了微前端架构的核心概念、优势及实施策略,通过将大型前端应用拆分为多个独立的微应用,提高开发效率、增强可维护性,并支持灵活的技术选型。实际案例包括Spotify和Zalando的成功应用。
|
23天前
|
前端开发 JavaScript jenkins
构建高效、可维护的Web应用
构建高效、可维护的Web应用
37 2
|
29天前
|
前端开发 JavaScript API
前端框架新探索:Svelte在构建高性能Web应用中的优势
【10月更文挑战第26天】近年来,前端技术飞速发展,Svelte凭借独特的编译时优化和简洁的API设计,成为构建高性能Web应用的优选。本文介绍Svelte的特点和优势,包括编译而非虚拟DOM、组件化开发、状态管理及响应式更新机制,并通过示例代码展示其使用方法。
42 2