Road to Web3 第七周 创建一个NFT市场

原文地址 :https://docs.alchemy.com/docs/how-to-build-an-nft-marketplace-from-scratch

请大家关注我的推特(twitter.com/SoullessL)和Link3(link3.to/caishen),获取最新的Alchemy小白教程。教程汇总链接(jayjiang.gitbook.io/web3book/alchemy-road-to-web3)。

Metamask 添加Alchemy的测试网络

大部分人应该已经添加过了,如果已经添加可以忽略

测试网络信息如下

Network Name: Goerli Test Network

RPC base URL: https://eth-goerli.alchemyapi.io/v2/{INSERT YOUR API KEY}

Chain ID: 5

Block Explorer URL: https://goerli.etherscan.io/

Symbol (Optional): ETH

登录(www.alchemy.com)其中{INSERT YOUR API KEY}需要修改为你的Api Key,如截图所示。

准备工作

进入Alchemy官方的Github地址(https://github.com/OMGWINNING/NFT-Marketplace-Tutorial),点击Fork按钮,把官方的代码复制到你的Github里。

然后系统就会自动转跳到你的Github页面,像截图里的地方会是你的github名字,我们记录下自己项目的GitHub浏览器地址。

代码修改

然后通过Gitpod打开项目,在浏览器输入 https://gitpod.io/#https://github.com/你的github名字/NFT-Marketplace-Tutorial 开打项目。

然后等待出现下面的窗口,你可以点击这个Open Browser,来预览我们项目。

然后会出来这样一个窗口,就说明我们的项目加载好了,我图里的图片还没完全加载完,可以忽略。

然后我们回到我们的Terminal窗口,同时按住Ctrl+C,取消程序的运行。

然后我们输入命令 npm install dotenv --save 安装一下dotenv环境

然后在Explorer窗体的空白处点击鼠标右键,弹出一个窗口,选择New File。

文件名字为.env,里面的内容为

REACT_APP_ALCHEMY_API_URL="<YOUR_API_URL>"

REACT_APP_PRIVATE_KEY="<YOUR_PRIVATE_KEY>"

请记得把”<YOUR_API_URL>”替换成Achemy的Goerli网络的Https部分,而”<YOUR_PRIVATE_KEY>”需要替换成你的Metamask账号的的私钥(这边最好使用一个新的账号,防止私钥泄密,这边使用的账号可以和你领取NFT的账号不一样,所以完全可以使用新的好账号,记得转点测试的ETH到新账号当手续费就行。用完最后记得把你的私钥删除,更安全一点)。

然后我们进入 https://pinata.cloud 注册一个新的账号,然后进入https//app.pinata.cloud/keys,点击New Key,勾选Admin,输入Key name,点击Create Key,创建一个新的Key。在弹出的窗口里,记录对应的 API Key 和 API Secret。

REACT_APP_PINATA_KEY="<YOUR_PINATA_KEY>"
REACT_APP_PINATA_SECRET="<YOUR_PINATA_SECRET>"

然后把pinata对应的KEY和SECRET替换"<YOUR_PINATA_KEY>"和"<YOUR_PINATA_SECRET>",并添加到.env文件里。

最终.env文件如上图所示,但是对应的Key都是你自己的。

贴上整体的.env文件内容,方便大家替换。

REACT_APP_ALCHEMY_API_URL="<YOUR_API_URL>"
REACT_APP_PRIVATE_KEY="<YOUR_PRIVATE_KEY>"
REACT_APP_PINATA_KEY="<YOUR_PINATA_KEY>"
REACT_APP_PINATA_SECRET="<YOUR_PINATA_SECRET>"

然后我们找到hardhat.config.js文件,把里的内容替换为

require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-ethers");
const fs = require('fs');
// const infuraId = fs.readFileSync(".infuraid").toString().trim() || "";
require('dotenv').config();

task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

module.exports = {
  networks: {
    goerli: {
      url: process.env.REACT_APP_ALCHEMY_API_URL,
      accounts: [process.env.REACT_APP_PRIVATE_KEY]
    }
  },
  solidity: {
    version: "0.8.4",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  }
};

打开Contracts文件夹下面的NFTMarketplace.sol 文件,替换全部内容为

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

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

contract NFTMarketplace is ERC721URIStorage {

    using Counters for Counters.Counter;
    //_tokenIds variable has the most recent minted tokenId
    Counters.Counter private _tokenIds;
    //Keeps track of the number of items sold on the marketplace
    Counters.Counter private _itemsSold;
    //owner is the contract address that created the smart contract
    address payable owner;
    //The fee charged by the marketplace to be allowed to list an NFT
    uint256 listPrice = 0.01 ether;

    //The structure to store info about a listed token
    struct ListedToken {
        uint256 tokenId;
        address payable owner;
        address payable seller;
        uint256 price;
        bool currentlyListed;
    }

    //the event emitted when a token is successfully listed
    event TokenListedSuccess (
        uint256 indexed tokenId,
        address owner,
        address seller,
        uint256 price,
        bool currentlyListed
    );

    //This mapping maps tokenId to token info and is helpful when retrieving details about a tokenId
    mapping(uint256 => ListedToken) private idToListedToken;

    constructor() ERC721("NFTMarketplace", "NFTM") {
        owner = payable(msg.sender);
    }

    function updateListPrice(uint256 _listPrice) public payable {
        require(owner == msg.sender, "Only owner can update listing price");
        listPrice = _listPrice;
    }

    function getListPrice() public view returns (uint256) {
        return listPrice;
    }

    function getLatestIdToListedToken() public view returns (ListedToken memory) {
        uint256 currentTokenId = _tokenIds.current();
        return idToListedToken[currentTokenId];
    }

    function getListedTokenForId(uint256 tokenId) public view returns (ListedToken memory) {
        return idToListedToken[tokenId];
    }

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

    //The first time a token is created, it is listed here
    function createToken(string memory tokenURI, uint256 price) public payable returns (uint) {
        //Increment the tokenId counter, which is keeping track of the number of minted NFTs
        _tokenIds.increment();
        uint256 newTokenId = _tokenIds.current();

        //Mint the NFT with tokenId newTokenId to the address who called createToken
        _safeMint(msg.sender, newTokenId);

        //Map the tokenId to the tokenURI (which is an IPFS URL with the NFT metadata)
        _setTokenURI(newTokenId, tokenURI);

        //Helper function to update Global variables and emit an event
        createListedToken(newTokenId, price);

        return newTokenId;
    }

    function createListedToken(uint256 tokenId, uint256 price) private {
        //Make sure the sender sent enough ETH to pay for listing
        require(msg.value == listPrice, "Hopefully sending the correct price");
        //Just sanity check
        require(price > 0, "Make sure the price isn't negative");

        //Update the mapping of tokenId's to Token details, useful for retrieval functions
        idToListedToken[tokenId] = ListedToken(
            tokenId,
            payable(address(this)),
            payable(msg.sender),
            price,
            true
        );

        _transfer(msg.sender, address(this), tokenId);
        //Emit the event for successful transfer. The frontend parses this message and updates the end user
        emit TokenListedSuccess(
            tokenId,
            address(this),
            msg.sender,
            price,
            true
        );
    }
    
    //This will return all the NFTs currently listed to be sold on the marketplace
    function getAllNFTs() public view returns (ListedToken[] memory) {
        uint nftCount = _tokenIds.current();
        ListedToken[] memory tokens = new ListedToken[](nftCount);
        uint currentIndex = 0;

        //at the moment currentlyListed is true for all, if it becomes false in the future we will 
        //filter out currentlyListed == false over here
        for(uint i=0;i<nftCount;i++)
        {
            uint currentId = i + 1;
            ListedToken storage currentItem = idToListedToken[currentId];
            tokens[currentIndex] = currentItem;
            currentIndex += 1;
        }
        //the array 'tokens' has the list of all NFTs in the marketplace
        return tokens;
    }
    
    //Returns all the NFTs that the current user is owner or seller in
    function getMyNFTs() public view returns (ListedToken[] memory) {
        uint totalItemCount = _tokenIds.current();
        uint itemCount = 0;
        uint currentIndex = 0;
        
        //Important to get a count of all the NFTs that belong to the user before we can make an array for them
        for(uint i=0; i < totalItemCount; i++)
        {
            if(idToListedToken[i+1].owner == msg.sender || idToListedToken[i+1].seller == msg.sender){
                itemCount += 1;
            }
        }

        //Once you have the count of relevant NFTs, create an array then store all the NFTs in it
        ListedToken[] memory items = new ListedToken[](itemCount);
        for(uint i=0; i < totalItemCount; i++) {
            if(idToListedToken[i+1].owner == msg.sender || idToListedToken[i+1].seller == msg.sender) {
                uint currentId = i+1;
                ListedToken storage currentItem = idToListedToken[currentId];
                items[currentIndex] = currentItem;
                currentIndex += 1;
            }
        }
        return items;
    }

    function executeSale(uint256 tokenId) public payable {
        uint price = idToListedToken[tokenId].price;
        address seller = idToListedToken[tokenId].seller;
        require(msg.value == price, "Please submit the asking price in order to complete the purchase");

        //update the details of the token
        idToListedToken[tokenId].currentlyListed = true;
        idToListedToken[tokenId].seller = payable(msg.sender);
        _itemsSold.increment();

        //Actually transfer the token to the new owner
        _transfer(address(this), msg.sender, tokenId);
        //approve the marketplace to sell NFTs on your behalf
        approve(address(this), tokenId);

        //Transfer the listing fee to the marketplace creator
        payable(owner).transfer(listPrice);
        //Transfer the proceeds from the sale to the seller of the NFT
        payable(seller).transfer(msg.value);
    }

    //We might add a resell token function in the future
    //In that case, tokens won't be listed by default but users can send a request to actually list a token
    //Currently NFTs are listed by default
}

然后我们打开Terminal,在里面输入 npx hardhat run scripts/deploy.js --network goerli 来部署我们的智能合约到goerli 测试网。

然后我们可以到src文件夹下找到Marketplace.json文件,里面的address就是我们部署的合约地址。然后我们可以通过 https://goerli.etherscan.io/address/合约地址 来查看到我们创建的合约。

然后我们在Termainl里面输入 npm start 把系统跑起来,并且点击Open Browser预览系统。

点击Conenct 连接我们的钱包。

然后你可以点击List My NFT,填写信息,选择一个图片,点击 List NFT,等弹出Metamask窗口,点击确定来上传一个NFT。

上传成功以后,你可以在Profile里面看到创建的NFT。

你也可以进你创建的合约地址(https://goerli.etherscan.io/address/你的合约地址),看到对应的创建NFT信息。

最后我们进入Source Control菜单,填写备注,然后点击Commit右边的小三角,然后点击Commit&Push按钮,把代码提交到我们的GitHub。

问题

由于国内网络变化,如果你碰到如图所示问题,那么你需要重新创建一个 pinata Key。如果你保存了之前Key的JWT的值也可以。

然后在.env文件里添加一行

REACT_APP_PINATA_JWT=你的JWT的token

总体的.env文件就变成如果所示。

然后找到pinata.js文件,把全部内容替换为如下内容

//require('dotenv').config();
const key = process.env.REACT_APP_PINATA_KEY;
const secret = process.env.REACT_APP_PINATA_SECRET;

const axios = require('axios');
const FormData = require('form-data');

export const uploadJSONToIPFS = async(JSONBody) => {
    const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS`;
    //making axios POST request to Pinata ⬇️
    return axios 
        .post(url, JSONBody, {
            headers: {
                // pinata_api_key: key,
                // pinata_secret_api_key: secret,
                authorization: "Bearer "+process.env.REACT_APP_PINATA_JWT
            }
        })
        .then(function (response) {
           return {
               success: true,
               pinataURL: "https://gateway.pinata.cloud/ipfs/" + response.data.IpfsHash
           };
        })
        .catch(function (error) {
            console.log(error)
            return {
                success: false,
                message: error.message,
            }

    });
};

export const uploadFileToIPFS = async(file) => {
    const url = `https://api.pinata.cloud/pinning/pinFileToIPFS`;
    //making axios POST request to Pinata ⬇️
    
    let data = new FormData();
    data.append('file', file);

    const metadata = JSON.stringify({
        name: 'testname',
        keyvalues: {
            exampleKey: 'exampleValue'
        }
    });
    data.append('pinataMetadata', metadata);

    //pinataOptions are optional
    const pinataOptions = JSON.stringify({
        cidVersion: 0,
        customPinPolicy: {
            regions: [
                {
                    id: 'FRA1',
                    desiredReplicationCount: 1
                },
                {
                    id: 'NYC1',
                    desiredReplicationCount: 2
                }
            ]
        }
    });
    data.append('pinataOptions', pinataOptions);

    return axios 
        .post(url, data, {
            maxBodyLength: 'Infinity',
            headers: {
                'Content-Type': `multipart/form-data; boundary=${data._boundary}`,
                // pinata_api_key: key,
                // pinata_secret_api_key: secret,
                authorization: "Bearer "+process.env.REACT_APP_PINATA_JWT
            }
        })
        .then(function (response) {
            console.log("image uploaded", response.data.IpfsHash)
            return {
               success: true,
               pinataURL: "https://gateway.pinata.cloud/ipfs/" + response.data.IpfsHash
           };
        })
        .catch(function (error) {
            console.log(error)
            return {
                success: false,
                message: error.message,
            }

    });
};

提交表单

链接:https://alchemyapi.typeform.com/roadtoweekseven

表单最后填写,项目的Github地址(https://github.com/你的Github名字/NFT-Marketplace-Tutorial)~~和~~你的部署的合约地址(https://goerli.etherscan.io/address/你的合约地址)。

Subscribe to SoullessL
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.