Раздел 30 · Web3 на Rust

Взаимодействие из Rust: alloy

senior~35 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Взаимодействие из Rust: alloy

Контракт живёт в цепи, но говорить с ним из Solidity-тестов это ещё не приложение. Настоящий бэкенд, бот или кошелёк написан на нормальном языке, и для нас это Rust. Свяжем два мира: возьмём alloy, соберём вызов контракта по ABI, прочитаем события из логов и подпишем транзакцию. Это мост между системным Rust из раздела 23 и всем, что мы построили в блоке Web3.

Идея

Исторически из Rust в Ethereum ходили через ethers-rs, но проект закрыт, и его команда собрала преемника alloy. Он даёт три вещи: провайдер (соединение с узлом), типы примитивов (Address, U256, B256) и макрос sol!, который прямо из Solidity-сигнатур генерирует типобезопасные Rust-обёртки.

Для всех примеров урока хватает трёх зависимостей. Фича full у alloy включает сразу провайдеры, подписантов, макрос sol! и типы RPC, для учебного проекта это самый простой путь (в проде можно собрать список фич уже, чтобы не тянуть лишнее):

[dependencies]
alloy = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
anyhow = "1"

Провайдер это просто клиент JSON-RPC, тот же протокол, что у Bitcoin из урока 23, только с методами Ethereum:

use alloy::providers::{Provider, ProviderBuilder};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // соединяемся с узлом (локальный anvil, Infura, свой reth)
    let provider = ProviderBuilder::new().connect_http("http://localhost:8545".parse()?);

    let block = provider.get_block_number().await?;
    println!("высота: {block}");
    Ok(())
}

Обрати внимание на async и tokio: сеть это тот самый асинхронный Rust, который мы разбирали в уроке 49 раздела Rust. Всё, что мы там учили про футуры и рантайм, работает и здесь.

ABI: как Rust узнаёт форму контракта

Контракт в цепи это байткод, а не типы. Чтобы вызвать его функцию, надо знать её сигнатуру и уметь закодировать аргументы так, как ждёт EVM. Это описывает ABI: имена функций, типы аргументов, события. Вызов кодируется в calldata как селектор (первые 4 байта keccak256 от сигнатуры) плюс упакованные по 32 байта аргументы. Это ровно тот calldata, который читал наш мини-EVM в уроке 26.

Руками это писать мучительно, поэтому sol! делает всё за тебя. Пишешь Solidity-сигнатуру, получаешь Rust-метод:

use alloy::{primitives::{address, U256}, providers::ProviderBuilder, sol};

// прямо из Solidity генерируется типобезопасная обёртка контракта
sol! {
    #[sol(rpc)]
    contract IERC20 {
        function balanceOf(address who) external view returns (uint256);
        function transfer(address to, uint256 amount) external returns (bool);
        event Transfer(address indexed from, address indexed to, uint256 value);
    }
}

async fn read_balance() -> anyhow::Result<()> {
    let provider = ProviderBuilder::new().connect_http("http://localhost:8545".parse()?);
    // адрес контракта USDC в mainnet
    let token = IERC20::new(address!("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"), &provider);

    // view-функция: .call() читает состояние, транзакцию не шлёт
    // (адрес аргумента это просто чей-то кошелёк, чей баланс мы смотрим)
    let balance = token.balanceOf(address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045")).call().await?;
    println!("баланс: {balance}");
    Ok(())
}

balanceOf(...).call() это чтение (селектор посчитан, calldata собрана, ответ раскодирован обратно в U256), а .send() уже транслирует транзакцию. Разница ровно как между eth_call и отправкой в мемпул: одно бесплатно и не меняет состояние, второе стоит газа и попадает в блок.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.