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:
Luke Parker
2023-01-28 01:47:13 -05:00
committed by GitHub
parent f12cc2cca6
commit 2ace339975
39 changed files with 1213 additions and 594 deletions

View File

@@ -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"

View File

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

View File

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

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

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

View File

@@ -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)*) => {

View File

@@ -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);
}
);

View File

@@ -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"]

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

View File

@@ -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,
}

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

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

View File

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