В этом руководстве пошагово рассмотрим, как создать reactjs проект и настроить его взаимодействие с блокчейном Ethereum.
Подготовка к работе с ReactJS
Предполагается, что node, npm и npx у вас уже установлены (если это не так, установите их).
Следующие 3 команды должны вам показать их версию:
$ node -v $ npm -v $ npx -v
Создавать проект reactjs мы будем через npx (вместо npm), – это тоже CLI, который позволяет упростить установку и управление зависимостями, размещенными в реестре npm.
Создание нового проекта ReactJs и установка необходимых пакетов
При помощи npx создадим новый проект reactjs с названием ‘frontend’ и запустим его. Обычно я создаю его внутри truffle проекта, чтобы фронтэнд лежал рядышком для удобства.
# создаем react-проект с названием 'frontend' $ npx create-react-app frontend # переходим в директорию проекта $ cd frontend
Для того, чтобы проверить, что проект корректно создался и запускается, можно при помощи команды:
npm start
.В итоге должен запуститься проект по локальному адресу: http://localhost:3000/
Теперь установим пакеты ‘bootstrap’ и ‘web3’ для нашего проекта:
- ‘bootstrap‘ нужен, чтобы не заморачиваться на дизайне и верстке, а использовать для этого готовый css framework.
- ‘web3‘ библиотека, которая будет обеспечивать взаимодействие с ethereum блокчейном.
# установка 'bootstrap' $ npm install react-bootstrap bootstrap # установка 'web3' и необходимых для ее работы доп.пакетов $ npm install web3, react-app-rewired, url, assert, buffer, crypto-browserify, stream-http, https-browserify, stream-browserify, os-browserify
Откроем директорию frontend/src и удалим оттуда файл index.css, так как все глобальные изменения со стилями мы будем делать в App.css.
Отредактируем файл frontend/src/index.js и внесем туда след изменения:
- импортируем ‘bootstrap’ добавив строчку
import 'bootstrap/dist/css/bootstrap.min.css';
- удалим строку
import './index.css';
чтобы исключить импорт удаленного ранее файла.
Отредактируем файл frontend/src/App.js и добавим туда импорт библиотеки Web3:
import Web3 from 'web3'
Чтобы Web3 увидел все свои зависимости, создадим в директории frontend файл config-overrides.js и скопируем туда следующую конфигурацию:
const webpack = require('webpack'); module.exports = function override(config, env) { //do stuff with the webpack config... config.resolve.fallback = { url: require.resolve('url'), assert: require.resolve('assert'), crypto: require.resolve('crypto-browserify'), http: require.resolve('stream-http'), https: require.resolve('https-browserify'), os: require.resolve('os-browserify/browser'), buffer: require.resolve('buffer'), stream: require.resolve('stream-browserify'), }; config.plugins.push( new webpack.ProvidePlugin({ process: 'process/browser', Buffer: ['buffer', 'Buffer'], }), ); return config; }
После этого, откроем файл package.json и в секцию scripts внесем следующие изменения:
"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject" },
При помощи команды:
npm start
проверьте, что после всех наших манимуляций проект успешно запустился по локальному адресу: http://localhost:3000/.Если все работает, то это значит, что наш reactjs-проект готов к взаимодействию с блокчейном Ethereum.
Прежде чем продолжить далее, рекомендуется выполнить действия, описанные в статье “Настройка взаимодействия MetaMask и Remix с Ganache“.
Этап 1. Авторизация на web-странице через Ethereum
Покажем простую авторизацию в Ethereum через MetaMask на нашей веб-странице и выведем на нее адрес нашего авторизованного кошелька.
Для этого отредактируем файл App.js следующим образом:
import React, { useEffect, useState } from 'react'; import {Container, Row, Col, Alert} from 'react-bootstrap'; import Web3 from 'web3'; import './App.css'; const App = () => { const [account, setAccount] = useState(); useEffect(() => { async function load() { const web3 = new Web3(Web3.givenProvider); console.log('Web3Version: ' + web3.version); // запрос авторизованного аккаунта (если их несколько, то выбирается первый) const accounts = await web3.eth.requestAccounts(); setAccount(accounts[0]); console.log('Account: ' + accounts[0]); } load(); }, []); return ( <Container className="px-4 py-5"> <Row> <Col> <Alert variant='info'> <p>Ваш аккаунт: <b>{account}</b></p> </Alert> </Col> </Row> </Container> ); }; export default App;
В отличие от дефолтной версии App.js, здесь мы используем библиотеку react-bootstrap для красивого оформления страницы и библиотеку Web3 для подключения к Ethereum блокчейну через MetaMask аккаунт.
При запуске приложения и переходе по адресу http://localhost:3000/ сработает хук useEffect, который вызовет функцию load(), а она в свою очередь вызовет функцию new Web3(Web3.givenProvider), которая автоматически попросит откроет окно плагина MetaMask в браузере и попросит авторизоваться через него.
Если вы создаете приложение через create-react-app, то можете столкнуться с ситуацией (и заметить это через консоль разработчика), когда код в хуке useEffect при загрузке страницы вызывается несколько раз, не смотря на установленный пустой массив зависимостей []. Это происходит потому, что в index.js у вас вызов компонента <App /> обернут в <React.StrictMode>, который в development режиме (но не в production) выполняет рендеринг компонентов дважды. По мнению разработчиков React – это позволяет обнаружить некоторые проблемы в коде и предупредить вас об этом.
Чтобы избежать этого, отредактируйте index.js и уберите обертку StrictMode:
// замените эти строки: root.render( <React.StrictMode> <App /> </React.StrictMode> ); // на эту: root.render(<App />);
Этап 2. Взаимодействие со смарт-контрактом Ethereum через ReactJs
- Напишем простой смарт-контракт на Solidity под названием CatFactory (Фабрика котиков).
Принцип его работы простой, – каждый может вызвать функцию createCat и создать себе котика. А при деплое смарт-контракта, владельцу автоматически создаются 3 котика.// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; /** * @title CatFactory * @dev Фабрика котиков :) */ contract CatFactory { uint public count = 0; // количество котят /** * @dev Структура для хранения характеристик котиков */ struct Cat { string name; // имя котика address owner; // адрес владельца котика } // @dev Каждый владелец может иметь только 1 котика (дефицит) mapping(uint => Cat) public cats; /** * @dev Конструктор - при деплое генерируем несколько котиков */ constructor() { createCat("Adam"); createCat("Bob"); createCat("Elena"); } /** * @dev Создание нового котика * @param _name имя котика */ function createCat(string memory _name) public { cats[count] = Cat({ name: _name, owner: msg.sender }); count++; } }
- Используя Remix откомпилируем его и задеплоим в наш локальный блокчейн Ganache:
- После успешного деплоя контракта, в логе Ganache находим его адрес:
- Теперь нужно вытащить интерфейс ABI нашего контракта, – это можно сделать через Remix.
В вашем Workspace после компиляции смарт-контракта в директории /contracts/artifacts/ дожен появиться файл CatFactory_metadata.json, открыв его найдите элемент “abi“.
На следующем шаге вам понадобится весь этот массив (вместе с квадратными скобками).
- Теперь перейдем к reactjs. В директории /frontend/src/ нужно создать файл config.js в котором указать две внешних переменные:
- CONTACT_ADDRESS – адрес вашего смарт-контракта полученный на шаге 3.
- CONTACT_ABI – интерфейс ABI смарт-контракта, полученный на шаге 4.
Вот полное содержимое файла config.js, которое должно получиться:
export const CONTACT_ADDRESS = '0xda595ea8bd21bf6e91978d67f1c1a7f23c8cd3c4'; export const CONTACT_ABI = [ { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "cats", "outputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "address", "name": "owner", "type": "address" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "count", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "string", "name": "_name", "type": "string" } ], "name": "createCat", "outputs": [], "stateMutability": "nonpayable", "type": "function" } ]
- Теперь расширим наш компонент <App />, добавив в него еще несколько переменных состояний для хранения переменной контракта и коллекции котиков, полученных из блокчейна. А в хуке useEffect мы теперь обращаемся к смарт-контракту и запрашиваем массив котиков, который сохраняем в переменной состояния cats. Далее, при рендере компонента, мы выводим этот массив при помощи цикла.
import React, { useEffect, useState } from 'react'; import {Container, Row, Col, Alert} from 'react-bootstrap'; import Web3 from 'web3'; import { CONTACT_ABI, CONTACT_ADDRESS } from './config'; import './App.css'; const App = () => { const [account, setAccount] = useState(); const [contract, setContract] = useState(); const [cats, setCats] = useState([]); useEffect(() => { async function load() { const web3 = new Web3(Web3.givenProvider); console.log('Web3Version: ' + web3.version); // запрос авторизованного аккаунта (если их несколько, то выбирается первый) const accounts = await web3.eth.requestAccounts(); setAccount(accounts[0]); console.log('Account: ' + accounts[0]); // создадим экземпляр смарт-контракта используя его ABI и адрес, а затем установим его в переменную состояния const contract = new web3.eth.Contract(CONTACT_ABI, CONTACT_ADDRESS); setContract(contract); // получим количество котят в хранилище const counter = await contract.methods.count().call(); console.log('Count cats: ' + counter); // прочитаем их всех и запишем в переменную состояния for (var i = 0; i <= counter-1; i++) { // вызов мапы cats из смарт-контракта, которое содержит список котиков const cat = await contract.methods.cats(i).call(); // добавление полученного котика в переменную состояния setCats((cats) => [...cats, cat]); } } load(); }, []); return ( <Container className="px-4 py-5"> <Row> <Col> <Alert variant='info'> <p>Ваш аккаунт: <b>{account}</b></p> <p>Коллекция котиков:</p> <div> { Object.keys(cats).map((cat, index) => ( <div key={`${cats[index].name}-${index}`}>Cat {index}: {cats[index].name}</div> )) } </div> </Alert> </Col> </Row> </Container> ); }; export default App;
- В итоге на странице вы должны увидеть примерно следующее:
Поздравляю!
Теперь вы научились делать для вашего приложения простую авторизацию с блокчейном через MetaMask, а также взаимодействовать со смарт-контрактом внутри блокчейна.