diff --git a/Cargo.lock b/Cargo.lock index 72526bc19..3b55f7b37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8033,6 +8033,7 @@ dependencies = [ "serai-coins-primitives", "serai-primitives", "sp-core", + "sp-io", "sp-runtime", "sp-std", ] diff --git a/substrate/coins/pallet/Cargo.toml b/substrate/coins/pallet/Cargo.toml index 8c59fb3e6..88ebfd32c 100644 --- a/substrate/coins/pallet/Cargo.toml +++ b/substrate/coins/pallet/Cargo.toml @@ -34,6 +34,9 @@ pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", d serai-primitives = { path = "../../primitives", default-features = false, features = ["serde"] } coins-primitives = { package = "serai-coins-primitives", path = "../primitives", default-features = false } +[dev-dependencies] +sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false } + [features] std = [ "frame-system/std", @@ -41,6 +44,7 @@ std = [ "sp-core/std", "sp-std/std", + "sp-io/std", "sp-runtime/std", "pallet-transaction-payment/std", @@ -49,8 +53,12 @@ std = [ "coins-primitives/std", ] -# TODO -try-runtime = [] +try-runtime = [ + "frame-system/try-runtime", + "frame-support/try-runtime", + + "sp-runtime/try-runtime", +] runtime-benchmarks = [ "frame-system/runtime-benchmarks", diff --git a/substrate/coins/pallet/src/lib.rs b/substrate/coins/pallet/src/lib.rs index dd64b2b6b..4499f432e 100644 --- a/substrate/coins/pallet/src/lib.rs +++ b/substrate/coins/pallet/src/lib.rs @@ -1,5 +1,11 @@ #![cfg_attr(not(feature = "std"), no_std)] +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + use serai_primitives::{Balance, Coin, ExternalBalance, SubstrateAmount}; pub trait AllowMint { diff --git a/substrate/coins/pallet/src/mock.rs b/substrate/coins/pallet/src/mock.rs new file mode 100644 index 000000000..bd4ebc55f --- /dev/null +++ b/substrate/coins/pallet/src/mock.rs @@ -0,0 +1,70 @@ +//! Test environment for Coins pallet. + +use super::*; + +use frame_support::{ + construct_runtime, + traits::{ConstU32, ConstU64}, +}; + +use sp_core::{H256, sr25519::Public}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +use crate as coins; + +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Test + { + System: frame_system, + Coins: coins, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = Public; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + + type AllowMint = (); +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + crate::GenesisConfig:: { accounts: vec![], _ignore: Default::default() } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(0)); + ext +} diff --git a/substrate/coins/pallet/src/tests.rs b/substrate/coins/pallet/src/tests.rs new file mode 100644 index 000000000..a6d16afde --- /dev/null +++ b/substrate/coins/pallet/src/tests.rs @@ -0,0 +1,129 @@ +use crate::{mock::*, primitives::*}; + +use frame_system::RawOrigin; +use sp_core::Pair; + +use serai_primitives::*; + +pub type CoinsEvent = crate::Event; + +#[test] +fn mint() { + new_test_ext().execute_with(|| { + // minting u64::MAX should work + let coin = Coin::Serai; + let to = insecure_pair_from_name("random1").public(); + let balance = Balance { coin, amount: Amount(u64::MAX) }; + + Coins::mint(to, balance).unwrap(); + assert_eq!(Coins::balance(to, coin), balance.amount); + + // minting more should fail + assert!(Coins::mint(to, Balance { coin, amount: Amount(1) }).is_err()); + + // supply now should be equal to sum of the accounts balance sum + assert_eq!(Coins::supply(coin), balance.amount.0); + + // test events + let mint_events = System::events() + .iter() + .filter_map(|event| { + if let RuntimeEvent::Coins(e) = &event.event { + if matches!(e, CoinsEvent::Mint { .. }) { + Some(e.clone()) + } else { + None + } + } else { + None + } + }) + .collect::>(); + + assert_eq!(mint_events, vec![CoinsEvent::Mint { to, balance }]); + }) +} + +#[test] +fn burn_with_instruction() { + new_test_ext().execute_with(|| { + // mint some coin + let coin = Coin::External(ExternalCoin::Bitcoin); + let to = insecure_pair_from_name("random1").public(); + let balance = Balance { coin, amount: Amount(10 * 10u64.pow(coin.decimals())) }; + + Coins::mint(to, balance).unwrap(); + assert_eq!(Coins::balance(to, coin), balance.amount); + assert_eq!(Coins::supply(coin), balance.amount.0); + + // we shouldn't be able to burn more than what we have + let mut instruction = OutInstructionWithBalance { + instruction: OutInstruction { address: ExternalAddress::new(vec![]).unwrap(), data: None }, + balance: ExternalBalance { + coin: coin.try_into().unwrap(), + amount: Amount(balance.amount.0 + 1), + }, + }; + assert!( + Coins::burn_with_instruction(RawOrigin::Signed(to).into(), instruction.clone()).is_err() + ); + + // it should now work + instruction.balance.amount = balance.amount; + Coins::burn_with_instruction(RawOrigin::Signed(to).into(), instruction.clone()).unwrap(); + + // balance & supply now should be back to 0 + assert_eq!(Coins::balance(to, coin), Amount(0)); + assert_eq!(Coins::supply(coin), 0); + + let burn_events = System::events() + .iter() + .filter_map(|event| { + if let RuntimeEvent::Coins(e) = &event.event { + if matches!(e, CoinsEvent::BurnWithInstruction { .. }) { + Some(e.clone()) + } else { + None + } + } else { + None + } + }) + .collect::>(); + + assert_eq!(burn_events, vec![CoinsEvent::BurnWithInstruction { from: to, instruction }]); + }) +} + +#[test] +fn transfer() { + new_test_ext().execute_with(|| { + // mint some coin + let coin = Coin::External(ExternalCoin::Bitcoin); + let from = insecure_pair_from_name("random1").public(); + let balance = Balance { coin, amount: Amount(10 * 10u64.pow(coin.decimals())) }; + + Coins::mint(from, balance).unwrap(); + assert_eq!(Coins::balance(from, coin), balance.amount); + assert_eq!(Coins::supply(coin), balance.amount.0); + + // we can't send more than what we have + let to = insecure_pair_from_name("random2").public(); + assert!(Coins::transfer( + RawOrigin::Signed(from).into(), + to, + Balance { coin, amount: Amount(balance.amount.0 + 1) } + ) + .is_err()); + + // we can send it all + Coins::transfer(RawOrigin::Signed(from).into(), to, balance).unwrap(); + + // check the balances + assert_eq!(Coins::balance(from, coin), Amount(0)); + assert_eq!(Coins::balance(to, coin), balance.amount); + + // supply shouldn't change + assert_eq!(Coins::supply(coin), balance.amount.0); + }) +}