Руководство для разработчиков APTOS | Ваше первое NFT | Typescript
Tags: blockchain, tutorial, developer, aptos
Published: 2022-08-23 16:03:48

> **ВНИМАНИЕ
**> Данное руководство является незавершенным. Спецификация Aptos (Non-Fungible) Token не была формализована.

## Ваше первое NFT

NFT - это невзаимозаменяемый токен или данные, хранящиеся в блокчейне, которые однозначно определяют право собственности на актив. Впервые NFT были определены в EIP-721, а затем расширены в EIP-1155. Как правило, NFT состоят из следующих аспектов:

- Имя, название актива, которое должно быть уникальным в пределах коллекции
- Описание, характеристика актива
- URL, не описанный указатель вне сети на дополнительную информацию об активе, это может быть медиа, например, изображение или видео, или дополнительные метаданные.
- Предложение, общее количество единиц данного NFT, многие NFT имеют только одну единицу предложения, а те, которые имеют более одной, называются коллекциями.

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

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

## Стандарт токенов цифровых активов Aptos
Стандарт токенов Aptos разрабатывается в соответствии со следующими принципами:

- Обеспечить стандартную реализацию для улучшения совместимости между проектами экосистемы.
- Достижение максимальной ликвидности путем определения NFT, Fungible (недесятичных) и Semi-Fungible токенов в одном контракте. Эти различные типы токенов могут легко храниться, передаваться и совершаться одинаковым образом.
- Возможность настройки свойств токенов, пользователи могут определять свои собственные свойства и хранить их в сети.
- Снизить затраты на майнинг большого количества токенов NFT. Мы поддерживаем `lazy` минт на сети с помощью Semi-Fungible токенов

> **РЕАЛИЗАЦИЯ APTOS ДЛЯ ОСНОВНЫХ NFT**
> Реализацию Aptos для основных NFT или токенов можно найти в [Token.move](https://aptos.dev/tutorials/your-first-nft#:~:text=be%20found%20in-,Token.move,-.).

## Определения токенов Aptos

**Модель данных токена**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i37mid8vv66hqx318gjf.jpg)

Данные, связанные с токеном, хранятся как в учетной записи создателя, так и в учетной записи владельца.

Ресурс, хранящийся по адресу создателя:

- Collections - Содержит таблицу `collection_data`, которая сопоставляет имя коллекции с `CollectionData`. В ней также хранятся все TokenData, которые создал данный создатель.
- CollectionData - Хранит метаданные коллекции. `supply` - количество токенов, созданных для текущей коллекции. `maxium` максимальное количество токенов в данной коллекции.
- CollectionMutabilityConfig - Определение об изменении коллекции
- TokenData - Основная структура для хранения метаданных токена. Properties - это место, где пользователь может добавить свои собственные свойства, которые не определены в данных токена. Пользователь может создавать несколько токенов на основе данных TokenData, и они используют одни и те же данные TokenData.	
- TokenMutabilityConfig - Контролирует, какие значения могут быть выполнены.
- TokenDataId - Идентификатор, используемый для представления и запроса TokenData в сети. Этот идентификатор в основном содержит 3 значения, включая адрес создателя, имя коллекции и имя токена.
- Royalty - Укажите знаменатель и числитель для расчета роялти. Здесь также указывается адрес учетной записи получателя для перечисления роялти.
- PropertyValue - Содержит как значение свойства, так и тип свойства.
 
Ресурс хранящиеся по адресу владельца:

- TokenStore - Основная структура для хранения токена, принадлежащего данному адресу. Она сопоставляет TokenId с фактическим токеном. 
- Token - `amount` - количество токенов.
- TokenId - `TokenDataId` указывает на метаданные этого токена. `Property_version` представляет токен с измененной PropertyMap из `default_properties` в TokenData.

## Руководство по токенам

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

- Создание собственной коллекции токенов.
- Создание токена с изображением любимой кошки.
- Дарим этот токен кому-то другому.
- Минт токена с помощью lazy в сети через изменение.

Это руководство основано на [вашей первой транзакции](https://dev.to/egormajj/rukovodstvo-dlia-razrabotchikov-aptos-vasha-piervaia-tranzaktsiia-8fk) в качестве библиотеки для данного примера. Следующее руководство содержит код примера, который можно полностью загрузить ниже:

В этом руководстве мы сосредоточимся на файле `first_nft.ts` и повторно используем библиотеку `first_transaction.ts` из предыдущего руководства.

Вы можете найти проект typescript [здесь](https://aptos.dev/tutorials/your-first-nft/#:~:text=You%20can%20find%20the%20typescript%20project%20here.)

**Создание коллекции**
Токен Aptos позволяет создателям создавать коллекции. `maximum` - это общее количество токенов, которое может быть создано для данной коллекции.

```javascript
public(script) fun create_collection_script (
    creator: &signer,
    name: String,
    description: String,
    uri: String,
    maximum: u64,
    mutate_setting: vector<bool>,
)
```
Эти функции скрипта можно вызывать через REST API. См. ниже:

```javascript
function serializeVectorBool(vecBool: boolean[]) {
  const serializer = new BCS.Serializer();
  serializer.serializeU32AsUleb128(vecBool.length);
  vecBool.forEach((el) => {
    serializer.serializeBool(el);
  });
  return serializer.getBytes();
}

const NUMBER_MAX: number = 9007199254740991;
const client = new AptosClient(NODE_URL);
/** Creates a new collection within the specified account */
async function createCollection(account: AptosAccount, name: string, description: string, uri: string) {
  const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
    TxnBuilderTypes.EntryFunction.natural(
      "0x3::token",
      "create_collection_script",
      [],
      [
        BCS.bcsSerializeStr(name),
        BCS.bcsSerializeStr(description),
        BCS.bcsSerializeStr(uri),
        BCS.bcsSerializeUint64(NUMBER_MAX),
        serializeVectorBool([false, false, false]),
      ],
    ),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(account.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(account.address()),
    BigInt(sequenceNumber),
    entryFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000) + 10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(account, rawTxn);
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);
  await client.waitForTransaction(pendingTxn.hash);
}
```
**Создание токена**

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

```javascript
public entry fun create_token_script(
    creator: &signer,
    collection: String,
    name: String,
    description: String,
    balance: u64,
    maximum: u64,
    uri: String,
    royalty_payee_address: address,
    royalty_points_denominator: u64,
    royalty_points_numerator: u64,
    token_mutate_setting: vector<bool>,
    property_keys: vector<String>,
    property_values: vector<vector<u8>>,
    property_types: vector<String>,
)
```

- Поле `balance` - это начальная сумма, которая будет создана для данного токена.
- `maximum` определяет максимальное количество токенов, которое будет минтиться для данного созданного TokenData.
- Поле `royalty_payee_address` - это адрес, на который выплачивается royalty.
- Количество `royalty_points_numerator` / `royalty_points_denominator` - это процент от цены продажи (`royalty`), который должен быть выплачен на адрес получателя. Это может быть адрес учетной записи одного владельца или адрес общей учетной записи, принадлежащей группе создателей.
- Свойство `token_mutate_setting` описывает, является ли поле `TokenData` изменяемым.
- `Property_keys`, `Property_values` и `Property_types` - это пары ключ-значение свойства, которые можно хранить, читать и записывать в сети.

Эти функции скрипта можно вызывать через REST API. См. ниже:

```javascript
async function createToken(
  account: AptosAccount,
  collection_name: string,
  name: string,
  description: string,
  supply: number | bigint,
  uri: string,
) {
  // Serializes empty arrays
  const serializer = new BCS.Serializer();
  serializer.serializeU32AsUleb128(0);

  const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
    TxnBuilderTypes.EntryFunction.natural(
      "0x3::token",
      "create_token_script",
      [],
      [
        BCS.bcsSerializeStr(collection_name),
        BCS.bcsSerializeStr(name),
        BCS.bcsSerializeStr(description),
        BCS.bcsSerializeUint64(supply),
        BCS.bcsSerializeUint64(NUMBER_MAX),
        BCS.bcsSerializeStr(uri),
        BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(account.address())),
        BCS.bcsSerializeUint64(0),
        BCS.bcsSerializeUint64(0),
        serializeVectorBool([false, false, false, false, false]),
        serializer.getBytes(),
        serializer.getBytes(),
        serializer.getBytes(),
      ],
    ),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(account.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(account.address()),
    BigInt(sequenceNumber),
    entryFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000) + 10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(account, rawTxn);
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);
  await client.waitForTransaction(pendingTxn.hash);
}
```
**Раздача токенов**

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

`SimpleToken` предоставляет несколько обернутых функций для поддержки перевода на другую учетную запись, утверждения этого перевода или прекращения перевода.

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

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

```javascript
async function tableItem(handle: string, keyType: string, valueType: string, key: any): Promise<any> {
  const getTokenTableItemRequest = {
    key_type: keyType,
    value_type: valueType,
    key,
  };
  return client.getTableItem(handle, getTokenTableItemRequest);
}

async function getTokenBalance(
  owner: HexString,
  creator: HexString,
  collection_name: string,
  token_name: string,
): Promise<number> {
  const token_store = await client.getAccountResource(owner, "0x3::token::TokenStore");

  const token_data_id = {
    creator: creator.hex(),
    collection: collection_name,
    name: token_name,
  };

  const token_id = {
    token_data_id,
    property_version: "0",
  };

  const token = await tableItem(
    (token_store.data as any)["tokens"]["handle"],
    "0x3::token::TokenId",
    "0x3::token::Token",
    token_id,
  );

  return token.amount;
}

async function getTokenData(creator: HexString, collection_name: string, token_name: string): Promise<any> {
  const collections = await client.getAccountResource(creator, "0x3::token::Collections");

  const token_data_id = {
    creator: creator.hex(),
    collection: collection_name,
    name: token_name,
  };

  const token = await tableItem(
    (collections.data as any)["token_data"]["handle"],
    "0x3::token::TokenDataId",
    "0x3::token::TokenData",
    token_data_id,
  );
  return token;
}
```
**Предложение токена**

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

```javascript
public entry fun offer_script(
    sender: signer,
    receiver: address,
    creator: address,
    collection: String,
    name: String,
    property_version: u64,
    amount: u64,
)
```

```javascript
async function offerToken(
  account: AptosAccount,
  receiver: HexString,
  creator: HexString,
  collection_name: string,
  token_name: string,
  amount: number,
) {
  const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
    TxnBuilderTypes.EntryFunction.natural(
      "0x3::token_transfers",
      "offer_script",
      [],
      [
        BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(receiver.hex())),
        BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(creator.hex())),
        BCS.bcsSerializeStr(collection_name),
        BCS.bcsSerializeStr(token_name),
        BCS.bcsSerializeUint64(0),
        BCS.bcsSerializeUint64(amount),
      ],
    ),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(account.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(account.address()),
    BigInt(sequenceNumber),
    entryFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000) + 10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(account, rawTxn);
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);
  await client.waitForTransaction(pendingTxn.hash);
}
```
**Заявление прав на токен **

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

```javascript
public entry fun claim_script(
    receiver: signer,
    sender: address,
    creator: address,
    collection: String,
    name: String,
    property_version: u64,
)
```

```javascript
async function claimToken(
  account: AptosAccount,
  sender: HexString,
  creator: HexString,
  collection_name: string,
  token_name: string,
) {
  const entryFunctionPayload = new TxnBuilderTypes.TransactionPayloadEntryFunction(
    TxnBuilderTypes.EntryFunction.natural(
      "0x3::token_transfers",
      "claim_script",
      [],
      [
        BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(sender.hex())),
        BCS.bcsToBytes(TxnBuilderTypes.AccountAddress.fromHex(creator.hex())),
        BCS.bcsSerializeStr(collection_name),
        BCS.bcsSerializeStr(token_name),
        BCS.bcsSerializeUint64(0),
      ],
    ),
  );

  const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
    client.getAccount(account.address()),
    client.getChainId(),
  ]);

  const rawTxn = new TxnBuilderTypes.RawTransaction(
    TxnBuilderTypes.AccountAddress.fromHex(account.address()),
    BigInt(sequenceNumber),
    entryFunctionPayload,
    1000n,
    1n,
    BigInt(Math.floor(Date.now() / 1000) + 10),
    new TxnBuilderTypes.ChainId(chainId),
  );

  const bcsTxn = AptosClient.generateBCSTransaction(account, rawTxn);
  const pendingTxn = await client.submitSignedBCSTransaction(bcsTxn);
  await client.waitForTransaction(pendingTxn.hash);
}
```
**Минт Lazy в сети**

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

Когда Jack хочет купить NFT у Alice, она может изменить один заменимый токен.

```javascript
    public entry fun mutate_token_properties(
        account: &signer,
        token_owner: address,
        creator: address,
        collection_name: String,
        token_name: String,
        token_property_version: u64,
        amount: u64,
        keys: vector<String>,
        values: vector<vector<u8>>,
        types: vector<String>,
    )
```
Это создаст новую `property_version` и создаст новый `TokenId` для предыдущего не инициализированного заменимого токена (`property_version` = 0), который станет NFT. Затем Alice может передать NFT Jack. Alice нужно будет оплатить стоимость создания NFT из заменимого токена только тогда, когда кто-то захочет купить.