Web3 приложение Twitter на React.js + Solidity | часть 1

ece9d826469c6d493f058ff2dfec195f.jpg

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

P.S. я не позиционирую себя в качестве эксперта в блокчейне и web3, так что жду вашу критику в комментариях :)

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

Пару слов о проекте. Если с клиентом все понятно, простое react приложение с парой кнопок и запросами к контракту, то backend представляет собой смарт-контракт написанный на solidity и развернутый в частной сети через truffle.

Касательно функционала: базовая регистрация через metamask и пароль, написания постов (твиттов) и просмотр уже написанных (как своих, так и всех пользователей). Можно добавить функционал редактирования своих твиттов и возможность ставить лайки, но это оставим на вторую часть.

Инструменты для создания приложения:

Подготовка среды и установка зависимостей

Я буду работать в vs code, но роли это не играет. Для начала ставим плагины для комфортной работы: Simple React Snippets и Solidity.

Также необходимо скачать Ganache и поставить Truffle.

$ npm install -g truffle

Создаем папку под проект и в ней две папки: contracts и client.
В первой будет лежать смарт-контракт и все, что с ним связано.
Во второй будет react приложение.

mkdir web3
cd web3

mkdir contracts
mkdir client

Теперь инициализируем truffle в папке contracts.

cd contracts
truffle init

После этого получаем в консоль сообщение об успехе и видим в папке контракта новые папки и файл truffle-config.js к нему мы вернемся позже, а сейчас начнем писать сам смарт-контракт в папке contracts (созданной внутри нашей папки contracts).

Папка после инициализации truffle

Папка после инициализации truffle

Создание смарт-контракта

В директории web3/contracts/contracts создаем файл Twitter.sol в нем будет вся логика нашего сервера. Не буду особо пояснять за solidity, если писали на js, то легко разберетесь, на всякий случай есть удобный гайд.

Для начала создадим две структуры: User и Twitt.

struct User {
    address login;
    string password;
    string username;
    string avatar;
}

struct Twitt {
    User author;
    string text;
    uint likes;
    uint createdTime;
}
  • В структуре User объявляем базовые поля:
    — Логин типа address (адрес кошелька пользователя).
    — Пароль, никнейм и аватарка. Эти поля типа string, в объяснении думаю не нуждаются. Аватарку будем хранить в виде ссылки на картинку, дабы не тратить время на загрузку картинок на сервер.

  • В структуре Twitt уже сложнее, тут мы объявляем поле author типа нашей структуры User, лайки и дату написания твита будем хранить в типе uint, ибо эти данные не могут быть меньше 0.

Теперь создаем mapping для хранения данных. Концепция mapping в Solidity аналогична HashMap в Java или dict в Python. В качестве ключей всех наших маппингов будут адреса пользователей, а значение уже будет отличаться в зависимости от маппинга.

Первый mapping будет хранить аккаунты пользователей, где каждый пользователь (адрес) может иметь только один аккаунт.
Второй mapping будет хранить состояние пользователя (в системе или нет), здесь в качестве значения ключа будет boolean переменная (true — в системе / false — нет).
Третий mapping хранит массив твитов пользователя.

Также объявим переменную owner типа address (владелец контракта) и переменную usersCount для подсчета кол-во пользователей, т.к. мы не может получить размер маппинга.

Hidden text

address owner;
    
mapping(address => User) accounts;
mapping(address => bool) isLogged;
mapping(address => Twitt[]) twitts;

uint usersCount = 0;

Далее объявляем пару модификаторов для проверки, что пользователь вошел в аккаунт и проверка на то, что пользователь является владельцем контракта (на самом деле статус владельца на данный момент в контракте не используется, но пригодится при расширении контракта).

Также создаем конструктор (он вызывается один раз при развертывании смарт-контракта), в нем мы назначаем владельцем контракта адрес, который этот контракт развернул (msg.sender).

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

В модификаторе onlyLogged мы проверяем, что пользователь вызывающий функцию с этим модификатором залогинен и находится в маппинге с залогиненными пользователями, в противном случаем отменяем вызов функции с ошибкой «You must login in your account».

modifier onlyLogged() {
    require(isLogged[msg.sender], "You must login in your account");
    _;
}

modifier onlyOwner() {
    require(msg.sender == owner, "Only for owner");
    _;
}

constructor() {
    owner = msg.sender;
}

Теперь переходим к написанию самих функций контракта, начнем с регистрации.

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

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

function Registration(string memory _username, string memory _password) public {
    require(accounts[msg.sender].login == address(0), "Account is already registered");

    accounts[msg.sender] = User({
        login: msg.sender,
        password: _password,
        username: _username,
        avatar: ""
    });

    usersCount++;
}

Функция авторизации принимает только пароль, т.к. логином является адрес аккаунта вызывающего функцию.

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

function Login(string memory _password) public {
    require(accounts[msg.sender].login != address(0), "You don't have account!");
    require(keccak256(bytes(accounts[msg.sender].password)) == keccak256(bytes(_password)), "Wrong password");
    
    isLogged[msg.sender] = true;
}

В функции выхода из аккаунта просто меняем статус пользователя на false в маппинге авторизованных юзеров.

function Logout() public onlyLogged {
    isLogged[msg.sender] = false;
}

В функции получения пользователя прописываем return т.к. она будет возвращать данные пользователя по его адресу (логину), который мы передаем в качестве параметра.

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

function GetUser(address _user) public view returns(User memory) {
    return accounts[_user];
}

В функции написания твитта добавляем модификатор onlyLogged и передаем текст твитта. Внутри с помощью нашего метода GetUser получаем данные и пользователе вызывающем функцию и записываем в переменную, которую в дальнейшем передадим в новый объект Twitt в качестве автора твитта. В дате создания твитта передаем текущее время (на клиенте отформатируем в привычный date time).

function AddTwitt(string memory _text) public onlyLogged {
    User memory _user = GetUser(msg.sender);

    twitts[msg.sender].push(Twitt({
        author: _user,
        text: _text,
        likes: 0,
        createdTime: block.timestamp
    }));
}

В функции получения твиттов аналогично функции GetUser принимаем адрес пользователя и возвращаем массив его твиттов.

В функции проверки регистрации также проверяем, что в mapping аккаунтов есть данные пользователя, и возвращаем true или false .

function UserTwitts(address _user) external view onlyLogged returns(Twitt[] memory) {
    return twitts[_user];
}

function CheckRegistration(address _user) external view returns(bool) {
    return accounts[_user].login != address(0);
}

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

Storage — именно там хранятся все переменные состояния. Поскольку состояние может быть изменено в контракте (например, внутри функции), переменные хранения должны быть изменяемыми. Однако их местоположение постоянно, и они хранятся в блокчейне.

function UpdateUser(string memory _avatar) public {
    User storage _user = accounts[msg.sender];

    _user.avatar = _avatar;
}

Полный код контракта:

Hidden text

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

contract Twitter {
    address owner;
    
    mapping(address => User) accounts;
    mapping(address => bool) isLogged;
    mapping(address => Twitt[]) twitts;
    
    uint usersCount = 0;

    struct User {
        address login;
        string password;
        string username;
        string avatar;
    }

    struct Twitt {
        User author;
        string text;
        uint likes;
        uint createdTime;
    }

    modifier onlyLogged() {
        require(isLogged[msg.sender], "You must login in your account");
        _;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Only for owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function Registration(string memory _username, string memory _password) public {
        require(accounts[msg.sender].login == address(0), "Account is already registered");

        accounts[msg.sender] = User({
            login: msg.sender,
            password: _password,
            username: _username,
            avatar: ""
        });

        usersCount++;
    }

    function Login(string memory _password) public {
        require(accounts[msg.sender].login != address(0), "You don't have account!");
        require(keccak256(bytes(accounts[msg.sender].password)) == keccak256(bytes(_password)), "Wrong password");
        
        isLogged[msg.sender] = true;
    }

    function Logout() public onlyLogged {
        isLogged[msg.sender] = false;
    }

    function AddTwitt(string memory _text) public onlyLogged {
        User memory _user = GetUser(msg.sender);

        twitts[msg.sender].push(Twitt({
            author: _user,
            text: _text,
            likes: 0,
            createdTime: block.timestamp
        }));
    }

    function UserTwitts(address _user) external view onlyLogged returns(Twitt[] memory) {
        return twitts[_user];
    }

    function CheckRegistration(address _user) external view returns(bool) {
        return accounts[_user].login != address(0);
    }

    function GetUser(address _user) public view returns(User memory) {
        return accounts[_user];
    }

    function UpdateUser(string memory _avatar) public {
        User storage _user = accounts[msg.sender];

        _user.avatar = _avatar;
    }
}

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

Перед деплоем контракта необходимо подготовить truffle-config и ganache.

Запускаем Ganache и создаем новую рабочую область. Далее все настройки можно оставить по умолчанию.

ef528aaff1a1b002ff1a40dd7e8ba859.png

После нажатия кнопки «start» мы видим наши аккаунты и некоторую информацию о сети. Нас интересует network id и rpc server, их мы будем вписывать в файле truffle-config.js .

5fa903032455651e0f63cf5a8c03acf2.png

Переходим в файл truffle-config, здесь мы ищем development и убираем комментарии, подставляя наши данные из Ganace (скрин выше), также network_id можно оставить со значением »*», чтобы подходил любой id.

608c3887e391a560336b8f59992888e5.png

Спускаемся ниже и меняем версию компилятора с 0.8.21 на 0.8.2 . Можно обойтись и без этого, но тогда есть шанс получить ошибку при развертывании контракта.

780b3f72ad1be8c53ad88582d1e7519d.png

Сохраняем файл и переходим в папку migrations, в ней мы создаем файл с названием 2_deploy_contracts.js который будет деплоить наш контракт и прописываем простой код.

По факту контракт находится в директории »'…/contracts/Twitter.sol'», но мы можем указать текущую директорию.

const twitter = artifacts.require('./Twitter.sol');

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

Теперь все готово для деплоя. Открываем cmd в корневой директории contracts и прописываем команду для сборки и деплоя смарт-контракта.

$ truffle migrate --network development

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

7e4125247e4c80c12c049bbef7d3d5b2.png

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

b7f7c1fc12d8b074b63ab1254b5d5fa5.png375c31eed3412d2e2923faee925fb69a.png

В завершении первой части, добавим несколько аккаунтов из ganache в metamask, чтобы работать с ними на клиенте.

Для этого нажимаем на значок ключа рядом с аккаунтом в ganache и копируем его приватный ключ.

339cfa57f13bad6554d313f7648aa658.png

Открываем в браузере metamask и добавляем новый аккаунт, далее выбираем «Импортировать счет», и в поле вставляем приватный ключ аккаунта.

c7cf9bf7c4ef01cb137a141e73d86b22.png

Аккаунт добавился, но баланс 0, чтобы связать баланс Ganache и MetaMask нужно добавить тестовую сеть в MetaMask.

bbda5b4f9211ada6b96d896b3f14b744.png

Для добавления сети переходим в настройки и выбираем «добавить сеть вручную».

d3d6ef573cf3faedccf7cbce2b5b4871.png

Т.к. я уже создавал сети, то id блокчейна 1337, у вас скорее всего будет 5777. Название сети и символ валюты можно оставить ETH, я впишу свои для примера.

b354302622c07064022fb1e18731550f.png

После этого сеть сама меняется на новую и предлагает сменить валюту, мы соглашаемся и видим наш баланс как в Ganache.

19381ff596e160f3fd56920be289af43.png

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

16756fe0371de82262090b60807480b5.png

На этом первую часть можно объявить завершенной. Все полностью готово к написанию клиентской части и работы со смарт-контрактом уже через web-приложение.

© Habrahabr.ru