Move substrate/serai/* to substrate/*

This commit is contained in:
Luke Parker
2023-04-08 03:00:35 -04:00
parent bd06b95c05
commit 7abc8f19cd
41 changed files with 17 additions and 17 deletions

View 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(),
)
}
}

View File

@@ -0,0 +1,5 @@
#[cfg(feature = "bitcoin")]
pub mod bitcoin;
#[cfg(feature = "monero")]
pub mod monero;

View 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()
}
}

View 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;

View 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 }),
))
}
}

View 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)
}
}

View 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, &registry).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"),
}
}

View 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
}
}

View 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 }),
)
}
}

View File

@@ -0,0 +1 @@
// TODO: Test the address back and forth

View File

@@ -0,0 +1,5 @@
#[cfg(feature = "bitcoin")]
mod bitcoin;
#[cfg(feature = "monero")]
mod monero;

View File

@@ -0,0 +1 @@
// TODO: Test the address back and forth

View File

@@ -0,0 +1,2 @@
#[cfg(feature = "coins")]
mod coins;