用Rust来做以太坊开发3之交易

文章目录

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

这次复刻《用Go来做以太坊开发》的第三章"交易"。

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

  • 查询区块
  • 查询交易
  • ETH转账
  • 代币转账
  • 监听新区块
  • 创建裸交易
  • 发送裸交易

往期文章:

本文用到的依赖如下:

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"

本文大部分代码改自https://github.com/gakonst/ethers-rs/blob/master/examples/contracts/examples, 下面就不一一列举来源了。

查询区块

ethers-rs并没有提供获取区块头的方法,所以直接获取对应的区块即可。

// author: https://youerning.top
use ethers::prelude::*;

const RPC_URL: &str = "http://127.0.0.1:8545";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = Provider::<Http>::try_from(RPC_URL)?;
    
    let block = provider.get_block(1).await?.unwrap();
    
    println!("{:?}", block.number);   
    println!("{:?}", block.time().unwrap());     
    println!("{:?}", block.timestamp);     
    println!("{:?}", block.difficulty);
    println!("{:?}", block.hash.unwrap());        
    println!("{:?}", block.transactions.len()); 
    Ok(())
}

查询交易

go-ethereum不同的是,ethers-rs的交易对象不需要自己构造签名对象用于解码数据以获得from字段。

// author: https://youerning.top
use ethers::prelude::*;

const RPC_URL: &str = "http://127.0.0.1:8545";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let provider = Provider::<Http>::try_from(RPC_URL)?;
    
    let block = provider.get_block_with_txs(1).await?.unwrap();
    for tx in block.transactions {
        println!("{:?}", tx.hash);       
        println!("{:?}", tx.value);   
        println!("{:?}", tx.gas);              
        println!("{:?}", tx.gas_price.unwrap());
        println!("{:?}", tx.nonce);            
        println!("{:?}", tx.input);
        // to可能为None, 因为创建合约的交易没有to字段
        println!("{:?}", tx.to.unwrap());
        println!("{:?}", tx.from);

        let recipt = provider.get_transaction_receipt(tx.hash).await?.unwrap();
        println!("{:?}", recipt.status.unwrap());
    }


    Ok(())
}

ETH转账

ethers-rs的转账比go-ethereum的稍稍简单一些,这主要是如果不发送原始交易的话,provider对象会设置交易对象的一些必要的字段,比如交易费,交易价格等。

// author: https://youerning.top
use ethers::prelude::*;
use eyre::Result;
use ethers::signers::{Signer, Wallet};
use ethers::utils::{parse_units, ParseUnits};

const RPC_URL: &str = "http://127.0.0.1:8545";


#[tokio::main]
async fn main() -> Result<()> {
    // 通过私钥创建一个钱包,并用它的公钥作为接受地址
    let prikey = hex::decode("0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e").unwrap();
    let wallet = Wallet::from_bytes(&prikey).unwrap();
    println!("钱包私钥: {:?}", wallet.signer().to_bytes());
    println!("钱包公钥: {:?}", wallet.address());

    // 连接节点
    let provider = Provider::<Http>::try_from(RPC_URL)?;
    let accounts = provider.get_accounts().await?;
    println!("节点账户: {:?}", accounts);
	
    // 设置from,to两个交易字段
    let from = accounts[0];
    let to = wallet.address();

    // 通过单位来构造交易的数值, 而不需要手动打18个0
    let pu: ParseUnits = parse_units("1.0", "ether").unwrap();
    let value = U256::from(pu);
    let tx = TransactionRequest::new().to(to).value(value).from(from);

    // 通过eth_sendTransaction接口发送(或者说广播)交易
    let balance_before = provider.get_balance(from, None).await?;
    let tx = provider.send_transaction(tx, None).await?.await?;
    println!("{}", serde_json::to_string(&tx)?);
    
    // 查看交易前后的余额变化
    // 值得注意的是, 交易需要付出手续费,所以不仅仅是减去 1 ether
    let balance_after = provider.get_balance(from, None).await?;
    assert!(balance_after < balance_before);
    println!("Balance before {balance_before}");
    println!("Balance after {balance_after}");
    Ok(())
}

代币转账

ethers-rs的代币转账比起go-ethereum可简单太多了,因为有强大的宏编程支持,可以直接在编译的时候生成代码,这样在写代码的时候就可以获得代码提示,作为一个面向代码提示编程的我来说是在是太棒了。

// author: https://youerning.top
use ethers::{
    contract::abigen,
    middleware::SignerMiddleware,
    providers::{Http, Provider, Middleware},
    signers::{Signer, Wallet},
    types::{Address, U256}
};
use eyre::Result;
use std::{convert::TryFrom, sync::Arc};


const RPC_URL: &str = "http://127.0.0.1:8545";


#[tokio::main]
async fn main() -> Result<()> {
    // 构造本地钱包,用于创建signer对象
    let prikey = hex::decode("0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e").unwrap();
    let wallet = Wallet::from_bytes(&prikey).unwrap();
    println!("钱包公钥: {:?}", wallet.address());
    let from_address = wallet.address();

    // 创建客户端
    let provider = Provider::<Http>::try_from(RPC_URL)?;
    let to_address: Address = "0xdD2FD4581271e230360230F9337D5c0430Bf44C0".parse().unwrap();    

    // ERC20合约地址
    const ERC20_CONTRACT_ADDRESS: &str = "0xEB1774bc66930a417A76Df89885CeE7c1A29f405";
    let token_address: Address = ERC20_CONTRACT_ADDRESS.parse()?;
    // 生成合约对象
    abigen!(
        ERC20Contract,
        r#"[
            function balanceOf(address account) external view returns (uint256)
            function decimals() external view returns (uint8)
            function symbol() external view returns (string memory)
            function transfer(address to, uint256 amount) external returns (bool)
            event Transfer(address indexed from, address indexed to, uint256 value)
        ]"#,
    );
    // chain_id在签名验证的时候很重要!!!
    let chain_id = provider.get_chainid().await?.as_u64();
    // contract对象实例化的时候需要一个ARC对象
    let signer =
        Arc::new(SignerMiddleware::new(provider, wallet.with_chain_id(chain_id)));
    let contract = ERC20Contract::new(token_address, signer);

    // 将转账单位设置成 whole_amount * (10^decimals)
    let whole_amount: u64 = 1;
    let decimals = contract.decimals().call().await?;
    let decimal_amount = U256::from(whole_amount) * U256::exp10(decimals as usize);

    // 调用合约transfer接口
    println!("从账户[{:?}]转账到账户[{:?}]: {:?}", from_address, to_address, decimal_amount);
    let tx = contract.transfer(to_address, decimal_amount);
    // 等待交易完成
    let pending_tx = tx.send().await?;
    let _mined_tx = pending_tx.await?;

    // 获取余额
    let balance = contract.balance_of(to_address).call().await?;
    println!("账户[{:?}]当前余额: {:?}", to_address, balance);
    Ok(())
}

监听新区块

下面的代码比较简单,没啥好讲的。

// author: https://youerning.top
use ethers::providers::{Middleware, Provider, StreamExt, Ws};
use eyre::Result;

const WEBSOCKET_RPC_URL: &str = "ws://127.0.0.1:8546";

#[tokio::main]
async fn main() -> Result<()> {
    let provider =
        Provider::<Ws>::connect(WEBSOCKET_RPC_URL)
            .await?;

    // take代表最多获取一个监听数据
    let mut stream = provider.subscribe_blocks().await?.take(1);
    println!("开始监听,仅监听最多一个区块事件");
    while let Some(block) = stream.next().await {
        println!(
            "在时间点{:?}, 创建了新的区块号[{}] -> 对应的hash:{:?}",
            block.timestamp,
            block.number.unwrap(),
            block.hash.unwrap()
        );
    }
    println!("监听完毕.");

    // 一直监听
    println!("开始监听,一直监听,直到程序被关闭");
    let mut stream = provider.subscribe_blocks().await?;
    while let Some(block) = stream.next().await {
        println!(
            "在时间点: {:?}, 创建了新的区块号[{}] -> 对应的hash:{:?}",
            block.timestamp,
            block.number.unwrap(),
            block.hash.unwrap()
        );
    }
    println!("监听完毕.");
    Ok(())
}

创建/发送裸交易

发送裸交易的目的是为了自定义签名,因此没必要为了自签名而自签名,我们可以借助ethers-rs提供的SignerMiddleware对象就可以将钱包(或者说私钥, 或者说signer)对象包装起来,然后只需简单的设置交易请求即可,与直接调用provder对象不同,我们需要自己处理交易费和交易价格。

use ethers::prelude::*;
use ethers::middleware::{SignerMiddleware};
use eyre::Result;
use ethers::signers::{Signer, Wallet};
use ethers::utils::{parse_units, ParseUnits};

const RPC_URL: &str = "http://127.0.0.1:8545";


#[tokio::main]
async fn main() -> Result<()> {
    // 自定义钱包
    let prikey = hex::decode("0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e").unwrap();
    let wallet = Wallet::from_bytes(&prikey).unwrap();
    println!("钱包私钥: {:?}", wallet.signer().to_bytes());
    println!("钱包公钥: {:?}", wallet.address());

    // 设置收发地址
    let from_address = wallet.address();
    let to_address: Address = "0xdD2FD4581271e230360230F9337D5c0430Bf44C0".parse().unwrap();
    
    // 创建交易金额
    let pu: ParseUnits = parse_units("1.0", "ether").unwrap();
    let value = U256::from(pu);
    
    // 构造交易请求    
    let provider = Provider::<Http>::try_from(RPC_URL)?;
    let chain_id = provider.get_chainid().await.unwrap();
    let tx = TransactionRequest::new().to(to_address).value(value).from(from_address).chain_id(chain_id.as_u64());
    // 特别要注意chain_id是否正确!!!
    let wallet =  wallet.with_chain_id(chain_id.as_u64());

    let gas_price = provider.get_gas_price().await?;
    let gas = provider.estimate_gas(&tx.clone().into(), None).await?;
    let tx = tx.gas(gas).gas_price(gas_price);

    // 构造SignerMiddleware, 是provider进一步包装
    let new_provider = SignerMiddleware::new(provider, wallet);
    let nonce1 = new_provider.get_transaction_count(from_address, None).await?;
    let balance_before = new_provider.get_balance(from_address, None).await?;

    let tx = new_provider.send_transaction(tx, None).await.unwrap().await.unwrap();
    
    let nonce2 = new_provider.get_transaction_count(from_address, None).await?;
    assert!(nonce1 < nonce2);
    println!("tx: {}", serde_json::to_string(&tx)?);

    let balance_after = new_provider.get_balance(from_address, None).await?;
    // assert!(balance_after < balance_before);

    println!("Balance before {balance_before}");
    println!("Balance after {balance_after}");
    Ok(())
}

通过keystore文件获取私钥

本文的代码测试都是使用的geth的开发模式,有时候可能需要使用私钥,那么可以借助这个网站还原私钥。

https://lab.miguelmota.com/ethereum-keystore/example/

开发模式的keystore文件的密码是空值,所以不需要填。

小结

这一部分的内容使用ethers-rs还是很简单的,因为不需要额外的编译solidity源码来生成对应的接口代码,这是ethers-rs强大之处,也是rust的强大之处。

通过这三章我们应该能够完成大多数的以太坊的交互了,除了

参考链接