mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Update serai-client to solely be an umbrella crate of the dedicated client libraries
This commit is contained in:
@@ -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"]
|
|
||||||
|
|||||||
4
substrate/client/README.md
Normal file
4
substrate/client/README.md
Normal 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).
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@@ -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));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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, ¤t_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)
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user