Build NFT

NFT metadata

NFT 有很多属性,名称(可能包括序列号)、描述、图片(包括音视频)、一些额外属性(在一些游戏的NFT中,可能有角色的力量值属性值等),那么这些属性都是怎么来的呢?

{
  "name": "Hassan #1",
  "description": "Hassan Calloway is heir to Calloway Arms, one of the world's leading mil-tech suppliers. Ingenious and determined, Hassan has been searching for his missing father, and for SHRAP, since Earthfall.",
  "image": "https://raw.githubusercontent.com/kasoqian/HassanNFT/main/metadata/images/4.png",
  "attributes": [
    {
      "trait_type": "health",
      "value": "9"
    },
    {
      "trait_type": "power",
      "value": "68"
    },
    {
      "trait_type": "magic",
      "value": "16"
    },
    {
      "trait_type": "exp",
      "value": "93"
    },
    {
      "trait_type": "lv",
      "value": "R"
    }
  ]
}

这个 json 字符串就是 NFT 的元数据,其中包含 NFT 的名称、描述、图片、属性等。

除了 NFT 有这些属性,在 NFT 铸造合约还有着合约属性,即NFT集合属性

{
  "name": "Kitaro World",
  "description": "Kitaro World is a collaborative community storytelling experience.",
  "image": "https://img1.baidu.com/it/u=1624676498,3671459173&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500",
  "external_link": "baidu.com",
  "seller_fee_basis_points": 1000, 
  "fee_recipient": "0xDc8611873fab19209f8845714a02eD17e287aB7C"
}

metadata 具体可以参考metadata

NFT mint

一个 NFT 合约本质就是两个映射,一个是 tokenId 到 tokenURI(上文提到的json元数据的链接) 的映射,另一个是 address 到 tokenId 的映射。

前者指定了一个 NFT 的元数据,后者是该 NFT 的拥有者。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract MerkleTreeWhiteList is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("SmartDog", "dog") {}

    function safeMint(address to, string memory tokenURI) public returns (uint256) {
        // 生成 tokenId, 一个tokenId 唯一指定一个 NFT
        uint256 tokenId = _tokenIds.current();
        // 映射 tokenId 和 address
        _mint(to,tokenId);
        // 映射 tokenId 和 tokenURI
        _setTokenURI(tokenId,tokenURI);

        _tokenIds.increment();
        return tokenId;
    }

}

这样我们就完成了一个 NFT 的 mint。

白名单

白名单就是 NFT 铸造合约中,只有白名单中的地址可以 mint NFT。 一个简单的白名单一个map就可以实现。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract MerkleTreeWhiteList is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    mapping(address => bool) public whiteLists;

    constructor() ERC721("SmartDog", "dog") {}

    // 录入白名单
    function setWhiteLists(address _userAddress,bool _whiteState) public {
        whiteLists[_userAddress] = _whiteState;
    }

    function safeMint(address to, string memory tokenURI) public returns (uint256) {
        // 放心白名单address
        require(whiteLists[to], "Not whiteLists");
        // 生成 tokenId, 一个tokenId 唯一指定一个 NFT
        uint256 tokenId = _tokenIds.current();
        // 映射 tokenId 和 address
        _mint(to,tokenId);
        // 映射 tokenId 和 tokenURI
        _setTokenURI(tokenId,tokenURI);

        _tokenIds.increment();
        return tokenId;
    }

}

不过这种录入白名单方式花费的手续费过于高了,即使是把入参改成地址数组,也没有节省多少手续费。

NFT Merkle Tree 白名单

gas 费和什么有关呢?简单的理解就是合约的复杂度和数据的复杂度,当我们的 mapping 不断增长时 gas 费会攀升!那我们的解决方法就出来了:

  • 降低数据复杂度;
  • 降低合约复杂度;

merkle tree 通过对一组庞大的数据进行解析,生成唯一hash,然后通过少量的参数校验用户地址是否在白名单里,从而实现从大量变成少量的数据操作,也就实现了省 gas 费。 如何通过 merkle tree 校验白名单? 核心需要3个数据

  • root,由大量的白名单生成的一个 hash 值
  • proof,一个数组,辅助验证的节点树
  • userAddress,用户的地址
sequenceDiagram
前端->>前端:①根据【用户地址】和【白名单】生成【proof】和【root】
前端->>合约:②携带【proof值】和【用户地址】请求合约
activate 合约
合约->>合约:③之前录入的【root】值
合约->>合约:④拿到前端传过来的【proof】,【useraddress】进行校验
合约->>合约:⑤校验完毕,返回true或者false,接着跑流程
合约->>前端:⑥将处理结果返回给前端
deactivate 合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
// openzeppelin 提供的校验工具
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract MerkleTreeWhiteList is ERC721, Ownable {
    using Counters for Counters.Counter;

    // 保存 MerkleProof 生成的白名单
    bytes32 public root;

    Counters.Counter private _tokenIdCounter;

    constructor(bytes32 _root) ERC721("MerkleTreeWhiteList", "MTWL") {
        // deploy 时候传入 Merkle Root
        root = _root;
    }
    
    // 校验方法,传入两个数据,proof = 证明数据、lear = 地址
    function isValid(bytes32[] memory proof, bytes32 leaf) public view returns (bool) {
        // 返回是否验证成功
        return MerkleProof.verify(proof, root, leaf);
    }
    
    function safeMint(address to, bytes32[] memory proof) public onlyOwner {
      // 校验 msg.sender 是否在白名单
        require(isValid(proof, keccak256(abi.encodePacked(msg.sender))), "Not a part of Allowlist");
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }

    function setRoot(bytes32 _root) public onlyOwner {
        root = _root;
    }

}

付费 mint 和 提现

核心点:

  • address payable 当传入地址前加上了payable,此时该账号即可接收代币。
  • function f() payable 当调用方法上加了 payable,此时该方法可以接收代币。

转账监听: 当合约接收到 ETH 时,将触发以下两种回调其中一个。

  • receive,当msg.data不存在时;
  • fallback,当 msg.data 存在时。

转账核心有3种方式 addr.transfer, addr.send,addr.call,其中 addr.call为迭代后新增的方法,建议仅使用 addr.call,作为唯一转账方法。

//SPDX-License-Identifier:MIT
pragma solidity ^0.8.4;

contract Transfer{

    string public name = "kasoqian";//监听反馈
    string public name2 = "kasoqian";//监听反馈

    //构建时可携带token存入
    constructor() payable{}

    //收到eth事件,记录amount和gas
    event Log(uint amount,uint gas);
    event fallbackCalled(address sender,uint amount,bytes data);

    //监听,接收eth,data不为空时被触发
    receive() external payable{
        name = "receive listener is working!!!";
        emit Log(msg.value,gasleft());
    }

    //监听,接收eth,data为空时被触发
    fallback() external payable{
        name2 = "fallback listener is working!!!";
        emit fallbackCalled(msg.sender,msg.value,msg.data);
    }

    // 查余额
    function getBalance() view public returns(uint){
        return address(this).balance;
    }

    // 转账
    function transferETH(address payable reciver, uint256 amount)external payable{
        //1.reciver.call{value:amount}("6666666");将会返回bool,data此处用=接收转账返回的bool值
        //2.666为本次交易的补充信息,可以要可不要
        //3.此处amount单位为wei
        (bool success,)= reciver.call{value:amount}("");
        //若不成功则报错
        require(success,"Contract execution Failed");
    }
}

具体到我们的 NFT 合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract MerkleTreeWhiteList is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("SmartDog", "dog") {}

  
    // 加入 payable ,合约账户就能接收金额了
    function safeMint(address to, string memory tokenURI) public payable  returns (uint256) {
        require(msg.value >= 1 ether,"eth not enough!");
        uint256 tokenId = _tokenIds.current();
        _mint(to,tokenId);
        _setTokenURI(tokenId,tokenURI);

        _tokenIds.increment();
        return tokenId;
    }

    // 加入 payable ,被修饰的账户就能转账了
    function withdraw(address payable owner) public  {
        (bool success,)= owner.call{value: address(this).balance}("");
    }

}

如何生成简单的合约随机数

合约

预言机 chainLink

视频课