1use std::collections::{HashMap, HashSet};
7use std::sync::{Arc, OnceLock};
8
9use async_trait::async_trait;
10use crate::chain_client::{ChainBlock, ChainClient};
11use crate::error::TransportError;
12use crate::method_safety::MethodSafety;
13use crate::request::{JsonRpcRequest, JsonRpcResponse};
14use crate::transport::{HealthStatus, RpcTransport};
15
16pub fn classify_substrate_method(method: &str) -> MethodSafety {
22 if substrate_unsafe_methods().contains(method) {
23 MethodSafety::Unsafe
24 } else if substrate_idempotent_methods().contains(method) {
25 MethodSafety::Idempotent
26 } else {
27 MethodSafety::Safe
28 }
29}
30
31pub fn is_substrate_safe_to_retry(method: &str) -> bool {
32 classify_substrate_method(method) == MethodSafety::Safe
33}
34
35pub fn is_substrate_safe_to_dedup(method: &str) -> bool {
36 classify_substrate_method(method) == MethodSafety::Safe
37}
38
39pub fn is_substrate_cacheable(method: &str) -> bool {
40 classify_substrate_method(method) == MethodSafety::Safe
41}
42
43fn substrate_unsafe_methods() -> &'static HashSet<&'static str> {
44 static UNSAFE: OnceLock<HashSet<&'static str>> = OnceLock::new();
45 UNSAFE.get_or_init(HashSet::new) }
47
48fn substrate_idempotent_methods() -> &'static HashSet<&'static str> {
49 static IDEMPOTENT: OnceLock<HashSet<&'static str>> = OnceLock::new();
50 IDEMPOTENT.get_or_init(|| {
51 ["author_submitExtrinsic", "author_submitAndWatchExtrinsic"]
52 .into_iter()
53 .collect()
54 })
55}
56
57#[derive(Debug, Clone)]
63pub struct SubstrateCuCostTable {
64 costs: HashMap<String, u32>,
65 default_cost: u32,
66}
67
68impl SubstrateCuCostTable {
69 pub fn defaults() -> Self {
70 let mut table = Self::new(15);
71 let entries: &[(&str, u32)] = &[
72 ("chain_getBlock", 20),
73 ("chain_getBlockHash", 5),
74 ("chain_getHeader", 10),
75 ("chain_getFinalizedHead", 5),
76 ("state_getStorage", 15),
77 ("state_getMetadata", 50),
78 ("state_getRuntimeVersion", 10),
79 ("state_queryStorageAt", 30),
80 ("system_chain", 5),
81 ("system_health", 5),
82 ("system_peers", 10),
83 ("system_properties", 5),
84 ("author_submitExtrinsic", 10),
85 ];
86 for &(method, cost) in entries {
87 table.costs.insert(method.to_string(), cost);
88 }
89 table
90 }
91
92 pub fn new(default_cost: u32) -> Self {
93 Self {
94 costs: HashMap::new(),
95 default_cost,
96 }
97 }
98
99 pub fn set_cost(&mut self, method: &str, cost: u32) {
100 self.costs.insert(method.to_string(), cost);
101 }
102
103 pub fn cost_for(&self, method: &str) -> u32 {
104 self.costs.get(method).copied().unwrap_or(self.default_cost)
105 }
106}
107
108impl Default for SubstrateCuCostTable {
109 fn default() -> Self {
110 Self::defaults()
111 }
112}
113
114pub fn polkadot_mainnet_endpoints() -> &'static [&'static str] {
119 &[
120 "wss://rpc.polkadot.io",
121 "wss://polkadot.api.onfinality.io/public-ws",
122 ]
123}
124
125pub fn kusama_mainnet_endpoints() -> &'static [&'static str] {
126 &[
127 "wss://kusama-rpc.polkadot.io",
128 "wss://kusama.api.onfinality.io/public-ws",
129 ]
130}
131
132pub struct SubstrateTransport {
138 inner: Arc<dyn RpcTransport>,
139}
140
141impl SubstrateTransport {
142 pub fn new(inner: Arc<dyn RpcTransport>) -> Self {
143 Self { inner }
144 }
145
146 pub fn inner(&self) -> &Arc<dyn RpcTransport> {
147 &self.inner
148 }
149}
150
151#[async_trait]
152impl RpcTransport for SubstrateTransport {
153 async fn send(&self, req: JsonRpcRequest) -> Result<JsonRpcResponse, TransportError> {
154 self.inner.send(req).await
155 }
156
157 async fn send_batch(&self, reqs: Vec<JsonRpcRequest>) -> Result<Vec<JsonRpcResponse>, TransportError> {
158 self.inner.send_batch(reqs).await
159 }
160
161 fn health(&self) -> HealthStatus {
162 self.inner.health()
163 }
164
165 fn url(&self) -> &str {
166 self.inner.url()
167 }
168}
169
170pub struct SubstrateChainClient {
176 transport: Arc<dyn RpcTransport>,
177 chain_id: String,
178}
179
180impl SubstrateChainClient {
181 pub fn new(transport: Arc<dyn RpcTransport>, chain_id: impl Into<String>) -> Self {
182 Self {
183 transport,
184 chain_id: chain_id.into(),
185 }
186 }
187}
188
189#[async_trait]
190impl ChainClient for SubstrateChainClient {
191 async fn get_head_height(&self) -> Result<u64, TransportError> {
192 let req = JsonRpcRequest::new(1, "chain_getHeader", vec![]);
193 let resp = self.transport.send(req).await?;
194 let result = resp.into_result().map_err(TransportError::Rpc)?;
195
196 let number_hex = result["number"]
197 .as_str()
198 .unwrap_or("0x0");
199 let stripped = number_hex.strip_prefix("0x").unwrap_or(number_hex);
200 u64::from_str_radix(stripped, 16).map_err(|e| {
201 TransportError::Other(format!("invalid substrate block number: {e}"))
202 })
203 }
204
205 async fn get_block_by_height(
206 &self,
207 height: u64,
208 ) -> Result<Option<ChainBlock>, TransportError> {
209 let hash_req = JsonRpcRequest::new(
211 1,
212 "chain_getBlockHash",
213 vec![serde_json::json!(height)],
214 );
215 let hash_resp = self.transport.send(hash_req).await?;
216 let hash_result = hash_resp.into_result().map_err(TransportError::Rpc)?;
217
218 let block_hash = match hash_result.as_str() {
219 Some(h) if !h.is_empty() => h.to_string(),
220 _ => return Ok(None),
221 };
222
223 let block_req = JsonRpcRequest::new(
225 1,
226 "chain_getBlock",
227 vec![serde_json::Value::String(block_hash.clone())],
228 );
229 let block_resp = self.transport.send(block_req).await?;
230 let block_result = block_resp.into_result().map_err(TransportError::Rpc)?;
231
232 if block_result.is_null() {
233 return Ok(None);
234 }
235
236 let header = &block_result["block"]["header"];
237 let parent_hash = header["parentHash"]
238 .as_str()
239 .unwrap_or_default()
240 .to_string();
241 let tx_count = block_result["block"]["extrinsics"]
242 .as_array()
243 .map(|a| a.len() as u32)
244 .unwrap_or(0);
245
246 Ok(Some(ChainBlock {
247 height,
248 hash: block_hash,
249 parent_hash,
250 timestamp: 0, tx_count,
252 }))
253 }
254
255 fn chain_id(&self) -> &str {
256 &self.chain_id
257 }
258
259 fn chain_family(&self) -> &str {
260 "substrate"
261 }
262
263 async fn health_check(&self) -> Result<bool, TransportError> {
264 let req = JsonRpcRequest::new(1, "system_health", vec![]);
265 let resp = self.transport.send(req).await?;
266 let result = resp.into_result().map_err(TransportError::Rpc)?;
267 Ok(!result["isSyncing"].as_bool().unwrap_or(true))
269 }
270}
271
272#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::request::RpcId;
280 use serde_json::Value;
281 use std::sync::Mutex;
282
283 struct MockTransport {
284 url: String,
285 responses: Mutex<Vec<JsonRpcResponse>>,
286 recorded: Mutex<Vec<String>>,
287 }
288
289 impl MockTransport {
290 fn new(responses: Vec<JsonRpcResponse>) -> Self {
291 Self {
292 url: "mock://substrate".to_string(),
293 responses: Mutex::new(responses),
294 recorded: Mutex::new(Vec::new()),
295 }
296 }
297 }
298
299 #[async_trait]
300 impl RpcTransport for MockTransport {
301 async fn send(&self, req: JsonRpcRequest) -> Result<JsonRpcResponse, TransportError> {
302 self.recorded.lock().unwrap().push(req.method.clone());
303 let mut responses = self.responses.lock().unwrap();
304 if responses.is_empty() {
305 Err(TransportError::Other("no mock responses".into()))
306 } else {
307 Ok(responses.remove(0))
308 }
309 }
310 fn url(&self) -> &str { &self.url }
311 }
312
313 fn ok_response(result: Value) -> JsonRpcResponse {
314 JsonRpcResponse {
315 jsonrpc: "2.0".to_string(),
316 id: RpcId::Number(1),
317 result: Some(result),
318 error: None,
319 }
320 }
321
322 #[test]
323 fn classify_methods() {
324 assert_eq!(classify_substrate_method("chain_getBlock"), MethodSafety::Safe);
325 assert_eq!(classify_substrate_method("state_getStorage"), MethodSafety::Safe);
326 assert_eq!(classify_substrate_method("author_submitExtrinsic"), MethodSafety::Idempotent);
327 assert_eq!(classify_substrate_method("unknown"), MethodSafety::Safe);
328 }
329
330 #[test]
331 fn cu_costs() {
332 let table = SubstrateCuCostTable::defaults();
333 assert_eq!(table.cost_for("chain_getBlock"), 20);
334 assert_eq!(table.cost_for("system_health"), 5);
335 assert_eq!(table.cost_for("unknown"), 15);
336 }
337
338 #[test]
339 fn endpoints() {
340 assert!(!polkadot_mainnet_endpoints().is_empty());
341 assert!(!kusama_mainnet_endpoints().is_empty());
342 }
343
344 #[tokio::test]
345 async fn substrate_get_head_height() {
346 let transport = Arc::new(MockTransport::new(vec![ok_response(serde_json::json!({
347 "number": "0x1234",
348 "parentHash": "0xabc"
349 }))]));
350 let client = SubstrateChainClient::new(transport, "polkadot");
351 let height = client.get_head_height().await.unwrap();
352 assert_eq!(height, 0x1234);
353 }
354
355 #[tokio::test]
356 async fn substrate_get_block() {
357 let transport = Arc::new(MockTransport::new(vec![
358 ok_response(serde_json::Value::String("0xblock_hash".to_string())),
359 ok_response(serde_json::json!({
360 "block": {
361 "header": {
362 "number": "0x64",
363 "parentHash": "0xparent"
364 },
365 "extrinsics": ["ext1", "ext2", "ext3"]
366 }
367 })),
368 ]));
369 let client = SubstrateChainClient::new(transport, "polkadot");
370 let block = client.get_block_by_height(100).await.unwrap().unwrap();
371 assert_eq!(block.height, 100);
372 assert_eq!(block.hash, "0xblock_hash");
373 assert_eq!(block.parent_hash, "0xparent");
374 assert_eq!(block.tx_count, 3);
375 }
376
377 #[tokio::test]
378 async fn substrate_health_check() {
379 let transport = Arc::new(MockTransport::new(vec![ok_response(serde_json::json!({
380 "peers": 10,
381 "isSyncing": false,
382 "shouldHavePeers": true
383 }))]));
384 let client = SubstrateChainClient::new(transport, "polkadot");
385 assert!(client.health_check().await.unwrap());
386 }
387
388 #[tokio::test]
389 async fn substrate_metadata() {
390 let transport = Arc::new(MockTransport::new(vec![]));
391 let client = SubstrateChainClient::new(transport, "kusama");
392 assert_eq!(client.chain_id(), "kusama");
393 assert_eq!(client.chain_family(), "substrate");
394 }
395}