Reorganize serai-client

Instead of functions taking a block hash, has a scope to a block hash before
functions can be called.

Separates functions by pallets.
This commit is contained in:
Luke Parker
2023-10-14 02:47:58 -04:00
parent 96cc5d0157
commit cb61c9052a
13 changed files with 384 additions and 365 deletions

View File

@@ -0,0 +1,94 @@
use sp_core::sr25519::Public;
use serai_runtime::{
primitives::{SeraiAddress, SubstrateAmount, Amount, Coin, Balance},
assets::{AssetDetails, AssetAccount},
tokens, Tokens, Runtime,
};
pub use tokens::primitives;
use primitives::OutInstruction;
use subxt::tx::Payload;
use crate::{TemporalSerai, SeraiError, Composite, scale_value, scale_composite};
const PALLET: &str = "Tokens";
pub type TokensEvent = tokens::Event<Runtime>;
#[derive(Clone, Copy)]
pub struct SeraiCoins<'a>(pub(crate) TemporalSerai<'a>);
impl<'a> SeraiCoins<'a> {
pub fn into_inner(self) -> TemporalSerai<'a> {
self.0
}
pub async fn mint_events(&self) -> Result<Vec<TokensEvent>, SeraiError> {
self.0.events::<Tokens, _>(|event| matches!(event, TokensEvent::Mint { .. })).await
}
pub async fn burn_events(&self) -> Result<Vec<TokensEvent>, SeraiError> {
self.0.events::<Tokens, _>(|event| matches!(event, TokensEvent::Burn { .. })).await
}
pub async fn sri_balance(&self, address: SeraiAddress) -> Result<u64, SeraiError> {
let data: Option<
serai_runtime::system::AccountInfo<u32, serai_runtime::balances::AccountData<u64>>,
> = self.0.storage("System", "Account", Some(vec![scale_value(address)])).await?;
Ok(data.map(|data| data.data.free).unwrap_or(0))
}
pub async fn token_supply(&self, coin: Coin) -> Result<Amount, SeraiError> {
Ok(Amount(
self
.0
.storage::<AssetDetails<SubstrateAmount, SeraiAddress, SubstrateAmount>>(
"Assets",
"Asset",
Some(vec![scale_value(coin)]),
)
.await?
.map(|token| token.supply)
.unwrap_or(0),
))
}
pub async fn token_balance(
&self,
coin: Coin,
address: SeraiAddress,
) -> Result<Amount, SeraiError> {
Ok(Amount(
self
.0
.storage::<AssetAccount<SubstrateAmount, SubstrateAmount, (), Public>>(
"Assets",
"Account",
Some(vec![scale_value(coin), scale_value(address)]),
)
.await?
.map(|account| account.balance())
.unwrap_or(0),
))
}
pub fn transfer_sri(to: SeraiAddress, amount: Amount) -> Payload<Composite<()>> {
Payload::new(
"Balances",
// TODO: Use transfer_allow_death?
// TODO: Replace the Balances pallet with something much simpler
"transfer",
scale_composite(serai_runtime::balances::Call::<Runtime>::transfer {
dest: to,
value: amount.0,
}),
)
}
pub fn burn(balance: Balance, instruction: OutInstruction) -> Payload<Composite<()>> {
Payload::new(
PALLET,
"burn",
scale_composite(tokens::Call::<Runtime>::burn { balance, instruction }),
)
}
}

View File

@@ -6,42 +6,42 @@ use subxt::utils::Encoded;
use crate::{
primitives::{BlockHash, NetworkId},
SeraiError, Serai, scale_value,
SeraiError, Serai, TemporalSerai, scale_value,
};
pub type InInstructionsEvent = in_instructions::Event<Runtime>;
const PALLET: &str = "InInstructions";
impl Serai {
pub async fn get_latest_block_for_network(
#[derive(Clone, Copy)]
pub struct SeraiInInstructions<'a>(pub(crate) TemporalSerai<'a>);
impl<'a> SeraiInInstructions<'a> {
pub fn into_inner(self) -> TemporalSerai<'a> {
self.0
}
pub async fn latest_block_for_network(
&self,
hash: [u8; 32],
network: NetworkId,
) -> Result<Option<BlockHash>, SeraiError> {
self.storage(PALLET, "LatestNetworkBlock", Some(vec![scale_value(network)]), hash).await
self.0.storage(PALLET, "LatestNetworkBlock", Some(vec![scale_value(network)])).await
}
pub async fn get_last_batch_for_network(
pub async fn last_batch_for_network(
&self,
hash: [u8; 32],
network: NetworkId,
) -> Result<Option<u32>, SeraiError> {
self.storage(PALLET, "LastBatch", Some(vec![scale_value(network)]), hash).await
self.0.storage(PALLET, "LastBatch", Some(vec![scale_value(network)])).await
}
pub async fn get_batch_events(
&self,
block: [u8; 32],
) -> Result<Vec<InInstructionsEvent>, SeraiError> {
pub async fn batch_events(&self) -> Result<Vec<InInstructionsEvent>, SeraiError> {
self
.events::<InInstructions, _>(block, |event| {
matches!(event, InInstructionsEvent::Batch { .. })
})
.0
.events::<InInstructions, _>(|event| matches!(event, InInstructionsEvent::Batch { .. }))
.await
}
pub fn execute_batch(batch: SignedBatch) -> Encoded {
Self::unsigned::<InInstructions, _>(&in_instructions::Call::<Runtime>::execute_batch { batch })
Serai::unsigned::<InInstructions, _>(&in_instructions::Call::<Runtime>::execute_batch { batch })
}
}

View File

@@ -33,9 +33,12 @@ use serai_runtime::{
system::Config, support::traits::PalletInfo as PalletInfoTrait, PalletInfo, Runtime,
};
pub mod tokens;
pub mod coins;
pub use coins::SeraiCoins;
pub mod in_instructions;
pub use in_instructions::SeraiInInstructions;
pub mod validator_sets;
pub use validator_sets::SeraiValidatorSets;
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Encode, Decode)]
pub struct Tip {
@@ -136,153 +139,14 @@ pub enum SeraiError {
#[derive(Clone)]
pub struct Serai(OnlineClient<SeraiConfig>);
#[derive(Clone, Copy)]
pub struct TemporalSerai<'a>(pub(crate) &'a Serai, pub(crate) [u8; 32]);
impl Serai {
pub async fn new(url: &str) -> Result<Self, SeraiError> {
Ok(Serai(OnlineClient::<SeraiConfig>::from_url(url).await.map_err(SeraiError::RpcError)?))
}
async fn storage<R: Decode>(
&self,
pallet: &'static str,
name: &'static str,
keys: Option<Vec<Value>>,
block: [u8; 32],
) -> Result<Option<R>, SeraiError> {
let storage = self.0.storage();
#[allow(clippy::unwrap_or_default)]
let address = subxt::dynamic::storage(pallet, name, keys.unwrap_or(vec![]));
debug_assert!(storage.validate(&address).is_ok(), "invalid storage address");
storage
.at(block.into())
.fetch(&address)
.await
.map_err(SeraiError::RpcError)?
.map(|res| R::decode(&mut res.encoded()).map_err(|_| SeraiError::InvalidRuntime))
.transpose()
}
async fn events<P: 'static, E: Decode>(
&self,
block: [u8; 32],
filter: impl Fn(&E) -> bool,
) -> Result<Vec<E>, SeraiError> {
let mut res = vec![];
for event in self.0.events().at(block.into()).await.map_err(SeraiError::RpcError)?.iter() {
let event = event.map_err(|_| SeraiError::InvalidRuntime)?;
if PalletInfo::index::<P>().unwrap() == usize::from(event.pallet_index()) {
let mut with_variant: &[u8] =
&[[event.variant_index()].as_ref(), event.field_bytes()].concat();
let event = E::decode(&mut with_variant).map_err(|_| SeraiError::InvalidRuntime)?;
if filter(&event) {
res.push(event);
}
}
}
Ok(res)
}
pub async fn get_latest_block_hash(&self) -> Result<[u8; 32], SeraiError> {
Ok(self.0.rpc().finalized_head().await.map_err(SeraiError::RpcError)?.into())
}
pub async fn get_latest_block(&self) -> Result<Block, SeraiError> {
Block::new(
self
.0
.rpc()
.block(Some(self.0.rpc().finalized_head().await.map_err(SeraiError::RpcError)?))
.await
.map_err(SeraiError::RpcError)?
.ok_or(SeraiError::InvalidNode)?
.block,
)
}
// There is no provided method for this
// TODO: Add one to Serai
pub async fn is_finalized(&self, header: &Header) -> Result<Option<bool>, SeraiError> {
// Get the latest finalized block
let finalized = self.get_latest_block_hash().await?.into();
// If the latest finalized block is this block, return true
if finalized == header.hash() {
return Ok(Some(true));
}
let Some(finalized) =
self.0.rpc().header(Some(finalized)).await.map_err(SeraiError::RpcError)?
else {
return Ok(None);
};
// If the finalized block has a lower number, this block can't be finalized
if finalized.number() < header.number() {
return Ok(Some(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.0.rpc().block_hash(Some(header.number().into())).await.map_err(SeraiError::RpcError)?
else {
// This is an error since there is a block at this index
Err(SeraiError::InvalidNode)?
};
Ok(Some(header.hash() == hash))
}
pub async fn get_block(&self, hash: [u8; 32]) -> Result<Option<Block>, SeraiError> {
let Some(res) = self.0.rpc().block(Some(hash.into())).await.map_err(SeraiError::RpcError)?
else {
return Ok(None);
};
// Only return finalized blocks
if self.is_finalized(&res.block.header).await? != Some(true) {
return Ok(None);
}
Ok(Some(Block::new(res.block)?))
}
// Ideally, this would be get_block_hash, not get_block_by_number
// Unfortunately, in order to only operate over only finalized data, we have to check the
// returned hash is for a finalized block. We can only do that by calling the extensive
// is_finalized method, which at least requires the header
// In practice, the block is likely more useful than the header
pub async fn get_block_by_number(&self, number: u64) -> Result<Option<Block>, SeraiError> {
let Some(hash) =
self.0.rpc().block_hash(Some(number.into())).await.map_err(SeraiError::RpcError)?
else {
return Ok(None);
};
self.get_block(hash.into()).await
}
/// 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::RpcError)?.map(
|next| {
next.map_err(SeraiError::RpcError)?;
Ok(())
},
))
}
pub async fn get_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::RpcError)
}
fn unsigned<P: 'static, C: Encode>(call: &C) -> Encoded {
// TODO: Should Serai purge the old transaction code AND set this to 0/1?
const TRANSACTION_VERSION: u8 = 4;
@@ -322,29 +186,173 @@ impl Serai {
self.0.rpc().submit_extrinsic(tx).await.map(|_| ()).map_err(SeraiError::RpcError)
}
pub async fn get_sri_balance(
&self,
block: [u8; 32],
address: SeraiAddress,
) -> Result<u64, SeraiError> {
let data: Option<
serai_runtime::system::AccountInfo<u32, serai_runtime::balances::AccountData<u64>>,
> = self.storage("System", "Account", Some(vec![scale_value(address)]), block).await?;
Ok(data.map(|data| data.data.free).unwrap_or(0))
pub async fn latest_block_hash(&self) -> Result<[u8; 32], SeraiError> {
Ok(self.0.rpc().finalized_head().await.map_err(SeraiError::RpcError)?.into())
}
pub fn transfer_sri(to: SeraiAddress, amount: Amount) -> Payload<Composite<()>> {
Payload::new(
"Balances",
// TODO: Use transfer_allow_death?
// TODO: Replace the Balances pallet with something much simpler
"transfer",
scale_composite(serai_runtime::balances::Call::<Runtime>::transfer {
dest: to,
value: amount.0,
}),
pub async fn latest_block(&self) -> Result<Block, SeraiError> {
Block::new(
self
.0
.rpc()
.block(Some(self.0.rpc().finalized_head().await.map_err(SeraiError::RpcError)?))
.await
.map_err(SeraiError::RpcError)?
.ok_or(SeraiError::InvalidNode)?
.block,
)
}
// There is no provided method for this
// TODO: Add one to Serai
pub async fn is_finalized(&self, header: &Header) -> Result<Option<bool>, SeraiError> {
// Get the latest finalized block
let finalized = self.latest_block_hash().await?.into();
// If the latest finalized block is this block, return true
if finalized == header.hash() {
return Ok(Some(true));
}
let Some(finalized) =
self.0.rpc().header(Some(finalized)).await.map_err(SeraiError::RpcError)?
else {
return Ok(None);
};
// If the finalized block has a lower number, this block can't be finalized
if finalized.number() < header.number() {
return Ok(Some(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.0.rpc().block_hash(Some(header.number().into())).await.map_err(SeraiError::RpcError)?
else {
// This is an error since there is a block at this index
Err(SeraiError::InvalidNode)?
};
Ok(Some(header.hash() == hash))
}
pub async fn block(&self, hash: [u8; 32]) -> Result<Option<Block>, SeraiError> {
let Some(res) = self.0.rpc().block(Some(hash.into())).await.map_err(SeraiError::RpcError)?
else {
return Ok(None);
};
// Only return finalized blocks
if self.is_finalized(&res.block.header).await? != Some(true) {
return Ok(None);
}
Ok(Some(Block::new(res.block)?))
}
// Ideally, this would be block_hash, not block_by_number
// Unfortunately, in order to only operate over only finalized data, we have to check the
// returned hash is for a finalized block. We can only do that by calling the extensive
// is_finalized method, which at least requires the header
// In practice, the block is likely more useful than the header
pub async fn block_by_number(&self, number: u64) -> Result<Option<Block>, SeraiError> {
let Some(hash) =
self.0.rpc().block_hash(Some(number.into())).await.map_err(SeraiError::RpcError)?
else {
return Ok(None);
};
self.block(hash.into()).await
}
/// 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::RpcError)?.map(
|next| {
next.map_err(SeraiError::RpcError)?;
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::RpcError)
}
pub async fn with_current_latest_block(&self) -> Result<TemporalSerai, SeraiError> {
let latest = self.latest_block_hash().await?;
Ok(TemporalSerai(self, latest))
}
/// Returns a TemporalSerai able to retrieve state as of the specified block.
pub fn as_of(&self, block: [u8; 32]) -> TemporalSerai {
TemporalSerai(self, block)
}
}
impl<'a> TemporalSerai<'a> {
pub fn into_inner(&self) -> &Serai {
self.0
}
async fn events<P: 'static, E: Decode>(
&self,
filter: impl Fn(&E) -> bool,
) -> Result<Vec<E>, SeraiError> {
let mut res = vec![];
for event in self.0 .0.events().at(self.1.into()).await.map_err(SeraiError::RpcError)?.iter() {
let event = event.map_err(|_| SeraiError::InvalidRuntime)?;
if PalletInfo::index::<P>().unwrap() == usize::from(event.pallet_index()) {
let mut with_variant: &[u8] =
&[[event.variant_index()].as_ref(), event.field_bytes()].concat();
let event = E::decode(&mut with_variant).map_err(|_| SeraiError::InvalidRuntime)?;
if filter(&event) {
res.push(event);
}
}
}
Ok(res)
}
async fn storage<R: Decode>(
&self,
pallet: &'static str,
name: &'static str,
keys: Option<Vec<Value>>,
) -> Result<Option<R>, SeraiError> {
let storage = self.0 .0.storage();
#[allow(clippy::unwrap_or_default)]
let address = subxt::dynamic::storage(pallet, name, keys.unwrap_or(vec![]));
debug_assert!(storage.validate(&address).is_ok(), "invalid storage address");
storage
.at(self.1.into())
.fetch(&address)
.await
.map_err(SeraiError::RpcError)?
.map(|res| R::decode(&mut res.encoded()).map_err(|_| SeraiError::InvalidRuntime))
.transpose()
}
pub fn coins(self) -> SeraiCoins<'a> {
SeraiCoins(self)
}
pub fn in_instructions(self) -> SeraiInInstructions<'a> {
SeraiInInstructions(self)
}
pub fn validator_sets(self) -> SeraiValidatorSets<'a> {
SeraiValidatorSets(self)
}
}
#[derive(Clone)]

View File

@@ -1,69 +0,0 @@
use sp_core::sr25519::Public;
use serai_runtime::{
primitives::{SeraiAddress, SubstrateAmount, Amount, Coin, Balance},
assets::{AssetDetails, AssetAccount},
tokens, Tokens, Runtime,
};
pub use tokens::primitives;
use primitives::OutInstruction;
use subxt::tx::Payload;
use crate::{Serai, SeraiError, Composite, scale_value, scale_composite};
const PALLET: &str = "Tokens";
pub type TokensEvent = tokens::Event<Runtime>;
impl Serai {
pub async fn get_mint_events(&self, block: [u8; 32]) -> Result<Vec<TokensEvent>, SeraiError> {
self.events::<Tokens, _>(block, |event| matches!(event, TokensEvent::Mint { .. })).await
}
pub async fn get_token_supply(&self, block: [u8; 32], coin: Coin) -> Result<Amount, SeraiError> {
Ok(Amount(
self
.storage::<AssetDetails<SubstrateAmount, SeraiAddress, SubstrateAmount>>(
"Assets",
"Asset",
Some(vec![scale_value(coin)]),
block,
)
.await?
.map(|token| token.supply)
.unwrap_or(0),
))
}
pub async fn get_token_balance(
&self,
block: [u8; 32],
coin: Coin,
address: SeraiAddress,
) -> Result<Amount, SeraiError> {
Ok(Amount(
self
.storage::<AssetAccount<SubstrateAmount, SubstrateAmount, (), Public>>(
"Assets",
"Account",
Some(vec![scale_value(coin), scale_value(address)]),
block,
)
.await?
.map(|account| account.balance())
.unwrap_or(0),
))
}
pub fn burn(balance: Balance, instruction: OutInstruction) -> Payload<Composite<()>> {
Payload::new(
PALLET,
"burn",
scale_composite(tokens::Call::<Runtime>::burn { balance, instruction }),
)
}
pub async fn get_burn_events(&self, block: [u8; 32]) -> Result<Vec<TokensEvent>, SeraiError> {
self.events::<Tokens, _>(block, |event| matches!(event, TokensEvent::Burn { .. })).await
}
}

View File

@@ -6,89 +6,67 @@ use primitives::{Session, ValidatorSet, KeyPair};
use subxt::utils::Encoded;
use crate::{primitives::NetworkId, Serai, SeraiError, scale_value};
use crate::{primitives::NetworkId, Serai, TemporalSerai, SeraiError, scale_value};
const PALLET: &str = "ValidatorSets";
pub type ValidatorSetsEvent = validator_sets::Event<Runtime>;
impl Serai {
pub async fn get_new_set_events(
&self,
block: [u8; 32],
) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
#[derive(Clone, Copy)]
pub struct SeraiValidatorSets<'a>(pub(crate) TemporalSerai<'a>);
impl<'a> SeraiValidatorSets<'a> {
pub fn into_inner(self) -> TemporalSerai<'a> {
self.0
}
pub async fn new_set_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
self
.events::<ValidatorSets, _>(block, |event| matches!(event, ValidatorSetsEvent::NewSet { .. }))
.0
.events::<ValidatorSets, _>(|event| matches!(event, ValidatorSetsEvent::NewSet { .. }))
.await
}
pub async fn get_key_gen_events(
&self,
block: [u8; 32],
) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
pub async fn key_gen_events(&self) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
self
.events::<ValidatorSets, _>(block, |event| matches!(event, ValidatorSetsEvent::KeyGen { .. }))
.0
.events::<ValidatorSets, _>(|event| matches!(event, ValidatorSetsEvent::KeyGen { .. }))
.await
}
pub async fn get_session(
&self,
network: NetworkId,
at_hash: [u8; 32],
) -> Result<Option<Session>, SeraiError> {
self.storage(PALLET, "CurrentSession", Some(vec![scale_value(network)]), at_hash).await
pub async fn session(&self, network: NetworkId) -> Result<Option<Session>, SeraiError> {
self.0.storage(PALLET, "CurrentSession", Some(vec![scale_value(network)])).await
}
pub async fn get_validator_set_participants(
&self,
network: NetworkId,
at_hash: [u8; 32],
) -> Result<Option<Vec<Public>>, SeraiError> {
self.storage(PALLET, "Participants", Some(vec![scale_value(network)]), at_hash).await
pub async fn participants(&self, network: NetworkId) -> Result<Option<Vec<Public>>, SeraiError> {
self.0.storage(PALLET, "Participants", Some(vec![scale_value(network)])).await
}
pub async fn get_allocation_per_key_share(
pub async fn allocation_per_key_share(
&self,
network: NetworkId,
at_hash: [u8; 32],
) -> Result<Option<Amount>, SeraiError> {
self.storage(PALLET, "AllocationPerKeyShare", Some(vec![scale_value(network)]), at_hash).await
self.0.storage(PALLET, "AllocationPerKeyShare", Some(vec![scale_value(network)])).await
}
pub async fn get_allocation(
pub async fn allocation(
&self,
network: NetworkId,
key: Public,
at_hash: [u8; 32],
) -> Result<Option<Amount>, SeraiError> {
self
.storage(PALLET, "Allocations", Some(vec![scale_value(network), scale_value(key)]), at_hash)
.await
self.0.storage(PALLET, "Allocations", Some(vec![scale_value(network), scale_value(key)])).await
}
pub async fn get_validator_set_musig_key(
&self,
set: ValidatorSet,
at_hash: [u8; 32],
) -> Result<Option<[u8; 32]>, SeraiError> {
self.storage(PALLET, "MuSigKeys", Some(vec![scale_value(set)]), at_hash).await
pub async fn musig_key(&self, set: ValidatorSet) -> Result<Option<[u8; 32]>, SeraiError> {
self.0.storage(PALLET, "MuSigKeys", Some(vec![scale_value(set)])).await
}
// TODO: Store these separately since we almost never need both at once?
pub async fn get_keys(
&self,
set: ValidatorSet,
at_hash: [u8; 32],
) -> Result<Option<KeyPair>, SeraiError> {
self.storage(PALLET, "Keys", Some(vec![scale_value(set)]), at_hash).await
pub async fn keys(&self, set: ValidatorSet) -> Result<Option<KeyPair>, SeraiError> {
self.0.storage(PALLET, "Keys", Some(vec![scale_value(set)])).await
}
pub fn set_validator_set_keys(
network: NetworkId,
key_pair: KeyPair,
signature: Signature,
) -> Encoded {
Self::unsigned::<ValidatorSets, _>(&validator_sets::Call::<Runtime>::set_keys {
pub fn set_keys(network: NetworkId, key_pair: KeyPair, signature: Signature) -> Encoded {
Serai::unsigned::<ValidatorSets, _>(&validator_sets::Call::<Runtime>::set_keys {
network,
key_pair,
signature,