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,
|
header: SubstrateHeader,
|
||||||
transactions: Vec<Transaction>,
|
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 {
|
impl sp_core::serde::Serialize for SubstrateBlock {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -27,3 +27,6 @@ borsh = { version = "1", default-features = false, features = ["std"] }
|
|||||||
serai-abi = { path = "../../abi", version = "0.1" }
|
serai-abi = { path = "../../abi", version = "0.1" }
|
||||||
|
|
||||||
async-lock = "3"
|
async-lock = "3"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", default-features = false, features = ["rt", "macros"] }
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
use std::io::Read;
|
use core::future::Future;
|
||||||
|
use std::{sync::Arc, io::Read};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use core_json_traits::{JsonDeserialize, JsonStructure};
|
use core_json_traits::{JsonDeserialize, JsonStructure};
|
||||||
@@ -11,7 +12,10 @@ use simple_request::{hyper, Request, TokioClient};
|
|||||||
|
|
||||||
use borsh::BorshDeserialize;
|
use borsh::BorshDeserialize;
|
||||||
pub use serai_abi as abi;
|
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;
|
use async_lock::RwLock;
|
||||||
|
|
||||||
@@ -23,13 +27,16 @@ pub enum RpcError {
|
|||||||
InternalError(String),
|
InternalError(String),
|
||||||
/// A failure with the connection occurred.
|
/// A failure with the connection occurred.
|
||||||
#[error("failed to communicate with serai")]
|
#[error("failed to communicate with serai")]
|
||||||
ConnectionError,
|
ConnectionError(simple_request::Error),
|
||||||
/// The node provided an invalid response.
|
/// The node provided an invalid response.
|
||||||
#[error("node is faulty: {0}")]
|
#[error("node is faulty: {0}")]
|
||||||
InvalidNode(String),
|
InvalidNode(String),
|
||||||
/// The response contained an error.
|
/// The response contained an error.
|
||||||
#[error("error in response: {0}")]
|
#[error("error in response: {0}")]
|
||||||
ErrorInResponse(String),
|
ErrorInResponse(String),
|
||||||
|
/// The requested block wasn't finalized.
|
||||||
|
#[error("the requested block wasn't finalized")]
|
||||||
|
NotFinalized,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An RPC client to a Serai node.
|
/// 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.
|
/// 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> {
|
pub struct TemporalSerai<'a> {
|
||||||
serai: &'a Serai,
|
serai: &'a Serai,
|
||||||
block: [u8; 32],
|
block: BlockHash,
|
||||||
events: RwLock<Option<Vec<Event>>>,
|
events: Arc<RwLock<Option<Vec<Event>>>>,
|
||||||
}
|
|
||||||
impl Clone for TemporalSerai<'_> {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
Self { serai: self.serai, block: self.block, events: RwLock::new(None) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serai {
|
impl Serai {
|
||||||
@@ -58,7 +65,7 @@ impl Serai {
|
|||||||
params: &str,
|
params: &str,
|
||||||
) -> Result<ResponseValue, RpcError> {
|
) -> Result<ResponseValue, RpcError> {
|
||||||
let request =
|
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)
|
let request = hyper::Request::post(&self.url)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.body(request.as_bytes().to_vec().into())
|
.body(request.as_bytes().to_vec().into())
|
||||||
@@ -78,10 +85,10 @@ impl Serai {
|
|||||||
.client
|
.client
|
||||||
.request(request)
|
.request(request)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| RpcError::ConnectionError)?
|
.map_err(RpcError::ConnectionError)?
|
||||||
.body()
|
.body()
|
||||||
.await
|
.await
|
||||||
.map_err(|_| RpcError::ConnectionError)?;
|
.map_err(RpcError::ConnectionError)?;
|
||||||
let mut response_vec = Vec::with_capacity(1024);
|
let mut response_vec = Vec::with_capacity(1024);
|
||||||
response_reader.read_to_end(&mut response_vec).map_err(|_| {
|
response_reader.read_to_end(&mut response_vec).map_err(|_| {
|
||||||
RpcError::InternalError("couldn't read response from `simple-request` into `Vec`".to_string())
|
RpcError::InternalError("couldn't read response from `simple-request` into `Vec`".to_string())
|
||||||
@@ -108,14 +115,20 @@ impl Serai {
|
|||||||
|
|
||||||
/// Create a new RPC client.
|
/// Create a new RPC client.
|
||||||
pub fn new(url: String) -> Result<Self, RpcError> {
|
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 })
|
Ok(Serai { url, client })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a block from the Serai blockchain.
|
/// Fetch if a block is finalized.
|
||||||
pub async fn block(&self, hash: [u8; 32]) -> Result<serai_abi::Block, RpcError> {
|
pub async fn finalized(&self, block: BlockHash) -> Result<bool, RpcError> {
|
||||||
let bin: String = self.call("serai_block", &format!("[{}]", hex::encode(hash))).await?;
|
self.call("serai_isFinalized", &format!(r#"["{block}"]"#)).await
|
||||||
serai_abi::Block::deserialize(
|
}
|
||||||
|
|
||||||
|
async fn block_internal(
|
||||||
|
block: impl Future<Output = Result<String, RpcError>>,
|
||||||
|
) -> Result<Block, RpcError> {
|
||||||
|
let bin = block.await?;
|
||||||
|
Block::deserialize(
|
||||||
&mut hex::decode(&bin)
|
&mut hex::decode(&bin)
|
||||||
.map_err(|_| RpcError::InvalidNode("node returned non-hex-encoded block".to_string()))?
|
.map_err(|_| RpcError::InvalidNode("node returned non-hex-encoded block".to_string()))?
|
||||||
.as_slice(),
|
.as_slice(),
|
||||||
@@ -123,15 +136,36 @@ impl Serai {
|
|||||||
.map_err(|_| RpcError::InvalidNode("node returned invalid block".to_string()))
|
.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.
|
/// Return the P2P addresses for the validators of the specified network.
|
||||||
pub async fn p2p_validators(&self, network: ExternalNetworkId) -> Result<Vec<String>, RpcError> {
|
pub async fn p2p_validators(&self, network: ExternalNetworkId) -> Result<Vec<String>, RpcError> {
|
||||||
self
|
self
|
||||||
.call(
|
.call(
|
||||||
"p2p_validators",
|
"p2p_validators",
|
||||||
match network {
|
match network {
|
||||||
ExternalNetworkId::Bitcoin => "[bitcoin]",
|
ExternalNetworkId::Bitcoin => r#"["bitcoin"]"#,
|
||||||
ExternalNetworkId::Ethereum => "[ethereum]",
|
ExternalNetworkId::Ethereum => r#"["ethereum"]"#,
|
||||||
ExternalNetworkId::Monero => "[monero]",
|
ExternalNetworkId::Monero => r#"["monero"]"#,
|
||||||
_ => Err(RpcError::InternalError("unrecognized external network ID".to_string()))?,
|
_ => 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-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-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-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-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-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" }
|
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"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
||||||
|
borsh = { version = "1", default-features = false, features = ["std"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
tokio = { version = "1", features = ["sync", "rt-multi-thread"] }
|
tokio = { version = "1", features = ["sync", "rt-multi-thread"] }
|
||||||
jsonrpsee = { version = "0.24", features = ["server"] }
|
jsonrpsee = { version = "0.24", features = ["server"] }
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ use rand_core::{RngCore, OsRng};
|
|||||||
|
|
||||||
use sp_core::Encode;
|
use sp_core::Encode;
|
||||||
use sp_blockchain::{Error as BlockchainError, HeaderMetadata, HeaderBackend};
|
use sp_blockchain::{Error as BlockchainError, HeaderMetadata, HeaderBackend};
|
||||||
|
use sp_consensus::BlockStatus;
|
||||||
use sp_block_builder::BlockBuilder;
|
use sp_block_builder::BlockBuilder;
|
||||||
use sp_api::ProvideRuntimeApi;
|
use sp_api::ProvideRuntimeApi;
|
||||||
|
use sc_client_api::BlockBackend;
|
||||||
|
|
||||||
use serai_abi::{primitives::prelude::*, SubstrateBlock as Block};
|
use serai_abi::{primitives::prelude::*, SubstrateBlock as Block};
|
||||||
use serai_runtime::*;
|
use serai_runtime::*;
|
||||||
|
|
||||||
use jsonrpsee::RpcModule;
|
use jsonrpsee::RpcModule;
|
||||||
|
|
||||||
use sc_client_api::BlockBackend;
|
|
||||||
|
|
||||||
pub(crate) fn module<
|
pub(crate) fn module<
|
||||||
C: 'static
|
C: 'static
|
||||||
+ Send
|
+ Send
|
||||||
@@ -26,7 +26,8 @@ pub(crate) fn module<
|
|||||||
client: Arc<C>,
|
client: Arc<C>,
|
||||||
) -> Result<RpcModule<impl 'static + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<RpcModule<impl 'static + Send + Sync>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let mut module = RpcModule::new(client);
|
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 [block_hash]: [String; 1] = params.parse()?;
|
||||||
let Some(block_hash) = hex::decode(&block_hash).ok().and_then(|bytes| {
|
let Some(block_hash) = hex::decode(&block_hash).ok().and_then(|bytes| {
|
||||||
<[u8; 32]>::try_from(bytes.as_slice())
|
<[u8; 32]>::try_from(bytes.as_slice())
|
||||||
@@ -39,14 +40,69 @@ pub(crate) fn module<
|
|||||||
Option::<()>::None,
|
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(
|
return Err(jsonrpsee::types::error::ErrorObjectOwned::owned(
|
||||||
-2,
|
-2,
|
||||||
"couldn't find requested block",
|
"couldn't find requested block",
|
||||||
Option::<()>::None,
|
Option::<()>::None,
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
Ok(hex::encode(block.block.encode()))
|
|
||||||
|
Ok(hex::encode(borsh::to_vec(&serai_abi::Block::from(block.block)).unwrap()))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(module)
|
Ok(module)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
|
use core::fmt;
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
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
|
// These share encodings as 32-byte arrays
|
||||||
#[cfg(feature = "non_canonical_scale_derivations")]
|
#[cfg(feature = "non_canonical_scale_derivations")]
|
||||||
impl scale::EncodeLike<sp_core::H256> for BlockHash {}
|
impl scale::EncodeLike<sp_core::H256> for BlockHash {}
|
||||||
|
|||||||
Reference in New Issue
Block a user