智能合约Smart Contract技术详解


theme: juejin

合约编写

建议读者先了解下solidity,这里推荐CryptoZombies,还是比较详细的。
ok当你大概知道自己在做什么之后,我们就可以开始编写智能合约了,首先我们需要一个编译器,我是用的web ide remix,当然他也有桌面版,使用起来都是一样的,web版本的话记得做备份,如果仅靠浏览器缓存来做备份的话,很容易吃亏找不到代码了等会。

基本介绍

先看几个关键常量

    uint public constant MAX_TOKENS = 2000;
    uint private constant TOKENS_RESERVED = 4;
    
    //normal whitelist price
    uint public white_price = 0.008 ether;
    //normal price
    uint public price = 0.015 ether;

    uint256 public constant MAX_MINT_PER_TX = 4;

    bool public isSaleActive = false;
    uint256 public totalSupply;
    mapping(address => uint256) private mintedPerWallet;

    string public baseUri;
    string public baseExtension = ".json";

MAX_TOKENS指的是该合约最大能mint的数量
white_price指的是白名单价格(如果你的合约有白名单的话),注意这里价格会带上ether关键字后缀,表示每一个nft的单价
price指的是普通价格
MAX_MINT_PER_TX表示一个账户能mint的数量(如果你的合约有这个需求的话)
isSaleActive表示当前合约是否可以mint的状态
mintedPerWallet是一个map,记录了每一个账户mint的数量,对应MAX_MINT_PER_TX.

构造方法

    constructor() ERC721("the smart contract's name", "SYMBOL") {
        baseUri = "ipfs://xxxxxxxxxxxxxxxxx/";
        whiteRootOG = xxxxxxxx;
        whiteRootNormal = xxxxxxxxx;
        // for(uint256 i = 1; i <= TOKENS_RESERVED; ++i) {
        //     _safeMint(msg.sender, i);
        // }
        // totalSupply = TOKENS_RESERVED;
    }

构造方法第一个参数为合约名字,baseUri为ipfs的json地址,白名单稍后会讲解,构造方法这里有些合约会直接构造出来几个给团队自己使用,看自己需求。

ipfs

ipfs全名InterPlanetary File System, 是一个分布式的web,实现了点到点超媒体协议,可以让我们的互联网速度更快,更加安全, 并且更加开放。 理论上的话未来可以取代http。如果我们传上去一个相同的图片,得到的ipfs链接是一样的,所以ipfs比http更能确保文件的安全性,而且由于是p2p的形式去下载,所以下载速度相较http也会快速很多。
ok,简单介绍了下ipfs,那么我们该如何使用呢?
ipfs上传工具目前还是比较多的,我这里建议使用ipfs desktop,像pinata也很方便,但普通用户都有存储限制。
首先我们上传一个包含图片的文件夹以后获取到一个ipfs的cid地址,然后我们就得生成一个json去告诉用户,你的nft的图片,描述,名字等。
类似:

image.png

tips:如果你要查看你的ipfs上传的文件,你可以使用这个链接:https://ipfs.io/ipfs/your-ipfs-cid/
把your-ipfs-cid换成你的文件cid即可。

当然一般nft使用场景里会有很多很多nft,那么这里就需要把生成json文件脚本化比较方便了,其实就是一个string字符串写入生成文件,可以用java,python等,这里就不贴了。

然后刚才生成的json文件夹必须取名为metadata,然后这个metadata文件夹的ipfs cid即是我们合约里要用到的baseUri,当然这个baseUri也是可以动态替换的,这个后面会详解,主要用在一些一开始给到用户的nft是未揭秘,然后解密后的这种场景。

mint

function mint(uint256 _numTokens, bytes32[] calldata whitelist_og_proof, bytes32[] calldata whitelist_normal_proof) external payable {
        require(isSaleActive, "The sale is paused.");
        require(_numTokens <= MAX_MINT_PER_TX, "You cannot mint that many in one transaction.");
        require(mintedPerWallet[msg.sender] + _numTokens <= MAX_MINT_PER_TX, "You cannot mint that many total.");
        uint256 curTotalSupply = totalSupply;
        require(curTotalSupply + _numTokens <= MAX_TOKENS, "Exceeds total supply.");
require(_numTokens * price <= msg.value, "Insufficient funds.");

        for(uint256 i = 1; i <= _numTokens; ++i) {
            _safeMint(msg.sender, curTotalSupply + i);
        }
        mintedPerWallet[msg.sender] += _numTokens;
        totalSupply += _numTokens;
    }

这里注意函数后缀加了一个关键词payable,意思就是这个是支付函数。require方法有点类似其他语言中的捕捉异常,如果条件为false的话,则直接报错,错误信息为后面的string。
那么我们来简单的看下这个mint函数,忽略白名单相关形参,第一行示意如果合约现在状态isSaleActive为false的话,那么现在无法交易。
第二行就是控制交易数量,如果用户申请了超过每个人最大能mint的数量的话,直接报错。
第三行是控制每个人能mint的数量,会去map里去读取每个人mint的数量,不能超哥一个人能mint的最大数量。
下面就是控制最大数量了,如果你发行200个nft,现在已经被mint了199个,这个时候你还要去mint2个话,就会直接报错Exceeds total supply。
当然以上这些情况都需要根据你实际合约的需求去自定义。

然后就是去判断价格,这里要注意一点,_safeMint方法是可以直接去mint的,msg.sender指的是发起该交易的用户的account,所以如果你要去给nft设置价格的话,必须在_safeMint前去做一道价格的关卡来控制价格,如果有白名单等价格不一样的话,这里都要去做价格限制。合约和支付宝、微信支付等不一样的地方就是他的设置价格是在这里进行条件判断设置的。

提现

    function withdrawAll() external payable onlyOwner {
        uint256 balance = address(this).balance;
        uint256 balanceOne = balance * 0.5;
        uint256 balanceTwo = balance * 0.5;
        ( bool transferOne, bool transferTwo ) = payable(msg.sender1, msg.sender2).call{value: balanceOne, balanceTwo}("");
        require(transferOne, "Transfer failed.");
    }

这里注意有一个关键字onlyOwner,意思就是只有创建合约的账号可以调用的方法。这里我们把收益五五分成,分给了msg.sender1和msg.sender2,当然这里也可以改成只给一个正好,这里根据自己的需求来自定义即可。

白名单

在一些nft中,会有一部分用户的mint价格和普通用户的mint价格不一样,所以我们要存下这部分用户的account id,然后如果是这个用户群体的话,那么前面控制价格那里可以针对这些用户进行不一样的价格控制操作。
正常逻辑,我们放一个数组来存放这些account,但是如果智能合约发布以后,我们要去修改这个白名单账号群体的增删改查,如果是一个数组的话,那就很麻烦,而且批量操作如果写的不当的话,就会多出很多gas费,而且数组的话存放空间也会变大,完全不适合这种动态化的case,所以我们只能另外寻找方法来解决这个case。
区块链技术中有一个概念叫做默克尔树,也就是Merkle树。
默克尔树是一种哈希树,其中每个叶子节点都标有数据块的加密哈希值,而每个非叶子节点都标有其子节点的加密哈希值的标签。大多数哈希树的实现是二进制的(每个节点有两个子节点),但它们也可以有更多的子节点。它允许你验证某些数据是否存在于树中,而不需要去轮训啊遍历啊。

image.png

我的理解是这样,有多少数据就有多少叶子结点,叶子结点的数据是该数据的hash值,两个叶子结点会生成对应的父节点,然后以此往上推,会有一个唯一的根结点,数据不同根结点也会不同,所以其实可以根据根节点的hash和叶子结点的hash来类推出这个叶子结点是否是该数据集中。
ok,原理介绍的差不多了,我们来简单介绍下具体该如何实操。

合约

import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

constructor() ERC721("smart contract's name", "SYMBOL") {
baseUri = "ipfs://xxxxxxxxxx/";
whiteRootOG = 0xad8403ee270f9d5d3aae410de98f923e33c6e9c57df0f1c986119fa61192e14c;
//.,.........
}

function isVerifyMerkleNormal(bytes32[] calldata proof) view public returns (bool) {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
    return MerkleProof.verify(proof, whiteRootNormal, leaf);
}

首先你需要import,然后合约的角色是去验证,传入proof,然后生成leaf对象,verify方法会根据root去做校验,这里会在构造方法里先初始化一个root,后面如果有白名单的增删改查的话,只要去修改这个root就行了,如果有修改只需要修改一次gas费即可。

前端

前端这里的流程是这样的,有一个accounts的数组,根据这个数组去生成默克尔树的roots,如果是部署合约的时候,直接把这个roots写进合约里就行了,但如果是增删改查白名单的话,就需要在合约里的write contract方法里去更新这个roots了。
这里注意,如果非数组里的账户生成的proof为空,前端如果要测试的话就可以这么来测试是否为白名单。

npm i -D merkletreejs keccak256

首先我们要npm install需要的插件。

  //生成白名单
  const generateWhiteOGProofs = () => {

    //buffer化叶子结点
    const leafNodes = whitelistAddressesOG.map(addr => keccak256(addr));
    //实例化默克尔树
    const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true });
    setMerkleTreeOG(merkleTree);
    //获取根哈希值
    const rootHash = merkleTree.getRoot();
    console.log(rootHash);
    console.log('Whitelist Merkle Tree og\n', merkleTree.toString());
  }

部署

image.png

选择compile 合约文件,也就是编译检查,编译通过后,我们就可以去做发布操作了。

image.png

我这里选择的是Injected Provider,用的是Goerli测试网络,如果你账号里没有币的话,推荐这里,每天可以提取0.5eth。然后deploy按钮就可以直接付了gas费后直接测试发布。可以在etherscan里直接view。

image.png

这里,合约就部署成功了。

验证合约代码

由于我们先mint了几个,所以opensea上直接可以查看了。这里要特别注意,opensea上json数据出来比较慢,如果你的图片或者视频比较大的话,也可能会出现过了很久也出不来的情况,在这里建议图片或者视频小一点。如果数据没刷出来,可以在opensea上点击refresh data按钮。

image.png

目前合约部署成功,但contract的方法并没有显示。所有如果我们要在etherscan上直接读取合约的一些数据,或者对合约进行了一些修改操作,比如修改价格,修改白名单等,就需要对contract方法进行验证。
可以直接在remix上操作,插件里搜索flattener,点击activate。

image.png

然后直接保存sol文件直接在etherscan里保存进去进行验证即可。

image.png

成功以后我们就可以直接在etherscan上read和write contract了。

前端和合约交互

前端和合约的交互的话主要分为两类,对应contact里的read和write方法,这里我分别以read里的获取已经mint了多少个和mint方法去对应read合约方法和write合约方法为例子。

准备工作

首先我们在remix里目录contracts/artifacts目录下找到对应的合约名字的json文件,在你的前端项目中新建一个contract文件夹,将这个json文件拷贝过来,并且记录下你的合约地址进行替换,我们需要根据这个地址去获取合约地址对象。

npm install ethers

npm下载ethers插件。

import { MerkleTree } from 'merkletreejs';
import { keccak256 } from 'ethers/lib/utils';
import { ethers } from 'ethers';
import { message } from 'antd';

import contract from "./../../../../contracts/NFT.json";

//............

const contractAddress = "your contract address";
const abi = contract.abi;

获取已经mint了的数量

const getTotalSupply = async () => {
    try {
      const { ethereum } = window;

      if (ethereum) {
        const provider = new ethers.providers.Web3Provider(ethereum);
        const signer = provider.getSigner();
        const nftContract = new ethers.Contract(contractAddress, abi, signer);

        //获取总共多少币了
        nftContract.totalSupply().then(c => {
          console.log('已经mint了: ' + parseInt(c));
          setAlreadyMint(parseInt(c));

          if (alreaderMint == maxSale) {
            setStatus('SoleOut');
          }
        });
      }
    } catch (err) {
      console.log(err);
    }
  }

异步方法,获取到nftContract对象后,直接可以调用totalSupply方法即可,totalSupply方法为编写合约的时候写的read方法,如果你要在前端查看价格等其他read方法,道理相同。

mint

const mintNftHandler = async () => {
    try {

      if (currentAccount == null) {
        connectWalletHandler();

        return;
      } else {
        if (minNum != "1" && minNum != "2" && minNum != "3" && minNum != "4") {
          // alert("Up to 5 can be minted");
          message.open({ content: 'Up to 4 can be minted' });
        } else {

          if (alreaderMint == maxSale) {
            message.open({ content: 'Sold Out' });
          }

          const { ethereum } = window;

          if (ethereum) {
            const provider = new ethers.providers.Web3Provider(ethereum);
            const signer = provider.getSigner();
            const nftContract = new ethers.Contract(contractAddress, abi, signer);

            let normalCost = 0;
            if (checkIsWhiteListOG()) {
              if (isCurrentAccountMinted) {
                normalCost = parseInt(minNum) * whitelistOGPriceLast3;
              } else {
                normalCost = whitelistOGPrice + (parseInt(minNum) - 1) * whitelistOGPriceLast3;
              }
            } else if (checkIsWhiteListNormal()) {
              if (isCurrentAccountMinted) {
                normalCost = parseInt(minNum) * whitelistNormalPriceLast3;
              } else {
                normalCost = whitelistNormalPrice + (parseInt(minNum) - 1) * whitelistNormalPriceLast3;
              }
            } else {
              normalCost = parseInt(minNum) * normalPrice;
            }

            const errAddress = keccak256(currentAccount);
            console.log(merkleTreeOG.toString());

            //取得默克尔证明
            const hexProofOG = merkleTreeOG.getHexProof(errAddress);
            const hexProofNormal = merkleTreeNormal.getHexProof(errAddress);
            let nftTxn = await nftContract.mint(minNum, hexProofOG, hexProofNormal,
              { value: ethers.utils.parseEther(normalCost + "") });

            message.open({ content: 'Transaction in progress, Please wait...' });
            await nftTxn.wait();

            message.open({ content: 'mint successful' });
          } else {
            message.open({ content: 'Ethereum object does not exist' });
          }
        }
      }
    } catch (err) {
      console.log(err + "");
    }
  }

注意这是一个异步方法,首先根据用户类型去做判断,需要支付的价格,然后就直接调用nftContract.mint方法直接去mint就可以了,这里参数直接参照合约里的mint方法,加入了一个value参数也就是算好的价格。这里有一个安全性考虑,如果用户在前端代码反编译了以后去修改了价格,因为我们合约里是有做价格保护的,所以会直接报错价格不够。

ok,至此,一个智能合约的基本流程就通了。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容