用Rust来做以太坊开发2之账户

文章目录

本系列文章主要是用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的使用体验足够好才能减缓这种情况。

正则表达式的依赖如下:

regex = "1.10.2"

验证的代码如下:

#[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绑定可以避免双花)

参考链接