1use async_trait::async_trait;
8use chrono::Utc;
9use serde::{Deserialize, Serialize};
10use vex_core::Hash;
11
12use crate::backend::{AnchorBackend, AnchorMetadata, AnchorReceipt};
13use crate::error::AnchorError;
14
15#[derive(Serialize)]
16struct JsonRpcRequest<'a, T: Serialize> {
17 jsonrpc: &'a str,
18 method: &'a str,
19 params: T,
20 id: u64,
21}
22
23#[derive(Deserialize)]
24struct JsonRpcResponse<T> {
25 result: Option<T>,
26 error: Option<JsonRpcError>,
27}
28
29#[derive(Deserialize)]
30struct JsonRpcError {
31 code: i64,
32 message: String,
33}
34
35#[derive(Debug, Clone)]
40pub struct EthereumAnchor {
41 rpc_url: String,
42 from_address: String,
43 client: reqwest::Client,
44}
45
46impl EthereumAnchor {
47 pub fn new(rpc_url: impl Into<String>, from_address: impl Into<String>) -> Self {
49 let client = reqwest::Client::builder()
50 .timeout(std::time::Duration::from_secs(60))
51 .user_agent("vex-anchor/0.1.5")
52 .build()
53 .expect("Failed to build Ethereum HTTP client");
54
55 Self {
56 rpc_url: rpc_url.into(),
57 from_address: from_address.into(),
58 client,
59 }
60 }
61
62 async fn get_block_number(&self) -> Result<u64, AnchorError> {
63 let req = JsonRpcRequest {
64 jsonrpc: "2.0",
65 method: "eth_blockNumber",
66 params: serde_json::json!([]),
67 id: 1,
68 };
69
70 let resp_bytes = self
71 .client
72 .post(&self.rpc_url)
73 .json(&req)
74 .send()
75 .await
76 .map_err(|e| AnchorError::Network(e.to_string()))?
77 .bytes()
78 .await
79 .map_err(|e| AnchorError::Network(e.to_string()))?;
80
81 let resp: JsonRpcResponse<String> =
82 serde_json::from_slice(&resp_bytes).map_err(|e| AnchorError::Network(e.to_string()))?;
83
84 if let Some(err) = resp.error {
85 return Err(AnchorError::Network(format!(
86 "RPC error {}: {}",
87 err.code, err.message
88 )));
89 }
90
91 let hex = resp.result.unwrap_or_default();
92 u64::from_str_radix(hex.trim_start_matches("0x"), 16)
93 .map_err(|e| AnchorError::Network(e.to_string()))
94 }
95}
96
97#[async_trait]
98impl AnchorBackend for EthereumAnchor {
99 async fn anchor(
100 &self,
101 root: &Hash,
102 metadata: AnchorMetadata,
103 ) -> Result<AnchorReceipt, AnchorError> {
104 let calldata = format!("0x56455800{}", root.to_hex());
106
107 let req = JsonRpcRequest {
108 jsonrpc: "2.0",
109 method: "eth_call",
110 params: serde_json::json!([{
111 "from": self.from_address,
112 "to": "0x0000000000000000000000000000000000000000",
113 "data": calldata
114 }, "latest"]),
115 id: 2,
116 };
117
118 let resp = self
119 .client
120 .post(&self.rpc_url)
121 .json(&req)
122 .send()
123 .await
124 .map_err(|e| AnchorError::Network(e.to_string()))?;
125
126 if !resp.status().is_success() {
127 return Err(AnchorError::Network(format!(
128 "Ethereum RPC returned HTTP {}",
129 resp.status()
130 )));
131 }
132
133 let block = self.get_block_number().await.unwrap_or(0);
134 let anchor_id = format!("eth://block:{}/calldata:{}", block, &root.to_hex()[..16]);
135
136 Ok(AnchorReceipt {
137 backend: self.name().to_string(),
138 root_hash: root.to_hex(),
139 anchor_id,
140 anchored_at: Utc::now(),
141 proof: Some(calldata),
142 metadata,
143 })
144 }
145
146 async fn verify(&self, receipt: &AnchorReceipt) -> Result<bool, AnchorError> {
147 let Some(ref proof) = receipt.proof else {
148 return Ok(false);
149 };
150 let expected = format!("0x56455800{}", receipt.root_hash);
151 Ok(proof == &expected)
152 }
153
154 fn name(&self) -> &str {
155 "ethereum"
156 }
157
158 async fn is_healthy(&self) -> bool {
159 self.get_block_number().await.is_ok()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::backend::AnchorMetadata;
167
168 #[test]
169 fn test_eth_verify_calldata() {
170 let root_hash = "abc123def456".to_string();
171 let receipt = AnchorReceipt {
172 backend: "ethereum".to_string(),
173 root_hash: root_hash.clone(),
174 anchor_id: "eth://block:12345/calldata:abc123".to_string(),
175 anchored_at: Utc::now(),
176 proof: Some(format!("0x56455800{}", root_hash)),
177 metadata: AnchorMetadata::new("test-tenant", 1),
178 };
179 assert!(receipt.proof.as_ref().unwrap().starts_with("0x56455800"));
180 assert!(receipt.proof.as_ref().unwrap().ends_with(&root_hash));
181 }
182}