看完这篇!新手也能写NFT合约!

趣链科技

    在《区块链杀手级应用落地畅想(上)》中我们提到,2021年被称为NFT“元年”。在短时间内,NFT已不再局限于加密世界的投机价值,其释放的潜力吸引了越来越多的国际品牌,例如耐克将鞋子作为NFT专利,允许用户“繁殖”不同的鞋子来创造属于自己的定制运动鞋;其他诸如美国国家篮球协会(NBA)、路易威登(LV)等国际知名品牌均在加速布局,可以说NFT正在为艺术收藏、音乐、游戏、体育、时尚圈等赋予新的价值加持
    NFT应用场景丰富,初学者怎么入门?本文将帮助开发小白了解NFT合约的编写。原文见公众号:QTech
    NFT合约标准介绍
    目前,NFT(Non-Fungible Tokens)最为主流有三种合约:ERC-721ERC-1155ERC-998
    在NFT的最初期,大家严格遵守NFT的定义规范,也就是ERC-721规范,早年非常火热的加密猫系列就是基于该规范开发的。从 ERC-721 协议标准来看,每一个基于ERC-721创建的NFT都是独一无二、不可复制的。用户可以在智能合约中编写一段代码来创建自己的NFT,该段代码遵循一个比较基础的通用模版格式,可通过该代码添加关于NFT的所有者名称、元数据或安全文件链接等细节。
    ERC-721规范虽然可以很好的描述NFT,却存在着一些不足。例如,假设我想一次性铸造30个NFT,那么我就需要发起30次铸造NFT的交易,效率和用户体验并不友好。为此ERC-1155提出了“打包”的概念,可以将多个NFT封装成一个Collection,允许开发者在一个智能合约中实现无限数量的FT和NFT。正是由于“打包”的特性,相当于ERC-1155协议标准集成了ERC-20和ERC-721的能力,具有效率高、灵活性强等优势,目前已经为多款游戏提供了动力,例如游戏开发者可以在一个合约里定义多种物品(角色、武器、盔甲、药水、超能力)。
    随着NFT概念的进一步火热,组合式NFT概念被提出。例如一个头像可以由眼睛、嘴巴和鼻子等元素组成,每个元素都是一个NFT或者FT,这些元素共同组成了一个独一无二的NFT头像。但是对于整个头像NFT而言,在过去传统合约中是没有所谓层级关系的,即鼻子部分并不知道自己属于哪个NFT,或者头像部分不知道自己是由哪些NFT或者FT组成的。为此,ERC-998便应运而生,也就是可组合Composable NFTs,缩写为CNFT,即一个ERC-998可以包含多个ERC-721和ERC-20形式的通证,而转移CNFT即是转移CNFT所拥有的整个层级结构和所属关系。
    为帮助大家快速理解并入门,下文将先分析ERC-721和ERC-1155的合约设计理念,随后详细介绍如何编写ERC 721合约。
    NFT合约设计理念 ERC-721
    ERC-721作为最为基础的NFT合约,具有以下几个接口:
    function balanceOf(address owner) -> uint256 balance /// @notice Find the owner of an NFT ? ?/// @dev NFTs assigned to zero address are considered invalid, and queries ? ?/// ?about them do throw. ? ?/// @param _tokenId The identifier for an NFT ? ?/// @return The address of the owner of the NFT
    function ownerOf(uint256 tokenId) -> address owner/// @notice Transfers the ownership of an NFT from one address to another address ? ?/// @dev Throws unless `msg.sender` is the current owner, an authorized ? ?/// ?operator, or the approved address for this NFT. Throws if `_from` is ? ?/// ?not the current owner. Throws if `_to` is the zero address. Throws if ? ?/// ?`_tokenId` is not a valid NFT. When transfer is complete, this function ? ?/// ?checks if `_to` is a smart contract (code size > 0). If so, it calls ? ?/// ?`onERC721Received` on `_to` and throws if the return value is not ? ?/// ?`bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`. ? ?/// @param _from The current owner of the NFT ? ?/// @param _to The new owner ? ?/// @param _tokenId The NFT to transfer ? ?/// @param data Additional data with no specified format, sent in call to `_to`
    function safeTransferFrom(address from, address to, uint256 tokenId)/// @notice Transfers the ownership of an NFT from one address to another address ? ?/// @dev This works identically to the other function with an extra data parameter, ? ?/// ?except this function just sets data to "". ? ?/// @param _from The current owner of the NFT ? ?/// @param _to The new owner ? ?/// @param _tokenId The NFT to transfer ? ?
    function transferFrom(address from, address to, uint256 tokenId) ? ?/// @notice Change or reaffirm the approved address for an NFT ? ?/// @dev The zero address indicates there is no approved address. ? ?/// ?Throws unless `msg.sender` is the current NFT owner, or an authorized ? ?/// ?operator of the current owner. ? ?/// @param _approved The new approved NFT controller ? ?/// @param _tokenId The NFT to approve
    function approve(address to, uint256 tokenId)/// @notice Enable or disable approval for a third party ("operator") to manage ? ?/// ?all of `msg.sender`'s assets ? ?/// @dev Emits the ApprovalForAll event. The contract MUST allow ? ?/// ?multiple operators per owner. ? ?/// @param _operator Address to add to the set of authorized operators ? ?/// @param _approved True if the operator is approved, false to revoke approval
    function getApproved(uint256 tokenId) -> address operator ? ?/// @notice Query if an address is an authorized operator for another address ? ?/// @param _owner The address that owns the NFTs ? ?/// @param _operator The address that acts on behalf of the owner ? ?/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
    function setApprovalForAll(address operator, bool _approved)/// @notice Get the approved address for a single NFT ? ?/// @dev Throws if `_tokenId` is not a valid NFT. ? ?/// @param _tokenId The NFT to find the approved address for ? ?/// @return The approved address for this NFT, or the zero address if there is none
    function isApprovedForAll(address owner, address operator) -> bool
    其中每一个函数的意义可参见上述注释,接下来我们会简要分析底层存储逻辑
    
mapping (uint256 => address) internal idToOwnermapping (uint256 => address) internal idToApprovalmapping (address => uint256) private ownerToNFTokenCountmapping (address => mapping (address => bool)) internal ownerToOperators

    上述的4个mapping维护了整个合约的存储结构:
    
  • idToOwner维护了谁拥有什么通证,映射关系是通证ID到其所有者地址;
  • idToApproval维护了谁被授权操作某个通证,映射关系是通证ID到被授权操作的地址;
  • ownerToNFTokenCount维护了某个地址所拥有的nft总量,映射关系是用户地址到代表总量的整数;
  • ownerToOperators维护了某个地址是否授权给了另外一个地址;
        

    一个主要的modifiercanOperate:
    
// 查看是否具备操作某个nft的权限modifier canOperate( ? ?uint256 _tokenId  )  {    // 找到对应token的所有者 ? ?address tokenOwner = idToOwner[_tokenId]; ? ?require( ?      // 需要操作者是所有者或者被所有者授权 ? ? ?tokenOwner == msg.sender || ownerToOperators[tokenOwner][msg.sender], ? ? ?// 否则返回错误 ? ? ?NOT_OWNER_OR_OPERATOR ?  ); ? ?_;  }

    同时,ERC-721还支持可选的实现项,metadata extension,主要用以返回NFT的描述信息。
    ERC-1155
    ERC-1155同上面的描述,因为实现了“打包”的功能,所以ERC-1155的大部分函数都支持batch的操作。相比于ERC-721,ERC-1155有很好的效率提升。
    ERC-1155具有以下接口:
    
function balanceOf(address account, uint256 id) → uint256function balanceOfBatch(address[] accounts, uint256[] ids) → uint256[]function setApprovalForAll(address operator, bool approved)function isApprovedForAll(address account, address operator) -> boolfunction safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes data)function safeBatchTransferFrom(address from, address to, uint256[] ids, uint256[] amounts, bytes data)

    具体的接口也都比较明确,这里不再赘述。存储结构上,ERC-1155具有以下两个基本的结构:
    
mapping (uint256 => mapping(address => uint256)) internal balances;mapping (address => mapping(address => bool)) internal operatorApproval;
  • balances维护了某个账户拥有的某个NFT总量,基本的映射逻辑是id=>(owner=>balances)
  • operatorApproval维护了某个账户是否已经被另一个账户授权,主要逻辑同上;

    同样的,ERC-1155具备一个可选的扩展合约ERC1155Metadata_URI,主要是返回某个通证的uri json。
    ERC-721合约编写
    由于目前社区已经有大量开源的ERC-721标准模板可供参考,在编写大部分的NFT合约时完全可以借鉴通用模板。若标准模板无法满足全部需求时,可在外部新建一个属于自己的合约(内部实现相应的业务逻辑),并且对标准合约进行继承。
    下面的示例将以某开源标准ERC-721合约作为基础模板,展示在趣链BaaS平台内的Web IDE内进行进一步的合约开发。
    1)进入Web IDE:如下图,在nf-token-mock合约中定义了mint NFT的方法,我们进入该合约并执行编译操作。
    
    2)编译合约:具体结果如下。
    
    3)Web IDE模拟部署与执行:不同于以太坊在线IDE编辑器如Remix,趣链BaaS的Web IDE直接提供模拟部署和执行环境,无需用户使用Metamask的测试网账户,相当于省去了用户在Metamask导入一个测试网账户并拥有测试通证的步骤,也无需在每次调用中进行签名授权,可提升调试效率。
    因此,如下图我们可选择NFTTokenMock合约进行模拟部署,该合约中封装了NFT mint等方法,我们先进行mint后,可进一步执行balanceof(查询余额)、Approve(授权)等操作。
    
    4)mint(铸造):向0xd69e9413029e7Fc483eFB5cB1aBCE4Ec44437F2C地址铸造一个通证ID为166的NFT
    
    5)balanceof(查询余额):查询0xd69e9413029e7Fc483eFB5cB1aBCE4Ec44437F2C地址共有几个NFT
    
    相似的,您可以参照合约设计中提到的不同接口信息,调用函数执行Approve(授权)等操作。
    6)合约安全检测:如前所述,上述合约是基于社区开源的合约文件,对于安全性未可知。因此我们可以借助趣链Web IDE的静态分析和形式验证等合约安全检测工具对合约进行检测,帮助最大化规避合约潜在漏洞造成的风险。
    
    7)个性化完善合约功能:本例的合约已经封装了很多函数方法,但开发者还可以根据需求编写更多功能,在模拟执行时还可以使用Debug操作帮助调试。
    8)合约编译文件集成至SDK:?做完以上所有调试并编译完成后,可将最终的合约编译文件集成至趣链BaaS提供的SDK中,由此可通过SDK进行NFT合约的部署、调用等管理操作。
    9)SDK集成至区块链应用:最后,开发者还需要打通业务系统和链上智能合约的交互,只需要将对应的SDK集成至自己的区块链应用项目中即可。
    【备注】
    在步骤8中介绍的是通过SDK部署合约,对于初学者依然存在一定的学习门槛。如下图趣链BaaS提供了一键可视化部署合约实例的功能。在部署完成后,可直接通过趣链BaaS平台进行智能合约的可视化调用。
    部署界面
    
    合约实例管理界面
    
    合约实例调用界面
    
    总结
    NFT历经十年发展,出现了ERC-721,ERC-1155和ERC-998等为典型的主流合约,技术架构日趋完善。初学者除了查阅一些开源项目了解通用NFT合约文件外,还可以借助诸如趣链Web IDE等便捷的智能合约研发设施,可充分赋能智能合约研发、部署、调用、升级等全生命周期管理流程,加速相关区块链应用的落地。