Источник: Nuances of Programming
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 нацелена на проектирование будущего интернета.
Читайте также:
Перевод статьи Jennifer Fu: Use Ethers.js to Interact With the Ethereum Virtual Machine in Remix