Подключите React к Ethereum

Как локально подключить смарт-контракт Ethereum к пользовательскому интерфейсу React для децентрализованной разработки приложений

После финансового кризиса 2008 года Сатоши Накамото предложил и разработал программное обеспечение, которое должно было стать новыми цифровыми деньгами - Биткойн. С помощью Биткойна Сатоши Накамото представил миру центральный банк Интернета, криптографическую валюту, что раньше было невозможно из-за проблемы двойных расходов, которую решил Биткойн. Эта проблема двойных расходов была решена путем создания базы данных нового типа, известной как блокчейн.

Пять лет спустя Виталик предложил расширить блокчейн Биткойн для использования не только в деньгах.

Я впервые написал первоначальный черновик технического документа Ethereum холодным ноябрьским днем ​​в Сан-Франциско как кульминацию месяцев размышлений и часто разочаровывающих работ в области, которую мы стали называть криптовалютой 2.0
- Виталик Бутерин

Видение Ethereum заключалось в том, чтобы создать платформу, которая станет следующим поколением Интернета, Web 3.0. Это новое поколение позволит пользователям исследовать как традиционную, так и децентрализованную сеть. На вершине Ethereum будет создано новое поколение приложений, децентрализованных приложений (dApps), которые используют Ethereum в качестве серверной части вместо обеспечения безопасности серверов.

Как и система обмена контентом BitTorrent, сетевые узлы Ethereum будут работать на тысячах компьютеров по всему миру, и, за исключением отключения Интернета, ее работу невозможно остановить.
- Джозеф Любин

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

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

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

В качестве введения в создание Ethereum мы рассмотрим самые фундаментальные части создания dApp: связь со смарт-контрактом с веб-сайта / пользовательского интерфейса.

Для этого проекта наш стек включает Ethereum и React. Я предпочитаю хранить файлы в отдельных каталогах, чтобы проект был организован.

project
|_ blockchain (Ethereum)
|_ client (React)

Полный проект можно найти в примере проекта на Github.

Настройка нашего блокчейна

Для этого стека мы будем использовать инструменты разработки Truffle.js suite. Сначала установите Truffle, если он еще не установлен на вашем компьютере.

npm install -g truffle

Я создал новый каталог для хранения наших файлов, составляющих наш смарт-контракт ./blockchain. В ./blockchain мы можем инициализировать наш код Ethereum с помощью Truffle. В вашей командной строке:

cd blockchain
truffle init

Теперь несколько файлов и каталогов должны заполнить ./blockchain.

blockchain
|_ contracts
|_ migrations
|_ test

В ./contracts должен быть Migrations.solsmart контракт. Миграция - это процесс публикации смарт-контракта в данной цепочке блоков. Включен весь стартовый код для настройки миграции, за исключением кода для миграции новых контрактов, которые мы создаем.

Чтобы просто понять процесс локальной миграции и протестировать наш пользовательский интерфейс, мы воспользуемся для начала очень простым смарт-контрактом. Документы Solidity содержат короткий пример смарт-контракта для простого хранилища. Мы будем использовать этот код контракта. Создайте и залейте SimpleStorage.sol.

// SimpleStorage.sol
pragma solidity >=0.4.0 <0.7.0;

contract SimpleStorage {
  uint storedData;

  function set(uint x) public {
    storedData = x;
  }

  function get() public view returns (uint) {
    return storedData;
  }
}

Этот смарт-контракт выполняет две функции:

  • Хранит положительное целое число (set())
  • Извлекает это положительное целое число из памяти (get())

У нас есть смарт-контракт на проект. Мы можем скомпилировать наш код. Мы делаем это с трюфелем.

truffle compile

Это должно создать новый каталог с именем ./build/contracts. Для нашего проекта должно быть два файла: Migrations.json и SimpleStorage.json. Каждый из этих файлов JSON должен содержать имя контракта, ABI и другую информацию, используемую при развертывании.

Используя файлы JSON, которые мы скомпилировали, мы можем создавать наши файлы миграции. В ./blockchain/migrations вы должны увидеть существующий файл миграции 1_initial_migrations.js. Файл должен содержать следующее:

// 1_initial_migrations.js
var Migrations = artifacts.require("Migrations");

module.exports = function(deployer) {
  // Deploy the Migrations contract as our only task
  deployer.deploy(Migrations);
};

В переменной Migrations вы можете увидеть, что "Migrations" импортируется. "Migrations" - название контракта, указанное в ./build/contracts/Migrations.json. Этот 1_initial_migrations.js использует Migrations.json для развертывания контракта на миграцию. Мы создадим новую миграцию, используя имя контракта для нового контракта, который мы создали,
"SimpleStorage”.

Создайте новый файл миграции 2_first_contracts.js. Мы заполним его кодом, похожим на 1_initial_migrations.js, но включим наш SimpleStorage смарт-контракт.

// 2_first_contracts.js
const SimpleStorage = artifacts.require("SimpleStorage");

module.exports = function(deployer) {
  deployer.deploy(SimpleStorage);
};

Это все настройки, необходимые для локального развертывания кода в нашем проекте. Настройка для более сложных контрактов может отличаться. Однако без добавления нашего контракта в файлы миграции миграция в Truffle приведет только к миграции Migration.sol контракта и не будет развертывать другие контракты, которые мы создали и скомпилировали. Итак, этот шаг обязателен.

Затем мы создаем локальную тестовую сеть Ethereum (testnet) и развертываем наш код локально. Мы делаем это снова с Truffle. Запустите блокчейн разработки:

truffle develop

Truffle должен отображать хост и порт, а также набор учетных записей с соответствующими закрытыми ключами.

Truffle Develop started at http://127.0.0.1:9545/

Accounts:
(0) 0x627306090abab3a6e1400e9345bc60c78a8bef57
(1) 0xf17f52151ebef6c7334fad080c5704d77216b732
(2) 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef
(3) 0x821aea9a577a9b44299b9c15c88cf3087f3b5544
(4) 0x0d1d4e623d10f9fba5db95830f7d3839406c6af2
(5) 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e
(6) 0x2191ef87e392377ec08e7c08eb105ef5448eced5
(7) 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5
(8) 0x6330a553fc93768f612722bb8c2ec78ac90b3bbc
(9) 0x5aeda56215b167893e80b4fe645ba6d5bab767de

Private Keys:
(0) c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3
(1) ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f
(2) 0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1
(3) c88b703fb08cbea894b6aeff5a544fb92e78a18e19814cd85da83b71f772aa6c
(4) 388c684f0ba1ef5017716adb5d21a053ea8e90277d0868337519f97bede61418
(5) 659cbb0e2411a44db63778987b1e22153c086a95eb6b18bdf89de078917abc63
(6) 82d052c865f5763aad42add438569276c00d3d88a2d062d36b2bae914d58b8c8
(7) aa3680d5d48a8283413f7a108367c7299ca73f553735860a87b08f39395618b7
(8) 0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4
(9) 8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5
truffle(development)>

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

Используя эту среду разработки, мы переносим наши контракты в эту локальную тестовую сеть:

truffle(development)> migrate

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

Перейдем к интерфейсу.

Настройка нашего Frontend-клиента

Для интерфейса мы будем использовать простой начальный загрузчик React create-react-app. Из корня проекта используйте create-react-app для инициализации нашего клиента.

npx create-react-app client

Теперь у нас должно быть две папки в корне проекта: ./blockchain и ./client. Каталог client теперь должен содержать стартовое приложение React.

client
|_ public
|_ src

Для взаимодействия со смарт-контрактами Ethereum в сети мы используем библиотеку web3. Давайте добавим web3 в наш проект.

cd client
npm install web3

Чтобы клиент мог взаимодействовать с данным смарт-контрактом, ему нужен шаблон ожидаемых входных и выходных данных для функций в смарт-контракте. Этот шаблон предоставляется в ABI.

ABI данного смарт-контракта создается при компиляции смарт-контракта в Truffle. Если мы вернемся в каталог blockchain, если ваши контракты были скомпилированы, должен быть путь build/contracts, содержащий файл JSON скомпилированного контракта. В JSON нашего SimpleStorage контракта должно быть поле с именем "abi", содержащее объект ABI контракта, который представляет собой массив объектов для каждой функции в контракте. Мы скопируем это и сохраним в нашем клиенте.

В новом файле abis.js. В нем мы сохраним наш ABI для экспорта. Давайте создадим это, предполагая, что мы можем сохранить и получить несколько ABI из этого файла. Для каждого объекта ABI для данного смарт-контракта копирования и вставки объекта из файла JSON скомпилированного контракта должно быть достаточно, когда дело доходит до Javascript.

// abis.js
export const simpleStorageAbi = [
  {
    "constant": false,
    "inputs": [
      {
        "internalType": "uint256",
        "name": "x",
        "type": "uint256"
      }
    ],
    "name": "set",
    "outputs": [],
    "payable": false,
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "get",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  }
]

Теперь мы можем начать писать код для подключения React к Ethereum.

В App.js мы импортируем web3 и наши ABI и подготовим Web3. Кроме того, мы настроим React Hooks.

// App.js
import React, { useState } from 'react';
import Web3 from 'web3';
import { simpleStorageAbi } from './abi/abis';
import './App.css';
const web3 = new Web3(Web3.givenProvider);

Данный провайдер является поставщиком Web 3 по умолчанию, он же кошелек Ethereum, совместимый со смарт-контрактами, предоставляемый браузером пользователя. Во многих случаях это может быть расширение браузера Metamask.

Мы можем начать с запуска объекта смарт-контракта, который мы будем использовать для взаимодействия с нашим развернутым SimpleStorage смарт-контрактом.

// contract address is provided by Truffle migration
const contractAddr = '0x97EaC1d4C5eA22dE6ba7292FA5d01a591Aac83A7';
const SimpleContract = new web3.eth.Contract(simpleStorageAbi, contractAddr);

Адрес контракта - это адрес SimpleStorage, предоставленный Truffle при выполнении миграции. Затем мы создаем новый экземпляр контракта с именем SimpleContract, передавая ABI simpleStorageAbi и адрес контракта contractAddr в new web3.eth.Contract(). Этот экземпляр контракта будет использоваться для вызова и отправки действий смарт-контракта.

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

// ... App.js
function App() {
  const [number, setNumber] = useState(0);
  const [getNumber, setGetNumber] = useState('0x00');

Строка '0x00' - это шестнадцатеричная форма «0».

Прежде чем добавлять какие-либо функции, мы начнем с настройки формы для отображения и отправки наших данных.

// ... App.js
return (
    <div className="App">
      <header className="App-header">
        <form onSubmit={handleSet}>
          <label>
            Set Number:
            <input 
              type="text"
              name="name"
              value={number}
              onChange={ e => setNumber(e.target.value) } />
          </label>
          <input type="submit" value="Set Number" />
        </form>
        <br/>
        <button
          onClick={handleGet}
          type="button" > 
          Get Number 
        </button>
        { getNumber }
      </header>
    </div>  
);

Мы сохраняем getNumber как шестнадцатеричную строку в хуке, поскольку именно в этой форме смарт-контракт вернет наш номер. Мы конвертируем шестнадцатеричную форму в число с помощью инструмента Web3 hexToNumber(). По умолчанию мы устанавливаем getNumber на отображение «0».

Форма и кнопка вызывают функции handleSet() и handleGet(). Сейчас мы создадим эти функции.

В смарт-контрактах Ethereum есть два типа функций или действий смарт-контрактов:

  • Действия, требующие газа (или отправка ETH)
  • Бесплатные акции

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

Мы можем определить, требует ли действие смарт-контракта газа, посмотрев на доступ, определенный при объявлении функции. Функции, которые только читают, имеют следующие модификаторы доступа:

  • pure
  • view

Все остальные действия требуют газа, так как они изменяют состояние блокчейна.

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

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

function set(uint x) public {
  storedData = x;
}

function get() public view returns (uint) {
  return storedData;
}

Функция set() принимает целое число и сохраняет его в цепочке блоков. Требуется газ. Функция get() установлена ​​на view доступ, только чтение переменной storedData, которая была сохранена. Не требует газа.

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

const handleGet = async (e) => {
  e.preventDefault();
  const result = await SimpleContract.methods.get().call();
  setGetNumber(result);
  console.log(result);
}
  • Утверждение e.preventDefault предотвращает события по умолчанию для кнопки.
  • Мы асинхронно вызываем метод контракта get() в экземпляре контракта SimpleContract с await SimpleContract.methods.get.call().
  • Установщик ловушек setGetNumber() сохраняет номер в состоянии, отображаемом в пользовательском интерфейсе.

Для всех других транзакций смарт-контрактов нашему приложению требуется разрешение на доступ к средствам пользователей для оплаты сборов за газ и любые оплачиваемые функции, запрашивающие эфир или любой токен ERC20. Поставщик Ethereum внедряется в браузер пользователя, когда у пользователя есть программное обеспечение с поддержкой Web 3, такое как Metamask. Этот провайдер доступен на веб-сайте через глобальную переменную window в window.ethereum. Этот провайдер вызывается для приложения для доступа к разрешению.

const handleSet = async (e) => {
  e.preventDefault();    
  const accounts = await window.ethereum.enable();
  const account = accounts[0];
  const gas = await SimpleContract.methods.set(number)
                      .estimateGas();
  const result = await SimpleContract.methods.set(number).send({
    from: account,
    gas 
  })
  console.log(result);
}
  • Мы запрашиваем у пользователя разрешение на доступ к своему кошельку, позвонив по номеру window.ethereum.enable().
  • Если пользователь предоставляет разрешение на доступ к своей учетной записи, window.ethereum.enable() вернет массив адресов, начиная с их текущего активного адреса, который мы сохраняем в accounts.
  • Мы изолируем текущую учетную запись пользователя, выбрав первый адрес в списке адресов с accounts[0].
  • Мы оцениваем количество газа, необходимое для нашей транзакции смарт-контракта, используя .estimateGas() метод, который мы выбираем в нашем экземпляре контракта.
  • Мы создаем транзакцию смарт-контракта, передавая параметры нашей функции методу смарт-контракта methods.set(), а расчетный газ и адрес учетной записи пользователя в .send().
  • Ответ записывается в консоль для тестирования.

Примечание. В этом проекте все приложение может существовать только в App.js. По этой причине контракт и экземпляр Web3 находятся в верхней части App.js. Однако в более сложном приложении, где нескольким компонентам требуется доступ к контракту или экземпляру Web3, библиотека управления состоянием может быть полезной для хранения этих экземпляров, делая их доступными повсюду в приложении.

Тестирование нашего приложения локально

Теперь мы можем протестировать приложение локально. Запустите локальную версию приложения React:

npm run start

Это должно открыть приложение на localhost: 3000.

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

После установки Metamask в вашем браузере мы настраиваем его для работы в нашей локальной тестовой сети, используя адрес, предоставленный Truffle. В нашем случае этот адрес находится в строке Truffle Develop started at http://127.0.0.1:9545/. Мы подходим к сетям, нажимаем Custom RPC и вводим URL, предоставленный Truffle.

После добавления RPC тестовой сети мы импортируем учетные записи, предоставленные Truffle. Для этого приложения требуется только одна учетная запись, но Truffle предоставляет десять учетных записей, если вам нужно протестировать более сложные взаимодействия. В этом примере импортируется закрытый ключ c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3, предоставленный Truffle, содержащий 100 тестовых сетей Ether.

После подключения с учетной записью, в которой есть эфир, мы можем протестировать наше приложение React с помощью Metamask. Если мы установим номер в хранилище, наше приложение должно связаться с нашим кошельком для получения разрешения на подписание этой транзакции.

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

И, обладая этими основами, имея взаимодействие смарт-контракта и пользовательского интерфейса, вы должны быть готовы к созданию приложения следующего поколения для следующего поколения Интернета, Web 3.0.