mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-09 04:39:24 +00:00
Tokens pallet (#243)
* Use Monero-compatible additional TX keys This still sends a fingerprinting flare up if you send to a subaddress which needs to be fixed. Despite that, Monero no should no longer fail to scan TXs from monero-serai regarding additional keys. Previously it failed becuase we supplied one key as THE key, and n-1 as additional. Monero expects n for additional. This does correctly select when to use THE key versus when to use the additional key when sending. That removes the ability for recipients to fingerprint monero-serai by receiving to a standard address yet needing to use an additional key. * Add tokens_primitives Moves OutInstruction from in-instructions. Turns Destination into OutInstruction. * Correct in-instructions DispatchClass * Add initial tokens pallet * Don't allow pallet addresses to equal identity * Add support for InInstruction::transfer Requires a cargo update due to modifications made to serai-dex/substrate. Successfully mints a token to a SeraiAddress. * Bind InInstructions to an amount * Add a call filter to the runtime Prevents worrying about calls to the assets pallet/generally tightens things up. * Restore Destination It was meged into OutInstruction, yet it didn't make sense for OutInstruction to contain a SeraiAddress. Also deletes the excessively dated Scenarios doc. * Split PublicKey/SeraiAddress Lets us define a custom Display/ToString for SeraiAddress. Also resolves an oddity where PublicKey would be encoded as String, not [u8; 32]. * Test burning tokens/retrieving OutInstructions Modularizes processor_coinUpdates into a shared testing utility. * Misc lint * Don't use PolkadotExtrinsicParams
This commit is contained in:
@@ -15,19 +15,22 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
[dependencies]
|
||||
thiserror = "1"
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
scale = { package = "parity-scale-codec", version = "3" }
|
||||
scale-info = "2"
|
||||
scale-value = "0.6"
|
||||
subxt = "0.25"
|
||||
|
||||
sp-core = { git = "https://github.com/serai-dex/substrate", version = "7" }
|
||||
|
||||
serai-primitives = { path = "../primitives", version = "0.1" }
|
||||
in-instructions-primitives = { path = "../../in-instructions/primitives", version = "0.1" }
|
||||
serai-runtime = { path = "../../runtime", version = "0.1" }
|
||||
|
||||
subxt = "0.25"
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1"
|
||||
|
||||
rand_core = "0.6"
|
||||
|
||||
tokio = "1"
|
||||
|
||||
jsonrpsee-server = "0.16"
|
||||
|
||||
0
substrate/serai/client/metadata.json
Normal file
0
substrate/serai/client/metadata.json
Normal file
@@ -1,15 +1,9 @@
|
||||
use scale::Decode;
|
||||
|
||||
use serai_runtime::{
|
||||
support::traits::PalletInfo as PalletInfoTrait, PalletInfo, in_instructions, InInstructions,
|
||||
Runtime,
|
||||
};
|
||||
|
||||
pub use in_instructions_primitives as primitives;
|
||||
use serai_runtime::{in_instructions, InInstructions, Runtime};
|
||||
pub use in_instructions::primitives;
|
||||
|
||||
use crate::{
|
||||
primitives::{Coin, BlockNumber},
|
||||
Serai, SeraiError,
|
||||
Serai, SeraiError, scale_value,
|
||||
};
|
||||
|
||||
const PALLET: &str = "InInstructions";
|
||||
@@ -21,22 +15,11 @@ impl Serai {
|
||||
&self,
|
||||
block: [u8; 32],
|
||||
) -> Result<Vec<InInstructionsEvent>, 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::<InInstructions>().unwrap() == usize::from(event.pallet_index()) {
|
||||
let mut with_variant: &[u8] =
|
||||
&[[event.variant_index()].as_ref(), event.field_bytes()].concat();
|
||||
let event =
|
||||
InInstructionsEvent::decode(&mut with_variant).map_err(|_| SeraiError::InvalidRuntime)?;
|
||||
if matches!(event, InInstructionsEvent::Batch { .. }) {
|
||||
res.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
self
|
||||
.events::<InInstructions, _>(block, |event| {
|
||||
matches!(event, InInstructionsEvent::Batch { .. })
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_coin_block_number(
|
||||
@@ -44,6 +27,11 @@ impl Serai {
|
||||
coin: Coin,
|
||||
block: [u8; 32],
|
||||
) -> Result<BlockNumber, SeraiError> {
|
||||
Ok(self.storage(PALLET, "BlockNumbers", Some(coin), block).await?.unwrap_or(BlockNumber(0)))
|
||||
Ok(
|
||||
self
|
||||
.storage(PALLET, "BlockNumbers", Some(vec![scale_value(coin)]), block)
|
||||
.await?
|
||||
.unwrap_or(BlockNumber(0)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use serde::Serialize;
|
||||
use scale::Decode;
|
||||
use scale::{Encode, Decode};
|
||||
mod scale_value;
|
||||
pub(crate) use crate::scale_value::{scale_value, scale_composite};
|
||||
use ::scale_value::Value;
|
||||
|
||||
use subxt::{tx::BaseExtrinsicParams, Config as SubxtConfig, OnlineClient};
|
||||
use subxt::{
|
||||
utils::Encoded,
|
||||
tx::{
|
||||
Signer, DynamicTxPayload, BaseExtrinsicParams, BaseExtrinsicParamsBuilder, TxClient,
|
||||
},
|
||||
Config as SubxtConfig, OnlineClient,
|
||||
};
|
||||
|
||||
pub use serai_primitives as primitives;
|
||||
use primitives::{Signature, SeraiAddress};
|
||||
|
||||
use serai_runtime::{system::Config, Runtime};
|
||||
use serai_runtime::{
|
||||
system::Config, support::traits::PalletInfo as PalletInfoTrait, PalletInfo, Runtime,
|
||||
};
|
||||
|
||||
pub mod tokens;
|
||||
pub mod in_instructions;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Encode, Decode)]
|
||||
pub struct Tip {
|
||||
#[codec(compact)]
|
||||
pub tip: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(crate) struct SeraiConfig;
|
||||
pub struct SeraiConfig;
|
||||
impl SubxtConfig for SeraiConfig {
|
||||
type BlockNumber = <Runtime as Config>::BlockNumber;
|
||||
|
||||
@@ -28,7 +45,7 @@ impl SubxtConfig for SeraiConfig {
|
||||
type Header = <Runtime as Config>::Header;
|
||||
type Signature = Signature;
|
||||
|
||||
type ExtrinsicParams = BaseExtrinsicParams<SeraiConfig, ()>;
|
||||
type ExtrinsicParams = BaseExtrinsicParams<SeraiConfig, Tip>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Error, Debug)]
|
||||
@@ -47,21 +64,16 @@ impl Serai {
|
||||
Ok(Serai(OnlineClient::<SeraiConfig>::from_url(url).await.map_err(|_| SeraiError::RpcError)?))
|
||||
}
|
||||
|
||||
async fn storage<K: Serialize, R: Decode>(
|
||||
async fn storage<R: Decode>(
|
||||
&self,
|
||||
pallet: &'static str,
|
||||
name: &'static str,
|
||||
key: Option<K>,
|
||||
keys: Option<Vec<Value>>,
|
||||
block: [u8; 32],
|
||||
) -> Result<Option<R>, SeraiError> {
|
||||
let mut keys = vec![];
|
||||
if let Some(key) = key {
|
||||
keys.push(scale_value::serde::to_value(key).unwrap());
|
||||
}
|
||||
|
||||
let storage = self.0.storage();
|
||||
let address = subxt::dynamic::storage(pallet, name, keys);
|
||||
debug_assert!(storage.validate(&address).is_ok());
|
||||
let address = subxt::dynamic::storage(pallet, name, keys.unwrap_or(vec![]));
|
||||
debug_assert!(storage.validate(&address).is_ok(), "invalid storage address");
|
||||
|
||||
storage
|
||||
.fetch(&address, Some(block.into()))
|
||||
@@ -71,7 +83,46 @@ impl Serai {
|
||||
.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())
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
18
substrate/serai/client/src/scale_value.rs
Normal file
18
substrate/serai/client/src/scale_value.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use ::scale::Encode;
|
||||
use scale_info::{MetaType, TypeInfo, Registry, PortableRegistry};
|
||||
use scale_value::{Composite, ValueDef, Value, scale};
|
||||
|
||||
pub(crate) fn scale_value<V: Encode + TypeInfo + 'static>(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: Encode + TypeInfo + 'static>(value: V) -> Composite<()> {
|
||||
match scale_value(value).value {
|
||||
ValueDef::Composite(composite) => composite,
|
||||
ValueDef::Variant(variant) => variant.values,
|
||||
_ => panic!("not composite"),
|
||||
}
|
||||
}
|
||||
68
substrate/serai/client/src/tokens.rs
Normal file
68
substrate/serai/client/src/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
|
||||
}
|
||||
}
|
||||
79
substrate/serai/client/tests/burn.rs
Normal file
79
substrate/serai/client/tests/burn.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use core::time::Duration;
|
||||
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use sp_core::Pair;
|
||||
use serai_runtime::in_instructions::{Batch, Update};
|
||||
|
||||
use tokio::time::sleep;
|
||||
|
||||
use subxt::tx::{BaseExtrinsicParamsBuilder, PairSigner};
|
||||
|
||||
use serai_client::{
|
||||
primitives::{
|
||||
BITCOIN, BlockNumber, BlockHash, SeraiAddress, Amount, WithAmount, Balance, Data,
|
||||
ExternalAddress, insecure_pair_from_name,
|
||||
},
|
||||
in_instructions::primitives::InInstruction,
|
||||
tokens::{primitives::OutInstruction, TokensEvent},
|
||||
Serai,
|
||||
};
|
||||
|
||||
mod runner;
|
||||
use runner::{URL, provide_updates};
|
||||
|
||||
serai_test!(
|
||||
async fn burn() {
|
||||
let coin = BITCOIN;
|
||||
let mut id = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut id.0);
|
||||
let block_number = BlockNumber(u32::try_from(OsRng.next_u64() >> 32).unwrap());
|
||||
|
||||
let pair = insecure_pair_from_name("Alice");
|
||||
let public = pair.public();
|
||||
let address = SeraiAddress::from(public);
|
||||
|
||||
let amount = Amount(OsRng.next_u64());
|
||||
let balance = Balance { coin, amount };
|
||||
|
||||
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 batch = Batch {
|
||||
id,
|
||||
instructions: vec![WithAmount { data: InInstruction::Transfer(address), amount }],
|
||||
};
|
||||
let update = Update { block_number, batches: vec![batch] };
|
||||
let block = provide_updates(vec![Some(update)]).await;
|
||||
|
||||
let serai = Serai::new(URL).await.unwrap();
|
||||
assert_eq!(serai.get_token_balance(block, coin, address).await.unwrap(), amount);
|
||||
|
||||
let out = OutInstruction { address: external_address, data: Some(data) };
|
||||
let burn = Serai::burn(balance, out.clone());
|
||||
|
||||
let signer = PairSigner::new(pair);
|
||||
serai
|
||||
.publish(&serai.sign(&signer, &burn, 0, BaseExtrinsicParamsBuilder::new()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let block = serai.get_latest_block_hash().await.unwrap();
|
||||
let events = serai.get_burn_events(block).await.unwrap();
|
||||
if events.is_empty() {
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
continue;
|
||||
}
|
||||
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));
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,6 +1,14 @@
|
||||
use core::time::Duration;
|
||||
use std::sync::Arc;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::{sync::Mutex, time::sleep};
|
||||
|
||||
use serai_runtime::in_instructions::Update;
|
||||
use serai_client::{primitives::Coin, in_instructions::InInstructionsEvent, Serai};
|
||||
|
||||
use jsonrpsee_server::RpcModule;
|
||||
|
||||
pub const URL: &str = "ws://127.0.0.1:9944";
|
||||
|
||||
@@ -8,6 +16,73 @@ lazy_static! {
|
||||
pub static ref SEQUENTIAL: Mutex<()> = Mutex::new(());
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn provide_updates(updates: Vec<Option<Update>>) -> [u8; 32] {
|
||||
let done = Arc::new(Mutex::new(false));
|
||||
let done_clone = done.clone();
|
||||
let updates_clone = updates.clone();
|
||||
|
||||
let mut rpc = RpcModule::new(());
|
||||
rpc
|
||||
.register_async_method("processor_coinUpdates", move |_, _| {
|
||||
let done_clone = done_clone.clone();
|
||||
let updates_clone = updates_clone.clone();
|
||||
async move {
|
||||
// Sleep to prevent a race condition where we submit the inherents for this block and the
|
||||
// next one, then remove them, making them unverifiable, causing the node to panic for
|
||||
// being self-malicious
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
if !*done_clone.lock().await {
|
||||
Ok(updates_clone)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let _handle = jsonrpsee_server::ServerBuilder::default()
|
||||
.build("127.0.0.1:5134")
|
||||
.await
|
||||
.unwrap()
|
||||
.start(rpc)
|
||||
.unwrap();
|
||||
|
||||
let serai = Serai::new(URL).await.unwrap();
|
||||
loop {
|
||||
let latest = serai.get_latest_block_hash().await.unwrap();
|
||||
let mut batches = serai.get_batch_events(latest).await.unwrap();
|
||||
if batches.is_empty() {
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
continue;
|
||||
}
|
||||
*done.lock().await = true;
|
||||
|
||||
for (index, update) in updates.iter().enumerate() {
|
||||
if let Some(update) = update {
|
||||
let coin_by_index = Coin(u32::try_from(index).unwrap() + 1);
|
||||
|
||||
for expected in &update.batches {
|
||||
match batches.swap_remove(0) {
|
||||
InInstructionsEvent::Batch { coin, id } => {
|
||||
assert_eq!(coin, coin_by_index);
|
||||
assert_eq!(expected.id, id);
|
||||
}
|
||||
_ => panic!("get_batches returned non-batch"),
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
serai.get_coin_block_number(coin_by_index, latest).await.unwrap(),
|
||||
update.block_number
|
||||
);
|
||||
}
|
||||
}
|
||||
// This will fail if there were more batch events than expected
|
||||
assert!(batches.is_empty());
|
||||
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! serai_test {
|
||||
($(async fn $name: ident() $body: block)*) => {
|
||||
|
||||
@@ -1,60 +1,45 @@
|
||||
use core::time::Duration;
|
||||
|
||||
use tokio::time::sleep;
|
||||
use rand_core::{RngCore, OsRng};
|
||||
|
||||
use serai_runtime::in_instructions::{Batch, Update};
|
||||
|
||||
use jsonrpsee_server::RpcModule;
|
||||
|
||||
use serai_client::{
|
||||
primitives::{BlockNumber, BlockHash, SeraiAddress, BITCOIN},
|
||||
primitives::{BITCOIN, BlockNumber, BlockHash, SeraiAddress, Amount, WithAmount, Balance},
|
||||
tokens::TokensEvent,
|
||||
in_instructions::{primitives::InInstruction, InInstructionsEvent},
|
||||
Serai,
|
||||
};
|
||||
|
||||
mod runner;
|
||||
use runner::URL;
|
||||
use runner::{URL, provide_updates};
|
||||
|
||||
serai_test!(
|
||||
async fn publish_update() {
|
||||
let mut rpc = RpcModule::new(());
|
||||
rpc
|
||||
.register_async_method("processor_coinUpdates", |_, _| async move {
|
||||
let batch = Batch {
|
||||
id: BlockHash([0xaa; 32]),
|
||||
instructions: vec![InInstruction::Transfer(SeraiAddress::from_raw([0xff; 32]))],
|
||||
};
|
||||
async fn publish_updates() {
|
||||
let coin = BITCOIN;
|
||||
let mut id = BlockHash([0; 32]);
|
||||
OsRng.fill_bytes(&mut id.0);
|
||||
let block_number = BlockNumber(u32::try_from(OsRng.next_u64() >> 32).unwrap());
|
||||
|
||||
Ok(vec![Some(Update { block_number: BlockNumber(123), batches: vec![batch] })])
|
||||
})
|
||||
.unwrap();
|
||||
let mut address = SeraiAddress::new([0; 32]);
|
||||
OsRng.fill_bytes(&mut address.0);
|
||||
let amount = Amount(OsRng.next_u64());
|
||||
|
||||
let _handle = jsonrpsee_server::ServerBuilder::default()
|
||||
.build("127.0.0.1:5134")
|
||||
.await
|
||||
.unwrap()
|
||||
.start(rpc)
|
||||
.unwrap();
|
||||
let batch = Batch {
|
||||
id,
|
||||
instructions: vec![WithAmount { data: InInstruction::Transfer(address), amount }],
|
||||
};
|
||||
let update = Update { block_number, batches: vec![batch] };
|
||||
let block = provide_updates(vec![Some(update)]).await;
|
||||
|
||||
let serai = Serai::new(URL).await.unwrap();
|
||||
loop {
|
||||
let latest = serai.get_latest_block_hash().await.unwrap();
|
||||
let batches = serai.get_batch_events(latest).await.unwrap();
|
||||
if let Some(batch) = batches.get(0) {
|
||||
match batch {
|
||||
InInstructionsEvent::Batch { coin, id } => {
|
||||
assert_eq!(coin, &BITCOIN);
|
||||
assert_eq!(id, &BlockHash([0xaa; 32]));
|
||||
assert_eq!(
|
||||
serai.get_coin_block_number(BITCOIN, latest).await.unwrap(),
|
||||
BlockNumber(123)
|
||||
);
|
||||
return;
|
||||
}
|
||||
_ => panic!("get_batches returned non-batch"),
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
let batches = serai.get_batch_events(block).await.unwrap();
|
||||
assert_eq!(batches, vec![InInstructionsEvent::Batch { coin, id }]);
|
||||
assert_eq!(serai.get_coin_block_number(coin, block).await.unwrap(), block_number);
|
||||
|
||||
assert_eq!(
|
||||
serai.get_mint_events(block).await.unwrap(),
|
||||
vec![TokensEvent::Mint { address, balance: Balance { coin, amount } }]
|
||||
);
|
||||
assert_eq!(serai.get_token_supply(block, coin).await.unwrap(), amount);
|
||||
assert_eq!(serai.get_token_balance(block, coin, address).await.unwrap(), amount);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,7 +18,8 @@ scale-info = { version = "2", default-features = false, features = ["derive"] }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
|
||||
[features]
|
||||
std = ["scale/std", "scale-info/std", "serde", "sp-core/std"]
|
||||
std = ["scale/std", "scale-info/std", "serde", "sp-core/std", "sp-runtime/std"]
|
||||
default = ["std"]
|
||||
|
||||
93
substrate/serai/primitives/src/account.rs
Normal file
93
substrate/serai/primitives/src/account.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use scale::{Encode, Decode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use sp_core::sr25519::{Public, Signature as RistrettoSignature};
|
||||
#[cfg(feature = "std")]
|
||||
use sp_core::{Pair as PairTrait, sr25519::Pair};
|
||||
|
||||
use sp_runtime::traits::{LookupError, Lookup, StaticLookup};
|
||||
|
||||
pub type PublicKey = Public;
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Encode, Decode, MaxEncodedLen, TypeInfo,
|
||||
)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct SeraiAddress(pub [u8; 32]);
|
||||
impl SeraiAddress {
|
||||
pub fn new(key: [u8; 32]) -> SeraiAddress {
|
||||
SeraiAddress(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 32]> for SeraiAddress {
|
||||
fn from(key: [u8; 32]) -> SeraiAddress {
|
||||
SeraiAddress(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PublicKey> for SeraiAddress {
|
||||
fn from(key: PublicKey) -> SeraiAddress {
|
||||
SeraiAddress(key.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SeraiAddress> for PublicKey {
|
||||
fn from(address: SeraiAddress) -> PublicKey {
|
||||
PublicKey::from_raw(address.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::fmt::Display for SeraiAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// TODO: Bech32
|
||||
write!(f, "{:?}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn insecure_pair_from_name(name: &'static str) -> Pair {
|
||||
Pair::from_string(&format!("//{name}"), None).unwrap()
|
||||
}
|
||||
|
||||
pub struct AccountLookup;
|
||||
impl Lookup for AccountLookup {
|
||||
type Source = SeraiAddress;
|
||||
type Target = PublicKey;
|
||||
fn lookup(&self, source: SeraiAddress) -> Result<PublicKey, LookupError> {
|
||||
Ok(PublicKey::from_raw(source.0))
|
||||
}
|
||||
}
|
||||
impl StaticLookup for AccountLookup {
|
||||
type Source = SeraiAddress;
|
||||
type Target = PublicKey;
|
||||
fn lookup(source: SeraiAddress) -> Result<PublicKey, LookupError> {
|
||||
Ok(source.into())
|
||||
}
|
||||
fn unlookup(source: PublicKey) -> SeraiAddress {
|
||||
source.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub type Signature = RistrettoSignature;
|
||||
|
||||
pub const fn pallet_address(pallet: &'static [u8]) -> SeraiAddress {
|
||||
let mut address = [0; 32];
|
||||
let mut set = false;
|
||||
// Implement a while loop since we can't use a for loop
|
||||
let mut i = 0;
|
||||
while i < pallet.len() {
|
||||
address[i] = pallet[i];
|
||||
if address[i] != 0 {
|
||||
set = true;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
// Make sure this address isn't the identity point
|
||||
// Doesn't do address != [0; 32] since that's not const
|
||||
assert!(set, "address is the identity point");
|
||||
SeraiAddress(address)
|
||||
}
|
||||
@@ -1,20 +1,25 @@
|
||||
use core::ops::{Add, Sub, Mul};
|
||||
use core::{
|
||||
ops::{Add, Sub, Mul},
|
||||
fmt::Debug,
|
||||
};
|
||||
|
||||
use scale::{Encode, Decode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
/// The type used for amounts within Substrate.
|
||||
// Distinct from Amount due to Substrate's requirements on this type.
|
||||
// While Amount could have all the necessary traits implemented, not only are they many, it'd make
|
||||
// Amount a large type with a variety of misc functions.
|
||||
// The current type's minimalism sets clear bounds on usage.
|
||||
pub type SubstrateAmount = u64;
|
||||
/// The type used for amounts.
|
||||
#[derive(
|
||||
Clone, Copy, PartialEq, Eq, PartialOrd, Debug, Encode, Decode, MaxEncodedLen, TypeInfo,
|
||||
)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct Amount(pub u64);
|
||||
|
||||
/// One whole coin with eight decimals.
|
||||
#[allow(clippy::inconsistent_digit_grouping)]
|
||||
pub const COIN: Amount = Amount(1_000_000_00);
|
||||
pub struct Amount(pub SubstrateAmount);
|
||||
|
||||
impl Add for Amount {
|
||||
type Output = Amount;
|
||||
@@ -37,3 +42,12 @@ impl Mul for Amount {
|
||||
Amount(self.0.checked_mul(other.0).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct WithAmount<
|
||||
T: Clone + PartialEq + Eq + Debug + Encode + Decode + MaxEncodedLen + TypeInfo,
|
||||
> {
|
||||
pub data: T,
|
||||
pub amount: Amount,
|
||||
}
|
||||
|
||||
37
substrate/serai/primitives/src/balance.rs
Normal file
37
substrate/serai/primitives/src/balance.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use core::ops::{Add, Sub, Mul};
|
||||
|
||||
use scale::{Encode, Decode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::{Coin, Amount};
|
||||
|
||||
/// The type used for balances (a Coin and Balance).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct Balance {
|
||||
pub coin: Coin,
|
||||
pub amount: Amount,
|
||||
}
|
||||
|
||||
impl Add<Amount> for Balance {
|
||||
type Output = Balance;
|
||||
fn add(self, other: Amount) -> Balance {
|
||||
Balance { coin: self.coin, amount: self.amount + other }
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Amount> for Balance {
|
||||
type Output = Balance;
|
||||
fn sub(self, other: Amount) -> Balance {
|
||||
Balance { coin: self.coin, amount: self.amount - other }
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Amount> for Balance {
|
||||
type Output = Balance;
|
||||
fn mul(self, other: Amount) -> Balance {
|
||||
Balance { coin: self.coin, amount: self.amount * other }
|
||||
}
|
||||
}
|
||||
46
substrate/serai/primitives/src/block.rs
Normal file
46
substrate/serai/primitives/src/block.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use scale::{Encode, Decode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use sp_core::H256;
|
||||
|
||||
/// The type used to identify block numbers.
|
||||
// Doesn't re-export tendermint-machine's due to traits.
|
||||
#[derive(
|
||||
Clone, Copy, Default, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo,
|
||||
)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct BlockNumber(pub u32);
|
||||
impl From<u32> for BlockNumber {
|
||||
fn from(number: u32) -> BlockNumber {
|
||||
BlockNumber(number)
|
||||
}
|
||||
}
|
||||
|
||||
/// The type used to identify block hashes.
|
||||
// This may not be universally compatible
|
||||
// If a block exists with a hash which isn't 32-bytes, it can be hashed into a value with 32-bytes
|
||||
// This would require the processor to maintain a mapping of 32-byte IDs to actual hashes, which
|
||||
// would be fine
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct BlockHash(pub [u8; 32]);
|
||||
|
||||
impl AsRef<[u8]> for BlockHash {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 32]> for BlockHash {
|
||||
fn from(hash: [u8; 32]) -> BlockHash {
|
||||
BlockHash(hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<H256> for BlockHash {
|
||||
fn from(hash: H256) -> BlockHash {
|
||||
BlockHash(hash.into())
|
||||
}
|
||||
}
|
||||
@@ -7,57 +7,75 @@ use scale_info::TypeInfo;
|
||||
#[cfg(feature = "std")]
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use sp_core::{
|
||||
H256,
|
||||
sr25519::{Public, Signature as RistrettoSignature},
|
||||
};
|
||||
use sp_core::{ConstU32, bounded::BoundedVec};
|
||||
|
||||
mod amount;
|
||||
pub use amount::*;
|
||||
|
||||
mod block;
|
||||
pub use block::*;
|
||||
|
||||
mod coins;
|
||||
pub use coins::*;
|
||||
|
||||
pub type PublicKey = Public;
|
||||
pub type SeraiAddress = PublicKey;
|
||||
pub type Signature = RistrettoSignature;
|
||||
mod balance;
|
||||
pub use balance::*;
|
||||
|
||||
/// The type used to identify block numbers.
|
||||
// Doesn't re-export tendermint-machine's due to traits.
|
||||
#[derive(
|
||||
Clone, Copy, Default, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo,
|
||||
)]
|
||||
mod account;
|
||||
pub use account::*;
|
||||
|
||||
// Monero, our current longest address candidate, has a longest address of featured with payment ID
|
||||
// 1 (enum) + 1 (flags) + 64 (two keys) + 8 (payment ID) = 74
|
||||
pub const MAX_ADDRESS_LEN: u32 = 74;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct BlockNumber(pub u32);
|
||||
impl From<u32> for BlockNumber {
|
||||
fn from(number: u32) -> BlockNumber {
|
||||
BlockNumber(number)
|
||||
pub struct ExternalAddress(BoundedVec<u8, ConstU32<{ MAX_ADDRESS_LEN }>>);
|
||||
impl ExternalAddress {
|
||||
#[cfg(feature = "std")]
|
||||
pub fn new(address: Vec<u8>) -> Result<ExternalAddress, &'static str> {
|
||||
Ok(ExternalAddress(address.try_into().map_err(|_| "address length exceeds {MAX_ADDRESS_LEN}")?))
|
||||
}
|
||||
|
||||
pub fn address(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn consume(self) -> Vec<u8> {
|
||||
self.0.into_inner()
|
||||
}
|
||||
}
|
||||
|
||||
/// The type used to identify block hashes.
|
||||
// This may not be universally compatible
|
||||
// If a block exists with a hash which isn't 32-bytes, it can be hashed into a value with 32-bytes
|
||||
// This would require the processor to maintain a mapping of 32-byte IDs to actual hashes, which
|
||||
// would be fine
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct BlockHash(pub [u8; 32]);
|
||||
|
||||
impl AsRef<[u8]> for BlockHash {
|
||||
impl AsRef<[u8]> for ExternalAddress {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 32]> for BlockHash {
|
||||
fn from(hash: [u8; 32]) -> BlockHash {
|
||||
BlockHash(hash)
|
||||
// Should be enough for a Uniswap v3 call
|
||||
pub const MAX_DATA_LEN: u32 = 512;
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||
pub struct Data(BoundedVec<u8, ConstU32<{ MAX_DATA_LEN }>>);
|
||||
impl Data {
|
||||
#[cfg(feature = "std")]
|
||||
pub fn new(data: Vec<u8>) -> Result<Data, &'static str> {
|
||||
Ok(Data(data.try_into().map_err(|_| "data length exceeds {MAX_DATA_LEN}")?))
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
pub fn consume(self) -> Vec<u8> {
|
||||
self.0.into_inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<H256> for BlockHash {
|
||||
fn from(hash: H256) -> BlockHash {
|
||||
BlockHash(hash.into())
|
||||
impl AsRef<[u8]> for Data {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user