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 #区块链
今日目标
- 掌握 serde 的 Serialize/Deserialize 宏和属性
- 学会 serde_json 的手动解析和自动反序列化
- 使用 reqwest 发送 HTTP GET/POST 请求
- 理解 Etherscan/Alchemy API 的调用方式
- 编写第一版链上查询 CLI 工具
核心概念
一、serde — Rust 的序列化/反序列化框架
serde 是 Rust 生态中最核心的库之一,几乎所有需要处理数据格式的项目都依赖它。serde 本身是一个框架(framework),实际的格式支持由 serde_json、serde_yaml、toml 等库提供。
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=ethprice | ETH 价格 | 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 管理最佳实践
- 永远不要硬编码 API Key
- 使用
.env文件 +dotenv库 .env加入.gitignore- 提供
.env.example模板
# .env.example
ETHERSCAN_API_KEY=your_key_here
ETH_RPC_URL=https://eth.llamarpc.com
常见误区
- serde 字段名大小写: JSON 是 camelCase,Rust 是 snake_case,忘记
#[serde(rename)]导致反序列化失败 - reqwest 忘记启用 json feature:
reqwest = { features = ["json"] }必须启用 - 没有处理 API 限流: Etherscan 免费 5次/秒,要加 rate limiting
- 大数溢出: ETH 余额单位是 Wei(18位小数),用
u128或字符串处理 - 错误吞没: 用
unwrap()而不处理Result,生产代码应该用?或match
面试关联
| 面试题 | 本课关联 |
|---|---|
| "如何用 Rust 调用区块链 RPC?" | reqwest + JSON-RPC 协议 |
| "serde 的 derive 宏做了什么?" | 编译时生成序列化/反序列化代码 |
| "如何处理 API key 安全?" | 环境变量 + dotenv + .gitignore |
| "JSON-RPC 和 REST API 的区别?" | RPC 是方法调用,REST 是资源操作 |
| "如何处理区块链大数?" | u128 / u256 / 字符串表示 |