Взаимодействие из 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-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.