# 您的第一个 NFT

{% hint style="info" %}
注意：以下教程正在进行中。 此外，Aptos（非同质化）代币规范尚未正式确定。
{% endhint %}

## Aptos 中的代币和 NFT

[NFT](https://en.wikipedia.org/wiki/Non-fungible_token) 是一种不可替代的代币或存储在区块链上的数据，它唯一地定义了资产的所有权。 NFT 最初是在 [EIP-721](https://eips.ethereum.org/EIPS/eip-721) 中定义的，后来在 [EIP-1155](https://eips.ethereum.org/EIPS/eip-1155) 中进行了扩展。 NFT 通常包括以下几个方面：

* 名称，资产的名称，在集合中必须是唯一的
* 描述，资产的描述
* URL，一个指向关于资产的更多信息的链外非描述指针可以是媒体，如图像或视频或更多元数据
* 供应量，这个 NFT 的单位总数，许多 NFT 只有一个，而那些有多个的被称为版本

此外，大多数 NFT 是具有共同属性（例如主题、创建者或最小合约）的集合或一组 NFT 的一部分。每个集合都有一组相似的属性：

* 名称，集合的名称，在创建者的帐户中必须是唯一的
* 描述，资产的描述
* URL，一个指向关于资产的更多信息的链外非描述指针可以是媒体，如图像或视频或更多元数据

核心 NFT 或代币的 Aptos 实现可以在 [Token.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/Token.move) 中找到。

### Aptos Token 定义[​](https://aptos.dev/tutorials/your-first-nft#aptos-token-definitions) <a href="#aptos-token-definitions" id="aptos-token-definitions"></a>

#### Token[​](https://aptos.dev/tutorials/your-first-nft#the-token) <a href="#the-token" id="the-token"></a>

Aptos Token 被定义为：

| 域            | 类型              | 描述                                        |
| ------------ | --------------- | ----------------------------------------- |
| `id`         | `GUID:ID`       | 代币的全局唯一标识符也可用于识别创建者                       |
| `name`       | `ASCII::String` | 代币的名称，在集合中必须是唯一的                          |
| `collection` | `GUID:ID`       | 包含此代币的集合的全局唯一标识符                          |
| `balance`    | `u64`           | 与供应相关的此代币的当前存储量, `1 <= balance <= supply` |

Aptos TokenData 被定义为：

| 域             | 类型              | 描述                               |
| ------------- | --------------- | -------------------------------- |
| `id`          | `GUID:ID`       | 代币的全局唯一标识符也可用于识别创建者              |
| `description` | `ASCII::String` | 代币描述信息                           |
| `name`        | `ASCII::String` | 代币的名称，在集合中必须是唯一的                 |
| `supply`      | `u64`           | 此 Token 的总版本数                    |
| `uri`         | `ASCII::String` | 附加信息/媒体的 URL                     |
| `metadata`    | `TokenType`     | 一个通用的、可选的用户定义结构，用于包含有关此代币链上的附加信息 |

Token 是使用 Move 属性  `store` 定义的，这意味着它们可以保存到全局存储中。Token 不能被隐式丢弃，必须被销毁以确保总余额等于供应。Token 不能被复制。也就是说，由于缺少复制操作，除了创建者之外，任何人都无法更改总余额或供应量。请注意，当前的 API 没有公开发布创建代币的能力。Token 可以通过其 id 或 TokenType、集合名称和 Token 名称的元组来唯一标识。

TokenData 具有  `copy`  属性，支持简单的代币余额拆分。每当余额大于 1 的个人向另一个人提供其余额的一部分少于其总余额时，就会发生代币拆分。交易代币的用户请注意，两个代币可以共享相同的 TokenData，因为 Aptos 标准不会尝试识别代币是否复制了另一个代币的属性。重复前面所说的，代币可以通过其 id 或 TokenType、集合名称和代币名称来唯一标识。创建者可以更改 TokenType、集合名称或代币名称集中的任何值，以创建相似但不完全相同的代币。

#### Token[​](https://aptos.dev/tutorials/your-first-nft#the-token-collection) Collections <a href="#the-token-collection" id="the-token-collection"></a>

Aptos 定义了一组按其唯一 ID 分组在一起的集合：

```rust
struct Collections<TokenType: copy + drop + store> has key {
    collections: Table<ASCII::String, Collection>,
}

struct TokenMetadata<TokenType: store> has key {
    metadata: Table<ID, TokenType>,
}
```

由于 Collections 具有属性键，因此它直接存储到创建者帐户。 需要注意的是，如果没有 Collections 的概念，而 Collection 具有 key 属性，则 Aptos 帐户只能有一个集合，而通常情况并非如此。 可以按名称在集合集中查找集合，因此强制使用唯一的集合名称。

Token 和 TokenData 结构在其内容中是固定的。 资源 TokenMetadata 使创建者能够存储额外的 Token 数据。 表中的数据存储为 Token 的唯一 ID。 由于 script 函数不能支持结构或泛型的限制，使用它是可选的并且需要定制化 API。

每个集合都有以下字段：

| 域                | 类型                                               | 描述                                 |
| ---------------- | ------------------------------------------------ | ---------------------------------- |
| `tokens`         | `Table<ASCII::String, TokenMetadata<TokenType>>` | 跟踪与此集合关联的所有Token                   |
| `claimed_tokens` | `Table<ASCII::String, address>`                  | 跟踪 supply == 1 的 Token 的存储地址       |
| `description`    | `ASCII::String`                                  | 集合描述                               |
| `name`           | `ASCII::String`                                  | 此集合的名称在指定 TokenType 的创建者帐户中必须是唯一的。 |
| `uri`            | `ASCII::String`                                  | 附加信息/媒体的 URL                       |
| `count`          | `u64`                                            | 此集合跟踪的不同 Token 总数                  |
| `maximum`        | `Option<u64>`                                    | 可在此集合中铸造的可选 Token 的最大数量            |

集合不是用于累积 Token 的存储，因此它不包含 Token，而是包含 TokenData：

| 域      | 类型          | 描述                        |
| ------ | ----------- | ------------------------- |
| `id`   | `GUID:ID`   | 此 Token 的全球唯一标识符也可用于识别创建者 |
| `data` | `TokenData` | 关于此代币的附加数据，这是代币的供应量 > 1   |

#### Token 存储[​](https://aptos.dev/tutorials/your-first-nft#storing-tokens) <a href="#storing-tokens" id="storing-tokens"></a>

为了获取和存储 Token，用户必须拥有一个 TokenType 库：

```rust
struct Gallery has key {
    gallery: Table<ID, Token>,
}
```

与 Collections 一样，它作为资源存储在 Aptos 帐户上。

### Token 介绍[​](https://aptos.dev/tutorials/your-first-nft#introducing-tokens) <a href="#introducing-tokens" id="introducing-tokens"></a>

作为我们核心框架的一部分，Aptos 提供了一个基本的 [Token](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/Token.move) 接口，没有额外的数据，或者明确的 TokenMetadata 资源没有该令牌的条目。 这样做的动机包括：

* 创建新 Token 需要编写 Move 代码
* 用于创建新标记的脚本函数必须定制化，因为 Move 不支持模板类型或结构作为输入参数
* 脚本函数上的模板类型为编写脚本函数增加了额外的难度

本教程将引导您完成：

* 创建自己的 Token 集合
* 我们最喜欢的猫的 Token
* 并将该 Token 给其他人。

{% hint style="info" %}
请从下面的代码仓库中 clone 最新代码！
{% endhint %}

本教程以您的第一个事务为基础，作为此示例的库。 以下教程包含可以在下面完整下载的示例代码：

{% tabs %}
{% tab title="Python" %}
对于本教程，将重点关注 first\_nft.py 并重新使用上一教程中的 first\_transaction.py 库。 你可以在[这里](https://github.com/aptos-labs/aptos-core/tree/main/developer-docs-site/static/examples/python)找到 python 项目
{% endtab %}

{% tab title="Rust" %}
TODO
{% endtab %}

{% tab title="Typescript" %}
TODO
{% endtab %}
{% endtabs %}

#### 创建一个集合

Aptos Token 使创作者能够创建有限或无限的收藏。 许多 NFT 具有有限的性质，创造者只打算永远创造一定数量，这会导致稀缺性。 而其他代币可能具有无限的性质，例如，用于实用程序的集合可能会随着时间的推移出现新的代币。 SimpleToken 集合可以通过使用适当的脚本函数以任一行为实例化：

有限，即不能超过最大数量的代币可以铸造：

```rust
public(script) fun create_finite_collection_script(
    account: signer,
    description: vector<u8>,
    name: vector<u8>,
    uri: vector<u8>,
    maximum: u64,
)
```

无限制，即可以添加到集合中的 Token 数量没有限制：

```rust
public(script) fun create_unlimited_collection_script(
    account: signer,
    description: vector<u8>,
    name: vector<u8>,
    uri: vector<u8>,
)
```

这些脚本函数可以通过 REST API 调用。 下面演示了如何调用 ，如下所示：

{% tabs %}
{% tab title="Python" %}

```python
    def create_collection(self, account: Account, description: str, name: str, uri: str):
        """Creates a new collection within the specified account"""

        payload = {
            "type": "script_function_payload",
            "function": f"0x1::Token::create_unlimited_collection_script",
            "type_arguments": [],
            "arguments": [
                description.encode("utf-8").hex(),
                name.encode("utf-8").hex(),
                uri.encode("utf-8").hex(),
            ]
        }
        self.submit_transaction_helper(account, payload)
```

{% endtab %}
{% endtabs %}

#### 创建 Token[​](https://aptos.dev/tutorials/your-first-nft#creating-a-token) <a href="#creating-a-token" id="creating-a-token"></a>

可以在集合创建后创建 Token。 为此，Token 必须指定与先前创建的集合名称相同的 collection\_name。 创建 SimpleToken 的 Move 脚本函数是：

```rust
public(script) fun create_token_script(
    account: signer,
    collection_name: vector<u8>,
    description: vector<u8>,
    name: vector<u8>,
    supply: u64,
    uri: vector<u8>,
)
```

这些脚本函数可以通过 REST API 调用。 下面演示了如何调用 ，如下所示：

{% tabs %}
{% tab title="Python" %}

```python
    def create_token(
            self,
            account: Account,
            collection_name: str,
            description: str,
            name: str,
            supply: int,
            uri: str,
    ):
        payload = {
            "type": "script_function_payload",
            "function": f"0x1::Token::create_token_script",
            "type_arguments": [],
            "arguments": [
                collection_name.encode("utf-8").hex(),    
                description.encode("utf-8").hex(),
                name.encode("utf-8").hex(),
                str(supply),
                uri.encode("utf-8").hex(),
            ]
        }
        self.submit_transaction_helper(account, payload)
```

{% endtab %}
{% endtabs %}

#### 发送 Token[​](https://aptos.dev/tutorials/your-first-nft#giving-away-a-token) <a href="#giving-away-a-token" id="giving-away-a-token"></a>

在 Aptos 和 Move 中，每个代币都占据空间并拥有所有权。 因此，代币转移不是单方面的，需要类似于公告板的两个阶段过程。 发送者必须首先注册一个 Token 可供接收者声明，然后接收者必须声明这个 Token。 这已在名为 [`TokenTransfer`](https://github.com/aptos-labs/aptos-core/blob/nft/aptos-move/framework/aptos-framework/sources/TokenTransfers.move) 的概念验证 Move 模块中实现。 SimpleToken 提供了一些包装函数来支持转账到另一个帐户、声明该转账或停止该转账。

**获得 Token ID**[**​**](https://aptos.dev/tutorials/your-first-nft#obtaining-the-token-id)

为了发送代币，发送者必须首先基于知道创建者的账户、集合名称和代币名称来识别代币id。 这可以通过查询 REST 获得：

{% tabs %}
{% tab title="Python" %}

```python
    def get_token_id(self, creator: str, collection_name: str, token_name: str) -> int:
        """ Retrieve the token's creation_num, which is useful for non-creator operations """

        resources = self.account_resources(creator)
        collections = []
        tokens = []
        for resource in resources:
            if resource["type"] == f"0x1::Token::Collections":
                collections = resource["data"]["collections"]["data"]
        for collection in collections:
            if collection["key"] == collection_name:
                tokens = collection["value"]["tokens"]["data"]
        for token in tokens:
            if token["key"] == token_name:
                return int(token["value"]["id"]["creation_num"])
            
        assert False
```

{% endtab %}
{% endtabs %}

**提供 Token**[**​**](https://aptos.dev/tutorials/your-first-nft#offering-the-token)

Token 中的以下 Move 脚本功能支持将 Token 转移到另一个帐户，有效地注册另一个帐户可以获取该 Token：

```rust
public(script) fun offer_script(
    sender: signer,
    receiver: address,
    creator: address,
    token_creation_num: u64,
    amount: u64,
)
```

{% tabs %}
{% tab title="Python" %}

```python
    def offer_token(
            self,
            account: Account,
            receiver: str,
            creator: str,
            token_creation_num: int,
            amount: int
    ):
        payload = {
            "type": "script_function_payload",
            "function": f"0x1::TokenTransfers::offer_script",
            "type_arguments": [],
            "arguments": [
                receiver,
                creator,
                str(token_creation_num),
                str(amount),
            ]
        }
        self.submit_transaction_helper(account, payload)
```

{% endtab %}
{% endtabs %}

**领取 Token**[**​**](https://aptos.dev/tutorials/your-first-nft#claiming-the-token)

SimpleToken 中的以下 Move 脚本函数支持接收上一个函数提供的 Token，有效地声明 Token：

```rust
public(script) fun claim_script(
    sender: signer,
    receiver: address,
    creator: address,
    token_creation_num: u64,
    amount: u64,
)
```

{% tabs %}
{% tab title="Python" %}

```python
    def claim_token(
            self,
            account: Account,
            sender: str,
            creator: str,
            token_creation_num: int,
    ):
        payload = {
            "type": "script_function_payload",
            "function": f"0x1::TokenTransfers::claim_script",
            "type_arguments": [],
            "arguments": [
                sender,
                creator,
                str(token_creation_num),
            ]
        }
        self.submit_transaction_helper(account, payload)
```

{% endtab %}
{% endtabs %}

### 待办 <a href="#todos-for-tokens" id="todos-for-tokens"></a>

* 添加额外的铸造能力
* 确保在铸造时至少产生一个代币
* 添加事件——需要关于什么事件的反馈
* 为 Token 提供可变 API
* 直接为泛型和简单 Token 写一个冒烟测试
* 以安全的方式启用燃烧
