用Rust来做以太坊开发4之智能合约

文章目录

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

这次复刻《用Go来做以太坊开发》的第四章智能合约

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

  • 智能合约的编译与ABI

  • 部署智能合约

  • 加载智能合约

  • 查询智能合约

  • 写入智能合约

  • 读取智能合约的字节码

  • 查询ERC20代币智能合约

往期文章:

本节会用到的合约代码

// 文件名:simple_contract.sol
// https://youerning.top/post/ethers-rs/totorial4
pragma solidity >=0.4.24;

contract SimpleStorage {

    event ValueChanged(address indexed author, string oldValue, string newValue);

    string _value;

    constructor(string memory value) public {
        emit ValueChanged(msg.sender, _value, value);
        _value = value;
    }

    function getValue() view public returns (string memory) {
        return _value;
    }

    function setValue(string memory value) public {
        emit ValueChanged(msg.sender, _value, value);
        _value = value;
    }
}

智能合约的编译与ABI

ethers-rs编译solidity代码也是需要编译器的,所以会在可执行路径中找到solc执行文件(windows下是solc.exe),如果想指定solc执行文件的路径可以设置环境变量SOLC_PATH来指定。

在编译这段代码的时候可能会报错: "Invalid EVM version requested", 这是因为ethers-rs在调用solc可执行文件的时候会传一个EVM的版本, ethers-rs 2.0.10版本默认使用的Shanghai, 我测试的时候是solc 0.8.19不行, 0.8.23可以, 具体solidityevm的版本关联关系我并没有去查询,所以建议开发的时候两者都用最新的就行。

solidity下载地址: https://github.com/ethereum/solidity/releases

use ethers::{prelude::Abigen, solc::Solc};
use eyre::Result;

fn main() -> Result<()> {
    let mut args = std::env::args();
    args.next().unwrap(); // skip program name

    let contract_name = "SimpleStorage";
    let contract: String = args
        .next()
        .unwrap_or_else(|| "simple_contract.sol".to_owned());

    println!("生成合约{contract}的绑定对象\n");

    // ethers-rs的一些默认设定不太好改,比如evm的版本设置,如果需要设置的话,会麻烦很多
    let abi = if contract.ends_with(".sol") {
        let contracts = Solc::default().compile_source(&contract)?;
        let abi = contracts.get(&contract, &contract_name).unwrap().abi.unwrap();
        serde_json::to_string(abi).unwrap()
    } else {
        contract
    };


    let bindings = Abigen::new(&contract_name, abi)?.generate()?;
    // 如果第二个参数指定输出文件就输出到文件中.
    if let Some(output_path) = args.next() {
        bindings.write_to_file(output_path)?;
    } else {
        bindings.write(&mut std::io::stdout())?;
    }

    Ok(())
}

输出是一个生成的rust版本的对象,通过它可以与以太坊节点交互,输出如下:

pub use simple_storage::*;
/// This module was auto-generated with ethers-rs Abigen.
/// More information at: <https://github.com/gakonst/ethers-rs>
#[allow(
    clippy::enum_variant_names,
    clippy::too_many_arguments,
    clippy::upper_case_acronyms,
    clippy::type_complexity,
    dead_code,
    non_camel_case_types,
)]
pub mod simple_storage {
// 省略其他代码
}

这个文件可以写到一个rs文件中,然后在其他代码中引用,这种方式感觉并不是那么友好,所以相较于这种情况,我感觉生成ABI文件可能更适合。

可以使用以下命令生成对应的ABI文件。

solc --abi simple_contract.sol -o .

它会在当前工作目录生成一个和合约名一样的SimpleStorage.abi,虽然是以ABI结尾,但是,就是JSON格式。

部署合约

合约部署和编译合约一样,也是需要对应的solidity编译器的,除此之外,对以太坊节点也是有要求的,因为ethers-rs 2.0.10默认生成的EVM字节码版本是Shanghai的EVM版本,所以太旧的以太坊节点可能不行,我测试的geth的节点版本是v1.13.1

use ethers::{
    contract::{abigen, ContractFactory},
    middleware::SignerMiddleware,
    providers::{Http, Provider, Middleware},
    signers::{Wallet, Signer},
    solc::Solc,
};
use eyre::Result;
use std::{convert::TryFrom, path::Path, sync::Arc};

// 生成类型安全的合约对象
abigen!(
    SimpleContract,
    "simple_contract.json",
    event_derives(serde::Deserialize, serde::Serialize)
);

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 source = Path::new(&env!("CARGO_MANIFEST_DIR")).join("simple_contract.sol");
    let compiled = Solc::default().compile_source(source).expect("Could not compile contracts");
    // 获得编译之后的对象,因为solidity里面可以定义接口继承,所以需要使用find方法指定对应的合约名
    let (abi, bytecode, _runtime_bytecode) =
        compiled.find("SimpleStorage").expect("could not find contract").into_parts_or_default();


    // 3. 连接网络
    let provider = Provider::<Http>::try_from(RPC_URL)?;

    // 4. 因为部署合约需要写入数据到链上,所以需要使用钱包对象
    let chain_id = provider.get_chainid().await?.as_u64();
    let client = SignerMiddleware::new(provider, wallet.with_chain_id(chain_id));
    let client = Arc::new(client);

    // 5. 创建一个工厂对象,用于后续部署实例
    let factory = ContractFactory::new(abi, bytecode, client.clone());

    // 6. 部署的时候需要必要的初始值
    let contract = factory.deploy("initial value".to_string())?.send().await?;

    // 7. 获取合约部署后的地址
    let addr = contract.address();
    println!("contract is deployed at: {addr:?}");

    // 8. 创建与合约交互的合约对象
    let contract = SimpleContract::new(addr, client.clone());

    // 9. 
    // 第一个await是等待交易返回PendingTransaction,第二个await是等待此交易被提交
    let _receipt = contract.set_value("hi".to_owned()).send().await?.await?;

    // 10. 获取所有事件
    let logs = contract.value_changed_filter().query().await?;

    // 11. 调用get_value方法
    let value = contract.get_value().call().await?;
    println!("Value: {value}. Logs: {}", serde_json::to_string(&logs)?);

    Ok(())
}

值得注意的是: rust和solidity的命名习惯不一样,rust习惯蛇形变量名,比如get_value,而solidity习惯驼峰命名,比如getValue, 为了符合rust的是用习惯,abigen生成的方法名自动将命名方式改变了,比如getValue变成了get_value

加载/查询智能合约

如果我们需要跟以太坊节点交互,那么就需要对应的ABI, 所以可以使用之前编译的ABI文件 SimpleStorage.abi

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

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)?;

    abigen!(
        SimpleContract,
        "SimpleStorage.abi",
    );
    
    const CONTRACT_ADDRESS: &str = "0x73511669fd4de447fed18bb79bafeac93ab7f31f";

    let contract_address: Address = CONTRACT_ADDRESS.parse()?;
    let client = Arc::new(provider);
    let contract = SimpleContract::new(contract_address, client);

    println!("合约设置的值: {:?}", contract.get_value().call().await?);

    Ok(())
}

写入智能合约

写入和查询的区别在于前者不要钱后者需要花钱。

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

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

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    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)?;

    abigen!(
        SimpleContract,
        "SimpleStorage.abi",
    );

    const CONTRACT_ADDRESS: &str = "0x73511669fd4de447fed18bb79bafeac93ab7f31f";

    let contract_address: Address = CONTRACT_ADDRESS.parse()?;
    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 = SimpleContract::new(contract_address, signer);

    let _receipt = contract.set_value("new value".to_owned()).send().await?.await?;

    println!("合约新设置的值: {:?}", contract.get_value().call().await?);

    Ok(())
}

读取智能合约的字节码

字节码是代码编译后的数据,需要专门的工具才能肉眼调试。

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 code = provider.get_code("0x73511669fd4de447fed18bb79bafeac93ab7f31f", None).await?;
    println!("合约的合约代码: {:?}", code);
    Ok(())
}

查询ERC20代币智能合约

和之前的代码差不多,这里直接使用以前文章的代码了。

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

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 balance = provider.get_balance("0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199", None).await?;
    println!("balance: {} ether", utils::format_ether(balance));
    println!("balance: {balance:?} wei");

    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(())
}

如果需要读取symbol, decimals之类的变量,需要使用ABI文件。

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

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 balance = provider.get_balance("0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199", None).await?;
    println!("balance: {} ether", utils::format_ether(balance));
    println!("balance: {balance:?} wei");

    abigen!(
        IERC20,
        "IERC20.json",
    );
    
    const ERC20_CONTRACT_ADDRESS: &str = "0x92B4FbB1Be8B98b00D643E485075186c3c61bae1";

    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:?}");
    }

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

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

    Ok(())
}

IERC20.json这个文件大家可以去https://github.com/gakonst/ethers-rs/blob/master/examples/contracts/examples/abi/IERC20.json 下载。

小结

链外开发可能最常编写的代码就是与合约交互的代码了,与合约交互的代码其实本质上都是差不多的,唯一的区别在于不同的合约暴露的方法和事件不一样,所以需要知道对应合约的ABI用于交互。

参考链接