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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1213 additions and 594 deletions

328
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,9 @@ members = [
"substrate/serai/primitives",
"substrate/serai/client",
"substrate/tokens/primitives",
"substrate/tokens/pallet",
"substrate/in-instructions/primitives",
"substrate/in-instructions/pallet",
"substrate/in-instructions/client",

View file

@ -48,6 +48,8 @@ exceptions = [
{ allow = ["AGPL-3.0"], name = "serai-processor" },
{ allow = ["AGPL-3.0"], name = "tokens-pallet" },
{ allow = ["AGPL-3.0"], name = "in-instructions-pallet" },
{ allow = ["AGPL-3.0"], name = "in-instructions-client" },

View file

@ -40,10 +40,10 @@ Serai token. If an Application Call, the encoded call will be executed.
### Refundable In Instruction
- `origin` (Option\<ExternalAddress>): Address, from the network of origin,
which sent coins in.
- `instruction` (InInstruction): The action to perform with the incoming
coins.
- `origin` (Option\<ExternalAddress>): Address, from the network of
origin, which sent coins in.
- `instruction` (InInstruction): The action to perform with the
incoming coins.
Networks may automatically provide `origin`. If they do, the instruction may
still provide `origin`, overriding the automatically provided value.
@ -51,19 +51,18 @@ still provide `origin`, overriding the automatically provided value.
If the instruction fails, coins are scheduled to be returned to `origin`,
if provided.
### Destination
Destination is an enum of SeraiAddress and ExternalAddress.
### Out Instruction
- `destination` (Destination): Address to receive coins to.
- `data` (Option\<Data>): The data to call the destination with.
- `address` (ExternalAddress): Address to transfer the coins included with
this instruction to.
- `data` (Option<Data>): Data to include when transferring coins.
Transfer the coins included with this instruction to the specified address with
the specified data. No validation of external addresses/data is performed
on-chain. If data is specified for a chain not supporting data, it is silently
dropped.
No validation of external addresses/data is performed on-chain. If data is
specified for a chain not supporting data, it is silently dropped.
### Destination
Destination is an enum of SeraiAddress and OutInstruction.
### Shorthand
@ -80,7 +79,7 @@ covered by Shorthand.
- `origin` (Option\<ExternalAddress>): Refundable In Instruction's `origin`.
- `coin` (Coin): Coin to swap funds for.
- `minimum` (Amount): Minimum amount of `coin` to receive.
- `out` (Out Instruction): Final destination for funds.
- `out` (Destination): Final destination for funds.
which expands to:

View file

@ -1,98 +0,0 @@
# Scenarios
### Pong
Pong has Serai receive funds, just to return them. It's a demonstration of the
in/out flow.
```
Shorthand::Raw(
In Instruction {
target: Incoming Asset Contract,
data: native_transfer(Incoming Asset Sender)
}
)
```
### Wrap
Wrap wraps an asset from a connected chain into a Serai Asset, making it usable
with applications on Serai, such as Serai DEX.
```
Shorthand::Raw(
In Instruction {
target: Serai Address
}
)
```
### Swap SRI to Bitcoin
For a SRI to Bitcoin swap, a SRI holder would perform an
[Application Call](../Serai.md#application-calls) to Serai DEX, purchasing
seraiBTC. Once they have seraiBTC, they are able to call `native_transfer`,
transferring the BTC underlying the seraiBTC to a specified Bitcoin address.
### Swap Bitcoin to Monero
For a Bitcoin to Monero swap, the following Shorthand would be used.
```
Shorthand::Swap {
coin: Monero,
minimum: Minimum Monero from Swap,
out: Monero Address
}
```
This Shorthand is expected to generally take:
- 1 byte to identify as Swap.
- 1 byte to not override `origin`.
- 1 byte for `coin`.
- 4 bytes for `minimum`.
- 1 byte for `out`'s `destination`'s ordinal byte.
- 65 bytes for `out`'s `destination`'s address.
- 1 byte to not include `data` in `out`.
Or 74 bytes.
### Add Liquidity (Fresh)
For a user who has never used Serai before, they have three requirements to add
liquidity:
- Minting the Serai asset they wish to add liquidity for
- Acquiring Serai, as liquidity is symmetric
- Acquiring Serai for gas fees
The Add Liquidity Shorthand enables all three of these actions, and actually
adding the liquidity, in just one transaction from a connected network.
```
Shorthand::AddLiquidity {
minimum: Minimum SRI from Swap,
gas: Amount of SRI to keep for gas
address: Serai address for the liquidity tokens and gas
}
```
For adding liquidity from Bitcoin, this Shorthand is expected to generally take:
- 1 byte to identify as Add Liquidity.
- 1 byte to not override `origin`.
- 5 bytes for `minimum`.
- 4 bytes for `gas`.
- 32 bytes for `address`.
Or 43 bytes.
### Add Liquidity (SRI Holder)
For a user who already has SRI, they solely need to have the asset they wish to
add liquidity for via their SRI. They can either purchase it from Serai DEX, or
wrap it as detailed above.
Once they have both their SRI and the asset they wish to provide liquidity for,
they would use a Serai transaction to call the DEX, adding the liquidity.

View file

@ -29,6 +29,8 @@ frame-support = { git = "https://github.com/serai-dex/substrate", default-featur
serai-primitives = { path = "../../serai/primitives", default-features = false }
in-instructions-primitives = { path = "../primitives", default-features = false }
tokens-pallet = { path = "../../tokens/pallet", default-features = false }
[features]
std = [
"thiserror",
@ -47,5 +49,7 @@ std = [
"serai-primitives/std",
"in-instructions-primitives/std",
"tokens-pallet/std",
]
default = ["std"]

View file

@ -13,7 +13,7 @@ use sp_inherents::{InherentIdentifier, IsFatalError};
use sp_runtime::RuntimeDebug;
use serai_primitives::{BlockNumber, BlockHash, Coin};
use serai_primitives::{BlockNumber, BlockHash, Coin, WithAmount, Balance};
pub use in_instructions_primitives as primitives;
use primitives::InInstruction;
@ -24,7 +24,7 @@ pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"ininstrs";
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct Batch {
pub id: BlockHash,
pub instructions: Vec<InInstruction>,
pub instructions: Vec<WithAmount<InInstruction>>,
}
#[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, RuntimeDebug)]
@ -89,10 +89,12 @@ pub mod pallet {
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use tokens_pallet::{Config as TokensConfig, Pallet as Tokens};
use super::*;
#[pallet::config]
pub trait Config: frame_system::Config<BlockNumber = u32> {
pub trait Config: frame_system::Config<BlockNumber = u32> + TokensConfig {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
@ -100,6 +102,7 @@ pub mod pallet {
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
Batch { coin: Coin, id: BlockHash },
Failure { coin: Coin, id: BlockHash, index: u32 },
}
#[pallet::pallet]
@ -122,23 +125,43 @@ pub mod pallet {
}
}
impl<T: Config> Pallet<T> {
fn execute(coin: Coin, instruction: WithAmount<InInstruction>) -> Result<(), ()> {
match instruction.data {
InInstruction::Transfer(address) => {
Tokens::<T>::mint(address, Balance { coin, amount: instruction.amount })
}
_ => panic!("unsupported instruction"),
}
Ok(())
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Mandatory))] // TODO
pub fn execute(origin: OriginFor<T>, updates: Updates) -> DispatchResult {
#[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn update(origin: OriginFor<T>, mut updates: Updates) -> DispatchResult {
ensure_none(origin)?;
assert!(!Once::<T>::exists());
Once::<T>::put(true);
for (coin, update) in updates.iter().enumerate() {
for (coin, update) in updates.iter_mut().enumerate() {
if let Some(update) = update {
let coin = coin_from_index(coin);
BlockNumbers::<T>::insert(coin, update.block_number);
for batch in &update.batches {
// TODO: EXECUTE
for batch in update.batches.iter_mut() {
Self::deposit_event(Event::Batch { coin, id: batch.id });
for (i, instruction) in batch.instructions.drain(..).enumerate() {
if Self::execute(coin, instruction).is_err() {
Self::deposit_event(Event::Failure {
coin,
id: batch.id,
index: u32::try_from(i).unwrap(),
});
}
}
}
}
}
@ -157,7 +180,7 @@ pub mod pallet {
data
.get_data::<Updates>(&INHERENT_IDENTIFIER)
.unwrap()
.map(|updates| Call::execute { updates })
.map(|updates| Call::update { updates })
}
// Assumes that only not yet handled batches are provided as inherent data
@ -167,7 +190,7 @@ pub mod pallet {
let expected = data.get_data::<Updates>(&INHERENT_IDENTIFIER).unwrap().unwrap();
// Match to be exhaustive
let updates = match call {
Call::execute { ref updates } => updates,
Call::update { ref updates } => updates,
_ => Err(InherentError::InvalidCall)?,
};

View file

@ -16,10 +16,9 @@ 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 }
serai-primitives = { path = "../../serai/primitives", default-features = false }
tokens-primitives = { path = "../../tokens/primitives", default-features = false }
[features]
std = ["scale/std", "scale-info/std", "serde", "sp-core/std", "serai-primitives/std"]
std = ["scale/std", "scale-info/std", "serde", "serai-primitives/std", "tokens-primitives/std"]
default = ["std"]

View file

@ -1,38 +0,0 @@
use scale::{Encode, Decode, MaxEncodedLen};
use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
use sp_core::{ConstU32, bounded::BoundedVec};
use serai_primitives::SeraiAddress;
use crate::{MAX_DATA_LEN, ExternalAddress};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum Application {
DEX,
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct ApplicationCall {
application: Application,
data: BoundedVec<u8, ConstU32<{ MAX_DATA_LEN }>>,
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum InInstruction {
Transfer(SeraiAddress),
Call(ApplicationCall),
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct RefundableInInstruction {
pub origin: Option<ExternalAddress>,
pub instruction: InInstruction,
}

View file

@ -8,40 +8,34 @@ use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
use sp_core::{ConstU32, bounded::BoundedVec};
// 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;
// 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 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()
}
}
// Not "in" as "in" is a keyword
mod incoming;
pub use incoming::*;
// Not "out" to match in
mod outgoing;
pub use outgoing::*;
use serai_primitives::{SeraiAddress, ExternalAddress, Data};
mod shorthand;
pub use shorthand::*;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum Application {
DEX,
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct ApplicationCall {
application: Application,
data: Data,
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum InInstruction {
Transfer(SeraiAddress),
Call(ApplicationCall),
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct RefundableInInstruction {
pub origin: Option<ExternalAddress>,
pub instruction: InInstruction,
}

View file

@ -1,25 +0,0 @@
use scale::{Encode, Decode, MaxEncodedLen};
use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
use sp_core::{ConstU32, bounded::BoundedVec};
use serai_primitives::SeraiAddress;
use crate::{MAX_DATA_LEN, ExternalAddress};
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum Destination {
Native(SeraiAddress),
External(ExternalAddress),
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct OutInstruction {
destination: Destination,
data: Option<BoundedVec<u8, ConstU32<{ MAX_DATA_LEN }>>>,
}

View file

@ -4,16 +4,18 @@ use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
use sp_core::{ConstU32, bounded::BoundedVec};
use serai_primitives::{Coin, Amount, SeraiAddress, ExternalAddress, Data};
use serai_primitives::{SeraiAddress, Coin, Amount};
use tokens_primitives::OutInstruction;
use crate::{MAX_DATA_LEN, ExternalAddress, RefundableInInstruction, InInstruction, OutInstruction};
use crate::RefundableInInstruction;
#[cfg(feature = "std")]
use crate::InInstruction;
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum Shorthand {
Raw(BoundedVec<u8, ConstU32<{ MAX_DATA_LEN }>>),
Raw(Data),
Swap {
origin: Option<ExternalAddress>,
coin: Coin,
@ -29,9 +31,10 @@ pub enum Shorthand {
}
impl Shorthand {
#[cfg(feature = "std")]
pub fn transfer(origin: Option<ExternalAddress>, address: SeraiAddress) -> Option<Self> {
Some(Self::Raw(
BoundedVec::try_from(
Data::new(
(RefundableInInstruction { origin, instruction: InInstruction::Transfer(address) })
.encode(),
)
@ -45,7 +48,7 @@ impl TryFrom<Shorthand> for RefundableInInstruction {
fn try_from(shorthand: Shorthand) -> Result<RefundableInInstruction, &'static str> {
Ok(match shorthand {
Shorthand::Raw(raw) => {
RefundableInInstruction::decode(&mut raw.as_ref()).map_err(|_| "invalid raw instruction")?
RefundableInInstruction::decode(&mut raw.data()).map_err(|_| "invalid raw instruction")?
}
Shorthand::Swap { .. } => todo!(),
Shorthand::AddLiquidity { .. } => todo!(),

View file

@ -1,43 +1,42 @@
use sp_core::{Decode, Pair as PairTrait, sr25519::Pair};
use sp_runtime::traits::TrailingZeroInput;
use sp_core::Pair as PairTrait;
use sc_service::ChainType;
use serai_runtime::{
primitives::*, tendermint::crypto::Public, WASM_BINARY, opaque::SessionKeys, GenesisConfig,
SystemConfig, BalancesConfig, AssetsConfig, ValidatorSetsConfig, SessionConfig,
primitives::*, tokens::primitives::ADDRESS as TOKENS_ADDRESS, tendermint::crypto::Public,
WASM_BINARY, opaque::SessionKeys, GenesisConfig, SystemConfig, BalancesConfig, AssetsConfig,
ValidatorSetsConfig, SessionConfig,
};
pub type ChainSpec = sc_service::GenericChainSpec<GenesisConfig>;
fn insecure_pair_from_name(name: &'static str) -> Pair {
Pair::from_string(&format!("//{name}"), None).unwrap()
}
fn address_from_name(name: &'static str) -> SeraiAddress {
fn account_from_name(name: &'static str) -> PublicKey {
insecure_pair_from_name(name).public()
}
fn testnet_genesis(
wasm_binary: &[u8],
validators: &[&'static str],
endowed_accounts: Vec<SeraiAddress>,
endowed_accounts: Vec<PublicKey>,
) -> GenesisConfig {
let session_key = |name| {
let key = address_from_name(name);
let key = account_from_name(name);
(key, key, SessionKeys { tendermint: Public::from(key) })
};
// TODO: Replace with a call to the pallet to ask for its account
let owner = SeraiAddress::decode(&mut TrailingZeroInput::new(b"tokens")).unwrap();
GenesisConfig {
system: SystemConfig { code: wasm_binary.to_vec() },
balances: BalancesConfig {
balances: endowed_accounts.iter().cloned().map(|k| (k, 1 << 60)).collect(),
},
transaction_payment: Default::default(),
assets: AssetsConfig {
assets: [BITCOIN, ETHER, DAI, MONERO].iter().map(|coin| (*coin, owner, true, 1)).collect(),
assets: [BITCOIN, ETHER, DAI, MONERO]
.iter()
.map(|coin| (*coin, TOKENS_ADDRESS.into(), true, 1))
.collect(),
metadata: vec![
(BITCOIN, b"Bitcoin".to_vec(), b"BTC".to_vec(), 8),
// Reduce to 8 decimals to feasibly fit within u64 (instead of its native u256)
@ -47,14 +46,13 @@ fn testnet_genesis(
],
accounts: vec![],
},
transaction_payment: Default::default(),
validator_sets: ValidatorSetsConfig {
bond: Amount(1_000_000) * COIN,
coins: vec![BITCOIN, ETHER, DAI, MONERO],
participants: validators.iter().map(|name| address_from_name(name)).collect(),
},
session: SessionConfig { keys: validators.iter().map(|name| session_key(*name)).collect() },
validator_sets: ValidatorSetsConfig {
bond: Amount(1_000_000 * 10_u64.pow(8)),
coins: vec![BITCOIN, ETHER, DAI, MONERO],
participants: validators.iter().map(|name| account_from_name(name)).collect(),
},
}
}
@ -72,18 +70,18 @@ pub fn development_config() -> Result<ChainSpec, &'static str> {
wasm_binary,
&["Alice"],
vec![
address_from_name("Alice"),
address_from_name("Bob"),
address_from_name("Charlie"),
address_from_name("Dave"),
address_from_name("Eve"),
address_from_name("Ferdie"),
address_from_name("Alice//stash"),
address_from_name("Bob//stash"),
address_from_name("Charlie//stash"),
address_from_name("Dave//stash"),
address_from_name("Eve//stash"),
address_from_name("Ferdie//stash"),
account_from_name("Alice"),
account_from_name("Bob"),
account_from_name("Charlie"),
account_from_name("Dave"),
account_from_name("Eve"),
account_from_name("Ferdie"),
account_from_name("Alice//stash"),
account_from_name("Bob//stash"),
account_from_name("Charlie//stash"),
account_from_name("Dave//stash"),
account_from_name("Eve//stash"),
account_from_name("Ferdie//stash"),
],
)
},
@ -116,18 +114,18 @@ pub fn testnet_config() -> Result<ChainSpec, &'static str> {
wasm_binary,
&["Alice", "Bob", "Charlie"],
vec![
address_from_name("Alice"),
address_from_name("Bob"),
address_from_name("Charlie"),
address_from_name("Dave"),
address_from_name("Eve"),
address_from_name("Ferdie"),
address_from_name("Alice//stash"),
address_from_name("Bob//stash"),
address_from_name("Charlie//stash"),
address_from_name("Dave//stash"),
address_from_name("Eve//stash"),
address_from_name("Ferdie//stash"),
account_from_name("Alice"),
account_from_name("Bob"),
account_from_name("Charlie"),
account_from_name("Dave"),
account_from_name("Eve"),
account_from_name("Ferdie"),
account_from_name("Alice//stash"),
account_from_name("Bob//stash"),
account_from_name("Charlie//stash"),
account_from_name("Dave//stash"),
account_from_name("Eve//stash"),
account_from_name("Ferdie//stash"),
],
)
},

View file

@ -67,7 +67,7 @@ pub fn create_benchmark_extrinsic(
UncheckedExtrinsic::new_signed(
call.clone(),
sender.public(),
sender.public().into(),
SignedPayload::from_raw(
call,
extra.clone(),

View file

@ -6,7 +6,11 @@ use sp_blockchain::{Error as BlockchainError, HeaderBackend, HeaderMetadata};
use sp_block_builder::BlockBuilder;
use sp_api::ProvideRuntimeApi;
use serai_runtime::{primitives::SeraiAddress, opaque::Block, Balance, Index};
use serai_runtime::{
primitives::{SubstrateAmount, PublicKey},
opaque::Block,
Index,
};
pub use sc_rpc_api::DenyUnsafe;
use sc_transaction_pool_api::TransactionPool;
@ -29,8 +33,8 @@ pub fn create_full<
deps: FullDeps<C, P>,
) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>>
where
C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, SeraiAddress, Index>
+ pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, Balance>
C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, PublicKey, Index>
+ pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, SubstrateAmount>
+ BlockBuilder<Block>,
{
use substrate_frame_rpc_system::{System, SystemApiServer};

View file

@ -76,9 +76,9 @@ impl TendermintClientMinimal for TendermintValidatorFirm {
// guaranteed not to grow the block?
const PROPOSED_BLOCK_SIZE_LIMIT: usize = { BLOCK_SIZE as usize };
// 3 seconds
const BLOCK_PROCESSING_TIME_IN_SECONDS: u32 = { (TARGET_BLOCK_TIME / 2 / 1000) as u32 };
const BLOCK_PROCESSING_TIME_IN_SECONDS: u32 = { (TARGET_BLOCK_TIME / 2) as u32 };
// 1 second
const LATENCY_TIME_IN_SECONDS: u32 = { (TARGET_BLOCK_TIME / 2 / 3 / 1000) as u32 };
const LATENCY_TIME_IN_SECONDS: u32 = { (TARGET_BLOCK_TIME / 2 / 3) as u32 };
type Block = Block;
type Backend = sc_client_db::Backend<Block>;
@ -101,7 +101,7 @@ impl TendermintValidator for TendermintValidatorFirm {
pub fn new_partial(
config: &Configuration,
) -> Result<(TendermintImport<TendermintValidatorFirm>, PartialComponents), ServiceError> {
debug_assert_eq!(TARGET_BLOCK_TIME, 6000);
debug_assert_eq!(TARGET_BLOCK_TIME, 6);
if config.keystore_remote.is_some() {
return Err(ServiceError::Other("Remote Keystores are not supported".to_string()));

View file

@ -41,6 +41,7 @@ pallet-balances = { git = "https://github.com/serai-dex/substrate", default-feat
pallet-assets = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", default-features = false }
tokens-pallet = { path = "../tokens/pallet", default-features = false }
in-instructions-pallet = { path = "../in-instructions/pallet", default-features = false }
validator-sets-pallet = { path = "../validator-sets/pallet", default-features = false }
@ -78,9 +79,10 @@ std = [
"serai-primitives/std",
"pallet-balances/std",
"pallet-assets/std",
"pallet-transaction-payment/std",
"pallet-assets/std",
"tokens-pallet/std",
"in-instructions-pallet/std",
"validator-sets-pallet/std",

View file

@ -16,6 +16,7 @@ pub use pallet_balances as balances;
pub use pallet_transaction_payment as transaction_payment;
pub use pallet_assets as assets;
pub use tokens_pallet as tokens;
pub use in_instructions_pallet as in_instructions;
pub use validator_sets_pallet as validator_sets;
@ -33,15 +34,15 @@ use sp_version::NativeVersion;
use sp_runtime::{
create_runtime_str, generic, impl_opaque_keys, KeyTypeId,
traits::{Convert, OpaqueKeys, IdentityLookup, BlakeTwo256, Block as BlockT},
traits::{Convert, OpaqueKeys, BlakeTwo256, Block as BlockT},
transaction_validity::{TransactionSource, TransactionValidity},
ApplyExtrinsicResult, Perbill,
};
use primitives::{PublicKey, Signature, SeraiAddress, Coin};
use primitives::{PublicKey, SeraiAddress, AccountLookup, Signature, SubstrateAmount, Coin};
use support::{
traits::{ConstU8, ConstU32, ConstU64},
traits::{ConstU8, ConstU32, ConstU64, Contains},
weights::{
constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND},
IdentityFee, Weight,
@ -56,14 +57,6 @@ use session::PeriodicSessions;
/// An index to a block.
pub type BlockNumber = u32;
/// Balance of an account.
// Distinct from serai-primitives Amount due to Substrate's requirements on this type.
// If Amount could be dropped in here, it would be.
// While Amount could have all the necessary traits implemented, not only are they many, yet it'd
// make Amount a larger type, providing more operations than desired.
// The current type's minimalism sets clear bounds on usage.
pub type Balance = u64;
/// Index of a transaction in the chain, for a given account.
pub type Index = u32;
@ -104,10 +97,10 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// 1 MB
pub const BLOCK_SIZE: u32 = 1024 * 1024;
// 6 seconds
pub const TARGET_BLOCK_TIME: u64 = 6000;
pub const TARGET_BLOCK_TIME: u64 = 6;
/// Measured in blocks.
pub const MINUTES: BlockNumber = 60_000 / (TARGET_BLOCK_TIME as BlockNumber);
pub const MINUTES: BlockNumber = 60 / (TARGET_BLOCK_TIME as BlockNumber);
pub const HOURS: BlockNumber = MINUTES * 60;
pub const DAYS: BlockNumber = HOURS * 24;
@ -134,13 +127,44 @@ parameter_types! {
);
}
pub struct CallFilter;
impl Contains<RuntimeCall> for CallFilter {
fn contains(call: &RuntimeCall) -> bool {
if let RuntimeCall::Balances(call) = call {
return matches!(call, balances::Call::transfer { .. } | balances::Call::transfer_all { .. });
}
if let RuntimeCall::Assets(call) = call {
return matches!(
call,
assets::Call::approve_transfer { .. } |
assets::Call::cancel_approval { .. } |
assets::Call::transfer { .. } |
assets::Call::transfer_approved { .. }
);
}
if let RuntimeCall::Tokens(call) = call {
return matches!(call, tokens::Call::burn { .. });
}
if let RuntimeCall::InInstructions(call) = call {
return matches!(call, in_instructions::Call::update { .. });
}
if let RuntimeCall::ValidatorSets(call) = call {
return matches!(call, validator_sets::Call::vote { .. });
}
false
}
}
impl system::Config for Runtime {
type BaseCallFilter = support::traits::Everything;
type BaseCallFilter = CallFilter;
type BlockWeights = BlockWeights;
type BlockLength = BlockLength;
type AccountId = SeraiAddress;
type AccountId = PublicKey;
type RuntimeCall = RuntimeCall;
type Lookup = IdentityLookup<SeraiAddress>;
type Lookup = AccountLookup;
type Index = Index;
type BlockNumber = BlockNumber;
type Hash = Hash;
@ -157,7 +181,7 @@ impl system::Config for Runtime {
type OnKilledAccount = ();
type OnSetCode = ();
type AccountData = balances::AccountData<Balance>;
type AccountData = balances::AccountData<SubstrateAmount>;
type SystemWeightInfo = ();
type SS58Prefix = SS58Prefix; // TODO: Remove for Bech32m
@ -168,7 +192,7 @@ impl balances::Config for Runtime {
type MaxLocks = ConstU32<50>;
type MaxReserves = ();
type ReserveIdentifier = [u8; 8];
type Balance = Balance;
type Balance = SubstrateAmount;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ConstU64<500>;
@ -176,9 +200,18 @@ impl balances::Config for Runtime {
type WeightInfo = balances::weights::SubstrateWeight<Runtime>;
}
impl transaction_payment::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type OnChargeTransaction = CurrencyAdapter<Balances, ()>;
type OperationalFeeMultiplier = ConstU8<5>;
type WeightToFee = IdentityFee<SubstrateAmount>;
type LengthToFee = IdentityFee<SubstrateAmount>;
type FeeMultiplierUpdate = ();
}
impl assets::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type Balance = Balance;
type Balance = SubstrateAmount;
type Currency = Balances;
type AssetId = Coin;
@ -186,8 +219,8 @@ impl assets::Config for Runtime {
type StringLimit = ConstU32<32>;
// Don't allow anyone to create assets
type CreateOrigin = support::traits::AsEnsureOriginWithArg<system::EnsureNever<SeraiAddress>>;
type ForceOrigin = system::EnsureRoot<SeraiAddress>;
type CreateOrigin = support::traits::AsEnsureOriginWithArg<system::EnsureNever<PublicKey>>;
type ForceOrigin = system::EnsureRoot<PublicKey>;
// Don't charge fees nor kill accounts
type RemoveItemsLimit = ConstU32<0>;
@ -207,13 +240,8 @@ impl assets::Config for Runtime {
type BenchmarkHelper = ();
}
impl transaction_payment::Config for Runtime {
impl tokens::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type OnChargeTransaction = CurrencyAdapter<Balances, ()>;
type OperationalFeeMultiplier = ConstU8<5>;
type WeightToFee = IdentityFee<Balance>;
type LengthToFee = IdentityFee<Balance>;
type FeeMultiplierUpdate = ();
}
impl in_instructions::Config for Runtime {
@ -236,7 +264,7 @@ impl validator_sets::Config for Runtime {
impl session::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type ValidatorId = SeraiAddress;
type ValidatorId = PublicKey;
type ValidatorIdOf = IdentityValidatorIdOf;
type ShouldEndSession = Sessions;
type NextSessionRotation = Sessions;
@ -283,6 +311,7 @@ construct_runtime!(
TransactionPayment: transaction_payment,
Assets: assets,
Tokens: tokens,
InInstructions: in_instructions,
ValidatorSets: validator_sets,
@ -381,31 +410,31 @@ sp_api::impl_runtime_apis! {
}
fn validators() -> Vec<PublicKey> {
Session::validators()
Session::validators().drain(..).map(Into::into).collect()
}
}
impl frame_system_rpc_runtime_api::AccountNonceApi<Block, SeraiAddress, Index> for Runtime {
fn account_nonce(account: SeraiAddress) -> Index {
impl frame_system_rpc_runtime_api::AccountNonceApi<Block, PublicKey, Index> for Runtime {
fn account_nonce(account: PublicKey) -> Index {
System::account_nonce(account)
}
}
impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi<
Block,
Balance
SubstrateAmount
> for Runtime {
fn query_info(
uxt: <Block as BlockT>::Extrinsic,
len: u32,
) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo<Balance> {
) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo<SubstrateAmount> {
TransactionPayment::query_info(uxt, len)
}
fn query_fee_details(
uxt: <Block as BlockT>::Extrinsic,
len: u32,
) -> transaction_payment::FeeDetails<Balance> {
) -> transaction_payment::FeeDetails<SubstrateAmount> {
TransactionPayment::query_fee_details(uxt, len)
}
}

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

View file

@ -0,0 +1,38 @@
[package]
name = "tokens-pallet"
version = "0.1.0"
description = "Mint and burn Serai tokens"
license = "AGPL-3.0-only"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
publish = false
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive", "max-encoded-len"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-assets = { git = "https://github.com/serai-dex/substrate", default-features = false }
serai-primitives = { path = "../../serai/primitives", default-features = false }
tokens-primitives = { path = "../primitives", default-features = false }
[features]
std = [
"scale/std",
"scale-info/std",
"frame-system/std",
"frame-support/std",
"pallet-assets/std",
"serai-primitives/std",
]
default = ["std"]

View file

@ -0,0 +1,15 @@
AGPL-3.0-only license
Copyright (c) 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/>.

View file

@ -0,0 +1,83 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
pub use tokens_primitives as primitives;
#[frame_support::pallet]
pub mod pallet {
use frame_support::pallet_prelude::*;
use frame_system::{pallet_prelude::*, RawOrigin};
use pallet_assets::{Config as AssetsConfig, Pallet as AssetsPallet};
use serai_primitives::{SubstrateAmount, Coin, Balance, PublicKey, SeraiAddress, AccountLookup};
use primitives::{ADDRESS, OutInstruction};
use super::*;
#[pallet::config]
pub trait Config:
frame_system::Config<AccountId = PublicKey, Lookup = AccountLookup>
+ AssetsConfig<AssetIdParameter = Coin, Balance = SubstrateAmount>
{
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
}
#[pallet::event]
#[pallet::generate_deposit(fn deposit_event)]
pub enum Event<T: Config> {
// Mint is technically redundant as the assets pallet has the exact same event already
// Providing our own definition here just helps consolidate code
Mint { address: SeraiAddress, balance: Balance },
Burn { address: SeraiAddress, balance: Balance, instruction: OutInstruction },
}
#[pallet::pallet]
#[pallet::generate_store(pub(crate) trait Store)]
pub struct Pallet<T>(PhantomData<T>);
impl<T: Config> Pallet<T> {
fn burn_internal(
address: SeraiAddress,
balance: Balance,
instruction: OutInstruction,
) -> DispatchResult {
AssetsPallet::<T>::burn(
RawOrigin::Signed(ADDRESS.into()).into(),
balance.coin,
address,
balance.amount.0,
)?;
Pallet::<T>::deposit_event(Event::Burn { address, balance, instruction });
Ok(())
}
pub fn mint(address: SeraiAddress, balance: Balance) {
// TODO: Prevent minting when it'd cause an amount exceeding the bond
AssetsPallet::<T>::mint(
RawOrigin::Signed(ADDRESS.into()).into(),
balance.coin,
address,
balance.amount.0,
)
.unwrap();
Pallet::<T>::deposit_event(Event::Mint { address, balance });
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn burn(
origin: OriginFor<T>,
balance: Balance,
instruction: OutInstruction,
) -> DispatchResult {
Self::burn_internal(ensure_signed(origin)?.into(), balance, instruction)
}
}
}
pub use pallet::*;

View file

@ -0,0 +1,26 @@
[package]
name = "tokens-primitives"
version = "0.1.0"
description = "Serai tokens primitives"
license = "MIT"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"] }
serde = { version = "1", features = ["derive"], optional = true }
serai-primitives = { path = "../../serai/primitives", default-features = false }
[dev-dependencies]
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
[features]
std = ["scale/std", "scale-info/std", "serde", "sp-runtime/std", "serai-primitives/std"]
default = ["std"]

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Luke Parker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,33 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![cfg_attr(not(feature = "std"), no_std)]
use scale::{Encode, Decode, MaxEncodedLen};
use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Serialize, Deserialize};
use serai_primitives::{SeraiAddress, ExternalAddress, Data, pallet_address};
pub const ADDRESS: SeraiAddress = pallet_address(b"Tokens");
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct OutInstruction {
pub address: ExternalAddress,
pub data: Option<Data>,
}
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub enum Destination {
Native(SeraiAddress),
External(OutInstruction),
}
#[test]
fn address() {
use sp_runtime::traits::TrailingZeroInput;
assert_eq!(ADDRESS, SeraiAddress::decode(&mut TrailingZeroInput::new(b"Tokens")).unwrap());
}