# 您的第一个 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 写一个冒烟测试
* 以安全的方式启用燃烧


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wiki.aptos.movemove.org/jiao-cheng/nin-de-di-yi-ge-nft.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
