返回 SC 笔记
SC Day 26

Rust serde + reqwest + JSON 处理 + 链上查询 CLI v1

### 一、serde — Rust 的序列化/反序列化框架

2026-04-26
第二阶段:框架实战 (Day 25-30)
RustserdereqwestJSONCLI区块链

日期: 2026-04-26 方向: Rust 阶段: 第二阶段:框架实战 (Day 25-30) 标签: #Rust #serde #reqwest #JSON #CLI #区块链


今日目标

  1. 掌握 serde 的 Serialize/Deserialize 宏和属性
  2. 学会 serde_json 的手动解析和自动反序列化
  3. 使用 reqwest 发送 HTTP GET/POST 请求
  4. 理解 Etherscan/Alchemy API 的调用方式
  5. 编写第一版链上查询 CLI 工具

核心概念

一、serde — Rust 的序列化/反序列化框架

serde 是 Rust 生态中最核心的库之一,几乎所有需要处理数据格式的项目都依赖它。serde 本身是一个框架(framework),实际的格式支持由 serde_jsonserde_yamltoml 等库提供。

1.1 基本使用

use serde::{Deserialize, Serialize};

// derive 宏自动生成序列化/反序列化代码
#[derive(Debug, Serialize, Deserialize)]
struct TokenInfo {
    name: String,
    symbol: String,
    decimals: u8,
    total_supply: String, // 大数用字符串
}

fn basic_example() {
    let token = TokenInfo {
        name: "Ethereum".to_string(),
        symbol: "ETH".to_string(),
        decimals: 18,
        total_supply: "120000000000000000000000000".to_string(),
    };

    // 序列化: Rust struct -> JSON string
    let json = serde_json::to_string(&token).unwrap();
    println!("JSON: {}", json);
    // {"name":"Ethereum","symbol":"ETH","decimals":18,"total_supply":"120000000000000000000000000"}

    // 美化输出
    let pretty = serde_json::to_string_pretty(&token).unwrap();
    println!("{}", pretty);

    // 反序列化: JSON string -> Rust struct
    let parsed: TokenInfo = serde_json::from_str(&json).unwrap();
    println!("Parsed: {:?}", parsed);
}

1.2 serde 属性详解

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] // 所有字段转 camelCase
struct EtherscanResponse {
    // JSON 字段名 "status" -> Rust 字段名 "status"
    status: String,

    // JSON 字段名 "message" -> Rust 字段名 "message"
    message: String,

    // JSON 字段名 "result" -> Rust 字段名 "result"
    result: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct TransactionInfo {
    // 重命名: JSON "blockNumber" <-> Rust "block_number"
    #[serde(rename = "blockNumber")]
    block_number: String,

    // 重命名: JSON "timeStamp" <-> Rust "timestamp"
    #[serde(rename = "timeStamp")]
    timestamp: String,

    // 重命名
    #[serde(rename = "from")]
    from_address: String,

    #[serde(rename = "to")]
    to_address: String,

    // 可选字段: JSON 中可能没有这个字段
    #[serde(default)]
    value: String,

    // 反序列化时的默认值
    #[serde(default = "default_gas")]
    gas: String,

    // 跳过序列化 (不写入 JSON)
    #[serde(skip_serializing)]
    internal_id: Option<u64>,

    // 跳过反序列化 (不从 JSON 读取)
    #[serde(skip_deserializing)]
    cached_at: Option<String>,
}

fn default_gas() -> String {
    "21000".to_string()
}

// 枚举的序列化
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")] // 内部标签模式
enum ChainEvent {
    #[serde(rename = "transfer")]
    Transfer { from: String, to: String, value: String },

    #[serde(rename = "approval")]
    Approval { owner: String, spender: String, value: String },

    #[serde(rename = "swap")]
    Swap { token_in: String, token_out: String, amount_in: String },
}

1.3 serde_json::Value — 动态 JSON

当 JSON 结构不固定或太复杂时,用 Value 动态处理:

use serde_json::{json, Value};

fn dynamic_json() {
    // 构建 JSON (json! 宏)
    let rpc_request = json!({
        "jsonrpc": "2.0",
        "method": "eth_getBalance",
        "params": ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "latest"],
        "id": 1
    });

    println!("{}", serde_json::to_string_pretty(&rpc_request).unwrap());

    // 解析动态 JSON
    let json_str = r#"{
        "jsonrpc": "2.0",
        "id": 1,
        "result": "0x1234abcd"
    }"#;

    let value: Value = serde_json::from_str(json_str).unwrap();

    // 访问字段 (返回 &Value, 不存在返回 Null)
    let result = &value["result"];
    println!("Result: {}", result); // "0x1234abcd"

    // 安全访问
    if let Some(result_str) = value["result"].as_str() {
        println!("Result string: {}", result_str);
    }

    // 嵌套访问
    let nested_json = json!({
        "data": {
            "tokens": [
                {"symbol": "ETH", "price": 3500.0},
                {"symbol": "BTC", "price": 65000.0}
            ]
        }
    });

    let first_token = &nested_json["data"]["tokens"][0]["symbol"];
    println!("First token: {}", first_token); // "ETH"
}

二、reqwest — HTTP 客户端

reqwest 是 Rust 最流行的 HTTP 客户端库,底层使用 hyper,支持异步和同步两种模式。

2.1 异步 GET 请求

use reqwest::Client;

async fn get_example() -> Result<(), reqwest::Error> {
    // 创建可复用的客户端 (连接池)
    let client = Client::new();

    // 简单 GET
    let response = client
        .get("https://api.coingecko.com/api/v3/simple/price")
        .query(&[("ids", "ethereum"), ("vs_currencies", "usd")])
        .send()
        .await?;

    // 检查状态码
    println!("Status: {}", response.status());

    // 获取响应体为文本
    let body = response.text().await?;
    println!("Body: {}", body);

    Ok(())
}

2.2 异步 POST 请求 (JSON-RPC)

use reqwest::Client;
use serde::{Deserialize, Serialize};

#[derive(Serialize)]
struct RpcRequest {
    jsonrpc: String,
    method: String,
    params: Vec<serde_json::Value>,
    id: u64,
}

#[derive(Deserialize, Debug)]
struct RpcResponse {
    jsonrpc: String,
    id: u64,
    result: Option<serde_json::Value>,
    error: Option<RpcError>,
}

#[derive(Deserialize, Debug)]
struct RpcError {
    code: i64,
    message: String,
}

async fn post_example() -> Result<String, Box<dyn std::error::Error>> {
    let client = Client::builder()
        .timeout(std::time::Duration::from_secs(10))
        .build()?;

    let request = RpcRequest {
        jsonrpc: "2.0".to_string(),
        method: "eth_blockNumber".to_string(),
        params: vec![],
        id: 1,
    };

    let response: RpcResponse = client
        .post("https://eth.llamarpc.com")
        .json(&request)  // 自动序列化为 JSON
        .send()
        .await?
        .json()          // 自动反序列化
        .await?;

    match response.result {
        Some(value) => {
            let hex = value.as_str().unwrap_or("0x0");
            let block_number = u64::from_str_radix(hex.trim_start_matches("0x"), 16)?;
            Ok(format!("Latest block: {} ({})", block_number, hex))
        }
        None => {
            let err = response.error.unwrap();
            Err(format!("RPC Error: {}", err.message).into())
        }
    }
}

2.3 reqwest 配置

use reqwest::{Client, header};
use std::time::Duration;

fn create_client() -> Client {
    let mut headers = header::HeaderMap::new();
    headers.insert("Accept", header::HeaderValue::from_static("application/json"));

    Client::builder()
        .timeout(Duration::from_secs(30))       // 请求超时
        .connect_timeout(Duration::from_secs(5)) // 连接超时
        .default_headers(headers)                 // 默认请求头
        .pool_max_idle_per_host(10)              // 连接池
        .user_agent("momo-cli/0.1.0")           // User-Agent
        .build()
        .expect("Failed to create client")
}

三、Etherscan API 基础

Etherscan 提供免费的 REST API 来查询链上数据:

端点功能免费限制
api.etherscan.io/api?module=account&action=balance查余额5次/秒
api.etherscan.io/api?module=account&action=txlist交易列表5次/秒
api.etherscan.io/api?module=stats&action=ethpriceETH 价格5次/秒
api.etherscan.io/api?module=proxy&action=eth_blockNumber最新区块5次/秒

API Key 申请:https://etherscan.io/apis


代码实战

链上查询 CLI v1 完整代码

// Cargo.toml:
// [package]
// name = "chain-cli"
// version = "0.1.0"
// edition = "2021"
//
// [dependencies]
// tokio = { version = "1", features = ["full"] }
// reqwest = { version = "0.12", features = ["json"] }
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
// dotenv = "0.15"

use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::env;

// ============ API 响应结构 ============

#[derive(Debug, Deserialize)]
struct EtherscanResponse<T> {
    status: String,
    message: String,
    result: T,
}

#[derive(Debug, Deserialize)]
struct EthPrice {
    ethbtc: String,
    ethbtc_timestamp: String,
    ethusd: String,
    ethusd_timestamp: String,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Transaction {
    #[serde(rename = "blockNumber")]
    block_number: String,
    #[serde(rename = "timeStamp")]
    timestamp: String,
    hash: String,
    from: String,
    to: String,
    value: String,
    gas: String,
    #[serde(rename = "gasUsed")]
    gas_used: String,
    #[serde(rename = "isError")]
    is_error: String,
    #[serde(rename = "functionName")]
    function_name: String,
}

// ============ JSON-RPC 结构 ============

#[derive(Serialize)]
struct JsonRpcRequest {
    jsonrpc: String,
    method: String,
    params: Vec<serde_json::Value>,
    id: u64,
}

#[derive(Deserialize)]
struct JsonRpcResponse {
    #[allow(dead_code)]
    jsonrpc: String,
    #[allow(dead_code)]
    id: u64,
    result: Option<serde_json::Value>,
    error: Option<JsonRpcError>,
}

#[derive(Deserialize, Debug)]
struct JsonRpcError {
    #[allow(dead_code)]
    code: i64,
    message: String,
}

// ============ CLI 客户端 ============

struct ChainClient {
    http: Client,
    etherscan_key: String,
    rpc_url: String,
}

impl ChainClient {
    fn new() -> Result<Self, Box<dyn std::error::Error>> {
        // 从环境变量读取配置
        let etherscan_key = env::var("ETHERSCAN_API_KEY")
            .unwrap_or_else(|_| "YourApiKeyToken".to_string());

        let rpc_url = env::var("ETH_RPC_URL")
            .unwrap_or_else(|_| "https://eth.llamarpc.com".to_string());

        let http = Client::builder()
            .timeout(std::time::Duration::from_secs(15))
            .build()?;

        Ok(Self {
            http,
            etherscan_key,
            rpc_url,
        })
    }

    // ---- Etherscan API 方法 ----

    /// 查询 ETH 余额
    async fn get_balance(&self, address: &str) -> Result<f64, Box<dyn std::error::Error>> {
        let url = format!(
            "https://api.etherscan.io/api?module=account&action=balance&address={}&tag=latest&apikey={}",
            address, self.etherscan_key
        );

        let resp: EtherscanResponse<String> = self.http.get(&url).send().await?.json().await?;

        if resp.status != "1" {
            return Err(format!("Etherscan error: {}", resp.message).into());
        }

        // Wei -> ETH
        let wei: f64 = resp.result.parse()?;
        Ok(wei / 1e18)
    }

    /// 查询 ETH 价格
    async fn get_eth_price(&self) -> Result<EthPrice, Box<dyn std::error::Error>> {
        let url = format!(
            "https://api.etherscan.io/api?module=stats&action=ethprice&apikey={}",
            self.etherscan_key
        );

        let resp: EtherscanResponse<EthPrice> = self.http.get(&url).send().await?.json().await?;

        if resp.status != "1" {
            return Err(format!("Etherscan error: {}", resp.message).into());
        }

        Ok(resp.result)
    }

    /// 查询最近交易
    async fn get_transactions(
        &self,
        address: &str,
        count: u32,
    ) -> Result<Vec<Transaction>, Box<dyn std::error::Error>> {
        let url = format!(
            "https://api.etherscan.io/api?module=account&action=txlist&address={}&startblock=0&endblock=99999999&page=1&offset={}&sort=desc&apikey={}",
            address, count, self.etherscan_key
        );

        let resp: EtherscanResponse<Vec<Transaction>> =
            self.http.get(&url).send().await?.json().await?;

        if resp.status != "1" {
            return Err(format!("Etherscan error: {}", resp.message).into());
        }

        Ok(resp.result)
    }

    // ---- JSON-RPC 方法 ----

    /// 通过 RPC 查询最新区块号
    async fn get_block_number(&self) -> Result<u64, Box<dyn std::error::Error>> {
        let request = JsonRpcRequest {
            jsonrpc: "2.0".to_string(),
            method: "eth_blockNumber".to_string(),
            params: vec![],
            id: 1,
        };

        let resp: JsonRpcResponse = self
            .http
            .post(&self.rpc_url)
            .json(&request)
            .send()
            .await?
            .json()
            .await?;

        if let Some(err) = resp.error {
            return Err(format!("RPC error: {}", err.message).into());
        }

        let hex = resp.result.unwrap().as_str().unwrap().to_string();
        let number = u64::from_str_radix(hex.trim_start_matches("0x"), 16)?;
        Ok(number)
    }

    /// 通过 RPC 查询 ETH 余额
    async fn get_balance_rpc(&self, address: &str) -> Result<f64, Box<dyn std::error::Error>> {
        let request = JsonRpcRequest {
            jsonrpc: "2.0".to_string(),
            method: "eth_getBalance".to_string(),
            params: vec![
                serde_json::Value::String(address.to_string()),
                serde_json::Value::String("latest".to_string()),
            ],
            id: 1,
        };

        let resp: JsonRpcResponse = self
            .http
            .post(&self.rpc_url)
            .json(&request)
            .send()
            .await?
            .json()
            .await?;

        if let Some(err) = resp.error {
            return Err(format!("RPC error: {}", err.message).into());
        }

        let hex = resp.result.unwrap().as_str().unwrap().to_string();
        let wei = u128::from_str_radix(hex.trim_start_matches("0x"), 16)?;
        Ok(wei as f64 / 1e18)
    }
}

// ============ 辅助函数 ============

fn format_eth(eth: f64) -> String {
    if eth >= 1.0 {
        format!("{:.4} ETH", eth)
    } else if eth >= 0.001 {
        format!("{:.6} ETH", eth)
    } else {
        format!("{:.10} ETH", eth)
    }
}

fn format_timestamp(ts: &str) -> String {
    let secs: i64 = ts.parse().unwrap_or(0);
    // 简单的时间格式化
    let hours_ago = (std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64
        - secs)
        / 3600;

    if hours_ago < 1 {
        "< 1 hour ago".to_string()
    } else if hours_ago < 24 {
        format!("{} hours ago", hours_ago)
    } else {
        format!("{} days ago", hours_ago / 24)
    }
}

fn shorten_address(addr: &str) -> String {
    if addr.len() > 10 {
        format!("{}...{}", &addr[..6], &addr[addr.len() - 4..])
    } else {
        addr.to_string()
    }
}

// ============ 主程序 ============

#[tokio::main]
async fn main() {
    // 加载 .env 文件
    dotenv::dotenv().ok();

    println!("============================================");
    println!("  Chain CLI v1 - Ethereum Query Tool");
    println!("============================================\n");

    let client = match ChainClient::new() {
        Ok(c) => c,
        Err(e) => {
            eprintln!("Failed to initialize client: {}", e);
            return;
        }
    };

    let address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"; // vitalik.eth

    // 1. 查询区块号
    println!("[1] Latest Block Number");
    match client.get_block_number().await {
        Ok(num) => println!("    Block: #{}\n", num),
        Err(e) => eprintln!("    Error: {}\n", e),
    }

    // 2. 查询 ETH 价格
    println!("[2] ETH Price");
    match client.get_eth_price().await {
        Ok(price) => {
            println!("    ETH/USD: ${}", price.ethusd);
            println!("    ETH/BTC: {}\n", price.ethbtc);
        }
        Err(e) => eprintln!("    Error: {}\n", e),
    }

    // 3. 查询余额 (通过 RPC)
    println!("[3] Balance for {}", shorten_address(address));
    match client.get_balance_rpc(address).await {
        Ok(balance) => println!("    Balance: {}\n", format_eth(balance)),
        Err(e) => eprintln!("    Error: {}\n", e),
    }

    // 4. 查询最近交易
    println!("[4] Recent Transactions (last 5)");
    match client.get_transactions(address, 5).await {
        Ok(txs) => {
            for (i, tx) in txs.iter().enumerate() {
                let value_eth: f64 = tx.value.parse::<f64>().unwrap_or(0.0) / 1e18;
                let direction = if tx.from.to_lowercase() == address.to_lowercase() {
                    "OUT"
                } else {
                    "IN "
                };
                println!(
                    "    {}. [{}] {} {} | {} | {}",
                    i + 1,
                    direction,
                    format_eth(value_eth),
                    if tx.function_name.is_empty() {
                        "Transfer"
                    } else {
                        &tx.function_name
                    },
                    shorten_address(&tx.hash),
                    format_timestamp(&tx.timestamp)
                );
            }
        }
        Err(e) => eprintln!("    Error: {}", e),
    }

    println!("\n============================================");
}

关键要点总结

serde 核心属性速查

属性作用示例
#[serde(rename = "...")]字段重命名blockNumber -> block_number
#[serde(rename_all = "camelCase")]全局重命名所有字段转 camelCase
#[serde(default)]缺失时用 Default可选字段
#[serde(skip)]跳过序列化和反序列化内部状态
#[serde(flatten)]展平嵌套结构合并字段
#[serde(tag = "type")]枚举内部标签区分事件类型

API Key 管理最佳实践

  1. 永远不要硬编码 API Key
  2. 使用 .env 文件 + dotenv
  3. .env 加入 .gitignore
  4. 提供 .env.example 模板
# .env.example
ETHERSCAN_API_KEY=your_key_here
ETH_RPC_URL=https://eth.llamarpc.com

常见误区

  1. serde 字段名大小写: JSON 是 camelCase,Rust 是 snake_case,忘记 #[serde(rename)] 导致反序列化失败
  2. reqwest 忘记启用 json feature: reqwest = { features = ["json"] } 必须启用
  3. 没有处理 API 限流: Etherscan 免费 5次/秒,要加 rate limiting
  4. 大数溢出: ETH 余额单位是 Wei(18位小数),用 u128 或字符串处理
  5. 错误吞没: 用 unwrap() 而不处理 Result,生产代码应该用 ?match

面试关联

面试题本课关联
"如何用 Rust 调用区块链 RPC?"reqwest + JSON-RPC 协议
"serde 的 derive 宏做了什么?"编译时生成序列化/反序列化代码
"如何处理 API key 安全?"环境变量 + dotenv + .gitignore
"JSON-RPC 和 REST API 的区别?"RPC 是方法调用,REST 是资源操作
"如何处理区块链大数?"u128 / u256 / 字符串表示

参考资源