惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

美团技术团队
人人都是产品经理
人人都是产品经理
月光博客
月光博客
V
V2EX
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
Last Week in AI
Last Week in AI
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
宝玉的分享
宝玉的分享
雷峰网
雷峰网
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - Franky
博客园 - 聂微东
博客园 - 司徒正美
博客园 - 【当耐特】
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志

又耳笔记

用AI挣钱之AI建站:二维码生成器工具 用AI挣钱之AI建站:https://markdowntopdf.top Cloudflare Workers实战番外一:用Static Assets托管网站,告别传统方案 Cloudflare Workers实战(五):不止JavaScript,拥抱Python与Rust Cloudflare Workers实战(四):托管和分发静态文件 Cloudflare Workers实战(三):实现认证、重定向与缓存 Cloudflare Workers实战(二):动态修改后端响应 Cloudflare Workers实战(一):随心所欲操作客户端请求 Cloudflare workers不完全指南 使用n8n创作短篇小说 水果风波:信任的代价 用Python将PDF文件转换成图片 rust网络框架Pingora源码阅读3 rust网络框架Pingora源码阅读2 rust网络框架Pingora源码阅读1 Pingora快速入门教程1之总览 Rust模板引擎askama快速入门引擎 使用Loco快速搭建自己的后台系统 用Rust发一封图文并茂的邮件 Rust小项目: 写一个简单的恶意流量阻断器 Rust小项目: 写一个简单的网页爬虫 Rust命令行库Clap快速入门教程 Rust小项目:用Rust写一个端口扫描器 Rust真全栈开发快速入门 rust声明宏快速入门教程 Rust文本处理快速入门教程 RUST web框架axum快速入门教程6之测试 白嫖免费的Rust在线运行时shuttle RUST web框架axum快速入门教程5之中间件 用Rust来做以太坊开发5之事件日志及签名
用Rust来做以太坊开发2之账户
About 又耳宁 关于技术与股票的一些零碎想法 · 2023-11-26 · via 又耳笔记

本系列文章主要是用Rustethers-rs来复刻《用Go来做以太坊开发》这本书本的内容,所以本系列文章的标题叫做《用Rust来做以太坊开发》, 算是ethers-rs的快速入门教程, 因为原书写得足够好了,所以本系列更多的只是代码层面的复刻,不会说明太多相关的基础知识。

上次研究了ethers-rs怎么创建各种客户端(Provider),这次复刻《用Go来做以太坊开发》的第二章"账户"。

账户这一章主要包括以下内容

  • 账户余额
  • 账户代币余额
  • 生成新钱包
  • 秘钥库
  • 地址验证

往期文章:

本文用到的依赖如下:

ethers = {version="2.0", features=["rustls", "ws"]}
tokio = {version="1", features=["full"]}
eyre = "0.6"
hex = { package = "const-hex", version = "1.6", features = ["hex"] }
regex = "1.10.2"

账户余额

这里用的账户是hardhat的测试账户。

use ethers::prelude::*;
use ethers::utils;

const RPC_URL: &str = "https://cloudflare-eth.com";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = Provider::<Http>::try_from(RPC_URL)?;
    
    let balance = provider.get_balance("0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199", None).await?;
    println!("balance: {} ether", utils::format_ether(balance));
    println!("balance: {balance:?} wei");
    Ok(())
}

get_balance返回的结果类型是U256, 默认单位是wei,如果我们需要转换格式可以使用其提供的工具库utils

账户代币余额

以太坊里的代币一般指ERC20, 所以这里也是展示获取ERC20代币余额的代码,而获取ERC20代币的余额其实就是调用其对应合约地址的balanceOf方法。

代码改自: https://github.com/gakonst/ethers-rs/blob/master/examples/contracts/examples/abigen.rs

use ethers::prelude::*;
use ethers::types::Address;
use ethers::utils;
use std::sync::Arc;

const RPC_URL: &str = "https://cloudflare-eth.com";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = Provider::<Http>::try_from(RPC_URL)?;
    
    let balance = provider.get_balance("0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199", None).await?;
    println!("balance: {} ether", utils::format_ether(balance));
    println!("balance: {balance:?} wei");

    // 通过宏直接生成一个IERC20的对象
    abigen!(
        IERC20,
        r#"[
            function totalSupply() external view returns (uint256)
            function balanceOf(address account) external view returns (uint256)
            function transfer(address recipient, uint256 amount) external returns (bool)
            function allowance(address owner, address spender) external view returns (uint256)
            function approve(address spender, uint256 amount) external returns (bool)
            function transferFrom( address sender, address recipient, uint256 amount) external returns (bool)
            event Transfer(address indexed from, address indexed to, uint256 value)
            event Approval(address indexed owner, address indexed spender, uint256 value)
        ]"#,
    );
    
    const ERC20_CONTRACT_ADDRESS: &str = "0xEB1774bc66930a417A76Df89885CeE7c1A29f405";

    let erc20_address: Address = ERC20_CONTRACT_ADDRESS.parse()?;
    let erc20_account_address: Address = "0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199".parse()?;
    let client = Arc::new(provider);
    let contract = IERC20::new(erc20_address, client);

    if let Ok(total_supply) = contract.total_supply().call().await {
        println!("ERC20 total supply is {total_supply:?}");
    }

    if let Ok(balance) = contract.balance_of(erc20_account_address).call().await {
        println!("ERC20 total supply is {balance:?}");
    }

    Ok(())
}

rust有很多优点,其中比较突出的就是宏编程,rust的宏要比其他语言的元编程要强大的多,它可以在编译期就完成宏的编译并生成对应的代码(虽然看不到),这样就能很快的得到代码提示,并且是类型安全的,比如这里的abigen宏生成了一个叫做IERC20的对象,这个对象可以通过new方法创建实例,而这个实例拥有ERC20所有接口对应的方法。

至于其他的代币如ERC721也是差不多的方法。

生成新钱包

ethers-rs支持多种钱包,比如Private key, Ledger, Trezor, YubiHSM2, AWS KMS等,而本文主要指讲第一种。

// use the eyre crate for easy idiomatic error handling
use eyre::Result;
// use the ethers_core rand for rng
use ethers::core::rand::thread_rng;
// use the ethers_signers crate to manage LocalWallet and Signer
use ethers::signers::{LocalWallet, Signer, Wallet};
use ethers::types::H256;

// Use the `tokio::main` macro for using async on the main function
#[tokio::main]
async fn main() -> Result<()> {
    let prikey = [254, 159, 17, 110, 10, 156, 237, 11, 156, 168, 117, 202, 17, 248, 112, 124, 221, 128, 127, 28, 175, 158, 45, 115, 141, 192, 28, 164, 208, 166, 104, 250];

    println!("钱包的私有地址: {:?}", H256::from_slice(&prikey).to_string());
    // 创建随机钱包
    let wallet = LocalWallet::new(&mut thread_rng());
    println!("钱包私钥: {:?}", wallet.signer().to_bytes());
    println!("钱包公钥: {:?}", wallet.address());

    // 从指定字节数组中创建
    let wallet = Wallet::from_bytes(&prikey).unwrap();
    println!("钱包私钥: {:?}", wallet.signer().to_bytes());
    println!("钱包公钥: {:?}", wallet.address());

    let prikey = hex::decode("0xfe9f116e0a9ced0b9ca875ca11f8707cdd807f1caf9e2d738dc01ca4d0a668fa").unwrap();
    let wallet = Wallet::from_bytes(&prikey).unwrap();
    println!("钱包私钥: {:?}", wallet.signer().to_bytes());
    println!("钱包公钥: {:?}", wallet.address());

    Ok(())
}

说到底,区块链的钱包只是一个256位32字节的数字,所以拥有这串数字就掌握了自己的钱包,但是这么长的0和1的二进制数字即使以16进制来保存也有64个字符,这些字符之间并没有什么关联,这对于人类的记忆来说是很不友好的,所以出现了助记词,这些助记词可以映射到自己的钱包(秘钥), 虽然有了一定的改善,但还是不友好,至少不符合多年的使用习惯,所以出现了各种钱包。

这些钱包有硬件的也有软件的,他们会通过一个密码来加密你的秘钥, 当你需要使用你的秘钥的时候,只需要使用一个自己设置的密码就能将秘钥从钱包中恢复出来,这样就比记助记词或者原秘钥要简单很多了。

秘钥库

类似钱包,ethers-rs没有提供一个类似的东西,可以使用钱包来代替。

地址验证

地址验证可以分为两个部分,账户地址是否合法以及账户地址属于那种类型。

验证账户地址

使用rust的一个痛点就是标准库比较精简,很多其他语言中的基础库在rust中需要使用第三方库,比如正则表达式的库,不过好在包管理器cargo的使用体验足够好才能减缓这种情况。

正则表达式的依赖如下:

验证的代码如下:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let re = regex::Regex::new(r"^0x[0-9a-fA-F]{40}$").expect("编译正则表达式失败.");

    // 结果为true
    println!("结果为{}\n", re.is_match("0x323b5d4c32345ced77393b3530b1eed0f346429d"));  
    // 结果为false
    println!("结果为{}\n", re.is_match("0xZYXb5d4c32345ced77393b3530b1eed0f346429d"));
    println!("结果为{}\n", re.is_match("youerning.top"));
    Ok(())
}

以太坊的账户地址与比特币不太一样,比特币有一定的校验规则,而以太坊没有,就像上面的代码那样,这样这个字符串只要是一个合法的256位16进制表示的字符串就是一个合法的账户地址。

验证地址是合约地址还是普通账户地址

以太坊中有两种类型的账户,合约账户和普通账户(又叫外部账户),判断两者的方法很简单,就是看其账户是否存储了代码,因为合约在创建的时候会在其账户对象上保存合约编译过后的代码。

use ethers::prelude::*;

const RPC_URL: &str = "https://cloudflare-eth.com";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = Provider::<Http>::try_from(RPC_URL)?;
	
    // 地址也能传ens
    let code = provider.get_code("0xEB1774bc66930a417A76Df89885CeE7c1A29f405", None).await.expect("查询失败");
    if code.len() > 1 {
        println!("该地址是一个合约");
    } else {
        println!("该地址不是一个合约");
    }
    
    Ok(())
}

小结

这一节主要专注于账户的创建和查询,通过ethers-rs我们可以读取多种钱包类型,也能自己创建本地钱包,有了钱包我们就能对交易签名,对消息签名,这样其他验证者才可以通过对签名和对应的账户地址验证来确定消息的合法性。

ethers-rs中钱包主要是对signer对象的包装,钱包会将这个signer对象与区块链的id绑定到一起(将消息跟chain id绑定可以避免双花)

参考链接