当前位置:   article > 正文

编写带版税的 NFT 智能合约(让我们写一个 ERC-2981合约)_erc2981

erc2981


原文链接:https://trufflesuite.com/guides/nft-royalty/#lets-write-an-erc-2981

概述

本文我们将简要介绍什么是以太坊改进提案(EIP)和以太坊意见征求稿(ERC),以及它们是如何使用的,实现一个 ERC-2981标准的 NFT 版税标准的合约案例。

什么是 EIP(Ethereum Improvement Proposals)?

官网:https://eips.ethereum.org/

EIP全称Ethereum Improvement Proposal(以太坊改进提案),是以太坊特定新功能和流程的技术设计文档

EIP 作为主要机制

  1. 提出新功能。
  2. 收集社区对某个问题的技术意见。
  3. 记录已经进入以太坊的设计决策。

EIP根据所涉及的领域不同大体可以分为三大类Standards Track EIP、Meta EIP和Informational EIP:

  1. Standards Track EIP
    它描述的是任何改变以太坊所有或大多数实现细节的EIP。进一步分为四个子类:Core、Networking、Interface、ERC。

    • Core: 描述的改进要么需要共识分叉,要么是对以太坊“规则”的重大技术变更(如EIP-5中的 gas 收费方式),要么通常与核心开发人员讨论相关(如:矿工检查EIP中的 gas 价格是否足够高 EIP-86)。
    • Networking: 描述了devp2p、Light Ethereum Subprotocol以及 whisper 和swarm网络协议规范的改进。
    • Interface:这类EIP指对以太坊客户端API/RPC定义和标准的修改以及对调用方法名称(如EIP-6)和合约ABI等的修改。
    • ERC:ERC描述了应用程序级标准
  2. Meta:描述了以太坊周边相关事物的改变过程,例如对决策过程的改变。

  3. Informational:提供一般信息或描述以太坊设计问题,但不提出新功能。用户可以自由地忽略Informational EIP,因为信息性 EIP 不一定代表以太坊社区的推荐。

什么是 ERC(Ethereum Request For Comment )?

ERC的全称是Ethereum Request For Comment ,即以太坊意见征求稿。主要是用来记录以太坊上应用级的开发标准和约定。例如Token标准、名称注册表、URI 方案、库/包格式和钱包格式(EIP75、EIP85)。ERC 指定了合约需要实现的一组必需功能,以便应用程序和其他合约可以理解如何与它们交互。 例如,最流行的标准之一是ERC-721标准,它定义了 NFT 是什么。因为应用程序知道 ERC-721 是什么样子,所以它知道它可以在合约上与哪些函数和属性交互。

请注意,ERC 不被视为core EIP,因此是否采用该标准取决于开发人员。因此,提高对 ERC 的认识对其作用至关重要。

为什么EIPs 和 ERCs 重要?

EIP 是以太坊治理的核心方式。任何人都可以提出它们,社区成员可以评论、辩论和协作来决定它是否应该被采纳!您可以在此处找到提交指南。

ERC 是智能合约的可组合性的动力!可组合性定义了 dapp 和合约相互交互的能力。例如,ERC-2981 NFT 版税标准定义了如何在合约上存储版税信息,以便在市场等 dapp 进行销售时,他们知道如何获得补偿艺术家所需的版税信息!

ERC-2981 是什么?

如上所述,ERC-2981 是版税标准。为了符合 ERC-2981 标准,智能合约必须具有以下功能:

pragma solidity ^0.6.0;
import "./IERC165.sol";

///
/// @dev Interface for the NFT Royalty Standard
///
interface IERC2981 is IERC165 {
    /// ERC165 bytes to add to interface array - set in parent contract
    /// implementing this standard
    ///
    /// bytes4(keccak256("royaltyInfo(uint256,uint256)")) == 0x2a55205a
    /// bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a;
    /// _registerInterface(_INTERFACE_ID_ERC2981);

    /// @notice Called with the sale price to determine how much royalty
    //          is owed and to whom.
    /// @param _tokenId - the NFT asset queried for royalty information
    /// @param _salePrice - the sale price of the NFT asset specified by _tokenId
    /// @return receiver - address of who should be sent the royalty payment
    /// @return royaltyAmount - the royalty payment amount for _salePrice
    function royaltyInfo(
        uint256 _tokenId,
        uint256 _salePrice
    ) external view returns (
        address receiver,
        uint256 royaltyAmount
    );
}

interface IERC165 {
    /// @notice Query if a contract implements an interface
    /// @param interfaceID The interface identifier, as specified in ERC-165
    /// @dev Interface identification is specified in ERC-165. This function
    ///  uses less than 30,000 gas.
    /// @return `true` if the contract implements `interfaceID` and
    ///  `interfaceID` is not 0xffffffff, `false` otherwise
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

ps:ERC-165一种允许合约声明其对接口的支持的标准。这将使市场能够检查 NFT 是否支持版税标准!在市场合约中可能看起来像这样:

bytes4 private constant _INTERFACE_ID_ERC2981 = 0x2a55205a;

function checkRoyalties(address _contract) internal returns (bool) {
  (bool success) = IERC165(_contract).supportsInterface(_INTERFACE_ID_ERC2981);
  return success;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在我们开始编写代码之前,我们先了解一些关于 NFT 版税标准的重要警告。

  1. 版权支付规范没有强制。根据该信息采取行动取决于市场。目前,Coinbase NFT、Rarible、SuperRare 和 Zora 为 ERC-2981 支付版税。如果你的 NFT 在 OpenSea 上出售,你将必须通过他们的网站单独设置你的 NFT 的版税。

  2. 该标准不要求兼容ERC-721(NFT 标准)和ERC-1155(多代币标准)。所以说“通用的版税标准”或许更合适!

让我们写一个 ERC-2981合约

我们已经介绍了什么是 EIP、什么是 ERC 以及它们如何表示 ERC-2981,我们实际编写一个实现 ERC-2981 版税标准的 ERC-721 NFT 智能合约。您可以在此处找到完整的代码。我们将导入 Open Zeppelin 的合约,它提供安全的、预写的 ERC 实现,我们的合约可以继承这些实现!

下载安装系统依赖
创建 Infura 帐户和项目

要将您的 DApp 连接到以太坊主网和测试网,您需要一个 Infura 帐户。在这里注册一个帐户。

登录后,创建一个项目!我们称之为nft-royalty,然后从下拉列表中选择 Web3 API

注册MetaMask 钱包

要与浏览器中的dapp互动,您需要一个MetaMask钱包。在此处注册一个帐户。

获取测试Eth

为了部署到公共测试网络,您需要一些测试ETH来支付您的gas费用!Paradigm 是一个很棒的多链水龙头,它一次将资金存放在8个不同的网络中。

设置您的项目

Truffle 用来构建您的 truffle 项目并添加示例合约和测试。我们创建一个名为nft-royalty的项目。

truffle init nft-royalty
cd nft-royalty
truffle create contract RoyalPets
truffle create test TestRoyalties
  • 1
  • 2
  • 3
  • 4

执行完,项目目录结构如下:

nft-royalty
├── contracts
│   └── RoyalPets.sol
├── migrations
│   └── 1_deploy_contracts.js
├── test
│   └── test_royalties.js
└── truffle-config.js
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
编写 NFT 智能合约

Open Zeppelin 已经提供了安全的、写好的 ERC-2981 和 ERC-721 合约实现,我们可以继承!要下载它们,只需调用npm i "@openzeppelin/contracts"

借助 OpenZeppelin,我们有几种方法可以识别 NFT 合约是否符合版税标准。由于我们的基础合约是 ERC-721,我们可以选择继承 OpenZeppelin 的版税合约ERC721Royalty。该合约覆盖了该_burn功能,以清除NFT的版税信息。

重要的提示!此函数和来自OpenZeppelin的_burn函数都不检查tokenId所有权。这意味着任何人都可以销毁这个 NFT。如果您想避免这种情况,请添加require检查该条件的检查。

function _burn(uint256 tokenId) internal virtual override {
  super._burn(tokenId);
  _resetTokenRoyalty(tokenId);
}
  • 1
  • 2
  • 3
  • 4

创建合约后,我们要设置默认的版税接收人和百分比。请注意,OpenZeppelin 使用基点计算特许权使用费。为了将默认接收人设置为合约所有者并将费用设置为 1%,可以在构造函数中进行设置:

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Royalty.sol";

contract RoyalPets is ERC721Royalty {
  constructor() ERC721("RoyalPets", "RP") {
    _setDefaultRoyalty(msg.sender, 100);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在本教程中,我们使用 OpenZeppelin 的ERC721URIStorage扩展。在这种情况下,我们希望它也继承OpenZeppelin ERC2981合约的属性,如下所示:

import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract RoyalPets is ERC721URIStorage, ERC2981 {
  constructor() ERC721("RoyalPets", "RP") {
    _setDefaultRoyalty(msg.sender, 100);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

此时,我们可以看到vscode报错,ERC721URIStorageERC2981都实现了supportsInterface方法!为了解决这个问题,我们也需要在RoyalPets 中自己实现。在这个函数中添加:

function supportsInterface(bytes4 interfaceId)
  public view virtual override(ERC721, ERC2981)
  returns (bool) {
    return super.supportsInterface(interfaceId);
}
  • 1
  • 2
  • 3
  • 4
  • 5

此外,因为我们不再继承ERC721Royalty,所以我们不再拥有它的_burn方法。需要我们自己添加:

function _burn(uint256 tokenId)
  internal virtual override {
    super._burn(tokenId);
    _resetTokenRoyalty(tokenId);
}
  • 1
  • 2
  • 3
  • 4
  • 5

为了让外部账户可以销毁他们的 NFT,开放了一个公开销毁方法:

function burnNFT(uint256 tokenId)
  public {
    _burn(tokenId);
}
  • 1
  • 2
  • 3
  • 4

最后,我们将添加 NFT 铸币功能。案例将创建两种类型的铸造函数:一种使用默认版税信息铸造代币,另一种在每个代币的基础上指定版税信息。

如前所述,Infura 博客涵盖了这些基础知识。一个细微的差别是我们不会使用静态元数据文件来填充tokenURI. 两个铸币函数如下所示:

function mintNFT(address recipient, string memory tokenURI)
  public onlyOwner
  returns (uint256) {
    _tokenIds.increment();

    uint256 newItemId = _tokenIds.current();
    _safeMint(recipient, newItemId);
    _setTokenURI(newItemId, tokenURI);

    return newItemId;
}

function mintNFTWithRoyalty(address recipient, string memory tokenURI, address royaltyReceiver, uint96 feeNumerator)
  public onlyOwner
  returns (uint256) {
    uint256 tokenId = mintNFT(recipient, tokenURI);
    _setTokenRoyalty(tokenId, royaltyReceiver, feeNumerator);

    return tokenId;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

最终的智能合约,如下:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts/token/common/ERC2981.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract RoyalPets is ERC721URIStorage, ERC2981, Ownable {
  using Counters for Counters.Counter;
  Counters.Counter private _tokenIds;

  constructor() ERC721("RoyalPets", "RP") {
    _setDefaultRoyalty(msg.sender, 100);
  }

  function supportsInterface(bytes4 interfaceId)
    public view virtual override(ERC721, ERC2981)
    returns (bool) {
      return super.supportsInterface(interfaceId);
  }

  function _burn(uint256 tokenId) internal virtual override {
    super._burn(tokenId);
    _resetTokenRoyalty(tokenId);
  }

  function burnNFT(uint256 tokenId)
    public onlyOwner {
      _burn(tokenId);
  }

  function mintNFT(address recipient, string memory tokenURI)
    public onlyOwner
    returns (uint256) {
      _tokenIds.increment();

      uint256 newItemId = _tokenIds.current();
      _safeMint(recipient, newItemId);
      _setTokenURI(newItemId, tokenURI);

      return newItemId;
  }

  function mintNFTWithRoyalty(address recipient, string memory tokenURI, address royaltyReceiver, uint96 feeNumerator)
    public onlyOwner
    returns (uint256) {
      uint256 tokenId = mintNFT(recipient, tokenURI);
      _setTokenRoyalty(tokenId, royaltyReceiver, feeNumerator);

      return tokenId;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
本地部署智能合约

为了部署我们的智能合约,我们需要对migrations/1_deploy_contracts.js修改:

const RoyalPets = artifacts.require("Royalpets");

module.exports = function (deployer) {
  deployer.deploy(RoyalPets);
};
  • 1
  • 2
  • 3
  • 4
  • 5

接下来,我们启动一个本地 Ganache 实例。

有多种方法可以做到这一点:通过 VS Code 扩展、Ganache CLI 和 Ganche 图形用户界面。2种方式都有自己的优势,您可以在此处查看Ganache v7 版本的功能。

在本教程中,我们将使用 GUI。运行Ganche 图形用户界面,创建一个工作区,然后点击保存。它会在 HTTP://127.0.0.1:7545 上创建一个正在运行的 Ganache 实例。

接下来,在truffle-config.js中取消下面代码注释,将 development 中的端口号修改为 7545 。

development: {
  host: "127.0.0.1",     // Localhost (default: none)
  port: 7545,            // Standard Ethereum port (default: none)
  network_id: "*",       // Any network (default: none)
}
  • 1
  • 2
  • 3
  • 4
  • 5

然后运行truffle migrate,默认为development网络,即可部署!
也可以从 VS Code 扩展进行部署。build/contracts然后,您可以在 VS Code 扩展中或在 Ganache UI中查看您构建的合约!

测试你的智能合约

如果您想在不编写完整测试的情况下即时测试您的智能合约命令,您可以使用truffle developtruffle console。在这里阅读更多相关信息。

出于本教程的目的,我们将继续编写 Javascript 测试。

请注意,使用 Truffle,您可以选择使用 Javascript、Typescript 或 Solidity 编写测试。

const RoyalPets = artifacts.require("RoyalPets");

contract("RoyalPets", function (accounts) {
  it("should support the ERC721 and ERC2198 standards", async () => {
    const royalPetsInstance = await RoyalPets.deployed();
    const ERC721InterfaceId = "0x80ac58cd";
    const ERC2981InterfaceId = "0x2a55205a";
    var isERC721 = await royalPetsInstance.supportsInterface(ERC721InterfaceId);
    var isER2981 = await royalPetsInstance.supportsInterface(ERC2981InterfaceId); 
    assert.equal(isERC721, true, "RoyalPets is not an ERC721");
    assert.equal(isER2981, true, "RoyalPets is not an ERC2981");
  });
  it("should return the correct royalty info when specified and burned", async () => {
    const royalPetsInstance = await RoyalPets.deployed();
    await royalPetsInstance.mintNFT(accounts[0], "fakeURI");
    // Override royalty for this token to be 10% and paid to a different account
    await royalPetsInstance.mintNFTWithRoyalty(accounts[0], "fakeURI", accounts[1], 1000);

    const defaultRoyaltyInfo = await royalPetsInstance.royaltyInfo.call(1, 1000);
    var tokenRoyaltyInfo = await royalPetsInstance.royaltyInfo.call(2, 1000);
    const owner = await royalPetsInstance.owner.call();
    assert.equal(defaultRoyaltyInfo[0], owner, "Default receiver is not the owner");
    // Default royalty percentage taken should be 1%. 
    assert.equal(defaultRoyaltyInfo[1].toNumber(), 10, "Royalty fee is not 10");
    assert.equal(tokenRoyaltyInfo[0], accounts[1], "Royalty receiver is not a different account");
    // Default royalty percentage taken should be 1%. 
    assert.equal(tokenRoyaltyInfo[1].toNumber(), 100, "Royalty fee is not 100");

    // Royalty info should be set back to default when NFT is burned
    await royalPetsInstance.burnNFT(2);
    tokenRoyaltyInfo = await royalPetsInstance.royaltyInfo.call(2, 1000);
    assert.equal(tokenRoyaltyInfo[0], owner, "Royalty receiver has not been set back to default");
    assert.equal(tokenRoyaltyInfo[1].toNumber(), 10, "Royalty has not been set back to default");
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

执行命令:

truffle test
  • 1

执行结果:

Contract: RoyalPets
  ✔ should support the ERC721 and ERC2198 standards (67ms)
  ✔ should return the correct royalty info when specified and burned (1077ms)


2 passing (1s)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

铸造一个 NFT 并在您的移动钱包或 OpenSea 中查看它!

如果您想铸造NFT并在MetaMask 钱包中查看它,则需要将合约部署到公共测试网或主网上。为此,您需要从Infura项目和MetAmask Wallet Secret Key中获取Infura Project API。在您的文件夹的根目录,添加一个.env文件,输入该信息。

警告:不要泄漏或提交此文件。我们建议将.env添加到.gitignore文件中。

MNEMONIC="YOUR SECRET KEY"
INFURA_API_KEY="YOUR INFURA_API_KEY"
  • 1
  • 2

ruffle-config.js文件的顶部,添加如下代码以获取这个信息:

require('dotenv').config();
const mnemonic = process.env["MNEMONIC"];
const infuraApiKey = process.env["INFURA_API_KEY"];

const HDWalletProvider = require('@truffle/hdwallet-provider');
  • 1
  • 2
  • 3
  • 4
  • 5

最后,将 Goerli 网络添加到以下networks列表中:

goerli: {
  provider: () => new HDWalletProvider(mnemonic, `https://goerli.infura.io/v3/${infuraApiKey}`),
  network_id: 5,       // Goerli's network id
  chain_id: 5,         // Goerli's chain id
  gas: 5500000,        // Gas limit used for deploys.
  confirmations: 2,    // # of confirmations to wait between deployments. (default: 0)
  timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)
  skipDryRun: true     // Skip dry run before migrations? (default: false for public nets)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

最终的truffle-config.js,如下

require('dotenv').config();
const mnemonic = process.env["MNEMONIC"];
const infuraApiKey = process.env["INFURA_API_KEY"];

const HDWalletProvider = require('@truffle/hdwallet-provider');

module.exports = {
  networks: {
    development: {
     host: "127.0.0.1",     // Localhost (default: none)
     port: 7545,            // Standard Ethereum port (default: none)
     network_id: "*",       // Any network (default: none)
    },
    goerli: {
      provider: () => new HDWalletProvider(mnemonic, `https://goerli.infura.io/v3/${infuraApiKey}`),
      network_id: 5,       // Goerli's network id
      chain_id: 5,         // Goerli's chain id
      gas: 5500000,        // Gas limit used for deploys.
      confirmations: 2,    // # of confirmations to wait between deployments. (default: 0)
      timeoutBlocks: 200,  // # of blocks before a deployment times out  (minimum/default: 50)
      skipDryRun: true     // Skip dry run before migrations? (default: false for public nets)
    }
  },

  // Set default mocha options here, use special reporters, etc.
  mocha: {
    // timeout: 100000
  },

  // Configure your compilers
  compilers: {
    solc: {
      version: "0.8.15",      // Fetch exact version from solc-bin (default: truffle's version)
    }
  },
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

需要为 dotenv@truffle/hdwallet-provider dev依赖项。

npm i --save-dev dotenv
npm i --save-dev @truffle/hdwallet-provider
truffle migrate --network goerli
  • 1
  • 2
  • 3

最后,运行 truffle migrate --network goerli 进行部署!

然后,要快速与Goerli网络进行交互,我们可以使用truffle console --network goerli,并调用合适的合约函数。我们已经将一些元数据固定在IPFS上,供您使用,tokenURI: ipfs://bafybeiffapvkruv2vwtomswqzxiaxdgm2dflet2cxmh6t4ixrgaezumbw4
命令执行如下:

truffle migrate --network goerli
truffle(goerli)> const contract = await RoyalPets.deployed()
undefined
truffle(goerli)> await contract.mintNFT("YOUR ADDRESS", "ipfs://bafybeiffapvkruv2vwtomswqzxiaxdgm2dflet2cxmh6t4ixrgaezumbw4")
  • 1
  • 2
  • 3
  • 4

如果您想使用自己的元数据,可以使用Truffle或Infura。在此处查看指南:

要在您的手机钱包上查看您的 NFT,请手机打开 MetaMask ,切换到 Goerli 网络,然后打开 NFTs 选项卡!要在 OpenSea 上查看,您必须部署到主网或Polygon。否则,如果您将合约部署到 rinkeby,您可以在 https://testnets.opensea.io/上查看它。请注意,合并后 rinkeby 将被弃用。

如果您不想在Infura项目中监视您的交易,也可以通过Truffle Dashboard设置,这使您可以通过Metamask部署和签署交易 - 并且永远不会泄漏您的私钥!为此,只需运行:

truffle dashboard
truffle migrate --network dashboard
truffle console --network dashboard
  • 1
  • 2
  • 3

未来扩展

至此,您已经写了一份NFT智能合约,可以查询版税信息。请注意将元数据上传到IPFS的更深入的指南!有关代码的详细演练,可以在YouTube上观看直播。在Web3的未来版本中,通过实现ERC-4907以及创建存在各种NFT标准的NFT租赁市场,可以看到我们如何通过实现ERC-4907来生成基本的ERC-721s!

您可能会考虑的其他一些扩展实现oyaltyInfo的方式。Gemini是一个很酷的博客,详细介绍了一些decaying royalties, multisig royalties, and stepped royalties在这里。如果您尝试任何一个,请告诉我们!

如果您想关于内容进行谈论,建议您在此处展开讨论。如果您想展示自己的建造或与Unleashed社区一起交流,请加入我们的 Discord!最后,不要忘记在Twitter上关注我们,以获取所有Truffle的最新更新。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/523579
推荐阅读
相关标签
  

闽ICP备14008679号