mirror of
https://github.com/serai-dex/serai.git
synced 2024-12-22 19:49:22 +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:
parent
f12cc2cca6
commit
2ace339975
39 changed files with 1213 additions and 594 deletions
328
Cargo.lock
generated
328
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||
|
|
|
@ -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" },
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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"]
|
||||
|
|
|
@ -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)?,
|
||||
};
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 }>>>,
|
||||
}
|
|
@ -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!(),
|
||||
|
|
|
@ -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"),
|
||||
],
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
38
substrate/tokens/pallet/Cargo.toml
Normal file
38
substrate/tokens/pallet/Cargo.toml
Normal 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"]
|
15
substrate/tokens/pallet/LICENSE
Normal file
15
substrate/tokens/pallet/LICENSE
Normal 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/>.
|
83
substrate/tokens/pallet/src/lib.rs
Normal file
83
substrate/tokens/pallet/src/lib.rs
Normal 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::*;
|
26
substrate/tokens/primitives/Cargo.toml
Normal file
26
substrate/tokens/primitives/Cargo.toml
Normal 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"]
|
21
substrate/tokens/primitives/LICENSE
Normal file
21
substrate/tokens/primitives/LICENSE
Normal 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.
|
33
substrate/tokens/primitives/src/lib.rs
Normal file
33
substrate/tokens/primitives/src/lib.rs
Normal 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());
|
||||
}
|
Loading…
Reference in a new issue