# Ваш первый NFT

Примечание: работа над данным руководством еще не завершена. Кроме того, спецификация Aptos' (Non-Fungible) Token (невзаимозаменяемого Aptos токена) не формализована.

## Токены и NFT в сети Aptos

[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-адрес - неопределенный указатель вне сети для получения дополнительной информации об активе, может быть медиа-объект, такой как изображение или видео, или другие метаданные
* Выпуск (supply) - общее количество единиц этого NFT, многие NFT представлены в единичном исполнении, в то время как другие в количестве более одного называются изданиями (editions)

Кроме того, большинство NFT являются частью коллекции или набора NFT с общим атрибутом, например, темой, создателем или минимальным контрактом. Каждая коллекция имеет похожий набор атрибутов:

* Имя - название коллекции, которое должно быть уникальным в учетной записи автора
* Описание - описание актива
* URL-адрес - неопределенный указатель вне сети для получения дополнительной информации об активе, может быть медиа-объект, такой как изображение или видео, или другие метаданные

Реализацию Aptos для основных NFT или Токенов можно найти здесь [Token.move](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/Token.move).

### Определение Aptos Token <a href="#aptos-token-definitions" id="aptos-token-definitions"></a>

#### Токен <a href="#the-token" id="the-token"></a>

Aptos Token определяется как:

<table data-header-hidden><thead><tr><th width="179.33333333333331">Field</th><th width="209.22784810126586">Type</th><th>Description</th></tr></thead><tbody><tr><td><code>id</code></td><td><code>GUID:ID</code></td><td>Глобальный уникальный идентификатор этого токена, также необходим для идентификации автора</td></tr><tr><td><code>name</code></td><td><code>ASCII::String</code></td><td>Имя этого токена должно быть уникальным в рамках коллекции</td></tr><tr><td><code>collection</code></td><td><code>GUID:ID</code></td><td>Глобальный уникальный идентификатор коллекции, содержащей этот токен</td></tr><tr><td><code>balance</code></td><td><code>u64</code></td><td>Текущее хранимое количество данного токена по отношению к общему выпуску (supply), <code>1 &#x3C;= balance &#x3C;= supply</code></td></tr></tbody></table>

Aptos TokenData определяется как:

| Field         | Type            | Description                                                                                                         |
| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------- |
| `id`          | `GUID:ID`       | Глобальный уникальный идентификатор для этого токена, также необходим для идентификации автора                      |
| `description` | `ASCII::String` | Описывает этот токен                                                                                                |
| `name`        | `ASCII::String` | Имя этого токена должна быть уникальным в рамках коллекции                                                          |
| `supply`      | `u64`           | Общее количество изданий этого Токена                                                                               |
| `uri`         | `ASCII::String` | URL-адрес для дополнительной информации/медиа                                                                       |
| `metadata`    | `TokenType`     | Общая опционально определяемая пользователем структура, содержащая дополнительную информацию об этом токене он-чейн |

Токены определяются с помощью хранилища атрибутов move  `store`, что означает, что их можно сохранять в глобальном хранилище. Токены не могут быть удалены неявным образом и должны быть сожжены, чтобы гарантировать, что общий баланс равен общему выпуску (supply). Токены не могут быть скопированы. То есть общий баланс или выпуск не может быть изменен никем, кроме автора, из-за отсутствия оператора копирования. Обратите внимание, что текущие API не предоставляют возможности минта после создания. Токен может быть однозначно идентифицирован либо по его `id` , или `TokenType, collection name, and token name`.&#x20;

TokenData имеет атрибут `copy` для упрощения разделения баланса токенов. Разделение токенов может произойти всякий раз, когда пользователь с балансом больше `1` предлагает другому лицу часть своего баланса, меньшую, чем общий баланс. Пользователям, торгующим токенами, следует помнить, что два Токена могут использовать одни и те же данные TokenData, поскольку стандарт Aptos не пытается определить, копирует ли токен атрибуты другого. Повторяя то, что было сказано ранее, токен может быть однозначно идентифицирован либо по его `id` , либо `TokenType`, имени коллекции, имени токена. Автор может изменить любое значение в`TokenType`, имени коллекции или имени токена, чтобы создать похожие, но не идентичные токены.

#### Коллекция Токенов <a href="#the-token-collection" id="the-token-collection"></a>

Aptos определяет набор коллекций, сгруппированных по их уникальным `id`:

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

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

Так как `Collections` имеет <mark style="background-color:red;">attribute</mark> `key`, он хранится непосредственно в учетной записи автора. Важно отметить, что если бы не было категории `Collections` , а вместо этого`Collection` имела бы `key` <mark style="background-color:red;">attribute</mark>, учетная запись Aptos могла бы иметь только одну коллекцию, что часто бывает не так. Коллекцию можно искать в наборе Коллекций по имени, что позволяет использовать уникальное имя коллекции.

Структуры Token и TokenData фиксированы по своему содержанию. Ресурс`TokenMetadata` позволяет создателям хранить дополнительные данные токена. Данные в таблице хранятся как уникальный `Token`'s `ID`. Использование этого является опциональным и требует специализации API из-за ограничения, заключающегося в том, что функции скрипта не могут поддерживать структуры или дженерики.

Каждая коллекция имеет следующие поля:

| Field            | Type                                             | Description                                                                                     |
| ---------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| `tokens`         | `Table<ASCII::String, TokenMetadata<TokenType>>` | Отслеживает все Токены, связанные с этой коллекцией                                             |
| `claimed_tokens` | `Table<ASCII::String, address>`                  | Отслеживает, где хранятся токены с `supply == 1`                                                |
| `description`    | `ASCII::String`                                  | Описывает эту коллекцию                                                                         |
| `name`           | `ASCII::String`                                  | Имя этой коллекции должно быть уникальным в учетной записи создателя для указанного `TokenType` |
| `uri`            | `ASCII::String`                                  | URL-адрес для дополнительной информации/медиа                                                   |
| `count`          | `u64`                                            | Общее количество отдельных токенов, отслеживаемых этой коллекцией                               |
| `maximum`        | `Option<u64>`                                    | Опционально, максимальное количество токенов, которое можно создать в этой коллекции            |

Коллекция не является хранилищем для накопления Токенов, поэтому вместо `Token`она содержит `TokenData`:

| Field  | Type        | Description                                                                                    |
| ------ | ----------- | ---------------------------------------------------------------------------------------------- |
| `id`   | `GUID:ID`   | Глобальный уникальный идентификатор для этого токена, также необходим для идентификации автора |
| `data` | `TokenData` | Дополнительные данные об этом токене, это набор выпуска токена `> 1`                           |

#### Хранение Токенов <a href="#storing-tokens" id="storing-tokens"></a>

Чтобы приобретать и хранить токены, у пользователя должна быть  `Gallery` `TokenType`:

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

Как и `Collections`, это хранится как ресурс в учетной записи Aptos.

### Представление Токенов <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 не имеет записи для этого токена. Мотивация такого решения включает следующее:

* Для создания нового токена требуется написать код Move
* Функция Script для создания нового токена должна быть специализирована, поскольку Move не поддерживает типы шаблонов или структуры в качестве входных аргументов
* Типы шаблонов в функциях скрипта добавляют дополнительные трудности при написании функций скрипта

Это руководство проведет вас через процессы:

* создания вашей собственной коллекции Токенов,
* Токена вашего любимого кота,
* а также передачи этого токена кому-либо другому.

Данное руководство основано на разделе [Ваша первая транзакция](/aptos-ru/rukovodstva-po-primeneniyu/vasha-pervaya-tranzakciya.md) в качестве библиотеки для этого примера. Руководство содержит пример кода, который можно загрузить, см. пример ниже:

{% tabs %}
{% tab title="Python" %}
В этом руководстве основное внимание будет уделено `first_nft.py` и повторному использованию библиотеки`first_transaction.py` из прошлого руководства.

Вы найдете python project [здесь](https://github.com/aptos-labs/aptos-core/tree/main/developer-docs-site/static/examples/python)
{% endtab %}

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

{% tab title="Typescript" %}
В этом руководстве основное внимание будет уделено `first_nft.ts` и повторному использованию библиотеки`first_transaction.ts` из предыдущего руководства.

Вы найдете typescript project [здесь](https://github.com/aptos-labs/aptos-core/tree/main/developer-docs-site/static/examples/typescript)
{% endtab %}
{% endtabs %}

#### Создание Коллекции <a href="#creating-a-collection" id="creating-a-collection"></a>

Aptos Token позволяет создателям создавать ограниченные или неограниченные коллекции. Многие NFT носят ограниченный характер, когда автор намерен создать только определенное количество навсегда, что усиливает дефицит. В то время как другие токены могут иметь неограниченную природу, например, в коллекции, служащей для утилиты, со временем могут появляться новые токены. Коллекции SimpleToken могут быть созданы с помощью соответствующей функции скрипта:

Для конечной, то есть не больше, чем максимальное количество токенов `maximum` , которое может быть когда-либо выпущено:

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

Для безлимитной, то есть количество токенов, которые можно добавить в коллекцию, не ограничено:

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

Эти функции скрипта можно запустить через REST API. Далее показано, как это сделать:

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

```
    def create_collection(self, account: Account, name: str, description, 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": [
                name.encode("utf-8").hex(),
                description.encode("utf-8").hex(),
                uri.encode("utf-8").hex(),
            ]
        }
        self.submit_transaction_helper(account, payload)
```

{% endtab %}

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

{% tab title="Typescript" %}

```
  /** Creates a new collection within the specified account */
  async createCollection(account: Account, name: string, description: string, uri: string) {
    const payload: { function: string; arguments: string[]; type: string; type_arguments: any[] } = {
      type: "script_function_payload",
      function: "0x1::Token::create_unlimited_collection_script",
      type_arguments: [],
      arguments: [
        Buffer.from(name).toString("hex"),
        Buffer.from(description).toString("hex"),
        Buffer.from(uri).toString("hex"),
      ],
    };
    await this.submitTransactionHelper(account, payload);
  }
  
```

{% endtab %}
{% endtabs %}

#### Создание Токена

Токены могут быть созданы после создания коллекции. Для этого в токене должно быть указано то же самое имя коллекции `collection_name` , которое указано в ранее созданной коллекции `name`. Функция сценария Move для создания `SimpleToken` :

```
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" %}

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

{% endtab %}

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

{% tab title="Typescript" %}

```
  async createToken(
    account: Account,
    collection_name: string,
    name: string,
    description: string,
    supply: number,
    uri: string,
  ) {
    const payload: { function: string; arguments: any[]; type: string; type_arguments: any[] } = {
      type: "script_function_payload",
      function: "0x1::Token::create_unlimited_token_script",
      type_arguments: [],
      arguments: [
        Buffer.from(collection_name).toString("hex"),
        Buffer.from(name).toString("hex"),
        Buffer.from(description).toString("hex"),
        true,
        supply.toString(),
        Buffer.from(uri).toString("hex"),
        "0",
      ],
    };
    await this.submitTransactionHelper(account, payload);
  }
  
```

{% endtab %}
{% endtabs %}

#### Передача Токена <a href="#giving-away-a-token" id="giving-away-a-token"></a>

В Aptos и Move каждый токен занимает определенное место и на него имеется право собственности. Из-за этого передача токенов не является односторонней и требует двухэтапного процесса, аналогичного доске объявлений. Отправитель должен сначала зарегистрировать, что токен доступен для запроса получателем, а затем получатель должен потребовать этот токен. Это реализовано в экспериментальном move модуле [`TokenTransfer`](https://github.com/aptos-labs/aptos-core/blob/nft/aptos-move/framework/aptos-framework/sources/TokenTransfers.move). `SimpleToken` предоставляет несколько функций-оболочек для поддержки трансфера токена в другую учетную запись, подтверждения трансфера или его остановки.

**Получение ID Токена**

Чтобы передать токен, отправитель должен сначала определить идентификатор токена, зная учетную запись создателя, имя коллекции и имя токена. Это можно получить, сделав запрос в REST сервис:

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

```
    def get_table_item(self, handle: str, key_type: str, value_type: str, key: Any) -> Any:
        response = requests.post(f"{self.url}/tables/{handle}/item", json={
            "key_type": key_type,
            "value_type": value_type,
            "key": key,
        })
        assert response.status_code == 200, response.text
        return response.json()

    def get_token_balance(self, owner: str, creator: str, collection_name: str, token_name: str) -> Any:
        token_store = self.account_resource(owner, "0x1::Token::TokenStore")["data"]["tokens"]["handle"]

        token_id = {
            "creator": creator,
            "collection": collection_name,
            "name": token_name,
        }

        return self.get_table_item(
            token_store,
            "0x1::Token::TokenId",
            "0x1::Token::Token",
            token_id,
        )["value"]

    def get_token_data(self, creator: str, collection_name: str, token_name: str) -> Any:
        token_data = self.account_resource(creator, "0x1::Token::Collections")["data"]["token_data"]["handle"]

        token_id = {
            "creator": creator,
            "collection": collection_name,
            "name": token_name,
        }

        return self.get_table_item(
            token_data,
            "0x1::Token::TokenId",
            "0x1::Token::TokenData",
            token_id,
        )

    def get_collection(self, creator: str, collection_name: str) -> Any:
        token_data = self.account_resource(creator, "0x1::Token::Collections")["data"]["collections"]["handle"]

        return self.get_table_item(
            token_data,
            "0x1::ASCII::String",
            "0x1::Token::Collection",
            collection_name,
        )
```

{% endtab %}

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

{% tab title="Typescript" %}

```
  async tableItem(handle: string, keyType: string, valueType: string, key: any): Promise<any> {
    const response = await fetch(`${this.restClient.url}/tables/${handle}/item`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        key_type: keyType,
        value_type: valueType,
        key: key,
      }),
    });

    if (response.status == 404) {
      return null;
    } else if (response.status != 200) {
      assert(response.status == 200, await response.text());
    } else {
      return await response.json();
    }
  }

  async getTokenBalance(owner: string, creator: string, collection_name: string, token_name: string): Promise<number> {
    const token_store = await this.restClient.accountResource(creator, "0x1::Token::TokenStore");

    const token_id = {
      creator: creator,
      collection: collection_name,
      name: token_name,
    };

    const token = await this.tableItem(
      token_store["data"]["tokens"]["handle"],
      "0x1::Token::TokenId",
      "0x1::Token::Token",
      token_id,
    );
    return token["value"];
  }

  async getTokenData(creator: string, collection_name: string, token_name: string): Promise<any> {
    const collections = await this.restClient.accountResource(creator, "0x1::Token::Collections");

    const token_id = {
      creator: creator,
      collection: collection_name,
      name: token_name,
    };

    return await this.tableItem(
      collections["data"]["token_data"]["handle"],
      "0x1::Token::TokenId",
      "0x1::Token::TokenData",
      token_id,
    );
  }
  
```

{% endtab %}
{% endtabs %}

#### Предложение Токена

Следующая функция сценария Move поддерживает передачу `Token`  в другую учетную запись, фактически регистрируя, что другая учетная запись может претендовать на этот токен:

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

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

```
    def offer_token(
            self,
            account: Account,
            receiver: str,
            creator: str,
            collection_name: str,
            token_name: str,
            amount: int
    ):
        payload = {
            "type": "script_function_payload",
            "function": f"0x1::TokenTransfers::offer_script",
            "type_arguments": [],
            "arguments": [
                receiver,
                creator,
                collection_name.encode("utf-8").hex(),
                token_name.encode("utf-8").hex(),
                str(amount),
            ]
        }
        self.submit_transaction_helper(account, payload)
```

{% endtab %}

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

{% tab title="Typescript" %}

```
  async offerToken(
    account: Account,
    receiver: string,
    creator: string,
    collection_name: string,
    token_name: string,
    amount: number,
  ) {
    const payload: { function: string; arguments: string[]; type: string; type_arguments: any[] } = {
      type: "script_function_payload",
      function: "0x1::TokenTransfers::offer_script",
      type_arguments: [],
      arguments: [
        receiver,
        creator,
        Buffer.from(collection_name).toString("hex"),
        Buffer.from(token_name).toString("hex"),
        amount.toString(),
      ],
    };
    await this.submitTransactionHelper(account, payload);
  }
  
```

{% endtab %}
{% endtabs %}

#### Запрос Токена

Следующая функция скрипта Move в `SimpleToken` поддерживает получение токена, фактически запрашивая токен:

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

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

```
    def claim_token(
            self,
            account: Account,
            sender: str,
            creator: str,
            collection_name: str,
            token_name: str,
    ):
        payload = {
            "type": "script_function_payload",
            "function": f"0x1::TokenTransfers::claim_script",
            "type_arguments": [],
            "arguments": [
                sender,
                creator,
                collection_name.encode("utf-8").hex(),
                token_name.encode("utf-8").hex(),
            ]
        }
        self.submit_transaction_helper(account, payload)
```

{% endtab %}

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

{% tab title="Typescript" %}

```
  async claimToken(account: Account, sender: string, creator: string, collection_name: string, token_name: string) {
    const payload: { function: string; arguments: string[]; type: string; type_arguments: any[] } = {
      type: "script_function_payload",
      function: "0x1::TokenTransfers::claim_script",
      type_arguments: [],
      arguments: [
        sender,
        creator,
        Buffer.from(collection_name).toString("hex"),
        Buffer.from(token_name).toString("hex"),
      ],
    };
    await this.submitTransactionHelper(account, payload);
  }
  
```

{% endtab %}
{% endtabs %}

### Задачи для реализации по Токенам <a href="#todos-for-tokens" id="todos-for-tokens"></a>

* Добавить возможность дополнительного минта
* Убедиться, что был произведен хотя бы один токен во время минта
* Добавить события — требуется фидбэк о том, какие события
* Предоставить изменяемые API для токенов
* Написать дымовое тестирование для дженериков и простых токенов напрямую
* Обеспечить безопасное сжигание


---

# 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://cr-nepos.gitbook.io/aptos-ru/rukovodstva-po-primeneniyu/vash-pervyi-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.
