mirror of
https://github.com/serai-dex/serai.git
synced 2025-01-23 11:15:16 +00:00
Coins pallet (#399)
* initial implementation * add function to get a balance of an account * add support for multiple coins * rename pallet to "coins-pallet" * replace balances, assets and tokens pallet with coins pallet in runtime * add total supply info * update client side for new Coins pallet * handle fees * bug fixes * Update FeeAccount test * Fmt * fix pr comments * remove extraneous Imbalance type * Minor tweaks --------- Co-authored-by: Luke Parker <lukeparker5132@gmail.com>
This commit is contained in:
parent
3255c0ace5
commit
fdfce9e207
32 changed files with 535 additions and 445 deletions
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
|
@ -60,8 +60,8 @@ jobs:
|
|||
run: |
|
||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
||||
-p serai-primitives \
|
||||
-p serai-tokens-primitives \
|
||||
-p serai-tokens-pallet \
|
||||
-p serai-coins-primitives \
|
||||
-p serai-coins-pallet \
|
||||
-p serai-in-instructions-primitives \
|
||||
-p serai-in-instructions-pallet \
|
||||
-p serai-validator-sets-primitives \
|
||||
|
|
95
Cargo.lock
generated
95
Cargo.lock
generated
|
@ -5488,21 +5488,6 @@ dependencies = [
|
|||
"group",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-assets"
|
||||
version = "4.0.0-dev"
|
||||
source = "git+https://github.com/serai-dex/substrate#98ab693fdf71f371d5059aa6924a410c8bb0a675"
|
||||
dependencies = [
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"sp-core",
|
||||
"sp-runtime",
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-authority-discovery"
|
||||
version = "4.0.0-dev"
|
||||
|
@ -5557,21 +5542,6 @@ dependencies = [
|
|||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-balances"
|
||||
version = "4.0.0-dev"
|
||||
source = "git+https://github.com/serai-dex/substrate#98ab693fdf71f371d5059aa6924a410c8bb0a675"
|
||||
dependencies = [
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"log",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"sp-runtime",
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pallet-grandpa"
|
||||
version = "4.0.0-dev"
|
||||
|
@ -8221,6 +8191,34 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serai-coins-pallet"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"pallet-transaction-payment",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"serai-coins-primitives",
|
||||
"serai-primitives",
|
||||
"sp-core",
|
||||
"sp-runtime",
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serai-coins-primitives"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"serai-primitives",
|
||||
"serde",
|
||||
"sp-runtime",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serai-coordinator"
|
||||
version = "0.1.0"
|
||||
|
@ -8325,9 +8323,9 @@ dependencies = [
|
|||
"frame-system",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"serai-coins-pallet",
|
||||
"serai-in-instructions-primitives",
|
||||
"serai-primitives",
|
||||
"serai-tokens-pallet",
|
||||
"serai-validator-sets-pallet",
|
||||
"sp-application-crypto",
|
||||
"sp-core",
|
||||
|
@ -8342,8 +8340,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"serai-coins-primitives",
|
||||
"serai-primitives",
|
||||
"serai-tokens-primitives",
|
||||
"serde",
|
||||
"sp-application-crypto",
|
||||
"sp-runtime",
|
||||
|
@ -8503,9 +8501,9 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"dkg",
|
||||
"parity-scale-codec",
|
||||
"serai-coins-primitives",
|
||||
"serai-in-instructions-primitives",
|
||||
"serai-primitives",
|
||||
"serai-tokens-primitives",
|
||||
"serai-validator-sets-primitives",
|
||||
"serde",
|
||||
"zeroize",
|
||||
|
@ -8556,10 +8554,8 @@ dependencies = [
|
|||
"frame-support",
|
||||
"frame-system",
|
||||
"frame-system-rpc-runtime-api",
|
||||
"pallet-assets",
|
||||
"pallet-authority-discovery",
|
||||
"pallet-babe",
|
||||
"pallet-balances",
|
||||
"pallet-grandpa",
|
||||
"pallet-session",
|
||||
"pallet-timestamp",
|
||||
|
@ -8567,10 +8563,10 @@ dependencies = [
|
|||
"pallet-transaction-payment-rpc-runtime-api",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"serai-coins-pallet",
|
||||
"serai-in-instructions-pallet",
|
||||
"serai-primitives",
|
||||
"serai-staking-pallet",
|
||||
"serai-tokens-pallet",
|
||||
"serai-validator-sets-pallet",
|
||||
"sp-api",
|
||||
"sp-authority-discovery",
|
||||
|
@ -8597,38 +8593,13 @@ dependencies = [
|
|||
"pallet-session",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"serai-coins-pallet",
|
||||
"serai-primitives",
|
||||
"serai-validator-sets-pallet",
|
||||
"serai-validator-sets-primitives",
|
||||
"sp-runtime",
|
||||
"sp-std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serai-tokens-pallet"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
"pallet-assets",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"serai-primitives",
|
||||
"serai-tokens-primitives",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serai-tokens-primitives"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"serai-primitives",
|
||||
"serde",
|
||||
"sp-runtime",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serai-validator-sets-pallet"
|
||||
version = "0.1.0"
|
||||
|
|
|
@ -37,8 +37,8 @@ members = [
|
|||
|
||||
"substrate/primitives",
|
||||
|
||||
"substrate/tokens/primitives",
|
||||
"substrate/tokens/pallet",
|
||||
"substrate/coins/primitives",
|
||||
"substrate/coins/pallet",
|
||||
|
||||
"substrate/in-instructions/primitives",
|
||||
"substrate/in-instructions/pallet",
|
||||
|
|
|
@ -16,7 +16,7 @@ use serai_client::{
|
|||
ValidatorSetsEvent,
|
||||
},
|
||||
in_instructions::InInstructionsEvent,
|
||||
coins::{primitives::OutInstructionWithBalance, TokensEvent},
|
||||
coins::CoinsEvent,
|
||||
};
|
||||
|
||||
use serai_db::DbTxn;
|
||||
|
@ -209,12 +209,12 @@ async fn handle_batch_and_burns<D: Db, Pro: Processors>(
|
|||
}
|
||||
|
||||
for burn in serai.coins().burn_events().await? {
|
||||
if let TokensEvent::Burn { address: _, balance, instruction } = burn {
|
||||
let network = balance.coin.network();
|
||||
if let CoinsEvent::Burn { address: _, instruction } = burn {
|
||||
let network = instruction.balance.coin.network();
|
||||
network_had_event(&mut burns, &mut batches, network);
|
||||
|
||||
// network_had_event should register an entry in burns
|
||||
burns.get_mut(&network).unwrap().push(OutInstructionWithBalance { balance, instruction });
|
||||
burns.get_mut(&network).unwrap().push(instruction);
|
||||
} else {
|
||||
panic!("Burn event wasn't Burn: {burn:?}");
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ exceptions = [
|
|||
{ allow = ["AGPL-3.0"], name = "tributary-chain" },
|
||||
{ allow = ["AGPL-3.0"], name = "serai-coordinator" },
|
||||
|
||||
{ allow = ["AGPL-3.0"], name = "serai-tokens-pallet" },
|
||||
{ allow = ["AGPL-3.0"], name = "serai-coins-pallet" },
|
||||
|
||||
{ allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" },
|
||||
|
||||
|
|
|
@ -23,5 +23,5 @@ dkg = { path = "../../crypto/dkg", features = ["serde"] }
|
|||
|
||||
serai-primitives = { path = "../../substrate/primitives" }
|
||||
in-instructions-primitives = { package = "serai-in-instructions-primitives", path = "../../substrate/in-instructions/primitives" }
|
||||
tokens-primitives = { package = "serai-tokens-primitives", path = "../../substrate/tokens/primitives" }
|
||||
coins-primitives = { package = "serai-coins-primitives", path = "../../substrate/coins/primitives" }
|
||||
validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../../substrate/validator-sets/primitives" }
|
||||
|
|
|
@ -9,7 +9,7 @@ use dkg::{Participant, ThresholdParams};
|
|||
|
||||
use serai_primitives::{BlockHash, NetworkId};
|
||||
use in_instructions_primitives::{Batch, SignedBatch};
|
||||
use tokens_primitives::OutInstructionWithBalance;
|
||||
use coins_primitives::OutInstructionWithBalance;
|
||||
use validator_sets_primitives::{ValidatorSet, KeyPair};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, Encode, Decode, Serialize, Deserialize)]
|
||||
|
|
|
@ -15,7 +15,7 @@ mod other_primitives {
|
|||
pub use serai_runtime::in_instructions::primitives;
|
||||
}
|
||||
pub mod coins {
|
||||
pub use serai_runtime::tokens::primitives;
|
||||
pub use serai_runtime::coins::primitives;
|
||||
}
|
||||
pub mod validator_sets {
|
||||
pub use serai_runtime::validator_sets::primitives;
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
use sp_core::sr25519::Public;
|
||||
use serai_runtime::{
|
||||
primitives::{SeraiAddress, SubstrateAmount, Amount, Coin, Balance},
|
||||
assets::{AssetDetails, AssetAccount},
|
||||
tokens, Tokens, Runtime,
|
||||
coins, Coins, Runtime,
|
||||
};
|
||||
pub use tokens::primitives;
|
||||
use primitives::OutInstruction;
|
||||
pub use coins::primitives;
|
||||
use primitives::OutInstructionWithBalance;
|
||||
|
||||
use subxt::tx::Payload;
|
||||
|
||||
use crate::{TemporalSerai, SeraiError, Composite, scale_value, scale_composite};
|
||||
|
||||
const PALLET: &str = "Tokens";
|
||||
const PALLET: &str = "Coins";
|
||||
|
||||
pub type TokensEvent = tokens::Event<Runtime>;
|
||||
pub type CoinsEvent = coins::Event<Runtime>;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SeraiCoins<'a>(pub(crate) TemporalSerai<'a>);
|
||||
|
@ -22,37 +20,25 @@ impl<'a> SeraiCoins<'a> {
|
|||
self.0
|
||||
}
|
||||
|
||||
pub async fn mint_events(&self) -> Result<Vec<TokensEvent>, SeraiError> {
|
||||
self.0.events::<Tokens, _>(|event| matches!(event, TokensEvent::Mint { .. })).await
|
||||
pub async fn mint_events(&self) -> Result<Vec<CoinsEvent>, SeraiError> {
|
||||
self.0.events::<Coins, _>(|event| matches!(event, CoinsEvent::Mint { .. })).await
|
||||
}
|
||||
|
||||
pub async fn burn_events(&self) -> Result<Vec<TokensEvent>, SeraiError> {
|
||||
self.0.events::<Tokens, _>(|event| matches!(event, TokensEvent::Burn { .. })).await
|
||||
pub async fn burn_events(&self) -> Result<Vec<CoinsEvent>, SeraiError> {
|
||||
self.0.events::<Coins, _>(|event| matches!(event, CoinsEvent::Burn { .. })).await
|
||||
}
|
||||
|
||||
pub async fn sri_balance(&self, address: SeraiAddress) -> Result<u64, SeraiError> {
|
||||
let data: Option<
|
||||
serai_runtime::system::AccountInfo<u32, serai_runtime::balances::AccountData<u64>>,
|
||||
> = self.0.storage("System", "Account", Some(vec![scale_value(address)])).await?;
|
||||
Ok(data.map(|data| data.data.free).unwrap_or(0))
|
||||
}
|
||||
|
||||
pub async fn token_supply(&self, coin: Coin) -> Result<Amount, SeraiError> {
|
||||
pub async fn coin_supply(&self, coin: Coin) -> Result<Amount, SeraiError> {
|
||||
Ok(Amount(
|
||||
self
|
||||
.0
|
||||
.storage::<AssetDetails<SubstrateAmount, SeraiAddress, SubstrateAmount>>(
|
||||
"Assets",
|
||||
"Asset",
|
||||
Some(vec![scale_value(coin)]),
|
||||
)
|
||||
.storage::<SubstrateAmount>(PALLET, "Supply", Some(vec![scale_value(coin)]))
|
||||
.await?
|
||||
.map(|token| token.supply)
|
||||
.unwrap_or(0),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn token_balance(
|
||||
pub async fn coin_balance(
|
||||
&self,
|
||||
coin: Coin,
|
||||
address: SeraiAddress,
|
||||
|
@ -60,35 +46,25 @@ impl<'a> SeraiCoins<'a> {
|
|||
Ok(Amount(
|
||||
self
|
||||
.0
|
||||
.storage::<AssetAccount<SubstrateAmount, SubstrateAmount, (), Public>>(
|
||||
"Assets",
|
||||
"Account",
|
||||
Some(vec![scale_value(coin), scale_value(address)]),
|
||||
.storage::<SubstrateAmount>(
|
||||
PALLET,
|
||||
"Balances",
|
||||
Some(vec![scale_value(address), scale_value(coin)]),
|
||||
)
|
||||
.await?
|
||||
.map(|account| account.balance())
|
||||
.unwrap_or(0),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn transfer_sri(to: SeraiAddress, amount: Amount) -> Payload<Composite<()>> {
|
||||
pub fn transfer(to: SeraiAddress, balance: Balance) -> Payload<Composite<()>> {
|
||||
Payload::new(
|
||||
"Balances",
|
||||
// TODO: Use transfer_allow_death?
|
||||
// TODO: Replace the Balances pallet with something much simpler
|
||||
PALLET,
|
||||
"transfer",
|
||||
scale_composite(serai_runtime::balances::Call::<Runtime>::transfer {
|
||||
dest: to,
|
||||
value: amount.0,
|
||||
}),
|
||||
scale_composite(serai_runtime::coins::Call::<Runtime>::transfer { to, balance }),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn burn(balance: Balance, instruction: OutInstruction) -> Payload<Composite<()>> {
|
||||
Payload::new(
|
||||
PALLET,
|
||||
"burn",
|
||||
scale_composite(tokens::Call::<Runtime>::burn { balance, instruction }),
|
||||
)
|
||||
pub fn burn(instruction: OutInstructionWithBalance) -> Payload<Composite<()>> {
|
||||
Payload::new(PALLET, "burn", scale_composite(coins::Call::<Runtime>::burn { instruction }))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ use serai_client::{
|
|||
primitives::{InInstruction, InInstructionWithBalance, Batch},
|
||||
InInstructionsEvent,
|
||||
},
|
||||
coins::TokensEvent,
|
||||
coins::CoinsEvent,
|
||||
Serai,
|
||||
};
|
||||
|
||||
|
@ -65,8 +65,8 @@ serai_test!(
|
|||
}
|
||||
|
||||
let serai = serai.coins();
|
||||
assert_eq!(serai.mint_events().await.unwrap(), vec![TokensEvent::Mint { address, balance }],);
|
||||
assert_eq!(serai.token_supply(coin).await.unwrap(), amount);
|
||||
assert_eq!(serai.token_balance(coin, address).await.unwrap(), amount);
|
||||
assert_eq!(serai.mint_events().await.unwrap(), vec![CoinsEvent::Mint { address, balance }],);
|
||||
assert_eq!(serai.coin_supply(coin).await.unwrap(), amount);
|
||||
assert_eq!(serai.coin_balance(coin, address).await.unwrap(), amount);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ use blake2::{
|
|||
|
||||
use scale::Encode;
|
||||
|
||||
use serai_runtime::coins::primitives::OutInstructionWithBalance;
|
||||
use sp_core::Pair;
|
||||
|
||||
use serai_client::{
|
||||
|
@ -19,7 +20,7 @@ use serai_client::{
|
|||
InInstructionsEvent,
|
||||
primitives::{InInstruction, InInstructionWithBalance, Batch},
|
||||
},
|
||||
coins::{primitives::OutInstruction, TokensEvent},
|
||||
coins::{primitives::OutInstruction, CoinsEvent},
|
||||
PairSigner, Serai, SeraiCoins,
|
||||
};
|
||||
|
||||
|
@ -69,10 +70,10 @@ serai_test!(
|
|||
|
||||
assert_eq!(
|
||||
serai.coins().mint_events().await.unwrap(),
|
||||
vec![TokensEvent::Mint { address, balance }]
|
||||
vec![CoinsEvent::Mint { address, balance }]
|
||||
);
|
||||
assert_eq!(serai.coins().token_supply(coin).await.unwrap(), amount);
|
||||
assert_eq!(serai.coins().token_balance(coin, address).await.unwrap(), amount);
|
||||
assert_eq!(serai.coins().coin_supply(coin).await.unwrap(), amount);
|
||||
assert_eq!(serai.coins().coin_balance(coin, address).await.unwrap(), amount);
|
||||
|
||||
// Now burn it
|
||||
let mut rand_bytes = vec![0; 32];
|
||||
|
@ -83,13 +84,16 @@ serai_test!(
|
|||
OsRng.fill_bytes(&mut rand_bytes);
|
||||
let data = Data::new(rand_bytes).unwrap();
|
||||
|
||||
let out = OutInstruction { address: external_address, data: Some(data) };
|
||||
let instruction = OutInstructionWithBalance {
|
||||
balance,
|
||||
instruction: OutInstruction { address: external_address, data: Some(data) },
|
||||
};
|
||||
let serai = serai.into_inner();
|
||||
let block = publish_tx(
|
||||
&serai
|
||||
.sign(
|
||||
&PairSigner::new(pair),
|
||||
&SeraiCoins::burn(balance, out.clone()),
|
||||
&SeraiCoins::burn(instruction.clone()),
|
||||
0,
|
||||
BaseExtrinsicParamsBuilder::new(),
|
||||
)
|
||||
|
@ -99,8 +103,8 @@ serai_test!(
|
|||
|
||||
let serai = serai.as_of(block).coins();
|
||||
let events = serai.burn_events().await.unwrap();
|
||||
assert_eq!(events, vec![TokensEvent::Burn { address, balance, instruction: out }]);
|
||||
assert_eq!(serai.token_supply(coin).await.unwrap(), Amount(0));
|
||||
assert_eq!(serai.token_balance(coin, address).await.unwrap(), Amount(0));
|
||||
assert_eq!(events, vec![CoinsEvent::Burn { address, instruction }]);
|
||||
assert_eq!(serai.coin_supply(coin).await.unwrap(), Amount(0));
|
||||
assert_eq!(serai.coin_balance(coin, address).await.unwrap(), Amount(0));
|
||||
}
|
||||
);
|
||||
|
|
49
substrate/coins/pallet/Cargo.toml
Normal file
49
substrate/coins/pallet/Cargo.toml
Normal file
|
@ -0,0 +1,49 @@
|
|||
[package]
|
||||
name = "serai-coins-pallet"
|
||||
version = "0.1.0"
|
||||
description = "Coins pallet for Serai"
|
||||
license = "AGPL-3.0-only"
|
||||
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/coins/pallet"
|
||||
authors = ["Akil Demir <aeg_asd@hotmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
parity-scale-codec = { version = "3", default-features = false, features = ["derive"] }
|
||||
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 }
|
||||
|
||||
sp-std = { 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 }
|
||||
|
||||
pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
|
||||
serai-primitives = { path = "../../primitives", default-features = false }
|
||||
coins-primitives = { package = "serai-coins-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
"frame-system/std",
|
||||
"frame-support/std",
|
||||
|
||||
"sp-std/std",
|
||||
"sp-runtime/std",
|
||||
|
||||
"pallet-transaction-payment/std",
|
||||
|
||||
"serai-primitives/std",
|
||||
"coins-primitives/std",
|
||||
]
|
||||
|
||||
runtime-benchmarks = [
|
||||
"frame-system/runtime-benchmarks",
|
||||
"frame-support/runtime-benchmarks",
|
||||
]
|
||||
|
||||
default = ["std"]
|
278
substrate/coins/pallet/src/lib.rs
Normal file
278
substrate/coins/pallet/src/lib.rs
Normal file
|
@ -0,0 +1,278 @@
|
|||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use sp_std::vec::Vec;
|
||||
use sp_core::sr25519::Public;
|
||||
use sp_runtime::{
|
||||
traits::{DispatchInfoOf, PostDispatchInfoOf},
|
||||
transaction_validity::{TransactionValidityError, InvalidTransaction},
|
||||
};
|
||||
|
||||
use frame_system::pallet_prelude::*;
|
||||
use frame_support::pallet_prelude::*;
|
||||
|
||||
use pallet_transaction_payment::{Config as TpConfig, OnChargeTransaction};
|
||||
|
||||
use serai_primitives::*;
|
||||
pub use coins_primitives as primitives;
|
||||
use primitives::*;
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config<AccountId = Public> + TpConfig {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
}
|
||||
|
||||
#[pallet::genesis_config]
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||
pub struct GenesisConfig<T: Config> {
|
||||
_config: PhantomData<T>,
|
||||
pub accounts: Vec<(Public, Balance)>,
|
||||
}
|
||||
|
||||
impl<T: Config> Default for GenesisConfig<T> {
|
||||
fn default() -> Self {
|
||||
GenesisConfig { _config: PhantomData, accounts: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
AmountOverflowed,
|
||||
NotEnoughCoins,
|
||||
SriBurnNotAllowed,
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
Mint { to: Public, balance: Balance },
|
||||
Burn { from: Public, instruction: OutInstructionWithBalance },
|
||||
SriBurn { from: Public, amount: Amount },
|
||||
Transfer { from: Public, to: Public, balance: Balance },
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(PhantomData<T>);
|
||||
|
||||
/// The amount of coins each account has.
|
||||
// Identity is used as the second key's hasher due to it being a non-manipulatable fixed-space
|
||||
// ID.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn balances)]
|
||||
pub type Balances<T: Config> = StorageDoubleMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
Public,
|
||||
Identity,
|
||||
Coin,
|
||||
SubstrateAmount,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
/// The total supply of each coin.
|
||||
// We use Identity type here again due to reasons stated in the Balances Storage.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn supply)]
|
||||
pub type Supply<T: Config> = StorageMap<_, Identity, Coin, SubstrateAmount, ValueQuery>;
|
||||
|
||||
#[pallet::genesis_build]
|
||||
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
||||
fn build(&self) {
|
||||
// initialize the supply of the coins
|
||||
for c in COINS.iter() {
|
||||
Supply::<T>::set(c, 0);
|
||||
}
|
||||
|
||||
// initialize the genesis accounts
|
||||
for (account, balance) in self.accounts.iter() {
|
||||
Pallet::<T>::mint(*account, *balance).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
fn on_initialize(_: BlockNumberFor<T>) -> Weight {
|
||||
// burn the fees collected previous block
|
||||
let coin = Coin::Serai;
|
||||
let amount = Self::balance(FEE_ACCOUNT.into(), coin);
|
||||
// we can unwrap, we are not burning more then what we have
|
||||
// If this errors, it'll halt the runtime however (due to being called at the start of every
|
||||
// block), requiring extra care when reviewing
|
||||
Self::burn_sri(FEE_ACCOUNT.into(), amount).unwrap();
|
||||
Weight::zero() // TODO
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Returns the balance of a given account for `coin`.
|
||||
pub fn balance(of: Public, coin: Coin) -> Amount {
|
||||
Amount(Self::balances(of, coin))
|
||||
}
|
||||
|
||||
fn decrease_balance_internal(from: Public, balance: Balance) -> Result<(), Error<T>> {
|
||||
let coin = &balance.coin;
|
||||
|
||||
// sub amount from account
|
||||
let new_amount = Self::balances(from, coin)
|
||||
.checked_sub(balance.amount.0)
|
||||
.ok_or(Error::<T>::NotEnoughCoins)?;
|
||||
|
||||
// save
|
||||
if new_amount == 0 {
|
||||
Balances::<T>::remove(from, coin);
|
||||
} else {
|
||||
Balances::<T>::set(from, coin, new_amount);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn increase_balance_internal(to: Public, balance: Balance) -> Result<(), Error<T>> {
|
||||
let coin = &balance.coin;
|
||||
|
||||
// sub amount from account
|
||||
let new_amount = Self::balances(to, coin)
|
||||
.checked_add(balance.amount.0)
|
||||
.ok_or(Error::<T>::AmountOverflowed)?;
|
||||
|
||||
// save
|
||||
Balances::<T>::set(to, coin, new_amount);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mint `balance` to the given account.
|
||||
///
|
||||
/// Errors if any amount overflows.
|
||||
pub fn mint(to: Public, balance: Balance) -> Result<(), Error<T>> {
|
||||
// update the balance
|
||||
Self::increase_balance_internal(to, balance)?;
|
||||
|
||||
// update the supply
|
||||
let new_supply = Self::supply(balance.coin)
|
||||
.checked_add(balance.amount.0)
|
||||
.ok_or(Error::<T>::AmountOverflowed)?;
|
||||
Supply::<T>::set(balance.coin, new_supply);
|
||||
|
||||
Self::deposit_event(Event::Mint { to, balance });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Burn `balance` from the specified account.
|
||||
fn burn_internal(
|
||||
from: Public,
|
||||
balance: Balance,
|
||||
) -> Result<(), Error<T>> {
|
||||
// don't waste time if amount == 0
|
||||
if balance.amount.0 == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// update the balance
|
||||
Self::decrease_balance_internal(from, balance)?;
|
||||
|
||||
// update the supply
|
||||
let new_supply = Self::supply(balance.coin)
|
||||
.checked_sub(balance.amount.0)
|
||||
.unwrap();
|
||||
Supply::<T>::set(balance.coin, new_supply);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn burn_sri(
|
||||
from: Public,
|
||||
amount: Amount,
|
||||
) -> Result<(), Error<T>> {
|
||||
Self::burn_internal(from, Balance { coin: Coin::Serai, amount })?;
|
||||
Self::deposit_event(Event::SriBurn { from, amount });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn burn_non_sri(
|
||||
from: Public,
|
||||
instruction: OutInstructionWithBalance,
|
||||
) -> Result<(), Error<T>> {
|
||||
if instruction.balance.coin == Coin::Serai {
|
||||
Err(Error::<T>::SriBurnNotAllowed)?;
|
||||
}
|
||||
Self::burn_internal(from, instruction.balance)?;
|
||||
Self::deposit_event(Event::Burn { from, instruction });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transfer `balance` from `from` to `to`.
|
||||
pub fn transfer_internal(
|
||||
from: Public,
|
||||
to: Public,
|
||||
balance: Balance,
|
||||
) -> Result<(), Error<T>> {
|
||||
// update balances of accounts
|
||||
Self::decrease_balance_internal(from, balance)?;
|
||||
Self::increase_balance_internal(to, balance)?;
|
||||
Self::deposit_event(Event::Transfer { from, to, balance });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||
pub fn transfer(origin: OriginFor<T>, to: Public, balance: Balance) -> DispatchResult {
|
||||
let from = ensure_signed(origin)?;
|
||||
Self::transfer_internal(from, to, balance)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||
pub fn burn(origin: OriginFor<T>, instruction: OutInstructionWithBalance) -> DispatchResult {
|
||||
let from = ensure_signed(origin)?;
|
||||
Self::burn_non_sri(from, instruction)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OnChargeTransaction<T> for Pallet<T> {
|
||||
type Balance = SubstrateAmount;
|
||||
type LiquidityInfo = Option<SubstrateAmount>;
|
||||
|
||||
fn withdraw_fee(
|
||||
who: &Public,
|
||||
_call: &T::RuntimeCall,
|
||||
_dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
|
||||
fee: Self::Balance,
|
||||
_tip: Self::Balance,
|
||||
) -> Result<Self::LiquidityInfo, TransactionValidityError> {
|
||||
if fee == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let balance = Balance { coin: Coin::Serai, amount: Amount(fee) };
|
||||
match Self::transfer_internal(*who, FEE_ACCOUNT.into(), balance) {
|
||||
Err(_) => Err(InvalidTransaction::Payment)?,
|
||||
Ok(()) => Ok(Some(fee)),
|
||||
}
|
||||
}
|
||||
|
||||
fn correct_and_deposit_fee(
|
||||
who: &Public,
|
||||
_dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
|
||||
_post_info: &PostDispatchInfoOf<T::RuntimeCall>,
|
||||
corrected_fee: Self::Balance,
|
||||
_tip: Self::Balance,
|
||||
already_withdrawn: Self::LiquidityInfo,
|
||||
) -> Result<(), TransactionValidityError> {
|
||||
if let Some(paid) = already_withdrawn {
|
||||
let refund_amount = paid.saturating_sub(corrected_fee);
|
||||
let balance = Balance { coin: Coin::Serai, amount: Amount(refund_amount) };
|
||||
Self::transfer_internal(FEE_ACCOUNT.into(), *who, balance)
|
||||
.map_err(|_| TransactionValidityError::Invalid(InvalidTransaction::Payment))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use pallet::*;
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "serai-tokens-primitives"
|
||||
name = "serai-coins-primitives"
|
||||
version = "0.1.0"
|
||||
description = "Serai tokens primitives"
|
||||
description = "Serai coins primitives"
|
||||
license = "MIT"
|
||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||
edition = "2021"
|
|
@ -10,9 +10,9 @@ use serde::{Serialize, Deserialize};
|
|||
use scale::{Encode, Decode, MaxEncodedLen};
|
||||
use scale_info::TypeInfo;
|
||||
|
||||
use serai_primitives::{Balance, SeraiAddress, ExternalAddress, Data, pallet_address};
|
||||
use serai_primitives::{Balance, SeraiAddress, ExternalAddress, Data, system_address};
|
||||
|
||||
pub const ADDRESS: SeraiAddress = pallet_address(b"Tokens");
|
||||
pub const FEE_ACCOUNT: SeraiAddress = system_address(b"FeeAccount");
|
||||
|
||||
#[derive(
|
||||
Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Encode, Decode, MaxEncodedLen, TypeInfo,
|
||||
|
@ -44,5 +44,8 @@ pub enum Destination {
|
|||
#[test]
|
||||
fn address() {
|
||||
use sp_runtime::traits::TrailingZeroInput;
|
||||
assert_eq!(ADDRESS, SeraiAddress::decode(&mut TrailingZeroInput::new(b"Tokens")).unwrap());
|
||||
assert_eq!(
|
||||
FEE_ACCOUNT,
|
||||
SeraiAddress::decode(&mut TrailingZeroInput::new(b"FeeAccount")).unwrap()
|
||||
);
|
||||
}
|
|
@ -28,7 +28,7 @@ frame-support = { git = "https://github.com/serai-dex/substrate", default-featur
|
|||
serai-primitives = { path = "../../primitives", default-features = false }
|
||||
in-instructions-primitives = { package = "serai-in-instructions-primitives", path = "../primitives", default-features = false }
|
||||
|
||||
tokens-pallet = { package = "serai-tokens-pallet", path = "../../tokens/pallet", default-features = false }
|
||||
coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
|
||||
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false }
|
||||
|
||||
[features]
|
||||
|
@ -49,7 +49,7 @@ std = [
|
|||
"serai-primitives/std",
|
||||
"in-instructions-primitives/std",
|
||||
|
||||
"tokens-pallet/std",
|
||||
"coins-pallet/std",
|
||||
"validator-sets-pallet/std",
|
||||
]
|
||||
default = ["std"]
|
||||
|
|
|
@ -30,7 +30,7 @@ pub mod pallet {
|
|||
use frame_support::pallet_prelude::*;
|
||||
use frame_system::pallet_prelude::*;
|
||||
|
||||
use tokens_pallet::{Config as TokensConfig, Pallet as Tokens};
|
||||
use coins_pallet::{Config as CoinsConfig, Pallet as Coins};
|
||||
use validator_sets_pallet::{
|
||||
primitives::{Session, ValidatorSet},
|
||||
Config as ValidatorSetsConfig, Pallet as ValidatorSets,
|
||||
|
@ -39,7 +39,7 @@ pub mod pallet {
|
|||
use super::*;
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config + ValidatorSetsConfig + TokensConfig {
|
||||
pub trait Config: frame_system::Config + ValidatorSetsConfig + CoinsConfig {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
}
|
||||
|
||||
|
@ -73,10 +73,11 @@ pub mod pallet {
|
|||
impl<T: Config> Pallet<T> {
|
||||
fn execute(instruction: InInstructionWithBalance) -> Result<(), ()> {
|
||||
match instruction.instruction {
|
||||
InInstruction::Transfer(address) => Tokens::<T>::mint(address, instruction.balance),
|
||||
InInstruction::Transfer(address) => {
|
||||
Coins::<T>::mint(&address.into(), instruction.balance).map_err(|_| ())
|
||||
}
|
||||
_ => panic!("unsupported instruction"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ sp-std = { git = "https://github.com/serai-dex/substrate", default-features = fa
|
|||
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
|
||||
serai-primitives = { path = "../../primitives", default-features = false }
|
||||
tokens-primitives = { package = "serai-tokens-primitives", path = "../../tokens/primitives", default-features = false }
|
||||
coins-primitives = { package = "serai-coins-primitives", path = "../../coins/primitives", default-features = false }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
|
@ -38,6 +38,6 @@ std = [
|
|||
"sp-runtime/std",
|
||||
|
||||
"serai-primitives/std",
|
||||
"tokens-primitives/std"
|
||||
"coins-primitives/std"
|
||||
]
|
||||
default = ["std"]
|
||||
|
|
|
@ -8,7 +8,7 @@ use scale_info::TypeInfo;
|
|||
|
||||
use serai_primitives::{Coin, Amount, SeraiAddress, ExternalAddress};
|
||||
|
||||
use tokens_primitives::OutInstruction;
|
||||
use coins_primitives::OutInstruction;
|
||||
|
||||
use crate::RefundableInInstruction;
|
||||
#[cfg(feature = "std")]
|
||||
|
|
|
@ -5,9 +5,9 @@ use sp_core::Pair as PairTrait;
|
|||
use sc_service::ChainType;
|
||||
|
||||
use serai_runtime::{
|
||||
primitives::*, tokens::primitives::ADDRESS as TOKENS_ADDRESS, WASM_BINARY, opaque::SessionKeys,
|
||||
BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig, SystemConfig, BalancesConfig, AssetsConfig,
|
||||
ValidatorSetsConfig, SessionConfig, BabeConfig, GrandpaConfig, AuthorityDiscoveryConfig,
|
||||
primitives::*, WASM_BINARY, opaque::SessionKeys, BABE_GENESIS_EPOCH_CONFIG, RuntimeGenesisConfig,
|
||||
SystemConfig, ValidatorSetsConfig, SessionConfig, BabeConfig, GrandpaConfig,
|
||||
AuthorityDiscoveryConfig, CoinsConfig,
|
||||
};
|
||||
|
||||
pub type ChainSpec = sc_service::GenericChainSpec<RuntimeGenesisConfig>;
|
||||
|
@ -34,24 +34,13 @@ fn testnet_genesis(
|
|||
RuntimeGenesisConfig {
|
||||
system: SystemConfig { code: wasm_binary.to_vec(), _config: PhantomData },
|
||||
|
||||
balances: BalancesConfig {
|
||||
balances: endowed_accounts.into_iter().map(|k| (k, 1 << 60)).collect(),
|
||||
},
|
||||
transaction_payment: Default::default(),
|
||||
|
||||
assets: AssetsConfig {
|
||||
assets: [Coin::Bitcoin, Coin::Ether, Coin::Dai, Coin::Monero]
|
||||
.iter()
|
||||
.map(|coin| (*coin, TOKENS_ADDRESS.into(), true, 1))
|
||||
coins: CoinsConfig {
|
||||
accounts: endowed_accounts
|
||||
.into_iter()
|
||||
.map(|a| (a, Balance { coin: Coin::Serai, amount: Amount(1 << 60) }))
|
||||
.collect(),
|
||||
metadata: vec![
|
||||
(Coin::Bitcoin, b"Bitcoin".to_vec(), b"BTC".to_vec(), 8),
|
||||
// Reduce to 8 decimals to feasibly fit within u64 (instead of its native u256)
|
||||
(Coin::Ether, b"Ether".to_vec(), b"ETH".to_vec(), 8),
|
||||
(Coin::Dai, b"Dai Stablecoin".to_vec(), b"DAI".to_vec(), 8),
|
||||
(Coin::Monero, b"Monero".to_vec(), b"XMR".to_vec(), 12),
|
||||
],
|
||||
accounts: vec![],
|
||||
},
|
||||
|
||||
validator_sets: ValidatorSetsConfig {
|
||||
|
|
|
@ -88,7 +88,7 @@ impl StaticLookup for AccountLookup {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn pallet_address(pallet: &'static [u8]) -> SeraiAddress {
|
||||
pub const fn system_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
|
||||
|
|
|
@ -34,6 +34,8 @@ pub enum NetworkId {
|
|||
pub const NETWORKS: [NetworkId; 4] =
|
||||
[NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero];
|
||||
|
||||
pub const COINS: [Coin; 5] = [Coin::Serai, Coin::Bitcoin, Coin::Ether, Coin::Dai, Coin::Monero];
|
||||
|
||||
/// The type used to identify coins.
|
||||
#[derive(
|
||||
Clone,
|
||||
|
@ -68,6 +70,37 @@ impl Coin {
|
|||
Coin::Monero => NetworkId::Monero,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Coin::Serai => "Serai",
|
||||
Coin::Bitcoin => "Bitcoin",
|
||||
Coin::Ether => "Ether",
|
||||
Coin::Dai => "Dai Stablecoin",
|
||||
Coin::Monero => "Monero",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn symbol(&self) -> &'static str {
|
||||
match self {
|
||||
Coin::Serai => "SRI",
|
||||
Coin::Bitcoin => "BTC",
|
||||
Coin::Ether => "ETH",
|
||||
Coin::Dai => "DAI",
|
||||
Coin::Monero => "XMR",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decimals(&self) -> u32 {
|
||||
match self {
|
||||
Coin::Serai => 8,
|
||||
Coin::Bitcoin => 8,
|
||||
// Ether and DAI have 18 decimals, yet we only track 8 in order to fit them within u64s
|
||||
Coin::Ether => 8,
|
||||
Coin::Dai => 8,
|
||||
Coin::Monero => 12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Max of 8 coins per network
|
||||
|
|
|
@ -43,11 +43,9 @@ serai-primitives = { path = "../primitives", default-features = false }
|
|||
|
||||
pallet-timestamp = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
|
||||
pallet-balances = { 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 }
|
||||
|
||||
tokens-pallet = { package = "serai-tokens-pallet", path = "../tokens/pallet", default-features = false }
|
||||
coins-pallet = { package = "serai-coins-pallet", path = "../coins/pallet", default-features = false }
|
||||
in-instructions-pallet = { package = "serai-in-instructions-pallet", path = "../in-instructions/pallet", default-features = false }
|
||||
|
||||
staking-pallet = { package = "serai-staking-pallet", path = "../staking/pallet", default-features = false }
|
||||
|
@ -97,11 +95,9 @@ std = [
|
|||
|
||||
"pallet-timestamp/std",
|
||||
|
||||
"pallet-balances/std",
|
||||
"pallet-transaction-payment/std",
|
||||
|
||||
"pallet-assets/std",
|
||||
"tokens-pallet/std",
|
||||
"coins-pallet/std",
|
||||
"in-instructions-pallet/std",
|
||||
|
||||
"staking-pallet/std",
|
||||
|
@ -126,11 +122,8 @@ runtime-benchmarks = [
|
|||
|
||||
"pallet-timestamp/runtime-benchmarks",
|
||||
|
||||
"pallet-balances/runtime-benchmarks",
|
||||
"pallet-assets/runtime-benchmarks",
|
||||
|
||||
"pallet-babe/runtime-benchmarks",
|
||||
"pallet-grandpa/runtime-benchmarks",
|
||||
"pallet-babe/runtime-benchmarks",
|
||||
"pallet-grandpa/runtime-benchmarks",
|
||||
]
|
||||
|
||||
default = ["std"]
|
||||
|
|
|
@ -14,11 +14,9 @@ pub use frame_support as support;
|
|||
|
||||
pub use pallet_timestamp as timestamp;
|
||||
|
||||
pub use pallet_balances as balances;
|
||||
pub use pallet_transaction_payment as transaction_payment;
|
||||
|
||||
pub use pallet_assets as assets;
|
||||
pub use tokens_pallet as tokens;
|
||||
pub use coins_pallet as coins;
|
||||
pub use in_instructions_pallet as in_instructions;
|
||||
|
||||
pub use staking_pallet as staking;
|
||||
|
@ -45,10 +43,10 @@ use sp_runtime::{
|
|||
ApplyExtrinsicResult, Perbill,
|
||||
};
|
||||
|
||||
use primitives::{PublicKey, SeraiAddress, AccountLookup, Signature, SubstrateAmount, Coin};
|
||||
use primitives::{PublicKey, SeraiAddress, AccountLookup, Signature, SubstrateAmount};
|
||||
|
||||
use support::{
|
||||
traits::{ConstU8, ConstU32, ConstU64, Contains},
|
||||
traits::{ConstU8, ConstU64, Contains},
|
||||
weights::{
|
||||
constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND},
|
||||
IdentityFee, Weight,
|
||||
|
@ -56,8 +54,6 @@ use support::{
|
|||
parameter_types, construct_runtime,
|
||||
};
|
||||
|
||||
use transaction_payment::CurrencyAdapter;
|
||||
|
||||
use babe::AuthorityId as BabeId;
|
||||
use grandpa::AuthorityId as GrandpaId;
|
||||
use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId;
|
||||
|
@ -153,22 +149,10 @@ impl Contains<RuntimeCall> for CallFilter {
|
|||
return matches!(call, timestamp::Call::set { .. });
|
||||
}
|
||||
|
||||
if let RuntimeCall::Balances(call) = call {
|
||||
return matches!(call, balances::Call::transfer { .. } | balances::Call::transfer_all { .. });
|
||||
if let RuntimeCall::Coins(call) = call {
|
||||
return matches!(call, coins::Call::transfer { .. } | coins::Call::burn { .. });
|
||||
}
|
||||
|
||||
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::execute_batch { .. });
|
||||
}
|
||||
|
@ -217,7 +201,7 @@ impl system::Config for Runtime {
|
|||
type OnKilledAccount = ();
|
||||
type OnSetCode = ();
|
||||
|
||||
type AccountData = balances::AccountData<SubstrateAmount>;
|
||||
type AccountData = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = SS58Prefix; // TODO: Remove for Bech32m
|
||||
|
||||
|
@ -231,83 +215,16 @@ impl timestamp::Config for Runtime {
|
|||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
impl balances::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
|
||||
type Balance = SubstrateAmount;
|
||||
|
||||
type ReserveIdentifier = ();
|
||||
type FreezeIdentifier = ();
|
||||
type RuntimeHoldReason = ();
|
||||
|
||||
type MaxLocks = ();
|
||||
type MaxReserves = ();
|
||||
type MaxHolds = ();
|
||||
type MaxFreezes = ();
|
||||
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ConstU64<1>;
|
||||
// TODO: What's the benefit to this?
|
||||
type AccountStore = System;
|
||||
type WeightInfo = balances::weights::SubstrateWeight<Runtime>;
|
||||
}
|
||||
|
||||
impl transaction_payment::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type OnChargeTransaction = CurrencyAdapter<Balances, ()>;
|
||||
type OnChargeTransaction = Coins;
|
||||
type OperationalFeeMultiplier = ConstU8<5>;
|
||||
type WeightToFee = IdentityFee<SubstrateAmount>;
|
||||
type LengthToFee = IdentityFee<SubstrateAmount>;
|
||||
type FeeMultiplierUpdate = ();
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub struct SeraiAssetBenchmarkHelper;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
impl assets::BenchmarkHelper<Coin> for SeraiAssetBenchmarkHelper {
|
||||
fn create_asset_id_parameter(id: u32) -> Coin {
|
||||
match (id % 4) + 1 {
|
||||
1 => Coin::Bitcoin,
|
||||
2 => Coin::Ether,
|
||||
3 => Coin::Dai,
|
||||
4 => Coin::Monero,
|
||||
_ => panic!("(id % 4) + 1 wasn't in [1, 4]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl assets::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Balance = SubstrateAmount;
|
||||
type Currency = Balances;
|
||||
|
||||
type AssetId = Coin;
|
||||
type AssetIdParameter = Coin;
|
||||
type StringLimit = ConstU32<32>;
|
||||
|
||||
// Don't allow anyone to create assets
|
||||
type CreateOrigin = support::traits::AsEnsureOriginWithArg<system::EnsureNever<PublicKey>>;
|
||||
type ForceOrigin = system::EnsureRoot<PublicKey>;
|
||||
|
||||
// Don't charge fees nor kill accounts
|
||||
type RemoveItemsLimit = ConstU32<0>;
|
||||
type AssetDeposit = ConstU64<0>;
|
||||
type AssetAccountDeposit = ConstU64<0>;
|
||||
type MetadataDepositBase = ConstU64<0>;
|
||||
type MetadataDepositPerByte = ConstU64<0>;
|
||||
type ApprovalDeposit = ConstU64<0>;
|
||||
|
||||
// Unused hooks
|
||||
type CallbackHandle = ();
|
||||
type Freezer = ();
|
||||
type Extra = ();
|
||||
|
||||
type WeightInfo = assets::weights::SubstrateWeight<Runtime>;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type BenchmarkHelper = SeraiAssetBenchmarkHelper;
|
||||
}
|
||||
|
||||
impl tokens::Config for Runtime {
|
||||
impl coins::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
}
|
||||
|
||||
|
@ -315,9 +232,7 @@ impl in_instructions::Config for Runtime {
|
|||
type RuntimeEvent = RuntimeEvent;
|
||||
}
|
||||
|
||||
impl staking::Config for Runtime {
|
||||
type Currency = Balances;
|
||||
}
|
||||
impl staking::Config for Runtime {}
|
||||
|
||||
impl validator_sets::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
|
@ -403,11 +318,9 @@ construct_runtime!(
|
|||
|
||||
Timestamp: timestamp,
|
||||
|
||||
Balances: balances,
|
||||
TransactionPayment: transaction_payment,
|
||||
|
||||
Assets: assets,
|
||||
Tokens: tokens,
|
||||
Coins: coins,
|
||||
InInstructions: in_instructions,
|
||||
|
||||
ValidatorSets: validator_sets,
|
||||
|
|
|
@ -15,17 +15,19 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||
parity-scale-codec = { version = "3", default-features = false, features = ["derive"] }
|
||||
scale-info = { version = "2", default-features = false, features = ["derive"] }
|
||||
|
||||
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
|
||||
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
|
||||
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false }
|
||||
pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
|
||||
serai-primitives = { path = "../../primitives", default-features = false }
|
||||
serai-validator-sets-primitives = { path = "../../validator-sets/primitives", default-features = false }
|
||||
|
||||
coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false }
|
||||
|
||||
validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false }
|
||||
|
||||
pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||
|
||||
[features]
|
||||
std = [
|
||||
|
@ -33,8 +35,14 @@ std = [
|
|||
"frame-support/std",
|
||||
|
||||
"sp-std/std",
|
||||
"sp-runtime/std",
|
||||
|
||||
"serai-primitives/std",
|
||||
|
||||
"coins-pallet/std",
|
||||
|
||||
"validator-sets-pallet/std",
|
||||
|
||||
"pallet-session/std",
|
||||
]
|
||||
|
||||
|
|
|
@ -6,12 +6,11 @@ pub mod pallet {
|
|||
use sp_std::vec::Vec;
|
||||
|
||||
use frame_system::pallet_prelude::*;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{Currency, tokens::ExistenceRequirement},
|
||||
};
|
||||
use frame_support::pallet_prelude::*;
|
||||
|
||||
use serai_primitives::{NetworkId, Amount, PublicKey};
|
||||
use serai_primitives::*;
|
||||
|
||||
use coins_pallet::{Config as CoinsConfig, Pallet as Coins};
|
||||
|
||||
use validator_sets_pallet::{
|
||||
primitives::{Session, ValidatorSet},
|
||||
|
@ -29,9 +28,8 @@ pub mod pallet {
|
|||
|
||||
#[pallet::config]
|
||||
pub trait Config:
|
||||
frame_system::Config + VsConfig + SessionConfig<ValidatorId = PublicKey>
|
||||
frame_system::Config + CoinsConfig + VsConfig + SessionConfig<ValidatorId = PublicKey>
|
||||
{
|
||||
type Currency: Currency<Self::AccountId, Balance = u64>;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
|
@ -98,10 +96,8 @@ pub mod pallet {
|
|||
#[pallet::weight((0, DispatchClass::Operational))] // TODO
|
||||
pub fn stake(origin: OriginFor<T>, #[pallet::compact] amount: u64) -> DispatchResult {
|
||||
let signer = ensure_signed(origin)?;
|
||||
// Serai accounts are solely public keys. Accordingly, there's no harm to letting accounts
|
||||
// die. They'll simply be re-instantiated later
|
||||
// AllowDeath accordingly to not add additional requirements (and therefore annoyances)
|
||||
T::Currency::transfer(&signer, &Self::account(), amount, ExistenceRequirement::AllowDeath)?;
|
||||
let balance = Balance { coin: Coin::Serai, amount: Amount(amount) };
|
||||
Coins::<T>::transfer_internal(&signer, &Self::account(), balance)?;
|
||||
Self::add_stake(&signer, amount);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -112,8 +108,8 @@ pub mod pallet {
|
|||
pub fn unstake(origin: OriginFor<T>, #[pallet::compact] amount: u64) -> DispatchResult {
|
||||
let signer = ensure_signed(origin)?;
|
||||
Self::remove_stake(&signer, amount)?;
|
||||
// This should never be out of funds as there should always be stakers. Accordingly...
|
||||
T::Currency::transfer(&Self::account(), &signer, amount, ExistenceRequirement::KeepAlive)?;
|
||||
let balance = Balance { coin: Coin::Serai, amount: Amount(amount) };
|
||||
Coins::<T>::transfer_internal(&Self::account(), &signer, balance)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
[package]
|
||||
name = "serai-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 = "../../primitives", default-features = false }
|
||||
tokens-primitives = { package = "serai-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"]
|
|
@ -1,82 +0,0 @@
|
|||
#![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]
|
||||
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 allocated stake
|
||||
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::*;
|
|
@ -19,7 +19,7 @@ use serai_client::{
|
|||
},
|
||||
coins::{
|
||||
primitives::{OutInstruction, OutInstructionWithBalance},
|
||||
TokensEvent,
|
||||
CoinsEvent,
|
||||
},
|
||||
in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch},
|
||||
SeraiCoins,
|
||||
|
@ -206,12 +206,13 @@ async fn sign_test() {
|
|||
let address = SeraiAddress::from(pair.public());
|
||||
|
||||
// Fund the new account to pay for fees
|
||||
let balance = Balance { coin: Coin::Serai, amount: Amount(1_000_000_000) };
|
||||
serai
|
||||
.publish(
|
||||
&serai
|
||||
.sign(
|
||||
&PairSigner::new(insecure_pair_from_name("Ferdie")),
|
||||
&SeraiCoins::transfer_sri(address, Amount(1_000_000_000)),
|
||||
&SeraiCoins::transfer(address, balance),
|
||||
0,
|
||||
Default::default(),
|
||||
)
|
||||
|
@ -249,29 +250,32 @@ async fn sign_test() {
|
|||
serai.block_by_number(block_included_in).await.unwrap().unwrap().hash();
|
||||
|
||||
let serai = serai.as_of(block_included_in_hash).coins();
|
||||
assert_eq!(serai.sri_balance(serai_addr).await.unwrap(), 1_000_000_000);
|
||||
assert_eq!(
|
||||
serai.coin_balance(Coin::Serai, serai_addr).await.unwrap(),
|
||||
Amount(1_000_000_000)
|
||||
);
|
||||
|
||||
// Verify the mint occurred as expected
|
||||
assert_eq!(
|
||||
serai.mint_events().await.unwrap(),
|
||||
vec![TokensEvent::Mint { address: serai_addr, balance }]
|
||||
vec![CoinsEvent::Mint { address: serai_addr, balance }]
|
||||
);
|
||||
assert_eq!(serai.token_supply(Coin::Bitcoin).await.unwrap(), amount);
|
||||
assert_eq!(serai.token_balance(Coin::Bitcoin, serai_addr).await.unwrap(), amount);
|
||||
assert_eq!(serai.coin_supply(Coin::Bitcoin).await.unwrap(), amount);
|
||||
assert_eq!(serai.coin_balance(Coin::Bitcoin, serai_addr).await.unwrap(), amount);
|
||||
}
|
||||
|
||||
// Trigger a burn
|
||||
let out_instruction =
|
||||
OutInstruction { address: ExternalAddress::new(b"external".to_vec()).unwrap(), data: None };
|
||||
let out_instruction = OutInstructionWithBalance {
|
||||
balance,
|
||||
instruction: OutInstruction {
|
||||
address: ExternalAddress::new(b"external".to_vec()).unwrap(),
|
||||
data: None,
|
||||
},
|
||||
};
|
||||
serai
|
||||
.publish(
|
||||
&serai
|
||||
.sign(
|
||||
&serai_pair,
|
||||
&SeraiCoins::burn(balance, out_instruction.clone()),
|
||||
0,
|
||||
Default::default(),
|
||||
)
|
||||
.sign(&serai_pair, &SeraiCoins::burn(out_instruction.clone()), 0, Default::default())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
|
@ -297,11 +301,7 @@ async fn sign_test() {
|
|||
assert_eq!(burn_events.len(), 1);
|
||||
assert_eq!(
|
||||
burn_events[0],
|
||||
TokensEvent::Burn {
|
||||
address: serai_addr,
|
||||
balance,
|
||||
instruction: out_instruction.clone()
|
||||
}
|
||||
CoinsEvent::Burn { address: serai_addr, instruction: out_instruction.clone() }
|
||||
);
|
||||
break 'outer;
|
||||
}
|
||||
|
@ -312,8 +312,8 @@ async fn sign_test() {
|
|||
let last_serai_block = serai.block_by_number(last_serai_block).await.unwrap().unwrap();
|
||||
let last_serai_block_hash = last_serai_block.hash();
|
||||
let serai = serai.as_of(last_serai_block_hash).coins();
|
||||
assert_eq!(serai.token_supply(Coin::Bitcoin).await.unwrap(), Amount(0));
|
||||
assert_eq!(serai.token_balance(Coin::Bitcoin, serai_addr).await.unwrap(), Amount(0));
|
||||
assert_eq!(serai.coin_supply(Coin::Bitcoin).await.unwrap(), Amount(0));
|
||||
assert_eq!(serai.coin_balance(Coin::Bitcoin, serai_addr).await.unwrap(), Amount(0));
|
||||
|
||||
let mut plan_id = [0; 32];
|
||||
OsRng.fill_bytes(&mut plan_id);
|
||||
|
@ -331,10 +331,7 @@ async fn sign_test() {
|
|||
},
|
||||
network: NetworkId::Bitcoin,
|
||||
block: last_serai_block.number(),
|
||||
burns: vec![OutInstructionWithBalance {
|
||||
instruction: out_instruction.clone(),
|
||||
balance: Balance { coin: Coin::Bitcoin, amount }
|
||||
}],
|
||||
burns: vec![out_instruction.clone()],
|
||||
batches: vec![],
|
||||
}
|
||||
)
|
||||
|
|
|
@ -15,7 +15,7 @@ use serai_client::{
|
|||
},
|
||||
validator_sets::primitives::{Session, ValidatorSet},
|
||||
in_instructions::primitives::Shorthand,
|
||||
coins::primitives::OutInstruction,
|
||||
coins::primitives::{OutInstruction, OutInstructionWithBalance},
|
||||
PairTrait, PairSigner, SeraiCoins,
|
||||
};
|
||||
|
||||
|
@ -236,12 +236,13 @@ async fn mint_and_burn_test() {
|
|||
let address = SeraiAddress::from(pair.public());
|
||||
|
||||
// Fund the new account to pay for fees
|
||||
let balance = Balance { coin: Coin::Serai, amount: Amount(1_000_000_000) };
|
||||
serai
|
||||
.publish(
|
||||
&serai
|
||||
.sign(
|
||||
&PairSigner::new(insecure_pair_from_name("Ferdie")),
|
||||
&SeraiCoins::transfer_sri(address, Amount(1_000_000_000)),
|
||||
&SeraiCoins::transfer(address, balance),
|
||||
0,
|
||||
Default::default(),
|
||||
)
|
||||
|
@ -488,17 +489,15 @@ async fn mint_and_burn_test() {
|
|||
let serai = &serai;
|
||||
let serai_pair = &serai_pair;
|
||||
move |nonce, coin, amount, address| async move {
|
||||
let out_instruction = OutInstruction { address, data: None };
|
||||
let out_instruction = OutInstructionWithBalance {
|
||||
balance: Balance { coin, amount: Amount(amount) },
|
||||
instruction: OutInstruction { address, data: None },
|
||||
};
|
||||
|
||||
serai
|
||||
.publish(
|
||||
&serai
|
||||
.sign(
|
||||
serai_pair,
|
||||
&SeraiCoins::burn(Balance { coin, amount: Amount(amount) }, out_instruction),
|
||||
nonce,
|
||||
Default::default(),
|
||||
)
|
||||
.sign(serai_pair, &SeraiCoins::burn(out_instruction), nonce, Default::default())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
|
|
Loading…
Reference in a new issue