Expand validator sets API with the rest of the events and some getters

We could've added a storage API, and fetched fields that way, except we want
the storage to be opaque. That meant we needed to add the RPC routes to the
node, which also simplifies other people writing RPC code and fetching these
fields. Then the node could've used the storage API, except a lot of the
storage in validator-sets is marked opaque and to only be read via functions,
so extending the runtime made the most sense.
This commit is contained in:
Luke Parker
2025-11-14 03:35:38 -05:00
parent a793aa18ef
commit f9e3d1b142
11 changed files with 387 additions and 57 deletions

View File

@@ -125,12 +125,12 @@ impl Serai {
/// Fetch the latest finalized block number.
pub async fn latest_finalized_block_number(&self) -> Result<u64, RpcError> {
self.call("serai_latestFinalizedBlockNumber", "[]").await
self.call("blockchain/latest_finalized_block_number", "[]").await
}
/// Fetch if a block is finalized.
pub async fn finalized(&self, block: BlockHash) -> Result<bool, RpcError> {
self.call("serai_isFinalized", &format!(r#"["{block}"]"#)).await
self.call("blockchain/is_finalized", &format!(r#"{{ "block": "{block}" }}"#)).await
}
async fn block_internal(
@@ -147,12 +147,14 @@ impl Serai {
/// 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
Self::block_internal(self.call("blockchain/block", &format!(r#"{{ "block": "{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
Self::block_internal(self.call("blockchain/block", &format!(r#"{{ "block": "{block}" }}"#)))
.await
}
/// Scope this RPC client to the state as of a specific block.
@@ -183,6 +185,14 @@ impl Serai {
}
impl<'a> TemporalSerai<'a> {
async fn call<ResponseValue: Default + JsonDeserialize>(
&self,
method: &str,
params: &str,
) -> Result<ResponseValue, RpcError> {
self.serai.call(method, &format!(r#"{{ "block": "{}", {params} }}"#, self.block)).await
}
/// Fetch the events for this block.
///
/// The returned `Option` will always be `Some(_)`.
@@ -195,8 +205,7 @@ impl<'a> TemporalSerai<'a> {
if events_mut.is_none() {
*events_mut = Some(
self
.serai
.call::<Vec<String>>("serai_events", &format!(r#"["{}"]"#, self.block))
.call::<Vec<String>>("blockchain/events", "")
.await?
.into_iter()
.map(|event| {

View File

@@ -1,6 +1,27 @@
pub use serai_abi::validator_sets::Event;
use borsh::BorshDeserialize;
pub use serai_abi::{
primitives::{
crypto::KeyPair,
network_id::{ExternalNetworkId, NetworkId},
validator_sets::{Session, ExternalValidatorSet, ValidatorSet},
balance::Amount,
},
validator_sets::Event,
};
use crate::{RpcError, TemporalSerai};
fn rpc_network(network: impl Into<NetworkId>) -> Result<&'static str, RpcError> {
Ok(match network.into() {
NetworkId::Serai => r#""serai""#,
NetworkId::External(ExternalNetworkId::Bitcoin) => r#""bitcoin""#,
NetworkId::External(ExternalNetworkId::Ethereum) => r#""ethereum""#,
NetworkId::External(ExternalNetworkId::Monero) => r#""monero""#,
_ => Err(RpcError::InternalError("unrecognized network ID".to_string()))?,
})
}
/// A `TemporalSerai` scoped to the validator sets module.
#[derive(Clone)]
pub struct ValidatorSets<'a>(pub(super) &'a TemporalSerai<'a>);
@@ -36,6 +57,18 @@ impl<'a> ValidatorSets<'a> {
)
}
/// The `SetKeys` events from the validator sets module.
pub async fn set_keys_events(&self) -> Result<Vec<Event>, RpcError> {
Ok(
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::SetKeys { .. }))
.collect(),
)
}
/// The `AcceptedHandover` events from the validator sets module.
pub async fn accepted_handover_events(&self) -> Result<Vec<Event>, RpcError> {
Ok(
@@ -47,4 +80,69 @@ impl<'a> ValidatorSets<'a> {
.collect(),
)
}
/// The `SlashReport` events from the validator sets module.
pub async fn slash_report_events(&self) -> Result<Vec<Event>, RpcError> {
Ok(
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::SlashReport { .. }))
.collect(),
)
}
/// The current session for the specified network.
pub async fn current_session(&self, network: NetworkId) -> Result<Option<Session>, RpcError> {
Ok(
self
.0
.call::<Option<_>>(
"validator-sets/session",
&format!(r#" "network": {} "#, rpc_network(network)?),
)
.await?
.map(Session),
)
}
/// The stake for the current validators for specified network.
pub async fn current_stake(&self, network: NetworkId) -> Result<Option<Amount>, RpcError> {
Ok(
self
.0
.call::<Option<_>>(
"validator-sets/current_stake",
&format!(r#" "network": {} "#, rpc_network(network)?),
)
.await?
.map(Amount),
)
}
/// The keys for the specified validator set.
pub async fn keys(&self, set: ExternalValidatorSet) -> Result<Option<KeyPair>, RpcError> {
let Some(key_pair) = self
.0
.call::<Option<String>>(
"validator-sets/keys",
&format!(
r#" "set": {{ "network": {}, "session": {} }} "#,
rpc_network(set.network)?,
set.session.0
),
)
.await?
else {
return Ok(None);
};
KeyPair::deserialize(
&mut hex::decode(key_pair)
.map_err(|_| RpcError::InvalidNode("validator set's keys weren't valid hex".to_string()))?
.as_slice(),
)
.map(Some)
.map_err(|_| RpcError::InvalidNode("validator set's keys weren't a valid key pair".to_string()))
}
}

View File

@@ -1,7 +1,8 @@
use serai_abi::{
primitives::{
network_id::{ExternalNetworkId, NetworkId},
validator_sets::{Session, ValidatorSet},
balance::Amount,
validator_sets::{Session, ExternalValidatorSet, ValidatorSet},
},
validator_sets::Event,
};
@@ -125,7 +126,24 @@ async fn validator_sets() {
);
}
println!("Finished `serai-client/blockchain` test");
{
let serai =
serai.as_of(serai.block_by_number(0).await.unwrap().header.hash()).await.unwrap();
let serai = serai.validator_sets();
for network in NetworkId::all() {
assert_eq!(serai.current_session(network).await.unwrap(), Some(Session(0)));
assert_eq!(serai.current_stake(network).await.unwrap(), Some(Amount(0)));
match network {
NetworkId::Serai => {}
NetworkId::External(network) => assert_eq!(
serai.keys(ExternalValidatorSet { network, session: Session(0) }).await.unwrap(),
None
),
}
}
}
println!("Finished `serai-client/validator_sets` test");
})
.await;
}