在 EDU Chain 上创建 NFT

204 阅读8分钟

继续跟着 HackQuest 学习合约知识

HackQuest

image.png

HackQuest 自己的合约编辑器

image.png

NFT 信息

首先需要一个数据结构来存储 NFT 的详细信息,例如名字,描述,持有者,而结构体是所有数据结构中最合适的。

 struct Token {
    string name;
    string description;
    address owner;
  }

有了Token 这个结构体后,一般而言,我们都需要一个映射来存储它,在这里,我需要把 tokenId 和 Token 对应起来,所以需要创建一个从 uint256⇒Token 的映射。

 mapping(uint256 => Token) private tokens;
 mapping(address => uint256[]) private ownerTokens;

同时我们还需要一个指针来指向下一个要铸造的 NFT,所以我们需要定义一个 整型 变量。

  uint256 nextTokenId = 1;

mint NFT

让我们来给合约增加一些功能。首先是 mint 函数,此函数的作用是铸造 NFT,你只需要输入指定 NFT 的名字和描述信息,就可以通过此函数铸造一个独一无二的 NFT

定义一个名为 mint 的公开函数,该函数需要知道被铸造 NFT 的 name 以及 description 。

owner 字段可以通过 msg.sender 来读取

因此我们定义两个参数分别为

1.string 类型的变量 _name

2.string 类型的变量 _description

	function mint(string memory _name, string memory _description) public { }

我们的目标是查询特定地址所拥有的所有 NFT。然而,仅仅依靠我们目前定义的 Token 结构体和 tokens 映射是无法实现的。

我们需要一个类似于钱包的变量,该变量需要将地址和其拥有的所有 NFT 联系起来。

这个变量可以被看作是一个映射关系,它将地址映射到一个 TokenId 数组上。

为了更好的理解这个变量,你可以把它想象成从地址到钱包的映射,而钱包的数据结构就是 tokenId 数组。

在后续教程中,我们会反复使用钱包来代指 TokenId 数组

因此我们首先需要定义一个映射。

在定义完 mint 函数之后,我们需要做三件事情。

1.更新 tokens 映射

2.更新 ownerTokens 映射

3.更新 tokenId 的值

首先我们更新 tokens 映射,共分为两步:第一步,我们需要使用参数信息新建一个 Token 结构体,因为每一个 Token 表示一个 NFT。

第二步,我们需要将此结构体赋值给 tokenId 对应的 tokens 映射,因为 tokens 是存储所有的 NFT 信息的变量。

在这里我们首先完成第一步,我们需要将 NFT 表示出来。而基于函数的参数,我们可以构建一个 Token 结构体来表示这些信息。

因此,在这个阶段,我们将根据函数的参数创建一个全新的Token实例,并将其暂时保存在内存 memory 中,以便后续操作能够方便地使用它。

选择 memory 的原因是,是因为将数据存储在 memory 更省 gas 费。所以我们在不需要操作 storage 的时候,一般都使用 memory 来存储数据。

更新 ownerTokens 映射,同样需要两步:第一步,我们需要在 ownerTokens 映射中获取该地址所持有的 tokenId 数组,因为该映射存储着每个地址对应的 tokenid 数组。

第二步,将铸造出的 NFT 的 tokenId 添加到该地址所持有的 tokenId 数组中。

经完成了 mint 功能的所有逻辑。

但在最后,我们还需要将铸造的 NFT 的 tokenId 返回给调用者,让他知道它铸造的 NFT 是哪一个。

需要注意的是我们要返回的这个 tokenId 应该是此次铸造的 NFT 所对应的 Id,因此我们应该 return 的值是 nextTokenId-1。

这是由于刚刚进行了 nextTokenId++; 另一种更好的做法是在 nextTokenId++ 之前使用局部变量将铸造的 tokenId 预先存储起来。

查询 NFT 信息

铸造新的 NFT 后,一个很自然的步骤是查询我们新铸造的 NFT 的信息。 因此,我们会在本节中实现一个查询函数,通过给定的TokenId,我们会找到与 NFT 相关的信息,并将其返回给函数调用者。

在本节中,我们将学习如何:

1.检索指定的 NFT 信息

2.确保 TokenId 有效

3.返回 NFT 信息

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

contract MyNFT {
    
    struct Token {
        string name;        // NFT 名称
        string description; // NFT 描述信息
        address owner;      // NFT 所有者地址
    }
    
    mapping(uint256 => Token) private tokens;
		mapping(address => uint256[]) private ownerTokens;


    // 记录下一个可用的 NFT ID
    uint256 nextTokenId = 1;
    
    function mint(string memory _name, string memory _description) public returns(uint256){ 
        Token memory newNFT = Token(_name, _description, msg.sender);
        uint256 tokenId = nextTokenId;
        tokens[tokenId] = newNFT;
        ownerTokens[msg.sender].push(tokenId);
				nextTokenId++;
        return tokenId;
    }

    function getNFT(uint256 _tokenId) public view returns (string memory name, string memory description, address owner) {
        require(_tokenId >= 1 && _tokenId < nextTokenId, "Invalid token ID");
        Token memory token = tokens[_tokenId];
        name = token.name;
        description = token.description;
        owner = token.owner;
    }

}

查询该地址下所有 NFT

而在 NFT 的系统中,我们通常还有通过某个地址,来查询该地址下拥有的所有 NFT 的需求。 因此,我们会在本节中实现一个查询函数,通过给定的地址,来查询该地址拥有的所有的 NFT,查询包括这几个步骤:

1.检索该地址拥有的 NFT 数组

2.将该数组作为返回值返回

转账

我们的代币系统已经初见雏形,但要成为真正的代币系统,还需要一个交易功能,即转账。转账包括这几个步骤:

1.根据要转账的 tokenId 查询到该 NFT 的信息

2.将该 NFT 的拥有者转换为接收者

3.将该 tokenId 从发送者的钱包移除

4.将该 tokenId 添加到接收者的钱包

如果实现了以上四步那么此时转账就被认为完成了。

在定义完函数头之后,我们需要考虑函数是否有参数限制和访问控制。

由于这是一个转账函数,我们需要确保调用者是 NFT 的拥有者。然而,由于我们目前无法获取要转移的 NFT 的所有者,所以我们将在稍后处理这个问题。

在参数限制方面,显然,由于这是一个转账函数,我们要转账的地址必须是一个非零地址。这是为了防止用户误操作,将 NFT 转移到零地址,从而导致 NFT 的丢失。

另一个需要检查的条件是 TokenId 的有效性。我们应该确保 TokenId 的值在我们已经铸造的 NFT 范围内,也就是 TokenId 必须大于1且小于 nextTokenId 的值。

在完成参数的检查后,我们就可以通过 TokenId 去检索 NFT 了,tokens 映射为我们提供了一个非常便捷的查询方式。

因为我们在后续的步骤中需要改变该 NFT 的信息,我们需要将查询到的 NFT 保存在 storage 里。

如果存在 memory 里,那么后续的修改将只存在于内存当中,而不会修改合约的状态。

实现修改 NFT 拥有者,将 NFT 从转账者钱包中删除,将 NFT 添加到收款者钱包中的功能,从而实现完整的转账功能。

实现 deleteById

在循环中,break 的作用是跳出循环,一般有以下两种用法:

  1. 当 break 语句出现在一个循环内时,循环会立即终止,且程序流将继续执行紧接着循环的下一条语句。 2. 它可用于终止 switch 语句中的一个case。

如果使用的是嵌套循环,break 语句会停止执行最内层的循环,然后开始执行该块之后的下一行代码。

注意:

break 语句对 if-else 的条件语句不起作用。

在多层循环中,一个 break 语句只向外跳一层。

由于它是用来退出循环或者 switch 语句的, 所以只有当它出现在这些语句的时候, 这种形式的 break 语句才是合法的。

burn

删除只是在钱包中删除指定的 TokenId,而这一节中要讲的删除,是将该 NFT 彻底的删除,他所有相关联的信息都必须删除。为了完成此功能,我们需要以下的步骤:

1.根据要删除的 tokenId 查询到该 NFT 的信息

2.将该 TokenId 从拥有者钱包删除

3.将该 NFT 从 NFT 列表中删除

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

contract MyNFT {
    
    // 定义 Token 结构体,用于存储 NFT 信息
    struct Token {
        string name;        // NFT 名称
        string description; // NFT 描述信息
        address owner;      // NFT 所有者地址
    }
    
    // 使用 mapping 存储每个 NFT 的信息
    mapping(uint256 => Token) private tokens;
    // 使用 mapping 存储每个地址所拥有的 NFT ID 列表
    mapping(address => uint256[]) private ownerTokens;

    // 记录下一个可用的 NFT ID
    uint256 nextTokenId = 1;
    
    // 创建 NFT 函数,用于创建一个新的 NFT,并将其分配给调用者
    function mint(string memory _name, string memory _description) public returns(uint256){ 
        Token memory newNFT = Token(_name, _description, msg.sender);
        tokens[nextTokenId] = newNFT;
        ownerTokens[msg.sender].push(nextTokenId);
        nextTokenId++;
        return nextTokenId-1;
    }

    // 获取指定 NFT 的信息
    function getNFT(uint256 _tokenId) public view returns (string memory name, string memory description, address owner) {
        require(_tokenId >= 1 && _tokenId < nextTokenId, "Invalid token ID");
        Token storage token = tokens[_tokenId];
        name = token.name;
        description = token.description;
        owner = token.owner;
    }

    // 获取指定地址所拥有的所有 NFT ID
    function getTokensByOwner(address _owner) public view returns (uint256[] memory) {
        return ownerTokens[_owner];
    }

    // 转移指定 NFT 的所有权给目标地址
    function transfer(address _to, uint256 _tokenId) public {
        require(_to != address(0), "Invalid recipien");
        require(_tokenId >= 1 && _tokenId < nextTokenId, "Invalid TokenID");
        Token storage token = tokens[_tokenId];
        require(token.owner == msg.sender, "You don't own this token");
        
        // 将 NFT 的所有权转移给目标地址
        token.owner = _to;
        
        deleteById(msg.sender, _tokenId);
        ownerTokens[_to].push(_tokenId);
    }

    function deleteById(address account, uint256 _tokenId) internal {
        uint256[] storage ownerTokenList = ownerTokens[account];
        for (uint256 i = 0; i < ownerTokenList.length; i++) {
            if (ownerTokenList[i] == _tokenId) {
                // 将该 NFT ID 与数组最后一个元素互换位置,然后删除数组最后一个元素
                ownerTokenList[i] = ownerTokenList[ownerTokenList.length - 1];
                ownerTokenList.pop();
                break;
            }
        }
    }

    function burn(uint256 _tokenId) public {
        require(_tokenId >= 1 && _tokenId < nextTokenId, "Invalid TokenID");
        Token storage token = tokens[_tokenId];
        require(token.owner == msg.sender, "You don't own this token");
        
        deleteById(msg.sender, _tokenId);
        delete tokens[_tokenId];
    }

}