August 11, 2022

Эфириум в Расте? Часть первая: Отправка простых транзакций.

В этом руководстве показано, как запустить локальную виртуальную машину Ethereum (EVM) в Rust, запросить баланс и совершить простые транзакции.

Настройка зависимостей

Прежде чем писать код, мы должны убедиться, что у нас установлены некоторые необходимые инструменты, а именно Rust и Ganache.

Rust

Установите Rust, следуя инструкциям. Или, если вы используете Arch Linux, вы можете следовать инструкциям здесь, чтобы установить Rust.

ganache-cli

Ganache поможет вам быстро создать среду Ethereum для тестирования. Если вам не нужен весь пакет, вы можете просто установить ganache-cli с помощью npm,

npm install -g ganache

Запустите ganache-cli, чтобы проверить, правильно ли вы его установили.

Настройка проекта Rust

Теперь мы создадим проект Rust и добавим необходимые зависимости.

Создайте папку нашего проекта и инициализируйте его с помощью cargo:

mkdir rust-ethereum-tutorial
cd rust-ethereum-tutorial
cargo init

Для удобства добавления зависимостей Rust в наш проект мы будем использовать cargo-edit:

cargo install cargo-edit

Это добавит последнюю версию ethers в качестве зависимости в Cargo.toml:

cargo add --no-default-features ethers +legacy

и еще несколько библиотек:

cargo add tokio +full clap hex eyre

Краткое описание этих библиотек:

  • tokio - позволяет нам использовать асинхронные функции в Rust
  • clap - для работы с аргументами командной строки
  • hex - для работы с шестнадцатеричными строками
  • eyre - это библиотека, которая помогает сократить количество кодового кода при обработке ошибок

Подключение к RPC Ethereum Web3

Узел Ethereum обычно предоставляет конечную точку HTTP, WebSocket или IPC для доступа к сети Ethereum. Для разработки в локальной среде мы можем подключиться к нашей конечной точке, предоставляемой локально запущенным ganache. Чтобы запустить ganache в Rust, мы можем просто использовать Ganache::new().spawn():

use ethers::utils::Ganache;
use eyre::Result;#[tokio::main]
async fn main() -> Result<()> {
    // Спавн Ganache экземпляра
    let ganache = Ganache::new().spawn();
    println!("HTTP Endpoint: {}", ganache.endpoint());
    Ok(())
}

Если мы выполним команду cargo run, мы увидим URL конечной точки, выведенный в консоли:

...
HTTP Endpoint: http://localhost:46795

Доступ к тестовым кошелькам ganache

При каждом запуске Ganache будет использовать случайно генерируемые мнемоники.

Чтобы сделать наш пример более детерминированным, мы можем настроить его на использование определенную мнемонику:
let mnemonic = "gas monster ski craft below illegal discover limit dog bundle bus artefact";
let ganache = Ganache::new().mnemonic(mnemonic).spawn();

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

// Получите первый кошелек, управляемый ganache
let wallet: LocalWallet = ganache.keys()[0].clone().into();
let wallet_address: String = wallet.address().encode_hex();
println!("Default wallet address: {}", wallet_address);

Подключение через JSON RPC

Для взаимодействия с узлом или сетью Ethereum нам потребуется создать клиент, который подключается к конечной точке ganache:

// Провайдер - это клиент Ethereum JsonRPC.
let provider = Provider::try_from(ganache.endpoint())?.interval(Duration::from_millis(10));

Запрос баланса по адресу

Теперь, когда наш клиент подключен к сети Ethereum, мы можем запросить баланс любого адреса в нашем кошельке:

// Запросить баланс нашего счета
let first_balance = provider.get_balance(first_address, None).await?;
println!("Баланс первого адреса кошелька: {}", first_balance);

Если мы снова запустим программу cargo run, то узнаем, что по этому адресу у нас есть 1000ETH. Это добавляется программой Ganache автоматически.

Чтобы запросить баланс, используя адрес в формате шестнадцатеричной строки, нам нужно сначала преобразовать строку в тип Address:

// Запросить баланс случайного счета
let other_address_hex = "0xaf206dCE72A0ef76643dfeDa34DB764E2126E646";
let other_address = "0xaf206dCE72A0ef76643dfeDa34DB764E2126E646".parse::<Address>()?;
let other_balance = provider.get_balance(other_address, None).await?;
println!(
    "Баланс адреса {}: {}",
    other_address_hex, other_balance
);

Попробуйте cargo run, и вы увидите, что баланс этого адреса равен нулю:

Баланс адреса 0xaf206dCE72A0ef76643dfeDa34DB764E2126E646: 0

Создать простую транзакцию

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

// Создайте транзакцию для перевода 1000 wei на `other_address`.
let tx = TransactionRequest::pay(other_address, U256::from(1000u64)).from(first_address);
// Отправляем транзакцию и ждем получения
let receipt = provider
    .send_transaction(tx, None)
    .await?
    .log_msg("Ожидание трансфера")
    .await?
    .context("Нет получателя")?;
    
println!(
    "Транзакция замайнена на блоке {}",
    receipt.block_number.context("Не получилось получить блок транзакции")?
);
println!(
    "Баланс у {} {}",
    other_address_hex,
    provider.get_balance(other_address, None).await?
);

Выполните cargo run, вы получите следующие результаты:

Ожидание трансфера: 
0xff6153310304732bb28856ca3a90ab9c94c5c9e20cf51e8a2803f3483670cd6f
Транзакция замайнена на блоке 1
Баланс у 0xaf206dCE72A0ef76643dfeDa34DB764E2126E646 1000