Skip to content

Ethers.rs Rust库

概览

Ethers.rs库提供一套工具,通过Rust编程语言与以太坊节点交互,其运作方式与Ethers.js相似。Moonbeam拥有类似以太坊的API,能够与以太坊式的JSON-RPC调用完全兼容。因此,开发者可以利用此兼容性并使用Ethers.rs库如同与以太坊一样与Moonbeam节点交互。您可以在其官方文档获取更多关于如何使用Ethers.rs的信息。

在本教程中,您将学习如何使用Ethers.rs库在Moonbase Alpha上发送交易和部署合约。本教程也同样适用于 MoonbeamMoonriverMoonbeam开发节点

查看先决条件

在本教程的示例中,您将需要准备以下内容:

  • 拥有资金的账户。 您可以每24小时一次从Moonbase Alpha水龙头上获取DEV代币以在Moonbase Alpha上进行测试
  • 要在Moonbeam或Moonriver网络上测试本指南中的示例,您可以从受支持的网络端点提供商之一获取您自己的端点和API密钥
  • 在设备上安装Rust
  • 在设备上安装solc。Ethers.rs包的建议使用solc-select

注意事项

本教程的示例中假设您拥有基于MacOS或Ubuntu 20.04的环境,且需要针对Windows系统进行相应调整。

创建一个Rust项目

首先,您可以使用Cargo工具创建一个新的Rust项目:

cargo init ethers-examples && cd ethers-examples

在本教程中,您将需要安装Ethers.rs库等。要在Rust项目中安装,您必须编辑文档中包含的Cargo.toml文件并将其包含在依赖项中:

[package]
name = "ethers-examples"
version = "0.1.0"
edition = "2021"

[dependencies]
ethers = "1.0.2"
ethers-solc = "1.0.2"
tokio = { version = "1", features = ["full"] }
serde_json = "1.0.89"
serde = "1.0.149"

本示例使用ethersethers-solc crate版本1.0.2用于RPC交互和Solidity编译。这也包含了tokio crate以运行异步Rust环境,因为与RPC交互需要异步代码。最后,这也包含了serde_jsonserde crates来帮助序列化/反序列化此示例的代码。

如果这是您第一次使用solc-select,您将需要使用以下命令来安装和配置Solidity版本:

solc-select install 0.8.17 && solc-select use 0.8.17

设置Ethers提供商和客户端

在整个教程中,您将编写多个函数,用于提供不同的功能,例如发送交易、部署合约,以及与部署的合约交互。在大部分这些脚本中,您将需要使用Ethers providerEthers signer client与网络进行交互。

要为Moonbeam或Moonriver网络配置您的项目,您可以从受支持的网络端点提供商之一获取您自己的端点和API密钥。

创建提供商和签署者有多种方式,但是最简单的方式是通过try_from操作:

  1. ethers crate导入ProviderHttp
  2. 为了方便操作,添加Client类型,当您开始创建发送交易和部署合约的函数时,将会使用此函数
  3. async fn main()上方添加tokio属性,用于异步执行
  4. 使用try_from尝试从RPC端点实例化JSON-RPC提供商对象
  5. 使用私钥创建钱包对象(私钥将用于签署交易)。请注意:此示例仅用于演示目的,请勿将您的私钥存储于普通的Rust文件中
  6. 通过将提供商和钱包提供到SignerMiddleware对象中,将其包装到客户端中
// 1. Import ethers crate
use ethers::providers::{Provider, Http};

// 2. Add client type
type Client = SignerMiddleware<Provider<Http>, Wallet<k256::ecdsa::SigningKey>>;

// 3. Add annotation
#[tokio::main]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 4. Use try_from with RPC endpoint
    let provider = Provider::<Http>::try_from(
        "INSERT_RPC_API_ENDPOINT"
    )?;
    // 5. Use a private key to create a wallet
    // Do not include the private key in plain text in any production code
    // This is just for demonstration purposes
    // Do not include '0x' at the start of the private key
    let wallet: LocalWallet = "INSERT_YOUR_PRIVATE_KEY"
        .parse::<LocalWallet>()?
        .with_chain_id(Chain::Moonbeam);

    // 6. Wrap the provider and wallet together to create a signer client
    let client = SignerMiddleware::new(provider.clone(), wallet.clone());
    Ok(())
}
// 1. Import ethers crate
use ethers::providers::{Provider, Http};

// 2. Add client type
type Client = SignerMiddleware<Provider<Http>, Wallet<k256::ecdsa::SigningKey>>;

// 3. Add annotation
#[tokio::main]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 4. Use try_from with RPC endpoint
    let provider = Provider::<Http>::try_from(
        "INSERT_RPC_API_ENDPOINT"
    )?;
    // 5. Use a private key to create a wallet
    // Do not include the private key in plain text in any production code
    // This is just for demonstration purposes
    // Do not include '0x' at the start of the private key
    let wallet: LocalWallet = "INSERT_YOUR_PRIVATE_KEY"
        .parse::<LocalWallet>()?
        .with_chain_id(Chain::Moonriver);

    // 6. Wrap the provider and wallet together to create a signer client
    let client = SignerMiddleware::new(provider.clone(), wallet.clone());
    Ok(())
}
// 1. Import ethers crate
use ethers::providers::{Provider, Http};

// 2. Add client type
type Client = SignerMiddleware<Provider<Http>, Wallet<k256::ecdsa::SigningKey>>;

// 3. Add annotation
#[tokio::main]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 4. Use try_from with RPC endpoint
    let provider = Provider::<Http>::try_from(
        "https://rpc.api.moonbase.moonbeam.network"
    )?;
    // 5. Use a private key to create a wallet
    // Do not include the private key in plain text in any production code
    // This is just for demonstration purposes
    // Do not include '0x' at the start of the private key
    let wallet: LocalWallet = "INSERT_YOUR_PRIVATE_KEY"
        .parse::<LocalWallet>()?
        .with_chain_id(Chain::Moonbase);

    // 6. Wrap the provider and wallet together to create a signer client
    let client = SignerMiddleware::new(provider.clone(), wallet.clone());
    Ok(())
}
// 1. Import ethers crate
use ethers::providers::{Provider, Http};

// 2. Add client type
type Client = SignerMiddleware<Provider<Http>, Wallet<k256::ecdsa::SigningKey>>;

// 3. Add annotation
#[tokio::main]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 4. Use try_from with RPC endpoint
    let provider = Provider::<Http>::try_from(
        "http://127.0.0.1:9944"
    )?;
    // 5. Use a private key to create a wallet
    // Do not include the private key in plain text in any production code
    // This is just for demonstration purposes
    // Do not include '0x' at the start of the private key
    let wallet: LocalWallet = "INSERT_YOUR_PRIVATE_KEY"
        .parse::<LocalWallet>()?
        .with_chain_id(Chain::MoonbeamDev);

    // 6. Wrap the provider and wallet together to create a signer client
    let client = SignerMiddleware::new(provider.clone(), wallet.clone());
    Ok(())
}

发送交易

在这一部分中,您将创建几个函数,这将包含在同一个main.rs文件中,以避免从实现模块带来的额外复杂性。第一个函数将在尝试发送交易前检查账户余额。第二个函数将实际发送交易。要运行这些函数,您需要编辑main函数并运行main.rs脚本。

您应该根据上述部分所描述的方法在main.rs中设置了提供商和客户端。要发送交易,您将需要添加几行代码:

  1. 在您的导入中添加use ethers::{utils, prelude::*};,这将为您提供访问实用程序函数的权限,并且prelude导入所有必要的数据类型和特征
  2. 因为您将从一个地址发送交易至另一个地址,您可以在main函数中指定发送和接收地址。请注意:address_from值应该对应于main函数中所使用的私钥
// ...
// 1. Add to imports
use ethers::{utils, prelude::*};

// ...

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ...

    // 2. Add from and to address
    let address_from = "YOUR FROM ADDRESS".parse::<Address>()?
    let address_to = "YOUR TO ADDRESS".parse::<Address>()?
}

查看余额函数

接下来,您将通过执行以下步骤创建函数以获取发送和接收账户的余额:

  1. 创建一个名为print_balances的新异步函数,这将提供商对象的引用以及发送和接收地址作为输入
  2. 使用provider对象的get_balance函数以获取交易发送和接收地址的余额
  3. 输出发送和接收地址的结果余额
  4. main函数中调用print_balances函数
// ...

// 1. Create an asynchronous function that takes a provider reference and from and to address as input
async fn print_balances(provider: &Provider<Http>, address_from: Address, address_to: Address) -> Result<(), Box<dyn std::error::Error>> {
    // 2. Use the get_balance function
    let balance_from = provider.get_balance(address_from, None).await?;
    let balance_to = provider.get_balance(address_to, None).await?;

    // 3. Print the resultant balance
    println!("{} has {}", address_from, balance_from);
    println!("{} has {}", address_to, balance_to);

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ...

    // 4. Call print_balances function in main
    print_balances(&provider).await?;

    Ok(())
}

发送交易脚本

在本示例中,您将从源地址(即您持有私钥的地址)发送1 DEV至另一个地址。

  1. 创建一个名为send_transaction的新异步函数,这将客户端对象的引用以及发送和接收地址作为输入
  2. 创建交易对象,并包含tovaluefrom。当编写value输入时,使用ethers::utils::parse_ether函数
  3. 使用client对象来发送交易
  4. 交易确认后输出交易
  5. main函数中调用send_transaction函数
// ...

// 1. Define an asynchronous function that takes a client provider and the from and to addresses as input
async fn send_transaction(client: &Client, address_from: Address, address_to: Address) -> Result<(), Box<dyn std::error::Error>> {
    println!(
        "Beginning transfer of 1 native currency from {} to {}.",
        address_from, address_to
    );

    // 2. Create a TransactionRequest object
    let tx = TransactionRequest::new()
        .to(address_to)
        .value(U256::from(utils::parse_ether(1)?))
        .from(address_from);

    // 3. Send the transaction with the client
    let tx = client.send_transaction(tx, None).await?.await?;

    // 4. Print out the result
    println!("Transaction Receipt: {}", serde_json::to_string(&tx)?);

    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ...

    // 5. Call send_transaction function in main
    send_transaction(&client, address_from, address_to).await?;

    Ok(())
}

您可以在GitHub上查看完整的脚本

要运行发送交易并在交易发送后检查余额的脚本,您可以运行以下命令:

cargo run

如果交易成功后,您将在终端看到交易详情以及地址余额。

Terminal logs from sending a transaction

部署合约

在下几个部分中您将要编译和部署的合约是一个简单的增量合约,命名为Incrementer.sol。您可以先为合约创建一个文件:

touch Incrementer.sol

接下来,您可以添加Solidity代码至文件:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Incrementer {
    uint256 public number;

    constructor(uint256 _initialNumber) {
        number = _initialNumber;
    }

    function increment(uint256 _value) public {
        number = number + _value;
    }

    function reset() public {
        number = 0;
    }
}

constructor函数将在合约部署时运行,设置存储在链上的数字变量的初始值(默认值为0)。increment函数将提供的_value添加至当前数字,但是需要发送一个交易以修改存储的数据。最后,reset函数将存储的数值重置为零。

注意事项

此合约为简单示例,仅供演示使用,其数值无实际意义。

在这一部分中,您将创建几个函数,这将包含在main.rs文件中,以避免从实现模块带来的额外复杂性。第一个函数将编译和部署合约。剩下的函数将用于与部署的合约交互。

您应该根据设置Ethers提供商和客户端部分所描述的方法在main.rs中设置了提供商和客户端。

在开始部署合约之前,您将需要添加一些导入至main.rs文件中:

use ethers_solc::Solc;
use ethers::{prelude::*};
use std::{path::Path, sync::Arc};

ethers_solc导入将用于编译智能合约。Ethers的prelude导入一些必要数据类型和特征。最后,std导入将使您能够存储智能合约并将客户端包装成Arc类型以实现线程安全。

编译和部署合约脚本

此示例函数将编译和部署您在上述部分中创建的Incrementer.sol智能合约。Incrementer.sol智能合约需要在根目录中。在main.rs文件中,请执行以下步骤:

  1. 创建名为compile_deploy_contract的新异步函数,这将客户端对象的引用作为输入,并返回H160格式的地址
  2. 定义名为source的变量作为托管所有需要编译的智能合约的目录路径,该目录为根目录
  3. 在根目录中使用Solc crate编译所有的智能合约
  4. 从编译的结果中获取ABI和字节码,搜索Incrementer.sol合约
  5. 使用ABI、字节码和客户端从智能合约创建一个合约工厂。客户端必须包装成Arc类型以实现线程安全
  6. 使用工厂部署。在本示例中,在构造函数处以5作为初始值
  7. 部署后输出地址
  8. 返回地址
  9. main函数中调用compile_deploy_contract函数
// ...

// 1. Define an asynchronous function that takes a client provider as input and returns H160
async fn compile_deploy_contract(client: &Client) -> Result<H160, Box<dyn std::error::Error>> {
    // 2. Define a path as the directory that hosts the smart contracts in the project
    let source = Path::new(&env!("CARGO_MANIFEST_DIR"));

    // 3. Compile all of the smart contracts
    let compiled = Solc::default()
        .compile_source(source)
        .expect("Could not compile contracts");

    // 4. Get ABI & Bytecode for Incrementer.sol
    let (abi, bytecode, _runtime_bytecode) = compiled
        .find("Incrementer")
        .expect("could not find contract")
        .into_parts_or_default();

    // 5. Create a contract factory which will be used to deploy instances of the contract
    let factory = ContractFactory::new(abi, bytecode, Arc::new(client.clone()));

    // 6. Deploy
    let contract = factory.deploy(U256::from(5))?.send().await?;

    // 7. Print out the address
    let addr = contract.address();
    println!("Incrementer.sol has been deployed to {:?}", addr);

    // 8. Return the address
    Ok(addr)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ...

    // 9. Call compile_deploy_contract function in main
    let addr = compile_deploy_contract(&client).await?;

    Ok(())
}

读取合约数据(调用函数)

调用函数是一种不修改合约存储(更改变量)的交互类型,这意味着无需发送交易。他们只读取已部署合约的各类存储变量。

Rust是typesafe,这就是为什么需要Incrementer.sol合约的ABI来生成typesafe Rust结构。在本示例中,您应该在名为Incrementer_ABI.json的Cargo项目的根目录中创建一个新文件:

touch Incrementer_ABI.json

Incrementer.sol的ABI如下所示,复制并将其粘贴至Incrementer_ABI.json文件中:

[
    {
        "inputs": [
            {
                "internalType": "uint256",
                "name": "_value",
                "type": "uint256"
            }
        ],
        "name": "increment",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "number",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "reset",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }
]

然后,请执行以下步骤创建一个可以读取并返回Incrementer.sol合约number函数的函数:

  1. 使用abigen macro为Incrementer智能合约生成一个type-safe接口
  2. 创建一个名为read_number的新异步函数,这将客户端对象的引用和合约地址引用作为输入,并返回U256
  3. 使用客户端和合约地址值创建一个由abigen macro生成的Incrementer对象的新实例
  4. 在新的Incrementer对象中调用number函数
  5. 输出结果值
  6. 返回结果值
  7. main函数中调用read_number函数
// ...

// 1. Generate a type-safe interface for the Incrementer smart contract
abigen!(
    Incrementer,
    "./Incrementer_ABI.json",
    event_derives(serde::Deserialize, serde::Serialize)
);

// 2. Define an asynchronous function that takes a client provider and address as input and returns a U256
async fn read_number(client: &Client, contract_addr: &H160) -> Result<U256, Box<dyn std::error::Error>> {
    // 3. Create contract instance
    let contract = Incrementer::new(contract_addr.clone(), Arc::new(client.clone()));

    // 4. Call contract's number function
    let value = contract.number().call().await?;

    // 5. Print out number
    println!("Incrementer's number is {}", value);

    // 6. Return the number
    Ok(value)
}

// ...

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ...

    // 7. Call read_number function in main
    read_number(&client, &addr).await?;

    Ok(())
}

您可以在GitHub中查看完整的脚本

要运行部署合约和返回存储在Incrementer合约中的当前值的脚本,您可以在终端中输入以下命令:

cargo run

如果成功,您将在终端中看到已部署合约的地址和初始值(应为5

Terminal logs from deploy the contract

与合约交互(发送函数)

发送函数是一种修改合约存储(更改变量)的交互类型,这意味着需要签署和发送交易。在这一部分中,您将创建两个函数:一个为increment,另一个为重置incrementer。此部分还将需要在从智能合约读取时初始化Incrementer_ABI.json文件。

执行以下步骤创建函数以递增:

  1. 确保在main.rs文件中为Incrementer_ABI.json调用了abigen macro(如果它已存在于main.rs文件中,则无需再有第二个)
  2. 创建一个名为increment_number的新异步函数,这将客户端对象的引用和地址作为输入
  3. 使用客户端和合约地址值创建一个由abigen macro生成的Incrementer对象的新实例
  4. 通过将U256对象作为输入值包含在新的Incrementer对象中调用increment函数。在本示例中,此数值为5
  5. main函数调用read_number函数
// ...

// 1. Generate a type-safe interface for the Incrementer smart contract
abigen!(
    Incrementer,
    "./Incrementer_ABI.json",
    event_derives(serde::Deserialize, serde::Serialize)
);

// 2. Define an asynchronous function that takes a client provider and address as input
async fn increment_number(client: &Client, contract_addr: &H160) -> Result<(), Box<dyn std::error::Error>> {
    println!("Incrementing number...");

    // 3. Create contract instance
    let contract = Incrementer::new(contract_addr.clone(), Arc::new(client.clone()));

    // 4. Send contract transaction
    let tx = contract.increment(U256::from(5)).send().await?.await?;
    println!("Transaction Receipt: {}", serde_json::to_string(&tx)?);

    Ok(())
}

// ...

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ...

    // 5. Call increment_number function in main
    increment_number(&client, &addr).await?;

    Ok(())
}

您可以在GitHub上查看完整的脚本

要运行脚本,您可以在终端输入以下命令:

cargo run

如果成功,交易收据将会显示在终端显示。您可以在main函数中使用read_number函数,以确保数值按预期变化。如果您在递增后使用read_number函数,您也会看到递增的数字,该数值应为10

Terminal logs from incrementing the number

接下来,您可以与reset函数进行交互:

  1. 确保在main.rs文件中为Incrementer_ABI.json调用了abigen macro(如果它已存在于main.rs文件中,则无需再有第二个)
  2. 创建一个名为reset的新异步函数,这将客户端对象的引用和地址作为输入
  3. 使用客户端和合约地址值创建一个由abigen macro生成的Incrementer对象的新实例
  4. 在新的Incrementer对象中调用reset函数
  5. main函数中调用reset函数
// ...

// 1. Generate a type-safe interface for the Incrementer smart contract
abigen!(
    Incrementer,
    "./Incrementer_ABI.json",
    event_derives(serde::Deserialize, serde::Serialize)
);

// 2. Define an asynchronous function that takes a client provider and address as input
async fn reset(client: &Client, contract_addr: &H160) -> Result<(), Box<dyn std::error::Error>> {
    println!("Resetting number...");

    // 3. Create contract instance
    let contract = Incrementer::new(contract_addr.clone(), Arc::new(client.clone()));

    // 4. Send contract transaction
    let tx = contract.reset().send().await?.await?;
    println!("Transaction Receipt: {}", serde_json::to_string(&tx)?);

    Ok(())
}

// ...

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ...

    // 5. Call reset function in main
    reset(&client, &addr).await?;

    Ok(())
}

如果成功,交易收据将会显示在终端显示。您可以在main函数中使用read_number函数,以确保数值按预期变化。如果您在重置数值后使用read_number函数,您应在终端看到0

Terminal logs from resetting the number

您可以在GitHub上查看完整的脚本

本网站的所有信息由第三方提供,仅供参考之用。Moonbeam文档网站(https://docs.moonbeam.network/)上列出和描述的任何项目与Moonbeam立场无关。Moonbeam Foundation不保证网站信息的准确性、完整性或真实性。如使用或依赖本网站信息,需自行承担相关风险,Moonbeam Foundation不承担任何责任和义务。这些材料的所有陈述和/或意见由提供方个人或实体负责,与Moonbeam Foundation立场无关,概不构成任何投资建议。对于任何特定事项或情况,应寻求专业权威人士的建议。此处的信息可能会包含或链接至第三方提供的信息与/或第三方服务(包括任何第三方网站等)。这类链接网站不受Moonbeam Foundation控制。Moonbeam Foundation对此类链接网站的内容(包括此类链接网站上包含的任何信息或资料)概不负责也不认可。这些链接内容仅为方便访客而提供,Moonbeam Foundation对因您使用此信息或任何第三方网站或服务提供的信息而产生的所有责任概不负责。
Last update: January 25, 2024
| Created: December 28, 2022