引介: Alloy - 与以太坊智能合约交互 Rust 库

Alloy - 与以太坊智能合约交互 Rust 库

序言

在像 Intuition 这样的快节奏初创公司环境中,我们必须以闪电般的速度前进。这意味着我们始终在利用最新最优秀的库 crate,推动界限以充分发挥 Rust 的性能和稳定性。这种敏捷的方法使我们能够保持在前沿,确保我们的解决方案不仅高效,而且创新。然而,这种现实与在大型科技公司工作的人的情况可能截然不同,在那里,稳定性和长期支持往往优先于采用最新技术。

作为一名经验丰富的后端工程师,对 Rust 有着深厚的热爱,我亲眼目睹了语言生态系统的演变和成长的烦恼。Rust 开发者面临的主要障碍之一是缺乏稳健且成熟的库生态系统。这不仅仅是一个小烦恼;它是一个重大挑战,影响着项目时间表、稳定性和整体开发者体验。

以一些最流行的 web 框架为例。Axum,在 Rust web 框架界崭露头角的明星,当前版本为 0.7.9。Rocket,另一款备受喜爱的框架,则在 0.5.1 版本。这些版本号生动地提醒我们,即便是 Rust 生态系统中最广泛使用的工具仍在不断开发中。数据库方面情况也类似。SQLX,一个异步 SQL crate,目前仅在 0.8.2 版本。虽然因其性能和类型安全而备受推崇,但其相对较早的版本可能让寻求长期稳定的开发者感到担忧。

更复杂的是,一些成熟的库已经不再维护。ethers-rs crate,一直以来在与以太坊智能合约交互中扮演重要角色,正被新的 Alloy 库取代。多年来,ethers 和 web3 crate 一直是处理合约事件的首选组合,老实说,这些组件的组合并不理想,开发体验也很奇怪,但现在情况发生了变化——而且是向好的方向发展!

使用 Alloy 与以太坊智能合约进行交互

一只精明的螃蟹深入以太坊智能合约,逐条条款进行!

Alloy 将应用程序连接到区块链。就这么简单。Alloy 是对 [ethers-rs](https://github.com/gakonst/ethers-rs) 的全面重写,具有令人兴奋的新特性、高性能和优秀的 文档。他们还拥有一本关于所有 Alloy 相关内容的 和许多 示例 来帮助你入门。精心编写的文档和示例对于开发者来说无价,因为它们显著减少学习曲线,提高生产力,确保软件的高效和无错误实现,并且可以为类似 cursor 的 AI 工具提供支持,帮助我们高效地构建所需解决方案。不再废话,让我们开始行动吧!

应用

在 Web3 应用中,一个常见的任务是检索有关帐户的 ENS 信息,有许多不同的实现方式,但最简单的方式就是直接调用合约。为了这个示例,我们将使用 clap 构建一个 CLI 应用。这个应用将需要一个参数:钱包地址,以便我们可以根据该信息尝试查找 ENS 信息。在合约交互方面,我们需要两个接口:ENSRegistryENSName,一会儿我们会介绍它们。

让我们开始创建一个新项目:

$ cargo new ens-resolution

导航到创建的 ens-resolution 文件夹并添加所需的库:

$ cargo add alloy --features full  
$ cargo add serde --features derive  
$ cargo add tokio --features full  
$ cargo add reqwest --features reqwest/multipart,json  
$ cargo add thiserror serde_json log envy dotenvy  
$ cargo add clap --features derive

现在可以尝试 cargo run,以确保一切正常且库 crate 正在正确下载和链接。

现在让我们继续创建一个 error.rs 文件,内容如下:

use thiserror::Error;  

#[derive(Error, Debug)]  
pub enum AppError {  
    #[error("解析地址失败: {0}")]  
    AddressParse(String),  
    #[error("铸造合约错误: {0}")]  
    AlloyContractError(#[from] alloy::contract::Error),  
    #[error(transparent)]  
    Env(#[from] envy::Error),  
    #[error("从十六进制解析失败: {0}")]  
    FromHexError(#[from] alloy::hex::FromHexError),  
    #[error("无效的 URL")]  
    InvalidUrl(),  
    #[error(transparent)]  
    Reqwest(#[from] reqwest::Error),  
}

这将使我们能够优雅地处理一些可能遇到的错误,并使我们的应用更加稳健。

现在让我们创建一个 app.rs 文件,内容如下:

use std::str::FromStr;  

use alloy::{  
    primitives::Address,  
    providers::{ProviderBuilder, RootProvider},  
    transports::http::Http,  
};  
use clap::Parser;  
use reqwest::Client;  
use serde::Deserialize;  

use crate::{  
    error::AppError,  
    ENSRegistry::{self, ENSRegistryInstance},  
    EnsAppArgs,  
};  

/// 环境变量  
#[derive(Deserialize)]  
pub struct Env {  
    rpc_url_mainnet: String,  
    ens_contract_address: String,  
}  

/// 应用数据  
pub struct AppData {  
    pub env: Env,  
    pub args: EnsAppArgs,  
}  

/// 应用  
pub struct App {  
    pub ens_client: ENSRegistryInstance<Http<Client>, RootProvider<Http<Client>>>,  
}  

impl App {  
    pub fn new(env: Env) -> Result<Self, AppError> {  
        let ens_client = Self::build_ens_client(&env.rpc_url_mainnet, &env.ens_contract_address)?;  

        Ok(Self { ens_client })  
    }  

    /// 构建 ENS 客户端  
    fn build_ens_client(  
        rpc_url: &str,  
        contract_address: &str,  
    ) -> Result<ENSRegistryInstance<Http<Client>, RootProvider<Http<Client>>>, AppError> {  
        let provider =  
            ProviderBuilder::new().on_http(rpc_url.parse().map_err(|_e| AppError::InvalidUrl())?);  

        let alloy_contract = ENSRegistry::new(  
            Address::from_str(contract_address)  
                .map_err(|e| AppError::AddressParse(e.to_string()))?,  
            provider.clone(),  
        );  

        Ok(alloy_contract)  
    }  

    /// 初始化应用  
    pub fn init() -> Result<AppData, AppError> {  
        // 从当前目录或父级读取 .env 文件  
        dotenvy::dotenv().ok();  
        // 解析环境变量  
        let env = envy::from_env::<Env>()?;  
        // 解析 CLI 参数  
        let args = EnsAppArgs::parse();  
        Ok(AppData { env, args })  
    }  
}

这里有很多内容,但简而言之,我们正在创建三个新结构体:EnvAppDataApp。第一个,顾名思义,将映射你系统中的环境变量到此结构体,因此你必须在系统中设置等效变量。你可以通过以下方式实现:

$ export RPC_URL_MAINNET=https://your-rpc-url.com  
$ export ENS_CONTRACT_ADDRESS=0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e

我在使用 Alchemy 作为 RPC 提供者,但你可以使用你选择的任何一个,只要确保你有一个可用的并将 URL 替换为正确的值。AppData 保存应用从环境变量和 CLI 参数获取的信息,并通过 App::init() 创建。最后,App 保存个 ENS 客户端,该客户端通过 build_end_client 相关函数构建。这个函数创建了一个 provider,它暴露以太坊 JSON-RPC 方法。在 alloy 中,提供者类似于 ethers.js 提供者。它们管理一个 RpcClient,并允许程序的其他部分轻松地进行 RPC 调用。如你所见,我们正在创建基于 ENSRegistryalloy_contract,并传入 ENS 合约地址,这是我们需要交互以获取 ENS 信息的两个接口之一。它们在 main.rs 文件中定义,但在我们跳转到那之前,让我们创建最后一个文件:ens.rs,内容如下:

use alloy::{  
    primitives::{keccak256, Address, FixedBytes},  
    providers::RootProvider,  
    transports::http::Http,  
};  
use log::info;  
use reqwest::Client;  

use crate::{error::AppError, ENSName::ENSNameInstance, ENSRegistry::ENSRegistryInstance};  

/// 该结构表示地址的 ENS 名称和头像。  
#[derive(Clone, Debug)]  
pub struct Ens {  
    pub name: Option<String>,  
    pub image: Option<String>,  
}  

impl Ens {  
    /// 此函数对 ENS 名称进行哈希。  
    fn namehash(name: &str) -> Vec<u8> {  
        if name.is_empty() {  
            return vec![0u8; 32];  
        }  
        let mut hash = vec![0u8; 32];  
        for label in name.rsplit('.') {  
            hash.append(&mut keccak256(label.as_bytes()).to_vec());  
            hash = keccak256(hash.as_slice()).to_vec();  
        }  
        hash  
    }  

    /// 此函数获取地址的 ENS 名称和头像。  
    pub async fn get_ens(  
        address: Address,  
        mainnet_client: &ENSRegistryInstance<Http<Client>, RootProvider<Http<Client>>>,  
    ) -> Result<Ens, AppError> {  
        let name = Self::get_ens_name(address, mainnet_client).await?;  
        let mut image = None;  
        if let Some(name_str) = &name {  
            image = Self::get_ens_avatar(name_str).await?;  
        }  
        Ok(Ens { name, image })  
    }  

    /// 获取给定名称的 ENS 头像 URL  
    async fn get_ens_avatar(name: &str) -> Result<Option<String>, AppError> {  
        let url = format!("https://metadata.ens.domains/mainnet/avatar/{}", name);  
        match reqwest::get(&url).await {  
            Ok(response) => {  
                if response.status() == 200 {  
                    Ok(Some(url))  
                } else {  
                    Ok(None)  
                }  
            }  
            Err(_) => Ok(None),  
        }  
    }  

    /// 此函数获取地址的 ENS 名称。  
    pub async fn get_ens_name(  
        address: Address,  
        mainnet_client: &ENSRegistryInstance<Http<Client>, RootProvider<Http<Client>>>,  
    ) -> Result<Option<String>, AppError> {  
        info!("Getting ENS name for {}", address);  
        let address_hash = Self::namehash(&Self::prepare_name(address));  
        let resolver_address =  
            Self::get_resolver_address(address, &address_hash, mainnet_client).await?;  

        if resolver_address != Address::ZERO {  
            let alloy_contract = ENSNameInstance::new(resolver_address, mainnet_client.provider());  
            let name = alloy_contract  
                .name(FixedBytes::from_slice(address_hash.as_slice()))  
                .call()  
                .await?  
                ._0;  
            info!("ResolvedENS name: {:?}", name);  
            Ok(Some(name))  
        } else {  
            Ok(None)  
        }  
    }  

    /// 此函数获取地址哈希的解析器地址。  
    async fn get_resolver_address(  
        address: Address,  
        address_hash: &[u8],  
        mainnet_client: &ENSRegistryInstance<Http<Client>, RootProvider<Http<Client>>>,  
    ) -> Result<Address, AppError> {  
        let resolver_address = mainnet_client  
            .resolver(FixedBytes::from_slice(address_hash))  
            .call()  
            .await?  
            ._0;  

        if resolver_address == Address::ZERO {  
            info!("No resolver found for {}", address);  
        } else {  
            info!("Resolver found for {}: {}", address, resolver_address);  
        }  

        Ok(resolver_address)  
    }  

    /// 此函数准备 ENS 解析器的名称。  
    fn prepare_name(address: Address) -> String {  
        let addr_str = address.to_string().to_lowercase();  
        format!("{}.addr.reverse", addr_str.trim_start_matches("0x"))  
    }  
}

Ens 结构包含两个可选字段:nameimageEns 模块的入口点是关联函数 get_ens,我们在这里尝试为提供的地址获取 ENS 名称和头像。我们首先使用 get_ens_name 关联函数,执行几个操作:

  • 我们对提供的 address 进行哈希
  • 我们使用 get_resolver_address 关联函数获取地址哈希的 resolver
  • 如果解析器存在于提供的地址中,我们调用 name 函数来获取名称

最后,我们使用 get_ens_avatar 函数获取用户的头像。请记住,ENS 名称和头像都是可选字段。

现在我们可以继续 main.rs 文件

#![allow(clippy::result_large_err)]  
use std::str::FromStr;  

use alloy::{primitives::Address, sol};  
use app::App;  
use clap::Parser;  
use ens::Ens;  
use error::AppError;  
use serde::{Deserialize, Serialize};  

mod app;  
mod ens;  
mod error;  

// 与 ENS 合约交互的代码生成。  
sol!(  
    #[derive(Debug, Deserialize, Serialize)]  
    #[allow(missing_docs)]  
    #[sol(rpc)]  
    interface ENSRegistry {  
        function resolver(bytes32 node) external view returns (address);  
    }  
);  

// 与 ENSName 合约交互的代码生成。  
sol! {  
    #[allow(missing_docs)]  
    #[sol(rpc)]  
    interface ENSName {  
        function name(bytes32 node) external view returns (string);  
    }  
}  

/// ENS 应用的 CLI 参数  
#[derive(Parser, Clone, Debug)]  
pub struct EnsAppArgs {  
    #[arg(short, long)]  
    address: String,  
}  

#[tokio::main]  
async fn main() -> Result<(), AppError> {  
    let app_data = App::init()?;  
    let app = App::new(app_data.env)?;  
    let ens = Ens::get_ens(Address::from_str(&app_data.args.address)?, &app.ens_client).await?;  
    if let Some(name) = &ens.name {  
        println!("ENS name: {}", name);  
    }  
    if let Some(image) = &ens.image {  
        println!("ENS image: {}", image);  
    }  
    Ok(())  
}

在这里,我们终于创建了与智能合约交互所需的两个接口:ENSRegistry 和 ENSName。我们使用了 sol 宏,这是一个过程宏,用于解析 Solidity 语法并生成 Rust 类型。此外,我们还有 EnsAppArgs 结构,它定义了 CLI 应用参数,在本例中,仅为地址。main 函数非常简单,目的是展示如何获取 ENS 数据。如果提供的钱包关联有信息,则数据将被打印。你可以用以下命令测试:

$ RUST_LOG=INFO cargo run -- --address 0x88d0af73508452c1a453356b3fac26525aec23a2

and the output is going to be:

RUST_LOG=INFO cargo run -- --address 0x88d0af73508452c1a453356b3fac26525aec23a2  
   Compiling ens-resolution v0.1.0 (/Users/leboiko/Documents/ens-resolution)  
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.85s  
     Running `target/debug/ens-resolution --address 0x88d0af73508452c1a453356b3fac26525aec23a2`  
ENS name: intuitionbilly.eth  
ENS image: https://metadata.ens.domains/mainnet/avatar/intuitionbilly.eth

I hope that you guys enjoyed this article, there are many interesting things happening at Intuition and we are going to start sharing more content on regular basis, so your feedback is very valuable!

The full project is available at [https://github.com/0xIntuition/rust-ens-example](https://github.com/0xIntuition/rust-ens-example). If you enjoyed this post and found it helpful, I’d really appreciate it if you could give it a star on GitHub! ⭐ Also, don’t forget to check out how we use all the technologies discussed in this article in a real-world open-source project ([https://github.com/0xIntuition/intuition-rs](https://github.com/0xIntuition/intuition-rs)). Your support helps us continue to innovate and share more insights. If you’re interested in contributing to open-source software written in Rust, now’s the time!
$ RUST_LOG=INFO cargo run -- --address 0x88d0af73508452c1a453356b3fac26525aec23a2

输出将会是:

RUST_LOG=INFO cargo run -- --address 0x88d0af73508452c1a453356b3fac26525aec23a2
编译 ens-resolution v0.1.0 (/Users/leboiko/Documents/ens-resolution)
完成 dev 配置 [未优化 + 调试信息] 目标在 1.85 秒内
运行 target/debug/ens-resolution --address 0x88d0af73508452c1a453356b3fac26525aec23a2
ENS 名称: intuitionbilly.eth
ENS 图片: https://metadata.ens.domains/mainnet/avatar/intuitionbilly.eth

我希望你们喜欢这篇文章,Intuition 正在发生许多有趣的事情,我们将开始定期分享更多内容,所以你的反馈非常重要!

完整项目可在 https://github.com/0xIntuition/rust-ens-example 获取。如果你喜欢这篇文章并发现它有帮助,我会非常感激你能在 GitHub 上给它一个星星! ⭐ 同时,不要忘记查看我们如何在一个实际的开源项目中使用本文讨论的所有技术 (https://github.com/0xIntuition/intuition-rs)。你的支持能帮助我们继续创新并分享更多见解。如果你有兴趣为用 Rust 编写的开源软件贡献,现在是时候了!

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
0xintuition
0xintuition
江湖只有他的大名,没有他的介绍。