Библиотека Ethers.js: новичкам на заметку

Ethers.js и web3.js  —  это две библиотеки JavaScript с открытым исходным кодом, которые позволяют разработчикам взаимодействовать с блокчейном Ethereum и выполнять разные задачи.

Данная диаграмма отображает тенденции npm между ethers.js и web3.js:

Web3.js разработана Ethereum Foundation при участии 281 специалиста. Библиотека широко используется во многих проектах.

В статье мы внимательно изучим ethers.js, которую создал и поддерживает канадский разработчик Rick Moore (Рик Мур). На данный момент она насчитывает 14 участников разработки. У библиотеки небольшой пакет установки, она протестирована, задокументирована и отлично обслуживается. 

Рабочая среда 

Remix, полностековый фреймворк React, послужит основой для изучения ethers.js. Создаем проект Remix, выполняя следующую команду:

% npx create-remix my-remix-app
% cd my-remix-app

Устанавливаем ethers вместе с react-json-pretty:

npm i ethers react-json-pretty

Эти пакеты становятся частью dependencies в package.json:

"dependencies": {
"@remix-run/node": "^1.3.5",
"@remix-run/react": "^1.3.5",
"@remix-run/serve": "^1.3.5",
"ethers": "^5.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-json-pretty": "^2.2.0"
}

Рабочая среда Remix готова для работы с ethers.js.

Классы Provider, Signer и Contract

Тремя основными классами в ether.js являются Provider (Провайдер), Signer (Подписант) и Contract (Контракт). Они применяются для взаимодействия со смарт-контрактом. 

Provider

Provider  —  это класс, который обеспечивает подключение к сети Ethereum Network, а также предоставляет доступ только для чтения к блокчейну и его состоянию. 

Для создания Provider используется getDefaultProvider, который экспортируется из ethers. Он принимает два параметра, первый из которых  —  network с одним из следующих значений.

  • homestead  —  основная сеть Mainnet. 
  • ropsten  —  тестовая сеть Ropsten по принципу proof-of-work (с подтверждением выполнения работы).
  • rinkeby  —  тестовая сеть Rinkeby по принципу proof-of-authority (с подтверждением полномочий).
  • goerli  —  тестовая сеть Görli, работающая с клиентами.
  • kovan  —  тестовая сеть Kovan по принципу proof-of-authority (с подтверждением полномочий).

Второй параметр  —  options. Он поддерживает нижеуказанные пары “ключ-значение”: 

  • etherscan: ETHERSCAN_API_KEY;
  • infura: INFURA_PROJECT_ID;
  • alchemy: ALCHEMY_API_KEY;
  • pocket: POCKET_APPLICATION_KEY;
  • ankr: ANKR_API_KEY.

Выбираем сеть infura вместе с INFURA_PROJECT_ID, 57e665ef67b44c4687ad529b8b89397c, созданным в проекте web3.js

import { getDefaultProvider } from "ethers";

export const provider = new getDefaultProvider("homestead", {
infura: "57e665ef67b44c4687ad529b8b89397c",
});

Как вариант, можно создать Provider, импортируя provides из ethers, и new providers.InfuraProvider с теми же параметрами. 

import { providers } from "ethers";

export const provider = new providers.InfuraProvider("homestead", "57e665ef67b44c4687ad529b8b89397c");

Signer

Signer  —  это класс, который применяет приватный ключ для подписания сообщений/транзакций с целью авторизации операций. Он представляет собой абстракцию адреса пользовательского кошелька. Его можно инстанцировать посредством статического метода Wallet

import { Wallet } from "ethers";

export const signer = Wallet.createRandom();

Contract

Contract  —  это класс, представляющий собой подключение к конкретному контракту в сети Ethereum. 

Мы можем импортировать Contract из ethers и создать экземпляр с new Contract(daiAddress, daiAbi, provider).

  • daiAddress  —  служба именования адресов Ethereum (англ. Ethereum Name Service, ENS). Dai является стабильной криптовалютой, которая стремится поддерживать свою стоимость как можно ближе к одному доллару США (USD). 
  • daiAbi определяет контракт с двоичным интерфейсом приложения (англ. Application Binary Interface, ABI), который описывает имеющиеся у него методы и события. Эти методы нужны для взаимодействия с контрактом в блокчейне (управляемый контракт, англ. on-chain), а также для кодирования и декодирования данных.
const daiAbi = [
// информация о токене
"function name() view returns (string)",
"function symbol() view returns (string)",

// получение баланса счета
"function balanceOf(address) view returns (uint)",

// отправка токенов другим лицам
"function transfer(address to, uint amount)",

// запуск события при выполнении транзакций другим лицам
"event Transfer(address indexed from, address indexed to, uint amount)"
];
  • provider  —  подключение к сети Ethereum.

Provider, Signer и Contract в Remix

Remix  —  наша рабочая среда. app/entry.server.jsx —  первый код JavaScript, который выполняется при поступлении запроса на сервер. Он загружает только необходимые данные, но разработчики должны обработать ответ. Этот файл рендерит приложение React в строку/поток, который отправляется клиенту в качестве ответа.  

Ниже представлен измененный app/entry.server.jsx, который создает provider (строка 6), signer (строка 11) и contract (строка 33). 

import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";
import { Contract, Wallet, getDefaultProvider } from "ethers";

// создание provider
export const provider = new getDefaultProvider("homestead", {
infura: "57e665ef67b44c4687ad529b8b89397c",
});

// создание signer
export const signer = Wallet.createRandom();

// создание первого параметра Contract
const daiAddress = "dai.tokens.ethers.eth";

// создание второго параметра Contract
const daiAbi = [
// информация о токене
"function name() view returns (string)",
"function symbol() view returns (string)",

// получение баланса счета
"function balanceOf(address) view returns (uint)",

// отправка токенов другим лицам
"function transfer(address to, uint amount)",

// запуск события при выполнении транзакций другим лицам
"event Transfer(address indexed from, address indexed to, uint amount)"
];

// создание Contract
export const daiContract = new Contract(daiAddress, daiAbi, provider);

export default function handleRequest(
request,
responseStatusCode,
responseHeaders,
remixContext
) {
let markup = renderToString(
<RemixServer context={remixContext} url={request.url} />
);

responseHeaders.set("Content-Type", "text/html");

return new Response("<!DOCTYPE html>" + markup, {
status: responseStatusCode,
headers: responseHeaders,
});
}

Эти экземпляры импортируются в индексный маршрут app/routes/index.jsx, который вызывается по умолчанию. 

import { useLoaderData } from "@remix-run/react";
import JSONPretty from "react-json-pretty";
import { utils } from "ethers";
import { daiContract, provider, signer } from "../entry.server";

export const loader = async () => {
const privateKey = signer.privateKey;
console.log('privateKey =', privateKey);
// privateKey = 0x54662899fea8d6fcd983549e9f17e725ef11d415cd715eaead74e1fa3ceb599a

const publicKey = signer.publicKey;
console.log('publicKey =', publicKey);
// publicKey = 0x044214736f4b64f7061edde89e78298d47bb91b67ae713edd1accb4601c8f513f0f37218d343a2a974d8e46e7075b39c9c51ce3df6b5a223e5633fda410b039c85

const address = await signer.getAddress();
console.log('address =', address);
// address = 0xf9911429fA0A7BACA28C9093398AFD527f73e6fF

const signatureForString = await signer.signMessage('Hello World');
console.log('signatureForString =', signatureForString);
// signatureForString = 0x43c669b9b08a835c922f265a00d185966ff97ad9fb0385e72711e6f54d521872003ff80c68f02e2e56d9b9e73400a5e8841c70f4eee515158991cc2d222178f91c

const messageHash = utils.id("Hello World"); // 0x592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba
const messageHashBytes = utils.arrayify(messageHash)
// Uint8Array(32) [
// 89, 47, 167, 67, 136, 159, 199, 249,
// 42, 194, 163, 123, 177, 245, 186, 29,
// 175, 42, 92, 132, 116, 28, 160, 224,
// 6, 29, 36, 58, 46, 103, 7, 186
// ]
const signatureForHash = await signer.signMessage(messageHashBytes);
console.log('signatureForHash =', signatureForHash);
// signatureForHash = 0x6f2f0911228927a9e6e54050a4c9ef4eb44df2de375ea5c057066008fc8d9dce77d035e82dc6fa7a6928d9b4063c45b49d56f3b55f2f43802e2d985539217c3f1c

const contractName = await daiContract.name();
console.log('contractName =', contractName);
// contractName = Dai Stablecoin

const contractSymbol = await daiContract.symbol();
console.log('contractSymbol =', contractSymbol);
// contractSymbol = DAI

const contractBalance = await daiContract.balanceOf('dai.tokens.ethers.eth');
console.log('contractBalance =', contractBalance);
// contractBalance = BigNumber { _hex: '0x758d5512acfe9a662174', _isBigNumber: true }

console.log('in Wei =', utils.formatUnits(contractBalance, 'wei')); // in Wei = 555123999562393853501812
console.log('in Kwei =', utils.formatUnits(contractBalance, 'kwei')); // in Kwei = 555123999562393853501.812
console.log('in Mwei =', utils.formatUnits(contractBalance, 'mwei')); // in Mwei = 555123999562393853.501812
console.log('in Gwei =', utils.formatUnits(contractBalance, 'gwei')); // in Gwei = 555123999562393.853501812
console.log('in Szabo =', utils.formatUnits(contractBalance, 'szabo')); // in Szabo = 555123999562.393853501812
console.log('in Finney =', utils.formatUnits(contractBalance, 'finney')); // in Finney = 555123999.562393853501812
console.log('in Ether =', utils.formatUnits(contractBalance, 'ether')); // in Ether = 555123.999562393853501812

return new Promise((resolve) => resolve(provider));
};

export default function Index() {
const result = useLoaderData();
return <JSONPretty data={result} />;
}

Строка 2. Импорт react-json-pretty как JSONPretty, который придает привлекательный вид данным JSON. Этот компонент используется в строке 60. 

Строка 3. Импорт коллекции утилит utils из ethers

Строка 4. Импорт daiContract, provider и signer из app/entry.server.jsx.

Функция loader (строки 6–56)  —  это специальный API. Она экспортируется для вызова на сервере перед рендерингом. Нужна для отображения содержимого signer, daiContract и provider.

В отношении signer мы можем просмотреть приватный ключ (строка 7), публичный ключ (строка 11) и адрес (строка 15). 

Подписание сообщений применяется для различных методов аутентификации и операций вне блокчейна (неуправляемый контракт, англ. off-chain), которые при необходимости можно переместить в блокчейн.

Строка 19. Подписание строкового сообщения. 

Строка 31. Подписание хеш-суммы. 

Строка 35. Извлечение имени контракта. 

Строка 39. Извлечение имени символа. 

Строка 43. Извлечение баланса в формате BigNumber, который является объектом, позволяющим безопасно выполнять математические операции с числами любой величины. 

Нативная криптовалюта Ethereum  —  это эфир (англ. ether, ETH), а ее наименьшей единицей по умолчанию является Wei. Ниже представлен курс конвертации между разными единицами: 

1 Ether = 10³Finney = 10⁶Szabo = 10⁹Gwei = 10¹²Mwei = 10¹⁵Kwei = 10¹⁸Wei

Строки 47–53. Форматирование баланса по различным единицам. 

Строка 55. Выполнение функции loader с объектом provider.

Строка 59. Вызов useLoaderData для извлечения загруженных данных result.

Строка 60. JSONPretty отображает result в браузере. 

Запускаем приложение Remix с помощью npm run dev, переходим в окно браузера и видим длинный список объекта provider:

Подожмем объект provider с помощью JSON Viewer по ссылке http://jsonviewer.stack.hu/:

Он отображает свойства и методы первого уровня. 

По сравнению с web3.js структура данных в ethers.js более компактная. Кроме того, отсутствует проблема циклических ссылок. В остальном же они выполняют похожие операции. 

Заключение

Мы рассмотрели принцип использования ethers.js для взаимодействия с виртуальной машиной Ethereum через Infura. 

Ethers.js и web3.js  —  две библиотеки JavaScript, позволяющие разработчикам взаимодействовать с блокчейном Ethereum. В процессе разработки Web3 они обе активно наращивают свой функционал. Разработка этих пакетов JavaScript нацелена на проектирование будущего интернета.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Jennifer Fu: Use Ethers.js to Interact With the Ethereum Virtual Machine in Remix

Предыдущая статьяПараллельные вычисления: введение
Следующая статьяПростое руководство по форматированию строк в Python с помощью f-строк