mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Add initial basic tests for serai-client-serai
This commit is contained in:
@@ -286,6 +286,13 @@ mod substrate {
|
||||
header: SubstrateHeader,
|
||||
transactions: Vec<Transaction>,
|
||||
}
|
||||
|
||||
impl From<SubstrateBlock> for Block {
|
||||
fn from(block: SubstrateBlock) -> Self {
|
||||
Self { header: (&block.header).into(), transactions: block.transactions }
|
||||
}
|
||||
}
|
||||
|
||||
impl sp_core::serde::Serialize for SubstrateBlock {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
||||
@@ -27,3 +27,6 @@ borsh = { version = "1", default-features = false, features = ["std"] }
|
||||
serai-abi = { path = "../../abi", version = "0.1" }
|
||||
|
||||
async-lock = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", default-features = false, features = ["rt", "macros"] }
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::io::Read;
|
||||
use core::future::Future;
|
||||
use std::{sync::Arc, io::Read};
|
||||
|
||||
use thiserror::Error;
|
||||
use core_json_traits::{JsonDeserialize, JsonStructure};
|
||||
@@ -11,7 +12,10 @@ use simple_request::{hyper, Request, TokioClient};
|
||||
|
||||
use borsh::BorshDeserialize;
|
||||
pub use serai_abi as abi;
|
||||
use abi::{primitives::network_id::ExternalNetworkId, Event};
|
||||
use abi::{
|
||||
primitives::{BlockHash, network_id::ExternalNetworkId},
|
||||
Block, Event,
|
||||
};
|
||||
|
||||
use async_lock::RwLock;
|
||||
|
||||
@@ -23,13 +27,16 @@ pub enum RpcError {
|
||||
InternalError(String),
|
||||
/// A failure with the connection occurred.
|
||||
#[error("failed to communicate with serai")]
|
||||
ConnectionError,
|
||||
ConnectionError(simple_request::Error),
|
||||
/// The node provided an invalid response.
|
||||
#[error("node is faulty: {0}")]
|
||||
InvalidNode(String),
|
||||
/// The response contained an error.
|
||||
#[error("error in response: {0}")]
|
||||
ErrorInResponse(String),
|
||||
/// The requested block wasn't finalized.
|
||||
#[error("the requested block wasn't finalized")]
|
||||
NotFinalized,
|
||||
}
|
||||
|
||||
/// An RPC client to a Serai node.
|
||||
@@ -40,15 +47,15 @@ pub struct Serai {
|
||||
}
|
||||
|
||||
/// An RPC client to a Serai node, scoped to a specific block.
|
||||
///
|
||||
/// Upon any request being made for the events emitted by this block, the entire list of events
|
||||
/// from this block will be cached within this. This allows future calls for events to be done
|
||||
/// cheaply.
|
||||
#[derive(Clone)]
|
||||
pub struct TemporalSerai<'a> {
|
||||
serai: &'a Serai,
|
||||
block: [u8; 32],
|
||||
events: RwLock<Option<Vec<Event>>>,
|
||||
}
|
||||
impl Clone for TemporalSerai<'_> {
|
||||
fn clone(&self) -> Self {
|
||||
Self { serai: self.serai, block: self.block, events: RwLock::new(None) }
|
||||
}
|
||||
block: BlockHash,
|
||||
events: Arc<RwLock<Option<Vec<Event>>>>,
|
||||
}
|
||||
|
||||
impl Serai {
|
||||
@@ -58,7 +65,7 @@ impl Serai {
|
||||
params: &str,
|
||||
) -> Result<ResponseValue, RpcError> {
|
||||
let request =
|
||||
format!(r#"{{ "jsonrpc": "2.0", "id": 0, "method": {method}, "params": {params} }}"#);
|
||||
format!(r#"{{ "jsonrpc": "2.0", "id": 0, "method": "{method}", "params": {params} }}"#);
|
||||
let request = hyper::Request::post(&self.url)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(request.as_bytes().to_vec().into())
|
||||
@@ -78,10 +85,10 @@ impl Serai {
|
||||
.client
|
||||
.request(request)
|
||||
.await
|
||||
.map_err(|_| RpcError::ConnectionError)?
|
||||
.map_err(RpcError::ConnectionError)?
|
||||
.body()
|
||||
.await
|
||||
.map_err(|_| RpcError::ConnectionError)?;
|
||||
.map_err(RpcError::ConnectionError)?;
|
||||
let mut response_vec = Vec::with_capacity(1024);
|
||||
response_reader.read_to_end(&mut response_vec).map_err(|_| {
|
||||
RpcError::InternalError("couldn't read response from `simple-request` into `Vec`".to_string())
|
||||
@@ -108,14 +115,20 @@ impl Serai {
|
||||
|
||||
/// Create a new RPC client.
|
||||
pub fn new(url: String) -> Result<Self, RpcError> {
|
||||
let client = TokioClient::with_connection_pool().map_err(|_| RpcError::ConnectionError)?;
|
||||
let client = TokioClient::with_connection_pool().map_err(RpcError::ConnectionError)?;
|
||||
Ok(Serai { url, client })
|
||||
}
|
||||
|
||||
/// Fetch a block from the Serai blockchain.
|
||||
pub async fn block(&self, hash: [u8; 32]) -> Result<serai_abi::Block, RpcError> {
|
||||
let bin: String = self.call("serai_block", &format!("[{}]", hex::encode(hash))).await?;
|
||||
serai_abi::Block::deserialize(
|
||||
/// Fetch if a block is finalized.
|
||||
pub async fn finalized(&self, block: BlockHash) -> Result<bool, RpcError> {
|
||||
self.call("serai_isFinalized", &format!(r#"["{block}"]"#)).await
|
||||
}
|
||||
|
||||
async fn block_internal(
|
||||
block: impl Future<Output = Result<String, RpcError>>,
|
||||
) -> Result<Block, RpcError> {
|
||||
let bin = block.await?;
|
||||
Block::deserialize(
|
||||
&mut hex::decode(&bin)
|
||||
.map_err(|_| RpcError::InvalidNode("node returned non-hex-encoded block".to_string()))?
|
||||
.as_slice(),
|
||||
@@ -123,15 +136,36 @@ impl Serai {
|
||||
.map_err(|_| RpcError::InvalidNode("node returned invalid block".to_string()))
|
||||
}
|
||||
|
||||
/// Fetch a block from the Serai blockchain.
|
||||
pub async fn block(&self, block: BlockHash) -> Result<Block, RpcError> {
|
||||
Self::block_internal(self.call("serai_block", &format!(r#"["{block}"]"#))).await
|
||||
}
|
||||
|
||||
/// Fetch a block from the Serai blockchain by its number.
|
||||
pub async fn block_by_number(&self, block: u64) -> Result<Block, RpcError> {
|
||||
Self::block_internal(self.call("serai_block", &format!("[{block}]"))).await
|
||||
}
|
||||
|
||||
/// Scope this RPC client to the state as of specific block.
|
||||
///
|
||||
/// This will yield an error if the block chosen isn't finalized. This ensures, given an honest
|
||||
/// node, that this scope will be available for the lifetime of this object.
|
||||
pub async fn at<'a>(&'a self, block: BlockHash) -> Result<TemporalSerai<'a>, RpcError> {
|
||||
if !self.finalized(block).await? {
|
||||
Err(RpcError::NotFinalized)?;
|
||||
}
|
||||
Ok(TemporalSerai { serai: self, block, events: Arc::new(RwLock::new(None)) })
|
||||
}
|
||||
|
||||
/// Return the P2P addresses for the validators of the specified network.
|
||||
pub async fn p2p_validators(&self, network: ExternalNetworkId) -> Result<Vec<String>, RpcError> {
|
||||
self
|
||||
.call(
|
||||
"p2p_validators",
|
||||
match network {
|
||||
ExternalNetworkId::Bitcoin => "[bitcoin]",
|
||||
ExternalNetworkId::Ethereum => "[ethereum]",
|
||||
ExternalNetworkId::Monero => "[monero]",
|
||||
ExternalNetworkId::Bitcoin => r#"["bitcoin"]"#,
|
||||
ExternalNetworkId::Ethereum => r#"["ethereum"]"#,
|
||||
ExternalNetworkId::Monero => r#"["monero"]"#,
|
||||
_ => Err(RpcError::InternalError("unrecognized external network ID".to_string()))?,
|
||||
},
|
||||
)
|
||||
|
||||
9
substrate/client/serai/tests/blockchain.rs
Normal file
9
substrate/client/serai/tests/blockchain.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use serai_client_serai::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn main() {
|
||||
let serai = Serai::new("http://127.0.0.1:9944".to_string()).unwrap();
|
||||
let block = serai.block_by_number(0).await.unwrap();
|
||||
assert_eq!(serai.block(block.header.hash()).await.unwrap(), block);
|
||||
assert!(serai.finalized(block.header.hash()).await.unwrap());
|
||||
}
|
||||
@@ -38,6 +38,7 @@ sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "b48e
|
||||
sp-inherents = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "b48efb12bf49a0dba1b3633403f716010b99dc56" }
|
||||
sp-timestamp = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "b48efb12bf49a0dba1b3633403f716010b99dc56" }
|
||||
sp-blockchain = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "b48efb12bf49a0dba1b3633403f716010b99dc56" }
|
||||
sp-consensus = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "b48efb12bf49a0dba1b3633403f716010b99dc56" }
|
||||
sp-state-machine = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "b48efb12bf49a0dba1b3633403f716010b99dc56" }
|
||||
sp-api = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "b48efb12bf49a0dba1b3633403f716010b99dc56" }
|
||||
sp-keystore = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "b48efb12bf49a0dba1b3633403f716010b99dc56" }
|
||||
@@ -53,6 +54,7 @@ serai-runtime = { path = "../runtime", features = ["std"] }
|
||||
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
borsh = { version = "1", default-features = false, features = ["std"] }
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1", features = ["sync", "rt-multi-thread"] }
|
||||
jsonrpsee = { version = "0.24", features = ["server"] }
|
||||
|
||||
@@ -4,16 +4,16 @@ use rand_core::{RngCore, OsRng};
|
||||
|
||||
use sp_core::Encode;
|
||||
use sp_blockchain::{Error as BlockchainError, HeaderMetadata, HeaderBackend};
|
||||
use sp_consensus::BlockStatus;
|
||||
use sp_block_builder::BlockBuilder;
|
||||
use sp_api::ProvideRuntimeApi;
|
||||
use sc_client_api::BlockBackend;
|
||||
|
||||
use serai_abi::{primitives::prelude::*, SubstrateBlock as Block};
|
||||
use serai_runtime::*;
|
||||
|
||||
use jsonrpsee::RpcModule;
|
||||
|
||||
use sc_client_api::BlockBackend;
|
||||
|
||||
pub(crate) fn module<
|
||||
C: 'static
|
||||
+ Send
|
||||
@@ -26,7 +26,8 @@ pub(crate) fn module<
|
||||
client: Arc<C>,
|
||||
) -> Result<RpcModule<impl 'static + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut module = RpcModule::new(client);
|
||||
module.register_async_method("serai_block", |params, client, _ext| async move {
|
||||
|
||||
module.register_async_method("serai_isFinalized", |params, client, _ext| async move {
|
||||
let [block_hash]: [String; 1] = params.parse()?;
|
||||
let Some(block_hash) = hex::decode(&block_hash).ok().and_then(|bytes| {
|
||||
<[u8; 32]>::try_from(bytes.as_slice())
|
||||
@@ -39,14 +40,69 @@ pub(crate) fn module<
|
||||
Option::<()>::None,
|
||||
));
|
||||
};
|
||||
let Some(block) = client.block(block_hash).ok().flatten() else {
|
||||
let finalized = client.info().finalized_number;
|
||||
let Ok(Some(number)) = client.number(block_hash) else {
|
||||
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-2,
|
||||
"failed to fetch block's number",
|
||||
Option::<()>::None,
|
||||
));
|
||||
};
|
||||
let Ok(status) = client.block_status(block_hash) else {
|
||||
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-3,
|
||||
"failed to fetch block's status",
|
||||
Option::<()>::None,
|
||||
));
|
||||
};
|
||||
Ok(
|
||||
matches!(status, BlockStatus::InChainWithState | BlockStatus::InChainPruned) &&
|
||||
(number <= finalized),
|
||||
)
|
||||
})?;
|
||||
|
||||
module.register_async_method("serai_block", |params, client, _ext| async move {
|
||||
let block_hash = if let Ok([block_hash]) = params.parse::<[String; 1]>() {
|
||||
let Some(block_hash) = hex::decode(&block_hash).ok().and_then(|bytes| {
|
||||
<[u8; 32]>::try_from(bytes.as_slice())
|
||||
.map(<Block as sp_runtime::traits::Block>::Hash::from)
|
||||
.ok()
|
||||
}) else {
|
||||
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-1,
|
||||
"requested block hash wasn't a valid hash",
|
||||
Option::<()>::None,
|
||||
));
|
||||
};
|
||||
block_hash
|
||||
} else {
|
||||
let Ok([block_number]) = params.parse::<[u64; 1]>() else {
|
||||
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-1,
|
||||
"requested block wasn't a valid hash nor number",
|
||||
Option::<()>::None,
|
||||
));
|
||||
};
|
||||
let Ok(Some(block_hash)) = client.block_hash(block_number) else {
|
||||
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-2,
|
||||
"couldn't find requested block's hash",
|
||||
Option::<()>::None,
|
||||
));
|
||||
};
|
||||
block_hash
|
||||
};
|
||||
|
||||
let Ok(Some(block)) = client.block(block_hash) else {
|
||||
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||
-2,
|
||||
"couldn't find requested block",
|
||||
Option::<()>::None,
|
||||
));
|
||||
};
|
||||
Ok(hex::encode(block.block.encode()))
|
||||
|
||||
Ok(hex::encode(borsh::to_vec(&serai_abi::Block::from(block.block)).unwrap()))
|
||||
})?;
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use core::fmt;
|
||||
extern crate alloc;
|
||||
|
||||
use zeroize::Zeroize;
|
||||
@@ -82,6 +83,15 @@ impl From<sp_core::H256> for BlockHash {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BlockHash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for byte in self.0 {
|
||||
write!(f, "{byte:02x}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// These share encodings as 32-byte arrays
|
||||
#[cfg(feature = "non_canonical_scale_derivations")]
|
||||
impl scale::EncodeLike<sp_core::H256> for BlockHash {}
|
||||
|
||||
Reference in New Issue
Block a user