mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 20:59:23 +00:00
Update serai-client to solely be an umbrella crate of the dedicated client libraries
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
#[cfg(feature = "networks")]
|
||||
mod networks;
|
||||
@@ -1 +0,0 @@
|
||||
// TODO: Test the address back and forth
|
||||
@@ -1,5 +0,0 @@
|
||||
#[cfg(feature = "bitcoin")]
|
||||
mod bitcoin;
|
||||
|
||||
#[cfg(feature = "monero")]
|
||||
mod monero;
|
||||
@@ -1 +0,0 @@
|
||||
// TODO: Test the address back and forth
|
||||
Reference in New Issue
Block a user