1use std::collections::HashMap;
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use crate::chain_client::{ChainBlock, ChainClient};
12use crate::error::TransportError;
13use crate::request::{JsonRpcRequest, JsonRpcResponse};
14use crate::transport::{HealthStatus, RpcTransport};
15
16#[derive(Debug, Clone)]
21pub struct AptosCuCostTable {
22 costs: HashMap<String, u32>,
23 default_cost: u32,
24}
25
26impl AptosCuCostTable {
27 pub fn defaults() -> Self {
28 let mut table = Self::new(15);
29 let entries: &[(&str, u32)] = &[
30 ("get_ledger_info", 5),
31 ("get_block_by_height", 20),
32 ("get_block_by_version", 20),
33 ("get_account", 10),
34 ("get_account_resources", 15),
35 ("get_account_modules", 15),
36 ("get_transactions", 20),
37 ("get_transaction_by_hash", 15),
38 ("submit_transaction", 10),
39 ("simulate_transaction", 30),
40 ("get_events_by_event_handle", 20),
41 ];
42 for &(method, cost) in entries {
43 table.costs.insert(method.to_string(), cost);
44 }
45 table
46 }
47
48 pub fn new(default_cost: u32) -> Self {
49 Self { costs: HashMap::new(), default_cost }
50 }
51
52 pub fn cost_for(&self, method: &str) -> u32 {
53 self.costs.get(method).copied().unwrap_or(self.default_cost)
54 }
55}
56
57impl Default for AptosCuCostTable {
58 fn default() -> Self {
59 Self::defaults()
60 }
61}
62
63pub fn aptos_mainnet_endpoints() -> &'static [&'static str] {
68 &[
69 "https://fullnode.mainnet.aptoslabs.com/v1",
70 "https://aptos-mainnet.nodereal.io/v1",
71 ]
72}
73
74pub fn aptos_testnet_endpoints() -> &'static [&'static str] {
75 &[
76 "https://fullnode.testnet.aptoslabs.com/v1",
77 ]
78}
79
80pub fn aptos_devnet_endpoints() -> &'static [&'static str] {
81 &[
82 "https://fullnode.devnet.aptoslabs.com/v1",
83 ]
84}
85
86pub struct AptosTransport {
95 inner: Arc<dyn RpcTransport>,
96}
97
98impl AptosTransport {
99 pub fn new(inner: Arc<dyn RpcTransport>) -> Self {
100 Self { inner }
101 }
102
103 pub fn inner(&self) -> &Arc<dyn RpcTransport> {
104 &self.inner
105 }
106}
107
108#[async_trait]
109impl RpcTransport for AptosTransport {
110 async fn send(&self, req: JsonRpcRequest) -> Result<JsonRpcResponse, TransportError> {
111 self.inner.send(req).await
112 }
113
114 async fn send_batch(&self, reqs: Vec<JsonRpcRequest>) -> Result<Vec<JsonRpcResponse>, TransportError> {
115 self.inner.send_batch(reqs).await
116 }
117
118 fn health(&self) -> HealthStatus { self.inner.health() }
119 fn url(&self) -> &str { self.inner.url() }
120}
121
122pub struct AptosChainClient {
131 transport: Arc<dyn RpcTransport>,
132 chain_id: String,
133}
134
135impl AptosChainClient {
136 pub fn new(transport: Arc<dyn RpcTransport>, chain_id: impl Into<String>) -> Self {
137 Self {
138 transport,
139 chain_id: chain_id.into(),
140 }
141 }
142}
143
144#[async_trait]
145impl ChainClient for AptosChainClient {
146 async fn get_head_height(&self) -> Result<u64, TransportError> {
147 let req = JsonRpcRequest::new(1, "get_ledger_info", vec![]);
148 let resp = self.transport.send(req).await?;
149 let result = resp.into_result().map_err(TransportError::Rpc)?;
150
151 let height_str = result["block_height"]
153 .as_str()
154 .or_else(|| result["result"]["block_height"].as_str())
155 .unwrap_or("0");
156 height_str.parse::<u64>().map_err(|e| {
157 TransportError::Other(format!("invalid aptos block height: {e}"))
158 })
159 }
160
161 async fn get_block_by_height(
162 &self,
163 height: u64,
164 ) -> Result<Option<ChainBlock>, TransportError> {
165 let req = JsonRpcRequest::new(
166 1,
167 "get_block_by_height",
168 vec![serde_json::json!(height.to_string())],
169 );
170 let resp = self.transport.send(req).await?;
171 let result = resp.into_result().map_err(TransportError::Rpc)?;
172
173 if result.is_null() {
174 return Ok(None);
175 }
176
177 let hash = result["block_hash"]
178 .as_str()
179 .unwrap_or_default()
180 .to_string();
181 let timestamp = result["block_timestamp"]
182 .as_str()
183 .and_then(|s| s.parse::<u64>().ok())
184 .map(|us| (us / 1_000_000) as i64) .unwrap_or(0);
186 let tx_count = result["transactions"]
187 .as_array()
188 .map(|a| a.len() as u32)
189 .unwrap_or(0);
190
191 let first_version = result["first_version"]
193 .as_str()
194 .unwrap_or_default()
195 .to_string();
196
197 Ok(Some(ChainBlock {
198 height,
199 hash,
200 parent_hash: first_version, timestamp,
202 tx_count,
203 }))
204 }
205
206 fn chain_id(&self) -> &str {
207 &self.chain_id
208 }
209
210 fn chain_family(&self) -> &str {
211 "aptos"
212 }
213
214 async fn health_check(&self) -> Result<bool, TransportError> {
215 self.get_head_height().await.map(|_| true)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::request::RpcId;
223 use serde_json::Value;
224 use std::sync::Mutex;
225
226 struct MockTransport {
227 url: String,
228 responses: Mutex<Vec<JsonRpcResponse>>,
229 }
230
231 impl MockTransport {
232 fn new(responses: Vec<JsonRpcResponse>) -> Self {
233 Self { url: "mock://aptos".into(), responses: Mutex::new(responses) }
234 }
235 }
236
237 #[async_trait]
238 impl RpcTransport for MockTransport {
239 async fn send(&self, _req: JsonRpcRequest) -> Result<JsonRpcResponse, TransportError> {
240 let mut r = self.responses.lock().unwrap();
241 if r.is_empty() { Err(TransportError::Other("no mock".into())) }
242 else { Ok(r.remove(0)) }
243 }
244 fn url(&self) -> &str { &self.url }
245 }
246
247 fn ok(result: Value) -> JsonRpcResponse {
248 JsonRpcResponse { jsonrpc: "2.0".into(), id: RpcId::Number(1), result: Some(result), error: None }
249 }
250
251 #[test]
252 fn cu_costs() {
253 let t = AptosCuCostTable::defaults();
254 assert_eq!(t.cost_for("get_ledger_info"), 5);
255 assert_eq!(t.cost_for("get_block_by_height"), 20);
256 }
257
258 #[test]
259 fn endpoints() {
260 assert!(!aptos_mainnet_endpoints().is_empty());
261 assert!(!aptos_testnet_endpoints().is_empty());
262 }
263
264 #[tokio::test]
265 async fn aptos_get_head_height() {
266 let t = Arc::new(MockTransport::new(vec![ok(serde_json::json!({
267 "block_height": "150000000"
268 }))]));
269 let c = AptosChainClient::new(t, "1");
270 assert_eq!(c.get_head_height().await.unwrap(), 150000000);
271 }
272
273 #[tokio::test]
274 async fn aptos_get_block() {
275 let t = Arc::new(MockTransport::new(vec![ok(serde_json::json!({
276 "block_hash": "0xabc123",
277 "block_timestamp": "1700000000000000",
278 "first_version": "100000",
279 "transactions": [{"type": "user"}, {"type": "user"}]
280 }))]));
281 let c = AptosChainClient::new(t, "1");
282 let b = c.get_block_by_height(100).await.unwrap().unwrap();
283 assert_eq!(b.hash, "0xabc123");
284 assert_eq!(b.timestamp, 1700000000);
285 assert_eq!(b.tx_count, 2);
286 }
287
288 #[tokio::test]
289 async fn aptos_metadata() {
290 let t = Arc::new(MockTransport::new(vec![]));
291 let c = AptosChainClient::new(t, "2");
292 assert_eq!(c.chain_family(), "aptos");
293 assert_eq!(c.chain_id(), "2");
294 }
295}