SoullessL

Posted on Aug 16, 2022Read on Mirror.xyz

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/你的合约地址)。

NFTWeb3