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

В этом техническом руководстве вы узнаете, как создать простой сквозной dApp, который позволяет пользователю получать и хранить текущую цену Ethereum в смарт-контракте. Готовую демонстрационную версию можно найти на GitHub.

Требования

Пожалуйста, убедитесь, что у вас установлено следующее:

Что такое децентрализованное приложение?

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

Благодаря тому, что логика бэкенда размещается в высокозащищенных, защищенных от взлома смарт-контрактах, dApp имеют множество преимуществ, недоступных для традиционных систем Web2:

  • нулевое время простоя

  • повышенная конфиденциальность

  • устойчивость к цензуре

  • выполнение логики с минимальным уровнем доверия

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

Компоненты dApp

Компоненты dApp можно разделить на три различные категории: смарт-контракты, фронтенд-логика и пользовательский интерфейс, а также хранилище данных.

Смарт-контракты Умные контракты хранят бизнес-логику dApp, а также состояние приложения. Это самое большое отличие dApp от традиционного веб-приложения, и именно это дает dApp все преимущества, упомянутые выше.

Фронтенд/интерфейс пользователя В то время как логика бэкенда dApp требует от разработчика написания кода смарт-контракта для развертывания на блокчейне, фронтенд или клиентская часть dApp может использовать стандартные веб-технологии, такие как HTML и JavaScript. Это позволяет разработчикам использовать знакомые инструменты, библиотеки и фреймворки. Пользовательский интерфейс на стороне клиента обычно связан со смарт-контрактами через клиентские библиотеки, такие как Web3.js или Ethers.js, которые поставляются в комплекте с ресурсами фронтенда и отправляются в браузер вместе с пользовательским интерфейсом. Взаимодействие со смарт-контрактами, такое как подписание сообщений и отправка транзакций смарт-контрактам, обычно осуществляется через браузерный Web3-кошелек, например MetaMask.

Хранение данных Большинству приложений необходимо хранить данные, но из-за распределенной природы блокчейн хранение больших объемов данных на цепочке нецелесообразно и может оказаться очень дорогим. Поэтому многие dApp, которым необходимо хранить данные, используют офф-чейн сервисы хранения данных, такие как IPFS или Filecoin, оставляя блокчейн только для хранения важной бизнес-логики и состояния.

Можно также использовать традиционные облачные сервисы хранения данных. Однако многие разработчики выбирают децентрализованные варианты, чтобы сохранить и расширить свойства минимизации доверия, которые обеспечивает dApp на базе блокчейна.

Архитектура Ethereum dApp

Теперь, когда мы знаем компоненты dApp, давайте рассмотрим пример создания простого сквозного контракта.

Шаг первый: создание смарт-контракта

Смарт-контракт в нашем dApp будет простым примером, используемым для поиска данных и отражения изменений состояния на блокчейне. В данном случае мы будем искать стоимость ETH/USD, используя ETH/USD Data Feed, а затем постоянно сохранять результат в смарт-контракте.

Первым делом откройте документацию и перейдите на страницу Using Data Feeds. Оттуда вы можете скопировать исходный код примера и вставить его в новый файл в выбранной вами IDE (например, Visual Code), либо нажать кнопку "Open In Remix" и работать из веб-версии Remix.

В этом примере мы будем работать с Visual Studio Code и Hardhat, фреймворком для разработки виртуальной машины Ethereum.

Сначала мы создадим новую структуру каталогов для нашего dApp с папкой backend для кода смарт-контракта:

mkdir chainlink-dapp-example
cd chainlink-dapp-example
mkdir backend
cd backend

Далее мы откроем созданную директорию для нашего dApp в редакторе VS Code, а затем установим Hardhat:

npm init -y
npm install --save-dev hardhat
npx hardhat
(choose create javascript project, choose default parameters)

После этого удалите файл Touch.sol в папке "contracts", создайте новый файл в этой папке под названием PriceConsumerV3.sol и сохраните. Здесь мы создадим наш смарт-контракт, поэтому скопируйте код из примера в документации Chainlink в этот файл и сохраните его.

В коде примера вы увидите, что в демо-контракте уже есть функция getLatestPrice для поиска текущей цены Ethereum на Rinkeby ETH/USD Data Feed.

function getLatestPrice() public view returns (int) {
        (
            /*uint80 roundID*/,
            int price,
            /*uint startedAt*/,
            /*uint timeStamp*/,
            /*uint80 answeredInRound*/
        ) = priceFeed.latestRoundData();
        return price;

Нам нужно создать новую переменную и новую функцию для хранения этого значения в смарт-контракте. Первым шагом будет создание новой переменной под существующей priceFeedone для хранения цены Ethereum:

int public storedPrice;

Далее нам нужно создать новую функцию, которая будет вызываться фронтендом dApp. Эта функция должна искать последнюю цену Ethereum, вызывая существующую функцию getLatestPrice. Затем она должна сохранить это значение в новом параметре storedPrice:

function storeLatestPrice() external {
        storedPrice = getLatestPrice();
    }

Ваш новый контракт должен выглядеть следующим образом:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract PriceConsumerV3 {

    AggregatorV3Interface internal priceFeed;
    int public storedPrice;

    /**
     * Network: Rinkeby
     * Aggregator: ETH/USD
     * Address: 0x8A753747A1Fa494EC906cE90E9f37563A8AF630e
     */
    constructor() {
        priceFeed =
AggregatorV3Interface(0x8A753747A1Fa494EC906cE90E9f37563A8AF630e);
    }

    /**
     * Returns the latest price
     */
    function getLatestPrice() public view returns (int) {
        (
            /*uint80 roundID*/,
            int price,
            /*uint startedAt*/,
            /*uint timeStamp*/,
            /*uint80 answeredInRound*/
        ) = priceFeed.latestRoundData();
        return price;
    }

    function storeLatestPrice() external {
        storedPrice = getLatestPrice();
    }
}

Шаг второй: развертывание смарт-контракта

Теперь вы готовы скомпилировать и развернуть свой контракт в тестовой сети Rinkeby. Не забудьте сначала пополнить свой кошелек MetaMask некоторым количеством Rinkeby ETH.

Если вы используете Remix, вы можете скомпилировать и развернуть свой контракт, используя стандартный процесс Remix. Если вы используете IDE, например Visual Studio Code, мы рекомендуем использовать Hardhat для управления контрактами.

Первым шагом к компиляции и развертыванию вашего контракта является установка библиотеки инструментов Hardhat, библиотеки контрактов Chainlink и библиотеки dotenv для хранения паролей и секретных ключей в отдельном файле .env:

npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install @chainlink/contracts --save
npm install dotenv

Затем замените содержимое файла hardhat-config.js на следующее:

require("@nomicfoundation/hardhat-toolbox");

//require("@nomiclabs/hardhat-ethers")
 require('dotenv').config()

 const RINKEBY_RPC_URL = process.env.RINKEBY_RPC_URL ||
"https://eth-rinkeby.alchemyapi.io/v2/your-api-key"
 const PRIVATE_KEY = process.env.PRIVATE_KEY || "abcdef"

 module.exports = {
     defaultNetwork: "rinkeby",
     networks: {
         hardhat: {
             // // If you want to do some forking, uncomment this
             // forking: {
             // url: MAINNET_RPC_URL
             // }
         },
         localhost: {
         },
         rinkeby: {
             url: RINKEBY_RPC_URL,
             accounts: [PRIVATE_KEY],
             saveDeployments: true,
         },
     },

  solidity: "0.8.9",
};

Следующим шагом будет создание файла .env в папке бэкенда. Затем вам нужно извлечь ваш приватный ключ из кошелька Web3 и вставить его в секцию значения поля PRIVATE_KEY в файле .env. Пожалуйста, убедитесь, что вы используете новый кошелек Web3, на котором нет средств в mainnet.

После этого вам необходимо получить конечную точку RPC для доступа к сети Rinkeby. Это можно сделать, вставив URL RPC в поле RINKEBY_RPC_URL в файле .env. Мы рекомендуем подписаться на бесплатную учетную запись Infura или Alchemy, чтобы получить URL RPC.

Создание файла .env

Следующим шагом будет изменение содержимого файла deploy.js в папке 'scripts', чтобы убедиться, что он развернет ваш новый контракт. Откройте файл и убедитесь, что следующий код заменяет уже имеющийся. Это просто возьмет ваш скомпилированный контракт PriceConsumerV3 и попытается развернуть его. Не забудьте сохранить изменения.

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// You can also run a script with `npx hardhat run <script>`. If you do that, Hardhat
// will compile your contracts, add the Hardhat Runtime Environment's members to the
// global scope, and execute the script.
const hre = require("hardhat");

async function main() {

  const PriceConsumer = await hre.ethers.getContractFactory("PriceConsumerV3");
  const priceConsumer = await PriceConsumer.deploy();

  await priceConsumer.deployed();

  console.log("Contract deployed to:", priceConsumer.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Теперь вы готовы составить и развернуть свой смарт-контракт в сети Rinkeby с помощью Hardhat:

npx hardhat compile
npx hardhat run --network rinkeby scripts/deploy.js

Вы должны увидеть сообщение, подобное приведенному ниже, в котором будет указан адрес на Rinkeby, на который был развернут ваш смарт-контракт. Запишите этот адрес, он понадобится нам для следующего шага.

Развернутый смарт-контракт

Поздравляем, теперь вы готовы перейти к фронтенд-части вашего dApp!

Шаг 3: Создание фронтенд-приложения

Логика фронтенда и пользовательский интерфейс вашего dApp могут быть построены с использованием широкого спектра различных фреймворков.

React - одна из самых популярных библиотек JavaScript для создания многофункциональных пользовательских веб-интерфейсов, поэтому она используется во многих Web3 dApp. Кроме того, Ethers.js - это библиотека JavaScript для подключения и взаимодействия с блокчейнами и смарт-контрактами на базе EVM. Если объединить эти два компонента, вы получите разумную отправную точку для создания фронтенда вашего dApp.

В этом разделе мы создадим новое приложение React с помощью генератора шаблонов create-react-app. Затем мы внедрим офф-чейн логику, использующую Ethers.js для связи пользовательского интерфейса с развернутым смарт-контрактом, что позволит нам создать полноценный сквозной dApp.

Создание React-приложения Первым шагом для создания фронтенда является установка и реализация проекта create-react-app boilerplate, а затем его модификация для нашего dApp. Первым шагом будет установка библиотеки в новую папку "frontend":

cd ..
npx create-react-app frontend

После этого в вашем проекте должна появиться новая папка "frontend" со всем связанным с ней кодом React. Раскройте папку "frontend" и выполните следующие действия:

  • Удалить /src/setupTests.js

  • Удалить /src/ReportWebVitals.js

  • Удалить /src/logo.svg

  • Удалить /src/App.test.js

  • Удалить /src/App.css

Структура папок должна выглядеть следующим образом:

Структура папок React front-end

Теперь мы почти готовы приступить к изменению кода приложения React. Но сначала не забудьте установить библиотеки для Bootstrap и Ethers.js. Bootstrap - это популярный CSS-фреймворк для фронтенда, который поставляется с удобными для React виджетами пользовательского интерфейса с наложенной стилизацией CSS, а Ethers.js позволяет нам подключить наш фронтенд к развернутым смарт-контрактам на блокчейне. Убедитесь, что эти команды выполняются из папки "frontend".

cd frontend
npm install bootstrap
npm install ethers

Теперь мы готовы к изменению кода приложения React. Откройте файл App.js в папке /src/ и удалите его содержимое. Мы начнем создавать его с нуля.

Первый шаг - сообщить приложению, что мы хотим использовать React (включая библиотеки useEffect и useState) и Ethers.js:

import React, { useEffect, useState } from 'react';
import { ethers } from "ethers";

Затем создайте функцию под названием "App" и экспортируйте ее:

Теперь мы начнем заполнять содержимое функции "App". Добавьте в нее следующий код. Этот код делает следующее:

Устанавливает реактивные хуки storedPrice и setStoresPrice. Создает соединение с вашим кошельком MetaMask Web3. Устанавливает адрес развернутого смарт-контракта и ABI. Оба эти параметра необходимы Ethers.js для взаимодействия с развернутым смарт-контрактом. Адрес смарт-контракта можно получить из шага развертывания, описанного ранее в этом руководстве. Вставьте это значение вместо строки REPLACE_WITH_DEPLOYED_CONTRACT_ADDRESS. ABI смарт-контракта можно получить из файла /backend/artifacts/contracts/PriceConsumerV3.json, в элементе abi. Вы можете использовать минификатор кода, чтобы отформатировать его в лучшем виде для хранения в вашем приложении.

const [storedPrice, setStoredPrice] = useState('');
  const provider = new ethers.providers.Web3Provider(window.ethereum)
  const signer = provider.getSigner()
  const contractAddress = <REPLACE_WITH_DEPLOYED_CONTRACT_ADDRESS>’';
  const ABI =
'[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"getLatestPrice","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"storeLatestPrice","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"storedPrice","outputs":[{"internalType":"int256","name":"","type":"int256"}],"stateMutability":"view","type":"function"}]'
  const contract = new ethers.Contract(contractAddress, ABI, signer);

Теперь мы создадим две функции, которые будут использоваться в нашем приложении:

getStoredPrice подключится к развернутому смарт-контракту и получит текущее значение геттер-функции storedPrice(). Функция setNewPrice будет вызывать функцию storeLatestPrice развернутого смарт-контракта, ждать завершения транзакции, а затем вызывать функцию getStoredPrice для получения сохраненной в смарт-контракте цены. Мы также добавим вызов getStoredPrice в нашу функцию App, чтобы она первоначально вызывала функцию getter при загрузке страницы:

const getStoredPrice = async () => {
    try {
      const contractPrice = await contract.storedPrice();
      setStoredPrice(parseInt(contractPrice) / 100000000);
    } catch (error) {
      console.log("getStoredPrice Error: ", error);
    }
  }

  async function updateNewPrice() {
    try {
      const transaction = await contract.storeLatestPrice();
      await transaction.wait();
      await getStoredPrice();
    } catch (error) {
      console.log("updateNewPrice Error: ", error);
    }

  }

  getStoredPrice()
  .catch(console.error)

Последним шагом в создании фронтенда является возврат JSX-кода для рендеринга браузером. Вставьте следующий код в нижней части функции App, под вызовом getStorePrice(). Этот код делает следующее:

Возвращает простой двухколоночный макет сетки. Первый столбец содержит текущую хранимую цену ETH/USD в смарт-контракте. Второй столбец содержит кнопку, которую пользователь может использовать для взаимодействия с умным контрактом и обновления сохраненной цены. Нажатие кнопки вызывает функцию setNewPrice, описанную выше.

return (
    <div className="container">
      <div className="row mt-5">

        <div className="col">
          <h3>Stored Price</h3>
          <p>Stored ETH/USD Price: {storedPrice}</p>
        </div>

        <div className="col">
          <h3>Update Price</h3>
          <button type="submit" className="btn btn-dark"
onClick={updateNewPrice}>Update</button>
        </div>
      </div>
    </div>
  );

Теперь ваше приложение готово. При необходимости вы можете сравнить свой код с готовым примером, чтобы убедиться, что вы все сделали правильно. Теперь вы готовы к запуску вашего dApp.

Запуск приложения dApp Убедившись, что все файлы сохранены, запустите свой dApp локально, выполнив следующую команду из папки frontend:

npm run start

После загрузки приложения в вашем браузере должно появиться новое окно, отображающее пользовательский интерфейс dApp. Вы также должны получить всплывающее уведомление от MetaMask с просьбой подключить ваш кошелек к приложению.

Фронтенд React

После того как вы убедитесь, что пополнили свой счет в MetaMask некоторым количеством Rinkeby ETH, нажмите кнопку "Обновить" в пользовательском интерфейсе dApp, чтобы взаимодействовать с развернутым смарт-контрактом в сети Rinkeby. Вы должны получить уведомление от MetaMask с просьбой подтвердить транзакцию. После этого в течение нескольких секунд ваш dApp должен автоматически обновиться, а в разделе "Сохраненная цена" появится текущая цена Ethereum:

Фронтенд React, показывающий результат работы Data Feed

Поздравляем, теперь вы успешно создали, развернули и взаимодействовали с простым dApp! В этом руководстве вы просто запустите фронтенд локально на своем компьютере, но при желании вы можете развернуть его на облачном сервере или даже децентрализовать фронтенд и развернуть его на IPFS! Вы также можете поработать с CSS приложения, чтобы изменить внешний вид пользовательского интерфейса.

Резюме

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

В этой технической демонстрации мы создали простой dApp, содержащий смарт-контракт, который получает последнюю цену от ETH/USD Data Feed и сохраняет результат в смарт-контракте. Затем мы создали простой пользовательский интерфейс, использующий React и Ethers.js для подключения к развернутому смарт-контракту и взаимодействия с ним.


Телеграм канал про web3 разработку, смарт-контракты и оракулы.