Update serai-client to solely be an umbrella crate of the dedicated client libraries

This commit is contained in:
Luke Parker
2025-11-16 12:03:29 -05:00
parent 302a43653f
commit df4aee2d59
29 changed files with 24 additions and 3300 deletions

View File

@@ -1,12 +1,17 @@
#[cfg(feature = "bitcoin")]
pub use serai_client_bitcoin as bitcoin;
#[cfg(feature = "ethereum")]
pub mod serai_client_ethereum as ethereum;
#[cfg(feature = "monero")]
pub mod serai_client_monero as monero;
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
/// The client for the Serai network itself.
#[cfg(feature = "serai")]
pub use serai_client_serai as serai;
#[cfg(test)]
mod tests;
/// The client for the Bitcoin integration.
#[cfg(feature = "bitcoin")]
pub use serai_client_bitcoin as bitcoin;
/// The client for the Ethereum integration.
#[cfg(feature = "ethereum")]
pub use serai_client_ethereum as ethereum;
/// The client for the Monero integration.
#[cfg(feature = "monero")]
pub use serai_client_monero as monero;

View File

@@ -1,83 +0,0 @@
use scale::Encode;
use serai_abi::primitives::{SeraiAddress, Amount, Coin, Balance};
pub use serai_abi::coins::primitives;
use primitives::OutInstructionWithBalance;
use crate::{TemporalSerai, SeraiError};
const PALLET: &str = "Coins";
pub type CoinsEvent = serai_abi::coins::Event;
#[derive(Clone, Copy)]
pub struct SeraiCoins<'a>(pub(crate) &'a TemporalSerai<'a>);
impl SeraiCoins<'_> {
pub async fn mint_events(&self) -> Result<Vec<CoinsEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::Coins(event) = event {
if matches!(event, CoinsEvent::Mint { .. }) {
Some(event.clone())
} else {
None
}
} else {
None
}
})
.await
}
pub async fn burn_with_instruction_events(&self) -> Result<Vec<CoinsEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::Coins(event) = event {
if matches!(event, CoinsEvent::BurnWithInstruction { .. }) {
Some(event.clone())
} else {
None
}
} else {
None
}
})
.await
}
pub async fn coin_supply(&self, coin: Coin) -> Result<Amount, SeraiError> {
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(Amount(0)))
}
pub async fn coin_balance(
&self,
coin: Coin,
address: SeraiAddress,
) -> Result<Amount, SeraiError> {
Ok(
self
.0
.storage(
PALLET,
"Balances",
(sp_core::hashing::blake2_128(&address.encode()), &address.0, coin),
)
.await?
.unwrap_or(Amount(0)),
)
}
pub fn transfer(to: SeraiAddress, balance: Balance) -> serai_abi::Call {
serai_abi::Call::Coins(serai_abi::coins::Call::transfer { to, balance })
}
pub fn burn(balance: Balance) -> serai_abi::Call {
serai_abi::Call::Coins(serai_abi::coins::Call::burn { balance })
}
pub fn burn_with_instruction(instruction: OutInstructionWithBalance) -> serai_abi::Call {
serai_abi::Call::Coins(serai_abi::coins::Call::burn_with_instruction { instruction })
}
}

View File

@@ -1,74 +0,0 @@
use sp_core::bounded::BoundedVec;
use serai_abi::primitives::{Amount, Coin, ExternalCoin, SeraiAddress};
use crate::{SeraiError, TemporalSerai};
pub type DexEvent = serai_abi::dex::Event;
const PALLET: &str = "Dex";
#[derive(Clone, Copy)]
pub struct SeraiDex<'a>(pub(crate) &'a TemporalSerai<'a>);
impl SeraiDex<'_> {
pub async fn events(&self) -> Result<Vec<DexEvent>, SeraiError> {
self
.0
.events(
|event| if let serai_abi::Event::Dex(event) = event { Some(event.clone()) } else { None },
)
.await
}
pub fn add_liquidity(
coin: ExternalCoin,
coin_amount: Amount,
sri_amount: Amount,
min_coin_amount: Amount,
min_sri_amount: Amount,
address: SeraiAddress,
) -> serai_abi::Call {
serai_abi::Call::Dex(serai_abi::dex::Call::add_liquidity {
coin,
coin_desired: coin_amount.0,
sri_desired: sri_amount.0,
coin_min: min_coin_amount.0,
sri_min: min_sri_amount.0,
mint_to: address,
})
}
pub fn swap(
from_coin: Coin,
to_coin: Coin,
amount_in: Amount,
amount_out_min: Amount,
address: SeraiAddress,
) -> serai_abi::Call {
let path = if to_coin.is_native() {
BoundedVec::try_from(vec![from_coin, Coin::Serai]).unwrap()
} else if from_coin.is_native() {
BoundedVec::try_from(vec![Coin::Serai, to_coin]).unwrap()
} else {
BoundedVec::try_from(vec![from_coin, Coin::Serai, to_coin]).unwrap()
};
serai_abi::Call::Dex(serai_abi::dex::Call::swap_exact_tokens_for_tokens {
path,
amount_in: amount_in.0,
amount_out_min: amount_out_min.0,
send_to: address,
})
}
/// Returns the reserves of `coin:SRI` pool.
pub async fn get_reserves(
&self,
coin: ExternalCoin,
) -> Result<Option<(Amount, Amount)>, SeraiError> {
self.0.runtime_api("DexApi_get_reserves", (Coin::from(coin), Coin::Serai)).await
}
pub async fn oracle_value(&self, coin: ExternalCoin) -> Result<Option<Amount>, SeraiError> {
self.0.storage(PALLET, "SecurityOracleValue", coin).await
}
}

View File

@@ -1,69 +0,0 @@
pub use serai_abi::genesis_liquidity::primitives;
use primitives::{Values, LiquidityAmount};
use serai_abi::primitives::*;
use sp_core::sr25519::Signature;
use scale::Encode;
use crate::{Serai, SeraiError, TemporalSerai, Transaction};
pub type GenesisLiquidityEvent = serai_abi::genesis_liquidity::Event;
const PALLET: &str = "GenesisLiquidity";
#[derive(Clone, Copy)]
pub struct SeraiGenesisLiquidity<'a>(pub(crate) &'a TemporalSerai<'a>);
impl SeraiGenesisLiquidity<'_> {
pub async fn events(&self) -> Result<Vec<GenesisLiquidityEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::GenesisLiquidity(event) = event {
Some(event.clone())
} else {
None
}
})
.await
}
pub fn oraclize_values(values: Values, signature: Signature) -> Transaction {
Serai::unsigned(serai_abi::Call::GenesisLiquidity(
serai_abi::genesis_liquidity::Call::oraclize_values { values, signature },
))
}
pub fn remove_coin_liquidity(balance: ExternalBalance) -> serai_abi::Call {
serai_abi::Call::GenesisLiquidity(serai_abi::genesis_liquidity::Call::remove_coin_liquidity {
balance,
})
}
pub async fn liquidity(
&self,
address: &SeraiAddress,
coin: ExternalCoin,
) -> Result<LiquidityAmount, SeraiError> {
Ok(
self
.0
.storage(
PALLET,
"Liquidity",
(coin, sp_core::hashing::blake2_128(&address.encode()), &address.0),
)
.await?
.unwrap_or(LiquidityAmount::zero()),
)
}
pub async fn supply(&self, coin: ExternalCoin) -> Result<LiquidityAmount, SeraiError> {
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero()))
}
pub async fn genesis_complete_block(&self) -> Result<Option<u64>, SeraiError> {
self.0.storage(PALLET, "GenesisCompleteBlock", ()).await
}
}

View File

@@ -1,42 +0,0 @@
pub use serai_abi::in_instructions::primitives;
use primitives::SignedBatch;
use crate::{primitives::ExternalNetworkId, Transaction, SeraiError, Serai, TemporalSerai};
pub type InInstructionsEvent = serai_abi::in_instructions::Event;
const PALLET: &str = "InInstructions";
#[derive(Clone, Copy)]
pub struct SeraiInInstructions<'a>(pub(crate) &'a TemporalSerai<'a>);
impl SeraiInInstructions<'_> {
pub async fn last_batch_for_network(
&self,
network: ExternalNetworkId,
) -> Result<Option<u32>, SeraiError> {
self.0.storage(PALLET, "LastBatch", network).await
}
pub async fn batch_events(&self) -> Result<Vec<InInstructionsEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::InInstructions(event) = event {
if matches!(event, InInstructionsEvent::Batch { .. }) {
Some(event.clone())
} else {
None
}
} else {
None
}
})
.await
}
pub fn execute_batch(batch: SignedBatch) -> Transaction {
Serai::unsigned(serai_abi::Call::InInstructions(
serai_abi::in_instructions::Call::execute_batch { batch },
))
}
}

View File

@@ -1,46 +0,0 @@
use scale::Encode;
use serai_abi::primitives::{Amount, ExternalBalance, ExternalCoin, SeraiAddress};
use crate::{TemporalSerai, SeraiError};
const PALLET: &str = "LiquidityTokens";
#[derive(Clone, Copy)]
pub struct SeraiLiquidityTokens<'a>(pub(crate) &'a TemporalSerai<'a>);
impl SeraiLiquidityTokens<'_> {
pub async fn token_supply(&self, coin: ExternalCoin) -> Result<Amount, SeraiError> {
Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(Amount(0)))
}
pub async fn token_balance(
&self,
coin: ExternalCoin,
address: SeraiAddress,
) -> Result<Amount, SeraiError> {
Ok(
self
.0
.storage(
PALLET,
"Balances",
(sp_core::hashing::blake2_128(&address.encode()), &address.0, coin),
)
.await?
.unwrap_or(Amount(0)),
)
}
pub fn transfer(to: SeraiAddress, balance: ExternalBalance) -> serai_abi::Call {
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::transfer {
to,
balance: balance.into(),
})
}
pub fn burn(balance: ExternalBalance) -> serai_abi::Call {
serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::burn {
balance: balance.into(),
})
}
}

View File

@@ -1,434 +0,0 @@
use thiserror::Error;
use async_lock::RwLock;
use simple_request::{hyper, Request, Client};
use scale::{Decode, Encode};
use serde::{Serialize, Deserialize, de::DeserializeOwned};
pub use sp_core::{
Pair as PairTrait,
sr25519::{Public, Pair},
};
pub use serai_abi as abi;
pub use abi::{primitives, Transaction};
use abi::*;
pub use primitives::{SeraiAddress, Signature, Amount};
use primitives::{Header, ExternalNetworkId, QuotePriceParams};
use crate::in_instructions::primitives::Shorthand;
pub mod coins;
pub use coins::SeraiCoins;
pub mod dex;
pub use dex::SeraiDex;
pub mod in_instructions;
pub use in_instructions::SeraiInInstructions;
pub mod validator_sets;
pub use validator_sets::SeraiValidatorSets;
pub mod genesis_liquidity;
pub use genesis_liquidity::SeraiGenesisLiquidity;
pub mod liquidity_tokens;
pub use liquidity_tokens::SeraiLiquidityTokens;
#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode)]
pub struct Block {
pub header: Header,
pub transactions: Vec<Transaction>,
}
impl Block {
pub fn hash(&self) -> [u8; 32] {
self.header.hash().into()
}
pub fn number(&self) -> u64 {
self.header.number
}
/// Returns the time of this block, set by its producer, in milliseconds since the epoch.
pub fn time(&self) -> Option<u64> {
for transaction in &self.transactions {
if let Call::Timestamp(timestamp::Call::set { now }) = transaction.call() {
return Some(*now);
}
}
None
}
}
#[derive(Debug, Error)]
pub enum SeraiError {
#[error("failed to communicate with serai")]
ConnectionError,
#[error("node is faulty: {0}")]
InvalidNode(String),
#[error("error in response: {0}")]
ErrorInResponse(String),
#[error("serai-client library was intended for a different runtime version: {0}")]
InvalidRuntime(String),
}
#[derive(Clone)]
pub struct Serai {
url: String,
client: Client,
genesis: [u8; 32],
}
type EventsInBlock = Vec<frame_system::EventRecord<Event, [u8; 32]>>;
pub struct TemporalSerai<'a> {
serai: &'a Serai,
block: [u8; 32],
events: RwLock<Option<EventsInBlock>>,
}
impl Clone for TemporalSerai<'_> {
fn clone(&self) -> Self {
Self { serai: self.serai, block: self.block, events: RwLock::new(None) }
}
}
impl Serai {
pub async fn call<Req: Serialize, Res: DeserializeOwned>(
&self,
method: &str,
params: Req,
) -> Result<Res, SeraiError> {
let request = Request::from(
hyper::Request::post(&self.url)
.header("Content-Type", "application/json")
.body(
serde_json::to_vec(
&serde_json::json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }),
)
.unwrap()
.into(),
)
.unwrap(),
);
#[derive(Deserialize)]
pub struct Error {
message: String,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum RpcResponse<T> {
Ok { result: T },
Err { error: Error },
}
let mut res = self
.client
.request(request)
.await
.map_err(|_| SeraiError::ConnectionError)?
.body()
.await
.map_err(|_| SeraiError::ConnectionError)?;
let res: RpcResponse<Res> = serde_json::from_reader(&mut res).map_err(|e| {
SeraiError::InvalidRuntime(format!(
"response was a different type than expected: {:?}",
e.classify()
))
})?;
match res {
RpcResponse::Ok { result } => Ok(result),
RpcResponse::Err { error } => Err(SeraiError::ErrorInResponse(error.message)),
}
}
fn hex_decode(str: String) -> Result<Vec<u8>, SeraiError> {
(if let Some(stripped) = str.strip_prefix("0x") {
hex::decode(stripped)
} else {
hex::decode(str)
})
.map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))
}
pub async fn block_hash(&self, number: u64) -> Result<Option<[u8; 32]>, SeraiError> {
let hash: Option<String> = self.call("chain_getBlockHash", [number]).await?;
let Some(hash) = hash else { return Ok(None) };
Self::hex_decode(hash)?
.try_into()
.map_err(|_| SeraiError::InvalidNode("didn't respond to getBlockHash with hash".to_string()))
.map(Some)
}
pub async fn new(url: String) -> Result<Self, SeraiError> {
let client = Client::with_connection_pool().map_err(|_| SeraiError::ConnectionError)?;
let mut res = Serai { url, client, genesis: [0xfe; 32] };
res.genesis = res.block_hash(0).await?.ok_or_else(|| {
SeraiError::InvalidNode("node didn't have the first block's hash".to_string())
})?;
Ok(res)
}
fn unsigned(call: Call) -> Transaction {
Transaction::new(call, None)
}
pub fn sign(&self, signer: &Pair, call: Call, nonce: u32, tip: u64) -> Transaction {
const SPEC_VERSION: u32 = 1;
const TX_VERSION: u32 = 1;
let extra = Extra { era: sp_runtime::generic::Era::Immortal, nonce, tip };
let signature_payload = (
&call,
&extra,
SignedPayloadExtra {
spec_version: SPEC_VERSION,
tx_version: TX_VERSION,
genesis: self.genesis,
mortality_checkpoint: self.genesis,
},
)
.encode();
let signature = signer.sign(&signature_payload);
Transaction::new(call, Some((signer.public().into(), signature, extra)))
}
pub async fn publish(&self, tx: &Transaction) -> Result<(), SeraiError> {
// Drop the returned hash, which is the hash of the raw extrinsic, as extrinsics are allowed
// to share hashes and this hash is accordingly useless/unsafe
// If we are to return something, it should be block included in and position within block
let _: String = self.call("author_submitExtrinsic", [hex::encode(tx.encode())]).await?;
Ok(())
}
pub async fn latest_finalized_block_hash(&self) -> Result<[u8; 32], SeraiError> {
let hash: String = self.call("chain_getFinalizedHead", ()).await?;
Self::hex_decode(hash)?.try_into().map_err(|_| {
SeraiError::InvalidNode("didn't respond to getFinalizedHead with hash".to_string())
})
}
pub async fn header(&self, hash: [u8; 32]) -> Result<Option<Header>, SeraiError> {
self.call("chain_getHeader", [hex::encode(hash)]).await
}
pub async fn block(&self, hash: [u8; 32]) -> Result<Option<Block>, SeraiError> {
let block: Option<String> = self.call("chain_getBlockBin", [hex::encode(hash)]).await?;
let Some(block) = block else { return Ok(None) };
let Ok(bytes) = Self::hex_decode(block) else {
Err(SeraiError::InvalidNode("didn't return a hex-encoded block".to_string()))?
};
let Ok(block) = Block::decode(&mut bytes.as_slice()) else {
Err(SeraiError::InvalidNode("didn't return a block".to_string()))?
};
Ok(Some(block))
}
pub async fn latest_finalized_block(&self) -> Result<Block, SeraiError> {
let latest = self.latest_finalized_block_hash().await?;
let Some(block) = self.block(latest).await? else {
Err(SeraiError::InvalidNode("node didn't have a latest block".to_string()))?
};
Ok(block)
}
// There is no provided method for this
// TODO: Add one to Serai
pub async fn is_finalized(&self, header: &Header) -> Result<bool, SeraiError> {
// Get the latest finalized block
let finalized = self.latest_finalized_block_hash().await?;
// If the latest finalized block is this block, return true
if finalized == header.hash().as_ref() {
return Ok(true);
}
let Some(finalized) = self.header(finalized).await? else {
Err(SeraiError::InvalidNode("couldn't get finalized header".to_string()))?
};
// If the finalized block has a lower number, this block can't be finalized
if finalized.number < header.number {
return Ok(false);
}
// This block, if finalized, comes before the finalized block
// If we request the hash of this block's number, Substrate will return the hash on the main
// chain
// If that hash is this hash, this block is finalized
let Some(hash) = self.block_hash(header.number).await? else {
// This is an error since there is a finalized block at this index
Err(SeraiError::InvalidNode(
"couldn't get block hash for a block number below the finalized block".to_string(),
))?
};
Ok(header.hash().as_ref() == hash)
}
pub async fn finalized_block_by_number(&self, number: u64) -> Result<Option<Block>, SeraiError> {
let hash = self.block_hash(number).await?;
let Some(hash) = hash else { return Ok(None) };
let Some(block) = self.block(hash).await? else { return Ok(None) };
if !self.is_finalized(&block.header).await? {
return Ok(None);
}
Ok(Some(block))
}
/*
/// A stream which yields whenever new block(s) have been finalized.
pub async fn newly_finalized_block(
&self,
) -> Result<impl Stream<Item = Result<(), SeraiError>>, SeraiError> {
Ok(self.0.rpc().subscribe_finalized_block_headers().await
.map_err(|_| SeraiError::ConnectionError)?.map(
|next| {
next.map_err(|_| SeraiError::ConnectionError)?;
Ok(())
},
))
}
pub async fn nonce(&self, address: &SeraiAddress) -> Result<u32, SeraiError> {
self
.0
.rpc()
.system_account_next_index(&sp_core::sr25519::Public(address.0).to_string())
.await
.map_err(|_| SeraiError::ConnectionError)
}
*/
/// Create a TemporalSerai bound to whatever is currently the latest finalized block.
///
/// The binding occurs at time of call. This does not track the latest finalized block and update
/// itself.
pub async fn as_of_latest_finalized_block(&self) -> Result<TemporalSerai<'_>, SeraiError> {
let latest = self.latest_finalized_block_hash().await?;
Ok(TemporalSerai { serai: self, block: latest, events: RwLock::new(None) })
}
/// Returns a TemporalSerai able to retrieve state as of the specified block.
pub fn as_of(&self, block: [u8; 32]) -> TemporalSerai<'_> {
TemporalSerai { serai: self, block, events: RwLock::new(None) }
}
/// Return the P2P Multiaddrs for the validators of the specified network.
pub async fn p2p_validators(
&self,
network: ExternalNetworkId,
) -> Result<Vec<multiaddr::Multiaddr>, SeraiError> {
self.call("p2p_validators", [network]).await
}
// TODO: move this to SeraiValidatorSets?
pub async fn external_network_address(
&self,
network: ExternalNetworkId,
) -> Result<String, SeraiError> {
self.call("external_network_address", [network]).await
}
// TODO: move this to SeraiInInstructions?
pub async fn encoded_shorthand(&self, shorthand: Shorthand) -> Result<Vec<u8>, SeraiError> {
self.call("encoded_shorthand", shorthand).await
}
// TODO: move this to SeraiDex?
pub async fn quote_price(&self, params: QuotePriceParams) -> Result<u64, SeraiError> {
self.call("quote_price", params).await
}
}
impl TemporalSerai<'_> {
async fn events<E>(
&self,
filter_map: impl Fn(&Event) -> Option<E>,
) -> Result<Vec<E>, SeraiError> {
let mut events = self.events.read().await;
if events.is_none() {
drop(events);
let mut events_write = self.events.write().await;
if events_write.is_none() {
*events_write = Some(self.storage("System", "Events", ()).await?.unwrap_or(vec![]));
}
drop(events_write);
events = self.events.read().await;
}
let mut res = vec![];
for event in events.as_ref().unwrap() {
if let Some(event) = filter_map(&event.event) {
res.push(event);
}
}
Ok(res)
}
async fn storage<K: Encode, R: Decode>(
&self,
pallet: &'static str,
name: &'static str,
key: K,
) -> Result<Option<R>, SeraiError> {
// TODO: Make this const?
let mut full_key = sp_core::hashing::twox_128(pallet.as_bytes()).to_vec();
full_key.extend(sp_core::hashing::twox_128(name.as_bytes()));
full_key.extend(key.encode());
let res: Option<String> =
self.serai.call("state_getStorage", [hex::encode(full_key), hex::encode(self.block)]).await?;
let Some(res) = res else { return Ok(None) };
let res = Serai::hex_decode(res)?;
Ok(Some(R::decode(&mut res.as_slice()).map_err(|_| {
SeraiError::InvalidRuntime(format!(
"different type present at storage location, raw value: {}",
hex::encode(res)
))
})?))
}
async fn runtime_api<P: Encode, R: Decode>(
&self,
method: &'static str,
params: P,
) -> Result<R, SeraiError> {
let result: String = self
.serai
.call(
"state_call",
[method.to_string(), hex::encode(params.encode()), hex::encode(self.block)],
)
.await?;
let bytes = Serai::hex_decode(result.clone())?;
R::decode(&mut bytes.as_slice()).map_err(|_| {
SeraiError::InvalidRuntime(format!(
"different type than what is expected to be returned, raw value: {}",
hex::encode(result)
))
})
}
pub fn coins(&self) -> SeraiCoins<'_> {
SeraiCoins(self)
}
pub fn dex(&self) -> SeraiDex<'_> {
SeraiDex(self)
}
pub fn in_instructions(&self) -> SeraiInInstructions<'_> {
SeraiInInstructions(self)
}
pub fn validator_sets(&self) -> SeraiValidatorSets<'_> {
SeraiValidatorSets(self)
}
pub fn genesis_liquidity(&self) -> SeraiGenesisLiquidity<'_> {
SeraiGenesisLiquidity(self)
}
pub fn liquidity_tokens(&self) -> SeraiLiquidityTokens<'_> {
SeraiLiquidityTokens(self)
}
}

View File

@@ -1,248 +0,0 @@
use scale::Encode;
use sp_core::sr25519::{Public, Signature};
use sp_runtime::BoundedVec;
use serai_abi::{primitives::Amount, validator_sets::primitives::ExternalValidatorSet};
pub use serai_abi::validator_sets::primitives;
use primitives::{MAX_KEY_LEN, Session, KeyPair, SlashReport};
use crate::{
primitives::{NetworkId, ExternalNetworkId, EmbeddedEllipticCurve},
Transaction, Serai, TemporalSerai, SeraiError,
};
const PALLET: &str = "ValidatorSets";
pub type ValidatorSetsEvent = serai_abi::validator_sets::Event;
#[derive(Clone, Copy)]
pub struct SeraiValidatorSets<'a>(pub(crate) &'a TemporalSerai<'a>);
impl SeraiValidatorSets<'_> {
pub async fn new_set_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::ValidatorSets(event) = event {
if matches!(event, ValidatorSetsEvent::NewSet { .. }) {
Some(event.clone())
} else {
None
}
} else {
None
}
})
.await
}
pub async fn participant_removed_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::ValidatorSets(event) = event {
if matches!(event, ValidatorSetsEvent::ParticipantRemoved { .. }) {
Some(event.clone())
} else {
None
}
} else {
None
}
})
.await
}
pub async fn key_gen_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::ValidatorSets(event) = event {
if matches!(event, ValidatorSetsEvent::KeyGen { .. }) {
Some(event.clone())
} else {
None
}
} else {
None
}
})
.await
}
pub async fn accepted_handover_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::ValidatorSets(event) = event {
if matches!(event, ValidatorSetsEvent::AcceptedHandover { .. }) {
Some(event.clone())
} else {
None
}
} else {
None
}
})
.await
}
pub async fn set_retired_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
self
.0
.events(|event| {
if let serai_abi::Event::ValidatorSets(event) = event {
if matches!(event, ValidatorSetsEvent::SetRetired { .. }) {
Some(event.clone())
} else {
None
}
} else {
None
}
})
.await
}
pub async fn session(&self, network: NetworkId) -> Result<Option<Session>, SeraiError> {
self.0.storage(PALLET, "CurrentSession", network).await
}
pub async fn embedded_elliptic_curve_key(
&self,
validator: Public,
embedded_elliptic_curve: EmbeddedEllipticCurve,
) -> Result<Option<Vec<u8>>, SeraiError> {
self
.0
.storage(
PALLET,
"EmbeddedEllipticCurveKeys",
(sp_core::hashing::blake2_128(&validator.encode()), validator, embedded_elliptic_curve),
)
.await
}
pub async fn participants(
&self,
network: NetworkId,
) -> Result<Option<Vec<(Public, u64)>>, SeraiError> {
self.0.storage(PALLET, "Participants", network).await
}
pub async fn allocation_per_key_share(
&self,
network: NetworkId,
) -> Result<Option<Amount>, SeraiError> {
self.0.storage(PALLET, "AllocationPerKeyShare", network).await
}
pub async fn total_allocated_stake(
&self,
network: NetworkId,
) -> Result<Option<Amount>, SeraiError> {
self.0.storage(PALLET, "TotalAllocatedStake", network).await
}
pub async fn allocation(
&self,
network: NetworkId,
key: Public,
) -> Result<Option<Amount>, SeraiError> {
self
.0
.storage(
PALLET,
"Allocations",
(sp_core::hashing::blake2_128(&(network, key).encode()), (network, key)),
)
.await
}
pub async fn pending_deallocations(
&self,
network: NetworkId,
account: Public,
session: Session,
) -> Result<Option<Amount>, SeraiError> {
self
.0
.storage(
PALLET,
"PendingDeallocations",
(sp_core::hashing::blake2_128(&(network, account).encode()), (network, account, session)),
)
.await
}
pub async fn active_network_validators(
&self,
network: NetworkId,
) -> Result<Vec<Public>, SeraiError> {
self.0.runtime_api("ValidatorSetsApi_validators", network).await
}
// TODO: Store these separately since we almost never need both at once?
pub async fn keys(&self, set: ExternalValidatorSet) -> Result<Option<KeyPair>, SeraiError> {
self.0.storage(PALLET, "Keys", (sp_core::hashing::twox_64(&set.encode()), set)).await
}
pub async fn key_pending_slash_report(
&self,
network: ExternalNetworkId,
) -> Result<Option<Public>, SeraiError> {
self.0.storage(PALLET, "PendingSlashReport", network).await
}
pub async fn session_begin_block(
&self,
network: NetworkId,
session: Session,
) -> Result<Option<u64>, SeraiError> {
self.0.storage(PALLET, "SessionBeginBlock", (network, session)).await
}
pub fn set_keys(
network: ExternalNetworkId,
key_pair: KeyPair,
signature_participants: bitvec::vec::BitVec<u8, bitvec::order::Lsb0>,
signature: Signature,
) -> Transaction {
Serai::unsigned(serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::set_keys {
network,
key_pair,
signature_participants,
signature,
}))
}
pub fn set_embedded_elliptic_curve_key(
embedded_elliptic_curve: EmbeddedEllipticCurve,
key: BoundedVec<u8, sp_core::ConstU32<{ MAX_KEY_LEN }>>,
) -> serai_abi::Call {
serai_abi::Call::ValidatorSets(
serai_abi::validator_sets::Call::set_embedded_elliptic_curve_key {
embedded_elliptic_curve,
key,
},
)
}
pub fn allocate(network: NetworkId, amount: Amount) -> serai_abi::Call {
serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::allocate { network, amount })
}
pub fn deallocate(network: NetworkId, amount: Amount) -> serai_abi::Call {
serai_abi::Call::ValidatorSets(serai_abi::validator_sets::Call::deallocate { network, amount })
}
pub fn report_slashes(
network: ExternalNetworkId,
slashes: SlashReport,
signature: Signature,
) -> Transaction {
Serai::unsigned(serai_abi::Call::ValidatorSets(
serai_abi::validator_sets::Call::report_slashes { network, slashes, signature },
))
}
}

View File

@@ -1,2 +0,0 @@
#[cfg(feature = "networks")]
mod networks;

View File

@@ -1 +0,0 @@
// TODO: Test the address back and forth

View File

@@ -1,5 +0,0 @@
#[cfg(feature = "bitcoin")]
mod bitcoin;
#[cfg(feature = "monero")]
mod monero;

View File

@@ -1 +0,0 @@
// TODO: Test the address back and forth