mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
Move substrate/serai/* to substrate/*
This commit is contained in:
47
substrate/client/Cargo.toml
Normal file
47
substrate/client/Cargo.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
[package]
|
||||
name = "serai-client"
|
||||
version = "0.1.0"
|
||||
description = "Client library for the Serai network"
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/client"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
keywords = ["serai"]
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "1", optional = true }
|
||||
|
||||
scale = { package = "parity-scale-codec", version = "3" }
|
||||
scale-info = { version = "2", optional = true }
|
||||
|
||||
serai-runtime = { path = "../runtime", version = "0.1" }
|
||||
|
||||
sp-core = { git = "https://github.com/serai-dex/substrate" }
|
||||
subxt = { version = "0.27", default-features = false, features = ["jsonrpsee-ws"], optional = true }
|
||||
|
||||
bitcoin = { version = "0.29", optional = true }
|
||||
|
||||
ciphersuite = { path = "../../crypto/ciphersuite", version = "0.3", optional = true }
|
||||
monero-serai = { path = "../../coins/monero", version = "0.1.4-alpha", optional = true }
|
||||
|
||||
[features]
|
||||
serai = ["thiserror", "scale-info", "subxt"]
|
||||
|
||||
coins = []
|
||||
bitcoin = ["coins", "dep:bitcoin"]
|
||||
monero = ["coins", "ciphersuite/ed25519", "monero-serai"]
|
||||
|
||||
# Assumes the default usage is to use Serai as a DEX, which doesn't actually
|
||||
# require connecting to a Serai node
|
||||
default = ["bitcoin", "monero"]
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1"
|
||||
|
||||
rand_core = "0.6"
|
||||
|
||||
tokio = "1"
|
||||
15
substrate/client/LICENSE
Normal file
15
substrate/client/LICENSE
Normal file
@@ -0,0 +1,15 @@
|
||||
AGPL-3.0-only license
|
||||
|
||||
Copyright (c) 2022-2023 Luke Parker
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License Version 3 as
|
||||
published by the Free Software Foundation.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
0
substrate/client/metadata.json
Normal file
0
substrate/client/metadata.json
Normal file
89
substrate/client/src/coins/bitcoin.rs
Normal file
89
substrate/client/src/coins/bitcoin.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use core::str::FromStr;
|
||||
|
||||
use scale::{Encode, Decode};
|
||||
|
||||
use bitcoin::{
|
||||
hashes::{Hash as HashTrait, hash160::Hash},
|
||||
PubkeyHash, ScriptHash,
|
||||
network::constants::Network,
|
||||
util::address::{Error, WitnessVersion, Payload, Address as BAddress},
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Address(pub BAddress);
|
||||
|
||||
impl FromStr for Address {
|
||||
type Err = Error;
|
||||
fn from_str(str: &str) -> Result<Address, Error> {
|
||||
BAddress::from_str(str).map(Address)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Address {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// SCALE-encoded variant of Monero addresses.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||
enum EncodedAddress {
|
||||
P2PKH([u8; 20]),
|
||||
P2SH([u8; 20]),
|
||||
P2WPKH([u8; 20]),
|
||||
P2WSH([u8; 32]),
|
||||
P2TR([u8; 32]),
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Address {
|
||||
type Error = ();
|
||||
fn try_from(data: Vec<u8>) -> Result<Address, ()> {
|
||||
Ok(Address(BAddress {
|
||||
network: Network::Bitcoin,
|
||||
payload: match EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())? {
|
||||
EncodedAddress::P2PKH(hash) => {
|
||||
Payload::PubkeyHash(PubkeyHash::from_hash(Hash::from_inner(hash)))
|
||||
}
|
||||
EncodedAddress::P2SH(hash) => {
|
||||
Payload::ScriptHash(ScriptHash::from_hash(Hash::from_inner(hash)))
|
||||
}
|
||||
EncodedAddress::P2WPKH(hash) => {
|
||||
Payload::WitnessProgram { version: WitnessVersion::V0, program: hash.to_vec() }
|
||||
}
|
||||
EncodedAddress::P2WSH(hash) => {
|
||||
Payload::WitnessProgram { version: WitnessVersion::V0, program: hash.to_vec() }
|
||||
}
|
||||
EncodedAddress::P2TR(key) => {
|
||||
Payload::WitnessProgram { version: WitnessVersion::V1, program: key.to_vec() }
|
||||
}
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl TryInto<Vec<u8>> for Address {
|
||||
type Error = ();
|
||||
fn try_into(self) -> Result<Vec<u8>, ()> {
|
||||
Ok(
|
||||
(match self.0.payload {
|
||||
Payload::PubkeyHash(hash) => EncodedAddress::P2PKH(hash.as_hash().into_inner()),
|
||||
Payload::ScriptHash(hash) => EncodedAddress::P2SH(hash.as_hash().into_inner()),
|
||||
Payload::WitnessProgram { version: WitnessVersion::V0, program } => {
|
||||
if program.len() == 20 {
|
||||
EncodedAddress::P2WPKH(program.try_into().map_err(|_| ())?)
|
||||
} else if program.len() == 32 {
|
||||
EncodedAddress::P2WSH(program.try_into().map_err(|_| ())?)
|
||||
} else {
|
||||
Err(())?
|
||||
}
|
||||
}
|
||||
Payload::WitnessProgram { version: WitnessVersion::V1, program } => {
|
||||
EncodedAddress::P2TR(program.try_into().map_err(|_| ())?)
|
||||
}
|
||||
_ => Err(())?,
|
||||
})
|
||||
.encode(),
|
||||
)
|
||||
}
|
||||
}
|
||||
5
substrate/client/src/coins/mod.rs
Normal file
5
substrate/client/src/coins/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "bitcoin")]
|
||||
pub mod bitcoin;
|
||||
|
||||
#[cfg(feature = "monero")]
|
||||
pub mod monero;
|
||||
101
substrate/client/src/coins/monero.rs
Normal file
101
substrate/client/src/coins/monero.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use core::str::FromStr;
|
||||
|
||||
use scale::{Encode, Decode};
|
||||
|
||||
use ciphersuite::{Ciphersuite, Ed25519};
|
||||
|
||||
use monero_serai::wallet::address::{AddressError, Network, AddressType, AddressMeta, MoneroAddress};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Address(MoneroAddress);
|
||||
impl Address {
|
||||
pub fn new(address: MoneroAddress) -> Option<Address> {
|
||||
if address.payment_id().is_some() {
|
||||
return None;
|
||||
}
|
||||
Some(Address(address))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Address {
|
||||
type Err = AddressError;
|
||||
fn from_str(str: &str) -> Result<Address, AddressError> {
|
||||
MoneroAddress::from_str(Network::Mainnet, str).map(Address)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Address {
|
||||
fn to_string(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
// SCALE-encoded variant of Monero addresses.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||
enum EncodedAddressType {
|
||||
Standard,
|
||||
Subaddress,
|
||||
Featured(u8),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||
struct EncodedAddress {
|
||||
kind: EncodedAddressType,
|
||||
spend: [u8; 32],
|
||||
view: [u8; 32],
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for Address {
|
||||
type Error = ();
|
||||
fn try_from(data: Vec<u8>) -> Result<Address, ()> {
|
||||
// Decode as SCALE
|
||||
let addr = EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())?;
|
||||
// Convert over
|
||||
Ok(Address(MoneroAddress::new(
|
||||
AddressMeta::new(
|
||||
Network::Mainnet,
|
||||
match addr.kind {
|
||||
EncodedAddressType::Standard => AddressType::Standard,
|
||||
EncodedAddressType::Subaddress => AddressType::Subaddress,
|
||||
EncodedAddressType::Featured(flags) => {
|
||||
let subaddress = (flags & 1) != 0;
|
||||
let integrated = (flags & (1 << 1)) != 0;
|
||||
let guaranteed = (flags & (1 << 2)) != 0;
|
||||
if integrated {
|
||||
Err(())?;
|
||||
}
|
||||
AddressType::Featured { subaddress, payment_id: None, guaranteed }
|
||||
}
|
||||
},
|
||||
),
|
||||
Ed25519::read_G(&mut addr.spend.as_ref()).map_err(|_| ())?.0,
|
||||
Ed25519::read_G(&mut addr.view.as_ref()).map_err(|_| ())?.0,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<MoneroAddress> for Address {
|
||||
fn into(self) -> MoneroAddress {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<Vec<u8>> for Address {
|
||||
fn into(self) -> Vec<u8> {
|
||||
EncodedAddress {
|
||||
kind: match self.0.meta.kind {
|
||||
AddressType::Standard => EncodedAddressType::Standard,
|
||||
AddressType::Subaddress => EncodedAddressType::Subaddress,
|
||||
AddressType::Integrated(_) => panic!("integrated address became Serai Monero address"),
|
||||
AddressType::Featured { subaddress, payment_id: _, guaranteed } => {
|
||||
EncodedAddressType::Featured(u8::from(subaddress) + (u8::from(guaranteed) << 2))
|
||||
}
|
||||
},
|
||||
spend: self.0.spend.compress().0,
|
||||
view: self.0.view.compress().0,
|
||||
}
|
||||
.encode()
|
||||
}
|
||||
}
|
||||
28
substrate/client/src/lib.rs
Normal file
28
substrate/client/src/lib.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
#[cfg(feature = "coins")]
|
||||
pub mod coins;
|
||||
|
||||
#[cfg(feature = "serai")]
|
||||
mod serai;
|
||||
#[cfg(feature = "serai")]
|
||||
pub use serai::*;
|
||||
|
||||
// If we aren't exposing the Serai client (subxt), still expose all primitives
|
||||
#[cfg(not(feature = "serai"))]
|
||||
pub use serai_runtime::primitives;
|
||||
#[cfg(not(feature = "serai"))]
|
||||
mod other_primitives {
|
||||
pub mod in_instructions {
|
||||
pub use serai_runtime::in_instructions::primitives;
|
||||
}
|
||||
pub mod tokens {
|
||||
pub use serai_runtime::tokens::primitives;
|
||||
}
|
||||
pub mod validator_sets {
|
||||
pub use serai_runtime::validator_sets::primitives;
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "serai"))]
|
||||
pub use other_primitives::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
32
substrate/client/src/serai/in_instructions.rs
Normal file
32
substrate/client/src/serai/in_instructions.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use serai_runtime::{in_instructions, InInstructions, Runtime};
|
||||
pub use in_instructions::primitives;
|
||||
use primitives::SignedBatch;
|
||||
|
||||
use subxt::{tx, utils::Encoded};
|
||||
|
||||
use crate::{Serai, SeraiError, scale_composite};
|
||||
|
||||
const PALLET: &str = "InInstructions";
|
||||
|
||||
pub type InInstructionsEvent = in_instructions::Event<Runtime>;
|
||||
|
||||
impl Serai {
|
||||
pub async fn get_batch_events(
|
||||
&self,
|
||||
block: [u8; 32],
|
||||
) -> Result<Vec<InInstructionsEvent>, SeraiError> {
|
||||
self
|
||||
.events::<InInstructions, _>(block, |event| {
|
||||
matches!(event, InInstructionsEvent::Batch { .. })
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn execute_batch(&self, batch: SignedBatch) -> Result<Encoded, SeraiError> {
|
||||
self.unsigned(&tx::dynamic(
|
||||
PALLET,
|
||||
"execute_batch",
|
||||
scale_composite(in_instructions::Call::<Runtime>::execute_batch { batch }),
|
||||
))
|
||||
}
|
||||
}
|
||||
244
substrate/client/src/serai/mod.rs
Normal file
244
substrate/client/src/serai/mod.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use scale::{Encode, Decode};
|
||||
mod scale_value;
|
||||
pub(crate) use scale_value::{scale_value, scale_composite};
|
||||
use subxt::ext::scale_value::Value;
|
||||
|
||||
use sp_core::{Pair as PairTrait, sr25519::Pair};
|
||||
|
||||
pub use subxt;
|
||||
use subxt::{
|
||||
error::Error as SubxtError,
|
||||
utils::Encoded,
|
||||
config::{
|
||||
Header as HeaderTrait,
|
||||
substrate::{BlakeTwo256, SubstrateHeader},
|
||||
extrinsic_params::{BaseExtrinsicParams, BaseExtrinsicParamsBuilder},
|
||||
},
|
||||
tx::{Signer, DynamicTxPayload, TxClient},
|
||||
rpc::types::ChainBlock,
|
||||
Config as SubxtConfig, OnlineClient,
|
||||
};
|
||||
|
||||
pub use serai_runtime::primitives;
|
||||
use primitives::{Signature, SeraiAddress};
|
||||
|
||||
use serai_runtime::{
|
||||
system::Config, support::traits::PalletInfo as PalletInfoTrait, PalletInfo, Runtime,
|
||||
};
|
||||
|
||||
pub mod tokens;
|
||||
pub mod in_instructions;
|
||||
pub mod validator_sets;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Encode, Decode)]
|
||||
pub struct Tip {
|
||||
#[codec(compact)]
|
||||
pub tip: u64,
|
||||
}
|
||||
|
||||
pub type Header = SubstrateHeader<<Runtime as Config>::BlockNumber, BlakeTwo256>;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub struct SeraiConfig;
|
||||
impl SubxtConfig for SeraiConfig {
|
||||
type Hash = <Runtime as Config>::Hash;
|
||||
type Hasher = BlakeTwo256;
|
||||
|
||||
type Index = <Runtime as Config>::Index;
|
||||
type AccountId = <Runtime as Config>::AccountId;
|
||||
// TODO: Bech32m
|
||||
type Address = SeraiAddress;
|
||||
|
||||
type Header = Header;
|
||||
type Signature = Signature;
|
||||
|
||||
type ExtrinsicParams = BaseExtrinsicParams<SeraiConfig, Tip>;
|
||||
}
|
||||
|
||||
pub type Block = ChainBlock<SeraiConfig>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SeraiError {
|
||||
#[error("failed to communicate with serai: {0}")]
|
||||
RpcError(SubxtError),
|
||||
#[error("serai-client library was intended for a different runtime version")]
|
||||
InvalidRuntime,
|
||||
#[error("node is faulty")]
|
||||
InvalidNode,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Serai(OnlineClient<SeraiConfig>);
|
||||
|
||||
impl Serai {
|
||||
pub async fn new(url: &str) -> Result<Self, SeraiError> {
|
||||
Ok(Serai(OnlineClient::<SeraiConfig>::from_url(url).await.map_err(SeraiError::RpcError)?))
|
||||
}
|
||||
|
||||
async fn storage<R: Decode>(
|
||||
&self,
|
||||
pallet: &'static str,
|
||||
name: &'static str,
|
||||
keys: Option<Vec<Value>>,
|
||||
block: [u8; 32],
|
||||
) -> Result<Option<R>, SeraiError> {
|
||||
let storage = self.0.storage();
|
||||
let address = subxt::dynamic::storage(pallet, name, keys.unwrap_or(vec![]));
|
||||
debug_assert!(storage.validate(&address).is_ok(), "invalid storage address");
|
||||
|
||||
storage
|
||||
.at(Some(block.into()))
|
||||
.await
|
||||
.map_err(SeraiError::RpcError)?
|
||||
.fetch(&address)
|
||||
.await
|
||||
.map_err(SeraiError::RpcError)?
|
||||
.map(|res| R::decode(&mut res.encoded()).map_err(|_| SeraiError::InvalidRuntime))
|
||||
.transpose()
|
||||
}
|
||||
|
||||
async fn events<P: 'static, E: Decode>(
|
||||
&self,
|
||||
block: [u8; 32],
|
||||
filter: impl Fn(&E) -> bool,
|
||||
) -> Result<Vec<E>, SeraiError> {
|
||||
let mut res = vec![];
|
||||
for event in self.0.events().at(Some(block.into())).await.map_err(SeraiError::RpcError)?.iter()
|
||||
{
|
||||
let event = event.map_err(|_| SeraiError::InvalidRuntime)?;
|
||||
if PalletInfo::index::<P>().unwrap() == usize::from(event.pallet_index()) {
|
||||
let mut with_variant: &[u8] =
|
||||
&[[event.variant_index()].as_ref(), event.field_bytes()].concat();
|
||||
let event = E::decode(&mut with_variant).map_err(|_| SeraiError::InvalidRuntime)?;
|
||||
if filter(&event) {
|
||||
res.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn get_latest_block_hash(&self) -> Result<[u8; 32], SeraiError> {
|
||||
Ok(self.0.rpc().finalized_head().await.map_err(SeraiError::RpcError)?.into())
|
||||
}
|
||||
|
||||
// There is no provided method for this
|
||||
// TODO: Add one to Serai
|
||||
pub async fn is_finalized(&self, header: &Header) -> Result<Option<bool>, SeraiError> {
|
||||
// Get the latest finalized block
|
||||
let finalized = self.get_latest_block_hash().await?.into();
|
||||
// If the latest finalized block is this block, return true
|
||||
if finalized == header.hash() {
|
||||
return Ok(Some(true));
|
||||
}
|
||||
|
||||
let Some(finalized) =
|
||||
self.0.rpc().header(Some(finalized)).await.map_err(SeraiError::RpcError)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// If the finalized block has a lower number, this block can't be finalized
|
||||
if finalized.number() < header.number() {
|
||||
return Ok(Some(false));
|
||||
}
|
||||
|
||||
// This block, if finalized, comes before the finalized block
|
||||
// If we request the hash of this block's number, Substrate will return the hash on the main
|
||||
// chain
|
||||
// If that hash is this hash, this block is finalized
|
||||
let Some(hash) =
|
||||
self
|
||||
.0
|
||||
.rpc()
|
||||
.block_hash(Some(header.number().into()))
|
||||
.await
|
||||
.map_err(SeraiError::RpcError)? else {
|
||||
// This is an error since there is a block at this index
|
||||
Err(SeraiError::InvalidNode)?
|
||||
};
|
||||
|
||||
Ok(Some(header.hash() == hash))
|
||||
}
|
||||
|
||||
pub async fn get_block(&self, hash: [u8; 32]) -> Result<Option<Block>, SeraiError> {
|
||||
let Some(res) =
|
||||
self.0.rpc().block(Some(hash.into())).await.map_err(SeraiError::RpcError)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Only return finalized blocks
|
||||
if self.is_finalized(&res.block.header).await? != Some(true) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(res.block))
|
||||
}
|
||||
|
||||
// Ideally, this would be get_block_hash, not get_block_by_number
|
||||
// Unfortunately, in order to only operate over only finalized data, we have to check the
|
||||
// returned hash is for a finalized block. We can only do that by calling the extensive
|
||||
// is_finalized method, which at least requires the header
|
||||
// In practice, the block is likely more useful than the header
|
||||
pub async fn get_block_by_number(&self, number: u64) -> Result<Option<Block>, SeraiError> {
|
||||
let Some(hash) =
|
||||
self.0.rpc().block_hash(Some(number.into())).await.map_err(SeraiError::RpcError)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
self.get_block(hash.into()).await
|
||||
}
|
||||
|
||||
pub async fn get_nonce(&self, address: &SeraiAddress) -> Result<u32, SeraiError> {
|
||||
self
|
||||
.0
|
||||
.rpc()
|
||||
.system_account_next_index(&sp_core::sr25519::Public(address.0).to_string())
|
||||
.await
|
||||
.map_err(SeraiError::RpcError)
|
||||
}
|
||||
|
||||
pub fn unsigned(&self, payload: &DynamicTxPayload<'static>) -> Result<Encoded, SeraiError> {
|
||||
TxClient::new(self.0.offline())
|
||||
.create_unsigned(payload)
|
||||
.map(|tx| Encoded(tx.into_encoded()))
|
||||
.map_err(|_| SeraiError::InvalidRuntime)
|
||||
}
|
||||
|
||||
pub fn sign<S: Send + Sync + Signer<SeraiConfig>>(
|
||||
&self,
|
||||
signer: &S,
|
||||
payload: &DynamicTxPayload<'static>,
|
||||
nonce: u32,
|
||||
params: BaseExtrinsicParamsBuilder<SeraiConfig, Tip>,
|
||||
) -> Result<Encoded, SeraiError> {
|
||||
TxClient::new(self.0.offline())
|
||||
.create_signed_with_nonce(payload, signer, nonce, params)
|
||||
.map(|tx| Encoded(tx.into_encoded()))
|
||||
.map_err(|_| SeraiError::InvalidRuntime)
|
||||
}
|
||||
|
||||
pub async fn publish(&self, tx: &Encoded) -> Result<[u8; 32], SeraiError> {
|
||||
self.0.rpc().submit_extrinsic(tx).await.map(Into::into).map_err(SeraiError::RpcError)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PairSigner(Pair, <SeraiConfig as SubxtConfig>::AccountId);
|
||||
impl PairSigner {
|
||||
pub fn new(pair: Pair) -> Self {
|
||||
let id = pair.public();
|
||||
PairSigner(pair, id)
|
||||
}
|
||||
}
|
||||
impl Signer<SeraiConfig> for PairSigner {
|
||||
fn account_id(&self) -> &<SeraiConfig as SubxtConfig>::AccountId {
|
||||
&self.1
|
||||
}
|
||||
fn address(&self) -> <SeraiConfig as SubxtConfig>::Address {
|
||||
self.1.into()
|
||||
}
|
||||
fn sign(&self, payload: &[u8]) -> <SeraiConfig as SubxtConfig>::Signature {
|
||||
self.0.sign(payload)
|
||||
}
|
||||
}
|
||||
18
substrate/client/src/serai/scale_value.rs
Normal file
18
substrate/client/src/serai/scale_value.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use ::scale::Encode;
|
||||
use scale_info::{MetaType, TypeInfo, Registry, PortableRegistry};
|
||||
use subxt::ext::scale_value::{Composite, ValueDef, Value, scale};
|
||||
|
||||
pub(crate) fn scale_value<V: 'static + Encode + TypeInfo>(value: V) -> Value {
|
||||
let mut registry = Registry::new();
|
||||
let id = registry.register_type(&MetaType::new::<V>()).id;
|
||||
let registry = PortableRegistry::from(registry);
|
||||
scale::decode_as_type(&mut value.encode().as_ref(), id, ®istry).unwrap().remove_context()
|
||||
}
|
||||
|
||||
pub(crate) fn scale_composite<V: 'static + Encode + TypeInfo>(value: V) -> Composite<()> {
|
||||
match scale_value(value).value {
|
||||
ValueDef::Composite(composite) => composite,
|
||||
ValueDef::Variant(variant) => variant.values,
|
||||
_ => panic!("not composite"),
|
||||
}
|
||||
}
|
||||
68
substrate/client/src/serai/tokens.rs
Normal file
68
substrate/client/src/serai/tokens.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use serai_runtime::{
|
||||
primitives::{SeraiAddress, SubstrateAmount, Amount, Coin, Balance},
|
||||
assets::{AssetDetails, AssetAccount},
|
||||
tokens, Tokens, Runtime,
|
||||
};
|
||||
pub use tokens::primitives;
|
||||
use primitives::OutInstruction;
|
||||
|
||||
use subxt::tx::{self, DynamicTxPayload};
|
||||
|
||||
use crate::{Serai, SeraiError, scale_value, scale_composite};
|
||||
|
||||
const PALLET: &str = "Tokens";
|
||||
|
||||
pub type TokensEvent = tokens::Event<Runtime>;
|
||||
|
||||
impl Serai {
|
||||
pub async fn get_mint_events(&self, block: [u8; 32]) -> Result<Vec<TokensEvent>, SeraiError> {
|
||||
self.events::<Tokens, _>(block, |event| matches!(event, TokensEvent::Mint { .. })).await
|
||||
}
|
||||
|
||||
pub async fn get_token_supply(&self, block: [u8; 32], coin: Coin) -> Result<Amount, SeraiError> {
|
||||
Ok(Amount(
|
||||
self
|
||||
.storage::<AssetDetails<SubstrateAmount, SeraiAddress, SubstrateAmount>>(
|
||||
"Assets",
|
||||
"Asset",
|
||||
Some(vec![scale_value(coin)]),
|
||||
block,
|
||||
)
|
||||
.await?
|
||||
.map(|token| token.supply)
|
||||
.unwrap_or(0),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_token_balance(
|
||||
&self,
|
||||
block: [u8; 32],
|
||||
coin: Coin,
|
||||
address: SeraiAddress,
|
||||
) -> Result<Amount, SeraiError> {
|
||||
Ok(Amount(
|
||||
self
|
||||
.storage::<AssetAccount<SubstrateAmount, SubstrateAmount, ()>>(
|
||||
"Assets",
|
||||
"Account",
|
||||
Some(vec![scale_value(coin), scale_value(address)]),
|
||||
block,
|
||||
)
|
||||
.await?
|
||||
.map(|account| account.balance)
|
||||
.unwrap_or(0),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn burn(balance: Balance, instruction: OutInstruction) -> DynamicTxPayload<'static> {
|
||||
tx::dynamic(
|
||||
PALLET,
|
||||
"burn",
|
||||
scale_composite(tokens::Call::<Runtime>::burn { balance, instruction }),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_burn_events(&self, block: [u8; 32]) -> Result<Vec<TokensEvent>, SeraiError> {
|
||||
self.events::<Tokens, _>(block, |event| matches!(event, TokensEvent::Burn { .. })).await
|
||||
}
|
||||
}
|
||||
59
substrate/client/src/serai/validator_sets.rs
Normal file
59
substrate/client/src/serai/validator_sets.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use serai_runtime::{validator_sets, ValidatorSets, Runtime};
|
||||
pub use validator_sets::primitives;
|
||||
use primitives::{ValidatorSet, ValidatorSetData, KeyPair};
|
||||
|
||||
use subxt::tx::{self, DynamicTxPayload};
|
||||
|
||||
use crate::{primitives::NetworkId, Serai, SeraiError, scale_value, scale_composite};
|
||||
|
||||
const PALLET: &str = "ValidatorSets";
|
||||
|
||||
pub type ValidatorSetsEvent = validator_sets::Event<Runtime>;
|
||||
|
||||
impl Serai {
|
||||
pub async fn get_vote_events(
|
||||
&self,
|
||||
block: [u8; 32],
|
||||
) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
|
||||
self
|
||||
.events::<ValidatorSets, _>(block, |event| matches!(event, ValidatorSetsEvent::Vote { .. }))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_key_gen_events(
|
||||
&self,
|
||||
block: [u8; 32],
|
||||
) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
|
||||
self
|
||||
.events::<ValidatorSets, _>(block, |event| matches!(event, ValidatorSetsEvent::KeyGen { .. }))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_validator_set(
|
||||
&self,
|
||||
set: ValidatorSet,
|
||||
) -> Result<Option<ValidatorSetData>, SeraiError> {
|
||||
self
|
||||
.storage(
|
||||
PALLET,
|
||||
"ValidatorSets",
|
||||
Some(vec![scale_value(set)]),
|
||||
self.get_latest_block_hash().await?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_keys(&self, set: ValidatorSet) -> Result<Option<KeyPair>, SeraiError> {
|
||||
self
|
||||
.storage(PALLET, "Keys", Some(vec![scale_value(set)]), self.get_latest_block_hash().await?)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn vote(network: NetworkId, key_pair: KeyPair) -> DynamicTxPayload<'static> {
|
||||
tx::dynamic(
|
||||
PALLET,
|
||||
"vote",
|
||||
scale_composite(validator_sets::Call::<Runtime>::vote { network, key_pair }),
|
||||
)
|
||||
}
|
||||
}
|
||||
1
substrate/client/src/tests/coins/bitcoin.rs
Normal file
1
substrate/client/src/tests/coins/bitcoin.rs
Normal file
@@ -0,0 +1 @@
|
||||
// TODO: Test the address back and forth
|
||||
5
substrate/client/src/tests/coins/mod.rs
Normal file
5
substrate/client/src/tests/coins/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "bitcoin")]
|
||||
mod bitcoin;
|
||||
|
||||
#[cfg(feature = "monero")]
|
||||
mod monero;
|
||||
1
substrate/client/src/tests/coins/monero.rs
Normal file
1
substrate/client/src/tests/coins/monero.rs
Normal file
@@ -0,0 +1 @@
|
||||
// TODO: Test the address back and forth
|
||||
2
substrate/client/src/tests/mod.rs
Normal file
2
substrate/client/src/tests/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[cfg(feature = "coins")]
|
||||
mod coins;
|
||||
54
substrate/client/tests/batch.rs
Normal file
54
substrate/client/tests/batch.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use serai_client::{
|
||||
primitives::{BITCOIN_NET_ID, BITCOIN, BlockHash, SeraiAddress, Amount, Balance},
|
||||
in_instructions::{
|
||||
primitives::{InInstruction, InInstructionWithBalance, Batch},
|
||||
InInstructionsEvent,
|
||||
},
|
||||
tokens::TokensEvent,
|
||||
Serai,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::{serai, in_instructions::provide_batch};
|
||||
|
||||
serai_test!(
|
||||
async fn publish_batch() {
|
||||
let network = BITCOIN_NET_ID;
|
||||
let id = 0;
|
||||
|
||||
let mut block_hash = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut block_hash.0);
|
||||
|
||||
let mut address = SeraiAddress::new([0; 32]);
|
||||
OsRng.fill_bytes(&mut address.0);
|
||||
|
||||
let coin = BITCOIN;
|
||||
let amount = Amount(OsRng.next_u64().saturating_add(1));
|
||||
let balance = Balance { coin, amount };
|
||||
|
||||
let batch = Batch {
|
||||
network,
|
||||
id,
|
||||
block: block_hash,
|
||||
instructions: vec![InInstructionWithBalance {
|
||||
instruction: InInstruction::Transfer(address),
|
||||
balance,
|
||||
}],
|
||||
};
|
||||
|
||||
let block = provide_batch(batch).await;
|
||||
|
||||
let serai = serai().await;
|
||||
let batches = serai.get_batch_events(block).await.unwrap();
|
||||
assert_eq!(batches, vec![InInstructionsEvent::Batch { network, id, block: block_hash }]);
|
||||
|
||||
assert_eq!(
|
||||
serai.get_mint_events(block).await.unwrap(),
|
||||
vec![TokensEvent::Mint { address, balance }],
|
||||
);
|
||||
assert_eq!(serai.get_token_supply(block, coin).await.unwrap(), amount);
|
||||
assert_eq!(serai.get_token_balance(block, coin, address).await.unwrap(), amount);
|
||||
}
|
||||
);
|
||||
88
substrate/client/tests/burn.rs
Normal file
88
substrate/client/tests/burn.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use sp_core::Pair;
|
||||
|
||||
use serai_client::{
|
||||
subxt::config::extrinsic_params::BaseExtrinsicParamsBuilder,
|
||||
primitives::{
|
||||
BITCOIN_NET_ID, BITCOIN, BlockHash, SeraiAddress, Amount, Balance, Data, ExternalAddress,
|
||||
insecure_pair_from_name,
|
||||
},
|
||||
in_instructions::{
|
||||
InInstructionsEvent,
|
||||
primitives::{InInstruction, InInstructionWithBalance, Batch},
|
||||
},
|
||||
tokens::{primitives::OutInstruction, TokensEvent},
|
||||
PairSigner, Serai,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::{serai, tx::publish_tx, in_instructions::provide_batch};
|
||||
|
||||
serai_test!(
|
||||
async fn burn() {
|
||||
let network = BITCOIN_NET_ID;
|
||||
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 = BITCOIN;
|
||||
let amount = Amount(OsRng.next_u64().saturating_add(1));
|
||||
let balance = Balance { coin, amount };
|
||||
|
||||
let batch = Batch {
|
||||
network,
|
||||
id,
|
||||
block: block_hash,
|
||||
instructions: vec![InInstructionWithBalance {
|
||||
instruction: InInstruction::Transfer(address),
|
||||
balance,
|
||||
}],
|
||||
};
|
||||
|
||||
let block = provide_batch(batch).await;
|
||||
|
||||
let serai = serai().await;
|
||||
let batches = serai.get_batch_events(block).await.unwrap();
|
||||
assert_eq!(batches, vec![InInstructionsEvent::Batch { network, id, block: block_hash }]);
|
||||
|
||||
assert_eq!(
|
||||
serai.get_mint_events(block).await.unwrap(),
|
||||
vec![TokensEvent::Mint { address, balance }]
|
||||
);
|
||||
assert_eq!(serai.get_token_supply(block, coin).await.unwrap(), amount);
|
||||
assert_eq!(serai.get_token_balance(block, coin, 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();
|
||||
|
||||
let mut rand_bytes = vec![0; 32];
|
||||
OsRng.fill_bytes(&mut rand_bytes);
|
||||
let data = Data::new(rand_bytes).unwrap();
|
||||
|
||||
let out = OutInstruction { address: external_address, data: Some(data) };
|
||||
let block = publish_tx(
|
||||
&serai
|
||||
.sign(
|
||||
&PairSigner::new(pair),
|
||||
&Serai::burn(balance, out.clone()),
|
||||
0,
|
||||
BaseExtrinsicParamsBuilder::new(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let events = serai.get_burn_events(block).await.unwrap();
|
||||
assert_eq!(events, vec![TokensEvent::Burn { address, balance, instruction: out }]);
|
||||
assert_eq!(serai.get_token_supply(block, coin).await.unwrap(), Amount(0));
|
||||
assert_eq!(serai.get_token_balance(block, coin, address).await.unwrap(), Amount(0));
|
||||
}
|
||||
);
|
||||
49
substrate/client/tests/common/in_instructions.rs
Normal file
49
substrate/client/tests/common/in_instructions.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use scale::Encode;
|
||||
|
||||
use sp_core::Pair;
|
||||
|
||||
use serai_client::{
|
||||
primitives::insecure_pair_from_name,
|
||||
validator_sets::primitives::{Session, ValidatorSet},
|
||||
in_instructions::{
|
||||
primitives::{Batch, SignedBatch},
|
||||
InInstructionsEvent,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::common::{serai, tx::publish_tx, validator_sets::vote_in_keys};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn provide_batch(batch: Batch) -> [u8; 32] {
|
||||
let serai = serai().await;
|
||||
|
||||
// TODO: Get the latest session
|
||||
let set = ValidatorSet { session: Session(0), network: batch.network };
|
||||
let pair = insecure_pair_from_name(&format!("ValidatorSet {:?}", set));
|
||||
let keys = if let Some(keys) = serai.get_keys(set).await.unwrap() {
|
||||
keys
|
||||
} else {
|
||||
let keys = (pair.public(), vec![].try_into().unwrap());
|
||||
vote_in_keys(set, keys.clone()).await;
|
||||
keys
|
||||
};
|
||||
assert_eq!(keys.0, pair.public());
|
||||
|
||||
let block = publish_tx(
|
||||
&serai
|
||||
.execute_batch(SignedBatch { batch: batch.clone(), signature: pair.sign(&batch.encode()) })
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let batches = serai.get_batch_events(block).await.unwrap();
|
||||
// TODO: impl From<Batch> for BatchEvent?
|
||||
assert_eq!(
|
||||
batches,
|
||||
vec![InInstructionsEvent::Batch { network: batch.network, id: batch.id, block: batch.block }],
|
||||
);
|
||||
|
||||
// TODO: Check the tokens events
|
||||
|
||||
block
|
||||
}
|
||||
82
substrate/client/tests/common/mod.rs
Normal file
82
substrate/client/tests/common/mod.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use serai_client::Serai;
|
||||
|
||||
pub mod tx;
|
||||
pub mod validator_sets;
|
||||
pub mod in_instructions;
|
||||
|
||||
pub const URL: &str = "ws://127.0.0.1:9944";
|
||||
|
||||
pub async fn serai() -> Serai {
|
||||
Serai::new(URL).await.unwrap()
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref SEQUENTIAL: Mutex<()> = Mutex::new(());
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! serai_test {
|
||||
($(async fn $name: ident() $body: block)*) => {
|
||||
$(
|
||||
#[tokio::test]
|
||||
async fn $name() {
|
||||
use std::process::Command;
|
||||
|
||||
let guard = common::SEQUENTIAL.lock().await;
|
||||
|
||||
let is_running = || {
|
||||
!Command::new("pidof").arg("serai-node").output().unwrap().stdout.is_empty()
|
||||
};
|
||||
|
||||
// Spawn a fresh Serai node
|
||||
let mut command = {
|
||||
use core::time::Duration;
|
||||
use std::path::Path;
|
||||
|
||||
// Make sure a node isn't already running
|
||||
assert!(!is_running());
|
||||
|
||||
let node = {
|
||||
let this_crate = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let top_level = this_crate.join("../../../");
|
||||
top_level.join("target/debug/serai-node")
|
||||
};
|
||||
|
||||
let command = Command::new(node).arg("--dev").spawn().unwrap();
|
||||
while Serai::new(common::URL).await.is_err() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
let serai = serai().await;
|
||||
while serai.get_latest_block_hash().await.is_err() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
// TODO: https://github.com/serai-dex/serai/247
|
||||
if std::env::var("GITHUB_CI") == Ok("true".to_string()) {
|
||||
tokio::time::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
|
||||
// Sanity check the pidof command is well-formed
|
||||
assert!(is_running());
|
||||
|
||||
command
|
||||
};
|
||||
|
||||
let local = tokio::task::LocalSet::new();
|
||||
local.run_until(async move {
|
||||
if let Err(err) = tokio::task::spawn_local(async move { $body }).await {
|
||||
drop(guard);
|
||||
let _ = command.kill();
|
||||
Err(err).unwrap()
|
||||
} else {
|
||||
command.kill().unwrap();
|
||||
}
|
||||
assert!(!is_running());
|
||||
}).await;
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
51
substrate/client/tests/common/tx.rs
Normal file
51
substrate/client/tests/common/tx.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use core::time::Duration;
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
use serai_client::subxt::{config::Header, utils::Encoded};
|
||||
|
||||
use crate::common::serai;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn publish_tx(tx: &Encoded) -> [u8; 32] {
|
||||
let serai = serai().await;
|
||||
|
||||
let mut latest = serai
|
||||
.get_block(serai.get_latest_block_hash().await.unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.header
|
||||
.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.get_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 extrinsic in block.extrinsics {
|
||||
if extrinsic.0 == tx.0[2 ..] {
|
||||
return block.header.hash().into();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
47
substrate/client/tests/common/validator_sets.rs
Normal file
47
substrate/client/tests/common/validator_sets.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use sp_core::Pair;
|
||||
|
||||
use serai_client::{
|
||||
subxt::config::extrinsic_params::BaseExtrinsicParamsBuilder,
|
||||
primitives::{SeraiAddress, insecure_pair_from_name},
|
||||
validator_sets::{
|
||||
primitives::{ValidatorSet, KeyPair},
|
||||
ValidatorSetsEvent,
|
||||
},
|
||||
PairSigner, Serai,
|
||||
};
|
||||
|
||||
use crate::common::{serai, tx::publish_tx};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn vote_in_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8; 32] {
|
||||
let pair = insecure_pair_from_name("Alice");
|
||||
let public = pair.public();
|
||||
|
||||
let serai = serai().await;
|
||||
|
||||
// Vote in a key pair
|
||||
let address = SeraiAddress::from(pair.public());
|
||||
let block = publish_tx(
|
||||
&serai
|
||||
.sign(
|
||||
&PairSigner::new(pair),
|
||||
&Serai::vote(set.network, key_pair.clone()),
|
||||
serai.get_nonce(&address).await.unwrap(),
|
||||
BaseExtrinsicParamsBuilder::new(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
serai.get_vote_events(block).await.unwrap(),
|
||||
vec![ValidatorSetsEvent::Vote { voter: public, set, key_pair: key_pair.clone(), votes: 1 }]
|
||||
);
|
||||
assert_eq!(
|
||||
serai.get_key_gen_events(block).await.unwrap(),
|
||||
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
|
||||
);
|
||||
assert_eq!(serai.get_keys(set).await.unwrap(), Some(key_pair));
|
||||
|
||||
block
|
||||
}
|
||||
54
substrate/client/tests/validator_sets.rs
Normal file
54
substrate/client/tests/validator_sets.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use sp_core::{sr25519::Public, Pair};
|
||||
|
||||
use serai_client::{
|
||||
primitives::{BITCOIN_NET_ID, BITCOIN_NET, insecure_pair_from_name},
|
||||
validator_sets::{
|
||||
primitives::{Session, ValidatorSet},
|
||||
ValidatorSetsEvent,
|
||||
},
|
||||
Serai,
|
||||
};
|
||||
|
||||
mod common;
|
||||
use common::{serai, validator_sets::vote_in_keys};
|
||||
|
||||
serai_test!(
|
||||
async fn vote_keys() {
|
||||
let network = BITCOIN_NET_ID;
|
||||
let set = ValidatorSet { session: Session(0), network };
|
||||
|
||||
let public = insecure_pair_from_name("Alice").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 mut ristretto_key = [0; 32];
|
||||
OsRng.fill_bytes(&mut ristretto_key);
|
||||
let mut external_key = vec![0; 33];
|
||||
OsRng.fill_bytes(&mut external_key);
|
||||
let key_pair = (Public(ristretto_key), external_key.try_into().unwrap());
|
||||
|
||||
let serai = serai().await;
|
||||
|
||||
// Make sure the genesis is as expected
|
||||
let set_data = serai.get_validator_set(set).await.unwrap().unwrap();
|
||||
assert_eq!(set_data.network, *BITCOIN_NET);
|
||||
let participants_ref: &[_] = set_data.participants.as_ref();
|
||||
assert_eq!(participants_ref, [(public, set_data.bond)].as_ref());
|
||||
|
||||
let block = vote_in_keys(set, key_pair.clone()).await;
|
||||
|
||||
// While the vote_in_keys function should handle this, it's beneficial to independently test it
|
||||
assert_eq!(
|
||||
serai.get_vote_events(block).await.unwrap(),
|
||||
vec![ValidatorSetsEvent::Vote { voter: public, set, key_pair: key_pair.clone(), votes: 1 }]
|
||||
);
|
||||
assert_eq!(
|
||||
serai.get_key_gen_events(block).await.unwrap(),
|
||||
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
|
||||
);
|
||||
assert_eq!(serai.get_keys(set).await.unwrap(), Some(key_pair));
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user