use std::sync::OnceLock;
use crate::chain::ChainId;
fn http_endpoints(chain: ChainId) -> &'static [&'static str] {
match chain {
ChainId::Individuality => &["https://pop3-testnet.parity-lab.parity.io/people"],
ChainId::PaseoPeople => &[
"https://people-paseo.dotters.network",
"https://sys.ibp.network/people-paseo",
],
_ => &[],
}
}
fn shared_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(|| {
#[cfg(not(target_arch = "wasm32"))]
{
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.expect("failed to build reqwest client")
}
#[cfg(target_arch = "wasm32")]
{
reqwest::Client::new()
}
})
}
async fn http_rpc(request: &str, endpoints: &[&str]) -> Result<String, String> {
let client = shared_client();
for endpoint in endpoints {
log::info!("[registration] trying RPC endpoint: {endpoint}");
let result = client
.post(*endpoint)
.header("Content-Type", "application/json")
.body(request.to_owned())
.send()
.await;
match result {
Ok(resp) => {
let bytes = match resp.bytes().await {
Ok(b) => b,
Err(e) => {
log::warn!("[registration] failed to read response from {endpoint}: {e}");
continue;
}
};
match String::from_utf8(bytes.to_vec()) {
Ok(s) => return Ok(s),
Err(e) => {
log::warn!("[registration] non-UTF-8 response from {endpoint}: {e}");
continue;
}
}
}
Err(e) => {
log::warn!("[registration] HTTP error for {endpoint}: {e}");
continue;
}
}
}
Err("all RPC endpoints failed for registration".into())
}
pub async fn query_runtime_version(chain: ChainId) -> Result<(u32, u32), String> {
let endpoints = http_endpoints(chain);
if endpoints.is_empty() {
return Err(format!("no HTTP endpoint for chain {chain:?}"));
}
let req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "state_getRuntimeVersion",
"params": []
})
.to_string();
let resp = http_rpc(&req, endpoints).await?;
let body: serde_json::Value =
serde_json::from_str(&resp).map_err(|e| format!("parse runtime version: {e}"))?;
let result = body.get("result").ok_or("no result in getRuntimeVersion")?;
let spec = result
.get("specVersion")
.and_then(|v| v.as_u64())
.ok_or("no specVersion")? as u32;
let tx = result
.get("transactionVersion")
.and_then(|v| v.as_u64())
.ok_or("no transactionVersion")? as u32;
Ok((spec, tx))
}
pub async fn query_block_hash(
chain: ChainId,
block_number: Option<u64>,
) -> Result<[u8; 32], String> {
let endpoints = http_endpoints(chain);
let params = match block_number {
Some(n) => serde_json::json!([n]),
None => serde_json::json!([]),
};
let req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "chain_getBlockHash",
"params": params
})
.to_string();
let resp = http_rpc(&req, endpoints).await?;
let body: serde_json::Value =
serde_json::from_str(&resp).map_err(|e| format!("parse block hash: {e}"))?;
let hex_str = body
.get("result")
.and_then(|v| v.as_str())
.ok_or("no result in getBlockHash")?;
let bytes = host_encoding::hex_decode(hex_str).ok_or("invalid hex in block hash")?;
if bytes.len() != 32 {
return Err(format!("block hash wrong length: {}", bytes.len()));
}
let mut hash = [0u8; 32];
hash.copy_from_slice(&bytes);
Ok(hash)
}
pub async fn query_account_nonce(chain: ChainId, ss58_address: &str) -> Result<u32, String> {
let endpoints = http_endpoints(chain);
let req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "system_accountNextIndex",
"params": [ss58_address]
})
.to_string();
let resp = http_rpc(&req, endpoints).await?;
let body: serde_json::Value =
serde_json::from_str(&resp).map_err(|e| format!("parse nonce response: {e}"))?;
let nonce = body
.get("result")
.and_then(|v| v.as_u64())
.ok_or("no result in accountNextIndex")? as u32;
Ok(nonce)
}
pub async fn query_raw_metadata(chain: ChainId) -> Result<Vec<u8>, String> {
let endpoints = http_endpoints(chain);
if endpoints.is_empty() {
return Err(format!("no HTTP endpoint for chain {chain:?}"));
}
let req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "state_getMetadata",
"params": []
})
.to_string();
let resp = http_rpc(&req, endpoints).await?;
let body: serde_json::Value =
serde_json::from_str(&resp).map_err(|e| format!("parse metadata: {e}"))?;
let hex = body
.get("result")
.and_then(|v| v.as_str())
.ok_or("no result in getMetadata")?;
host_encoding::hex_decode(hex).ok_or_else(|| "invalid hex in metadata".to_string())
}
pub async fn query_storage(chain: ChainId, key_hex: &str) -> Result<Option<String>, String> {
let endpoints = http_endpoints(chain);
let req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "state_getStorage",
"params": [key_hex]
})
.to_string();
let resp = http_rpc(&req, endpoints).await?;
let body: serde_json::Value =
serde_json::from_str(&resp).map_err(|e| format!("parse storage response: {e}"))?;
match body.get("result") {
Some(serde_json::Value::Null) | None => Ok(None),
Some(serde_json::Value::String(s)) => Ok(Some(s.clone())),
Some(other) => Ok(Some(other.to_string())),
}
}
pub async fn submit_extrinsic(chain: ChainId, extrinsic_hex: &str) -> Result<String, String> {
let endpoints = http_endpoints(chain);
let req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "author_submitExtrinsic",
"params": [extrinsic_hex]
})
.to_string();
let resp = http_rpc(&req, endpoints).await?;
let body: serde_json::Value =
serde_json::from_str(&resp).map_err(|e| format!("parse submit response: {e}"))?;
if let Some(err) = body.get("error") {
return Err(format!("extrinsic submission failed: {err}"));
}
let tx_hash = body
.get("result")
.and_then(|v| v.as_str())
.unwrap_or("(no hash)")
.to_string();
Ok(tx_hash)
}
pub async fn poll_storage_once(chain: ChainId, key_hex: &str) -> Result<Option<String>, String> {
query_storage(chain, key_hex).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_endpoints_individuality() {
let eps = http_endpoints(ChainId::Individuality);
assert!(!eps.is_empty());
assert!(eps[0].contains("pop3-testnet"));
}
#[test]
fn test_endpoints_unsupported_chain() {
let eps = http_endpoints(ChainId::PolkadotAssetHub);
assert!(eps.is_empty());
}
}