mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-09 12:29:27 +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/primitives",
|
||||||
"substrate/serai/client",
|
"substrate/serai/client",
|
||||||
|
|
||||||
|
"substrate/tokens/primitives",
|
||||||
|
"substrate/tokens/pallet",
|
||||||
|
|
||||||
"substrate/in-instructions/primitives",
|
"substrate/in-instructions/primitives",
|
||||||
"substrate/in-instructions/pallet",
|
"substrate/in-instructions/pallet",
|
||||||
"substrate/in-instructions/client",
|
"substrate/in-instructions/client",
|
||||||
|
|
|
@ -48,6 +48,8 @@ exceptions = [
|
||||||
|
|
||||||
{ allow = ["AGPL-3.0"], name = "serai-processor" },
|
{ 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-pallet" },
|
||||||
{ allow = ["AGPL-3.0"], name = "in-instructions-client" },
|
{ 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
|
### Refundable In Instruction
|
||||||
|
|
||||||
- `origin` (Option\<ExternalAddress>): Address, from the network of origin,
|
- `origin` (Option\<ExternalAddress>): Address, from the network of
|
||||||
which sent coins in.
|
origin, which sent coins in.
|
||||||
- `instruction` (InInstruction): The action to perform with the incoming
|
- `instruction` (InInstruction): The action to perform with the
|
||||||
coins.
|
incoming coins.
|
||||||
|
|
||||||
Networks may automatically provide `origin`. If they do, the instruction may
|
Networks may automatically provide `origin`. If they do, the instruction may
|
||||||
still provide `origin`, overriding the automatically provided value.
|
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 the instruction fails, coins are scheduled to be returned to `origin`,
|
||||||
if provided.
|
if provided.
|
||||||
|
|
||||||
### Destination
|
|
||||||
|
|
||||||
Destination is an enum of SeraiAddress and ExternalAddress.
|
|
||||||
|
|
||||||
### Out Instruction
|
### Out Instruction
|
||||||
|
|
||||||
- `destination` (Destination): Address to receive coins to.
|
- `address` (ExternalAddress): Address to transfer the coins included with
|
||||||
- `data` (Option\<Data>): The data to call the destination 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
|
No validation of external addresses/data is performed on-chain. If data is
|
||||||
the specified data. No validation of external addresses/data is performed
|
specified for a chain not supporting data, it is silently dropped.
|
||||||
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
|
### Shorthand
|
||||||
|
|
||||||
|
@ -80,7 +79,7 @@ covered by Shorthand.
|
||||||
- `origin` (Option\<ExternalAddress>): Refundable In Instruction's `origin`.
|
- `origin` (Option\<ExternalAddress>): Refundable In Instruction's `origin`.
|
||||||
- `coin` (Coin): Coin to swap funds for.
|
- `coin` (Coin): Coin to swap funds for.
|
||||||
- `minimum` (Amount): Minimum amount of `coin` to receive.
|
- `minimum` (Amount): Minimum amount of `coin` to receive.
|
||||||
- `out` (Out Instruction): Final destination for funds.
|
- `out` (Destination): Final destination for funds.
|
||||||
|
|
||||||
which expands to:
|
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 }
|
serai-primitives = { path = "../../serai/primitives", default-features = false }
|
||||||
in-instructions-primitives = { path = "../primitives", default-features = false }
|
in-instructions-primitives = { path = "../primitives", default-features = false }
|
||||||
|
|
||||||
|
tokens-pallet = { path = "../../tokens/pallet", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = [
|
std = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
@ -47,5 +49,7 @@ std = [
|
||||||
|
|
||||||
"serai-primitives/std",
|
"serai-primitives/std",
|
||||||
"in-instructions-primitives/std",
|
"in-instructions-primitives/std",
|
||||||
|
|
||||||
|
"tokens-pallet/std",
|
||||||
]
|
]
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
|
|
|
@ -13,7 +13,7 @@ use sp_inherents::{InherentIdentifier, IsFatalError};
|
||||||
|
|
||||||
use sp_runtime::RuntimeDebug;
|
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;
|
pub use in_instructions_primitives as primitives;
|
||||||
use primitives::InInstruction;
|
use primitives::InInstruction;
|
||||||
|
@ -24,7 +24,7 @@ pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"ininstrs";
|
||||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||||
pub struct Batch {
|
pub struct Batch {
|
||||||
pub id: BlockHash,
|
pub id: BlockHash,
|
||||||
pub instructions: Vec<InInstruction>,
|
pub instructions: Vec<WithAmount<InInstruction>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, RuntimeDebug)]
|
#[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, RuntimeDebug)]
|
||||||
|
@ -89,10 +89,12 @@ pub mod pallet {
|
||||||
use frame_support::pallet_prelude::*;
|
use frame_support::pallet_prelude::*;
|
||||||
use frame_system::pallet_prelude::*;
|
use frame_system::pallet_prelude::*;
|
||||||
|
|
||||||
|
use tokens_pallet::{Config as TokensConfig, Pallet as Tokens};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[pallet::config]
|
#[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>;
|
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)]
|
#[pallet::generate_deposit(fn deposit_event)]
|
||||||
pub enum Event<T: Config> {
|
pub enum Event<T: Config> {
|
||||||
Batch { coin: Coin, id: BlockHash },
|
Batch { coin: Coin, id: BlockHash },
|
||||||
|
Failure { coin: Coin, id: BlockHash, index: u32 },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pallet::pallet]
|
#[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]
|
#[pallet::call]
|
||||||
impl<T: Config> Pallet<T> {
|
impl<T: Config> Pallet<T> {
|
||||||
#[pallet::call_index(0)]
|
#[pallet::call_index(0)]
|
||||||
#[pallet::weight((0, DispatchClass::Mandatory))] // TODO
|
#[pallet::weight((0, DispatchClass::Operational))] // TODO
|
||||||
pub fn execute(origin: OriginFor<T>, updates: Updates) -> DispatchResult {
|
pub fn update(origin: OriginFor<T>, mut updates: Updates) -> DispatchResult {
|
||||||
ensure_none(origin)?;
|
ensure_none(origin)?;
|
||||||
assert!(!Once::<T>::exists());
|
assert!(!Once::<T>::exists());
|
||||||
Once::<T>::put(true);
|
Once::<T>::put(true);
|
||||||
|
|
||||||
for (coin, update) in updates.iter().enumerate() {
|
for (coin, update) in updates.iter_mut().enumerate() {
|
||||||
if let Some(update) = update {
|
if let Some(update) = update {
|
||||||
let coin = coin_from_index(coin);
|
let coin = coin_from_index(coin);
|
||||||
BlockNumbers::<T>::insert(coin, update.block_number);
|
BlockNumbers::<T>::insert(coin, update.block_number);
|
||||||
|
|
||||||
for batch in &update.batches {
|
for batch in update.batches.iter_mut() {
|
||||||
// TODO: EXECUTE
|
|
||||||
Self::deposit_event(Event::Batch { coin, id: batch.id });
|
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
|
data
|
||||||
.get_data::<Updates>(&INHERENT_IDENTIFIER)
|
.get_data::<Updates>(&INHERENT_IDENTIFIER)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.map(|updates| Call::execute { updates })
|
.map(|updates| Call::update { updates })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assumes that only not yet handled batches are provided as inherent data
|
// 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();
|
let expected = data.get_data::<Updates>(&INHERENT_IDENTIFIER).unwrap().unwrap();
|
||||||
// Match to be exhaustive
|
// Match to be exhaustive
|
||||||
let updates = match call {
|
let updates = match call {
|
||||||
Call::execute { ref updates } => updates,
|
Call::update { ref updates } => updates,
|
||||||
_ => Err(InherentError::InvalidCall)?,
|
_ => Err(InherentError::InvalidCall)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,10 +16,9 @@ scale-info = { version = "2", default-features = false, features = ["derive"] }
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
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 }
|
serai-primitives = { path = "../../serai/primitives", default-features = false }
|
||||||
|
tokens-primitives = { path = "../../tokens/primitives", default-features = false }
|
||||||
|
|
||||||
[features]
|
[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"]
|
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")]
|
#[cfg(feature = "std")]
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use sp_core::{ConstU32, bounded::BoundedVec};
|
use serai_primitives::{SeraiAddress, ExternalAddress, Data};
|
||||||
|
|
||||||
// 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::*;
|
|
||||||
|
|
||||||
mod shorthand;
|
mod shorthand;
|
||||||
pub use 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")]
|
#[cfg(feature = "std")]
|
||||||
use serde::{Serialize, Deserialize};
|
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)]
|
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)]
|
||||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||||
pub enum Shorthand {
|
pub enum Shorthand {
|
||||||
Raw(BoundedVec<u8, ConstU32<{ MAX_DATA_LEN }>>),
|
Raw(Data),
|
||||||
Swap {
|
Swap {
|
||||||
origin: Option<ExternalAddress>,
|
origin: Option<ExternalAddress>,
|
||||||
coin: Coin,
|
coin: Coin,
|
||||||
|
@ -29,9 +31,10 @@ pub enum Shorthand {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Shorthand {
|
impl Shorthand {
|
||||||
|
#[cfg(feature = "std")]
|
||||||
pub fn transfer(origin: Option<ExternalAddress>, address: SeraiAddress) -> Option<Self> {
|
pub fn transfer(origin: Option<ExternalAddress>, address: SeraiAddress) -> Option<Self> {
|
||||||
Some(Self::Raw(
|
Some(Self::Raw(
|
||||||
BoundedVec::try_from(
|
Data::new(
|
||||||
(RefundableInInstruction { origin, instruction: InInstruction::Transfer(address) })
|
(RefundableInInstruction { origin, instruction: InInstruction::Transfer(address) })
|
||||||
.encode(),
|
.encode(),
|
||||||
)
|
)
|
||||||
|
@ -45,7 +48,7 @@ impl TryFrom<Shorthand> for RefundableInInstruction {
|
||||||
fn try_from(shorthand: Shorthand) -> Result<RefundableInInstruction, &'static str> {
|
fn try_from(shorthand: Shorthand) -> Result<RefundableInInstruction, &'static str> {
|
||||||
Ok(match shorthand {
|
Ok(match shorthand {
|
||||||
Shorthand::Raw(raw) => {
|
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::Swap { .. } => todo!(),
|
||||||
Shorthand::AddLiquidity { .. } => todo!(),
|
Shorthand::AddLiquidity { .. } => todo!(),
|
||||||
|
|
|
@ -1,43 +1,42 @@
|
||||||
use sp_core::{Decode, Pair as PairTrait, sr25519::Pair};
|
use sp_core::Pair as PairTrait;
|
||||||
use sp_runtime::traits::TrailingZeroInput;
|
|
||||||
|
|
||||||
use sc_service::ChainType;
|
use sc_service::ChainType;
|
||||||
|
|
||||||
use serai_runtime::{
|
use serai_runtime::{
|
||||||
primitives::*, tendermint::crypto::Public, WASM_BINARY, opaque::SessionKeys, GenesisConfig,
|
primitives::*, tokens::primitives::ADDRESS as TOKENS_ADDRESS, tendermint::crypto::Public,
|
||||||
SystemConfig, BalancesConfig, AssetsConfig, ValidatorSetsConfig, SessionConfig,
|
WASM_BINARY, opaque::SessionKeys, GenesisConfig, SystemConfig, BalancesConfig, AssetsConfig,
|
||||||
|
ValidatorSetsConfig, SessionConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type ChainSpec = sc_service::GenericChainSpec<GenesisConfig>;
|
pub type ChainSpec = sc_service::GenericChainSpec<GenesisConfig>;
|
||||||
|
|
||||||
fn insecure_pair_from_name(name: &'static str) -> Pair {
|
fn account_from_name(name: &'static str) -> PublicKey {
|
||||||
Pair::from_string(&format!("//{name}"), None).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn address_from_name(name: &'static str) -> SeraiAddress {
|
|
||||||
insecure_pair_from_name(name).public()
|
insecure_pair_from_name(name).public()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn testnet_genesis(
|
fn testnet_genesis(
|
||||||
wasm_binary: &[u8],
|
wasm_binary: &[u8],
|
||||||
validators: &[&'static str],
|
validators: &[&'static str],
|
||||||
endowed_accounts: Vec<SeraiAddress>,
|
endowed_accounts: Vec<PublicKey>,
|
||||||
) -> GenesisConfig {
|
) -> GenesisConfig {
|
||||||
let session_key = |name| {
|
let session_key = |name| {
|
||||||
let key = address_from_name(name);
|
let key = account_from_name(name);
|
||||||
(key, key, SessionKeys { tendermint: Public::from(key) })
|
(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 {
|
GenesisConfig {
|
||||||
system: SystemConfig { code: wasm_binary.to_vec() },
|
system: SystemConfig { code: wasm_binary.to_vec() },
|
||||||
|
|
||||||
balances: BalancesConfig {
|
balances: BalancesConfig {
|
||||||
balances: endowed_accounts.iter().cloned().map(|k| (k, 1 << 60)).collect(),
|
balances: endowed_accounts.iter().cloned().map(|k| (k, 1 << 60)).collect(),
|
||||||
},
|
},
|
||||||
|
transaction_payment: Default::default(),
|
||||||
|
|
||||||
assets: AssetsConfig {
|
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![
|
metadata: vec![
|
||||||
(BITCOIN, b"Bitcoin".to_vec(), b"BTC".to_vec(), 8),
|
(BITCOIN, b"Bitcoin".to_vec(), b"BTC".to_vec(), 8),
|
||||||
// Reduce to 8 decimals to feasibly fit within u64 (instead of its native u256)
|
// Reduce to 8 decimals to feasibly fit within u64 (instead of its native u256)
|
||||||
|
@ -47,14 +46,13 @@ fn testnet_genesis(
|
||||||
],
|
],
|
||||||
accounts: vec![],
|
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() },
|
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,
|
wasm_binary,
|
||||||
&["Alice"],
|
&["Alice"],
|
||||||
vec![
|
vec![
|
||||||
address_from_name("Alice"),
|
account_from_name("Alice"),
|
||||||
address_from_name("Bob"),
|
account_from_name("Bob"),
|
||||||
address_from_name("Charlie"),
|
account_from_name("Charlie"),
|
||||||
address_from_name("Dave"),
|
account_from_name("Dave"),
|
||||||
address_from_name("Eve"),
|
account_from_name("Eve"),
|
||||||
address_from_name("Ferdie"),
|
account_from_name("Ferdie"),
|
||||||
address_from_name("Alice//stash"),
|
account_from_name("Alice//stash"),
|
||||||
address_from_name("Bob//stash"),
|
account_from_name("Bob//stash"),
|
||||||
address_from_name("Charlie//stash"),
|
account_from_name("Charlie//stash"),
|
||||||
address_from_name("Dave//stash"),
|
account_from_name("Dave//stash"),
|
||||||
address_from_name("Eve//stash"),
|
account_from_name("Eve//stash"),
|
||||||
address_from_name("Ferdie//stash"),
|
account_from_name("Ferdie//stash"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -116,18 +114,18 @@ pub fn testnet_config() -> Result<ChainSpec, &'static str> {
|
||||||
wasm_binary,
|
wasm_binary,
|
||||||
&["Alice", "Bob", "Charlie"],
|
&["Alice", "Bob", "Charlie"],
|
||||||
vec![
|
vec![
|
||||||
address_from_name("Alice"),
|
account_from_name("Alice"),
|
||||||
address_from_name("Bob"),
|
account_from_name("Bob"),
|
||||||
address_from_name("Charlie"),
|
account_from_name("Charlie"),
|
||||||
address_from_name("Dave"),
|
account_from_name("Dave"),
|
||||||
address_from_name("Eve"),
|
account_from_name("Eve"),
|
||||||
address_from_name("Ferdie"),
|
account_from_name("Ferdie"),
|
||||||
address_from_name("Alice//stash"),
|
account_from_name("Alice//stash"),
|
||||||
address_from_name("Bob//stash"),
|
account_from_name("Bob//stash"),
|
||||||
address_from_name("Charlie//stash"),
|
account_from_name("Charlie//stash"),
|
||||||
address_from_name("Dave//stash"),
|
account_from_name("Dave//stash"),
|
||||||
address_from_name("Eve//stash"),
|
account_from_name("Eve//stash"),
|
||||||
address_from_name("Ferdie//stash"),
|
account_from_name("Ferdie//stash"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -67,7 +67,7 @@ pub fn create_benchmark_extrinsic(
|
||||||
|
|
||||||
UncheckedExtrinsic::new_signed(
|
UncheckedExtrinsic::new_signed(
|
||||||
call.clone(),
|
call.clone(),
|
||||||
sender.public(),
|
sender.public().into(),
|
||||||
SignedPayload::from_raw(
|
SignedPayload::from_raw(
|
||||||
call,
|
call,
|
||||||
extra.clone(),
|
extra.clone(),
|
||||||
|
|
|
@ -6,7 +6,11 @@ use sp_blockchain::{Error as BlockchainError, HeaderBackend, HeaderMetadata};
|
||||||
use sp_block_builder::BlockBuilder;
|
use sp_block_builder::BlockBuilder;
|
||||||
use sp_api::ProvideRuntimeApi;
|
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;
|
pub use sc_rpc_api::DenyUnsafe;
|
||||||
use sc_transaction_pool_api::TransactionPool;
|
use sc_transaction_pool_api::TransactionPool;
|
||||||
|
@ -29,8 +33,8 @@ pub fn create_full<
|
||||||
deps: FullDeps<C, P>,
|
deps: FullDeps<C, P>,
|
||||||
) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>>
|
) -> Result<RpcModule<()>, Box<dyn std::error::Error + Send + Sync>>
|
||||||
where
|
where
|
||||||
C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, SeraiAddress, Index>
|
C::Api: substrate_frame_rpc_system::AccountNonceApi<Block, PublicKey, Index>
|
||||||
+ pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, Balance>
|
+ pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi<Block, SubstrateAmount>
|
||||||
+ BlockBuilder<Block>,
|
+ BlockBuilder<Block>,
|
||||||
{
|
{
|
||||||
use substrate_frame_rpc_system::{System, SystemApiServer};
|
use substrate_frame_rpc_system::{System, SystemApiServer};
|
||||||
|
|
|
@ -76,9 +76,9 @@ impl TendermintClientMinimal for TendermintValidatorFirm {
|
||||||
// guaranteed not to grow the block?
|
// guaranteed not to grow the block?
|
||||||
const PROPOSED_BLOCK_SIZE_LIMIT: usize = { BLOCK_SIZE as usize };
|
const PROPOSED_BLOCK_SIZE_LIMIT: usize = { BLOCK_SIZE as usize };
|
||||||
// 3 seconds
|
// 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
|
// 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 Block = Block;
|
||||||
type Backend = sc_client_db::Backend<Block>;
|
type Backend = sc_client_db::Backend<Block>;
|
||||||
|
@ -101,7 +101,7 @@ impl TendermintValidator for TendermintValidatorFirm {
|
||||||
pub fn new_partial(
|
pub fn new_partial(
|
||||||
config: &Configuration,
|
config: &Configuration,
|
||||||
) -> Result<(TendermintImport<TendermintValidatorFirm>, PartialComponents), ServiceError> {
|
) -> 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() {
|
if config.keystore_remote.is_some() {
|
||||||
return Err(ServiceError::Other("Remote Keystores are not supported".to_string()));
|
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-assets = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||||
pallet-transaction-payment = { 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 }
|
in-instructions-pallet = { path = "../in-instructions/pallet", default-features = false }
|
||||||
|
|
||||||
validator-sets-pallet = { path = "../validator-sets/pallet", default-features = false }
|
validator-sets-pallet = { path = "../validator-sets/pallet", default-features = false }
|
||||||
|
@ -78,9 +79,10 @@ std = [
|
||||||
"serai-primitives/std",
|
"serai-primitives/std",
|
||||||
|
|
||||||
"pallet-balances/std",
|
"pallet-balances/std",
|
||||||
"pallet-assets/std",
|
|
||||||
"pallet-transaction-payment/std",
|
"pallet-transaction-payment/std",
|
||||||
|
|
||||||
|
"pallet-assets/std",
|
||||||
|
"tokens-pallet/std",
|
||||||
"in-instructions-pallet/std",
|
"in-instructions-pallet/std",
|
||||||
|
|
||||||
"validator-sets-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_transaction_payment as transaction_payment;
|
||||||
|
|
||||||
pub use pallet_assets as assets;
|
pub use pallet_assets as assets;
|
||||||
|
pub use tokens_pallet as tokens;
|
||||||
pub use in_instructions_pallet as in_instructions;
|
pub use in_instructions_pallet as in_instructions;
|
||||||
|
|
||||||
pub use validator_sets_pallet as validator_sets;
|
pub use validator_sets_pallet as validator_sets;
|
||||||
|
@ -33,15 +34,15 @@ use sp_version::NativeVersion;
|
||||||
|
|
||||||
use sp_runtime::{
|
use sp_runtime::{
|
||||||
create_runtime_str, generic, impl_opaque_keys, KeyTypeId,
|
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},
|
transaction_validity::{TransactionSource, TransactionValidity},
|
||||||
ApplyExtrinsicResult, Perbill,
|
ApplyExtrinsicResult, Perbill,
|
||||||
};
|
};
|
||||||
|
|
||||||
use primitives::{PublicKey, Signature, SeraiAddress, Coin};
|
use primitives::{PublicKey, SeraiAddress, AccountLookup, Signature, SubstrateAmount, Coin};
|
||||||
|
|
||||||
use support::{
|
use support::{
|
||||||
traits::{ConstU8, ConstU32, ConstU64},
|
traits::{ConstU8, ConstU32, ConstU64, Contains},
|
||||||
weights::{
|
weights::{
|
||||||
constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND},
|
constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND},
|
||||||
IdentityFee, Weight,
|
IdentityFee, Weight,
|
||||||
|
@ -56,14 +57,6 @@ use session::PeriodicSessions;
|
||||||
/// An index to a block.
|
/// An index to a block.
|
||||||
pub type BlockNumber = u32;
|
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.
|
/// Index of a transaction in the chain, for a given account.
|
||||||
pub type Index = u32;
|
pub type Index = u32;
|
||||||
|
|
||||||
|
@ -104,10 +97,10 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
|
||||||
// 1 MB
|
// 1 MB
|
||||||
pub const BLOCK_SIZE: u32 = 1024 * 1024;
|
pub const BLOCK_SIZE: u32 = 1024 * 1024;
|
||||||
// 6 seconds
|
// 6 seconds
|
||||||
pub const TARGET_BLOCK_TIME: u64 = 6000;
|
pub const TARGET_BLOCK_TIME: u64 = 6;
|
||||||
|
|
||||||
/// Measured in blocks.
|
/// 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 HOURS: BlockNumber = MINUTES * 60;
|
||||||
pub const DAYS: BlockNumber = HOURS * 24;
|
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 {
|
impl system::Config for Runtime {
|
||||||
type BaseCallFilter = support::traits::Everything;
|
type BaseCallFilter = CallFilter;
|
||||||
type BlockWeights = BlockWeights;
|
type BlockWeights = BlockWeights;
|
||||||
type BlockLength = BlockLength;
|
type BlockLength = BlockLength;
|
||||||
type AccountId = SeraiAddress;
|
type AccountId = PublicKey;
|
||||||
type RuntimeCall = RuntimeCall;
|
type RuntimeCall = RuntimeCall;
|
||||||
type Lookup = IdentityLookup<SeraiAddress>;
|
type Lookup = AccountLookup;
|
||||||
type Index = Index;
|
type Index = Index;
|
||||||
type BlockNumber = BlockNumber;
|
type BlockNumber = BlockNumber;
|
||||||
type Hash = Hash;
|
type Hash = Hash;
|
||||||
|
@ -157,7 +181,7 @@ impl system::Config for Runtime {
|
||||||
type OnKilledAccount = ();
|
type OnKilledAccount = ();
|
||||||
type OnSetCode = ();
|
type OnSetCode = ();
|
||||||
|
|
||||||
type AccountData = balances::AccountData<Balance>;
|
type AccountData = balances::AccountData<SubstrateAmount>;
|
||||||
type SystemWeightInfo = ();
|
type SystemWeightInfo = ();
|
||||||
type SS58Prefix = SS58Prefix; // TODO: Remove for Bech32m
|
type SS58Prefix = SS58Prefix; // TODO: Remove for Bech32m
|
||||||
|
|
||||||
|
@ -168,7 +192,7 @@ impl balances::Config for Runtime {
|
||||||
type MaxLocks = ConstU32<50>;
|
type MaxLocks = ConstU32<50>;
|
||||||
type MaxReserves = ();
|
type MaxReserves = ();
|
||||||
type ReserveIdentifier = [u8; 8];
|
type ReserveIdentifier = [u8; 8];
|
||||||
type Balance = Balance;
|
type Balance = SubstrateAmount;
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
type DustRemoval = ();
|
type DustRemoval = ();
|
||||||
type ExistentialDeposit = ConstU64<500>;
|
type ExistentialDeposit = ConstU64<500>;
|
||||||
|
@ -176,9 +200,18 @@ impl balances::Config for Runtime {
|
||||||
type WeightInfo = balances::weights::SubstrateWeight<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 {
|
impl assets::Config for Runtime {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
type Balance = Balance;
|
type Balance = SubstrateAmount;
|
||||||
type Currency = Balances;
|
type Currency = Balances;
|
||||||
|
|
||||||
type AssetId = Coin;
|
type AssetId = Coin;
|
||||||
|
@ -186,8 +219,8 @@ impl assets::Config for Runtime {
|
||||||
type StringLimit = ConstU32<32>;
|
type StringLimit = ConstU32<32>;
|
||||||
|
|
||||||
// Don't allow anyone to create assets
|
// Don't allow anyone to create assets
|
||||||
type CreateOrigin = support::traits::AsEnsureOriginWithArg<system::EnsureNever<SeraiAddress>>;
|
type CreateOrigin = support::traits::AsEnsureOriginWithArg<system::EnsureNever<PublicKey>>;
|
||||||
type ForceOrigin = system::EnsureRoot<SeraiAddress>;
|
type ForceOrigin = system::EnsureRoot<PublicKey>;
|
||||||
|
|
||||||
// Don't charge fees nor kill accounts
|
// Don't charge fees nor kill accounts
|
||||||
type RemoveItemsLimit = ConstU32<0>;
|
type RemoveItemsLimit = ConstU32<0>;
|
||||||
|
@ -207,13 +240,8 @@ impl assets::Config for Runtime {
|
||||||
type BenchmarkHelper = ();
|
type BenchmarkHelper = ();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl transaction_payment::Config for Runtime {
|
impl tokens::Config for Runtime {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
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 {
|
impl in_instructions::Config for Runtime {
|
||||||
|
@ -236,7 +264,7 @@ impl validator_sets::Config for Runtime {
|
||||||
|
|
||||||
impl session::Config for Runtime {
|
impl session::Config for Runtime {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
type ValidatorId = SeraiAddress;
|
type ValidatorId = PublicKey;
|
||||||
type ValidatorIdOf = IdentityValidatorIdOf;
|
type ValidatorIdOf = IdentityValidatorIdOf;
|
||||||
type ShouldEndSession = Sessions;
|
type ShouldEndSession = Sessions;
|
||||||
type NextSessionRotation = Sessions;
|
type NextSessionRotation = Sessions;
|
||||||
|
@ -283,6 +311,7 @@ construct_runtime!(
|
||||||
TransactionPayment: transaction_payment,
|
TransactionPayment: transaction_payment,
|
||||||
|
|
||||||
Assets: assets,
|
Assets: assets,
|
||||||
|
Tokens: tokens,
|
||||||
InInstructions: in_instructions,
|
InInstructions: in_instructions,
|
||||||
|
|
||||||
ValidatorSets: validator_sets,
|
ValidatorSets: validator_sets,
|
||||||
|
@ -381,31 +410,31 @@ sp_api::impl_runtime_apis! {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validators() -> Vec<PublicKey> {
|
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 {
|
impl frame_system_rpc_runtime_api::AccountNonceApi<Block, PublicKey, Index> for Runtime {
|
||||||
fn account_nonce(account: SeraiAddress) -> Index {
|
fn account_nonce(account: PublicKey) -> Index {
|
||||||
System::account_nonce(account)
|
System::account_nonce(account)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi<
|
impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi<
|
||||||
Block,
|
Block,
|
||||||
Balance
|
SubstrateAmount
|
||||||
> for Runtime {
|
> for Runtime {
|
||||||
fn query_info(
|
fn query_info(
|
||||||
uxt: <Block as BlockT>::Extrinsic,
|
uxt: <Block as BlockT>::Extrinsic,
|
||||||
len: u32,
|
len: u32,
|
||||||
) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo<Balance> {
|
) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo<SubstrateAmount> {
|
||||||
TransactionPayment::query_info(uxt, len)
|
TransactionPayment::query_info(uxt, len)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_fee_details(
|
fn query_fee_details(
|
||||||
uxt: <Block as BlockT>::Extrinsic,
|
uxt: <Block as BlockT>::Extrinsic,
|
||||||
len: u32,
|
len: u32,
|
||||||
) -> transaction_payment::FeeDetails<Balance> {
|
) -> transaction_payment::FeeDetails<SubstrateAmount> {
|
||||||
TransactionPayment::query_fee_details(uxt, len)
|
TransactionPayment::query_fee_details(uxt, len)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,19 +15,22 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
|
|
||||||
scale = { package = "parity-scale-codec", version = "3" }
|
scale = { package = "parity-scale-codec", version = "3" }
|
||||||
|
scale-info = "2"
|
||||||
scale-value = "0.6"
|
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" }
|
serai-primitives = { path = "../primitives", version = "0.1" }
|
||||||
in-instructions-primitives = { path = "../../in-instructions/primitives", version = "0.1" }
|
|
||||||
serai-runtime = { path = "../../runtime", version = "0.1" }
|
serai-runtime = { path = "../../runtime", version = "0.1" }
|
||||||
|
|
||||||
|
subxt = "0.25"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
|
|
||||||
|
rand_core = "0.6"
|
||||||
|
|
||||||
tokio = "1"
|
tokio = "1"
|
||||||
|
|
||||||
jsonrpsee-server = "0.16"
|
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::{in_instructions, InInstructions, Runtime};
|
||||||
|
pub use in_instructions::primitives;
|
||||||
use serai_runtime::{
|
|
||||||
support::traits::PalletInfo as PalletInfoTrait, PalletInfo, in_instructions, InInstructions,
|
|
||||||
Runtime,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use in_instructions_primitives as primitives;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
primitives::{Coin, BlockNumber},
|
primitives::{Coin, BlockNumber},
|
||||||
Serai, SeraiError,
|
Serai, SeraiError, scale_value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PALLET: &str = "InInstructions";
|
const PALLET: &str = "InInstructions";
|
||||||
|
@ -21,22 +15,11 @@ impl Serai {
|
||||||
&self,
|
&self,
|
||||||
block: [u8; 32],
|
block: [u8; 32],
|
||||||
) -> Result<Vec<InInstructionsEvent>, SeraiError> {
|
) -> Result<Vec<InInstructionsEvent>, SeraiError> {
|
||||||
let mut res = vec![];
|
self
|
||||||
for event in
|
.events::<InInstructions, _>(block, |event| {
|
||||||
self.0.events().at(Some(block.into())).await.map_err(|_| SeraiError::RpcError)?.iter()
|
matches!(event, InInstructionsEvent::Batch { .. })
|
||||||
{
|
})
|
||||||
let event = event.map_err(|_| SeraiError::InvalidRuntime)?;
|
.await
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_coin_block_number(
|
pub async fn get_coin_block_number(
|
||||||
|
@ -44,6 +27,11 @@ impl Serai {
|
||||||
coin: Coin,
|
coin: Coin,
|
||||||
block: [u8; 32],
|
block: [u8; 32],
|
||||||
) -> Result<BlockNumber, SeraiError> {
|
) -> 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 thiserror::Error;
|
||||||
|
|
||||||
use serde::Serialize;
|
use scale::{Encode, Decode};
|
||||||
use scale::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;
|
pub use serai_primitives as primitives;
|
||||||
use primitives::{Signature, SeraiAddress};
|
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;
|
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)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub(crate) struct SeraiConfig;
|
pub struct SeraiConfig;
|
||||||
impl SubxtConfig for SeraiConfig {
|
impl SubxtConfig for SeraiConfig {
|
||||||
type BlockNumber = <Runtime as Config>::BlockNumber;
|
type BlockNumber = <Runtime as Config>::BlockNumber;
|
||||||
|
|
||||||
|
@ -28,7 +45,7 @@ impl SubxtConfig for SeraiConfig {
|
||||||
type Header = <Runtime as Config>::Header;
|
type Header = <Runtime as Config>::Header;
|
||||||
type Signature = Signature;
|
type Signature = Signature;
|
||||||
|
|
||||||
type ExtrinsicParams = BaseExtrinsicParams<SeraiConfig, ()>;
|
type ExtrinsicParams = BaseExtrinsicParams<SeraiConfig, Tip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Error, Debug)]
|
#[derive(Clone, Error, Debug)]
|
||||||
|
@ -47,21 +64,16 @@ impl Serai {
|
||||||
Ok(Serai(OnlineClient::<SeraiConfig>::from_url(url).await.map_err(|_| SeraiError::RpcError)?))
|
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,
|
&self,
|
||||||
pallet: &'static str,
|
pallet: &'static str,
|
||||||
name: &'static str,
|
name: &'static str,
|
||||||
key: Option<K>,
|
keys: Option<Vec<Value>>,
|
||||||
block: [u8; 32],
|
block: [u8; 32],
|
||||||
) -> Result<Option<R>, SeraiError> {
|
) -> 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 storage = self.0.storage();
|
||||||
let address = subxt::dynamic::storage(pallet, name, keys);
|
let address = subxt::dynamic::storage(pallet, name, keys.unwrap_or(vec![]));
|
||||||
debug_assert!(storage.validate(&address).is_ok());
|
debug_assert!(storage.validate(&address).is_ok(), "invalid storage address");
|
||||||
|
|
||||||
storage
|
storage
|
||||||
.fetch(&address, Some(block.into()))
|
.fetch(&address, Some(block.into()))
|
||||||
|
@ -71,7 +83,46 @@ impl Serai {
|
||||||
.transpose()
|
.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> {
|
pub async fn get_latest_block_hash(&self) -> Result<[u8; 32], SeraiError> {
|
||||||
Ok(self.0.rpc().finalized_head().await.map_err(|_| SeraiError::RpcError)?.into())
|
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 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";
|
pub const URL: &str = "ws://127.0.0.1:9944";
|
||||||
|
|
||||||
|
@ -8,6 +16,73 @@ lazy_static! {
|
||||||
pub static ref SEQUENTIAL: Mutex<()> = Mutex::new(());
|
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_export]
|
||||||
macro_rules! serai_test {
|
macro_rules! serai_test {
|
||||||
($(async fn $name: ident() $body: block)*) => {
|
($(async fn $name: ident() $body: block)*) => {
|
||||||
|
|
|
@ -1,60 +1,45 @@
|
||||||
use core::time::Duration;
|
use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
use serai_runtime::in_instructions::{Batch, Update};
|
use serai_runtime::in_instructions::{Batch, Update};
|
||||||
|
|
||||||
use jsonrpsee_server::RpcModule;
|
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::{BlockNumber, BlockHash, SeraiAddress, BITCOIN},
|
primitives::{BITCOIN, BlockNumber, BlockHash, SeraiAddress, Amount, WithAmount, Balance},
|
||||||
|
tokens::TokensEvent,
|
||||||
in_instructions::{primitives::InInstruction, InInstructionsEvent},
|
in_instructions::{primitives::InInstruction, InInstructionsEvent},
|
||||||
Serai,
|
Serai,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod runner;
|
mod runner;
|
||||||
use runner::URL;
|
use runner::{URL, provide_updates};
|
||||||
|
|
||||||
serai_test!(
|
serai_test!(
|
||||||
async fn publish_update() {
|
async fn publish_updates() {
|
||||||
let mut rpc = RpcModule::new(());
|
let coin = BITCOIN;
|
||||||
rpc
|
let mut id = BlockHash([0; 32]);
|
||||||
.register_async_method("processor_coinUpdates", |_, _| async move {
|
OsRng.fill_bytes(&mut id.0);
|
||||||
|
let block_number = BlockNumber(u32::try_from(OsRng.next_u64() >> 32).unwrap());
|
||||||
|
|
||||||
|
let mut address = SeraiAddress::new([0; 32]);
|
||||||
|
OsRng.fill_bytes(&mut address.0);
|
||||||
|
let amount = Amount(OsRng.next_u64());
|
||||||
|
|
||||||
let batch = Batch {
|
let batch = Batch {
|
||||||
id: BlockHash([0xaa; 32]),
|
id,
|
||||||
instructions: vec![InInstruction::Transfer(SeraiAddress::from_raw([0xff; 32]))],
|
instructions: vec![WithAmount { data: InInstruction::Transfer(address), amount }],
|
||||||
};
|
};
|
||||||
|
let update = Update { block_number, batches: vec![batch] };
|
||||||
Ok(vec![Some(Update { block_number: BlockNumber(123), batches: vec![batch] })])
|
let block = provide_updates(vec![Some(update)]).await;
|
||||||
})
|
|
||||||
.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();
|
let serai = Serai::new(URL).await.unwrap();
|
||||||
loop {
|
let batches = serai.get_batch_events(block).await.unwrap();
|
||||||
let latest = serai.get_latest_block_hash().await.unwrap();
|
assert_eq!(batches, vec![InInstructionsEvent::Batch { coin, id }]);
|
||||||
let batches = serai.get_batch_events(latest).await.unwrap();
|
assert_eq!(serai.get_coin_block_number(coin, block).await.unwrap(), block_number);
|
||||||
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!(
|
assert_eq!(
|
||||||
serai.get_coin_block_number(BITCOIN, latest).await.unwrap(),
|
serai.get_mint_events(block).await.unwrap(),
|
||||||
BlockNumber(123)
|
vec![TokensEvent::Mint { address, balance: Balance { coin, amount } }]
|
||||||
);
|
);
|
||||||
return;
|
assert_eq!(serai.get_token_supply(block, coin).await.unwrap(), amount);
|
||||||
}
|
assert_eq!(serai.get_token_balance(block, coin, address).await.unwrap(), amount);
|
||||||
_ => panic!("get_batches returned non-batch"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sleep(Duration::from_millis(50)).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,7 +18,8 @@ scale-info = { version = "2", default-features = false, features = ["derive"] }
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|
||||||
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
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]
|
[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"]
|
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::{Encode, Decode, MaxEncodedLen};
|
||||||
use scale_info::TypeInfo;
|
use scale_info::TypeInfo;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use serde::{Serialize, Deserialize};
|
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.
|
/// The type used for amounts.
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone, Copy, PartialEq, Eq, PartialOrd, Debug, Encode, Decode, MaxEncodedLen, TypeInfo,
|
Clone, Copy, PartialEq, Eq, PartialOrd, Debug, Encode, Decode, MaxEncodedLen, TypeInfo,
|
||||||
)]
|
)]
|
||||||
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||||
pub struct Amount(pub u64);
|
pub struct Amount(pub SubstrateAmount);
|
||||||
|
|
||||||
/// One whole coin with eight decimals.
|
|
||||||
#[allow(clippy::inconsistent_digit_grouping)]
|
|
||||||
pub const COIN: Amount = Amount(1_000_000_00);
|
|
||||||
|
|
||||||
impl Add for Amount {
|
impl Add for Amount {
|
||||||
type Output = Amount;
|
type Output = Amount;
|
||||||
|
@ -37,3 +42,12 @@ impl Mul for Amount {
|
||||||
Amount(self.0.checked_mul(other.0).unwrap())
|
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")]
|
#[cfg(feature = "std")]
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use sp_core::{
|
use sp_core::{ConstU32, bounded::BoundedVec};
|
||||||
H256,
|
|
||||||
sr25519::{Public, Signature as RistrettoSignature},
|
|
||||||
};
|
|
||||||
|
|
||||||
mod amount;
|
mod amount;
|
||||||
pub use amount::*;
|
pub use amount::*;
|
||||||
|
|
||||||
|
mod block;
|
||||||
|
pub use block::*;
|
||||||
|
|
||||||
mod coins;
|
mod coins;
|
||||||
pub use coins::*;
|
pub use coins::*;
|
||||||
|
|
||||||
pub type PublicKey = Public;
|
mod balance;
|
||||||
pub type SeraiAddress = PublicKey;
|
pub use balance::*;
|
||||||
pub type Signature = RistrettoSignature;
|
|
||||||
|
|
||||||
/// The type used to identify block numbers.
|
mod account;
|
||||||
// Doesn't re-export tendermint-machine's due to traits.
|
pub use account::*;
|
||||||
#[derive(
|
|
||||||
Clone, Copy, Default, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo,
|
// 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))]
|
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
|
||||||
pub struct BlockNumber(pub u32);
|
pub struct ExternalAddress(BoundedVec<u8, ConstU32<{ MAX_ADDRESS_LEN }>>);
|
||||||
impl From<u32> for BlockNumber {
|
impl ExternalAddress {
|
||||||
fn from(number: u32) -> BlockNumber {
|
#[cfg(feature = "std")]
|
||||||
BlockNumber(number)
|
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.
|
impl AsRef<[u8]> for ExternalAddress {
|
||||||
// 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] {
|
fn as_ref(&self) -> &[u8] {
|
||||||
self.0.as_ref()
|
self.0.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<[u8; 32]> for BlockHash {
|
// Should be enough for a Uniswap v3 call
|
||||||
fn from(hash: [u8; 32]) -> BlockHash {
|
pub const MAX_DATA_LEN: u32 = 512;
|
||||||
BlockHash(hash)
|
#[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 {
|
impl AsRef<[u8]> for Data {
|
||||||
fn from(hash: H256) -> BlockHash {
|
fn as_ref(&self) -> &[u8] {
|
||||||
BlockHash(hash.into())
|
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