From 2567e0b5ce897c04e5339596e8596aba1bf1b675 Mon Sep 17 00:00:00 2001 From: akildemir Date: Fri, 13 Sep 2024 16:59:19 +0300 Subject: [PATCH] add pallet tests --- Cargo.lock | 4 + substrate/emissions/pallet/src/lib.rs | 17 +- substrate/genesis-liquidity/pallet/src/lib.rs | 2 + substrate/in-instructions/pallet/Cargo.toml | 27 +- substrate/in-instructions/pallet/src/lib.rs | 6 + substrate/in-instructions/pallet/src/mock.rs | 199 +++++++ substrate/in-instructions/pallet/src/tests.rs | 500 ++++++++++++++++++ 7 files changed, 747 insertions(+), 8 deletions(-) create mode 100644 substrate/in-instructions/pallet/src/mock.rs create mode 100644 substrate/in-instructions/pallet/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index aeab97f3..3c2bc9e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8292,10 +8292,14 @@ version = "0.1.0" dependencies = [ "frame-support", "frame-system", + "pallet-babe", + "pallet-grandpa", + "pallet-timestamp", "parity-scale-codec", "scale-info", "serai-coins-pallet", "serai-dex-pallet", + "serai-economic-security-pallet", "serai-emissions-pallet", "serai-genesis-liquidity-pallet", "serai-in-instructions-primitives", diff --git a/substrate/emissions/pallet/src/lib.rs b/substrate/emissions/pallet/src/lib.rs index 400f8921..78ba9377 100644 --- a/substrate/emissions/pallet/src/lib.rs +++ b/substrate/emissions/pallet/src/lib.rs @@ -12,7 +12,7 @@ pub mod pallet { use frame_system::{pallet_prelude::*, RawOrigin}; use frame_support::{pallet_prelude::*, sp_runtime::SaturatedConversion}; - use sp_std::{vec, vec::Vec, ops::Mul, collections::btree_map::BTreeMap}; + use sp_std::{vec, vec::Vec, collections::btree_map::BTreeMap}; use coins_pallet::{Config as CoinsConfig, Pallet as Coins}; use dex_pallet::{Config as DexConfig, Pallet as Dex}; @@ -59,6 +59,7 @@ pub mod pallet { NetworkHasEconomicSecurity, NoValueForCoin, InsufficientAllocation, + AmountOverflow, } #[pallet::event] @@ -399,9 +400,17 @@ pub mod pallet { let last_block = >::block_number() - 1u32.into(); let value = Dex::::spot_price_for_block(last_block, balance.coin) .ok_or(Error::::NoValueForCoin)?; - // TODO: may panic? It might be best for this math ops to return the result as is instead of - // doing an unwrap so that it can be properly dealt with. - let sri_amount = balance.amount.mul(value); + + let sri_amount = Amount( + u64::try_from( + u128::from(balance.amount.0) + .checked_mul(u128::from(value.0)) + .ok_or(Error::::AmountOverflow)? + .checked_div(u128::from(10u64.pow(balance.coin.decimals()))) + .ok_or(Error::::AmountOverflow)?, + ) + .map_err(|_| Error::::AmountOverflow)?, + ); // Mint Coins::::mint(to, Balance { coin: Coin::Serai, amount: sri_amount })?; diff --git a/substrate/genesis-liquidity/pallet/src/lib.rs b/substrate/genesis-liquidity/pallet/src/lib.rs index c9e4e4f4..e5662d55 100644 --- a/substrate/genesis-liquidity/pallet/src/lib.rs +++ b/substrate/genesis-liquidity/pallet/src/lib.rs @@ -64,11 +64,13 @@ pub mod pallet { /// Keeps shares and the amount of coins per account. #[pallet::storage] + #[pallet::getter(fn liquidity)] pub(crate) type Liquidity = StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, LiquidityAmount, OptionQuery>; /// Keeps the total shares and the total amount of coins per coin. #[pallet::storage] + #[pallet::getter(fn supply)] pub(crate) type Supply = StorageMap<_, Identity, Coin, LiquidityAmount, OptionQuery>; #[pallet::storage] diff --git a/substrate/in-instructions/pallet/Cargo.toml b/substrate/in-instructions/pallet/Cargo.toml index a12e38b3..2ccd2b0d 100644 --- a/substrate/in-instructions/pallet/Cargo.toml +++ b/substrate/in-instructions/pallet/Cargo.toml @@ -40,6 +40,14 @@ validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../.. genesis-liquidity-pallet = { package = "serai-genesis-liquidity-pallet", path = "../../genesis-liquidity/pallet", default-features = false } emissions-pallet = { package = "serai-emissions-pallet", path = "../../emissions/pallet", default-features = false } + +[dev-dependencies] +pallet-babe = { git = "https://github.com/serai-dex/substrate", default-features = false } +pallet-grandpa = { git = "https://github.com/serai-dex/substrate", default-features = false } +pallet-timestamp = { git = "https://github.com/serai-dex/substrate", default-features = false } + +economic-security-pallet = { package = "serai-economic-security-pallet", path = "../../economic-security/pallet", default-features = false } + [features] std = [ "scale/std", @@ -62,8 +70,19 @@ std = [ "validator-sets-pallet/std", "genesis-liquidity-pallet/std", "emissions-pallet/std", -] -default = ["std"] -# TODO -try-runtime = [] + "economic-security-pallet/std", + + "pallet-babe/std", + "pallet-grandpa/std", + "pallet-timestamp/std", +] + +try-runtime = [ + "frame-system/try-runtime", + "frame-support/try-runtime", + + "sp-runtime/try-runtime", +] + +default = ["std"] diff --git a/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs index f90ae412..6fddbcbe 100644 --- a/substrate/in-instructions/pallet/src/lib.rs +++ b/substrate/in-instructions/pallet/src/lib.rs @@ -9,6 +9,12 @@ use serai_primitives::{BlockHash, NetworkId}; pub use in_instructions_primitives as primitives; use primitives::*; +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + // TODO: Investigate why Substrate generates these #[allow( unreachable_patterns, diff --git a/substrate/in-instructions/pallet/src/mock.rs b/substrate/in-instructions/pallet/src/mock.rs new file mode 100644 index 00000000..93c17d87 --- /dev/null +++ b/substrate/in-instructions/pallet/src/mock.rs @@ -0,0 +1,199 @@ +//! Test environment for InInstructions pallet. + +use super::*; + +use frame_support::{ + construct_runtime, + traits::{ConstU16, ConstU32, ConstU64}, +}; + +use sp_core::{H256, Pair, sr25519::Public}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +use serai_primitives::*; +use validator_sets::{primitives::MAX_KEY_SHARES_PER_SET, MembershipProof}; + +pub use crate as in_instructions; +pub use coins_pallet as coins; +pub use validator_sets_pallet as validator_sets; +pub use genesis_liquidity_pallet as genesis_liquidity; +pub use emissions_pallet as emissions; +pub use dex_pallet as dex; +pub use pallet_babe as babe; +pub use pallet_grandpa as grandpa; +pub use pallet_timestamp as timestamp; +pub use economic_security_pallet as economic_security; + +type Block = frame_system::mocking::MockBlock; +// Maximum number of authorities per session. +pub type MaxAuthorities = ConstU32<{ MAX_KEY_SHARES_PER_SET }>; + +pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = 10; + +construct_runtime!( + pub enum Test + { + System: frame_system, + Timestamp: timestamp, + Coins: coins, + LiquidityTokens: coins::::{Pallet, Call, Storage, Event}, + Emissions: emissions, + ValidatorSets: validator_sets, + GenesisLiquidity: genesis_liquidity, + EconomicSecurity: economic_security, + Dex: dex, + Babe: babe, + Grandpa: grandpa, + InInstructions: in_instructions, + } +); + +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 timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = Babe; + type MinimumPeriod = ConstU64<{ (TARGET_BLOCK_TIME * 1000) / 2 }>; + type WeightInfo = (); +} + +impl babe::Config for Test { + type EpochDuration = ConstU64<{ FAST_EPOCH_DURATION }>; + + type ExpectedBlockTime = ConstU64<{ TARGET_BLOCK_TIME * 1000 }>; + type EpochChangeTrigger = babe::ExternalTrigger; + type DisabledValidators = ValidatorSets; + + type WeightInfo = (); + type MaxAuthorities = MaxAuthorities; + + type KeyOwnerProof = MembershipProof; + type EquivocationReportSystem = (); +} + +impl grandpa::Config for Test { + type RuntimeEvent = RuntimeEvent; + + type WeightInfo = (); + type MaxAuthorities = MaxAuthorities; + + type MaxSetIdSessionEntries = ConstU64<0>; + type KeyOwnerProof = MembershipProof; + type EquivocationReportSystem = (); +} + +impl coins::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AllowMint = (); +} + +impl coins::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AllowMint = (); +} + +impl dex::Config for Test { + type RuntimeEvent = RuntimeEvent; + + type LPFee = ConstU32<3>; // 0.3% + type MintMinLiquidity = ConstU64<10000>; + + type MaxSwapPathLength = ConstU32<3>; // coin1 -> SRI -> coin2 + + type MedianPriceWindowLength = ConstU16<{ MEDIAN_PRICE_WINDOW_LENGTH }>; + + type WeightInfo = dex::weights::SubstrateWeight; +} + +impl validator_sets::Config for Test { + type RuntimeEvent = RuntimeEvent; + type ShouldEndSession = Babe; +} + +impl genesis_liquidity::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +impl emissions::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +impl economic_security::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + let accounts: Vec = vec![ + insecure_pair_from_name("Alice").public(), + insecure_pair_from_name("Bob").public(), + insecure_pair_from_name("Charlie").public(), + insecure_pair_from_name("Dave").public(), + insecure_pair_from_name("Eve").public(), + insecure_pair_from_name("Ferdie").public(), + ]; + let validators = accounts.clone(); + + let networks = NETWORKS + .iter() + .map(|network| match network { + NetworkId::Serai => (NetworkId::Serai, Amount(50_000 * 10_u64.pow(8))), + NetworkId::Bitcoin => (NetworkId::Bitcoin, Amount(1_000_000 * 10_u64.pow(8))), + NetworkId::Ethereum => (NetworkId::Ethereum, Amount(1_000_000 * 10_u64.pow(8))), + NetworkId::Monero => (NetworkId::Monero, Amount(100_000 * 10_u64.pow(8))), + }) + .collect::>(); + + coins::GenesisConfig:: { + accounts: accounts + .into_iter() + .map(|a| (a, Balance { coin: Coin::Serai, amount: Amount(1 << 60) })) + .collect(), + _ignore: Default::default(), + } + .assimilate_storage(&mut t) + .unwrap(); + + validator_sets::GenesisConfig:: { + networks: networks.clone(), + participants: validators.clone(), + } + .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/in-instructions/pallet/src/tests.rs b/substrate/in-instructions/pallet/src/tests.rs new file mode 100644 index 00000000..ca627441 --- /dev/null +++ b/substrate/in-instructions/pallet/src/tests.rs @@ -0,0 +1,500 @@ +use super::*; +use crate::mock::*; + +use emissions_pallet::primitives::POL_ACCOUNT; +use genesis_liquidity_pallet::primitives::INITIAL_GENESIS_LP_SHARES; +use scale::Encode; + +use frame_support::{pallet_prelude::InvalidTransaction, traits::OnFinalize}; +use frame_system::RawOrigin; + +use sp_core::{sr25519::Public, Pair}; +use sp_runtime::{traits::ValidateUnsigned, transaction_validity::TransactionSource, BoundedVec}; + +use validator_sets::{Pallet as ValidatorSets, primitives::KeyPair}; +use coins::primitives::{OutInstruction, OutInstructionWithBalance}; +use genesis_liquidity::primitives::GENESIS_LIQUIDITY_ACCOUNT; + +use serai_primitives::*; + +fn set_keys_for_session(key: Public) { + for n in NETWORKS { + if n == NetworkId::Serai { + continue; + } + + ValidatorSets::::set_keys( + RawOrigin::None.into(), + n, + BoundedVec::new(), + KeyPair(key, vec![].try_into().unwrap()), + Signature([0u8; 64]), + ) + .unwrap(); + } +} + +fn get_events() -> Vec> { + let events = System::events() + .iter() + .filter_map(|event| { + if let RuntimeEvent::InInstructions(e) = &event.event { + Some(e.clone()) + } else { + None + } + }) + .collect::>(); + + System::reset_events(); + events +} + +fn make_liquid_pool(coin: Coin, amount: u64) { + // mint coins so that we can add liquidity + let account = insecure_pair_from_name("make-pool-account").public(); + Coins::mint(account, Balance { coin, amount: Amount(amount) }).unwrap(); + Coins::mint(account, Balance { coin: Coin::Serai, amount: Amount(amount) }).unwrap(); + + // make some liquid pool + Dex::add_liquidity(RawOrigin::Signed(account).into(), coin, amount, amount, 1, 1, account) + .unwrap(); +} + +#[test] +fn validate_batch() { + new_test_ext().execute_with(|| { + let pair = insecure_pair_from_name("Alice"); + set_keys_for_session(pair.public()); + + let mut batch_size = 0; + let mut batch = + Batch { network: NetworkId::Serai, id: 1, block: BlockHash([0u8; 32]), instructions: vec![] }; + + // batch size bigger than MAX_BATCH_SIZE should fail + while batch_size <= MAX_BATCH_SIZE + 1000 { + batch.instructions.push(InInstructionWithBalance { + instruction: InInstruction::Transfer(SeraiAddress::new([0u8; 32])), + balance: Balance { coin: Coin::Serai, amount: Amount(1) }, + }); + batch_size = batch.encode().len(); + } + + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature: Signature([0u8; 64]) }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::ExhaustsResources.into() + ); + + // reduce the batch size into allowed size + while batch_size > MAX_BATCH_SIZE { + batch.instructions.pop(); + batch_size = batch.encode().len(); + } + + // serai network can't submit batches + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature: Signature([0u8; 64]) }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Custom(0).into() // network is serai error + ); + + // change the network to an external network + batch.network = NetworkId::Monero; + + // 0 signature should be invalid + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature: Signature([0u8; 64]) }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::BadProof.into() + ); + + // submit a valid signature + let signature = pair.sign(&batch_message(&batch)); + + // network shouldn't be halted + InInstructions::halt(NetworkId::Monero).unwrap(); + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Custom(1).into() // network halted error + ); + + // submit from an un-halted network + batch.network = NetworkId::Bitcoin; + let signature = pair.sign(&batch_message(&batch)); + + // can't submit in the first block(Block 0) + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature: signature.clone() }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Future.into() + ); + + // update block number + System::set_block_number(1); + + // first batch id should be 0 + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature: signature.clone() }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Future.into() + ); + + // update batch id + batch.id = 0; + let signature = pair.sign(&batch_message(&batch)); + + // can't have more than 1 batch per block + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature: signature.clone() }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Future.into() + ); + + // update block number + System::set_block_number(2); + + // network and the instruction coins should match + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Custom(2).into() // network and instruction coins doesn't match error + ); + + // update block number & batch + System::set_block_number(3); + for ins in &mut batch.instructions { + ins.balance.coin = Coin::Bitcoin + } + let signature = pair.sign(&batch_message(&batch)); + + // batch id can't be equal or less than previous id + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Stale.into() + ); + + // update block number & batch + System::set_block_number(4); + batch.id += 2; + let signature = pair.sign(&batch_message(&batch)); + + // batch id can't be incremented more than once per batch + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature }, + }; + assert_eq!( + InInstructions::validate_unsigned(TransactionSource::External, &call), + InvalidTransaction::Future.into() + ); + + // update block number & batch + System::set_block_number(5); + batch.id = (batch.id - 2) + 1; + let signature = pair.sign(&batch_message(&batch)); + + // it should now pass + let call = pallet::Call::::execute_batch { + batch: SignedBatch { batch: batch.clone(), signature }, + }; + InInstructions::validate_unsigned(TransactionSource::External, &call).unwrap(); + }); +} + +#[test] +fn transfer_instruction() { + new_test_ext().execute_with(|| { + let coin = Coin::Bitcoin; + let amount = Amount(2 * 10u64.pow(coin.decimals())); + let account = insecure_pair_from_name("random1").public(); + let batch = SignedBatch { + batch: Batch { + network: coin.network(), + id: 0, + block: BlockHash([0u8; 32]), + instructions: vec![InInstructionWithBalance { + instruction: InInstruction::Transfer(account.into()), + balance: Balance { coin, amount }, + }], + }, + signature: Signature([0u8; 64]), + }; + InInstructions::execute_batch(RawOrigin::None.into(), batch).unwrap(); + + // check that account has the coins + assert_eq!(Coins::balance(account, coin), amount); + }) +} + +#[test] +fn dex_instruction_add_liquidity() { + new_test_ext().execute_with(|| { + let coin = Coin::Ether; + let amount = Amount(2 * 10u64.pow(coin.decimals())); + let account = insecure_pair_from_name("random1").public(); + + let batch = SignedBatch { + batch: Batch { + network: coin.network(), + id: 0, + block: BlockHash([0u8; 32]), + instructions: vec![InInstructionWithBalance { + instruction: InInstruction::Dex(DexCall::SwapAndAddLiquidity(account.into())), + balance: Balance { coin, amount }, + }], + }, + signature: Signature([0u8; 64]), + }; + + // we should have a liquid pool before we can swap + InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); + + // check that the instruction is failed + assert_eq!( + get_events() + .into_iter() + .filter(|event| matches!(event, in_instructions::Event::::InstructionFailure { .. })) + .collect::>(), + vec![in_instructions::Event::::InstructionFailure { + network: batch.batch.network, + id: batch.batch.id, + index: 0 + }] + ); + + let original_coin_amount = 5 * 10u64.pow(coin.decimals()); + make_liquid_pool(coin, original_coin_amount); + + // this should now be successful + InInstructions::execute_batch(RawOrigin::None.into(), batch).unwrap(); + + // check that the instruction was successful + assert_eq!( + get_events() + .into_iter() + .filter(|event| matches!(event, in_instructions::Event::::InstructionFailure { .. })) + .collect::>(), + vec![] + ); + + // check that we now have a Ether pool with correct liquidity + // we can't know the actual SRI amount since we don't know the result of the swap. + // Moreover, knowing exactly how much isn't the responsibility of InInstruction pallet, + // it is responsibility of the Dex pallet. + let (coin_amount, _serai_amount) = Dex::get_reserves(&coin, &Coin::Serai).unwrap(); + assert_eq!(coin_amount, original_coin_amount + amount.0); + + // assert that the account got the liquidity tokens, again we don't how much and + // it isn't this pallets responsibility. + assert!(LiquidityTokens::balance(account, coin).0 > 0); + + // check that in ins account doesn't have the coins + assert_eq!(Coins::balance(IN_INSTRUCTION_EXECUTOR.into(), coin), Amount(0)); + assert_eq!(Coins::balance(IN_INSTRUCTION_EXECUTOR.into(), Coin::Serai), Amount(0)); + }) +} + +#[test] +fn dex_instruction_swap() { + new_test_ext().execute_with(|| { + let coin = Coin::Bitcoin; + let amount = Amount(2 * 10u64.pow(coin.decimals())); + let account = insecure_pair_from_name("random1").public(); + + // make a pool so that can actually swap + make_liquid_pool(coin, 5 * 10u64.pow(coin.decimals())); + + let mut batch = SignedBatch { + batch: Batch { + network: coin.network(), + id: 0, + block: BlockHash([0u8; 32]), + instructions: vec![InInstructionWithBalance { + instruction: InInstruction::Dex(DexCall::Swap( + Balance { coin: Coin::Serai, amount: Amount(1) }, + OutAddress::External(ExternalAddress::new([0u8; 64].to_vec()).unwrap()), + )), + balance: Balance { coin, amount }, + }], + }, + signature: Signature([0u8; 64]), + }; + + // we can't send SRI to external address + InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); + + // check that the instruction was failed + assert_eq!( + get_events() + .into_iter() + .filter(|event| matches!(event, in_instructions::Event::::InstructionFailure { .. })) + .collect::>(), + vec![in_instructions::Event::::InstructionFailure { + network: batch.batch.network, + id: batch.batch.id, + index: 0 + }] + ); + + // make it internal address + batch.batch.instructions[0].instruction = InInstruction::Dex(DexCall::Swap( + Balance { coin: Coin::Serai, amount: Amount(1) }, + OutAddress::Serai(account.into()), + )); + + // check that swap is successful this time + assert_eq!(Coins::balance(account, Coin::Serai), Amount(0)); + InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); + assert!(Coins::balance(account, Coin::Serai).0 > 0); + + // make another pool for external coin + let coin2 = Coin::Monero; + make_liquid_pool(coin2, 5 * 10u64.pow(coin.decimals())); + + // update the batch + let out_addr = ExternalAddress::new([0u8; 64].to_vec()).unwrap(); + batch.batch.instructions[0].instruction = InInstruction::Dex(DexCall::Swap( + Balance { coin: Coin::Monero, amount: Amount(1) }, + OutAddress::External(out_addr.clone()), + )); + InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); + + // check that we got out instruction + let events = System::events() + .iter() + .filter_map(|event| { + if let RuntimeEvent::Coins(e) = &event.event { + if matches!(e, coins::Event::::BurnWithInstruction { .. }) { + Some(e.clone()) + } else { + None + } + } else { + None + } + }) + .collect::>(); + + assert_eq!( + events, + vec![coins::Event::::BurnWithInstruction { + from: IN_INSTRUCTION_EXECUTOR.into(), + instruction: OutInstructionWithBalance { + instruction: OutInstruction { address: out_addr, data: None }, + balance: Balance { coin: coin2, amount: Amount(68228493) } + } + }] + ) + }) +} + +#[test] +fn genesis_liquidity_instruction() { + new_test_ext().execute_with(|| { + let coin = Coin::Bitcoin; + let amount = Amount(2 * 10u64.pow(coin.decimals())); + let account = insecure_pair_from_name("random1").public(); + + let batch = SignedBatch { + batch: Batch { + network: coin.network(), + id: 0, + block: BlockHash([0u8; 32]), + instructions: vec![InInstructionWithBalance { + instruction: InInstruction::GenesisLiquidity(account.into()), + balance: Balance { coin, amount }, + }], + }, + signature: Signature([0u8; 64]), + }; + + InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); + + // check that genesis liq account got the coins + assert_eq!(Coins::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin), amount); + + // check that it registered the liquidity for the account + // detailed tests about the amounts has to be done in GenesisLiquidity pallet tests. + let liquidity_amount = GenesisLiquidity::liquidity(coin, account).unwrap(); + assert_eq!(liquidity_amount.coins, amount.0); + assert_eq!(liquidity_amount.shares, INITIAL_GENESIS_LP_SHARES); + + let supply = GenesisLiquidity::supply(coin).unwrap(); + assert_eq!(supply.coins, amount.0); + assert_eq!(supply.shares, INITIAL_GENESIS_LP_SHARES); + }) +} + +#[test] +fn swap_to_staked_sri_instruction() { + new_test_ext().execute_with(|| { + let coin = Coin::Monero; + let key_share = ValidatorSets::::allocation_per_key_share(coin.network()).unwrap(); + let amount = Amount(2 * key_share.0); + let account = insecure_pair_from_name("random1").public(); + + // make a pool so that can actually swap + make_liquid_pool(coin, 5 * 10u64.pow(coin.decimals())); + + // make sure account doesn't already have lTs or allocation + let current_liq_tokens = LiquidityTokens::balance(POL_ACCOUNT.into(), coin).0; + assert_eq!(current_liq_tokens, 0); + assert_eq!(ValidatorSets::::allocation((coin.network(), account)), None); + + // we need this so that value for the coin exist + Dex::on_finalize(0); + System::set_block_number(1); // we need this for the spot price + + let batch = SignedBatch { + batch: Batch { + network: coin.network(), + id: 0, + block: BlockHash([0u8; 32]), + instructions: vec![InInstructionWithBalance { + instruction: InInstruction::SwapToStakedSRI(account.into(), coin.network()), + balance: Balance { coin, amount }, + }], + }, + signature: Signature([0u8; 64]), + }; + + InInstructions::execute_batch(RawOrigin::None.into(), batch.clone()).unwrap(); + + // assert that we added liq from POL account + assert!(LiquidityTokens::balance(POL_ACCOUNT.into(), coin).0 > current_liq_tokens); + + // assert that user allocated SRI for the network + let value = Dex::spot_price_for_block(0, coin).unwrap(); + let sri_amount = Amount( + u64::try_from( + u128::from(amount.0) + .checked_mul(u128::from(value.0)) + .unwrap() + .checked_div(u128::from(10u64.pow(coin.decimals()))) + .unwrap(), + ) + .unwrap(), + ); + assert_eq!(ValidatorSets::::allocation((coin.network(), account)).unwrap(), sri_amount); + }) +}