# NFT入门指南

By [Untitled](https://paragraph.com/@0x1471de55b23dd393568e1b7e211bdc9396fc49f2) · 2022-10-19

---

非同质化通证（Non-Fungible Token，NFT）是一种架构在区块链技术上的，不可复制、篡改、分割的加密数字权益证明，可以理解为 一种去中心化的“虚拟资产或实物资产的数字所有权证书”。 从技术层面来看，NFT以智能合约的形式发行，一份智能合约可以发行一种或多种NFT资产，包括实体收藏品、活动门票等实物资产和图 像、音乐、游戏道具等虚拟资产。

目前市面上使用最广泛、知名度最高的NFT主流协议标准中，ERC721协议下一份合约只能发行一种NFT 资产（如BAYC代币），ERC1155协议支持一份合约发行任意种类的NFT资产（如交易平台Opensea的代币OpenStore）。一种NFT资产 可映射多个NFT（如BAYC代币共发行了一万个），NFT智能合约记录了每个NFT资产的token ID、资源存储地址及它们的各项信息。 NFT储存于区块链上，但受到成本影响，其映射的实物资产或数字资产一般不上链，而是储存于其他中心化或非中心化的存储系统中，如 IPFS，并通过哈希值或URL映射上链。

![](https://storage.googleapis.com/papyrus_images/c4949034a98228a183802666b758adf071705101dee14786fb70984ca84b9d97.png)

![](https://storage.googleapis.com/papyrus_images/f6d3051d267db2171934201c6ad6a57c1d9571e6bcd560a4bf2d6f4a14d38990.png)

价值十亿美金的 200 行代码
===============

码
=

    /**
     *Submitted for verification at Etherscan.io on 2017-07-19
    */
    
    pragma solidity ^0.4.8;
    contract CryptoPunksMarket {
    
        // You can use this hash to verify the image file containing all the punks
        string public imageHash = "ac39af4793119ee46bbff351d8cb6b5f23da60222126add4268e261199a2921b";
    
        address owner;
    
        string public standard = 'CryptoPunks';
        string public name;
        string public symbol;
        uint8 public decimals;
        uint256 public totalSupply;
    
        uint public nextPunkIndexToAssign = 0;
    
        bool public allPunksAssigned = false;
        uint public punksRemainingToAssign = 0;
    
        //mapping (address => uint) public addressToPunkIndex;
        mapping (uint => address) public punkIndexToAddress;
    
        /* This creates an array with all balances */
        mapping (address => uint256) public balanceOf;
    
        struct Offer {
            bool isForSale;
            uint punkIndex;
            address seller;
            uint minValue;          // in ether
            address onlySellTo;     // specify to sell only to a specific person
        }
    
        struct Bid {
            bool hasBid;
            uint punkIndex;
            address bidder;
            uint value;
        }
    
        // A record of punks that are offered for sale at a specific minimum value, and perhaps to a specific person
        mapping (uint => Offer) public punksOfferedForSale;
    
        // A record of the highest punk bid
        mapping (uint => Bid) public punkBids;
    
        mapping (address => uint) public pendingWithdrawals;
    
        event Assign(address indexed to, uint256 punkIndex);
        event Transfer(address indexed from, address indexed to, uint256 value);
        event PunkTransfer(address indexed from, address indexed to, uint256 punkIndex);
        event PunkOffered(uint indexed punkIndex, uint minValue, address indexed toAddress);
        event PunkBidEntered(uint indexed punkIndex, uint value, address indexed fromAddress);
        event PunkBidWithdrawn(uint indexed punkIndex, uint value, address indexed fromAddress);
        event PunkBought(uint indexed punkIndex, uint value, address indexed fromAddress, address indexed toAddress);
        event PunkNoLongerForSale(uint indexed punkIndex);
    
        /* Initializes contract with initial supply tokens to the creator of the contract */
        function CryptoPunksMarket() payable {
            //        balanceOf[msg.sender] = initialSupply;              // Give the creator all initial tokens
            owner = msg.sender;
            totalSupply = 10000;                        // Update total supply
            punksRemainingToAssign = totalSupply;
            name = "CRYPTOPUNKS";                                   // Set the name for display purposes
            symbol = "Ͼ";                               // Set the symbol for display purposes
            decimals = 0;                                       // Amount of decimals for display purposes
        }
    
        function setInitialOwner(address to, uint punkIndex) {
            if (msg.sender != owner) throw;
            if (allPunksAssigned) throw;
            if (punkIndex >= 10000) throw;
            if (punkIndexToAddress[punkIndex] != to) {
                if (punkIndexToAddress[punkIndex] != 0x0) {
                    balanceOf[punkIndexToAddress[punkIndex]]--;
                } else {
                    punksRemainingToAssign--;
                }
                punkIndexToAddress[punkIndex] = to;
                balanceOf[to]++;
                Assign(to, punkIndex);
            }
        }
    
        function setInitialOwners(address[] addresses, uint[] indices) {
            if (msg.sender != owner) throw;
            uint n = addresses.length;
            for (uint i = 0; i < n; i++) {
                setInitialOwner(addresses[i], indices[i]);
            }
        }
    
        function allInitialOwnersAssigned() {
            if (msg.sender != owner) throw;
            allPunksAssigned = true;
        }
    
        function getPunk(uint punkIndex) {
            if (!allPunksAssigned) throw;
            if (punksRemainingToAssign == 0) throw;
            if (punkIndexToAddress[punkIndex] != 0x0) throw;
            if (punkIndex >= 10000) throw;
            punkIndexToAddress[punkIndex] = msg.sender;
            balanceOf[msg.sender]++;
            punksRemainingToAssign--;
            Assign(msg.sender, punkIndex);
        }
    
        // Transfer ownership of a punk to another user without requiring payment
        function transferPunk(address to, uint punkIndex) {
            if (!allPunksAssigned) throw;
            if (punkIndexToAddress[punkIndex] != msg.sender) throw;
            if (punkIndex >= 10000) throw;
            if (punksOfferedForSale[punkIndex].isForSale) {
                punkNoLongerForSale(punkIndex);
            }
            punkIndexToAddress[punkIndex] = to;
            balanceOf[msg.sender]--;
            balanceOf[to]++;
            Transfer(msg.sender, to, 1);
            PunkTransfer(msg.sender, to, punkIndex);
            // Check for the case where there is a bid from the new owner and refund it.
            // Any other bid can stay in place.
            Bid bid = punkBids[punkIndex];
            if (bid.bidder == to) {
                // Kill bid and refund value
                pendingWithdrawals[to] += bid.value;
                punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);
            }
        }
    
        function punkNoLongerForSale(uint punkIndex) {
            if (!allPunksAssigned) throw;
            if (punkIndexToAddress[punkIndex] != msg.sender) throw;
            if (punkIndex >= 10000) throw;
            punksOfferedForSale[punkIndex] = Offer(false, punkIndex, msg.sender, 0, 0x0);
            PunkNoLongerForSale(punkIndex);
        }
    
        function offerPunkForSale(uint punkIndex, uint minSalePriceInWei) {
            if (!allPunksAssigned) throw;
            if (punkIndexToAddress[punkIndex] != msg.sender) throw;
            if (punkIndex >= 10000) throw;
            punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, 0x0);
            PunkOffered(punkIndex, minSalePriceInWei, 0x0);
        }
    
        function offerPunkForSaleToAddress(uint punkIndex, uint minSalePriceInWei, address toAddress) {
            if (!allPunksAssigned) throw;
            if (punkIndexToAddress[punkIndex] != msg.sender) throw;
            if (punkIndex >= 10000) throw;
            punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, toAddress);
            PunkOffered(punkIndex, minSalePriceInWei, toAddress);
        }
    
        function buyPunk(uint punkIndex) payable {
            if (!allPunksAssigned) throw;
            Offer offer = punksOfferedForSale[punkIndex];
            if (punkIndex >= 10000) throw;
            if (!offer.isForSale) throw;                // punk not actually for sale
            if (offer.onlySellTo != 0x0 && offer.onlySellTo != msg.sender) throw;  // punk not supposed to be sold to this user
            if (msg.value < offer.minValue) throw;      // Didn't send enough ETH
            if (offer.seller != punkIndexToAddress[punkIndex]) throw; // Seller no longer owner of punk
    
            address seller = offer.seller;
    
            punkIndexToAddress[punkIndex] = msg.sender;
            balanceOf[seller]--;
            balanceOf[msg.sender]++;
            Transfer(seller, msg.sender, 1);
    
            punkNoLongerForSale(punkIndex);
            pendingWithdrawals[seller] += msg.value;
            PunkBought(punkIndex, msg.value, seller, msg.sender);
    
            // Check for the case where there is a bid from the new owner and refund it.
            // Any other bid can stay in place.
            Bid bid = punkBids[punkIndex];
            if (bid.bidder == msg.sender) {
                // Kill bid and refund value
                pendingWithdrawals[msg.sender] += bid.value;
                punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);
            }
        }
    
        function withdraw() {
            if (!allPunksAssigned) throw;
            uint amount = pendingWithdrawals[msg.sender];
            // Remember to zero the pending refund before
            // sending to prevent re-entrancy attacks
            pendingWithdrawals[msg.sender] = 0;
            msg.sender.transfer(amount);
        }
    
        function enterBidForPunk(uint punkIndex) payable {
            if (punkIndex >= 10000) throw;
            if (!allPunksAssigned) throw;                
            if (punkIndexToAddress[punkIndex] == 0x0) throw;
            if (punkIndexToAddress[punkIndex] == msg.sender) throw;
            if (msg.value == 0) throw;
            Bid existing = punkBids[punkIndex];
            if (msg.value <= existing.value) throw;
            if (existing.value > 0) {
                // Refund the failing bid
                pendingWithdrawals[existing.bidder] += existing.value;
            }
            punkBids[punkIndex] = Bid(true, punkIndex, msg.sender, msg.value);
            PunkBidEntered(punkIndex, msg.value, msg.sender);
        }
    
        function acceptBidForPunk(uint punkIndex, uint minPrice) {
            if (punkIndex >= 10000) throw;
            if (!allPunksAssigned) throw;                
            if (punkIndexToAddress[punkIndex] != msg.sender) throw;
            address seller = msg.sender;
            Bid bid = punkBids[punkIndex];
            if (bid.value == 0) throw;
            if (bid.value < minPrice) throw;
    
            punkIndexToAddress[punkIndex] = bid.bidder;
            balanceOf[seller]--;
            balanceOf[bid.bidder]++;
            Transfer(seller, bid.bidder, 1);
    
            punksOfferedForSale[punkIndex] = Offer(false, punkIndex, bid.bidder, 0, 0x0);
            uint amount = bid.value;
            punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);
            pendingWithdrawals[seller] += amount;
            PunkBought(punkIndex, bid.value, seller, bid.bidder);
        }
    
        function withdrawBidForPunk(uint punkIndex) {
            if (punkIndex >= 10000) throw;
            if (!allPunksAssigned) throw;                
            if (punkIndexToAddress[punkIndex] == 0x0) throw;
            if (punkIndexToAddress[punkIndex] == msg.sender) throw;
            Bid bid = punkBids[punkIndex];
            if (bid.bidder != msg.sender) throw;
            PunkBidWithdrawn(punkIndex, bid.value, msg.sender);
            uint amount = bid.value;
            punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0);
            // Refund the bid money
            msg.sender.transfer(amount);
        }
    
    }
    

而上面这段代码就是大名鼎鼎的 NFT [CryptoPunks](https://opensea.io/collection/cryptopunks?tab=activity) 的合约代码。

CryptoPunks 是啥？就是下面这一组图片，总市值超10亿美金。

项目方也是赚得盆满锅满的，目前合约上的ETH价值1亿美金

NFT怎么玩
======

mint - 一级市场
-----------

mint的意思是nft上链，或者更简单的理解就是生产出这个nft。目前mint这个过程项目方是直接对接到用户的，他们会为项目建一个官方网站，然后在网站上提供mint的按钮，用户在当前页面链接钱包之后，点击mint 按钮就会生成nft到自己的账户里面。

比如最近话题度比较高的NBA nft项目的官网[https://theassociationnft.com/](https://theassociationnft.com/)

![](https://storage.googleapis.com/papyrus_images/a8984a6704743df52a82310cdce8df61e5247b20fdf2ae3fc011fa3a870a4e17.png)

当然不是所有的项目都是free mint的，mint是否收费要看项目方了。还有一种是空投，项目方直接mint到你的账号里面，这种一般都是项目成员或者大V了。

mint目前比较主流的都有2轮，第一轮是白名单用户mint，第二轮是全员均可mint。白名单用户mint既有确定性，又可以避免gas war，所以但凡有点流量的项目白名单的申请都非常火爆。如何成为白名单用户就有各种玩法了，什么唱歌、跳舞、写感想、聊天啥都有，所有也派生出了一个玩法：炒白名单，甚至还有打白工作室。

mint nft之后还有一个玩法就是开彩蛋，还是用NBA项目举例，你mint下来的nft在项目方约定的时间之前，只是个纯盲盒，每个盲盒里面对应的图片是什么，原则上只有项目方知道，而这个时候其实NFT也可以在二级市场上买卖，所以炒盲盒也是一种玩法。（技术层面怎么实现的后文会讲）

![](https://storage.googleapis.com/papyrus_images/418f8d6072a4fe01e4e757475fd4a1e81e7864db6cd180be996b0b2c4bde3b07.png)

盲盒开出来之后，每个nft都不一样，那价格高低如何界定？目前主流的是按属性稀有度来估价。我们以[azuki](https://opensea.io/collection/azuki)项目来举例说明下，实际每个NFT本身是有很多属性（properties），而属性的稀有度就直接影响NFT的价格。

![](https://storage.googleapis.com/papyrus_images/dc4d1ecd92076ccd284c657d08e0371f9ad951b2de1d963c741595168d906bc0.png)

比如Type下属性为 spirit的肯定价格就高，因为有这个属性的只有97个nft

![](https://storage.googleapis.com/papyrus_images/2a563d919c03e9f5284092358b17d4b1226d40e118ab357abb4f2081a83e2761.png)

buy - 二级市场
----------

当所有nft都mint结束后，就只有二级市场可以买卖了，目前最大的 NFT 二级交易市场就是[https://opensea.io/](https://opensea.io/)。原则上就是buy now就完了。

![](https://storage.googleapis.com/papyrus_images/085d3fb00fbef8bf1ace47bdc8053519e368addd755a8afaeafe835a933dae80.png)

NFT玩法中的不同角色 在实际的操作中，NFT玩法又可以细分为以下4种角色，各种在玩法里面操作和获利各不相同。当然食物链的顶端是项目方，食物链的最低端就是普通散户玩家。

做散户
---

散户玩NFT其实劣势是比较多的，但是是不是就一定不能玩，我觉得也不是，核心还是要自己能分析项目，自己能估值（但最大的问题就在这里，NFT目前估值非常难，最简单的锚定法基本没法操作）那其实也可以玩。

散户的玩法2种，一种是玩一级市场，一种是玩二级市场。

玩一级市场成本最低，主要就是几点：

*   关注大ip的发售信息，什么是大ip这个问题其实是有一定门槛，比如村上隆（就是那个花的项目），如果不是潮流圈的人可能不知道，但是其实这个ip是非常牛逼的。
    
*   积极参与项目方的活动，争取到白名单机会
    
*   如果争取不到，那就蹲点刷公开mint
    
*   盲盒时第一波收割其实就可以
    
*   如果说对项目信心非常足，而且想赌一波大的，可以等盲盒开出来再卖
    

玩二级市场：

*   如果二级市场再去收NFT，价格原则上肯定是涨了一波的，到底项目值不值得买，这个有个非常重要的指标，项目fomo的程度。这个就跟短线炒股差不多了，有个建议的参考指数，含鲸量（就一些大户是否持有），以及社区的讨论度，但这个也不能完全说明后续涨跌，这个对于市场的把控还是非常考验能力的。
    

做项目方
----

项目方这块我主要就讲讲技术角度的东西，实操下如何发一组NFT。

先讲几个基础知识点：

### IPFS

虽然一个NFT原则上对应一个图片，但是图片本身并不是存在链上，而且存在[IPFS](https://ipfs.io/)上，然后通过合约代码将每个token与对应的图片链接进行关联来保证一一对应的关系。

为什么用IPFS，而不用HTTP直接存？核心在于HTTP服务器的所有者，可以在链接不变的情况下，随意更换内容，而且作为中心化服务，其他人也无从查证和监督。而IPFS本质是分布式存储，没有人可以在不破坏链接的情况下替换或更改内容。但NFT项目方就不能改IPFS地址了吗？其实也可以改，只是只要改了就有记录，这就是区块链的价值所在，当然通过改合约的IPFS地址本质就是前文的开盲盒玩法的技术方案。

### Metadata

前文提到的NFT属性以及它的所有参数，在技术层面上对应的是一个MetaData的协议，结构是JSON的。这个JSON文件也存储在IPFS上。

### solidity

这是专为智能合约而生的语言，具体写法就不讲了，有兴趣的直接看[文档](https://solidity-cn.readthedocs.io/zh/develop/)

### hardhat

Hardhat是一个便于在以太坊上进行构建的开发环境。它帮助开发人员管理和自动化构建智能合约和dApp的过程中固有的重复任务，以及轻松地围绕此工作流程引入更多功能，并且内置了开发专用以太坊网络，这意味着从根本上进行编译和测试。

### 测试网准备工作

首先是有个[小狐狸（metamask）](https://metamask.io/)账号，这个自己先官网下个小狐狸的chrome插件，然后按流程注册个账号既可。这是你进入币圈的通行证，没有就赶紧注册一个。

因为是测试网，所以ETH是可以随便领的，因为后面合约部署是需要钱的，我们需要先准备一点ETH，打开[https://faucets.chain.link/rinkeby](https://faucets.chain.link/rinkeby)，链接自己的钱包就可以领钱了。后续我们所有的操作都是在Rinkeby测试网进行。

### 开始code

创建一个文件夹

    mkdir nft-tutorial
    cd nft-tutorial
    

确认安装了npm和node，前端应该很熟悉这个

    npm init
    

生成默认的package.json配置

修改package.json配置，并安装依赖

    {
      "name": "nft-tutorial",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@nomiclabs/hardhat-ethers": "^2.0.5",
        "@nomiclabs/hardhat-etherscan": "^3.0.3",
        "ethers": "^5.6.4",
        "hardhat": "^2.9.3",
        "node-fetch": "^2.6.7"
      },
      "dependencies": {
        "@openzeppelin/contracts": "^4.5.0",
        "dotenv": "^16.0.0"
      }
    }
    

安装Hardhat

    > npx hardhat
    888    888                      888 888               888
    888    888                      888 888               888
    888    888                      888 888               888
    8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
    888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
    888    888 .d888888 888    888  888 888  888 .d888888 888
    888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
    888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888
    
    👷 Welcome to Hardhat v2.7.0 👷‍
    
    ? What do you want to do? …
      Create a basic sample project
      Create an advanced sample project
      Create an advanced sample project that uses TypeScript
    ❯ Create an empty hardhat.config.js
      Quit
      
      ...
      
      ✨ Config file created ✨
    

写核心合约代码，[openzeppelin](https://openzeppelin.com/)就是一堆的协议库，NFT对应的协议就是ERC721，里面代码对于NFT的实现已经做好了封装，我们只需要在上面再添加我们自己的逻辑就可以了。因为智能合约的代码执行就要烧钱，对代码的要求是非常高的，一般都建议用成熟的方案和依赖，非必要不要自己乱造。

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
    import "@openzeppelin/contracts/utils/Counters.sol";
    
    contract NFT is ERC721 {
      using Counters for Counters.Counter;
    
      Counters.Counter private currentTokenId;
    
      /// @dev Base token URI used as a prefix by tokenURI().
      string public baseTokenURI;
    
      constructor() ERC721("NFTTutorial", "NFT") {
        baseTokenURI = "";
      }
    
      function mintTo(address recipient) public returns (uint256) {
        currentTokenId.increment();
        uint256 newItemId = currentTokenId.current();
        _safeMint(recipient, newItemId);
        return newItemId;
      }
    
      /// @dev Returns an URI for a given token ID
      function _baseURI() internal view virtual override returns (string memory) {
        return baseTokenURI;
      }
    
      /// @dev Sets the base token URI prefix.
      function setBaseTokenURI(string memory _baseTokenURI) public {
        baseTokenURI = _baseTokenURI;
      }
    }
    

修改 hardhat.config.js ，配置一些本地命令行操作

    /**
    * @type import('hardhat/config').HardhatUserConfig
    */
    
    require('dotenv').config();
    require("@nomiclabs/hardhat-ethers");
    require("./scripts/deploy.js");
    require("./scripts/mint.js");
    
    const { ALCHEMY_KEY, ACCOUNT_PRIVATE_KEY } = process.env;
    
    module.exports = {
       solidity: "0.8.1",
       defaultNetwork: "rinkeby",
       networks: {
        hardhat: {},
        rinkeby: {
          url: `https://eth-rinkeby.alchemyapi.io/v2/${ALCHEMY_KEY}`,
          accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
        },
        ethereum: {
          chainId: 1,
          url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`,
          accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
        },
      },
    }
    

同时新增相关的3个script文件

    const { task } = require("hardhat/config");
    const { getAccount } = require("./helpers");
    
    
    task("check-balance", "Prints out the balance of your account").setAction(async function (taskArguments, hre) {
        const account = getAccount();
        console.log(`Account balance for ${account.address}: ${await account.getBalance()}`);
    });
    
    task("deploy", "Deploys the NFT.sol contract").setAction(async function (taskArguments, hre) {
        const nftContractFactory = await hre.ethers.getContractFactory("NFT", getAccount());
        const nft = await nftContractFactory.deploy();
        console.log(`Contract deployed to address: ${nft.address}`);
    });
    

    const { ethers } = require("ethers");
    const { getContractAt } = require("@nomiclabs/hardhat-ethers/internal/helpers");
    
    
    // Helper method for fetching environment variables from .env
    function getEnvVariable(key, defaultValue) {
        if (process.env[key]) {
            return process.env[key];
        }
        if (!defaultValue) {
            throw `${key} is not defined and no default value was provided`;
        }
        return defaultValue;
    }
    
    // Helper method for fetching a connection provider to the Ethereum network
    function getProvider() {
        return ethers.getDefaultProvider(getEnvVariable("NETWORK", "rinkeby"), {
            alchemy: getEnvVariable("ALCHEMY_KEY"),
        });
    }
    
    // Helper method for fetching a wallet account using an environment variable for the PK
    function getAccount() {
        return new ethers.Wallet(getEnvVariable("ACCOUNT_PRIVATE_KEY"), getProvider());
    }
    
    // Helper method for fetching a contract instance at a given address
    function getContract(contractName, hre) {
        const account = getAccount();
        return getContractAt(hre, contractName, getEnvVariable("NFT_CONTRACT_ADDRESS"), account);
    }
    
    module.exports = {
        getEnvVariable,
        getProvider,
        getAccount,
        getContract,
    }
    

    const { task } = require("hardhat/config");
    const { getContract } = require("./helpers");
    const fetch = require("node-fetch");
    
    task("mint", "Mints from the NFT contract")
    .addParam("address", "The address to receive a token")
    .setAction(async function (taskArguments, hre) {
        const contract = await getContract("NFT", hre);
        const transactionResponse = await contract.mintTo(taskArguments.address, {
            gasLimit: 500_000,
        });
        console.log(`Transaction Hash: ${transactionResponse.hash}`);
    });
    
    task("set-base-token-uri", "Sets the base token URI for the deployed smart contract")
    .addParam("baseUrl", "The base of the tokenURI endpoint to set")
    .setAction(async function (taskArguments, hre) {
        const contract = await getContract("NFT", hre);
        const transactionResponse = await contract.setBaseTokenURI(taskArguments.baseUrl, {
            gasLimit: 500_000,
        });
        console.log(`Transaction Hash: ${transactionResponse.hash}`);
    });
    
    
    task("token-uri", "Fetches the token metadata for the given token ID")
    .addParam("tokenId", "The tokenID to fetch metadata for")
    .setAction(async function (taskArguments, hre) {
        const contract = await getContract("NFT", hre);
        const response = await contract.tokenURI(taskArguments.tokenId, {
            gasLimit: 500_000,
        });
        
        const metadata_url = response;
        console.log(`Metadata URL: ${metadata_url}`);
    
        const metadata = await fetch(metadata_url).then(res => res.json());
        console.log(`Metadata fetch response: ${JSON.stringify(metadata, null, 2)}`);
    });
    

添加 .env 文件，存一些关键参数

    ALCHEMY_KEY = "" --alchemy api key
    ACCOUNT_PRIVATE_KEY = "" --你小狐狸账号的私钥
    NETWORK="rinkeby"
    NFT_CONTRACT_ADDRESS=""  --合约部署之后再添加
    

[alchemy](https://www.alchemy.com/) 相当于区块链世界的亚马逊云服务，用它可以降低我们的部署和开发的成本。

到这为止，其实我们的合约代码已经写完了。

![](https://storage.googleapis.com/papyrus_images/48d814af0cf9aaf6ad71c36191f8bd4c338b549c838a09f5a891e3272ea4027d.png)

### 上传IPFS

准备几张图片，都可以

    npx ipfs-car --pack images --output images.car
    

打开[nft.storage](https://nft.storage/)，注册完成，把.car文件传到上面

![](https://storage.googleapis.com/papyrus_images/5afd98c852c4be764f9cbbfa20243e826fc23ee15c38fc4fcaa030ec1d9aaa9b.png)

![](https://storage.googleapis.com/papyrus_images/51a6d73d12a5164f6dfdf15cfff8f144f948510631fa2e27d6550cae63559239.png)

![](https://storage.googleapis.com/papyrus_images/fa9f673b21b2eda9cd6a8092c1f3958aac7980227fbbf5c57a47b46918f67516.png)

![](https://storage.googleapis.com/papyrus_images/c75f84c023996c2fecd58d926d52152a04d6c728d0567e5e7578ddcba3e86bf1.png)

写好与图片一一对应的metadata，链接换成生成的IPFS链接

![](https://storage.googleapis.com/papyrus_images/d78f6d61cc1f80fd5c9661cec8a38008e6ee114745976c005eef1a2cdb49c9bc.png)

    {
       "attributes" : [
          {
             "trait_type" : "level",
             "value" : 3
          },
          {
             "trait_type" : "stamina",
             "value" : 11.7
          },
          {
             "trait_type" : "personality",
             "value" : "sleepy"
          },
          {
             "display_type" : "boost_number",
             "trait_type" : "aqua_power",
             "value" : 30
          },
          {
             "display_type" : "boost_percentage",
             "trait_type" : "stamina_increase",
             "value" : 15
          },
          {
             "display_type" : "number",
             "trait_type" : "generation",
             "value" : 1
          }
       ],
       "description" : "Friendly OpenSea Creature that enjoys long swims in the ocean.",
       "external_url" : "https://example.com/?token_id=2",
       "image" : "https://bafybeiddnebsbc3hfhdvoxp4m7qi4gdwunivy7xxg2euszecgraapnbnwm.ipfs.nftstorage.link/images/2.png",
       "name" : "OolongTea Boris McCoy"
    }
    

    npx ipfs-car --pack metadata --output metadata.car
    

也上传到[nft.storage](https://nft.storage/)，这个 metadata.car 链接地址后面要用

### 合约上线

    > npx hardhat compile
    Compiling 11 files with 0.8.0
    Compilation finished successfully
    
    > npx hardhat deploy
    Contract deployed to address: 0x3d87Da4E9be05D1559694C758001438aebE0B32C
    
    
    Set your NFT_CONTRACT_ADDRESS env variable and source your .env file
    > . .env
    > npx hardhat set-base-token-uri --base-url "https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/"
    Transaction Hash: 0xdd9a44d4131ee48db493d0484b0b294bff8135b678fa0e029677d04e3683eca8
    
    > npx hardhat mint --address 0xb9720BE63Ea8896956A06d2dEd491De125fD705E
    Transaction Hash: 0x99b09300643f6e95563a6f54b94366f79daef835969a53933b1dc43208341a18
    
    > npx hardhat mint --address 0xb9720BE63Ea8896956A06d2dEd491De125fD705E
    Transaction Hash: 0x661c48b2e306210fde28765786116a6dc4f955f2d44f09dee2ed66f9c9c4f79a
    
    > npx hardhat token-uri --token-id 1
    Metadata URL: https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/1
    Metadata fetch response: {
      "description": "Friendly OpenSea Creature that enjoys long swims in the ocean.",
      "external_url": "https://example.com/?token_id=1",
      "image": "https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/1.png",
      "name": "Sprinkles Fisherton"
    }
    
    > npx hardhat token-uri --token-id 2
    Metadata URL: https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/2
    Metadata fetch response: {
      "description": "Friendly OpenSea Creature that enjoys long swims in the ocean.",
      "external_url": "https://example.com/?token_id=1",
      "image": "https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/2.png",
      "name": "Boris McCoy"
    }
    

打开o

也上传到nft.storage，这个 metadata.car 链接地址后面要用 合约上线

> npx hardhat compile Compiling 11 files with 0.8.0 Compilation finished successfully

> npx hardhat deploy Contract deployed to address: 0x3d87Da4E9be05D1559694C758001438aebE0B32C

Set your NFT\_CONTRACT\_ADDRESS env variable and source your .env file

> . .env npx hardhat set-base-token-uri --base-url "[https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/](https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/)" Transaction Hash: 0xdd9a44d4131ee48db493d0484b0b294bff8135b678fa0e029677d04e3683eca8

> npx hardhat mint --address 0xb9720BE63Ea8896956A06d2dEd491De125fD705E Transaction Hash: 0x99b09300643f6e95563a6f54b94366f79daef835969a53933b1dc43208341a18

> npx hardhat mint --address 0xb9720BE63Ea8896956A06d2dEd491De125fD705E Transaction Hash: 0x661c48b2e306210fde28765786116a6dc4f955f2d44f09dee2ed66f9c9c4f79a

> npx hardhat token-uri --token-id 1 Metadata URL: [https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/1](https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/1) Metadata fetch response: { "description": "Friendly OpenSea Creature that enjoys long swims in the ocean.", "external\_url": "[https://example.com/?token\_id=1](https://example.com/?token_id=1)", "image": "[https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/1.png](https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/1.png)", "name": "Sprinkles Fisherton" }

> npx hardhat token-uri --token-id 2 Metadata URL: [https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/2](https://bafybeif6iuokmmcuwj7jgscybx3gvlcwkb6ybspwcduivl7mbqmgmmxubi.ipfs.dweb.link/metadata/2) Metadata fetch response: { "description": "Friendly OpenSea Creature that enjoys long swims in the ocean.", "external\_url": "[https://example.com/?token\_id=1](https://example.com/?token_id=1)", "image": "[https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/2.png](https://bafybeihslhol5draa26unhhe7j2crwedr4tyfrvmba5qt3kyxbvb5olk4i.ipfs.dweb.link/images/2.png)", "name": "Boris McCoy" } 打开opensea就可以看到我们mint了的nft了 [https://testnets.opensea.io/assets/0xCfF29CA8a2B8302Cd873b1ACFa1D6e3BC9516cf1/1](https://testnets.opensea.io/assets/0xCfF29CA8a2B8302Cd873b1ACFa1D6e3BC9516cf1/1) 也可以在以太坊浏览器上面看 [https://rinkeby.etherscan.io/address/0xcff29ca8a2b8302cd873b1acfa1d6e3bc9516cf1](https://rinkeby.etherscan.io/address/0xcff29ca8a2b8302cd873b1acfa1d6e3bc9516cf1) 我们可以在etherscan查看任何链上信息，同时也可以进行读写操作，所以这个地方有个财富密码就是直接绕过NFT项目方官网，在etherscan直接进行mint，可以快于在官网mint的的人。
> 
> 打开opensea就可以看到我们mint了的nft了
> 
> [https://testnets.opensea.io/assets/0xCfF29CA8a2B8302Cd873b1ACFa1D6e3BC9516cf1/1](https://testnets.opensea.io/assets/0xCfF29CA8a2B8302Cd873b1ACFa1D6e3BC9516cf1/1)
> 
> 也可以在以太坊浏览器上面看
> 
> [https://rinkeby.etherscan.io/address/0xcff29ca8a2b8302cd873b1acfa1d6e3bc9516cf1](https://rinkeby.etherscan.io/address/0xcff29ca8a2b8302cd873b1acfa1d6e3bc9516cf1)
> 
> 我们可以在etherscan查看任何链上信息，同时也可以进行读写操作，所以这个地方有个财富密码就是直接绕过NFT项目方官网，在etherscan直接进行mint，可以快于在官网mint的的人。

---

*Originally published on [Untitled](https://paragraph.com/@0x1471de55b23dd393568e1b7e211bdc9396fc49f2/nft)*
