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

В этом руководстве вы узнаете, как создать [dapp](https://en.wikipedia.org/wiki/Decentralized_application) на блокчейне Aptos. Обычно dapp состоит из пользовательского интерфейса, написанного на JavaScript, который взаимодействует с одним или несколькими Move-модулями.

В этом руководстве мы будем использовать Move-модуль `HelloBlockchain`, описанный в разделе [Ваш первый модуль Move](https://dev.to/egormajj/rukovodstvo-dlia-razrabotchikov-aptos-vash-piervyi-modul-move-typescript-41g0), и сосредоточимся на создании пользовательского интерфейса.

Мы будем использовать:

- [Aptos SDK](https://aptos.dev/tutorials/your-first-dapp#:~:text=The-,Aptos%20SDK,-.).
- [Aptos Wallet](https://aptos.dev/tutorials/your-first-dapp#:~:text=The-,Aptos%20Wallet,-%2C%20and), и 
- [Aptos CLI](https://aptos.dev/tutorials/your-first-dapp#:~:text=The-,Aptos%20CLI,-to%20interact%20with) для взаимодействия с блокчейном.

Конечным результатом будет dapp, позволяющий пользователям публиковать и делиться фрагментами текста на блокчейне Aptos.

Полный исходный код этого руководства доступен [здесь](https://aptos.dev/tutorials/your-first-dapp#:~:text=tutorial%20is%20available-,here,-.).

## Необходимые условия

**Aptos Wallet**

Перед началом этого урока необходимо [установить расширение Aptos Wallet](https://aptos.dev/tutorials/your-first-dapp#:~:text=Aptos%20Wallet%20extension).

После того как вы его установите:

1. Откройте Кошелек и нажмите **Создать новый кошелек**. Затем нажмите **Создать аккаунт**, чтобы создать аккаунт Aptos.
2. Скопируйте приватный ключ. Он понадобится вам для настройки Aptos CLI в следующем разделе.

> **ПРИМЕЧАНИЕ**
> Убедитесь, что на вашем счете достаточно средств для проведения транзакций, нажав кнопку Faucet.

**Aptos CLI**

1. Установите [Aptos CLI](https://aptos.dev/tutorials/your-first-dapp#:~:text=Install%20the-,Aptos%20CLI,-.).
2. Запустите `aptos init`, и когда он запросит ваш закрытый ключ, вставьте закрытый ключ из Aptos Wallet, который вы скопировали ранее. Это инициализирует Aptos CLI для использования той же учетной записи, которая используется в Aptos Wallet.
3. Запустите `aptos account list`, чтобы убедиться, что все работает.

## Шаг 1: Создайте одностраничное приложение

Теперь мы настроим внешний пользовательский интерфейс для нашего dapp. В этом руководстве мы будем использовать `[create-react-app](https://aptos.dev/tutorials/your-first-dapp#:~:text=We%20will%20use-,create%2Dreact%2Dapp,-to%20set%20up)` для настройки приложения, но ни React, ни `create-react-app` не являются обязательными. Вы можете использовать ваш любимый JavaScript фреймворк.

```
$ npx create-react-app first-dapp --template typescript
$ cd first-dapp
$ npm start

```
Теперь у вас есть базовое приложение React, запущенное в браузере.

## Шаг 2: Интеграция API Web3 Aptos Wallet

Aptos Wallet предоставляет Web3 API для dapps в `window.aptos`. Вы можете увидеть, как он работает, открыв консоль браузера и выполнив await `window.aptos.account()`. Он выведет адрес, соответствующий учетной записи, которую вы создали в Aptos Wallet.

Далее мы обновим наше приложение, чтобы использовать этот API для отображения адреса учетной записи кошелька.

**Подождите, пока `window.aptos` будет определен**

Первым шагом при интеграции с API `window.aptos` является задержка отображения приложения до наступления события `window.onload`.

Откройте файл `src/index.tsx` и измените следующий фрагмент кода:

```javascript 
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
```
в этом:

```javascript 
window.addEventListener('load', () => {
  root.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>
  );
});
```
Это изменение гарантирует, что API `window.aptos` будет инициализирован к моменту отображения приложения (если мы выполним отображение слишком рано, расширение Wallet, возможно, еще не успеет инициализировать API и, таким образом, `window.aptos` будет `undefined`).

**(Необязательно) Настройка TypeScript для `window.aptos`**

Если вы используете TypeScript, вы также можете сообщить компилятору о существовании API `window.aptos`. Добавьте следующее в `src/index.tsx`:

```javascript
declare global {
  interface Window { aptos: any; }
}
```
Это позволяет нам использовать API `window.aptos` без необходимости делать `(window as any).aptos`.

**Отображение `window.aptos.account()` в приложении**

Теперь наше приложение готово к использованию API `window.aptos.` Мы изменим `src/App.tsx`, чтобы получить значение `window.aptos.account()` ( учетная запись кошелька) при начальном отображении, сохранить его в состоянии, а затем отобразить:

```javascript
import React from 'react';
import './App.css';

function App() {
  // Retrieve aptos.account on initial render and store it.
  const [address, setAddress] = React.useState<string | null>(null);
  React.useEffect(() => {
    window.aptos.account().then((data : {address: string}) => setAddress(data.address));
  }, []);

  return (
    <div className="App">
      <p><code>{ address }</code></p>
    </div>
  );
}

export default App;
```
Обновите страницу, и вы увидите адрес своей учетной записи.

**Добавим некоторые CSS**

Затем замените содержимое `src/App.css`:

```javascript
a, input, textarea {
  display: block;
}

textarea {
  border: 0;
  min-height: 50vh;
  outline: 0;
  padding: 0;
  width: 100%;
}
```

## Шаг 3: Используйте SDK для получения данных из блокчейна

Кошелек теперь интегрирован в наш dapp. Далее мы интегрируем Aptos SDK для получения данных из блокчейна. Мы будем использовать Aptos SDK для получения информации о нашей учетной записи и отображения этой информации на странице.

**Добавьте `aptos` в `package.json`**

Сначала добавьте SDK в проект:

```javascript
$ npm install --save aptos
```
Теперь вы увидите `"aptos": "^0.0.20"` (или подобное) в вашем `package.json`.

**Создайте `AptosClient`**
Теперь мы можем импортировать SDK и создать `AptosClient` для взаимодействия с блокчейном (технически он взаимодействует с [REST API](https://aptos.dev/tutorials/your-first-dapp#:~:text=it%20interacts%20with-,the%20REST%20API,-%2C%20which%20interacts%20with), который взаимодействует с блокчейном).

Поскольку наша учетная запись кошелька находится в devnet, мы настроим `AptosClient` для взаимодействия с devnet. Добавьте следующее в `src/App.tsx`:

```javascript
import { Types, AptosClient } from 'aptos';

// Create an AptosClient to interact with devnet.
const client = new AptosClient('https://fullnode.devnet.aptoslabs.com/v1');

function App() {
  // ...

  // Use the AptosClient to retrieve details about the account.
  const [account, setAccount] = React.useState<Types.AccountData | null>(null);
  React.useEffect(() => {
    if (!address) return;
    client.getAccount(address).then(setAccount);
  }, [address]);

  return (
    <div className="App">
      <p><code>{ address }</code></p>
      <p><code>{ account?.sequence_number }</code></p>
    </div>
  );
}
```
Теперь, помимо отображения адреса учетной записи, приложение будет также отображать `sequence_number` учетной записи. Этот `sequence_number` представляет собой порядковый номер следующей транзакции для предотвращения атак воспроизведения транзакций. Вы увидите, что этот номер увеличивается по мере совершения операций с учетной записью.

## Шаг 4: Публикация модуля Move

Теперь наш dapp настроен на чтение из блокчейна. Следующим шагом будет запись в блокчейн. Для этого мы опубликуем модуль Move на нашей учетной записи.

Модуль Move предоставляет место для хранения этих данных. В частности, мы будем использовать модуль `HelloBlockchain` из вашего [первого модуля Move](https://dev.to/egormajj/rukovodstvo-dlia-razrabotchikov-aptos-vash-piervyi-modul-move-typescript-41g0), который предоставляет ресурс `MessageHolder`, хранящий строку (называемую `message`).

**Публикация модуля `HelloBlockchain` с помощью Aptos CLI**

Мы будем использовать Aptos CLI для компиляции и публикации модуля `HelloBlockchain`.

1. Скачайте [`hello_blockchain` package](https://aptos.dev/tutorials/your-first-dapp#:~:text=the%20hello_blockchain%20package).
2. Затем используйте команду `aptos move publish` (заменив `/path/to/hello_blockchain/` и `<address>`):

```javascript
$ aptos move publish --package-dir /path/to/hello_blockchain/ --named-addresses HelloBlockchain=<address>
```
Например:

```javascript
$ aptos move publish --package-dir ~/code/aptos-core/aptos-move/move-examples/hello_blockchain/ --named-addresses HelloBlockchain=0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481
```
Параметр `--named-addresses` заменяет именованный адрес `HelloBlockchain` в `HelloBlockchain.move` на указанный адрес. Например, если мы укажем `--named-addresses HelloBlockchain=0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481`, то произойдет следующее:

```javascript
module HelloBlockchain::Message {
```
станет:

```javascript
module 0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::Message {
```
Это позволяет опубликовать модуль для данной учетной записи (в данном случае нашей учетной записи кошелька, `0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481`).

Предполагая, что на вашей учетной записи достаточно средств для выполнения транзакции, теперь вы можете опубликовать модуль `HelloBlockchain` в своей учетной записи. Если вы обновите приложение, то увидите, что порядковый номер счета увеличился с 0 до 1.

Вы также можете проверить, что модуль был опубликован, зайдя в [Aptos Explorer](https://aptos.dev/tutorials/your-first-dapp#:~:text=going%20to%20the-,Aptos%20Explorer,-and%20looking%20up) и найдя свою учетную запись. Если вы прокрутите вниз до раздела "Модули учетной записи", вы должны увидеть что-то вроде следующего:

```javascript
{
  "address": "0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481",
  "name": "Message",
  "friends": [],
  "exposedFunctions": [
    {
      "name": "get_message",
      "visibility": "public",
      "genericTypeParams": [],
      "params": [
        "address"
      ],
      "_return": [
        "0x1::string::String"
      ]
    },
    {
      "name": "set_message",
      "visibility": "script",
      "genericTypeParams": [],
      "params": [
        "signer",
        "vector"
      ],
      "_return": []
    }
  ],
  "structs": [
    {
      "name": "MessageChangeEvent",
      "isNative": false,
      "abilities": [
        "drop",
        "store"
      ],
      "genericTypeParams": [],
      "fields": [
        {
          "name": "from_message",
          "type": "0x1::string::String"
        },
        {
          "name": "to_message",
          "type": "0x1::string::String"
        }
      ]
    },
    {
      "name": "MessageHolder",
      "isNative": false,
      "abilities": [
        "key"
      ],
      "genericTypeParams": [],
      "fields": [
        {
          "name": "message",
          "type": "0x1::string::String"
        },
        {
          "name": "message_change_events",
          "type": "0x1::event::EventHandle<0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::Message::MessageChangeEvent>"
        }
      ]
    }
  ]
}
```
Запишите `"name": "Message"`, мы будем использовать его в следующем разделе.

**Добавьте инструкции по публикации модулей в dapp**

Для удобства пользователей мы можем заставить приложение отображать команду `aptos move publish`, если модуль не существует. Для этого мы воспользуемся Aptos SDK для получения модулей учетных записей и поищем тот, в котором `module.abi.name` равен `"Message"` (т.е. `"name": "Message"`, который мы видели в Aptos Explorer).

Обновите `src/App.tsx`:

```javascript
function App() {
  // ...

  // Check for the module; show publish instructions if not present.
  const [modules, setModules] = React.useState<Types.MoveModule[]>([]);
  React.useEffect(() => {
    if (!address) return;
    client.getAccountModules(address).then(setModules);
  }, [address]);

  const hasModule = modules.some((m) => m.abi?.name === 'Message');
  const publishInstructions = (
    <pre>
      Run this command to publish the module:
      <br />
      aptos move publish --package-dir /path/to/hello_blockchain/
      --named-addresses HelloBlockchain={address}
    </pre>
  );

  return (
    <div className="App">
      {!hasModule && publishInstructions}
    </div>
  );
}
```
Новые пользователи смогут использовать эту команду для создания страницы для своей учетной записи.

## Шаг 5. Записать сообщение в блокчейне

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

**Транзакция, вызывающая функцию `set_message`**

Подпись для `set_message` выглядит следующим образом:

```javascript 
public(script) fun set_message(account: signer, message_bytes: vector<u8>)
```

Чтобы вызвать эту функцию, нам нужно использовать API `window.aptos`, предоставляемый кошельком для отправки транзакции. В частности, мы создадим транзакцию `script_function_payload`, которая будет выглядеть следующим образом:

```javascript
{
  type: "script_function_payload",
  function: "<address>::Message::set_message",
  arguments: ["<hex encoded utf-8 message>"],
  type_arguments: []
}
```
Нет необходимости указывать аргумент `account: signer`. Aptos предоставляет его автоматически.

Однако нам необходимо указать аргумент `message_bytes`: это `"<hex encoded utf-8 message>"` в транзакции. Нам нужен способ преобразовать строку JS в этот формат. Мы можем сделать это, используя `TextEncoder` для преобразования в байты utf-8, а затем однострочное предложение для шестнадцатеричной кодировки байтов.

Добавьте эту функцию в `src/App.tsx`:

```javascript
/** Convert string to hex-encoded utf-8 bytes. */
function stringToHex(text: string) {
  const encoder = new TextEncoder();
  const encoded = encoder.encode(text);
  return Array.from(encoded, (i) => i.toString(16).padStart(2, "0")).join("");
}
```
Используя эту функцию, наша полезная нагрузка транзакции становится:

```javascript
{
  type: "script_function_payload",
  function: "<address>::Message::set_message",
  arguments: [stringToHex(message)],
  type_arguments: []
}
```
**Используйте API `window.aptos` для отправки транзакции `set_message`**

Теперь, когда мы поняли, как использовать транзакцию для вызова функции `set_message`, мы вызовем эту функцию из нашего приложения с помощью `window.aptos.signAndSubmitTransaction()`.

Мы добавим:

- где `<textarea>` пользователь может ввести сообщение, и
- `<button>`, который вызывает `set_message` функцию с содержимым файла `<textarea>`.

Обновите `src/App.tsx`:

```javascript
function App() {
  // ...

  // Call set_message with the textarea value on submit.
  const ref = React.createRef<HTMLTextAreaElement>();
  const [isSaving, setIsSaving] = React.useState(false);
  const handleSubmit = async (e: any) => {
    e.preventDefault();
    if (!ref.current) return;

    const message = ref.current.value;
    const transaction = {
      type: "script_function_payload",
      function: `${address}::Message::set_message`,
      arguments: [stringToHex(message)],
      type_arguments: [],
    };

    try {
      setIsSaving(true);
      await window.aptos.signAndSubmitTransaction(transaction);
    } finally {
      setIsSaving(false);
    }
  };

  return (
    <div className="App">
      {hasModule ? (
        <form onSubmit={handleSubmit}>
          <textarea ref={ref} />
          <input disabled={isSaving} type="submit" />
        </form>
      ) : publishInstructions}
    </div>
  );
}
```
Для проверки:

- Введите что-нибудь в `<textarea>` и отправьте форму.
- Найдите свою учетную запись в [Aptos Explorer](https://aptos.dev/tutorials/your-first-dapp#:~:text=account%20in%20the-,Aptos%20Explorer,-and%20you%20will), и теперь вы увидите ресурс `MessageHolder` в разделе Account Resources с написанным вами `message`.

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

## Шаг 6. Отображение сообщения в dapp

Теперь, когда ресурс `MessageHolder` создан, мы можем использовать Aptos SDK для его получения и отображения сообщения.

**Получить сообщение о состоянии учетной записи кошелька**

Чтобы получить сообщение, мы сделаем следующее:

- Сначала воспользуемся функцией `AptosClient.getAccountResources()` для получения ресурсов учетной записи и хранения их в состоянии.
- Затем мы будем искать тот, `type` которого `MessageHolder`. Полный тип - `$address::Message::MessageHolder`, поскольку он является частью модуля `$address::Message`.

В нашем примере это:
 
```javascript
0x5af503b5c379bd69f46184304975e1ef1fa57f422dd193cdad67dc139d532481::Message::MessageHolder
```

- Мы будем использовать его для начального значения `<textarea>`.

Обновите `src/App.tsx`:

```javascript
function App() {
  // ...

  // Get the message from account resources.
  const [resources, setResources] = React.useState<Types.AccountResource[]>([]);
  React.useEffect(() => {
    if (!address) return;
    client.getAccountResources(address).then(setResourdces);
  }, [address]);
  const resourceType = `${address}::Message::MessageHolder`;
  const resource = resources.find((r) => r.type === resourceType);
  const data = resource?.data as {message: string} | undefined;
  const message = data?.message;

  return (
    // ...
          <textarea ref={ref} defaultValue={message} />
    // ...
  );
}
```
Для проверки:

- Обновите страницу и увидите сообщение, которое вы написали ранее.
- Измените текст, отправьте форму и снова обновите страницу. Вы увидите, что содержимое страницы было обновлено вашим новым сообщением.

Это подтверждает, что вы читаете и записываете сообщения на блокчейне Aptos.

**Отображение сообщений с других учетных записей**

На данный момент мы создали "однопользовательский" dapp, в котором вы можете читать и писать сообщения на своей учетной записи. Далее мы сделаем так, чтобы другие люди могли читать сообщения, включая тех, у кого не установлен Aptos Wallet.

Мы настроим его так, чтобы переход по URL `/<account address>` отображал сообщение, хранящееся по адресу `/<account address>` (если оно существует).

- Если приложение загружено по адресу `/<account address>`, мы также отключим редактирование.
- Если редактирование включено, мы покажем ссылку "Get public URL", чтобы вы могли поделиться своим сообщением

Обновите `src/App.tsx`:

```javascript
function App() {
  // Retrieve aptos.account on initial render and store it.
  const urlAddress = window.location.pathname.slice(1);
  const isEditable = !urlAddress;
  const [address, setAddress] = React.useState<string | null>(null);
  React.useEffect(() => {
    if (urlAddress) {
      setAddress(urlAddress);
    } else {
      window.aptos.account().then((data : {address: string}) => setAddress(data.address));
    }
  }, [urlAddress]);

  // ...

  return (
    <div className="App">
      {hasModule ? (
        <form onSubmit={handleSubmit}>
          <textarea ref={ref} defaultValue={message} readOnly={!isEditable} />
          {isEditable && (<input disabled={isSaving} type="submit" />)}
          {isEditable && (<a href={address!}>Get public URL</a>)}
        </form>
      ) : publishInstructions}
    </div>
  );
}
```
На этом мы завершаем данное руководство.