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,7 +1,7 @@
[package] [package]
name = "serai-client" name = "serai-client"
version = "0.1.0" version = "0.1.0"
description = "Client library for the Serai network" description = "A client for Serai and its connected networks"
license = "MIT" license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/client" repository = "https://github.com/serai-dex/serai/tree/develop/substrate/client"
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
@@ -17,58 +17,17 @@ rustdoc-args = ["--cfg", "docsrs"]
workspace = true workspace = true
[dependencies] [dependencies]
zeroize = "^1.5" serai-client-serai = { path = "./serai", optional = true }
thiserror = { version = "2", default-features = false, optional = true }
bitvec = { version = "1", default-features = false, features = ["alloc", "serde"] }
hex = "0.4"
scale = { package = "parity-scale-codec", version = "3", optional = true }
borsh = { version = "1", features = ["derive"] }
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
serai-abi = { path = "../abi", version = "0.1" }
multiaddr = { version = "0.18", optional = true }
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", optional = true }
sp-runtime = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", optional = true }
frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", optional = true }
async-lock = "3"
simple-request = { path = "../../common/request", version = "0.3", optional = true }
serai-client-bitcoin = { path = "./bitcoin", optional = true } serai-client-bitcoin = { path = "./bitcoin", optional = true }
serai-client-ethereum = { path = "./ethereum", optional = true } serai-client-ethereum = { path = "./ethereum", optional = true }
serai-client-monero = { path = "./monero", optional = true } serai-client-monero = { path = "./monero", optional = true }
[dev-dependencies]
rand_core = "0.6"
hex = "0.4"
blake2 = "0.11.0-rc.0"
ciphersuite = { path = "../../crypto/ciphersuite" }
dalek-ff-group = { path = "../../crypto/dalek-ff-group" }
ciphersuite-kp256 = { path = "../../crypto/ciphersuite/kp256" }
dkg-musig = { path = "../../crypto/dkg/musig" }
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
schnorrkel = { path = "../../crypto/schnorrkel", package = "frost-schnorrkel" }
tokio = "1"
dockertest = "0.5"
serai-docker-tests = { path = "../../tests/docker" }
[features] [features]
serai = ["thiserror/std", "scale", "serde", "serde_json", "multiaddr", "sp-core", "sp-runtime", "frame-system", "simple-request"] serai = ["serai-client-serai"]
networks = [] bitcoin = ["serai-client-bitcoin"]
bitcoin = ["networks", "serai-client-bitcoin"] ethereum = ["serai-client-ethereum"]
ethereum = ["networks", "serai-client-ethereum"] monero = ["serai-client-monero"]
monero = ["networks", "serai-client-monero"]
# Assumes the default usage is to use Serai as a DEX, which doesn't actually default = ["serai", "bitcoin", "ethereum", "monero"]
# require connecting to a Serai node
default = ["bitcoin", "ethereum", "monero"]

View File

@@ -0,0 +1,4 @@
# serai-client
This crate is an umbrella crate for each of Serai's clients (the network itself
and its connected networks).

View File

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

View File

@@ -1,76 +0,0 @@
use rand_core::{RngCore, OsRng};
use blake2::{
digest::{consts::U32, Digest},
Blake2b,
};
use scale::Encode;
use serai_client::{
primitives::{BlockHash, ExternalCoin, Amount, ExternalBalance, SeraiAddress},
coins::CoinsEvent,
validator_sets::primitives::Session,
in_instructions::{
primitives::{InInstruction, InInstructionWithBalance, Batch},
InInstructionsEvent,
},
Serai,
};
mod common;
use common::in_instructions::provide_batch;
serai_test!(
publish_batch: (|serai: Serai| async move {
let id = 0;
let mut address = SeraiAddress::new([0; 32]);
OsRng.fill_bytes(&mut address.0);
let coin = ExternalCoin::Bitcoin;
let network = coin.network();
let amount = Amount(OsRng.next_u64().saturating_add(1));
let balance = ExternalBalance { coin, amount };
let mut external_network_block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut external_network_block_hash.0);
let batch = Batch {
network,
id,
external_network_block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Transfer(address),
balance,
}],
};
let block = provide_batch(&serai, batch.clone()).await;
let serai = serai.as_of(block);
{
let serai = serai.in_instructions();
let batches = serai.batch_events().await.unwrap();
assert_eq!(
batches,
vec![InInstructionsEvent::Batch {
network,
publishing_session: Session(0),
id,
external_network_block_hash,
in_instructions_hash: Blake2b::<U32>::digest(batch.instructions.encode()).into(),
in_instruction_results: bitvec::bitvec![u8, bitvec::order::Lsb0; 1; 1],
}]
);
}
let serai = serai.coins();
assert_eq!(
serai.mint_events().await.unwrap(),
vec![CoinsEvent::Mint { to: address, balance: balance.into() }]
);
assert_eq!(serai.coin_supply(coin.into()).await.unwrap(), amount);
assert_eq!(serai.coin_balance(coin.into(), address).await.unwrap(), amount);
})
);

View File

@@ -1,105 +0,0 @@
use rand_core::{RngCore, OsRng};
use blake2::{
digest::{consts::U32, Digest},
Blake2b,
};
use scale::Encode;
use sp_core::Pair;
use serai_client::{
primitives::{
BlockHash, ExternalCoin, Amount, ExternalBalance, SeraiAddress, ExternalAddress,
insecure_pair_from_name,
},
coins::{
primitives::{OutInstruction, OutInstructionWithBalance},
CoinsEvent,
},
validator_sets::primitives::Session,
in_instructions::{
InInstructionsEvent,
primitives::{InInstruction, InInstructionWithBalance, Batch},
},
Serai, SeraiCoins,
};
mod common;
use common::{tx::publish_tx, in_instructions::provide_batch};
serai_test!(
burn: (|serai: Serai| async move {
let id = 0;
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let pair = insecure_pair_from_name("Dave");
let public = pair.public();
let address = SeraiAddress::from(public);
let coin = ExternalCoin::Bitcoin;
let network = coin.network();
let amount = Amount(OsRng.next_u64().saturating_add(1));
let balance = ExternalBalance { coin, amount };
let batch = Batch {
network,
id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Transfer(address),
balance,
}],
};
let block = provide_batch(&serai, batch.clone()).await;
let instruction = {
let serai = serai.as_of(block);
let batches = serai.in_instructions().batch_events().await.unwrap();
assert_eq!(
batches,
vec![InInstructionsEvent::Batch {
network,
publishing_session: Session(0),
id,
external_network_block_hash: block_hash,
in_instructions_hash: Blake2b::<U32>::digest(batch.instructions.encode()).into(),
in_instruction_results: bitvec::bitvec![u8, bitvec::order::Lsb0; 1; 1],
}]
);
assert_eq!(
serai.coins().mint_events().await.unwrap(),
vec![CoinsEvent::Mint { to: address, balance: balance.into() }]
);
assert_eq!(serai.coins().coin_supply(coin.into()).await.unwrap(), amount);
assert_eq!(serai.coins().coin_balance(coin.into(), address).await.unwrap(), amount);
// Now burn it
let mut rand_bytes = vec![0; 32];
OsRng.fill_bytes(&mut rand_bytes);
let external_address = ExternalAddress::new(rand_bytes).unwrap();
OutInstructionWithBalance {
balance,
instruction: OutInstruction { address: external_address },
}
};
let block = publish_tx(
&serai,
&serai.sign(&pair, SeraiCoins::burn_with_instruction(instruction.clone()), 0, 0),
)
.await;
let serai = serai.as_of(block);
let serai = serai.coins();
let events = serai.burn_with_instruction_events().await.unwrap();
assert_eq!(events, vec![CoinsEvent::BurnWithInstruction { from: address, instruction }]);
assert_eq!(serai.coin_supply(coin.into()).await.unwrap(), Amount(0));
assert_eq!(serai.coin_balance(coin.into(), address).await.unwrap(), Amount(0));
})
);

View File

@@ -1,49 +0,0 @@
use serai_abi::primitives::{Amount, Coin, ExternalCoin};
use serai_client::{Serai, SeraiDex};
use sp_core::{sr25519::Pair, Pair as PairTrait};
use crate::common::tx::publish_tx;
#[allow(dead_code)]
pub async fn add_liquidity(
serai: &Serai,
coin: ExternalCoin,
coin_amount: Amount,
sri_amount: Amount,
nonce: u32,
pair: Pair,
) -> [u8; 32] {
let address = pair.public();
let tx = serai.sign(
&pair,
SeraiDex::add_liquidity(coin, coin_amount, sri_amount, Amount(1), Amount(1), address.into()),
nonce,
0,
);
publish_tx(serai, &tx).await
}
#[allow(dead_code)]
pub async fn swap(
serai: &Serai,
from_coin: Coin,
to_coin: Coin,
amount_in: Amount,
amount_out_min: Amount,
nonce: u32,
pair: Pair,
) -> [u8; 32] {
let address = pair.public();
let tx = serai.sign(
&pair,
SeraiDex::swap(from_coin, to_coin, amount_in, amount_out_min, address.into()),
nonce,
Default::default(),
);
publish_tx(serai, &tx).await
}

View File

@@ -1,122 +0,0 @@
use std::collections::HashMap;
use rand_core::{RngCore, OsRng};
use zeroize::Zeroizing;
use frost::curve::Ristretto;
use ciphersuite::{WrappedGroup, GroupIo};
use dkg_musig::musig;
use schnorrkel::Schnorrkel;
use sp_core::{sr25519::Signature, Pair as PairTrait};
use serai_abi::{
primitives::{
EXTERNAL_COINS, BlockHash, ExternalNetworkId, NetworkId, ExternalCoin, Amount, ExternalBalance,
SeraiAddress, insecure_pair_from_name,
},
validator_sets::primitives::{Session, ValidatorSet, musig_context},
genesis_liquidity::primitives::{Values, oraclize_values_message},
in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch},
};
use serai_client::{Serai, SeraiGenesisLiquidity};
use crate::common::{in_instructions::provide_batch, tx::publish_tx};
#[allow(dead_code)]
pub async fn set_up_genesis(
serai: &Serai,
values: &HashMap<ExternalCoin, u64>,
) -> (HashMap<ExternalCoin, Vec<(SeraiAddress, Amount)>>, HashMap<ExternalNetworkId, u32>) {
// make accounts with amounts
let mut accounts = HashMap::new();
for coin in EXTERNAL_COINS {
// make 5 accounts per coin
let mut values = vec![];
for _ in 0 .. 5 {
let mut address = SeraiAddress::new([0; 32]);
OsRng.fill_bytes(&mut address.0);
values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals()))));
}
accounts.insert(coin, values);
}
// send a batch per coin
let mut batch_ids: HashMap<ExternalNetworkId, u32> = HashMap::new();
for coin in EXTERNAL_COINS {
// set up instructions
let instructions = accounts[&coin]
.iter()
.map(|(addr, amount)| InInstructionWithBalance {
instruction: InInstruction::GenesisLiquidity(*addr),
balance: ExternalBalance { coin, amount: *amount },
})
.collect::<Vec<_>>();
// set up block hash
let mut block = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block.0);
// set up batch id
batch_ids
.entry(coin.network())
.and_modify(|v| {
*v += 1;
})
.or_insert(0);
let batch = Batch {
network: coin.network(),
external_network_block_hash: block,
id: batch_ids[&coin.network()],
instructions,
};
provide_batch(serai, batch).await;
}
// set values relative to each other. We can do that without checking for genesis period blocks
// since we are running in test(fast-epoch) mode.
// TODO: Random values here
let values = Values {
monero: values[&ExternalCoin::Monero],
ether: values[&ExternalCoin::Ether],
dai: values[&ExternalCoin::Dai],
};
set_values(serai, &values).await;
(accounts, batch_ids)
}
#[allow(dead_code)]
async fn set_values(serai: &Serai, values: &Values) {
// prepare a Musig tx to oraclize the relative values
let pair = insecure_pair_from_name("Alice");
let public = pair.public();
// we publish the tx in set 1
let set = ValidatorSet { session: Session(1), network: NetworkId::Serai };
let public_key = <Ristretto as GroupIo>::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap();
let secret_key =
<Ristretto as GroupIo>::read_F::<&[u8]>(&mut pair.as_ref().secret.to_bytes()[.. 32].as_ref())
.unwrap();
assert_eq!(Ristretto::generator() * secret_key, public_key);
let threshold_keys =
musig::<Ristretto>(musig_context(set), Zeroizing::new(secret_key), &[public_key]).unwrap();
let sig = frost::tests::sign_without_caching(
&mut OsRng,
frost::tests::algorithm_machines(
&mut OsRng,
&Schnorrkel::new(b"substrate"),
&HashMap::from([(threshold_keys.params().i(), threshold_keys)]),
),
&oraclize_values_message(&set, values),
);
// oraclize values
let _ =
publish_tx(serai, &SeraiGenesisLiquidity::oraclize_values(*values, Signature(sig.to_bytes())))
.await;
}

View File

@@ -1,98 +0,0 @@
use rand_core::{RngCore, OsRng};
use blake2::{
digest::{consts::U32, Digest},
Blake2b,
};
use scale::Encode;
use sp_core::Pair;
use serai_client::{
primitives::{BlockHash, ExternalBalance, SeraiAddress, insecure_pair_from_name},
validator_sets::primitives::{ExternalValidatorSet, KeyPair},
in_instructions::{
primitives::{Batch, SignedBatch, batch_message, InInstruction, InInstructionWithBalance},
InInstructionsEvent,
},
SeraiInInstructions, Serai,
};
use crate::common::{tx::publish_tx, validator_sets::set_keys};
#[allow(dead_code)]
pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] {
let serai_latest = serai.as_of_latest_finalized_block().await.unwrap();
let session = serai_latest.validator_sets().session(batch.network.into()).await.unwrap().unwrap();
let set = ExternalValidatorSet { session, network: batch.network };
let pair = insecure_pair_from_name(&format!("ValidatorSet {set:?}"));
let keys = if let Some(keys) = serai_latest.validator_sets().keys(set).await.unwrap() {
keys
} else {
let keys = KeyPair(pair.public(), vec![].try_into().unwrap());
set_keys(serai, set, keys.clone(), &[insecure_pair_from_name("Alice")]).await;
keys
};
assert_eq!(keys.0, pair.public());
let block = publish_tx(
serai,
&SeraiInInstructions::execute_batch(SignedBatch {
batch: batch.clone(),
signature: pair.sign(&batch_message(&batch)),
}),
)
.await;
{
let mut batches = serai.as_of(block).in_instructions().batch_events().await.unwrap();
assert_eq!(batches.len(), 1);
let InInstructionsEvent::Batch {
network,
publishing_session,
id,
external_network_block_hash,
in_instructions_hash,
in_instruction_results: _,
} = batches.swap_remove(0)
else {
panic!("Batch event wasn't Batch event")
};
assert_eq!(network, batch.network);
assert_eq!(publishing_session, session);
assert_eq!(id, batch.id);
assert_eq!(external_network_block_hash, batch.external_network_block_hash);
assert_eq!(
in_instructions_hash,
<[u8; 32]>::from(Blake2b::<U32>::digest(batch.instructions.encode()))
);
}
// TODO: Check the tokens events
block
}
#[allow(dead_code)]
pub async fn mint_coin(
serai: &Serai,
balance: ExternalBalance,
batch_id: u32,
address: SeraiAddress,
) -> [u8; 32] {
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: balance.coin.network(),
id: batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Transfer(address),
balance,
}],
};
provide_batch(serai, batch).await
}

View File

@@ -1,140 +0,0 @@
pub mod tx;
pub mod validator_sets;
pub mod in_instructions;
pub mod dex;
pub mod genesis_liquidity;
#[macro_export]
macro_rules! serai_test {
($($name: ident: $test: expr)*) => {
$(
#[tokio::test]
async fn $name() {
use std::collections::HashMap;
use dockertest::{
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
TestBodySpecification, DockerTest,
};
serai_docker_tests::build("serai".to_string());
let handle = concat!("serai_client-serai_node-", stringify!($name));
let composition = TestBodySpecification::with_image(
Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never),
)
.replace_cmd(vec![
"serai-node".to_string(),
"--dev".to_string(),
"--unsafe-rpc-external".to_string(),
"--rpc-cors".to_string(),
"all".to_string(),
])
.replace_env(
HashMap::from([
("RUST_LOG".to_string(), "runtime=debug".to_string()),
("KEY".to_string(), " ".to_string()),
])
)
.set_publish_all_ports(true)
.set_handle(handle)
.set_start_policy(StartPolicy::Strict)
.set_log_options(Some(LogOptions {
action: LogAction::Forward,
policy: LogPolicy::Always,
source: LogSource::Both,
}));
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
test.provide_container(composition);
test.run_async(|ops| async move {
// Sleep until the Substrate RPC starts
let mut ticks = 0;
let serai_rpc = loop {
// Bound execution to 60 seconds
if ticks > 60 {
panic!("Serai node didn't start within 60 seconds");
}
tokio::time::sleep(core::time::Duration::from_secs(1)).await;
ticks += 1;
let Some(serai_rpc) = ops.handle(handle).host_port(9944) else { continue };
let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1);
let Ok(client) = Serai::new(serai_rpc.clone()).await else { continue };
if client.latest_finalized_block_hash().await.is_err() {
continue;
}
break serai_rpc;
};
#[allow(clippy::redundant_closure_call)]
$test(Serai::new(serai_rpc).await.unwrap()).await;
}).await;
}
)*
}
}
#[macro_export]
macro_rules! serai_test_fast_epoch {
($($name: ident: $test: expr)*) => {
$(
#[tokio::test]
async fn $name() {
use std::collections::HashMap;
use dockertest::{
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
TestBodySpecification, DockerTest,
};
serai_docker_tests::build("serai-fast-epoch".to_string());
let handle = concat!("serai_client-serai_node-", stringify!($name));
let composition = TestBodySpecification::with_image(
Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never),
)
.replace_cmd(vec![
"serai-node".to_string(),
"--dev".to_string(),
"--unsafe-rpc-external".to_string(),
"--rpc-cors".to_string(),
"all".to_string(),
])
.replace_env(
HashMap::from([
("RUST_LOG".to_string(), "runtime=debug".to_string()),
("KEY".to_string(), " ".to_string()),
])
)
.set_publish_all_ports(true)
.set_handle(handle)
.set_start_policy(StartPolicy::Strict)
.set_log_options(Some(LogOptions {
action: LogAction::Forward,
policy: LogPolicy::Always,
source: LogSource::Both,
}));
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
test.provide_container(composition);
test.run_async(|ops| async move {
// Sleep until the Substrate RPC starts
let serai_rpc = ops.handle(handle).host_port(9944).unwrap();
let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1);
// Bound execution to 60 seconds
for _ in 0 .. 60 {
tokio::time::sleep(core::time::Duration::from_secs(1)).await;
let Ok(client) = Serai::new(serai_rpc.clone()).await else { continue };
if client.latest_finalized_block_hash().await.is_err() {
continue;
}
break;
}
#[allow(clippy::redundant_closure_call)]
$test(Serai::new(serai_rpc).await.unwrap()).await;
}).await;
}
)*
}
}

View File

@@ -1,46 +0,0 @@
use core::time::Duration;
use tokio::time::sleep;
use serai_client::{Transaction, Serai};
#[allow(dead_code)]
pub async fn publish_tx(serai: &Serai, tx: &Transaction) -> [u8; 32] {
let mut latest = serai
.block(serai.latest_finalized_block_hash().await.unwrap())
.await
.unwrap()
.unwrap()
.number();
serai.publish(tx).await.unwrap();
// Get the block it was included in
// TODO: Add an RPC method for this/check the guarantee on the subscription
let mut ticks = 0;
loop {
latest += 1;
let block = {
let mut block;
while {
block = serai.finalized_block_by_number(latest).await.unwrap();
block.is_none()
} {
sleep(Duration::from_secs(1)).await;
ticks += 1;
if ticks > 60 {
panic!("60 seconds without inclusion in a finalized block");
}
}
block.unwrap()
};
for transaction in &block.transactions {
if transaction == tx {
return block.hash();
}
}
}
}

View File

@@ -1,132 +0,0 @@
use std::collections::HashMap;
use zeroize::Zeroizing;
use rand_core::OsRng;
use frost::curve::Ristretto;
use ciphersuite::{WrappedGroup, GroupIo};
use dkg_musig::musig;
use schnorrkel::Schnorrkel;
use sp_core::{
ConstU32,
bounded::BoundedVec,
sr25519::{Pair, Signature},
Pair as PairTrait,
};
use serai_abi::primitives::NetworkId;
use serai_client::{
primitives::{EmbeddedEllipticCurve, Amount},
validator_sets::{
primitives::{MAX_KEY_LEN, ExternalValidatorSet, KeyPair, musig_context, set_keys_message},
ValidatorSetsEvent,
},
SeraiValidatorSets, Serai,
};
use crate::common::tx::publish_tx;
#[allow(dead_code)]
pub async fn set_keys(
serai: &Serai,
set: ExternalValidatorSet,
key_pair: KeyPair,
pairs: &[Pair],
) -> [u8; 32] {
let mut pub_keys = vec![];
for pair in pairs {
let public_key =
<Ristretto as GroupIo>::read_G::<&[u8]>(&mut pair.public().0.as_ref()).unwrap();
pub_keys.push(public_key);
}
let mut threshold_keys = vec![];
for i in 0 .. pairs.len() {
let secret_key = <Ristretto as GroupIo>::read_F::<&[u8]>(
&mut pairs[i].as_ref().secret.to_bytes()[.. 32].as_ref(),
)
.unwrap();
assert_eq!(Ristretto::generator() * secret_key, pub_keys[i]);
threshold_keys.push(
musig::<Ristretto>(musig_context(set.into()), Zeroizing::new(secret_key), &pub_keys).unwrap(),
);
}
let mut musig_keys = HashMap::new();
for threshold_keys in threshold_keys {
musig_keys.insert(threshold_keys.params().i(), threshold_keys);
}
let sig = frost::tests::sign_without_caching(
&mut OsRng,
frost::tests::algorithm_machines(&mut OsRng, &Schnorrkel::new(b"substrate"), &musig_keys),
&set_keys_message(&set, &key_pair),
);
// Set the key pair
let block = publish_tx(
serai,
&SeraiValidatorSets::set_keys(
set.network,
key_pair.clone(),
bitvec::bitvec!(u8, bitvec::prelude::Lsb0; 1; musig_keys.len()),
Signature(sig.to_bytes()),
),
)
.await;
assert_eq!(
serai.as_of(block).validator_sets().key_gen_events().await.unwrap(),
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
);
assert_eq!(serai.as_of(block).validator_sets().keys(set).await.unwrap(), Some(key_pair));
block
}
#[allow(dead_code)]
pub async fn set_embedded_elliptic_curve_key(
serai: &Serai,
pair: &Pair,
embedded_elliptic_curve: EmbeddedEllipticCurve,
key: BoundedVec<u8, ConstU32<{ MAX_KEY_LEN }>>,
nonce: u32,
) -> [u8; 32] {
// get the call
let tx = serai.sign(
pair,
SeraiValidatorSets::set_embedded_elliptic_curve_key(embedded_elliptic_curve, key),
nonce,
0,
);
publish_tx(serai, &tx).await
}
#[allow(dead_code)]
pub async fn allocate_stake(
serai: &Serai,
network: NetworkId,
amount: Amount,
pair: &Pair,
nonce: u32,
) -> [u8; 32] {
// get the call
let tx = serai.sign(pair, SeraiValidatorSets::allocate(network, amount), nonce, 0);
publish_tx(serai, &tx).await
}
#[allow(dead_code)]
pub async fn deallocate_stake(
serai: &Serai,
network: NetworkId,
amount: Amount,
pair: &Pair,
nonce: u32,
) -> [u8; 32] {
// get the call
let tx = serai.sign(pair, SeraiValidatorSets::deallocate(network, amount), nonce, 0);
publish_tx(serai, &tx).await
}

View File

@@ -1,434 +0,0 @@
use rand_core::{RngCore, OsRng};
use sp_core::{Pair as PairTrait, bounded::BoundedVec};
use serai_abi::in_instructions::primitives::DexCall;
use serai_client::{
primitives::{
BlockHash, ExternalCoin, Coin, Amount, ExternalBalance, Balance, SeraiAddress, ExternalAddress,
insecure_pair_from_name,
},
in_instructions::primitives::{
InInstruction, InInstructionWithBalance, Batch, IN_INSTRUCTION_EXECUTOR, OutAddress,
},
dex::DexEvent,
Serai,
};
mod common;
use common::{
in_instructions::{provide_batch, mint_coin},
dex::{add_liquidity as common_add_liquidity, swap as common_swap},
};
// TODO: Calculate all constants in the following tests
// TODO: Check LP token, coin balances
// TODO: Modularize common code
// TODO: Check Transfer events
serai_test!(
add_liquidity: (|serai: Serai| async move {
let coin = ExternalCoin::Monero;
let pair = insecure_pair_from_name("Ferdie");
// mint sriXMR in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
ExternalBalance { coin, amount: Amount(100_000_000_000_000) },
0,
pair.clone().public().into(),
)
.await;
// add liquidity
let coin_amount = Amount(50_000_000_000_000);
let sri_amount = Amount(50_000_000_000_000);
let block = common_add_liquidity(&serai,
coin,
coin_amount,
sri_amount,
0,
pair.clone()
).await;
// get only the add liq events
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::LiquidityAdded { .. }));
assert_eq!(
events,
vec![DexEvent::LiquidityAdded {
who: pair.public().into(),
mint_to: pair.public().into(),
pool_id: coin,
coin_amount: coin_amount.0,
sri_amount: sri_amount.0,
lp_token_minted: 49_999999990000
}]
);
})
// Tests coin -> SRI and SRI -> coin swaps.
swap_coin_to_sri: (|serai: Serai| async move {
let coin = ExternalCoin::Ether;
let pair = insecure_pair_from_name("Ferdie");
// mint sriXMR in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
ExternalBalance { coin, amount: Amount(100_000_000_000_000) },
0,
pair.clone().public().into(),
)
.await;
// add liquidity
common_add_liquidity(&serai,
coin,
Amount(50_000_000_000_000),
Amount(50_000_000_000_000),
0,
pair.clone()
).await;
// now that we have our liquid pool, swap some coin to SRI.
let mut amount_in = Amount(25_000_000_000_000);
let mut block = common_swap(
&serai,
coin.into(),
Coin::Serai,
amount_in,
Amount(1),
1,
pair.clone())
.await;
// get only the swap events
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let mut path = BoundedVec::try_from(vec![coin.into(), Coin::Serai]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: pair.clone().public().into(),
send_to: pair.public().into(),
path,
amount_in: amount_in.0,
amount_out: 16633299966633
}]
);
// now swap some SRI to coin
amount_in = Amount(10_000_000_000_000);
block = common_swap(
&serai,
Coin::Serai,
coin.into(),
amount_in,
Amount(1),
2,
pair.clone()
).await;
// get only the swap events
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
path = BoundedVec::try_from(vec![Coin::Serai, coin.into()]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: pair.clone().public().into(),
send_to: pair.public().into(),
path,
amount_in: amount_in.0,
amount_out: 17254428681101
}]
);
})
swap_coin_to_coin: (|serai: Serai| async move {
let coin1 = ExternalCoin::Monero;
let coin2 = ExternalCoin::Dai;
let pair = insecure_pair_from_name("Ferdie");
// mint coins
mint_coin(
&serai,
ExternalBalance { coin: coin1, amount: Amount(100_000_000_000_000) },
0,
pair.clone().public().into(),
)
.await;
mint_coin(
&serai,
ExternalBalance { coin: coin2, amount: Amount(100_000_000_000_000) },
0,
pair.clone().public().into(),
)
.await;
// add liquidity to pools
common_add_liquidity(&serai,
coin1,
Amount(50_000_000_000_000),
Amount(50_000_000_000_000),
0,
pair.clone()
).await;
common_add_liquidity(&serai,
coin2,
Amount(50_000_000_000_000),
Amount(50_000_000_000_000),
1,
pair.clone()
).await;
// swap coin1 -> coin2
let amount_in = Amount(25_000_000_000_000);
let block = common_swap(
&serai,
coin1.into(),
coin2.into(),
amount_in,
Amount(1),
2,
pair.clone()
).await;
// get only the swap events
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai, coin2.into()]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: pair.clone().public().into(),
send_to: pair.public().into(),
path,
amount_in: amount_in.0,
amount_out: 12453103964435,
}]
);
})
add_liquidity_in_instructions: (|serai: Serai| async move {
let coin = ExternalCoin::Bitcoin;
let pair = insecure_pair_from_name("Ferdie");
let mut batch_id = 0;
// mint sriBTC in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
ExternalBalance { coin, amount: Amount(100_000_000_000_000) },
batch_id,
pair.clone().public().into(),
)
.await;
batch_id += 1;
// add liquidity
common_add_liquidity(&serai,
coin,
Amount(5_000_000_000_000),
Amount(500_000_000_000),
0,
pair.clone()
).await;
// now that we have our liquid SRI/BTC pool, we can add more liquidity to it via an
// InInstruction
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: coin.network(),
id: batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(pair.public().into())),
balance: ExternalBalance { coin, amount: Amount(20_000_000_000_000) },
}],
};
let block = provide_batch(&serai, batch).await;
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::LiquidityAdded { .. }));
assert_eq!(
events,
vec![DexEvent::LiquidityAdded {
who: IN_INSTRUCTION_EXECUTOR,
mint_to: pair.public().into(),
pool_id: coin,
coin_amount: 10_000_000_000_000, // half of sent amount
sri_amount: 111_333_778_668,
lp_token_minted: 1_054_092_553_383
}]
);
})
swap_in_instructions: (|serai: Serai| async move {
let coin1 = ExternalCoin::Monero;
let coin2 = ExternalCoin::Ether;
let pair = insecure_pair_from_name("Ferdie");
let mut coin1_batch_id = 0;
let mut coin2_batch_id = 0;
// mint coins
mint_coin(
&serai,
ExternalBalance { coin: coin1, amount: Amount(10_000_000_000_000_000) },
coin1_batch_id,
pair.clone().public().into(),
)
.await;
coin1_batch_id += 1;
mint_coin(
&serai,
ExternalBalance { coin: coin2, amount: Amount(100_000_000_000_000) },
coin2_batch_id,
pair.clone().public().into(),
)
.await;
coin2_batch_id += 1;
// add liquidity to pools
common_add_liquidity(&serai,
coin1,
Amount(5_000_000_000_000_000), // monero has 12 decimals
Amount(50_000_000_000),
0,
pair.clone()
).await;
common_add_liquidity(&serai,
coin2,
Amount(5_000_000_000_000), // ether still has 8 in our codebase
Amount(500_000_000_000),
1,
pair.clone()
).await;
// rand address bytes
let mut rand_bytes = vec![0; 32];
OsRng.fill_bytes(&mut rand_bytes);
// XMR -> ETH
{
// make an out address
let out_address = OutAddress::External(ExternalAddress::new(rand_bytes.clone()).unwrap());
// amount is the min out amount
let out_balance = Balance { coin: coin2.into(), amount: Amount(1) };
// now that we have our pools, we can try to swap
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: coin1.network(),
id: coin1_batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address)),
balance: ExternalBalance { coin: coin1, amount: Amount(200_000_000_000_000) },
}],
};
let block = provide_batch(&serai, batch).await;
coin1_batch_id += 1;
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai, coin2.into()]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: IN_INSTRUCTION_EXECUTOR,
send_to: IN_INSTRUCTION_EXECUTOR,
path,
amount_in: 200_000_000_000_000,
amount_out: 19_044_944_233
}]
);
}
// ETH -> sriXMR
{
// make an out address
let out_address =
OutAddress::Serai(SeraiAddress::new(rand_bytes.clone().try_into().unwrap()));
// amount is the min out amount
let out_balance = Balance { coin: coin1.into(), amount: Amount(1) };
// now that we have our pools, we can try to swap
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: coin2.network(),
id: coin2_batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())),
balance: ExternalBalance { coin: coin2, amount: Amount(200_000_000_000) },
}],
};
let block = provide_batch(&serai, batch).await;
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin2.into(), Coin::Serai, coin1.into()]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: IN_INSTRUCTION_EXECUTOR,
send_to: out_address.as_native().unwrap(),
path,
amount_in: 200_000_000_000,
amount_out: 1487294253782353
}]
);
}
// XMR -> SRI
{
// make an out address
let out_address = OutAddress::Serai(SeraiAddress::new(rand_bytes.try_into().unwrap()));
// amount is the min out amount
let out_balance = Balance { coin: Coin::Serai, amount: Amount(1) };
// now that we have our pools, we can try to swap
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: coin1.network(),
id: coin1_batch_id,
external_network_block_hash: block_hash,
instructions: vec![InInstructionWithBalance {
instruction: InInstruction::Dex(DexCall::Swap(out_balance, out_address.clone())),
balance: ExternalBalance { coin: coin1, amount: Amount(100_000_000_000_000) },
}],
};
let block = provide_batch(&serai, batch).await;
let mut events = serai.as_of(block).dex().events().await.unwrap();
events.retain(|e| matches!(e, DexEvent::SwapExecuted { .. }));
let path = BoundedVec::try_from(vec![coin1.into(), Coin::Serai]).unwrap();
assert_eq!(
events,
vec![DexEvent::SwapExecuted {
who: IN_INSTRUCTION_EXECUTOR,
send_to: out_address.as_native().unwrap(),
path,
amount_in: 100_000_000_000_000,
amount_out: 1_762_662_819
}]
);
}
})
);

View File

@@ -1,53 +0,0 @@
use serai_client::{primitives::ExternalNetworkId, Serai};
#[tokio::test]
async fn dht() {
use dockertest::{
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
TestBodySpecification, DockerTest,
};
serai_docker_tests::build("serai".to_string());
let handle = |name: &str| format!("serai_client-serai_node-{name}");
let composition = |name: &str| {
TestBodySpecification::with_image(
Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never),
)
.replace_env(
[("SERAI_NAME".to_string(), name.to_string()), ("KEY".to_string(), " ".to_string())].into(),
)
.set_publish_all_ports(true)
.set_handle(handle(name))
.set_start_policy(StartPolicy::Strict)
.set_log_options(Some(LogOptions {
action: LogAction::Forward,
policy: LogPolicy::Always,
source: LogSource::Both,
}))
};
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
test.provide_container(composition("alice"));
test.provide_container(composition("bob"));
test.provide_container(composition("charlie"));
test.provide_container(composition("dave"));
test
.run_async(|ops| async move {
// Sleep until the Substrate RPC starts
let alice = handle("alice");
let serai_rpc = ops.handle(&alice).host_port(9944).unwrap();
let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1);
// Sleep for a minute
tokio::time::sleep(core::time::Duration::from_secs(60)).await;
// Check the DHT has been populated
assert!(!Serai::new(serai_rpc.clone())
.await
.unwrap()
.p2p_validators(ExternalNetworkId::Bitcoin)
.await
.unwrap()
.is_empty());
})
.await;
}

View File

@@ -1,260 +0,0 @@
use std::{time::Duration, collections::HashMap};
use rand_core::{RngCore, OsRng};
use serai_client::TemporalSerai;
use serai_abi::{
primitives::{
EXTERNAL_NETWORKS, NETWORKS, TARGET_BLOCK_TIME, FAST_EPOCH_DURATION, FAST_EPOCH_INITIAL_PERIOD,
BlockHash, ExternalNetworkId, NetworkId, ExternalCoin, Amount, ExternalBalance,
},
validator_sets::primitives::Session,
emissions::primitives::{INITIAL_REWARD_PER_BLOCK, SECURE_BY},
in_instructions::primitives::Batch,
};
use serai_client::Serai;
mod common;
use common::{genesis_liquidity::set_up_genesis, in_instructions::provide_batch};
serai_test_fast_epoch!(
emissions: (|serai: Serai| async move {
test_emissions(serai).await;
})
);
async fn send_batches(serai: &Serai, ids: &mut HashMap<ExternalNetworkId, u32>) {
for network in EXTERNAL_NETWORKS {
// set up batch id
ids
.entry(network)
.and_modify(|v| {
*v += 1;
})
.or_insert(0);
// set up block hash
let mut block = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block.0);
provide_batch(
serai,
Batch {
network,
id: ids[&network],
external_network_block_hash: block,
instructions: vec![],
},
)
.await;
}
}
async fn test_emissions(serai: Serai) {
// set up the genesis
let values = HashMap::from([
(ExternalCoin::Monero, 184100),
(ExternalCoin::Ether, 4785000),
(ExternalCoin::Dai, 1500),
]);
let (_, mut batch_ids) = set_up_genesis(&serai, &values).await;
// wait until genesis is complete
let mut genesis_complete_block = None;
while genesis_complete_block.is_none() {
tokio::time::sleep(Duration::from_secs(1)).await;
genesis_complete_block = serai
.as_of_latest_finalized_block()
.await
.unwrap()
.genesis_liquidity()
.genesis_complete_block()
.await
.unwrap();
}
for _ in 0 .. 3 {
// get current stakes
let mut current_stake = HashMap::new();
for n in NETWORKS {
// TODO: investigate why serai network TAS isn't visible at session 0.
let stake = serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.total_allocated_stake(n)
.await
.unwrap()
.unwrap_or(Amount(0))
.0;
current_stake.insert(n, stake);
}
// wait for a session change
let current_session = wait_for_session_change(&serai).await;
// get last block
let last_block = serai.latest_finalized_block().await.unwrap();
let serai_latest = serai.as_of(last_block.hash());
let change_block_number = last_block.number();
// get distances to ec security & block count of the previous session
let (distances, total_distance) = get_distances(&serai_latest, &current_stake).await;
let block_count = get_session_blocks(&serai_latest, current_session - 1).await;
// calculate how much reward in this session
let reward_this_epoch =
if change_block_number < (genesis_complete_block.unwrap() + FAST_EPOCH_INITIAL_PERIOD) {
block_count * INITIAL_REWARD_PER_BLOCK
} else {
let blocks_until = SECURE_BY - change_block_number;
let block_reward = total_distance / blocks_until;
block_count * block_reward
};
let reward_per_network = distances
.into_iter()
.map(|(n, distance)| {
let reward = u64::try_from(
u128::from(reward_this_epoch).saturating_mul(u128::from(distance)) /
u128::from(total_distance),
)
.unwrap();
(n, reward)
})
.collect::<HashMap<NetworkId, u64>>();
// retire the prev-set so that TotalAllocatedStake updated.
send_batches(&serai, &mut batch_ids).await;
for (n, reward) in reward_per_network {
let stake = serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.total_allocated_stake(n)
.await
.unwrap()
.unwrap_or(Amount(0))
.0;
// all reward should automatically staked for the network since we are in initial period.
assert_eq!(stake, *current_stake.get(&n).unwrap() + reward);
}
// TODO: check stake per address?
// TODO: check post ec security era
}
}
/// Returns the required stake in terms SRI for a given `Balance`.
async fn required_stake(serai: &TemporalSerai<'_>, balance: ExternalBalance) -> u64 {
// This is inclusive to an increase in accuracy
let sri_per_coin = serai.dex().oracle_value(balance.coin).await.unwrap().unwrap_or(Amount(0));
// See dex-pallet for the reasoning on these
let coin_decimals = balance.coin.decimals().max(5);
let accuracy_increase = u128::from(10u64.pow(coin_decimals));
let total_coin_value =
u64::try_from(u128::from(balance.amount.0) * u128::from(sri_per_coin.0) / accuracy_increase)
.unwrap_or(u64::MAX);
// required stake formula (COIN_VALUE * 1.5) + margin(20%)
let required_stake = total_coin_value.saturating_mul(3).saturating_div(2);
required_stake.saturating_add(total_coin_value.saturating_div(5))
}
async fn wait_for_session_change(serai: &Serai) -> u32 {
let current_session = serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.session(NetworkId::Serai)
.await
.unwrap()
.unwrap()
.0;
let next_session = current_session + 1;
// lets wait double the epoch time.
tokio::time::timeout(
tokio::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2),
async {
while serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.session(NetworkId::Serai)
.await
.unwrap()
.unwrap()
.0 <
next_session
{
tokio::time::sleep(Duration::from_secs(6)).await;
}
},
)
.await
.unwrap();
next_session
}
async fn get_distances(
serai: &TemporalSerai<'_>,
current_stake: &HashMap<NetworkId, u64>,
) -> (HashMap<NetworkId, u64>, u64) {
// we should be in the initial period, so calculate how much each network supposedly get..
// we can check the supply to see how much coin hence liability we have.
let mut distances: HashMap<NetworkId, u64> = HashMap::new();
let mut total_distance = 0;
for n in EXTERNAL_NETWORKS {
let mut required = 0;
for c in n.coins() {
let amount = serai.coins().coin_supply(c.into()).await.unwrap();
required += required_stake(serai, ExternalBalance { coin: c, amount }).await;
}
let mut current = *current_stake.get(&n.into()).unwrap();
if current > required {
current = required;
}
let distance = required - current;
total_distance += distance;
distances.insert(n.into(), distance);
}
// add serai network portion(20%)
let new_total_distance = total_distance.saturating_mul(10) / 8;
distances.insert(NetworkId::Serai, new_total_distance - total_distance);
total_distance = new_total_distance;
(distances, total_distance)
}
async fn get_session_blocks(serai: &TemporalSerai<'_>, session: u32) -> u64 {
let begin_block = serai
.validator_sets()
.session_begin_block(NetworkId::Serai, Session(session))
.await
.unwrap()
.unwrap();
let next_begin_block = serai
.validator_sets()
.session_begin_block(NetworkId::Serai, Session(session + 1))
.await
.unwrap()
.unwrap();
next_begin_block.saturating_sub(begin_block)
}

View File

@@ -1,111 +0,0 @@
use std::{time::Duration, collections::HashMap};
use serai_client::Serai;
use serai_abi::primitives::{Amount, Coin, ExternalCoin, COINS, EXTERNAL_COINS, GENESIS_SRI};
use serai_client::genesis_liquidity::primitives::{
GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES,
};
mod common;
use common::genesis_liquidity::set_up_genesis;
serai_test_fast_epoch!(
genesis_liquidity: (|serai: Serai| async move {
test_genesis_liquidity(serai).await;
})
);
pub async fn test_genesis_liquidity(serai: Serai) {
// set up the genesis
let values = HashMap::from([
(ExternalCoin::Monero, 184100),
(ExternalCoin::Ether, 4785000),
(ExternalCoin::Dai, 1500),
]);
let (accounts, _) = set_up_genesis(&serai, &values).await;
// wait until genesis is complete
while serai
.as_of_latest_finalized_block()
.await
.unwrap()
.genesis_liquidity()
.genesis_complete_block()
.await
.unwrap()
.is_none()
{
tokio::time::sleep(Duration::from_secs(1)).await;
}
// check total SRI supply is +100M
// there are 6 endowed accounts in dev-net. Take this into consideration when checking
// for the total sri minted at this time.
let serai = serai.as_of_latest_finalized_block().await.unwrap();
let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap();
let endowed_amount: u64 = 1 << 60;
let total_sri = (6 * endowed_amount) + GENESIS_SRI;
assert_eq!(sri, Amount(total_sri));
// check genesis account has no coins, all transferred to pools.
for coin in COINS {
let amount = serai.coins().coin_balance(coin, GENESIS_LIQUIDITY_ACCOUNT).await.unwrap();
assert_eq!(amount.0, 0);
}
// check pools has proper liquidity
let mut pool_amounts = HashMap::new();
let mut total_value = 0u128;
for coin in EXTERNAL_COINS {
let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0));
let value = if coin != ExternalCoin::Bitcoin {
(total_coin * u128::from(values[&coin])) / 10u128.pow(coin.decimals())
} else {
total_coin
};
total_value += value;
pool_amounts.insert(coin, (total_coin, value));
}
// check distributed SRI per pool
let mut total_sri_distributed = 0u128;
for coin in EXTERNAL_COINS {
let sri = if coin == *EXTERNAL_COINS.last().unwrap() {
u128::from(GENESIS_SRI).checked_sub(total_sri_distributed).unwrap()
} else {
(pool_amounts[&coin].1 * u128::from(GENESIS_SRI)) / total_value
};
total_sri_distributed += sri;
let reserves = serai.dex().get_reserves(coin).await.unwrap().unwrap();
assert_eq!(u128::from(reserves.0 .0), pool_amounts[&coin].0); // coin side
assert_eq!(u128::from(reserves.1 .0), sri); // SRI side
}
// check each liquidity provider got liquidity tokens proportional to their value
for coin in EXTERNAL_COINS {
let liq_supply = serai.genesis_liquidity().supply(coin).await.unwrap();
for (acc, amount) in &accounts[&coin] {
let acc_liq_shares = serai.genesis_liquidity().liquidity(acc, coin).await.unwrap().shares;
// since we can't test the ratios directly(due to integer division giving 0)
// we test whether they give the same result when multiplied by another constant.
// Following test ensures the account in fact has the right amount of shares.
let mut shares_ratio = (INITIAL_GENESIS_LP_SHARES * acc_liq_shares) / liq_supply.shares;
let amounts_ratio =
(INITIAL_GENESIS_LP_SHARES * amount.0) / u64::try_from(pool_amounts[&coin].0).unwrap();
// we can tolerate 1 unit diff between them due to integer division.
if shares_ratio.abs_diff(amounts_ratio) == 1 {
shares_ratio = amounts_ratio;
}
assert_eq!(shares_ratio, amounts_ratio);
}
}
// TODO: test remove the liq before/after genesis ended.
}

View File

@@ -1,157 +0,0 @@
use std::str::FromStr;
use scale::Decode;
use zeroize::Zeroizing;
use ciphersuite::{
group::{ff::Field, GroupEncoding},
WrappedGroup,
};
use dalek_ff_group::Ed25519;
use ciphersuite_kp256::Secp256k1;
use sp_core::{
Pair as PairTrait,
sr25519::{Public, Pair},
};
use serai_abi::{
in_instructions::primitives::Shorthand,
primitives::{
insecure_pair_from_name, ExternalBalance, ExternalCoin, ExternalNetworkId, QuotePriceParams,
Amount,
},
validator_sets::primitives::{ExternalValidatorSet, KeyPair, Session},
};
use serai_client::{Serai, SeraiAddress};
use rand_core::{RngCore, OsRng};
mod common;
use common::{validator_sets::set_keys, in_instructions::mint_coin, dex::add_liquidity};
serai_test!(
external_address: (|serai: Serai| async move {
test_external_address(serai).await;
})
encoded_shorthand: (|serai: Serai| async move {
test_encoded_shorthand(serai).await;
})
dex_quote_price: (|serai: Serai| async move {
test_dex_quote_price(serai).await;
})
);
async fn set_network_keys<C: WrappedGroup>(
serai: &Serai,
set: ExternalValidatorSet,
pairs: &[Pair],
) {
// Ristretto key
let mut ristretto_key = [0; 32];
OsRng.fill_bytes(&mut ristretto_key);
// network key
let network_priv_key = Zeroizing::new(C::F::random(&mut OsRng));
let network_key = (C::generator() * *network_priv_key).to_bytes().as_ref().to_vec();
let key_pair = KeyPair(Public(ristretto_key), network_key.try_into().unwrap());
let _ = set_keys(serai, set, key_pair, pairs).await;
}
async fn test_external_address(serai: Serai) {
let pair = insecure_pair_from_name("Alice");
// set btc keys
let network = ExternalNetworkId::Bitcoin;
set_network_keys::<Secp256k1>(
&serai,
ExternalValidatorSet { session: Session(0), network },
core::slice::from_ref(&pair),
)
.await;
// get the address from the node
let btc_address: String = serai.external_network_address(network).await.unwrap();
// make sure it is a valid address
let _ = bitcoin::Address::from_str(&btc_address)
.unwrap()
.require_network(bitcoin::Network::Bitcoin)
.unwrap();
// set monero keys
let network = ExternalNetworkId::Monero;
set_network_keys::<Ed25519>(
&serai,
ExternalValidatorSet { session: Session(0), network },
&[pair],
)
.await;
// get the address from the node
let xmr_address: String = serai.external_network_address(network).await.unwrap();
// make sure it is a valid address
let _ = monero_address::MoneroAddress::from_str(monero_address::Network::Mainnet, &xmr_address)
.unwrap();
}
async fn test_encoded_shorthand(serai: Serai) {
let shorthand = Shorthand::transfer(None, SeraiAddress::new([0u8; 32]));
let encoded = serai.encoded_shorthand(shorthand.clone()).await.unwrap();
assert_eq!(Shorthand::decode::<&[u8]>(&mut encoded.as_slice()).unwrap(), shorthand);
}
async fn test_dex_quote_price(serai: Serai) {
// make a liquid pool to get the quote on
let coin1 = ExternalCoin::Bitcoin;
let coin2 = ExternalCoin::Monero;
let amount1 = Amount(10u64.pow(coin1.decimals()));
let amount2 = Amount(10u64.pow(coin2.decimals()));
let pair = insecure_pair_from_name("Ferdie");
// mint sriBTC in the account so that we can add liq.
// Ferdie account is already pre-funded with SRI.
mint_coin(
&serai,
ExternalBalance { coin: coin1, amount: amount1 },
0,
pair.clone().public().into(),
)
.await;
// add liquidity
let coin_amount = Amount(amount1.0 / 2);
let sri_amount = Amount(amount1.0 / 2);
let _ = add_liquidity(&serai, coin1, coin_amount, sri_amount, 0, pair.clone()).await;
// same for xmr
mint_coin(
&serai,
ExternalBalance { coin: coin2, amount: amount2 },
0,
pair.clone().public().into(),
)
.await;
// add liquidity
let coin_amount = Amount(amount2.0 / 2);
let sri_amount = Amount(amount2.0 / 2);
let _ = add_liquidity(&serai, coin2, coin_amount, sri_amount, 1, pair.clone()).await;
// price for BTC -> SRI -> XMR path
let params = QuotePriceParams {
coin1: coin1.into(),
coin2: coin2.into(),
amount: coin_amount.0 / 2,
include_fee: true,
exact_in: true,
};
let res = serai.quote_price(params).await.unwrap();
assert!(res > 0);
}

View File

@@ -1,28 +0,0 @@
use std::time::{Duration, SystemTime};
use tokio::time::sleep;
use serai_client::Serai;
mod common;
serai_test!(
time: (|serai: Serai| async move {
let mut number = serai.latest_finalized_block().await.unwrap().number();
let mut done = 0;
while done < 3 {
// Wait for the next block
let block = serai.latest_finalized_block().await.unwrap();
if block.number() == number {
sleep(Duration::from_secs(1)).await;
continue;
}
number = block.number();
// Make sure the time we extract from the block is within 5 seconds of now
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
assert!(now.saturating_sub(block.time().unwrap()) < 5);
done += 1;
}
})
);

View File

@@ -1,428 +0,0 @@
use rand_core::{RngCore, OsRng};
use sp_core::{
sr25519::{Public, Pair},
Pair as PairTrait,
};
use serai_client::{
primitives::{
FAST_EPOCH_DURATION, TARGET_BLOCK_TIME, NETWORKS, BlockHash, ExternalNetworkId, NetworkId,
EmbeddedEllipticCurve, Amount, insecure_pair_from_name,
},
validator_sets::{
primitives::{Session, ExternalValidatorSet, ValidatorSet, KeyPair},
ValidatorSetsEvent,
},
in_instructions::{
primitives::{Batch, SignedBatch, batch_message},
SeraiInInstructions,
},
Serai,
};
mod common;
use common::{
tx::publish_tx,
validator_sets::{set_embedded_elliptic_curve_key, allocate_stake, deallocate_stake, set_keys},
};
fn get_random_key_pair() -> KeyPair {
let mut ristretto_key = [0; 32];
OsRng.fill_bytes(&mut ristretto_key);
let mut external_key = vec![0; 33];
OsRng.fill_bytes(&mut external_key);
KeyPair(Public(ristretto_key), external_key.try_into().unwrap())
}
async fn get_ordered_keys(serai: &Serai, network: NetworkId, accounts: &[Pair]) -> Vec<Pair> {
// retrieve the current session validators so that we know the order of the keys
// that is necessary for the correct musig signature.
let validators = serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.active_network_validators(network)
.await
.unwrap();
// collect the pairs of the validators
let mut pairs = vec![];
for v in validators {
let p = accounts.iter().find(|pair| pair.public() == v).unwrap().clone();
pairs.push(p);
}
pairs
}
serai_test!(
set_keys_test: (|serai: Serai| async move {
let network = ExternalNetworkId::Bitcoin;
let set = ExternalValidatorSet { session: Session(0), network };
let pair = insecure_pair_from_name("Alice");
let public = pair.public();
// Neither of these keys are validated
// The external key is infeasible to validate on-chain, the Ristretto key is feasible
// TODO: Should the Ristretto key be validated?
let key_pair = get_random_key_pair();
// Make sure the genesis is as expected
assert_eq!(
serai
.as_of(serai.finalized_block_by_number(0).await.unwrap().unwrap().hash())
.validator_sets()
.new_set_events()
.await
.unwrap(),
NETWORKS
.iter()
.copied()
.map(|network| ValidatorSetsEvent::NewSet {
set: ValidatorSet { session: Session(0), network }
})
.collect::<Vec<_>>(),
);
{
let vs_serai = serai.as_of_latest_finalized_block().await.unwrap();
let vs_serai = vs_serai.validator_sets();
let participants = vs_serai.participants(set.network.into()).await
.unwrap()
.unwrap()
.into_iter()
.map(|(k, _)| k)
.collect::<Vec<_>>();
let participants_ref: &[_] = participants.as_ref();
assert_eq!(participants_ref, [public].as_ref());
}
let block = set_keys(&serai, set, key_pair.clone(), &[pair]).await;
// While the set_keys function should handle this, it's beneficial to
// independently test it
let serai = serai.as_of(block);
let serai = serai.validator_sets();
assert_eq!(
serai.key_gen_events().await.unwrap(),
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
);
assert_eq!(serai.keys(set).await.unwrap(), Some(key_pair));
})
);
#[tokio::test]
async fn validator_set_rotation() {
use dockertest::{
PullPolicy, StartPolicy, LogOptions, LogAction, LogPolicy, LogSource, Image,
TestBodySpecification, DockerTest,
};
use std::collections::HashMap;
serai_docker_tests::build("serai-fast-epoch".to_string());
let handle = |name| format!("serai_client-serai_node-{name}");
let composition = |name| {
TestBodySpecification::with_image(
Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never),
)
.replace_cmd(vec![
"serai-node".to_string(),
"--unsafe-rpc-external".to_string(),
"--rpc-cors".to_string(),
"all".to_string(),
"--chain".to_string(),
"local".to_string(),
format!("--{name}"),
])
.replace_env(HashMap::from([
("RUST_LOG".to_string(), "runtime=debug".to_string()),
("KEY".to_string(), " ".to_string()),
]))
.set_publish_all_ports(true)
.set_handle(handle(name))
.set_start_policy(StartPolicy::Strict)
.set_log_options(Some(LogOptions {
action: LogAction::Forward,
policy: LogPolicy::Always,
source: LogSource::Both,
}))
};
let mut test = DockerTest::new().with_network(dockertest::Network::Isolated);
test.provide_container(composition("alice"));
test.provide_container(composition("bob"));
test.provide_container(composition("charlie"));
test.provide_container(composition("dave"));
test.provide_container(composition("eve"));
test
.run_async(|ops| async move {
// Sleep until the Substrate RPC starts
let alice = handle("alice");
let alice_rpc = ops.handle(&alice).host_port(9944).unwrap();
let alice_rpc = format!("http://{}:{}", alice_rpc.0, alice_rpc.1);
// Sleep for some time
tokio::time::sleep(core::time::Duration::from_secs(20)).await;
let serai = Serai::new(alice_rpc.clone()).await.unwrap();
// Make sure the genesis is as expected
assert_eq!(
serai
.as_of(serai.finalized_block_by_number(0).await.unwrap().unwrap().hash())
.validator_sets()
.new_set_events()
.await
.unwrap(),
NETWORKS
.iter()
.copied()
.map(|network| ValidatorSetsEvent::NewSet {
set: ValidatorSet { session: Session(0), network }
})
.collect::<Vec<_>>(),
);
// genesis accounts
let accounts = vec![
insecure_pair_from_name("Alice"),
insecure_pair_from_name("Bob"),
insecure_pair_from_name("Charlie"),
insecure_pair_from_name("Dave"),
insecure_pair_from_name("Eve"),
];
// amounts for single key share per network
let key_shares = HashMap::from([
(NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))),
(NetworkId::External(ExternalNetworkId::Bitcoin), Amount(1_000_000 * 10_u64.pow(8))),
(NetworkId::External(ExternalNetworkId::Monero), Amount(100_000 * 10_u64.pow(8))),
(NetworkId::External(ExternalNetworkId::Ethereum), Amount(1_000_000 * 10_u64.pow(8))),
]);
// genesis participants per network
#[allow(clippy::redundant_closure_for_method_calls)]
let default_participants =
accounts[.. 4].to_vec().iter().map(|pair| pair.public()).collect::<Vec<_>>();
let mut participants = HashMap::from([
(NetworkId::Serai, default_participants.clone()),
(NetworkId::External(ExternalNetworkId::Bitcoin), default_participants.clone()),
(NetworkId::External(ExternalNetworkId::Monero), default_participants.clone()),
(NetworkId::External(ExternalNetworkId::Ethereum), default_participants),
]);
// test the set rotation
for (i, network) in NETWORKS.into_iter().enumerate() {
let participants = participants.get_mut(&network).unwrap();
// we start the chain with 4 default participants that has a single key share each
participants.sort();
verify_session_and_active_validators(&serai, network, 0, participants).await;
// add 1 participant
let last_participant = accounts[4].clone();
// If this is the first iteration, set embedded elliptic curve keys
if i == 0 {
for (i, embedded_elliptic_curve) in
[EmbeddedEllipticCurve::Embedwards25519, EmbeddedEllipticCurve::Secq256k1]
.into_iter()
.enumerate()
{
set_embedded_elliptic_curve_key(
&serai,
&last_participant,
embedded_elliptic_curve,
vec![
0;
match embedded_elliptic_curve {
EmbeddedEllipticCurve::Embedwards25519 => 32,
EmbeddedEllipticCurve::Secq256k1 => 33,
}
]
.try_into()
.unwrap(),
i.try_into().unwrap(),
)
.await;
}
}
let hash = allocate_stake(
&serai,
network,
key_shares[&network],
&last_participant,
(2 + i).try_into().unwrap(),
)
.await;
participants.push(last_participant.public());
// the session at which set changes becomes active
let activation_session = get_session_at_which_changes_activate(&serai, network, hash).await;
// set the keys if it is an external set
if network != NetworkId::Serai {
let set =
ExternalValidatorSet { session: Session(0), network: network.try_into().unwrap() };
let key_pair = get_random_key_pair();
let pairs = get_ordered_keys(&serai, network, &accounts).await;
set_keys(&serai, set, key_pair, &pairs).await;
}
// verify
participants.sort();
verify_session_and_active_validators(&serai, network, activation_session, participants)
.await;
// remove 1 participant
let participant_to_remove = accounts[1].clone();
let hash = deallocate_stake(
&serai,
network,
key_shares[&network],
&participant_to_remove,
i.try_into().unwrap(),
)
.await;
participants.swap_remove(
participants.iter().position(|k| *k == participant_to_remove.public()).unwrap(),
);
let activation_session = get_session_at_which_changes_activate(&serai, network, hash).await;
if network != NetworkId::Serai {
// set the keys if it is an external set
let set =
ExternalValidatorSet { session: Session(1), network: network.try_into().unwrap() };
// we need the whole substrate key pair to sign the batch
let (substrate_pair, key_pair) = {
let pair = insecure_pair_from_name("session-1-key-pair");
let public = pair.public();
let mut external_key = vec![0; 33];
OsRng.fill_bytes(&mut external_key);
(pair, KeyPair(public, external_key.try_into().unwrap()))
};
let pairs = get_ordered_keys(&serai, network, &accounts).await;
set_keys(&serai, set, key_pair, &pairs).await;
// provide a batch to complete the handover and retire the previous set
let mut block_hash = BlockHash([0; 32]);
OsRng.fill_bytes(&mut block_hash.0);
let batch = Batch {
network: network.try_into().unwrap(),
id: 0,
external_network_block_hash: block_hash,
instructions: vec![],
};
publish_tx(
&serai,
&SeraiInInstructions::execute_batch(SignedBatch {
batch: batch.clone(),
signature: substrate_pair.sign(&batch_message(&batch)),
}),
)
.await;
}
// verify
participants.sort();
verify_session_and_active_validators(&serai, network, activation_session, participants)
.await;
// check pending deallocations
let pending = serai
.as_of_latest_finalized_block()
.await
.unwrap()
.validator_sets()
.pending_deallocations(
network,
participant_to_remove.public(),
Session(activation_session + 1),
)
.await
.unwrap();
assert_eq!(pending, Some(key_shares[&network]));
}
})
.await;
}
async fn session_for_block(serai: &Serai, block: [u8; 32], network: NetworkId) -> u32 {
serai.as_of(block).validator_sets().session(network).await.unwrap().unwrap().0
}
async fn verify_session_and_active_validators(
serai: &Serai,
network: NetworkId,
session: u32,
participants: &[Public],
) {
// wait until the active session.
let block = tokio::time::timeout(
core::time::Duration::from_secs(FAST_EPOCH_DURATION * TARGET_BLOCK_TIME * 2),
async move {
loop {
let mut block = serai.latest_finalized_block_hash().await.unwrap();
if session_for_block(serai, block, network).await < session {
// Sleep a block
tokio::time::sleep(core::time::Duration::from_secs(TARGET_BLOCK_TIME)).await;
continue;
}
while session_for_block(serai, block, network).await > session {
block = serai.block(block).await.unwrap().unwrap().header.parent_hash.0;
}
assert_eq!(session_for_block(serai, block, network).await, session);
break block;
}
},
)
.await
.unwrap();
let serai_for_block = serai.as_of(block);
// verify session
let s = serai_for_block.validator_sets().session(network).await.unwrap().unwrap();
assert_eq!(s.0, session);
// verify participants
let mut validators =
serai_for_block.validator_sets().active_network_validators(network).await.unwrap();
validators.sort();
assert_eq!(validators, participants);
// make sure finalization continues as usual after the changes
let current_finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
tokio::time::timeout(core::time::Duration::from_secs(TARGET_BLOCK_TIME * 10), async move {
let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
while finalized_block <= current_finalized_block + 2 {
tokio::time::sleep(core::time::Duration::from_secs(TARGET_BLOCK_TIME)).await;
finalized_block = serai.latest_finalized_block().await.unwrap().header.number;
}
})
.await
.unwrap();
// TODO: verify key shares as well?
}
async fn get_session_at_which_changes_activate(
serai: &Serai,
network: NetworkId,
hash: [u8; 32],
) -> u32 {
let session = session_for_block(serai, hash, network).await;
// changes should be active in the next session
if network == NetworkId::Serai {
// it takes 1 extra session for serai net to make the changes active.
session + 2
} else {
session + 1
}
}